You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1789 lines
63 KiB
JavaScript
1789 lines
63 KiB
JavaScript
/**
|
|
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
|
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
( function() {
|
|
|
|
var template = '<img alt="" src="" />',
|
|
templateBlock = new CKEDITOR.template(
|
|
'<figure class="{captionedClass}">' +
|
|
template +
|
|
'<figcaption>{captionPlaceholder}</figcaption>' +
|
|
'</figure>' ),
|
|
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' +
|
|
'}' +
|
|
// Hide resizer in read only mode (#2816).
|
|
'.cke_editable[contenteditable="false"] .cke_image_resizer{' +
|
|
'display:none;' +
|
|
'}' +
|
|
// 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 │<wrapper> │<img /> │
|
|
// │ │ <img /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │left │<wrapper style=”float:left”> │<img style=”float:left” /> │
|
|
// │ │ <img /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │center│<wrapper> │<p style=”text-align:center”>│
|
|
// │ │ <p style=”text-align:center”> │ <img /> │
|
|
// │ │ <img /> │</p> │
|
|
// │ │ </p> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │right │<wrapper style=”float:right”> │<img style=”float:right” /> │
|
|
// │ │ <img /> │ │
|
|
// │ │</wrapper> │ │
|
|
// └──────┴───────────────────────────────┴─────────────────────────────┘
|
|
//
|
|
// Non-captioned widget (config.image2_alignClasses defined)
|
|
// ┌──────┬───────────────────────────────┬─────────────────────────────┐
|
|
// │Align │Internal form │Data │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │none │<wrapper> │<img /> │
|
|
// │ │ <img /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │left │<wrapper class=”left”> │<img class=”left” /> │
|
|
// │ │ <img /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │center│<wrapper> │<p class=”center”> │
|
|
// │ │ <p class=”center”> │ <img /> │
|
|
// │ │ <img /> │</p> │
|
|
// │ │ </p> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼───────────────────────────────┼─────────────────────────────┤
|
|
// │right │<wrapper class=”right”> │<img class=”right” /> │
|
|
// │ │ <img /> │ │
|
|
// │ │</wrapper> │ │
|
|
// └──────┴───────────────────────────────┴─────────────────────────────┘
|
|
//
|
|
// Captioned widget (inline styles)
|
|
// ┌──────┬────────────────────────────────────────┬────────────────────────────────────────┐
|
|
// │Align │Internal form │Data │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │none │<wrapper> │<figure /> │
|
|
// │ │ <figure /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │left │<wrapper style=”float:left”> │<figure style=”float:left” /> │
|
|
// │ │ <figure /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │center│<wrapper style=”text-align:center”> │<div style=”text-align:center”> │
|
|
// │ │ <figure style=”display:inline-block” />│ <figure style=”display:inline-block” />│
|
|
// │ │</wrapper> │</p> │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │right │<wrapper style=”float:right”> │<figure style=”float:right” /> │
|
|
// │ │ <figure /> │ │
|
|
// │ │</wrapper> │ │
|
|
// └──────┴────────────────────────────────────────┴────────────────────────────────────────┘
|
|
//
|
|
// Captioned widget (config.image2_alignClasses defined)
|
|
// ┌──────┬────────────────────────────────────────┬────────────────────────────────────────┐
|
|
// │Align │Internal form │Data │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │none │<wrapper> │<figure /> │
|
|
// │ │ <figure /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │left │<wrapper class=”left”> │<figure class=”left” /> │
|
|
// │ │ <figure /> │ │
|
|
// │ │</wrapper> │ │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │center│<wrapper class=”center”> │<div class=”center”> │
|
|
// │ │ <figure /> │ <figure /> │
|
|
// │ │</wrapper> │</p> │
|
|
// ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
|
|
// │right │<wrapper class=”right”> │<figure class=”right” /> │
|
|
// │ │ <figure /> │ │
|
|
// │ │</wrapper> │ │
|
|
// └──────┴────────────────────────────────────────┴────────────────────────────────────────┘
|
|
//
|
|
// @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 <img/>.
|
|
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).
|
|
if ( editor.filter.checkFeature( this.features.dimension ) && editor.config.image2_disableResizer !== 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 <img/> or <a><img/></a> from widget. Note that widget element might itself
|
|
// be what we're looking for. Also element can be <p style="text-align:center"><a>...</a></p>.
|
|
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 <figure> from widget template.
|
|
var figure = CKEDITOR.dom.element.createFromHtml( templateBlock.output( {
|
|
captionedClass: captionedClass,
|
|
captionPlaceholder: editor.lang.image2.captionPlaceholder
|
|
} ), doc );
|
|
|
|
// Replace element with <figure>.
|
|
replaceSafely( figure, shift.element );
|
|
|
|
// Use old <img/> or <a><img/></a> 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 <img/> or <a><img/></a> 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 <img/>, it will be wrapped into <a>,
|
|
// which becomes a new widget.element.
|
|
// If element is <a><img/></a>, it will be unlinked
|
|
// so <img/> 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 <img>.
|
|
if ( !newValue )
|
|
newEl = unwrapFromLink( link );
|
|
else {
|
|
// If linked the image, returned element is <a>.
|
|
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 <img/> -> <a><img/></a>.
|
|
// Returns reference to <a>.
|
|
//
|
|
// @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 <a><img/></a> -> <img/>.
|
|
// Returns the reference to <img/>
|
|
//
|
|
// @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:
|
|
//
|
|
// <div class="cke_widget_wrapper">
|
|
// <p class="center-class">
|
|
// <img />
|
|
// </p>
|
|
// </div>
|
|
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 <img> and
|
|
// <figure class="{config.image2_captionedClass}"> 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. <div style="text-align:center"><figure>...</figure></div>.
|
|
// 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. <p style="text-align:center"><img/></p>.
|
|
// Nothing to do here: <p> remains for styling purposes.
|
|
//
|
|
// 3. <div style="text-align:center"><img/></div>.
|
|
// 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 <a><img/></a>.
|
|
image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' );
|
|
}
|
|
|
|
// No center wrapper has been found.
|
|
else if ( name == 'figure' && el.hasClass( captionedClass ) ) {
|
|
image = el.find( function( child ) {
|
|
return child.name === 'img' &&
|
|
CKEDITOR.tools.array.indexOf( [ 'figure', 'a' ], child.parent.name ) !== -1;
|
|
}, true )[ 0 ];
|
|
|
|
// Upcast linked image like <a><img/></a>.
|
|
} 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 <a><img/></a>, <img/> 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 (<figure>) and internally centering is done
|
|
// with widget's wrapper style/class, in the external data representation,
|
|
// <figure> must be wrapped with an element holding an style/class:
|
|
//
|
|
// <div style="text-align:center">
|
|
// <figure class="image" style="display:inline-block">...</figure>
|
|
// </div>
|
|
// or
|
|
// <div class="some-center-class">
|
|
// <figure class="image">...</figure>
|
|
// </div>
|
|
//
|
|
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 <div> or <p>.
|
|
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 <figure> or <img /> can be first (only) child of centering wrapper,
|
|
// regardless of its type.
|
|
if ( !( child.name in validChildren ) )
|
|
return false;
|
|
|
|
// If centering wrapper is <p>, only <img /> can be the child.
|
|
// <p style="text-align:center"><img /></p>
|
|
if ( el.name == 'p' ) {
|
|
if ( !isLinkedOrStandaloneImage( child ) )
|
|
return false;
|
|
}
|
|
// Centering <div> can hold <img/> or <figure>, depending on enterMode.
|
|
else {
|
|
// If a <figure> is the first (only) child, it must have a class.
|
|
// <div style="text-align:center"><figure>...</figure><div>
|
|
if ( child.name == 'figure' ) {
|
|
if ( !child.hasClass( captionedClass ) )
|
|
return false;
|
|
} else {
|
|
// Centering <div> can hold <img/> or <a><img/></a> only when enterMode
|
|
// is ENTER_(BR|DIV).
|
|
// <div style="text-align:center"><img /></div>
|
|
// <div style="text-align:center"><a><img /></a></div>
|
|
if ( editor.enterMode == CKEDITOR.ENTER_P )
|
|
return false;
|
|
|
|
// Regardless of enterMode, a child which is not <figure> must be
|
|
// either <img/> or <a><img/></a>.
|
|
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 <img/> or <a><img/></a>.
|
|
//
|
|
// @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,
|
|
|
|
// Don't update attributes if less than 15.
|
|
// This is to prevent images to visually disappear.
|
|
min = {
|
|
width: 15,
|
|
height: 15
|
|
},
|
|
|
|
max = getMaxSize(),
|
|
|
|
// "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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isAllowedSize( newWidth, newHeight ) ) {
|
|
updateData = { width: newWidth, height: newHeight };
|
|
image.setAttributes( updateData );
|
|
}
|
|
}
|
|
|
|
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( updateData );
|
|
|
|
// Save another undo snapshot: after resizing.
|
|
editor.fire( 'saveSnapshot' );
|
|
}
|
|
|
|
// Don't update data twice or more.
|
|
updateData = false;
|
|
}
|
|
|
|
function getMaxSize() {
|
|
var maxSize = editor.config.image2_maxSize,
|
|
natural;
|
|
|
|
if ( !maxSize ) {
|
|
return null;
|
|
}
|
|
|
|
maxSize = CKEDITOR.tools.copy( maxSize );
|
|
natural = CKEDITOR.plugins.image2.getNatural( image );
|
|
|
|
maxSize.width = Math.max( maxSize.width === 'natural' ? natural.width : maxSize.width, min.width );
|
|
maxSize.height = Math.max( maxSize.height === 'natural' ? natural.height : maxSize.height, min.width );
|
|
|
|
return maxSize;
|
|
}
|
|
|
|
function isAllowedSize( width, height ) {
|
|
var isTooSmall = width < min.width || height < min.height,
|
|
isTooBig = max && ( width > max.width || height > max.height );
|
|
return !isTooSmall && !isTooBig;
|
|
}
|
|
} );
|
|
|
|
// 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;
|
|
|
|
var listener = 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.
|
|
// <a>foo<inline widget/>bar</a>
|
|
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.
|
|
// <a>foo<inline widget/>bar</a>
|
|
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 );
|
|
}
|
|
};
|
|
}
|
|
} );
|
|
// Listener has to be removed due to leaking the editor reference (#589).
|
|
editor.on( 'destroy', function() {
|
|
listener.removeListener();
|
|
} );
|
|
|
|
// Overwrite the default behavior 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 <div> or <p> 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 `<figure>` element of a captioned image.
|
|
*
|
|
* Read more in the {@glink features/image2 documentation} and see the
|
|
* {@glink examples/image2 example}.
|
|
*
|
|
* // 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 {@glink features/image2 documentation} and see the
|
|
* {@glink examples/image2 example}.
|
|
*
|
|
* config.image2_prefillDimensions = false;
|
|
*
|
|
* @since 4.5.0
|
|
* @cfg {Boolean} [image2_prefillDimensions=true]
|
|
* @member CKEDITOR.config
|
|
*/
|
|
|
|
/**
|
|
* Disables the image resizer. By default the resizer is enabled.
|
|
*
|
|
* Read more in the {@glink features/image2 documentation} and see the
|
|
* {@glink examples/image2 example}.
|
|
*
|
|
* config.image2_disableResizer = true;
|
|
*
|
|
* @since 4.5.0
|
|
* @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:
|
|
*
|
|
* <img alt="My image" class="custom-center-class" src="foo.png" />
|
|
*
|
|
* instead of:
|
|
*
|
|
* <img alt="My image" style="float:left" src="foo.png" />
|
|
*
|
|
* **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 `<style> ... <style>` or `<link href="..." rel="stylesheet">`, i.e. within the `<head>`
|
|
* of the page.
|
|
*
|
|
* For example, considering the following configuration:
|
|
*
|
|
* config.image2_alignClasses = [ 'align-left', 'align-center', 'align-right' ];
|
|
*
|
|
* CSS rules can be defined as follows:
|
|
*
|
|
* .align-left {
|
|
* float: left;
|
|
* }
|
|
*
|
|
* .align-right {
|
|
* float: right;
|
|
* }
|
|
*
|
|
* .align-center {
|
|
* text-align: center;
|
|
* }
|
|
*
|
|
* .align-center > figure {
|
|
* display: inline-block;
|
|
* }
|
|
*
|
|
* Read more in the {@glink features/image2 documentation} and see the
|
|
* {@glink examples/image2 example}.
|
|
*
|
|
* @since 4.4.0
|
|
* @cfg {String[]} [image2_alignClasses=null]
|
|
* @member CKEDITOR.config
|
|
*/
|
|
|
|
/**
|
|
* Determines whether alternative text is required for the captioned image.
|
|
*
|
|
* config.image2_altRequired = true;
|
|
*
|
|
* Read more in the {@glink features/image2 documentation} and see the
|
|
* {@glink examples/image2 example}.
|
|
*
|
|
* @since 4.6.0
|
|
* @cfg {Boolean} [image2_altRequired=false]
|
|
* @member CKEDITOR.config
|
|
*/
|
|
|
|
/**
|
|
* Determines the maximum size that an image can be resized to with the resize handle.
|
|
*
|
|
* It stores two properties: `width` and `height`. They can be set with one of the two types:
|
|
*
|
|
* * A number representing a value that limits the maximum size in pixel units:
|
|
*
|
|
* ```js
|
|
* config.image2_maxSize = {
|
|
* height: 300,
|
|
* width: 250
|
|
* };
|
|
* ```
|
|
*
|
|
* * A string representing the natural image size, so each image resize operation is limited to its own natural height or width:
|
|
*
|
|
* ```js
|
|
* config.image2_maxSize = {
|
|
* height: 'natural',
|
|
* width: 'natural'
|
|
* }
|
|
* ```
|
|
*
|
|
* Note: An image can still be resized to bigger dimensions when using the image dialog.
|
|
*
|
|
* @since 4.12.0
|
|
* @cfg {Object.<String, Number/String>} [image2_maxSize]
|
|
* @member CKEDITOR.config
|
|
*/
|