/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ 'use strict'; ( function() { var template = '', templateBlock = new CKEDITOR.template( '
' + template + '
{captionPlaceholder}
' + '
' ), alignmentsObj = { left: 0, center: 1, right: 2 }, regexPercent = /^\s*(\d+\%)\s*$/i; CKEDITOR.plugins.add( 'image2', { // jscs:disable maximumLineLength lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,es-mx,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE% // jscs:enable maximumLineLength requires: 'widget,dialog', icons: 'image', hidpi: true, onLoad: function() { CKEDITOR.addCss( '.cke_image_nocaption{' + // This is to remove unwanted space so resize // wrapper is displayed property. 'line-height:0' + '}' + '.cke_editable.cke_image_sw, .cke_editable.cke_image_sw *{cursor:sw-resize !important}' + '.cke_editable.cke_image_se, .cke_editable.cke_image_se *{cursor:se-resize !important}' + '.cke_image_resizer{' + 'display:none;' + 'position:absolute;' + 'width:10px;' + 'height:10px;' + 'bottom:-5px;' + 'right:-5px;' + 'background:#000;' + 'outline:1px solid #fff;' + // Prevent drag handler from being misplaced (https://dev.ckeditor.com/ticket/11207). 'line-height:0;' + 'cursor:se-resize;' + '}' + '.cke_image_resizer_wrapper{' + 'position:relative;' + 'display:inline-block;' + 'line-height:0;' + '}' + // Bottom-left corner style of the resizer. '.cke_image_resizer.cke_image_resizer_left{' + 'right:auto;' + 'left:-5px;' + 'cursor:sw-resize;' + '}' + '.cke_widget_wrapper:hover .cke_image_resizer,' + '.cke_image_resizer.cke_image_resizing{' + 'display:block' + '}' + // Expand widget wrapper when linked inline image. '.cke_widget_wrapper>a{' + 'display:inline-block' + '}' ); }, init: function( editor ) { // Abort when Easyimage is to be loaded since this plugins // share the same functionality (#1791). if ( editor.plugins.detectConflict( 'image2', [ 'easyimage' ] ) ) { return; } // Adapts configuration from original image plugin. Should be removed // when we'll rename image2 to image. var config = editor.config, lang = editor.lang.image2, image = widgetDef( editor ); // Since filebrowser plugin discovers config properties by dialog (plugin?) // names (sic!), this hack will be necessary as long as Image2 is not named // Image. And since Image2 will never be Image, for sure some filebrowser logic // got to be refined. config.filebrowserImage2BrowseUrl = config.filebrowserImageBrowseUrl; config.filebrowserImage2UploadUrl = config.filebrowserImageUploadUrl; // Add custom elementspath names to widget definition. image.pathName = lang.pathName; image.editables.caption.pathName = lang.pathNameCaption; // Register the widget. editor.widgets.add( 'image', image ); // Add toolbar button for this plugin. editor.ui.addButton && editor.ui.addButton( 'Image', { label: editor.lang.common.image, command: 'image', toolbar: 'insert,10' } ); // Register context menu option for editing widget. if ( editor.contextMenu ) { editor.addMenuGroup( 'image', 10 ); editor.addMenuItem( 'image', { label: lang.menu, command: 'image', group: 'image' } ); } CKEDITOR.dialog.add( 'image2', this.path + 'dialogs/image2.js' ); }, afterInit: function( editor ) { // Integrate with align commands (justify plugin). var align = { left: 1, right: 1, center: 1, block: 1 }, integrate = alignCommandIntegrator( editor ); for ( var value in align ) integrate( value ); // Integrate with link commands (link plugin). linkCommandIntegrator( editor ); } } ); // Wiget states (forms) depending on alignment and configuration. // // Non-captioned widget (inline styles) // ┌──────┬───────────────────────────────┬─────────────────────────────┐ // │Align │Internal form │Data │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │none │ │ // │ │ │ │ // │ │ │ │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │left │ │ // │ │ │ │ // │ │ │ │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │center│

│ // │ │

│ // │ │

│ // │ │

│ │ // │ │
│ │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │right │ │ // │ │ │ │ // │ │ │ │ // └──────┴───────────────────────────────┴─────────────────────────────┘ // // Non-captioned widget (config.image2_alignClasses defined) // ┌──────┬───────────────────────────────┬─────────────────────────────┐ // │Align │Internal form │Data │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │none │ │ // │ │ │ │ // │ │ │ │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │left │ │ // │ │ │ │ // │ │ │ │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │center│

│ // │ │

│ // │ │

│ // │ │

│ │ // │ │
│ │ // ├──────┼───────────────────────────────┼─────────────────────────────┤ // │right │ │ // │ │ │ │ // │ │ │ │ // └──────┴───────────────────────────────┴─────────────────────────────┘ // // Captioned widget (inline styles) // ┌──────┬────────────────────────────────────────┬────────────────────────────────────────┐ // │Align │Internal form │Data │ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │none │
│ // │ │
│ │ // │ │ │ │ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │left │
│ // │ │
│ │ // │ │ │ │ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │center│
│ // │ │
│ // │ │ │

│ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │right │
│ // │ │
│ │ // │ │ │ │ // └──────┴────────────────────────────────────────┴────────────────────────────────────────┘ // // Captioned widget (config.image2_alignClasses defined) // ┌──────┬────────────────────────────────────────┬────────────────────────────────────────┐ // │Align │Internal form │Data │ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │none │
│ // │ │
│ │ // │ │ │ │ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │left │
│ // │ │
│ │ // │ │ │ │ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │center│
│ // │ │
│ // │ │ │

│ // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤ // │right │
│ // │ │
│ │ // │ │ │ │ // └──────┴────────────────────────────────────────┴────────────────────────────────────────┘ // // @param {CKEDITOR.editor} // @returns {Object} function widgetDef( editor ) { var alignClasses = editor.config.image2_alignClasses, captionedClass = editor.config.image2_captionedClass; function deflate() { if ( this.deflated ) return; // Remember whether widget was focused before destroyed. if ( editor.widgets.focused == this.widget ) this.focused = true; editor.widgets.destroy( this.widget ); // Mark widget was destroyed. this.deflated = true; } function inflate() { var editable = editor.editable(), doc = editor.document; // Create a new widget. This widget will be either captioned // non-captioned, block or inline according to what is the // new state of the widget. if ( this.deflated ) { this.widget = editor.widgets.initOn( this.element, 'image', this.widget.data ); // Once widget was re-created, it may become an inline element without // block wrapper (i.e. when unaligned, end not captioned). Let's do some // sort of autoparagraphing here (https://dev.ckeditor.com/ticket/10853). if ( this.widget.inline && !( new CKEDITOR.dom.elementPath( this.widget.wrapper, editable ).block ) ) { var block = doc.createElement( editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); block.replace( this.widget.wrapper ); this.widget.wrapper.move( block ); } // The focus must be transferred from the old one (destroyed) // to the new one (just created). if ( this.focused ) { this.widget.focus(); delete this.focused; } delete this.deflated; } // If now widget was destroyed just update wrapper's alignment. // According to the new state. else { setWrapperAlign( this.widget, alignClasses ); } } return { allowedContent: getWidgetAllowedContent( editor ), requiredContent: 'img[src,alt]', features: getWidgetFeatures( editor ), styleableElements: 'img figure', // This widget converts style-driven dimensions to attributes. contentTransformations: [ [ 'img[width]: sizeToAttribute' ] ], // This widget has an editable caption. editables: { caption: { selector: 'figcaption', allowedContent: 'br em strong sub sup u s; a[!href,target]' } }, parts: { image: 'img', caption: 'figcaption' // parts#link defined in widget#init }, // The name of this widget's dialog. dialog: 'image2', // Template of the widget: plain image. template: template, data: function() { var features = this.features; // Image can't be captioned when figcaption is disallowed (https://dev.ckeditor.com/ticket/11004). if ( this.data.hasCaption && !editor.filter.checkFeature( features.caption ) ) this.data.hasCaption = false; // Image can't be aligned when floating is disallowed (https://dev.ckeditor.com/ticket/11004). if ( this.data.align != 'none' && !editor.filter.checkFeature( features.align ) ) this.data.align = 'none'; // Convert the internal form of the widget from the old state to the new one. this.shiftState( { widget: this, element: this.element, oldData: this.oldData, newData: this.data, deflate: deflate, inflate: inflate } ); // Update widget.parts.link since it will not auto-update unless widget // is destroyed and re-inited. if ( !this.data.link ) { if ( this.parts.link ) delete this.parts.link; } else { if ( !this.parts.link ) this.parts.link = this.parts.image.getParent(); } this.parts.image.setAttributes( { src: this.data.src, // This internal is required by the editor. 'data-cke-saved-src': this.data.src, alt: this.data.alt } ); // If shifting non-captioned -> captioned, remove classes // related to styles from . if ( this.oldData && !this.oldData.hasCaption && this.data.hasCaption ) { for ( var c in this.data.classes ) this.parts.image.removeClass( c ); } // Set dimensions of the image according to gathered data. // Do it only when the attributes are allowed (https://dev.ckeditor.com/ticket/11004). if ( editor.filter.checkFeature( features.dimension ) ) setDimensions( this ); // Cache current data. this.oldData = CKEDITOR.tools.extend( {}, this.data ); }, init: function() { var helpers = CKEDITOR.plugins.image2, image = this.parts.image, data = { hasCaption: !!this.parts.caption, src: image.getAttribute( 'src' ), alt: image.getAttribute( 'alt' ) || '', width: image.getAttribute( 'width' ) || '', height: image.getAttribute( 'height' ) || '', // Lock ratio is on by default (https://dev.ckeditor.com/ticket/10833). lock: this.ready ? helpers.checkHasNaturalRatio( image ) : true }; // If we used 'a' in widget#parts definition, it could happen that // selected element is a child of widget.parts#caption. Since there's no clever // way to solve it with CSS selectors, it's done like that. (https://dev.ckeditor.com/ticket/11783). var link = image.getAscendant( 'a' ); if ( link && this.wrapper.contains( link ) ) this.parts.link = link; // Depending on configuration, read style/class from element and // then remove it. Removed style/class will be set on wrapper in #data listener. // Note: Center alignment is detected during upcast, so only left/right cases // are checked below. if ( !data.align ) { var alignElement = data.hasCaption ? this.element : image; // Read the initial left/right alignment from the class set on element. if ( alignClasses ) { if ( alignElement.hasClass( alignClasses[ 0 ] ) ) { data.align = 'left'; } else if ( alignElement.hasClass( alignClasses[ 2 ] ) ) { data.align = 'right'; } if ( data.align ) { alignElement.removeClass( alignClasses[ alignmentsObj[ data.align ] ] ); } else { data.align = 'none'; } } // Read initial float style from figure/image and then remove it. else { data.align = alignElement.getStyle( 'float' ) || 'none'; alignElement.removeStyle( 'float' ); } } // Update data.link object with attributes if the link has been discovered. if ( editor.plugins.link && this.parts.link ) { data.link = helpers.getLinkAttributesParser()( editor, this.parts.link ); // Get rid of cke_widget_* classes in data. Otherwise // they might appear in link dialog. var advanced = data.link.advanced; if ( advanced && advanced.advCSSClasses ) { advanced.advCSSClasses = CKEDITOR.tools.trim( advanced.advCSSClasses.replace( /cke_\S+/, '' ) ); } } // Get rid of extra vertical space when there's no caption. // It will improve the look of the resizer. this.wrapper[ ( data.hasCaption ? 'remove' : 'add' ) + 'Class' ]( 'cke_image_nocaption' ); this.setData( data ); // Setup dynamic image resizing with mouse. // Don't initialize resizer when dimensions are disallowed (https://dev.ckeditor.com/ticket/11004). // Don't initialize resizer when editor.readOnly is set to true (#719). if ( editor.filter.checkFeature( this.features.dimension ) && editor.config.image2_disableResizer !== true && editor.readOnly != true ) { setupResizer( this ); } this.shiftState = helpers.stateShifter( this.editor ); // Add widget editing option to its context menu. this.on( 'contextMenu', function( evt ) { evt.data.image = CKEDITOR.TRISTATE_OFF; // Integrate context menu items for link. // Note that widget may be wrapped in a link, which // does not belong to that widget (https://dev.ckeditor.com/ticket/11814). if ( this.parts.link || this.wrapper.getAscendant( 'a' ) ) evt.data.link = evt.data.unlink = CKEDITOR.TRISTATE_OFF; } ); // Pass the reference to this widget to the dialog. this.on( 'dialog', function( evt ) { evt.data.widget = this; }, this ); }, // Overrides default method to handle internal mutability of Image2. // @see CKEDITOR.plugins.widget#addClass addClass: function( className ) { getStyleableElement( this ).addClass( className ); }, // Overrides default method to handle internal mutability of Image2. // @see CKEDITOR.plugins.widget#hasClass hasClass: function( className ) { return getStyleableElement( this ).hasClass( className ); }, // Overrides default method to handle internal mutability of Image2. // @see CKEDITOR.plugins.widget#removeClass removeClass: function( className ) { getStyleableElement( this ).removeClass( className ); }, // Overrides default method to handle internal mutability of Image2. // @see CKEDITOR.plugins.widget#getClasses getClasses: ( function() { var classRegex = new RegExp( '^(' + [].concat( captionedClass, alignClasses ).join( '|' ) + ')$' ); return function() { var classes = this.repository.parseElementClasses( getStyleableElement( this ).getAttribute( 'class' ) ); // Neither config.image2_captionedClass nor config.image2_alignClasses // do not belong to style classes. for ( var c in classes ) { if ( classRegex.test( c ) ) delete classes[ c ]; } return classes; }; } )(), upcast: upcastWidgetElement( editor ), downcast: downcastWidgetElement( editor ), getLabel: function() { var label = ( this.data.alt || '' ) + ' ' + this.pathName; return this.editor.lang.widget.label.replace( /%1/, label ); } }; } /** * A set of Enhanced Image (image2) plugin helpers. * * @class * @singleton */ CKEDITOR.plugins.image2 = { stateShifter: function( editor ) { // Tag name used for centering non-captioned widgets. var doc = editor.document, alignClasses = editor.config.image2_alignClasses, captionedClass = editor.config.image2_captionedClass, editable = editor.editable(), // The order that stateActions get executed. It matters! shiftables = [ 'hasCaption', 'align', 'link' ]; // Atomic procedures, one per state variable. var stateActions = { align: function( shift, oldValue, newValue ) { var el = shift.element; // Alignment changed. if ( shift.changed.align ) { // No caption in the new state. if ( !shift.newData.hasCaption ) { // Changed to "center" (non-captioned). if ( newValue == 'center' ) { shift.deflate(); shift.element = wrapInCentering( editor, el ); } // Changed to "non-center" from "center" while caption removed. if ( !shift.changed.hasCaption && oldValue == 'center' && newValue != 'center' ) { shift.deflate(); shift.element = unwrapFromCentering( el ); } } } // Alignment remains and "center" removed caption. else if ( newValue == 'center' && shift.changed.hasCaption && !shift.newData.hasCaption ) { shift.deflate(); shift.element = wrapInCentering( editor, el ); } // Finally set display for figure. if ( !alignClasses && el.is( 'figure' ) ) { if ( newValue == 'center' ) el.setStyle( 'display', 'inline-block' ); else el.removeStyle( 'display' ); } }, hasCaption: function( shift, oldValue, newValue ) { // This action is for real state change only. if ( !shift.changed.hasCaption ) return; // Get or from widget. Note that widget element might itself // be what we're looking for. Also element can be

...

. var imageOrLink; if ( shift.element.is( { img: 1, a: 1 } ) ) imageOrLink = shift.element; else imageOrLink = shift.element.findOne( 'a,img' ); // Switching hasCaption always destroys the widget. shift.deflate(); // There was no caption, but the caption is to be added. if ( newValue ) { // Create new
from widget template. var figure = CKEDITOR.dom.element.createFromHtml( templateBlock.output( { captionedClass: captionedClass, captionPlaceholder: editor.lang.image2.captionPlaceholder } ), doc ); // Replace element with
. replaceSafely( figure, shift.element ); // Use old or instead of the one from the template, // so we won't lose additional attributes. imageOrLink.replace( figure.findOne( 'img' ) ); // Update widget's element. shift.element = figure; } // The caption was present, but now it's to be removed. else { // Unwrap or from figure. imageOrLink.replace( shift.element ); // Update widget's element. shift.element = imageOrLink; } }, link: function( shift, oldValue, newValue ) { if ( shift.changed.link ) { var img = shift.element.is( 'img' ) ? shift.element : shift.element.findOne( 'img' ), link = shift.element.is( 'a' ) ? shift.element : shift.element.findOne( 'a' ), // Why deflate: // If element is , it will be wrapped into , // which becomes a new widget.element. // If element is , it will be unlinked // so becomes a new widget.element. needsDeflate = ( shift.element.is( 'a' ) && !newValue ) || ( shift.element.is( 'img' ) && newValue ), newEl; if ( needsDeflate ) shift.deflate(); // If unlinked the image, returned element is . if ( !newValue ) newEl = unwrapFromLink( link ); else { // If linked the image, returned element is . if ( !oldValue ) newEl = wrapInLink( img, shift.newData.link ); // Set and remove all attributes associated with this state. var attributes = CKEDITOR.plugins.image2.getLinkAttributesGetter()( editor, newValue ); if ( !CKEDITOR.tools.isEmpty( attributes.set ) ) ( newEl || link ).setAttributes( attributes.set ); if ( attributes.removed.length ) ( newEl || link ).removeAttributes( attributes.removed ); } if ( needsDeflate ) shift.element = newEl; } } }; function wrapInCentering( editor, element ) { var attribsAndStyles = {}; if ( alignClasses ) attribsAndStyles.attributes = { 'class': alignClasses[ 1 ] }; else attribsAndStyles.styles = { 'text-align': 'center' }; // There's no gentle way to center inline element with CSS, so create p/div // that wraps widget contents and does the trick either with style or class. var center = doc.createElement( editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div', attribsAndStyles ); // Replace element with centering wrapper. replaceSafely( center, element ); element.move( center ); return center; } function unwrapFromCentering( element ) { var imageOrLink = element.findOne( 'a,img' ); imageOrLink.replace( element ); return imageOrLink; } // Wraps -> . // Returns reference to . // // @param {CKEDITOR.dom.element} img // @param {Object} linkData // @returns {CKEDITOR.dom.element} function wrapInLink( img, linkData ) { var link = doc.createElement( 'a', { attributes: { href: linkData.url } } ); link.replace( img ); img.move( link ); return link; } // De-wraps -> . // Returns the reference to // // @param {CKEDITOR.dom.element} link // @returns {CKEDITOR.dom.element} function unwrapFromLink( link ) { var img = link.findOne( 'img' ); img.replace( link ); return img; } function replaceSafely( replacing, replaced ) { if ( replaced.getParent() ) { var range = editor.createRange(); range.moveToPosition( replaced, CKEDITOR.POSITION_BEFORE_START ); // Remove old element. Do it before insertion to avoid a case when // element is moved from 'replaced' element before it, what creates // a tricky case which insertElementIntorRange does not handle. replaced.remove(); editable.insertElementIntoRange( replacing, range ); } else { replacing.replace( replaced ); } } return function( shift ) { var name, i; shift.changed = {}; for ( i = 0; i < shiftables.length; i++ ) { name = shiftables[ i ]; shift.changed[ name ] = shift.oldData ? shift.oldData[ name ] !== shift.newData[ name ] : false; } // Iterate over possible state variables. for ( i = 0; i < shiftables.length; i++ ) { name = shiftables[ i ]; stateActions[ name ]( shift, shift.oldData ? shift.oldData[ name ] : null, shift.newData[ name ] ); } shift.inflate(); }; }, /** * Checks whether the current image ratio matches the natural one * by comparing dimensions. * * @param {CKEDITOR.dom.element} image * @returns {Boolean} */ checkHasNaturalRatio: function( image ) { var $ = image.$, natural = this.getNatural( image ); // The reason for two alternative comparisons is that the rounding can come from // both dimensions, e.g. there are two cases: // 1. height is computed as a rounded relation of the real height and the value of width, // 2. width is computed as a rounded relation of the real width and the value of heigh. return Math.round( $.clientWidth / natural.width * natural.height ) == $.clientHeight || Math.round( $.clientHeight / natural.height * natural.width ) == $.clientWidth; }, /** * Returns natural dimensions of the image. For modern browsers * it uses natural(Width|Height). For old ones (IE8) it creates * a new image and reads the dimensions. * * @param {CKEDITOR.dom.element} image * @returns {Object} */ getNatural: function( image ) { var dimensions; if ( image.$.naturalWidth ) { dimensions = { width: image.$.naturalWidth, height: image.$.naturalHeight }; } else { var img = new Image(); img.src = image.getAttribute( 'src' ); dimensions = { width: img.width, height: img.height }; } return dimensions; }, /** * Returns an attribute getter function. Default getter comes from the Link plugin * and is documented by {@link CKEDITOR.plugins.link#getLinkAttributes}. * * **Note:** It is possible to override this method and use a custom getter e.g. * in the absence of the Link plugin. * * **Note:** If a custom getter is used, a data model format it produces * must be compatible with {@link CKEDITOR.plugins.link#getLinkAttributes}. * * **Note:** A custom getter must understand the data model format produced by * {@link #getLinkAttributesParser} to work correctly. * * @returns {Function} A function that gets (composes) link attributes. * @since 4.5.5 */ getLinkAttributesGetter: function() { // https://dev.ckeditor.com/ticket/13885 return CKEDITOR.plugins.link.getLinkAttributes; }, /** * Returns an attribute parser function. Default parser comes from the Link plugin * and is documented by {@link CKEDITOR.plugins.link#parseLinkAttributes}. * * **Note:** It is possible to override this method and use a custom parser e.g. * in the absence of the Link plugin. * * **Note:** If a custom parser is used, a data model format produced by the parser * must be compatible with {@link #getLinkAttributesGetter}. * * **Note:** If a custom parser is used, it should be compatible with the * {@link CKEDITOR.plugins.link#parseLinkAttributes} data model format. Otherwise the * Link plugin dialog may not be populated correctly with parsed data. However * as long as Enhanced Image is **not** used with the Link plugin dialog, any custom data model * will work, being stored as an internal property of Enhanced Image widget's data only. * * @returns {Function} A function that parses attributes. * @since 4.5.5 */ getLinkAttributesParser: function() { // https://dev.ckeditor.com/ticket/13885 return CKEDITOR.plugins.link.parseLinkAttributes; } }; function setWrapperAlign( widget, alignClasses ) { var wrapper = widget.wrapper, align = widget.data.align, hasCaption = widget.data.hasCaption; if ( alignClasses ) { // Remove all align classes first. for ( var i = 3; i--; ) wrapper.removeClass( alignClasses[ i ] ); if ( align == 'center' ) { // Avoid touching non-captioned, centered widgets because // they have the class set on the element instead of wrapper: // //
//

// //

//
if ( hasCaption ) { wrapper.addClass( alignClasses[ 1 ] ); } } else if ( align != 'none' ) { wrapper.addClass( alignClasses[ alignmentsObj[ align ] ] ); } } else { if ( align == 'center' ) { if ( hasCaption ) wrapper.setStyle( 'text-align', 'center' ); else wrapper.removeStyle( 'text-align' ); wrapper.removeStyle( 'float' ); } else { if ( align == 'none' ) wrapper.removeStyle( 'float' ); else wrapper.setStyle( 'float', align ); wrapper.removeStyle( 'text-align' ); } } } // Returns a function that creates widgets from all and //
elements. // // @param {CKEDITOR.editor} editor // @returns {Function} function upcastWidgetElement( editor ) { var isCenterWrapper = centerWrapperChecker( editor ), captionedClass = editor.config.image2_captionedClass; // @param {CKEDITOR.htmlParser.element} el // @param {Object} data return function( el, data ) { var dimensions = { width: 1, height: 1 }, name = el.name, image; // https://dev.ckeditor.com/ticket/11110 Don't initialize on pasted fake objects. if ( el.attributes[ 'data-cke-realelement' ] ) return; // If a center wrapper is found, there are 3 possible cases: // // 1.
...
. // In this case centering is done with a class set on widget.wrapper. // Simply replace centering wrapper with figure (it's no longer necessary). // // 2.

. // Nothing to do here:

remains for styling purposes. // // 3.

. // Nothing to do here (2.) but that case is only possible in enterMode different // than ENTER_P. if ( isCenterWrapper( el ) ) { if ( name == 'div' ) { var figure = el.getFirst( 'figure' ); // Case #1. if ( figure ) { el.replaceWith( figure ); el = figure; } } // Cases #2 and #3 (handled transparently) // If there's a centering wrapper, save it in data. data.align = 'center'; // Image can be wrapped in link . image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' ); } // No center wrapper has been found. else if ( name == 'figure' && el.hasClass( captionedClass ) ) { image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' ); // Upcast linked image like . } else if ( isLinkedOrStandaloneImage( el ) ) { image = el.name == 'a' ? el.children[ 0 ] : el; } if ( !image ) return; // If there's an image, then cool, we got a widget. // Now just remove dimension attributes expressed with %. for ( var d in dimensions ) { var dimension = image.attributes[ d ]; if ( dimension && dimension.match( regexPercent ) ) delete image.attributes[ d ]; } return el; }; } // Returns a function which transforms the widget to the external format // according to the current configuration. // // @param {CKEDITOR.editor} function downcastWidgetElement( editor ) { var alignClasses = editor.config.image2_alignClasses; // @param {CKEDITOR.htmlParser.element} el return function( el ) { // In case of , is the element to hold // inline styles or classes (image2_alignClasses). var attrsHolder = el.name == 'a' ? el.getFirst() : el, attrs = attrsHolder.attributes, align = this.data.align; // De-wrap the image from resize handle wrapper. // Only block widgets have one. if ( !this.inline ) { var resizeWrapper = el.getFirst( 'span' ); if ( resizeWrapper ) resizeWrapper.replaceWith( resizeWrapper.getFirst( { img: 1, a: 1 } ) ); } if ( align && align != 'none' ) { var styles = CKEDITOR.tools.parseCssText( attrs.style || '' ); // When the widget is captioned (
) and internally centering is done // with widget's wrapper style/class, in the external data representation, //
must be wrapped with an element holding an style/class: // //
//
...
//
// or //
//
...
//
// if ( align == 'center' && el.name == 'figure' ) { el = el.wrapWith( new CKEDITOR.htmlParser.element( 'div', alignClasses ? { 'class': alignClasses[ 1 ] } : { style: 'text-align:center' } ) ); } // If left/right, add float style to the downcasted element. else if ( align in { left: 1, right: 1 } ) { if ( alignClasses ) attrsHolder.addClass( alignClasses[ alignmentsObj[ align ] ] ); else styles[ 'float' ] = align; } // Update element styles. if ( !alignClasses && !CKEDITOR.tools.isEmpty( styles ) ) attrs.style = CKEDITOR.tools.writeCssText( styles ); } return el; }; } // Returns a function that checks if an element is a centering wrapper. // // @param {CKEDITOR.editor} editor // @returns {Function} function centerWrapperChecker( editor ) { var captionedClass = editor.config.image2_captionedClass, alignClasses = editor.config.image2_alignClasses, validChildren = { figure: 1, a: 1, img: 1 }; return function( el ) { // Wrapper must be either
or

