│
// │ │ │ │
// │ │ │ │
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
// │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