. if ( !( el.name in { div: 1, p: 1 } ) ) return false; var children = el.children; // Centering wrapper can have only one child. if ( children.length !== 1 ) return false; var child = children[ 0 ]; // Only

or can be first (only) child of centering wrapper, // regardless of its type. if ( !( child.name in validChildren ) ) return false; // If centering wrapper is

, only can be the child. //

if ( el.name == 'p' ) { if ( !isLinkedOrStandaloneImage( child ) ) return false; } // Centering
can hold or
, depending on enterMode. else { // If a
is the first (only) child, it must have a class. //
...
if ( child.name == 'figure' ) { if ( !child.hasClass( captionedClass ) ) return false; } else { // Centering
can hold or only when enterMode // is ENTER_(BR|DIV). //
//
if ( editor.enterMode == CKEDITOR.ENTER_P ) return false; // Regardless of enterMode, a child which is not
must be // either or . if ( !isLinkedOrStandaloneImage( child ) ) return false; } } // Centering wrapper got to be... centering. If image2_alignClasses are defined, // check for centering class. Otherwise, check the style. if ( alignClasses ? el.hasClass( alignClasses[ 1 ] ) : CKEDITOR.tools.parseCssText( el.attributes.style || '', true )[ 'text-align' ] == 'center' ) return true; return false; }; } // Checks whether element is or . // // @param {CKEDITOR.htmlParser.element} function isLinkedOrStandaloneImage( el ) { if ( el.name == 'img' ) return true; else if ( el.name == 'a' ) return el.children.length == 1 && el.getFirst( 'img' ); return false; } // Sets width and height of the widget image according to current widget data. // // @param {CKEDITOR.plugins.widget} widget function setDimensions( widget ) { var data = widget.data, dimensions = { width: data.width, height: data.height }, image = widget.parts.image; for ( var d in dimensions ) { if ( dimensions[ d ] ) image.setAttribute( d, dimensions[ d ] ); else image.removeAttribute( d ); } } // Defines all features related to drag-driven image resizing. // // @param {CKEDITOR.plugins.widget} widget function setupResizer( widget ) { var editor = widget.editor, editable = editor.editable(), doc = editor.document, // Store the resizer in a widget for testing (https://dev.ckeditor.com/ticket/11004). resizer = widget.resizer = doc.createElement( 'span' ); resizer.addClass( 'cke_image_resizer' ); resizer.setAttribute( 'title', editor.lang.image2.resizer ); resizer.append( new CKEDITOR.dom.text( '\u200b', doc ) ); // Inline widgets don't need a resizer wrapper as an image spans the entire widget. if ( !widget.inline ) { var imageOrLink = widget.parts.link || widget.parts.image, oldResizeWrapper = imageOrLink.getParent(), resizeWrapper = doc.createElement( 'span' ); resizeWrapper.addClass( 'cke_image_resizer_wrapper' ); resizeWrapper.append( imageOrLink ); resizeWrapper.append( resizer ); widget.element.append( resizeWrapper, true ); // Remove the old wrapper which could came from e.g. pasted HTML // and which could be corrupted (e.g. resizer span has been lost). if ( oldResizeWrapper.is( 'span' ) ) oldResizeWrapper.remove(); } else { widget.wrapper.append( resizer ); } // Calculate values of size variables and mouse offsets. resizer.on( 'mousedown', function( evt ) { var image = widget.parts.image, // "factor" can be either 1 or -1. I.e.: For right-aligned images, we need to // subtract the difference to get proper width, etc. Without "factor", // resizer starts working the opposite way. factor = widget.data.align == 'right' ? -1 : 1, // The x-coordinate of the mouse relative to the screen // when button gets pressed. startX = evt.data.$.screenX, startY = evt.data.$.screenY, // The initial dimensions and aspect ratio of the image. startWidth = image.$.clientWidth, startHeight = image.$.clientHeight, ratio = startWidth / startHeight, listeners = [], // A class applied to editable during resizing. cursorClass = 'cke_image_s' + ( !~factor ? 'w' : 'e' ), nativeEvt, newWidth, newHeight, updateData, moveDiffX, moveDiffY, moveRatio; // Save the undo snapshot first: before resizing. editor.fire( 'saveSnapshot' ); // Mousemove listeners are removed on mouseup. attachToDocuments( 'mousemove', onMouseMove, listeners ); // Clean up the mousemove listener. Update widget data if valid. attachToDocuments( 'mouseup', onMouseUp, listeners ); // The entire editable will have the special cursor while resizing goes on. editable.addClass( cursorClass ); // This is to always keep the resizer element visible while resizing. resizer.addClass( 'cke_image_resizing' ); // Attaches an event to a global document if inline editor. // Additionally, if classic (`iframe`-based) editor, also attaches the same event to `iframe`'s document. function attachToDocuments( name, callback, collection ) { var globalDoc = CKEDITOR.document, listeners = []; if ( !doc.equals( globalDoc ) ) listeners.push( globalDoc.on( name, callback ) ); listeners.push( doc.on( name, callback ) ); if ( collection ) { for ( var i = listeners.length; i--; ) collection.push( listeners.pop() ); } } // Calculate with first, and then adjust height, preserving ratio. function adjustToX() { newWidth = startWidth + factor * moveDiffX; newHeight = Math.round( newWidth / ratio ); } // Calculate height first, and then adjust width, preserving ratio. function adjustToY() { newHeight = startHeight - moveDiffY; newWidth = Math.round( newHeight * ratio ); } // This is how variables refer to the geometry. // Note: x corresponds to moveOffset, this is the position of mouse // Note: o corresponds to [startX, startY]. // // +--------------+--------------+ // | | | // | I | II | // | | | // +------------- o -------------+ _ _ _ // | | | ^ // | VI | III | | moveDiffY // | | x _ _ _ _ _ v // +--------------+---------|----+ // | | // <-------> // moveDiffX function onMouseMove( evt ) { nativeEvt = evt.data.$; // This is how far the mouse is from the point the button was pressed. moveDiffX = nativeEvt.screenX - startX; moveDiffY = startY - nativeEvt.screenY; // This is the aspect ratio of the move difference. moveRatio = Math.abs( moveDiffX / moveDiffY ); // Left, center or none-aligned widget. if ( factor == 1 ) { if ( moveDiffX <= 0 ) { // Case: IV. if ( moveDiffY <= 0 ) adjustToX(); // Case: I. else { if ( moveRatio >= ratio ) adjustToX(); else adjustToY(); } } else { // Case: III. if ( moveDiffY <= 0 ) { if ( moveRatio >= ratio ) adjustToY(); else adjustToX(); } // Case: II. else { adjustToY(); } } } // Right-aligned widget. It mirrors behaviours, so I becomes II, // IV becomes III and vice-versa. else { if ( moveDiffX <= 0 ) { // Case: IV. if ( moveDiffY <= 0 ) { if ( moveRatio >= ratio ) adjustToY(); else adjustToX(); } // Case: I. else { adjustToY(); } } else { // Case: III. if ( moveDiffY <= 0 ) adjustToX(); // Case: II. else { if ( moveRatio >= ratio ) { adjustToX(); } else { adjustToY(); } } } } // Don't update attributes if less than 10. // This is to prevent images to visually disappear. if ( newWidth >= 15 && newHeight >= 15 ) { image.setAttributes( { width: newWidth, height: newHeight } ); updateData = true; } else { updateData = false; } } function onMouseUp() { var l; while ( ( l = listeners.pop() ) ) l.removeListener(); // Restore default cursor by removing special class. editable.removeClass( cursorClass ); // This is to bring back the regular behaviour of the resizer. resizer.removeClass( 'cke_image_resizing' ); if ( updateData ) { widget.setData( { width: newWidth, height: newHeight } ); // Save another undo snapshot: after resizing. editor.fire( 'saveSnapshot' ); } // Don't update data twice or more. updateData = false; } } ); // Change the position of the widget resizer when data changes. widget.on( 'data', function() { resizer[ widget.data.align == 'right' ? 'addClass' : 'removeClass' ]( 'cke_image_resizer_left' ); } ); } // Integrates widget alignment setting with justify // plugin's commands (execution and refreshment). // @param {CKEDITOR.editor} editor // @param {String} value 'left', 'right', 'center' or 'block' function alignCommandIntegrator( editor ) { var execCallbacks = [], enabled; return function( value ) { var command = editor.getCommand( 'justify' + value ); // Most likely, the justify plugin isn't loaded. if ( !command ) return; // This command will be manually refreshed along with // other commands after exec. execCallbacks.push( function() { command.refresh( editor, editor.elementPath() ); } ); if ( value in { right: 1, left: 1, center: 1 } ) { command.on( 'exec', function( evt ) { var widget = getFocusedWidget( editor ); if ( widget ) { widget.setData( 'align', value ); // Once the widget changed its align, all the align commands // must be refreshed: the event is to be cancelled. for ( var i = execCallbacks.length; i--; ) execCallbacks[ i ](); evt.cancel(); } } ); } command.on( 'refresh', function( evt ) { var widget = getFocusedWidget( editor ), allowed = { right: 1, left: 1, center: 1 }; if ( !widget ) return; // Cache "enabled" on first use. This is because filter#checkFeature may // not be available during plugin's afterInit in the future — a moment when // alignCommandIntegrator is called. if ( enabled === undefined ) enabled = editor.filter.checkFeature( editor.widgets.registered.image.features.align ); // Don't allow justify commands when widget alignment is disabled (https://dev.ckeditor.com/ticket/11004). if ( !enabled ) this.setState( CKEDITOR.TRISTATE_DISABLED ); else { this.setState( ( widget.data.align == value ) ? ( CKEDITOR.TRISTATE_ON ) : ( ( value in allowed ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ) ); } evt.cancel(); } ); }; } function linkCommandIntegrator( editor ) { // Nothing to integrate with if link is not loaded. if ( !editor.plugins.link ) return; CKEDITOR.on( 'dialogDefinition', function( evt ) { var dialog = evt.data; if ( dialog.name == 'link' ) { var def = dialog.definition; var onShow = def.onShow, onOk = def.onOk; def.onShow = function() { var widget = getFocusedWidget( editor ), displayTextField = this.getContentElement( 'info', 'linkDisplayText' ).getElement().getParent().getParent(); // Widget cannot be enclosed in a link, i.e. // foobar if ( widget && ( widget.inline ? !widget.wrapper.getAscendant( 'a' ) : 1 ) ) { this.setupContent( widget.data.link || {} ); // Hide the display text in case of linking image2 widget. displayTextField.hide(); } else { // Make sure that display text is visible, as it might be hidden by image2 integration // before. displayTextField.show(); onShow.apply( this, arguments ); } }; // Set widget data if linking the widget using // link dialog (instead of default action). // State shifter handles data change and takes // care of internal DOM structure of linked widget. def.onOk = function() { var widget = getFocusedWidget( editor ); // Widget cannot be enclosed in a link, i.e. // foobar if ( widget && ( widget.inline ? !widget.wrapper.getAscendant( 'a' ) : 1 ) ) { var data = {}; // Collect data from fields. this.commitContent( data ); // Set collected data to widget. widget.setData( 'link', data ); } else { onOk.apply( this, arguments ); } }; } } ); // Overwrite default behaviour of unlink command. editor.getCommand( 'unlink' ).on( 'exec', function( evt ) { var widget = getFocusedWidget( editor ); // Override unlink only when link truly belongs to the widget. // If wrapped inline widget in a link, let default unlink work (https://dev.ckeditor.com/ticket/11814). if ( !widget || !widget.parts.link ) return; widget.setData( 'link', null ); // Selection (which is fake) may not change if unlinked image in focused widget, // i.e. if captioned image. Let's refresh command state manually here. this.refresh( editor, editor.elementPath() ); evt.cancel(); } ); // Overwrite default refresh of unlink command. editor.getCommand( 'unlink' ).on( 'refresh', function( evt ) { var widget = getFocusedWidget( editor ); if ( !widget ) return; // Note that widget may be wrapped in a link, which // does not belong to that widget (https://dev.ckeditor.com/ticket/11814). this.setState( widget.data.link || widget.wrapper.getAscendant( 'a' ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ); evt.cancel(); } ); } // Returns the focused widget, if of the type specific for this plugin. // If no widget is focused, `null` is returned. // // @param {CKEDITOR.editor} // @returns {CKEDITOR.plugins.widget} function getFocusedWidget( editor ) { var widget = editor.widgets.focused; if ( widget && widget.name == 'image' ) return widget; return null; } // Returns a set of widget allowedContent rules, depending // on configurations like config#image2_alignClasses or // config#image2_captionedClass. // // @param {CKEDITOR.editor} // @returns {Object} function getWidgetAllowedContent( editor ) { var alignClasses = editor.config.image2_alignClasses, rules = { // Widget may need
or

centering wrapper. div: { match: centerWrapperChecker( editor ) }, p: { match: centerWrapperChecker( editor ) }, img: { attributes: '!src,alt,width,height' }, figure: { classes: '!' + editor.config.image2_captionedClass }, figcaption: true }; if ( alignClasses ) { // Centering class from the config. rules.div.classes = alignClasses[ 1 ]; rules.p.classes = rules.div.classes; // Left/right classes from the config. rules.img.classes = alignClasses[ 0 ] + ',' + alignClasses[ 2 ]; rules.figure.classes += ',' + rules.img.classes; } else { // Centering with text-align. rules.div.styles = 'text-align'; rules.p.styles = 'text-align'; rules.img.styles = 'float'; rules.figure.styles = 'float,display'; } return rules; } // Returns a set of widget feature rules, depending // on editor configuration. Note that the following may not cover // all the possible cases since requiredContent supports a single // tag only. // // @param {CKEDITOR.editor} // @returns {Object} function getWidgetFeatures( editor ) { var alignClasses = editor.config.image2_alignClasses, features = { dimension: { requiredContent: 'img[width,height]' }, align: { requiredContent: 'img' + ( alignClasses ? '(' + alignClasses[ 0 ] + ')' : '{float}' ) }, caption: { requiredContent: 'figcaption' } }; return features; } // Returns element which is styled, considering current // state of the widget. // // @see CKEDITOR.plugins.widget#applyStyle // @param {CKEDITOR.plugins.widget} widget // @returns {CKEDITOR.dom.element} function getStyleableElement( widget ) { return widget.data.hasCaption ? widget.element : widget.parts.image; } } )(); /** * A CSS class applied to the `

` element of a captioned image. * * Read more in the [documentation](#!/guide/dev_image2) and see the * [SDK sample](https://sdk.ckeditor.com/samples/image2.html). * * // Changes the class to "captionedImage". * config.image2_captionedClass = 'captionedImage'; * * @cfg {String} [image2_captionedClass='image'] * @member CKEDITOR.config */ CKEDITOR.config.image2_captionedClass = 'image'; /** * Determines whether dimension inputs should be automatically filled when the image URL changes in the Enhanced Image * plugin dialog window. * * Read more in the [documentation](#!/guide/dev_image2) and see the * [SDK sample](https://sdk.ckeditor.com/samples/image2.html). * * config.image2_prefillDimensions = false; * * @since 4.5 * @cfg {Boolean} [image2_prefillDimensions=true] * @member CKEDITOR.config */ /** * Disables the image resizer. By default the resizer is enabled. * * Read more in the [documentation](#!/guide/dev_image2) and see the * [SDK sample](https://sdk.ckeditor.com/samples/image2.html). * * config.image2_disableResizer = true; * * @since 4.5 * @cfg {Boolean} [image2_disableResizer=false] * @member CKEDITOR.config */ /** * CSS classes applied to aligned images. Useful to take control over the way * the images are aligned, i.e. to customize output HTML and integrate external stylesheets. * * Classes should be defined in an array of three elements, containing left, center, and right * alignment classes, respectively. For example: * * config.image2_alignClasses = [ 'align-left', 'align-center', 'align-right' ]; * * **Note**: Once this configuration option is set, the plugin will no longer produce inline * styles for alignment. It means that e.g. the following HTML will be produced: * * My image * * instead of: * * My image * * **Note**: Once this configuration option is set, corresponding style definitions * must be supplied to the editor: * * * For {@glink guide/dev_framed classic editor} it can be done by defining additional * styles in the {@link CKEDITOR.config#contentsCss stylesheets loaded by the editor}. The same * styles must be provided on the target page where the content will be loaded. * * For {@glink guide/dev_inline inline editor} the styles can be defined directly * with `