/**
* @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview [Widget](http://ckeditor.com/addon/widget) plugin.
*/
'use strict';
( function() {
var DRAG_HANDLER_SIZE = 15;
CKEDITOR.plugins.add( 'widget', {
// jscs:disable maximumLineLength
lang: 'af,ar,bg,ca,cs,cy,da,de,el,en,en-gb,eo,es,eu,fa,fi,fr,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,pl,pt,pt-br,ru,sk,sl,sq,sv,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
// jscs:enable maximumLineLength
requires: 'lineutils,clipboard',
onLoad: function() {
CKEDITOR.addCss(
'.cke_widget_wrapper{' +
'position:relative;' +
'outline:none' +
'}' +
'.cke_widget_inline{' +
'display:inline-block' +
'}' +
'.cke_widget_wrapper:hover>.cke_widget_element{' +
'outline:2px solid yellow;' +
'cursor:default' +
'}' +
'.cke_widget_wrapper:hover .cke_widget_editable{' +
'outline:2px solid yellow' +
'}' +
'.cke_widget_wrapper.cke_widget_focused>.cke_widget_element,' +
// We need higher specificity than hover style.
'.cke_widget_wrapper .cke_widget_editable.cke_widget_editable_focused{' +
'outline:2px solid #ace' +
'}' +
'.cke_widget_editable{' +
'cursor:text' +
'}' +
'.cke_widget_drag_handler_container{' +
'position:absolute;' +
'width:' + DRAG_HANDLER_SIZE + 'px;' +
'height:0;' +
// Initially drag handler should not be visible, until its position will be
// calculated (#11177).
// We need to hide unpositined handlers, so they don't extend
// widget's outline far to the left (#12024).
'display:none;' +
'opacity:0.75;' +
'transition:height 0s 0.2s;' + // Delay hiding drag handler.
// Prevent drag handler from being misplaced (#11198).
'line-height:0' +
'}' +
'.cke_widget_wrapper:hover>.cke_widget_drag_handler_container{' +
'height:' + DRAG_HANDLER_SIZE + 'px;' +
'transition:none' +
'}' +
'.cke_widget_drag_handler_container:hover{' +
'opacity:1' +
'}' +
'img.cke_widget_drag_handler{' +
'cursor:move;' +
'width:' + DRAG_HANDLER_SIZE + 'px;' +
'height:' + DRAG_HANDLER_SIZE + 'px;' +
'display:inline-block' +
'}' +
'.cke_widget_mask{' +
'position:absolute;' +
'top:0;' +
'left:0;' +
'width:100%;' +
'height:100%;' +
'display:block' +
'}' +
'.cke_editable.cke_widget_dragging, .cke_editable.cke_widget_dragging *{' +
'cursor:move !important' +
'}'
);
},
beforeInit: function( editor ) {
/**
* An instance of widget repository. It contains all
* {@link CKEDITOR.plugins.widget.repository#registered registered widget definitions} and
* {@link CKEDITOR.plugins.widget.repository#instances initialized instances}.
*
* editor.widgets.add( 'someName', {
* // Widget definition...
* } );
*
* editor.widgets.registered.someName; // -> Widget definition
*
* @since 4.3
* @readonly
* @property {CKEDITOR.plugins.widget.repository} widgets
* @member CKEDITOR.editor
*/
editor.widgets = new Repository( editor );
},
afterInit: function( editor ) {
addWidgetButtons( editor );
setupContextMenu( editor );
}
} );
/**
* Widget repository. It keeps track of all {@link #registered registered widget definitions} and
* {@link #instances initialized instances}. An instance of the repository is available under
* the {@link CKEDITOR.editor#widgets} property.
*
* @class CKEDITOR.plugins.widget.repository
* @mixins CKEDITOR.event
* @constructor Creates a widget repository instance. Note that the widget plugin automatically
* creates a repository instance which is available under the {@link CKEDITOR.editor#widgets} property.
* @param {CKEDITOR.editor} editor The editor instance for which the repository will be created.
*/
function Repository( editor ) {
/**
* The editor instance for which this repository was created.
*
* @readonly
* @property {CKEDITOR.editor} editor
*/
this.editor = editor;
/**
* A hash of registered widget definitions (definition name => {@link CKEDITOR.plugins.widget.definition}).
*
* To register a definition use the {@link #add} method.
*
* @readonly
*/
this.registered = {};
/**
* An object containing initialized widget instances (widget id => {@link CKEDITOR.plugins.widget}).
*
* @readonly
*/
this.instances = {};
/**
* An array of selected widget instances.
*
* @readonly
* @property {CKEDITOR.plugins.widget[]} selected
*/
this.selected = [];
/**
* The focused widget instance. See also {@link CKEDITOR.plugins.widget#event-focus}
* and {@link CKEDITOR.plugins.widget#event-blur} events.
*
* editor.on( 'selectionChange', function() {
* if ( editor.widgets.focused ) {
* // Do something when a widget is focused...
* }
* } );
*
* @readonly
* @property {CKEDITOR.plugins.widget} focused
*/
this.focused = null;
/**
* The widget instance that contains the nested editable which is currently focused.
*
* @readonly
* @property {CKEDITOR.plugins.widget} widgetHoldingFocusedEditable
*/
this.widgetHoldingFocusedEditable = null;
this._ = {
nextId: 0,
upcasts: [],
upcastCallbacks: [],
filters: {}
};
setupWidgetsLifecycle( this );
setupSelectionObserver( this );
setupMouseObserver( this );
setupKeyboardObserver( this );
setupDragAndDrop( this );
setupNativeCutAndCopy( this );
}
Repository.prototype = {
/**
* Minimum interval between selection checks.
*
* @private
*/
MIN_SELECTION_CHECK_INTERVAL: 500,
/**
* Adds a widget definition to the repository. Fires the {@link CKEDITOR.editor#widgetDefinition} event
* which allows to modify the widget definition which is going to be registered.
*
* @param {String} name The name of the widget definition.
* @param {CKEDITOR.plugins.widget.definition} widgetDef Widget definition.
* @returns {CKEDITOR.plugins.widget.definition}
*/
add: function( name, widgetDef ) {
// Create prototyped copy of original widget definition, so we won't modify it.
widgetDef = CKEDITOR.tools.prototypedCopy( widgetDef );
widgetDef.name = name;
widgetDef._ = widgetDef._ || {};
this.editor.fire( 'widgetDefinition', widgetDef );
if ( widgetDef.template )
widgetDef.template = new CKEDITOR.template( widgetDef.template );
addWidgetCommand( this.editor, widgetDef );
addWidgetProcessors( this, widgetDef );
this.registered[ name ] = widgetDef;
return widgetDef;
},
/**
* Adds a callback for element upcasting. Each callback will be executed
* for every element which is later tested by upcast methods. If a callback
* returns `false`, the element will not be upcasted.
*
* // Images with the "banner" class will not be upcasted (e.g. to the image widget).
* editor.widgets.addUpcastCallback( function( element ) {
* if ( element.name == 'img' && element.hasClass( 'banner' ) )
* return false;
* } );
*
* @param {Function} callback
* @param {CKEDITOR.htmlParser.element} callback.element
*/
addUpcastCallback: function( callback ) {
this._.upcastCallbacks.push( callback );
},
/**
* Checks the selection to update widget states (selection and focus).
*
* This method is triggered by the {@link #event-checkSelection} event.
*/
checkSelection: function() {
var sel = this.editor.getSelection(),
selectedElement = sel.getSelectedElement(),
updater = stateUpdater( this ),
widget;
// Widget is focused so commit and finish checking.
if ( selectedElement && ( widget = this.getByElement( selectedElement, true ) ) )
return updater.focus( widget ).select( widget ).commit();
var range = sel.getRanges()[ 0 ];
// No ranges or collapsed range mean that nothing is selected, so commit and finish checking.
if ( !range || range.collapsed )
return updater.commit();
// Range is not empty, so create walker checking for wrappers.
var walker = new CKEDITOR.dom.walker( range ),
wrapper;
walker.evaluator = Widget.isDomWidgetWrapper;
while ( ( wrapper = walker.next() ) )
updater.select( this.getByElement( wrapper ) );
updater.commit();
},
/**
* Checks if all widget instances are still present in the DOM.
* Destroys those instances that are not present.
* Reinitializes widgets on widget wrappers for which widget instances
* cannot be found. Takes nested widgets into account, too.
*
* This method triggers the {@link #event-checkWidgets} event whose listeners
* can cancel the method's execution or modify its options.
*
* @param [options] The options object.
* @param {Boolean} [options.initOnlyNew] Initializes widgets only on newly wrapped
* widget elements (those which still have the `cke_widget_new` class). When this option is
* set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure)
* will not be reinitialized. This makes the check faster.
* @param {Boolean} [options.focusInited] If only one widget is initialized by
* the method, it will be focused.
*/
checkWidgets: function( options ) {
this.fire( 'checkWidgets', CKEDITOR.tools.copy( options || {} ) );
},
/**
* Removes the widget from the editor and moves the selection to the closest
* editable position if the widget was focused before.
*
* @param {CKEDITOR.plugins.widget} widget The widget instance to be deleted.
*/
del: function( widget ) {
if ( this.focused === widget ) {
var editor = widget.editor,
range = editor.createRange(),
found;
// If haven't found place for caret on the default side,
// try to find it on the other side.
if ( !( found = range.moveToClosestEditablePosition( widget.wrapper, true ) ) )
found = range.moveToClosestEditablePosition( widget.wrapper, false );
if ( found )
editor.getSelection().selectRanges( [ range ] );
}
widget.wrapper.remove();
this.destroy( widget, true );
},
/**
* Destroys the widget instance and all its nested widgets (widgets inside its nested editables).
*
* @param {CKEDITOR.plugins.widget} widget The widget instance to be destroyed.
* @param {Boolean} [offline] Whether the widget is offline (detached from the DOM tree) —
* in this case the DOM (attributes, classes, etc.) will not be cleaned up.
*/
destroy: function( widget, offline ) {
if ( this.widgetHoldingFocusedEditable === widget )
setFocusedEditable( this, widget, null, offline );
widget.destroy( offline );
delete this.instances[ widget.id ];
this.fire( 'instanceDestroyed', widget );
},
/**
* Destroys all widget instances.
*
* @param {Boolean} [offline] Whether the widgets are offline (detached from the DOM tree) —
* in this case the DOM (attributes, classes, etc.) will not be cleaned up.
* @param {CKEDITOR.dom.element} [container] The container within widgets will be destroyed.
* This option will be ignored if the `offline` flag was set to `true`, because in such case
* it is not possible to find widgets within the passed block.
*/
destroyAll: function( offline, container ) {
var widget,
id,
instances = this.instances;
if ( container && !offline ) {
var wrappers = container.find( '.cke_widget_wrapper' ),
l = wrappers.count(),
i = 0;
// Length is constant, because this is not a live node list.
// Note: since querySelectorAll returns nodes in document order,
// outer widgets are always placed before their nested widgets and therefore
// are destroyed before them.
for ( ; i < l; ++i ) {
widget = this.getByElement( wrappers.getItem( i ), true );
// Widget might not be found, because it could be a nested widget,
// which would be destroyed when destroying its parent.
if ( widget )
this.destroy( widget );
}
return;
}
for ( id in instances ) {
widget = instances[ id ];
this.destroy( widget, offline );
}
},
/**
* Finalizes a process of widget creation. This includes:
*
* * inserting widget element into editor,
* * marking widget instance as ready (see {@link CKEDITOR.plugins.widget#event-ready}),
* * focusing widget instance.
*
* This method is used by the default widget's command and is called
* after widget's dialog (if set) is closed. It may also be used in a
* customized process of widget creation and insertion.
*
* widget.once( 'edit', function() {
* // Finalize creation only of not ready widgets.
* if ( widget.isReady() )
* return;
*
* // Cancel edit event to prevent automatic widget insertion.
* evt.cancel();
*
* CustomDialog.open( widget.data, function saveCallback( savedData ) {
* // Cache the container, because widget may be destroyed while saving data,
* // if this process will require some deep transformations.
* var container = widget.wrapper.getParent();
*
* widget.setData( savedData );
*
* // Widget will be retrieved from container and inserted into editor.
* editor.widgets.finalizeCreation( container );
* } );
* } );
*
* @param {CKEDITOR.dom.element/CKEDITOR.dom.documentFragment} container The element
* or document fragment which contains widget wrapper. The container is used, so before
* finalizing creation the widget can be freely transformed (even destroyed and reinitialized).
*/
finalizeCreation: function( container ) {
var wrapper = container.getFirst();
if ( wrapper && Widget.isDomWidgetWrapper( wrapper ) ) {
this.editor.insertElement( wrapper );
var widget = this.getByElement( wrapper );
// Fire postponed #ready event.
widget.ready = true;
widget.fire( 'ready' );
widget.focus();
}
},
/**
* Finds a widget instance which contains a given element. The element will be the {@link CKEDITOR.plugins.widget#wrapper wrapper}
* of the returned widget or a descendant of this {@link CKEDITOR.plugins.widget#wrapper wrapper}.
*
* editor.widgets.getByElement( someWidget.wrapper ); // -> someWidget
* editor.widgets.getByElement( someWidget.parts.caption ); // -> someWidget
*
* // Check wrapper only:
* editor.widgets.getByElement( someWidget.wrapper, true ); // -> someWidget
* editor.widgets.getByElement( someWidget.parts.caption, true ); // -> null
*
* @param {CKEDITOR.dom.element} element The element to be checked.
* @param {Boolean} [checkWrapperOnly] If set to `true`, the method will not check wrappers' descendants.
* @returns {CKEDITOR.plugins.widget} The widget instance or `null`.
*/
getByElement: ( function() {
var validWrapperElements = { div: 1, span: 1 };
function getWidgetId( element ) {
return element.is( validWrapperElements ) && element.data( 'cke-widget-id' );
}
return function( element, checkWrapperOnly ) {
if ( !element )
return null;
var id = getWidgetId( element );
// There's no need to check element parents if element is a wrapper.
if ( !checkWrapperOnly && !id ) {
var limit = this.editor.editable();
// Try to find a closest ascendant which is a widget wrapper.
do {
element = element.getParent();
} while ( element && !element.equals( limit ) && !( id = getWidgetId( element ) ) );
}
return this.instances[ id ] || null;
};
} )(),
/**
* Initializes a widget on a given element if the widget has not been initialized on it yet.
*
* @param {CKEDITOR.dom.element} element The future widget element.
* @param {String/CKEDITOR.plugins.widget.definition} [widgetDef] Name of a widget or a widget definition.
* The widget definition should be previously registered by using the
* {@link CKEDITOR.plugins.widget.repository#add} method.
* @param [startupData] Widget startup data (has precedence over default one).
* @returns {CKEDITOR.plugins.widget} The widget instance or `null` if a widget could not be initialized on
* a given element.
*/
initOn: function( element, widgetDef, startupData ) {
if ( !widgetDef )
widgetDef = this.registered[ element.data( 'widget' ) ];
else if ( typeof widgetDef == 'string' )
widgetDef = this.registered[ widgetDef ];
if ( !widgetDef )
return null;
// Wrap element if still wasn't wrapped (was added during runtime by method that skips dataProcessor).
var wrapper = this.wrapElement( element, widgetDef.name );
if ( wrapper ) {
// Check if widget wrapper is new (widget hasn't been initialized on it yet).
// This class will be removed by widget constructor to avoid locking snapshot twice.
if ( wrapper.hasClass( 'cke_widget_new' ) ) {
var widget = new Widget( this, this._.nextId++, element, widgetDef, startupData );
// Widget could be destroyed when initializing it.
if ( widget.isInited() ) {
this.instances[ widget.id ] = widget;
return widget;
} else {
return null;
}
}
// Widget already has been initialized, so try to get widget by element.
// Note - it may happen that other instance will returned than the one created above,
// if for example widget was destroyed and reinitialized.
return this.getByElement( element );
}
// No wrapper means that there's no widget for this element.
return null;
},
/**
* Initializes widgets on all elements which were wrapped by {@link #wrapElement} and
* have not been initialized yet.
*
* @param {CKEDITOR.dom.element} [container=editor.editable()] The container which will be checked for not
* initialized widgets. Defaults to editor's {@link CKEDITOR.editor#editable editable} element.
* @returns {CKEDITOR.plugins.widget[]} Array of widget instances which have been initialized.
* Note: Only first-level widgets are returned — without nested widgets.
*/
initOnAll: function( container ) {
var newWidgets = ( container || this.editor.editable() ).find( '.cke_widget_new' ),
newInstances = [],
instance;
for ( var i = newWidgets.count(); i--; ) {
instance = this.initOn( newWidgets.getItem( i ).getFirst( Widget.isDomWidgetElement ) );
if ( instance )
newInstances.push( instance );
}
return newInstances;
},
/**
* Allows to listen to events on specific types of widgets, even if they are not created yet.
*
* Please note that this method inherits parameters from the {@link CKEDITOR.event#method-on} method with one
* extra parameter at the beginning which is the widget name.
*
* editor.widgets.onWidget( 'image', 'action', function( evt ) {
* // Event `action` occurs on `image` widget.
* } );
*
* @since 4.5
* @param {String} widgetName
* @param {String} eventName
* @param {Function} listenerFunction
* @param {Object} [scopeObj]
* @param {Object} [listenerData]
* @param {Number} [priority=10]
*/
onWidget: function( widgetName ) {
var args = Array.prototype.slice.call( arguments );
args.shift();
for ( var i in this.instances ) {
var instance = this.instances[ i ];
if ( instance.name == widgetName ) {
instance.on.apply( instance, args );
}
}
this.on( 'instanceCreated', function( evt ) {
var widget = evt.data;
if ( widget.name == widgetName ) {
widget.on.apply( widget, args );
}
} );
},
/**
* Parses element classes string and returns an object
* whose keys contain class names. Skips all `cke_*` classes.
*
* This method is used by the {@link CKEDITOR.plugins.widget#getClasses} method and
* may be used when overriding that method.
*
* @since 4.4
* @param {String} classes String (value of `class` attribute).
* @returns {Object} Object containing classes or `null` if no classes found.
*/
parseElementClasses: function( classes ) {
if ( !classes )
return null;
classes = CKEDITOR.tools.trim( classes ).split( /\s+/ );
var cl,
obj = {},
hasClasses = 0;
while ( ( cl = classes.pop() ) ) {
if ( cl.indexOf( 'cke_' ) == -1 )
obj[ cl ] = hasClasses = 1;
}
return hasClasses ? obj : null;
},
/**
* Wraps an element with a widget's non-editable container.
*
* If this method is called on an {@link CKEDITOR.htmlParser.element}, then it will
* also take care of fixing the DOM after wrapping (the wrapper may not be allowed in element's parent).
*
* @param {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} element The widget element to be wrapped.
* @param {String} [widgetName] The name of the widget definition. Defaults to element's `data-widget`
* attribute value.
* @returns {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} The wrapper element or `null` if
* the widget definition of this name is not registered.
*/
wrapElement: function( element, widgetName ) {
var wrapper = null,
widgetDef,
isInline;
if ( element instanceof CKEDITOR.dom.element ) {
widgetDef = this.registered[ widgetName || element.data( 'widget' ) ];
if ( !widgetDef )
return null;
// Do not wrap already wrapped element.
wrapper = element.getParent();
if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.data( 'cke-widget-wrapper' ) )
return wrapper;
// If attribute isn't already set (e.g. for pasted widget), set it.
if ( !element.hasAttribute( 'data-cke-widget-keep-attr' ) )
element.data( 'cke-widget-keep-attr', element.data( 'widget' ) ? 1 : 0 );
if ( widgetName )
element.data( 'widget', widgetName );
isInline = isWidgetInline( widgetDef, element.getName() );
wrapper = new CKEDITOR.dom.element( isInline ? 'span' : 'div' );
wrapper.setAttributes( getWrapperAttributes( isInline ) );
wrapper.data( 'cke-display-name', widgetDef.pathName ? widgetDef.pathName : element.getName() );
// Replace element unless it is a detached one.
if ( element.getParent( true ) )
wrapper.replace( element );
element.appendTo( wrapper );
}
else if ( element instanceof CKEDITOR.htmlParser.element ) {
widgetDef = this.registered[ widgetName || element.attributes[ 'data-widget' ] ];
if ( !widgetDef )
return null;
wrapper = element.parent;
if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.attributes[ 'data-cke-widget-wrapper' ] )
return wrapper;
// If attribute isn't already set (e.g. for pasted widget), set it.
if ( !( 'data-cke-widget-keep-attr' in element.attributes ) )
element.attributes[ 'data-cke-widget-keep-attr' ] = element.attributes[ 'data-widget' ] ? 1 : 0;
if ( widgetName )
element.attributes[ 'data-widget' ] = widgetName;
isInline = isWidgetInline( widgetDef, element.name );
wrapper = new CKEDITOR.htmlParser.element( isInline ? 'span' : 'div', getWrapperAttributes( isInline ) );
wrapper.attributes[ 'data-cke-display-name' ] = widgetDef.pathName ? widgetDef.pathName : element.name;
var parent = element.parent,
index;
// Don't detach already detached element.
if ( parent ) {
index = element.getIndex();
element.remove();
}
wrapper.add( element );
// Insert wrapper fixing DOM (splitting parents if wrapper is not allowed inside them).
parent && insertElement( parent, index, wrapper );
}
return wrapper;
},
// Expose for tests.
_tests_createEditableFilter: createEditableFilter
};
CKEDITOR.event.implementOn( Repository.prototype );
/**
* An event fired when a widget instance is created, but before it is fully initialized.
*
* @event instanceCreated
* @param {CKEDITOR.plugins.widget} data The widget instance.
*/
/**
* An event fired when a widget instance was destroyed.
*
* See also {@link CKEDITOR.plugins.widget#event-destroy}.
*
* @event instanceDestroyed
* @param {CKEDITOR.plugins.widget} data The widget instance.
*/
/**
* An event fired to trigger the selection check.
*
* See the {@link #method-checkSelection} method.
*
* @event checkSelection
*/
/**
* An event fired by the the {@link #method-checkWidgets} method.
*
* It can be canceled in order to stop the {@link #method-checkWidgets}
* method execution or the event listener can modify the method's options.
*
* @event checkWidgets
* @param [data]
* @param {Boolean} [data.initOnlyNew] Initialize widgets only on newly wrapped
* widget elements (those which still have the `cke_widget_new` class). When this option is
* set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure)
* will not be reinitialized. This makes the check faster.
* @param {Boolean} [data.focusInited] If only one widget is initialized by
* the method, it will be focused.
*/
/**
* An instance of a widget. Together with {@link CKEDITOR.plugins.widget.repository} these
* two classes constitute the core of the Widget System.
*
* Note that neither the repository nor the widget instances can be created by using their constructors.
* A repository instance is automatically set up by the Widget plugin and is accessible under
* {@link CKEDITOR.editor#widgets}, while widget instances are created and destroyed by the repository.
*
* To create a widget, first you need to {@link CKEDITOR.plugins.widget.repository#add register} its
* {@link CKEDITOR.plugins.widget.definition definition}:
*
* editor.widgets.add( 'simplebox', {
* upcast: function( element ) {
* // Defines which elements will become widgets.
* if ( element.hasClass( 'simplebox' ) )
* return true;
* },
* init: function() {
* // ...
* }
* } );
*
* Once the widget definition is registered, widgets will be automatically
* created when loading data:
*
* editor.setData( '<div class="simplebox">foo</div>', function() {
* console.log( editor.widgets.instances ); // -> An object containing one instance.
* } );
*
* It is also possible to create instances during runtime by using a command
* (if a {@link CKEDITOR.plugins.widget.definition#template} property was defined):
*
* // You can execute an automatically defined command to
* // insert a new simplebox widget or edit the one currently focused.
* editor.execCommand( 'simplebox' );
*
* Note: Since CKEditor 4.5 widget's `startupData` can be passed as the command argument:
*
* editor.execCommand( 'simplebox', {
* startupData: {
* align: 'left'
* }
* } );
*
* A widget can also be created in a completely custom way:
*
* var element = editor.document.createElement( 'div' );
* editor.insertElement( element );
* var widget = editor.widgets.initOn( element, 'simplebox' );
*
* @since 4.3
* @class CKEDITOR.plugins.widget
* @mixins CKEDITOR.event
* @extends CKEDITOR.plugins.widget.definition
* @constructor Creates an instance of the widget class. Do not use it directly, but instead initialize widgets
* by using the {@link CKEDITOR.plugins.widget.repository#initOn} method or by the upcasting system.
* @param {CKEDITOR.plugins.widget.repository} widgetsRepo
* @param {Number} id Unique ID of this widget instance.
* @param {CKEDITOR.dom.element} element The widget element.
* @param {CKEDITOR.plugins.widget.definition} widgetDef Widget's registered definition.
* @param [startupData] Initial widget data. This data object will overwrite the default data and
* the data loaded from the DOM.
*/
function Widget( widgetsRepo, id, element, widgetDef, startupData ) {
var editor = widgetsRepo.editor;
// Extend this widget with widgetDef-specific methods and properties.
CKEDITOR.tools.extend( this, widgetDef, {
/**
* The editor instance.
*
* @readonly
* @property {CKEDITOR.editor}
*/
editor: editor,
/**
* This widget's unique (per editor instance) ID.
*
* @readonly
* @property {Number}
*/
id: id,
/**
* Whether this widget is an inline widget (based on an inline element unless
* forced otherwise by {@link CKEDITOR.plugins.widget.definition#inline}).
*
* **Note:** This option does not allow to turn a block element into an inline widget.
* However, it makes it possible to turn an inline element into a block widget or to
* force a correct type in case when automatic recognition fails.
*
* @readonly
* @property {Boolean}
*/
inline: element.getParent().getName() == 'span',
/**
* The widget element — the element on which the widget was initialized.
*
* @readonly
* @property {CKEDITOR.dom.element} element
*/
element: element,
/**
* Widget's data object.
*
* The data can only be set by using the {@link #setData} method.
* Changes made to the data fire the {@link #event-data} event.
*
* @readonly
*/
data: CKEDITOR.tools.extend( {}, typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults ),
/**
* Indicates if a widget is data-ready. Set to `true` when data from all sources
* ({@link CKEDITOR.plugins.widget.definition#defaults}, set in the
* {@link #init} method, loaded from the widget's element and startup data coming from the constructor)
* are finally loaded. This is immediately followed by the first {@link #event-data}.
*
* @readonly
*/
dataReady: false,
/**
* Whether a widget instance was initialized. This means that:
*
* * An instance was created,
* * Its properties were set,
* * The `init` method was executed.
*
* **Note**: The first {@link #event-data} event could not be fired yet which
* means that the widget's DOM has not been set up yet. Wait for the {@link #event-ready}
* event to be notified when a widget is fully initialized and ready.
*
* **Note**: Use the {@link #isInited} method to check whether a widget is initialized and
* has not been destroyed.
*
* @readonly
*/
inited: false,
/**
* Whether a widget instance is ready. This means that the widget is {@link #inited} and
* that its DOM was finally set up.
*
* **Note:** Use the {@link #isReady} method to check whether a widget is ready and
* has not been destroyed.
*
* @readonly
*/
ready: false,
// Revert what widgetDef could override (automatic #edit listener).
edit: Widget.prototype.edit,
/**
* The nested editable element which is currently focused.
*
* @readonly
* @property {CKEDITOR.plugins.widget.nestedEditable}
*/
focusedEditable: null,
/**
* The widget definition from which this instance was created.
*
* @readonly
* @property {CKEDITOR.plugins.widget.definition} definition
*/
definition: widgetDef,
/**
* Link to the widget repository which created this instance.
*
* @readonly
* @property {CKEDITOR.plugins.widget.repository} repository
*/
repository: widgetsRepo,
draggable: widgetDef.draggable !== false,
// WAAARNING: Overwrite widgetDef's priv object, because otherwise violent unicorn's gonna visit you.
_: {
downcastFn: ( widgetDef.downcast && typeof widgetDef.downcast == 'string' ) ?
widgetDef.downcasts[ widgetDef.downcast ] : widgetDef.downcast
}
}, true );
/**
* An object of widget component elements.
*
* For every `partName => selector` pair in {@link CKEDITOR.plugins.widget.definition#parts},
* one `partName => element` pair is added to this object during the widget initialization.
*
* @readonly
* @property {Object} parts
*/
/**
* The template which will be used to create a new widget element (when the widget's command is executed).
* It will be populated with {@link #defaults default values}.
*
* @readonly
* @property {CKEDITOR.template} template
*/
/**
* The widget wrapper — a non-editable `div` or `span` element (depending on {@link #inline})
* which is a parent of the {@link #element} and widget compontents like the drag handler and the {@link #mask}.
* It is the outermost widget element.
*
* @readonly
* @property {CKEDITOR.dom.element} wrapper
*/
widgetsRepo.fire( 'instanceCreated', this );
setupWidget( this, widgetDef );
this.init && this.init();
// Finally mark widget as inited.
this.inited = true;
setupWidgetData( this, startupData );
// If at some point (e.g. in #data listener) widget hasn't been destroyed
// and widget is already attached to document then fire #ready.
if ( this.isInited() && editor.editable().contains( this.wrapper ) ) {
this.ready = true;
this.fire( 'ready' );
}
}
Widget.prototype = {
/**
* Adds a class to the widget element. This method is used by
* the {@link #applyStyle} method and should be overriden by widgets
* which should handle classes differently (e.g. add them to other elements).
*
* **Note**: This method should not be used directly. Use the {@link #setData} method to
* set the `classes` property. Read more in the {@link #setData} documentation.
*
* See also: {@link #removeClass}, {@link #hasClass}, {@link #getClasses}.
*
* @since 4.4
* @param {String} className The class name to be added.
*/
addClass: function( className ) {
this.element.addClass( className );
},
/**
* Applies the specified style to the widget. It is highly recommended to use the
* {@link CKEDITOR.editor#applyStyle} or {@link CKEDITOR.style#apply} methods instead of
* using this method directly, because unlike editor's and style's methods, this one
* does not perform any checks.
*
* By default this method handles only classes defined in the style. It clones existing
* classes which are stored in the {@link #property-data widget data}'s `classes` property,
* adds new classes, and calls the {@link #setData} method if at least one new class was added.
* Then, using the {@link #event-data} event listener widget applies modifications passing
* new classes to the {@link #addClass} method.
*
* If you need to handle classes differently than in the default way, you can override the
* {@link #addClass} and related methods. You can also handle other style properties than `classes`
* by overriding this method.
*
* See also: {@link #checkStyleActive}, {@link #removeStyle}.
*
* @since 4.4
* @param {CKEDITOR.style} style The custom widget style to be applied.
*/
applyStyle: function( style ) {
applyRemoveStyle( this, style, 1 );
},
/**
* Checks if the specified style is applied to this widget. It is highly recommended to use the
* {@link CKEDITOR.style#checkActive} method instead of using this method directly,
* because unlike style's method, this one does not perform any checks.
*
* By default this method handles only classes defined in the style and passes
* them to the {@link #hasClass} method. You can override these methods to handle classes
* differently or to handle more of the style properties.
*
* See also: {@link #applyStyle}, {@link #removeStyle}.
*
* @since 4.4
* @param {CKEDITOR.style} style The custom widget style to be checked.
* @returns {Boolean} Whether the style is applied to this widget.
*/
checkStyleActive: function( style ) {
var classes = getStyleClasses( style ),
cl;
if ( !classes )
return false;
while ( ( cl = classes.pop() ) ) {
if ( !this.hasClass( cl ) )
return false;
}
return true;
},
/**
* Destroys this widget instance.
*
* Use {@link CKEDITOR.plugins.widget.repository#destroy} when possible instead of this method.
*
* This method fires the {#event-destroy} event.
*
* @param {Boolean} [offline] Whether a widget is offline (detached from the DOM tree) —
* in this case the DOM (attributes, classes, etc.) will not be cleaned up.
*/
destroy: function( offline ) {
this.fire( 'destroy' );
if ( this.editables ) {
for ( var name in this.editables )
this.destroyEditable( name, offline );
}
if ( !offline ) {
if ( this.element.data( 'cke-widget-keep-attr' ) == '0' )
this.element.removeAttribute( 'data-widget' );
this.element.removeAttributes( [ 'data-cke-widget-data', 'data-cke-widget-keep-attr' ] );
this.element.removeClass( 'cke_widget_element' );
this.element.replace( this.wrapper );
}
this.wrapper = null;
},
/**
* Destroys a nested editable and all nested widgets.
*
* @param {String} editableName Nested editable name.
* @param {Boolean} [offline] See {@link #method-destroy} method.
*/
destroyEditable: function( editableName, offline ) {
var editable = this.editables[ editableName ];
editable.removeListener( 'focus', onEditableFocus );
editable.removeListener( 'blur', onEditableBlur );
this.editor.focusManager.remove( editable );
if ( !offline ) {
this.repository.destroyAll( false, editable );
editable.removeClass( 'cke_widget_editable' );
editable.removeClass( 'cke_widget_editable_focused' );
editable.removeAttributes( [ 'contenteditable', 'data-cke-widget-editable', 'data-cke-enter-mode' ] );
}
delete this.editables[ editableName ];
},
/**
* Starts widget editing.
*
* This method fires the {@link CKEDITOR.plugins.widget#event-edit} event
* which may be canceled in order to prevent it from opening a dialog window.
*
* The dialog window name is obtained from the event's data `dialog` property or
* from {@link CKEDITOR.plugins.widget.definition#dialog}.
*
* @returns {Boolean} Returns `true` if a dialog window was opened.
*/
edit: function() {
var evtData = { dialog: this.dialog },
that = this;
// Edit event was blocked or there's no dialog to be automatically opened.
if ( this.fire( 'edit', evtData ) === false || !evtData.dialog )
return false;
this.editor.openDialog( evtData.dialog, function( dialog ) {
var showListener,
okListener;
// Allow to add a custom dialog handler.
if ( that.fire( 'dialog', dialog ) === false )
return;
showListener = dialog.on( 'show', function() {
dialog.setupContent( that );
} );
okListener = dialog.on( 'ok', function() {
// Commit dialog's fields, but prevent from
// firing data event for every field. Fire only one,
// bulk event at the end.
var dataChanged,
dataListener = that.on( 'data', function( evt ) {
dataChanged = 1;
evt.cancel();
}, null, null, 0 );
// Create snapshot preceeding snapshot with changed widget...
// TODO it should not be required, but it is and I found similar
// code in dialog#ok listener in dialog/plugin.js.
that.editor.fire( 'saveSnapshot' );
dialog.commitContent( that );
dataListener.removeListener();
if ( dataChanged ) {
that.fire( 'data', that.data );
that.editor.fire( 'saveSnapshot' );
}
} );
dialog.once( 'hide', function() {
showListener.removeListener();
okListener.removeListener();
} );
} );
return true;
},
/**
* Returns widget element classes parsed to an object. This method
* is used to populate the `classes` property of widget's {@link #property-data}.
*
* This method reuses {@link CKEDITOR.plugins.widget.repository#parseElementClasses}.
* It should be overriden if a widget should handle classes differently (e.g. on other elements).
*
* See also: {@link #removeClass}, {@link #addClass}, {@link #hasClass}.
*
* @since 4.4
* @returns {Object}
*/
getClasses: function() {
return this.repository.parseElementClasses( this.element.getAttribute( 'class' ) );
},
/**
* Checks if the widget element has specified class. This method is used by
* the {@link #checkStyleActive} method and should be overriden by widgets
* which should handle classes differently (e.g. on other elements).
*
* See also: {@link #removeClass}, {@link #addClass}, {@link #getClasses}.
*
* @since 4.4
* @param {String} className The class to be checked.
* @param {Boolean} Whether a widget has specified class.
*/
hasClass: function( className ) {
return this.element.hasClass( className );
},
/**
* Initializes a nested editable.
*
* **Note**: Only elements from {@link CKEDITOR.dtd#$editable} may become editables.
*
* @param {String} editableName The nested editable name.
* @param {CKEDITOR.plugins.widget.nestedEditable.definition} definition The definition of the nested editable.
* @returns {Boolean} Whether an editable was successfully initialized.
*/
initEditable: function( editableName, definition ) {
// Don't fetch just first element which matched selector but look for a correct one. (#13334)
var editable = this._findOneNotNested( definition.selector );
if ( editable && editable.is( CKEDITOR.dtd.$editable ) ) {
editable = new NestedEditable( this.editor, editable, {
filter: createEditableFilter.call( this.repository, this.name, editableName, definition )
} );
this.editables[ editableName ] = editable;
editable.setAttributes( {
contenteditable: 'true',
'data-cke-widget-editable': editableName,
'data-cke-enter-mode': editable.enterMode
} );
if ( editable.filter )
editable.data( 'cke-filter', editable.filter.id );
editable.addClass( 'cke_widget_editable' );
// This class may be left when d&ding widget which
// had focused editable. Clean this class here, not in
// cleanUpWidgetElement for performance and code size reasons.
editable.removeClass( 'cke_widget_editable_focused' );
if ( definition.pathName )
editable.data( 'cke-display-name', definition.pathName );
this.editor.focusManager.add( editable );
editable.on( 'focus', onEditableFocus, this );
CKEDITOR.env.ie && editable.on( 'blur', onEditableBlur, this );
// Finally, process editable's data. This data wasn't processed when loading
// editor's data, becuase they need to be processed separately, with its own filters and settings.
editable._.initialSetData = true;
editable.setData( editable.getHtml() );
return true;
}
return false;
},
/**
* Looks inside wrapper element to find a node that
* matches given selector and is not nested in other widget. (#13334)
*
* @since 4.5
* @private
* @param {String} selector Selector to match.
* @returns {CKEDITOR.dom.element} Matched element or `null` if a node has not been found.
*/
_findOneNotNested: function( selector ) {
var matchedElements = this.wrapper.find( selector ),
match,
closestWrapper;
for ( var i = 0; i < matchedElements.count(); i++ ) {
match = matchedElements.getItem( i );
closestWrapper = match.getAscendant( Widget.isDomWidgetWrapper );
// The closest ascendant-wrapper of this match defines to which widget
// this match belongs. If the ascendant is this widget's wrapper
// it means that the match is not nested in other widget.
if ( this.wrapper.equals( closestWrapper ) ) {
return match;
}
}
return null;
},
/**
* Checks if a widget has already been initialized and has not been destroyed yet.
*
* See {@link #inited} for more details.
*
* @returns {Boolean}
*/
isInited: function() {
return !!( this.wrapper && this.inited );
},
/**
* Checks if a widget is ready and has not been destroyed yet.
*
* See {@link #property-ready} for more details.
*
* @returns {Boolean}
*/
isReady: function() {
return this.isInited() && this.ready;
},
/**
* Focuses a widget by selecting it.
*/
focus: function() {
var sel = this.editor.getSelection();
// Fake the selection before focusing editor, to avoid unpreventable viewports scrolling
// on Webkit/Blink/IE which is done because there's no selection or selection was somewhere else than widget.
if ( sel ) {
var isDirty = this.editor.checkDirty();
sel.fake( this.wrapper );
!isDirty && this.editor.resetDirty();
}
// Always focus editor (not only when focusManger.hasFocus is false) (because of #10483).
this.editor.focus();
},
/**
* Removes a class from the widget element. This method is used by
* the {@link #removeStyle} method and should be overriden by widgets
* which should handle classes differently (e.g. on other elements).
*
* **Note**: This method should not be used directly. Use the {@link #setData} method to
* set the `classes` property. Read more in the {@link #setData} documentation.
*
* See also: {@link #hasClass}, {@link #addClass}.
*
* @since 4.4
* @param {String} className The class to be removed.
*/
removeClass: function( className ) {
this.element.removeClass( className );
},
/**
* Removes the specified style from the widget. It is highly recommended to use the
* {@link CKEDITOR.editor#removeStyle} or {@link CKEDITOR.style#remove} methods instead of
* using this method directly, because unlike editor's and style's methods, this one
* does not perform any checks.
*
* Read more about how applying/removing styles works in the {@link #applyStyle} method documentation.
*
* See also {@link #checkStyleActive}, {@link #applyStyle}, {@link #getClasses}.
*
* @since 4.4
* @param {CKEDITOR.style} style The custom widget style to be removed.
*/
removeStyle: function( style ) {
applyRemoveStyle( this, style, 0 );
},
/**
* Sets widget value(s) in the {@link #property-data} object.
* If the given value(s) modifies current ones, the {@link #event-data} event is fired.
*
* this.setData( 'align', 'left' );
* this.data.align; // -> 'left'
*
* this.setData( { align: 'right', opened: false } );
* this.data.align; // -> 'right'
* this.data.opened; // -> false
*
* Set values are stored in {@link #element}'s attribute (`data-cke-widget-data`),
* in a JSON string, therefore {@link #property-data} should contain
* only serializable data.
*
* **Note:** A special data property, `classes`, exists. It contains an object with
* classes which were returned by the {@link #getClasses} method during the widget initialization.
* This property is then used by the {@link #applyStyle} and {@link #removeStyle} methods.
* When it is changed (the reference to object must be changed!), the widget updates its classes by
* using the {@link #addClass} and {@link #removeClass} methods.
*
* // Adding a new class.
* var classes = CKEDITOR.tools.clone( widget.data.classes );
* classes.newClass = 1;
* widget.setData( 'classes', classes );
*
* // Removing a class.
* var classes = CKEDITOR.tools.clone( widget.data.classes );
* delete classes.newClass;
* widget.setData( 'classes', classes );
*
* @param {String/Object} keyOrData
* @param {Object} value
* @chainable
*/
setData: function( key, value ) {
var data = this.data,
modified = 0;
if ( typeof key == 'string' ) {
if ( data[ key ] !== value ) {
data[ key ] = value;
modified = 1;
}
}
else {
var newData = key;
for ( key in newData ) {
if ( data[ key ] !== newData[ key ] ) {
modified = 1;
data[ key ] = newData[ key ];
}
}
}
// Block firing data event and overwriting data element before setupWidgetData is executed.
if ( modified && this.dataReady ) {
writeDataToElement( this );
this.fire( 'data', data );
}
return this;
},
/**
* Changes the widget's focus state. This method is executed automatically after
* a widget was focused by the {@link #method-focus} method or the selection was moved
* out of the widget.
*
* This is a low-level method which is not integrated with e.g. the undo manager.
* Use the {@link #method-focus} method instead.
*
* @param {Boolean} selected Whether to select or deselect this widget.
* @chainable
*/
setFocused: function( focused ) {
this.wrapper[ focused ? 'addClass' : 'removeClass' ]( 'cke_widget_focused' );
this.fire( focused ? 'focus' : 'blur' );
return this;
},
/**
* Changes the widget's select state. This method is executed automatically after
* a widget was selected by the {@link #method-focus} method or the selection
* was moved out of the widget.
*
* This is a low-level method which is not integrated with e.g. the undo manager.
* Use the {@link #method-focus} method instead or simply change the selection.
*
* @param {Boolean} selected Whether to select or deselect this widget.
* @chainable
*/
setSelected: function( selected ) {
this.wrapper[ selected ? 'addClass' : 'removeClass' ]( 'cke_widget_selected' );
this.fire( selected ? 'select' : 'deselect' );
return this;
},
/**
* Repositions drag handler according to the widget's element position. Should be called from events, like mouseover.
*/
updateDragHandlerPosition: function() {
var editor = this.editor,
domElement = this.element.$,
oldPos = this._.dragHandlerOffset,
newPos = {
x: domElement.offsetLeft,
y: domElement.offsetTop - DRAG_HANDLER_SIZE
};
if ( oldPos && newPos.x == oldPos.x && newPos.y == oldPos.y )
return;
// We need to make sure that dirty state is not changed (#11487).
var initialDirty = editor.checkDirty();
editor.fire( 'lockSnapshot' );
this.dragHandlerContainer.setStyles( {
top: newPos.y + 'px',
left: newPos.x + 'px',
display: 'block'
} );
editor.fire( 'unlockSnapshot' );
!initialDirty && editor.resetDirty();
this._.dragHandlerOffset = newPos;
}
};
CKEDITOR.event.implementOn( Widget.prototype );
/**
* Gets the {@link #isDomNestedEditable nested editable}
* (returned as a {@link CKEDITOR.dom.element}, not as a {@link CKEDITOR.plugins.widget.nestedEditable})
* closest to the `node` or the `node` if it is a nested editable itself.
*
* @since 4.5
* @static
* @param {CKEDITOR.dom.element} guard Stop ancestor search on this node (usually editor's editable).
* @param {CKEDITOR.dom.node} node Start the search from this node.
* @returns {CKEDITOR.dom.element/null} Element or `null` if not found.
*/
Widget.getNestedEditable = function( guard, node ) {
if ( !node || node.equals( guard ) )
return null;
if ( Widget.isDomNestedEditable( node ) )
return node;
return Widget.getNestedEditable( guard, node.getParent() );
};
/**
* Checks whether the `node` is a widget's drag handle element.
*
* @since 4.5
* @static
* @param {CKEDITOR.dom.node} node
* @returns {Boolean}
*/
Widget.isDomDragHandler = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-drag-handler' );
};
/**
* Checks whether the `node` is a container of the widget's drag handle element.
*
* @since 4.5
* @static
* @param {CKEDITOR.dom.node} node
* @returns {Boolean}
*/
Widget.isDomDragHandlerContainer = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_widget_drag_handler_container' );
};
/**
* Checks whether the `node` is a {@link CKEDITOR.plugins.widget#editables nested editable}.
* Note that this function only checks whether it is the right element, not whether
* the passed `node` is an instance of {@link CKEDITOR.plugins.widget.nestedEditable}.
*
* @since 4.5
* @static
* @param {CKEDITOR.dom.node} node
* @returns {Boolean}
*/
Widget.isDomNestedEditable = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-editable' );
};
/**
* Checks whether the `node` is a {@link CKEDITOR.plugins.widget#element widget element}.
*
* @since 4.5
* @static
* @param {CKEDITOR.dom.node} node
* @returns {Boolean}
*/
Widget.isDomWidgetElement = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-widget' );
};
/**
* Checks whether the `node` is a {@link CKEDITOR.plugins.widget#wrapper widget wrapper}.
*
* @since 4.5
* @static
* @param {CKEDITOR.dom.element} node
* @returns {Boolean}
*/
Widget.isDomWidgetWrapper = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-wrapper' );
};
/**
* Checks whether the `node` is a {@link CKEDITOR.plugins.widget#element widget element}.
*
* @since 4.5
* @static
* @param {CKEDITOR.htmlParser.node} node
* @returns {Boolean}
*/
Widget.isParserWidgetElement = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && !!node.attributes[ 'data-widget' ];
};
/**
* Checks whether the `node` is a {@link CKEDITOR.plugins.widget#wrapper widget wrapper}.
*
* @since 4.5
* @static
* @param {CKEDITOR.htmlParser.element} node
* @returns {Boolean}
*/
Widget.isParserWidgetWrapper = function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && !!node.attributes[ 'data-cke-widget-wrapper' ];
};
/**
* An event fired when a widget is ready (fully initialized). This event is fired after:
*
* * {@link #init} is called,
* * The first {@link #event-data} event is fired,
* * A widget is attached to the document.
*
* Therefore, in case of widget creation with a command which opens a dialog window, this event
* will be delayed after the dialog window is closed and the widget is finally inserted into the document.
*
* **Note**: If your widget does not use automatic dialog window binding (i.e. you open the dialog window manually)
* or another situation in which the widget wrapper is not attached to document at the time when it is
* initialized occurs, you need to take care of firing {@link #event-ready} yourself.
*
* See also {@link #property-ready} and {@link #property-inited} properties, and
* {@link #isReady} and {@link #isInited} methods.
*
* @event ready
*/
/**
* An event fired when a widget is about to be destroyed, but before it is
* fully torn down.
*
* @event destroy
*/
/**
* An event fired when a widget is focused.
*
* Widget can be focused by executing {@link #method-focus}.
*
* @event focus
*/
/**
* An event fired when a widget is blurred.
*
* @event blur
*/
/**
* An event fired when a widget is selected.
*
* @event select
*/
/**
* An event fired when a widget is deselected.
*
* @event deselect
*/
/**
* An event fired by the {@link #method-edit} method. It can be canceled
* in order to stop the default action (opening a dialog window and/or
* {@link CKEDITOR.plugins.widget.repository#finalizeCreation finalizing widget creation}).
*
* @event edit
* @param data
* @param {String} data.dialog Defaults to {@link CKEDITOR.plugins.widget.definition#dialog}
* and can be changed or set by the listener.
*/
/**
* An event fired when a dialog window for widget editing is opened.
* This event can be canceled in order to handle the editing dialog in a custom manner.
*
* @event dialog
* @param {CKEDITOR.dialog} data The opened dialog window instance.
*/
/**
* An event fired when a key is pressed on a focused widget.
* This event is forwarded from the {@link CKEDITOR.editor#key} event and
* has the ability to block editor keystrokes if it is canceled.
*
* @event key
* @param data
* @param {Number} data.keyCode A number representing the key code (or combination).
*/
/**
* An event fired when a widget is double clicked.
*
* **Note:** If a default editing action is executed on double click (i.e. a widget has a
* {@link CKEDITOR.plugins.widget.definition#dialog dialog} defined and the {@link #event-doubleclick} event was not
* canceled), this event will be automatically canceled, so a listener added with the default priority (10)
* will not be executed. Use a listener with low priority (e.g. 5) to be sure that it will be executed.
*
* widget.on( 'doubleclick', function( evt ) {
* console.log( 'widget#doubleclick' );
* }, null, null, 5 );
*
* If your widget handles double click in a special way (so the default editing action is not executed),
* make sure you cancel this event, because otherwise it will be propagated to {@link CKEDITOR.editor#doubleclick}
* and another feature may step in (e.g. a Link dialog window may be opened if your widget was inside a link).
*
* @event doubleclick
* @param data
* @param {CKEDITOR.dom.element} data.element The double-clicked element.
*/
/**
* An event fired when the context menu is opened for a widget.
*
* @event contextMenu
* @param data The object containing context menu options to be added
* for this widget. See {@link CKEDITOR.plugins.contextMenu#addListener}.
*/
/**
* An event fired when the widget data changed. See the {@link #setData} method and the {@link #property-data} property.
*
* @event data
*/
/**
* The wrapper class for editable elements inside widgets.
*
* Do not use directly. Use {@link CKEDITOR.plugins.widget.definition#editables} or
* {@link CKEDITOR.plugins.widget#initEditable}.
*
* @class CKEDITOR.plugins.widget.nestedEditable
* @extends CKEDITOR.dom.element
* @constructor
* @param {CKEDITOR.editor} editor
* @param {CKEDITOR.dom.element} element
* @param config
* @param {CKEDITOR.filter} [config.filter]
*/
function NestedEditable( editor, element, config ) {
// Call the base constructor.
CKEDITOR.dom.element.call( this, element.$ );
this.editor = editor;
this._ = {};
var filter = this.filter = config.filter;
// If blockless editable - always use BR mode.
if ( !CKEDITOR.dtd[ this.getName() ].p )
this.enterMode = this.shiftEnterMode = CKEDITOR.ENTER_BR;
else {
this.enterMode = filter ? filter.getAllowedEnterMode( editor.enterMode ) : editor.enterMode;
this.shiftEnterMode = filter ? filter.getAllowedEnterMode( editor.shiftEnterMode, true ) : editor.shiftEnterMode;
}
}
NestedEditable.prototype = CKEDITOR.tools.extend( CKEDITOR.tools.prototypedCopy( CKEDITOR.dom.element.prototype ), {
/**
* Sets the editable data. The data will be passed through the {@link CKEDITOR.editor#dataProcessor}
* and the {@link CKEDITOR.editor#filter}. This ensures that the data was filtered and prepared to be
* edited like the {@link CKEDITOR.editor#method-setData editor data}.
*
* Before content is changed, all nested widgets are destroyed. Afterwards, after new content is loaded,
* all nested widgets are initialized.
*
* @param {String} data
*/
setData: function( data ) {
// For performance reasons don't call destroyAll when initializing a nested editable,
// because there are no widgets inside.
if ( !this._.initialSetData ) {
// Destroy all nested widgets before setting data.
this.editor.widgets.destroyAll( false, this );
}
this._.initialSetData = false;
data = this.editor.dataProcessor.toHtml( data, {
context: this.getName(),
filter: this.filter,
enterMode: this.enterMode
} );
this.setHtml( data );
this.editor.widgets.initOnAll( this );
},
/**
* Gets the editable data. Like {@link #setData}, this method will process and filter the data.
*
* @returns {String}
*/
getData: function() {
return this.editor.dataProcessor.toDataFormat( this.getHtml(), {
context: this.getName(),
filter: this.filter,
enterMode: this.enterMode
} );
}
} );
/**
* The editor instance.
*
* @readonly
* @property {CKEDITOR.editor} editor
*/
/**
* The filter instance if allowed content rules were defined.
*
* @readonly
* @property {CKEDITOR.filter} filter
*/
/**
* The enter mode active in this editable.
* It is determined from editable's name (whether it is a blockless editable),
* its allowed content rules (if defined) and the default editor's mode.
*
* @readonly
* @property {Number} enterMode
*/
/**
* The shift enter move active in this editable.
*
* @readonly
* @property {Number} shiftEnterMode
*/
//
// REPOSITORY helpers -----------------------------------------------------
//
function addWidgetButtons( editor ) {
var widgets = editor.widgets.registered,
widget,
widgetName,
widgetButton;
for ( widgetName in widgets ) {
widget = widgets[ widgetName ];
// Create button if defined.
widgetButton = widget.button;
if ( widgetButton && editor.ui.addButton ) {
editor.ui.addButton( CKEDITOR.tools.capitalize( widget.name, true ), {
label: widgetButton,
command: widget.name,
toolbar: 'insert,10'
} );
}
}
}
// Create a command creating and editing widget.
//
// @param editor
// @param {CKEDITOR.plugins.widget.definition} widgetDef
function addWidgetCommand( editor, widgetDef ) {
editor.addCommand( widgetDef.name, {
exec: function( editor, commandData ) {
var focused = editor.widgets.focused;
// If a widget of the same type is focused, start editing.
if ( focused && focused.name == widgetDef.name )
focused.edit();
// Otherwise...
// ... use insert method is was defined.
else if ( widgetDef.insert )
widgetDef.insert();
// ... or create a brand-new widget from template.
else if ( widgetDef.template ) {
var defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults,
element = CKEDITOR.dom.element.createFromHtml( widgetDef.template.output( defaults ) ),
instance,
wrapper = editor.widgets.wrapElement( element, widgetDef.name ),
temp = new CKEDITOR.dom.documentFragment( wrapper.getDocument() );
// Append wrapper to a temporary document. This will unify the environment
// in which #data listeners work when creating and editing widget.
temp.append( wrapper );
instance = editor.widgets.initOn( element, widgetDef, commandData && commandData.startupData );
// Instance could be destroyed during initialization.
// In this case finalize creation if some new widget
// was left in temporary document fragment.
if ( !instance ) {
finalizeCreation();
return;
}
// Listen on edit to finalize widget insertion.
//
// * If dialog was set, then insert widget after dialog was successfully saved or destroy this
// temporary instance.
// * If dialog wasn't set and edit wasn't canceled, insert widget.
var editListener = instance.once( 'edit', function( evt ) {
if ( evt.data.dialog ) {
instance.once( 'dialog', function( evt ) {
var dialog = evt.data,
okListener,
cancelListener;
// Finalize creation AFTER (20) new data was set.
okListener = dialog.once( 'ok', finalizeCreation, null, null, 20 );
cancelListener = dialog.once( 'cancel', function( evt ) {
if ( !( evt.data && evt.data.hide === false ) ) {
editor.widgets.destroy( instance, true );
}
} );
dialog.once( 'hide', function() {
okListener.removeListener();
cancelListener.removeListener();
} );
} );
} else {
// Dialog hasn't been set, so insert widget now.
finalizeCreation();
}
}, null, null, 999 );
instance.edit();
// Remove listener in case someone canceled it before this
// listener was executed.
editListener.removeListener();
}
function finalizeCreation() {
editor.widgets.finalizeCreation( temp );
}
},
allowedContent: widgetDef.allowedContent,
requiredContent: widgetDef.requiredContent,
contentForms: widgetDef.contentForms,
contentTransformations: widgetDef.contentTransformations
} );
}
function addWidgetProcessors( widgetsRepo, widgetDef ) {
var upcast = widgetDef.upcast,
upcasts,
priority = widgetDef.upcastPriority || 10;
if ( !upcast )
return;
// Multiple upcasts defined in string.
if ( typeof upcast == 'string' ) {
upcasts = upcast.split( ',' );
while ( upcasts.length ) {
addUpcast( widgetDef.upcasts[ upcasts.pop() ], widgetDef.name, priority );
}
}
// Single rule which is automatically activated.
else {
addUpcast( upcast, widgetDef.name, priority );
}
function addUpcast( upcast, name, priority ) {
// Find index of the first higher (in terms of value) priority upcast.
var index = CKEDITOR.tools.getIndex( widgetsRepo._.upcasts, function( element ) {
return element[ 2 ] > priority;
} );
// Add at the end if it is the highest priority so far.
if ( index < 0 ) {
index = widgetsRepo._.upcasts.length;
}
widgetsRepo._.upcasts.splice( index, 0, [ upcast, name, priority ] );
}
}
function blurWidget( widgetsRepo, widget ) {
widgetsRepo.focused = null;
if ( widget.isInited() ) {
var isDirty = widget.editor.checkDirty();
// Widget could be destroyed in the meantime - e.g. data could be set.
widgetsRepo.fire( 'widgetBlurred', { widget: widget } );
widget.setFocused( false );
!isDirty && widget.editor.resetDirty();
}
}
function checkWidgets( evt ) {
var options = evt.data;
if ( this.editor.mode != 'wysiwyg' )
return;
var editable = this.editor.editable(),
instances = this.instances,
newInstances, i, count, wrapper, notYetInitialized;
if ( !editable )
return;
// Remove widgets which have no corresponding elements in DOM.
for ( i in instances ) {
// #13410 Remove widgets that are ready. This prevents from destroying widgets that are during loading process.
if ( instances[ i ].isReady() && !editable.contains( instances[ i ].wrapper ) )
this.destroy( instances[ i ], true );
}
// Init on all (new) if initOnlyNew option was passed.
if ( options && options.initOnlyNew )
newInstances = this.initOnAll();
else {
var wrappers = editable.find( '.cke_widget_wrapper' );
newInstances = [];
// Create widgets on existing wrappers if they do not exists.
for ( i = 0, count = wrappers.count(); i < count; i++ ) {
wrapper = wrappers.getItem( i );
notYetInitialized = !this.getByElement( wrapper, true );
// Check if:
// * there's no instance for this widget
// * wrapper is not inside some temporary element like copybin (#11088)
// * it was a nested widget's wrapper which has been detached from DOM,
// when nested editable has been initialized (it overwrites its innerHTML
// and initializes nested widgets).
if ( notYetInitialized && !findParent( wrapper, isDomTemp ) && editable.contains( wrapper ) ) {
// Add cke_widget_new class because otherwise
// widget will not be created on such wrapper.
wrapper.addClass( 'cke_widget_new' );
newInstances.push( this.initOn( wrapper.getFirst( Widget.isDomWidgetElement ) ) );
}
}
}
// If only single widget was initialized and focusInited was passed, focus it.
if ( options && options.focusInited && newInstances.length == 1 )
newInstances[ 0 ].focus();
}
// Unwraps widget element and clean up element.
//
// This function is used to clean up pasted widgets.
// It should have similar result to widget#destroy plus
// some additional adjustments, specific for pasting.
//
// @param {CKEDITOR.htmlParser.element} el
function cleanUpWidgetElement( el ) {
var parent = el.parent;
if ( parent.type == CKEDITOR.NODE_ELEMENT && parent.attributes[ 'data-cke-widget-wrapper' ] )
parent.replaceWith( el );
}
// Similar to cleanUpWidgetElement, but works on DOM and finds
// widget elements by its own.
//
// Unlike cleanUpWidgetElement it will wrap element back.
//
// @param {CKEDITOR.dom.element} container
function cleanUpAllWidgetElements( widgetsRepo, container ) {
var wrappers = container.find( '.cke_widget_wrapper' ),
wrapper, element,
i = 0,
l = wrappers.count();
for ( ; i < l; ++i ) {
wrapper = wrappers.getItem( i );
element = wrapper.getFirst( Widget.isDomWidgetElement );
// If wrapper contains widget element - unwrap it and wrap again.
if ( element.type == CKEDITOR.NODE_ELEMENT && element.data( 'widget' ) ) {
element.replace( wrapper );
widgetsRepo.wrapElement( element );
} else {
// Otherwise - something is wrong... clean this up.
wrapper.remove();
}
}
}
// Creates {@link CKEDITOR.filter} instance for given widget, editable and rules.
//
// Once filter for widget-editable pair is created it is cached, so the same instance
// will be returned when method is executed again.
//
// @param {String} widgetName
// @param {String} editableName
// @param {CKEDITOR.plugins.widget.nestedEditableDefinition} editableDefinition The nested editable definition.
// @returns {CKEDITOR.filter} Filter instance or `null` if rules are not defined.
// @context CKEDITOR.plugins.widget.repository
function createEditableFilter( widgetName, editableName, editableDefinition ) {
if ( !editableDefinition.allowedContent )
return null;
var editables = this._.filters[ widgetName ];
if ( !editables )
this._.filters[ widgetName ] = editables = {};
var filter = editables[ editableName ];
if ( !filter )
editables[ editableName ] = filter = new CKEDITOR.filter( editableDefinition.allowedContent );
return filter;
}
// Creates an iterator function which when executed on all
// elements in DOM tree will gather elements that should be wrapped
// and initialized as widgets.
function createUpcastIterator( widgetsRepo ) {
var toBeWrapped = [],
upcasts = widgetsRepo._.upcasts,
upcastCallbacks = widgetsRepo._.upcastCallbacks;
return {
toBeWrapped: toBeWrapped,
iterator: function( element ) {
var upcast, upcasted,
data,
i,
upcastsLength,
upcastCallbacksLength;
// Wrapper found - find widget element, add it to be
// cleaned up (unwrapped) and wrapped and stop iterating in this branch.
if ( 'data-cke-widget-wrapper' in element.attributes ) {
element = element.getFirst( Widget.isParserWidgetElement );
if ( element )
toBeWrapped.push( [ element ] );
// Do not iterate over descendants.
return false;
}
// Widget element found - add it to be cleaned up (just in case)
// and wrapped and stop iterating in this branch.
else if ( 'data-widget' in element.attributes ) {
toBeWrapped.push( [ element ] );
// Do not iterate over descendants.
return false;
}
else if ( ( upcastsLength = upcasts.length ) ) {
// Ignore elements with data-cke-widget-upcasted to avoid multiple upcasts (#11533).
// Do not iterate over descendants.
if ( element.attributes[ 'data-cke-widget-upcasted' ] )
return false;
// Check element with upcast callbacks first.
// If any of them return false abort upcasting.
for ( i = 0, upcastCallbacksLength = upcastCallbacks.length; i < upcastCallbacksLength; ++i ) {
if ( upcastCallbacks[ i ]( element ) === false )
return;
// Return nothing in order to continue iterating over ascendants.
// See http://dev.ckeditor.com/ticket/11186#comment:6
}
for ( i = 0; i < upcastsLength; ++i ) {
upcast = upcasts[ i ];
data = {};
if ( ( upcasted = upcast[ 0 ]( element, data ) ) ) {
// If upcast function returned element, upcast this one.
// It can be e.g. a new element wrapping the original one.
if ( upcasted instanceof CKEDITOR.htmlParser.element )
element = upcasted;
// Set initial data attr with data from upcast method.
element.attributes[ 'data-cke-widget-data' ] = encodeURIComponent( JSON.stringify( data ) );
element.attributes[ 'data-cke-widget-upcasted' ] = 1;
toBeWrapped.push( [ element, upcast[ 1 ] ] );
// Do not iterate over descendants.
return false;
}
}
}
}
};
}
// Finds a first parent that matches query.
//
// @param {CKEDITOR.dom.element} element
// @param {Function} query
function findParent( element, query ) {
var parent = element;
while ( ( parent = parent.getParent() ) ) {
if ( query( parent ) )
return true;
}
return false;
}
function getWrapperAttributes( inlineWidget ) {
return {
// tabindex="-1" means that it can receive focus by code.
tabindex: -1,
contenteditable: 'false',
'data-cke-widget-wrapper': 1,
'data-cke-filter': 'off',
// Class cke_widget_new marks widgets which haven't been initialized yet.
'class': 'cke_widget_wrapper cke_widget_new cke_widget_' +
( inlineWidget ? 'inline' : 'block' )
};
}
// Inserts element at given index.
// It will check DTD and split ancestor elements up to the first
// that can contain this element.
//
// @param {CKEDITOR.htmlParser.element} parent
// @param {Number} index
// @param {CKEDITOR.htmlParser.element} element
function insertElement( parent, index, element ) {
// Do not split doc fragment...
if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
var parentAllows = CKEDITOR.dtd[ parent.name ];
// Parent element is known (included in DTD) and cannot contain
// this element.
if ( parentAllows && !parentAllows[ element.name ] ) {
var parent2 = parent.split( index ),
parentParent = parent.parent;
// Element will now be inserted at right parent's index.
index = parent2.getIndex();
// If left part of split is empty - remove it.
if ( !parent.children.length ) {
index -= 1;
parent.remove();
}
// If right part of split is empty - remove it.
if ( !parent2.children.length )
parent2.remove();
// Try inserting as grandpas' children.
return insertElement( parentParent, index, element );
}
}
// Finally we can add this element.
parent.add( element, index );
}
// Checks whether for the given widget definition and element widget should be created in inline or block mode.
//
// See also: {@link CKEDITOR.plugins.widget.definition#inline} and {@link CKEDITOR.plugins.widget#element}.
//
// @param {CKEDITOR.plugins.widget.definition} widgetDef The widget definition.
// @param {String} elementName The name of the widget element.
// @returns {Boolean}
function isWidgetInline( widgetDef, elementName ) {
return typeof widgetDef.inline == 'boolean' ? widgetDef.inline : !!CKEDITOR.dtd.$inline[ elementName ];
}
// @param {CKEDITOR.dom.element}
// @returns {Boolean}
function isDomTemp( element ) {
return element.hasAttribute( 'data-cke-temp' );
}
function onEditableKey( widget, keyCode ) {
var focusedEditable = widget.focusedEditable,
range;
// CTRL+A.
if ( keyCode == CKEDITOR.CTRL + 65 ) {
var bogus = focusedEditable.getBogus();
range = widget.editor.createRange();
range.selectNodeContents( focusedEditable );
// Exclude bogus if exists.
if ( bogus )
range.setEndAt( bogus, CKEDITOR.POSITION_BEFORE_START );
range.select();
// Cancel event - block default.
return false;
}
// DEL or BACKSPACE.
else if ( keyCode == 8 || keyCode == 46 ) {
var ranges = widget.editor.getSelection().getRanges();
range = ranges[ 0 ];
// Block del or backspace if at editable's boundary.
return !( ranges.length == 1 && range.collapsed &&
range.checkBoundaryOfElement( focusedEditable, CKEDITOR[ keyCode == 8 ? 'START' : 'END' ] ) );
}
}
function setFocusedEditable( widgetsRepo, widget, editableElement, offline ) {
var editor = widgetsRepo.editor;
editor.fire( 'lockSnapshot' );
if ( editableElement ) {
var editableName = editableElement.data( 'cke-widget-editable' ),
editableInstance = widget.editables[ editableName ];
widgetsRepo.widgetHoldingFocusedEditable = widget;
widget.focusedEditable = editableInstance;
editableElement.addClass( 'cke_widget_editable_focused' );
if ( editableInstance.filter )
editor.setActiveFilter( editableInstance.filter );
editor.setActiveEnterMode( editableInstance.enterMode, editableInstance.shiftEnterMode );
} else {
if ( !offline )
widget.focusedEditable.removeClass( 'cke_widget_editable_focused' );
widget.focusedEditable = null;
widgetsRepo.widgetHoldingFocusedEditable = null;
editor.setActiveFilter( null );
editor.setActiveEnterMode( null, null );
}
editor.fire( 'unlockSnapshot' );
}
function setupContextMenu( editor ) {
if ( !editor.contextMenu )
return;
editor.contextMenu.addListener( function( element ) {
var widget = editor.widgets.getByElement( element, true );
if ( widget )
return widget.fire( 'contextMenu', {} );
} );
}
// And now we've got two problems - original problem and RegExp.
// Some softeners:
// * FF tends to copy all blocks up to the copybin container.
// * IE tends to copy only the copybin, without its container.
// * We use spans on IE and blockless editors, but divs in other cases.
var pasteReplaceRegex = new RegExp(
'^' +
'(?:<(?:div|span)(?: data-cke-temp="1")?(?: id="cke_copybin")?(?: data-cke-temp="1")?>)?' +
'(?:<(?:div|span)(?: style="[^"]+")?>)?' +
'<span [^>]*data-cke-copybin-start="1"[^>]*>.?</span>([\\s\\S]+)<span [^>]*data-cke-copybin-end="1"[^>]*>.?</span>' +
'(?:</(?:div|span)>)?' +
'(?:</(?:div|span)>)?' +
'$',
// IE8 prefers uppercase when browsers stick to lowercase HTML (#13460).
'i'
);
function pasteReplaceFn( match, wrapperHtml ) {
// Avoid polluting pasted data with any whitspaces,
// what's going to break check whether only one widget was pasted.
return CKEDITOR.tools.trim( wrapperHtml );
}
function setupDragAndDrop( widgetsRepo ) {
var editor = widgetsRepo.editor,
lineutils = CKEDITOR.plugins.lineutils;
// These listeners handle inline and block widgets drag and drop.
// The only thing we need to do to make block widgets custom drag and drop functionality
// is to fire those events with the right properties (like the target which must be the drag handle).
editor.on( 'dragstart', function( evt ) {
var target = evt.data.target;
if ( Widget.isDomDragHandler( target ) ) {
var widget = widgetsRepo.getByElement( target );
evt.data.dataTransfer.setData( 'cke/widget-id', widget.id );
// IE needs focus.
editor.focus();
// and widget need to be focused on drag start (#12172#comment:10).
widget.focus();
}
} );
editor.on( 'drop', function( evt ) {
var dataTransfer = evt.data.dataTransfer,
id = dataTransfer.getData( 'cke/widget-id' ),
transferType = dataTransfer.getTransferType( editor ),
dragRange = editor.createRange(),
sourceWidget;
// Disable cross-editor drag & drop for widgets - #13599.
if ( id !== '' && transferType === CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
evt.cancel();
return;
}
if ( id === '' || transferType != CKEDITOR.DATA_TRANSFER_INTERNAL ) {
return;
}
sourceWidget = widgetsRepo.instances[ id ];
if ( !sourceWidget ) {
return;
}
dragRange.setStartBefore( sourceWidget.wrapper );
dragRange.setEndAfter( sourceWidget.wrapper );
evt.data.dragRange = dragRange;
// [IE8-9] Reset state of the clipboard#fixSplitNodesAfterDrop fix because by setting evt.data.dragRange
// (see above) after drop happened we do not need it. That fix is needed only if dragRange was created
// before drop (before text node was split).
delete CKEDITOR.plugins.clipboard.dragStartContainerChildCount;
delete CKEDITOR.plugins.clipboard.dragEndContainerChildCount;
evt.data.dataTransfer.setData( 'text/html', editor.editable().getHtmlFromRange( dragRange ).getHtml() );
editor.widgets.destroy( sourceWidget, true );
} );
editor.on( 'contentDom', function() {
var editable = editor.editable();
// Register Lineutils's utilities as properties of repo.
CKEDITOR.tools.extend( widgetsRepo, {
finder: new lineutils.finder( editor, {
lookups: {
// Element is block but not list item and not in nested editable.
'default': function( el ) {
if ( el.is( CKEDITOR.dtd.$listItem ) )
return;
if ( !el.is( CKEDITOR.dtd.$block ) )
return;
// Allow drop line inside, but never before or after nested editable (#12006).
if ( Widget.isDomNestedEditable( el ) )
return;
// Do not allow droping inside the widget being dragged (#13397).
if ( widgetsRepo._.draggedWidget.wrapper.contains( el ) ) {
return;
}
// If element is nested editable, make sure widget can be dropped there (#12006).
var nestedEditable = Widget.getNestedEditable( editable, el );
if ( nestedEditable ) {
var draggedWidget = widgetsRepo._.draggedWidget;
// Don't let the widget to be dropped into its own nested editable.
if ( widgetsRepo.getByElement( nestedEditable ) == draggedWidget )
return;
var filter = CKEDITOR.filter.instances[ nestedEditable.data( 'cke-filter' ) ],
draggedRequiredContent = draggedWidget.requiredContent;
// There will be no relation if the filter of nested editable does not allow
// requiredContent of dragged widget.
if ( filter && draggedRequiredContent && !filter.check( draggedRequiredContent ) )
return;
}
return CKEDITOR.LINEUTILS_BEFORE | CKEDITOR.LINEUTILS_AFTER;
}
}
} ),
locator: new lineutils.locator( editor ),
liner: new lineutils.liner( editor, {
lineStyle: {
cursor: 'move !important',
'border-top-color': '#666'
},
tipLeftStyle: {
'border-left-color': '#666'
},
tipRightStyle: {
'border-right-color': '#666'
}
} )
}, true );
} );
}
// Setup mouse observer which will trigger:
// * widget focus on widget click,
// * widget#doubleclick forwarded from editor#doubleclick.
function setupMouseObserver( widgetsRepo ) {
var editor = widgetsRepo.editor;
editor.on( 'contentDom', function() {
var editable = editor.editable(),
evtRoot = editable.isInline() ? editable : editor.document,
widget,
mouseDownOnDragHandler;
editable.attachListener( evtRoot, 'mousedown', function( evt ) {
var target = evt.data.getTarget();
// #10887 Clicking scrollbar in IE8 will invoke event with empty target object.
if ( !target.type )
return false;
widget = widgetsRepo.getByElement( target );
mouseDownOnDragHandler = 0; // Reset.
// Widget was clicked, but not editable nested in it.
if ( widget ) {
// Ignore mousedown on drag and drop handler if the widget is inline.
// Block widgets are handled by Lineutils.
if ( widget.inline && target.type == CKEDITOR.NODE_ELEMENT && target.hasAttribute( 'data-cke-widget-drag-handler' ) ) {
mouseDownOnDragHandler = 1;
// When drag handler is pressed we have to clear current selection if it wasn't already on this widget.
// Otherwise, the selection may be in a fillingChar, which prevents dragging a widget. (#13284, see comment 8 and 9.)
if ( widgetsRepo.focused != widget )
editor.getSelection().removeAllRanges();
return;
}
if ( !Widget.getNestedEditable( widget.wrapper, target ) ) {
evt.data.preventDefault();
if ( !CKEDITOR.env.ie )
widget.focus();
} else {
// Reset widget so mouseup listener is not confused.
widget = null;
}
}
} );
// Focus widget on mouseup if mousedown was fired on drag handler.
// Note: mouseup won't be fired at all if widget was dragged and dropped, so
// this code will be executed only when drag handler was clicked.
editable.attachListener( evtRoot, 'mouseup', function() {
// Check if widget is not destroyed (if widget is destroyed the wrapper will be null).
if ( mouseDownOnDragHandler && widget && widget.wrapper ) {
mouseDownOnDragHandler = 0;
widget.focus();
}
} );
// On IE it is not enough to block mousedown. If widget wrapper (element with
// contenteditable=false attribute) is clicked directly (it is a target),
// then after mouseup/click IE will select that element.
// It is not possible to prevent that default action,
// so we force fake selection after everything happened.
if ( CKEDITOR.env.ie ) {
editable.attachListener( evtRoot, 'mouseup', function() {
setTimeout( function() {
// Check if widget is not destroyed (if widget is destroyed the wrapper will be null) and
// in editable contains widget (it could be dragged and removed).
if ( widget && widget.wrapper && editable.contains( widget.wrapper ) ) {
widget.focus();
widget = null;
}
} );
} );
}
} );
editor.on( 'doubleclick', function( evt ) {
var widget = widgetsRepo.getByElement( evt.data.element );
// Not in widget or in nested editable.
if ( !widget || Widget.getNestedEditable( widget.wrapper, evt.data.element ) )
return;
return widget.fire( 'doubleclick', { element: evt.data.element } );
}, null, null, 1 );
}
// Setup editor#key observer which will forward it
// to focused widget.
function setupKeyboardObserver( widgetsRepo ) {
var editor = widgetsRepo.editor;
editor.on( 'key', function( evt ) {
var focused = widgetsRepo.focused,
widgetHoldingFocusedEditable = widgetsRepo.widgetHoldingFocusedEditable,
ret;
if ( focused )
ret = focused.fire( 'key', { keyCode: evt.data.keyCode } );
else if ( widgetHoldingFocusedEditable )
ret = onEditableKey( widgetHoldingFocusedEditable, evt.data.keyCode );
return ret;
}, null, null, 1 );
}
// Setup copybin on native copy and cut events in order to handle copy and cut commands
// if user accepted security alert on IEs.
// Note: when copying or cutting using keystroke, copySingleWidget will be first executed
// by the keydown listener. Conflict between two calls will be resolved by copy_bin existence check.
function setupNativeCutAndCopy( widgetsRepo ) {
var editor = widgetsRepo.editor;
editor.on( 'contentDom', function() {
var editable = editor.editable();
editable.attachListener( editable, 'copy', eventListener );
editable.attachListener( editable, 'cut', eventListener );
} );
function eventListener( evt ) {
if ( widgetsRepo.focused )
copySingleWidget( widgetsRepo.focused, evt.name == 'cut' );
}
}
// Setup selection observer which will trigger:
// * widget select & focus on selection change,
// * nested editable focus (related properites and classes) on selection change,
// * deselecting and blurring all widgets on data,
// * blurring widget on editor blur.
function setupSelectionObserver( widgetsRepo ) {
var editor = widgetsRepo.editor;
editor.on( 'selectionCheck', function() {
widgetsRepo.fire( 'checkSelection' );
} );
widgetsRepo.on( 'checkSelection', widgetsRepo.checkSelection, widgetsRepo );
editor.on( 'selectionChange', function( evt ) {
var nestedEditable = Widget.getNestedEditable( editor.editable(), evt.data.selection.getStartElement() ),
newWidget = nestedEditable && widgetsRepo.getByElement( nestedEditable ),
oldWidget = widgetsRepo.widgetHoldingFocusedEditable;
if ( oldWidget ) {
if ( oldWidget !== newWidget || !oldWidget.focusedEditable.equals( nestedEditable ) ) {
setFocusedEditable( widgetsRepo, oldWidget, null );
if ( newWidget && nestedEditable )
setFocusedEditable( widgetsRepo, newWidget, nestedEditable );
}
}
// It may happen that there's no widget even if editable was found -
// e.g. if selection was automatically set in editable although widget wasn't initialized yet.
else if ( newWidget && nestedEditable ) {
setFocusedEditable( widgetsRepo, newWidget, nestedEditable );
}
} );
// Invalidate old widgets early - immediately on dataReady.
editor.on( 'dataReady', function() {
// Deselect and blur all widgets.
stateUpdater( widgetsRepo ).commit();
} );
editor.on( 'blur', function() {
var widget;
if ( ( widget = widgetsRepo.focused ) )
blurWidget( widgetsRepo, widget );
if ( ( widget = widgetsRepo.widgetHoldingFocusedEditable ) )
setFocusedEditable( widgetsRepo, widget, null );
} );
}
// Set up actions like:
// * processing in toHtml/toDataFormat,
// * pasting handling,
// * insertion handling,
// * editable reload handling (setData, mode switch, undo/redo),
// * DOM invalidation handling,
// * widgets checks.
function setupWidgetsLifecycle( widgetsRepo ) {
setupWidgetsLifecycleStart( widgetsRepo );
setupWidgetsLifecycleEnd( widgetsRepo );
widgetsRepo.on( 'checkWidgets', checkWidgets );
widgetsRepo.editor.on( 'contentDomInvalidated', widgetsRepo.checkWidgets, widgetsRepo );
}
function setupWidgetsLifecycleEnd( widgetsRepo ) {
var editor = widgetsRepo.editor,
downcastingSessions = {};
// Listen before htmlDP#htmlFilter is applied to cache all widgets, because we'll
// loose data-cke-* attributes.
editor.on( 'toDataFormat', function( evt ) {
// To avoid conflicts between htmlDP#toDF calls done at the same time
// (e.g. nestedEditable#getData called during downcasting some widget)
// mark every toDataFormat event chain with the downcasting session id.
var id = CKEDITOR.tools.getNextNumber(),
toBeDowncasted = [];
evt.data.downcastingSessionId = id;
downcastingSessions[ id ] = toBeDowncasted;
evt.data.dataValue.forEach( function( element ) {
var attrs = element.attributes,
widget, widgetElement;
// Wrapper.
// Perform first part of downcasting (cleanup) and cache widgets,
// because after applying DP's filter all data-cke-* attributes will be gone.
if ( 'data-cke-widget-id' in attrs ) {
widget = widgetsRepo.instances[ attrs[ 'data-cke-widget-id' ] ];
if ( widget ) {
widgetElement = element.getFirst( Widget.isParserWidgetElement );
toBeDowncasted.push( {
wrapper: element,
element: widgetElement,
widget: widget,
editables: {}
} );
// If widget did not have data-cke-widget attribute before upcasting remove it.
if ( widgetElement.attributes[ 'data-cke-widget-keep-attr' ] != '1' )
delete widgetElement.attributes[ 'data-widget' ];
}
}
// Nested editable.
else if ( 'data-cke-widget-editable' in attrs ) {
// Save the reference to this nested editable in the closest widget to be downcasted.
// Nested editables are downcasted in the successive toDataFormat to create an opportunity
// for dataFilter's "excludeNestedEditable" option to do its job (that option relies on
// contenteditable="true" attribute) (#11372).
toBeDowncasted[ toBeDowncasted.length - 1 ].editables[ attrs[ 'data-cke-widget-editable' ] ] = element;
// Don't check children - there won't be next wrapper or nested editable which we
// should process in this session.
return false;
}
}, CKEDITOR.NODE_ELEMENT, true );
}, null, null, 8 );
// Listen after dataProcessor.htmlFilter and ACF were applied
// so wrappers securing widgets' contents are removed after all filtering was done.
editor.on( 'toDataFormat', function( evt ) {
// Ignore some unmarked sessions.
if ( !evt.data.downcastingSessionId )
return;
var toBeDowncasted = downcastingSessions[ evt.data.downcastingSessionId ],
toBe, widget, widgetElement, retElement, editableElement, e;
while ( ( toBe = toBeDowncasted.shift() ) ) {
widget = toBe.widget;
widgetElement = toBe.element;
retElement = widget._.downcastFn && widget._.downcastFn.call( widget, widgetElement );
// Replace nested editables' content with their output data.
for ( e in toBe.editables ) {
editableElement = toBe.editables[ e ];
delete editableElement.attributes.contenteditable;
editableElement.setHtml( widget.editables[ e ].getData() );
}
// Returned element always defaults to widgetElement.
if ( !retElement )
retElement = widgetElement;
toBe.wrapper.replaceWith( retElement );
}
}, null, null, 13 );
editor.on( 'contentDomUnload', function() {
widgetsRepo.destroyAll( true );
} );
}
function setupWidgetsLifecycleStart( widgetsRepo ) {
var editor = widgetsRepo.editor,
processedWidgetOnly,
snapshotLoaded;
// Listen after ACF (so data are filtered),
// but before dataProcessor.dataFilter was applied (so we can secure widgets' internals).
editor.on( 'toHtml', function( evt ) {
var upcastIterator = createUpcastIterator( widgetsRepo ),
toBeWrapped;
evt.data.dataValue.forEach( upcastIterator.iterator, CKEDITOR.NODE_ELEMENT, true );
// Clean up and wrap all queued elements.
while ( ( toBeWrapped = upcastIterator.toBeWrapped.pop() ) ) {
cleanUpWidgetElement( toBeWrapped[ 0 ] );
widgetsRepo.wrapElement( toBeWrapped[ 0 ], toBeWrapped[ 1 ] );
}
// Used to determine whether only widget was pasted.
if ( evt.data.protectedWhitespaces ) {
// Whitespaces are protected by wrapping content with spans. Take the middle node only.
processedWidgetOnly = evt.data.dataValue.children.length == 3 &&
Widget.isParserWidgetWrapper( evt.data.dataValue.children[ 1 ] );
} else {
processedWidgetOnly = evt.data.dataValue.children.length == 1 &&
Widget.isParserWidgetWrapper( evt.data.dataValue.children[ 0 ] );
}
}, null, null, 8 );
editor.on( 'dataReady', function() {
// Clean up all widgets loaded from snapshot.
if ( snapshotLoaded )
cleanUpAllWidgetElements( widgetsRepo, editor.editable() );
snapshotLoaded = 0;
// Some widgets were destroyed on contentDomUnload,
// some on loadSnapshot, but that does not include
// e.g. setHtml on inline editor or widgets removed just
// before setting data.
widgetsRepo.destroyAll( true );
widgetsRepo.initOnAll();
} );
// Set flag so dataReady will know that additional
// cleanup is needed, because snapshot containing widgets was loaded.
editor.on( 'loadSnapshot', function( evt ) {
// Primitive but sufficient check which will prevent from executing
// heavier cleanUpAllWidgetElements if not needed.
if ( ( /data-cke-widget/ ).test( evt.data ) )
snapshotLoaded = 1;
widgetsRepo.destroyAll( true );
}, null, null, 9 );
// Handle pasted single widget.
editor.on( 'paste', function( evt ) {
var data = evt.data;
data.dataValue = data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn );
// If drag'n'drop kind of paste into nested editable (data.range), selection is set AFTER
// data is pasted, which means editor has no chance to change activeFilter's context.
// As a result, pasted data is filtered with default editor's filter instead of NE's and
// funny things get inserted. Changing the filter by analysis of the paste range below (#13186).
if ( data.range ) {
// Check if pasting into nested editable.
var nestedEditable = Widget.getNestedEditable( editor.editable(), data.range.startContainer );
if ( nestedEditable ) {
// Retrieve the filter from NE's data and set it active before editor.insertHtml is done
// in clipboard plugin.
var filter = CKEDITOR.filter.instances[ nestedEditable.data( 'cke-filter' ) ];
if ( filter ) {
editor.setActiveFilter( filter );
}
}
}
} );
// Listen with high priority to check widgets after data was inserted.
editor.on( 'afterInsertHtml', function( evt ) {
if ( evt.data.intoRange ) {
widgetsRepo.checkWidgets( { initOnlyNew: true } );
} else {
editor.fire( 'lockSnapshot' );
// Init only new for performance reason.
// Focus inited if only widget was processed.
widgetsRepo.checkWidgets( { initOnlyNew: true, focusInited: processedWidgetOnly } );
editor.fire( 'unlockSnapshot' );
}
} );
}
// Helper for coordinating which widgets should be
// selected/deselected and which one should be focused/blurred.
function stateUpdater( widgetsRepo ) {
var currentlySelected = widgetsRepo.selected,
toBeSelected = [],
toBeDeselected = currentlySelected.slice( 0 ),
focused = null;
return {
select: function( widget ) {
if ( CKEDITOR.tools.indexOf( currentlySelected, widget ) < 0 )
toBeSelected.push( widget );
var index = CKEDITOR.tools.indexOf( toBeDeselected, widget );
if ( index >= 0 )
toBeDeselected.splice( index, 1 );
return this;
},
focus: function( widget ) {
focused = widget;
return this;
},
commit: function() {
var focusedChanged = widgetsRepo.focused !== focused,
widget, isDirty;
widgetsRepo.editor.fire( 'lockSnapshot' );
if ( focusedChanged && ( widget = widgetsRepo.focused ) )
blurWidget( widgetsRepo, widget );
while ( ( widget = toBeDeselected.pop() ) ) {
currentlySelected.splice( CKEDITOR.tools.indexOf( currentlySelected, widget ), 1 );
// Widget could be destroyed in the meantime - e.g. data could be set.
if ( widget.isInited() ) {
isDirty = widget.editor.checkDirty();
widget.setSelected( false );
!isDirty && widget.editor.resetDirty();
}
}
if ( focusedChanged && focused ) {
isDirty = widgetsRepo.editor.checkDirty();
widgetsRepo.focused = focused;
widgetsRepo.fire( 'widgetFocused', { widget: focused } );
focused.setFocused( true );
!isDirty && widgetsRepo.editor.resetDirty();
}
while ( ( widget = toBeSelected.pop() ) ) {
currentlySelected.push( widget );
widget.setSelected( true );
}
widgetsRepo.editor.fire( 'unlockSnapshot' );
}
};
}
//
// WIDGET helpers ---------------------------------------------------------
//
// LEFT, RIGHT, UP, DOWN, DEL, BACKSPACE - unblock default fake sel handlers.
var keystrokesNotBlockedByWidget = { 37: 1, 38: 1, 39: 1, 40: 1, 8: 1, 46: 1 };
// Applies or removes style's classes from widget.
// @param {CKEDITOR.style} style Custom widget style.
// @param {Boolean} apply Whether to apply or remove style.
function applyRemoveStyle( widget, style, apply ) {
var changed = 0,
classes = getStyleClasses( style ),
updatedClasses = widget.data.classes || {},
cl;
// Ee... Something is wrong with this style.
if ( !classes )
return;
// Clone, because we need to break reference.
updatedClasses = CKEDITOR.tools.clone( updatedClasses );
while ( ( cl = classes.pop() ) ) {
if ( apply ) {
if ( !updatedClasses[ cl ] )
changed = updatedClasses[ cl ] = 1;
} else {
if ( updatedClasses[ cl ] ) {
delete updatedClasses[ cl ];
changed = 1;
}
}
}
if ( changed )
widget.setData( 'classes', updatedClasses );
}
function cancel( evt ) {
evt.cancel();
}
function copySingleWidget( widget, isCut ) {
var editor = widget.editor,
doc = editor.document;
// We're still handling previous copy/cut.
// When keystroke is used to copy/cut this will also prevent
// conflict with copySingleWidget called again for native copy/cut event.
if ( doc.getById( 'cke_copybin' ) )
return;
// [IE] Use span for copybin and its container to avoid bug with expanding editable height by
// absolutely positioned element.
var copybinName = ( editor.blockless || CKEDITOR.env.ie ) ? 'span' : 'div',
copybin = doc.createElement( copybinName ),
copybinContainer = doc.createElement( copybinName ),
// IE8 always jumps to the end of document.
needsScrollHack = CKEDITOR.env.ie && CKEDITOR.env.version < 9;
copybinContainer.setAttributes( {
id: 'cke_copybin',
'data-cke-temp': '1'
} );
// Position copybin element outside current viewport.
copybin.setStyles( {
position: 'absolute',
width: '1px',
height: '1px',
overflow: 'hidden'
} );
copybin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-5000px' );
var range = editor.createRange();
range.setStartBefore( widget.wrapper );
range.setEndAfter( widget.wrapper );
copybin.setHtml(
'<span data-cke-copybin-start="1">\u200b</span>' +
editor.editable().getHtmlFromRange( range ).getHtml() +
'<span data-cke-copybin-end="1">\u200b</span>' );
// Save snapshot with the current state.
editor.fire( 'saveSnapshot' );
// Ignore copybin.
editor.fire( 'lockSnapshot' );
copybinContainer.append( copybin );
editor.editable().append( copybinContainer );
var listener1 = editor.on( 'selectionChange', cancel, null, null, 0 ),
listener2 = widget.repository.on( 'checkSelection', cancel, null, null, 0 );
if ( needsScrollHack ) {
var docElement = doc.getDocumentElement().$,
scrollTop = docElement.scrollTop;
}
// Once the clone of the widget is inside of copybin, select
// the entire contents. This selection will be copied by the
// native browser's clipboard system.
range = editor.createRange();
range.selectNodeContents( copybin );
range.select();
if ( needsScrollHack )
docElement.scrollTop = scrollTop;
setTimeout( function() {
// [IE] Focus widget before removing copybin to avoid scroll jump.
if ( !isCut )
widget.focus();
copybinContainer.remove();
listener1.removeListener();
listener2.removeListener();
editor.fire( 'unlockSnapshot' );
if ( isCut ) {
widget.repository.del( widget );
editor.fire( 'saveSnapshot' );
}
}, 100 ); // Use 100ms, so Chrome (@Mac) will be able to grab the content.
}
// Extracts classes array from style instance.
function getStyleClasses( style ) {
var attrs = style.getDefinition().attributes,
classes = attrs && attrs[ 'class' ];
return classes ? classes.split( /\s+/ ) : null;
}
// [IE] Force keeping focus because IE sometimes forgets to fire focus on main editable
// when blurring nested editable.
// @context widget
function onEditableBlur() {
var active = CKEDITOR.document.getActive(),
editor = this.editor,
editable = editor.editable();
// If focus stays within editor override blur and set currentActive because it should be
// automatically changed to editable on editable#focus but it is not fired.
if ( ( editable.isInline() ? editable : editor.document.getWindow().getFrame() ).equals( active ) )
editor.focusManager.focus( editable );
}
// Force selectionChange when editable was focused.
// Similar to hack in selection.js#~620.
// @context widget
function onEditableFocus() {
// Gecko does not support 'DOMFocusIn' event on which we unlock selection
// in selection.js to prevent selection locking when entering nested editables.
if ( CKEDITOR.env.gecko )
this.editor.unlockSelection();
// We don't need to force selectionCheck on Webkit, because on Webkit
// we do that on DOMFocusIn in selection.js.
if ( !CKEDITOR.env.webkit ) {
this.editor.forceNextSelectionCheck();
this.editor.selectionChange( 1 );
}
}
// Setup listener on widget#data which will update (remove/add) classes
// by comparing newly set classes with the old ones.
function setupDataClassesListener( widget ) {
// Note: previousClasses and newClasses may be null!
// Tip: for ( cl in null ) is correct.
var previousClasses = null;
widget.on( 'data', function() {
var newClasses = this.data.classes,
cl;
// When setting new classes one need to remember
// that he must break reference.
if ( previousClasses == newClasses )
return;
for ( cl in previousClasses ) {
// Avoid removing and adding classes again.
if ( !( newClasses && newClasses[ cl ] ) )
this.removeClass( cl );
}
for ( cl in newClasses )
this.addClass( cl );
previousClasses = newClasses;
} );
}
function setupDragHandler( widget ) {
if ( !widget.draggable )
return;
var editor = widget.editor,
// Use getLast to find wrapper's direct descendant (#12022).
container = widget.wrapper.getLast( Widget.isDomDragHandlerContainer ),
img;
// Reuse drag handler if already exists (#11281).
if ( container )
img = container.findOne( 'img' );
else {
container = new CKEDITOR.dom.element( 'span', editor.document );
container.setAttributes( {
'class': 'cke_reset cke_widget_drag_handler_container',
// Split background and background-image for IE8 which will break on rgba().
style: 'background:rgba(220,220,220,0.5);background-image:url(' + editor.plugins.widget.path + 'images/handle.png)'
} );
img = new CKEDITOR.dom.element( 'img', editor.document );
img.setAttributes( {
'class': 'cke_reset cke_widget_drag_handler',
'data-cke-widget-drag-handler': '1',
src: CKEDITOR.tools.transparentImageData,
width: DRAG_HANDLER_SIZE,
title: editor.lang.widget.move,
height: DRAG_HANDLER_SIZE
} );
widget.inline && img.setAttribute( 'draggable', 'true' );
container.append( img );
widget.wrapper.append( container );
}
// Preventing page reload when dropped content on widget wrapper (#13015).
// Widget is not editable so by default drop on it isn't allowed what means that
// browser handles it (there's no editable#drop event). If there's no drop event we cannot block
// the drop, so page is reloaded. This listener enables drop on widget wrappers.
widget.wrapper.on( 'dragover', function( evt ) {
evt.data.preventDefault();
} );
widget.wrapper.on( 'mouseenter', widget.updateDragHandlerPosition, widget );
setTimeout( function() {
widget.on( 'data', widget.updateDragHandlerPosition, widget );
}, 50 );
if ( !widget.inline ) {
img.on( 'mousedown', onBlockWidgetDrag, widget );
// On IE8 'dragstart' is propagated to editable, so editor#dragstart is fired twice on block widgets.
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
img.on( 'dragstart', function( evt ) {
evt.data.preventDefault( true );
} );
}
}
widget.dragHandlerContainer = container;
}
function onBlockWidgetDrag( evt ) {
var finder = this.repository.finder,
locator = this.repository.locator,
liner = this.repository.liner,
editor = this.editor,
editable = editor.editable(),
listeners = [],
sorted = [];
// Mark dragged widget for repository#finder.
this.repository._.draggedWidget = this;
// Harvest all possible relations and display some closest.
var relations = finder.greedySearch(),
buffer = CKEDITOR.tools.eventsBuffer( 50, function() {
locations = locator.locate( relations );
// There's only a single line displayed for D&D.
sorted = locator.sort( y, 1 );
if ( sorted.length ) {
liner.prepare( relations, locations );
liner.placeLine( sorted[ 0 ] );
liner.cleanup();
}
} ),
locations, y;
// Let's have the "dragging cursor" over entire editable.
editable.addClass( 'cke_widget_dragging' );
// Cache mouse position so it is re-used in events buffer.
listeners.push( editable.on( 'mousemove', function( evt ) {
y = evt.data.$.clientY;
buffer.input();
} ) );
// Fire drag start as it happens during the native D&D.
editor.fire( 'dragstart', { target: evt.sender } );
function onMouseUp() {
var l;
buffer.reset();
// Stop observing events.
while ( ( l = listeners.pop() ) )
l.removeListener();
onBlockWidgetDrop.call( this, sorted, evt.sender );
}
// Mouseup means "drop". This is when the widget is being detached
// from DOM and placed at range determined by the line (location).
listeners.push( editor.document.once( 'mouseup', onMouseUp, this ) );
// Prevent calling 'onBlockWidgetDrop' twice in the inline editor.
// `removeListener` does not work if it is called at the same time event is fired.
if ( !editable.isInline() ) {
// Mouseup may occur when user hovers the line, which belongs to
// the outer document. This is, of course, a valid listener too.
listeners.push( CKEDITOR.document.once( 'mouseup', onMouseUp, this ) );
}
}
function onBlockWidgetDrop( sorted, dragTarget ) {
var finder = this.repository.finder,
liner = this.repository.liner,
editor = this.editor,
editable = this.editor.editable();
if ( !CKEDITOR.tools.isEmpty( liner.visible ) ) {
// Retrieve range for the closest location.
var dropRange = finder.getRange( sorted[ 0 ] );
// Focus widget (it could lost focus after mousedown+mouseup)
// and save this state as the one where we want to be taken back when undoing.
this.focus();
// Drag range will be set in the drop listener.
editor.fire( 'drop', {
dropRange: dropRange,
target: dropRange.startContainer
} );
}
// Clean-up custom cursor for editable.
editable.removeClass( 'cke_widget_dragging' );
// Clean-up all remaining lines.
liner.hideVisible();
// Clean-up drag & drop.
editor.fire( 'dragend', { target: dragTarget } );
}
function setupEditables( widget ) {
var editableName,
editableDef,
definedEditables = widget.editables;
widget.editables = {};
if ( !widget.editables )
return;
for ( editableName in definedEditables ) {
editableDef = definedEditables[ editableName ];
widget.initEditable( editableName, typeof editableDef == 'string' ? { selector: editableDef } : editableDef );
}
}
function setupMask( widget ) {
if ( !widget.mask )
return;
// Reuse mask if already exists (#11281).
var img = widget.wrapper.findOne( '.cke_widget_mask' );
if ( !img ) {
img = new CKEDITOR.dom.element( 'img', widget.editor.document );
img.setAttributes( {
src: CKEDITOR.tools.transparentImageData,
'class': 'cke_reset cke_widget_mask'
} );
widget.wrapper.append( img );
}
widget.mask = img;
}
// Replace parts object containing:
// partName => selector pairs
// with:
// partName => element pairs
function setupParts( widget ) {
if ( widget.parts ) {
var parts = {},
el, partName;
for ( partName in widget.parts ) {
el = widget.wrapper.findOne( widget.parts[ partName ] );
parts[ partName ] = el;
}
widget.parts = parts;
}
}
function setupWidget( widget, widgetDef ) {
setupWrapper( widget );
setupParts( widget );
setupEditables( widget );
setupMask( widget );
setupDragHandler( widget );
setupDataClassesListener( widget );
// #11145: [IE8] Non-editable content of widget is draggable.
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
widget.wrapper.on( 'dragstart', function( evt ) {
var target = evt.data.getTarget();
// Allow text dragging inside nested editables or dragging inline widget's drag handler.
if ( !Widget.getNestedEditable( widget, target ) && !( widget.inline && Widget.isDomDragHandler( target ) ) )
evt.data.preventDefault();
} );
}
widget.wrapper.removeClass( 'cke_widget_new' );
widget.element.addClass( 'cke_widget_element' );
widget.on( 'key', function( evt ) {
var keyCode = evt.data.keyCode;
// ENTER.
if ( keyCode == 13 ) {
widget.edit();
// CTRL+C or CTRL+X.
} else if ( keyCode == CKEDITOR.CTRL + 67 || keyCode == CKEDITOR.CTRL + 88 ) {
copySingleWidget( widget, keyCode == CKEDITOR.CTRL + 88 );
return; // Do not preventDefault.
} else if ( keyCode in keystrokesNotBlockedByWidget || ( CKEDITOR.CTRL & keyCode ) || ( CKEDITOR.ALT & keyCode ) ) {
// Pass chosen keystrokes to other plugins or default fake sel handlers.
// Pass all CTRL/ALT keystrokes.
return;
}
return false;
}, null, null, 999 );
// Listen with high priority so it's possible
// to overwrite this callback.
widget.on( 'doubleclick', function( evt ) {
if ( widget.edit() ) {
// We have to cancel event if edit method opens a dialog, otherwise
// link plugin may open extra dialog (#12140).
evt.cancel();
}
} );
if ( widgetDef.data )
widget.on( 'data', widgetDef.data );
if ( widgetDef.edit )
widget.on( 'edit', widgetDef.edit );
}
function setupWidgetData( widget, startupData ) {
var widgetDataAttr = widget.element.data( 'cke-widget-data' );
if ( widgetDataAttr )
widget.setData( JSON.parse( decodeURIComponent( widgetDataAttr ) ) );
if ( startupData )
widget.setData( startupData );
// Populate classes if they are not preset.
if ( !widget.data.classes )
widget.setData( 'classes', widget.getClasses() );
// Unblock data and...
widget.dataReady = true;
// Write data to element because this was blocked when data wasn't ready.
writeDataToElement( widget );
// Fire data event first time, because this was blocked when data wasn't ready.
widget.fire( 'data', widget.data );
}
function setupWrapper( widget ) {
// Retrieve widget wrapper. Assign an id to it.
var wrapper = widget.wrapper = widget.element.getParent();
wrapper.setAttribute( 'data-cke-widget-id', widget.id );
}
function writeDataToElement( widget ) {
widget.element.data( 'cke-widget-data', encodeURIComponent( JSON.stringify( widget.data ) ) );
}
//
// WIDGET STYLE HANDLER ---------------------------------------------------
//
( function() {
/**
* The class representing a widget style. It is an {@link CKEDITOR#STYLE_OBJECT object} like
* the styles handler for widgets.
*
* **Note:** This custom style handler does not support all methods of the {@link CKEDITOR.style} class.
* Not supported methods: {@link #applyToRange}, {@link #removeFromRange}, {@link #applyToObject}.
*
* @since 4.4
* @class CKEDITOR.style.customHandlers.widget
* @extends CKEDITOR.style
*/
CKEDITOR.style.addCustomHandler( {
type: 'widget',
setup: function( styleDefinition ) {
/**
* The name of widget to which this style can be applied.
* It is extracted from style definition's `widget` property.
*
* @property {String} widget
*/
this.widget = styleDefinition.widget;
},
apply: function( editor ) {
// Before CKEditor 4.4 wasn't a required argument, so we need to
// handle a case when it wasn't provided.
if ( !( editor instanceof CKEDITOR.editor ) )
return;
// Theoretically we could bypass checkApplicable, get widget from
// widgets.focused and check its name, what would be faster, but then
// this custom style would work differently than the default style
// which checks if it's applicable before applying or removeing itself.
if ( this.checkApplicable( editor.elementPath(), editor ) )
editor.widgets.focused.applyStyle( this );
},
remove: function( editor ) {
// Before CKEditor 4.4 wasn't a required argument, so we need to
// handle a case when it wasn't provided.
if ( !( editor instanceof CKEDITOR.editor ) )
return;
if ( this.checkApplicable( editor.elementPath(), editor ) )
editor.widgets.focused.removeStyle( this );
},
checkActive: function( elementPath, editor ) {
return this.checkElementMatch( elementPath.lastElement, 0, editor );
},
checkApplicable: function( elementPath, editor ) {
// Before CKEditor 4.4 wasn't a required argument, so we need to
// handle a case when it wasn't provided.
if ( !( editor instanceof CKEDITOR.editor ) )
return false;
return this.checkElement( elementPath.lastElement );
},
checkElementMatch: checkElementMatch,
checkElementRemovable: checkElementMatch,
/**
* Checks if an element is a {@link CKEDITOR.plugins.widget#wrapper wrapper} of a
* widget whose name matches the {@link #widget widget name} specified in the style definition.
*
* @param {CKEDITOR.dom.element} element
* @returns {Boolean}
*/
checkElement: function( element ) {
if ( !Widget.isDomWidgetWrapper( element ) )
return false;
var widgetElement = element.getFirst( Widget.isDomWidgetElement );
return widgetElement && widgetElement.data( 'widget' ) == this.widget;
},
buildPreview: function( label ) {
return label || this._.definition.name;
},
/**
* Returns allowed content rules which should be registered for this style.
* Uses widget's {@link CKEDITOR.plugins.widget.definition#styleableElements} to make a rule
* allowing classes on specified elements or use widget's
* {@link CKEDITOR.plugins.widget.definition#styleToAllowedContentRules} method to transform a style
* into allowed content rules.
*
* @param {CKEDITOR.editor} The editor instance.
* @returns {CKEDITOR.filter.allowedContentRules}
*/
toAllowedContentRules: function( editor ) {
if ( !editor )
return null;
var widgetDef = editor.widgets.registered[ this.widget ],
classes,
rule = {};
if ( !widgetDef )
return null;
if ( widgetDef.styleableElements ) {
classes = this.getClassesArray();
if ( !classes )
return null;
rule[ widgetDef.styleableElements ] = {
classes: classes,
propertiesOnly: true
};
return rule;
}
if ( widgetDef.styleToAllowedContentRules )
return widgetDef.styleToAllowedContentRules( this );
return null;
},
/**
* Returns classes defined in the style in form of an array.
*
* @returns {String[]}
*/
getClassesArray: function() {
var classes = this._.definition.attributes && this._.definition.attributes[ 'class' ];
return classes ? CKEDITOR.tools.trim( classes ).split( /\s+/ ) : null;
},
/**
* Not implemented.
*
* @method applyToRange
*/
applyToRange: notImplemented,
/**
* Not implemented.
*
* @method removeFromRange
*/
removeFromRange: notImplemented,
/**
* Not implemented.
*
* @method applyToObject
*/
applyToObject: notImplemented
} );
function notImplemented() {}
// @context style
function checkElementMatch( element, fullMatch, editor ) {
// Before CKEditor 4.4 wasn't a required argument, so we need to
// handle a case when it wasn't provided.
if ( !editor )
return false;
if ( !this.checkElement( element ) )
return false;
var widget = editor.widgets.getByElement( element, true );
return widget && widget.checkStyleActive( this );
}
} )();
//
// EXPOSE PUBLIC API ------------------------------------------------------
//
CKEDITOR.plugins.widget = Widget;
Widget.repository = Repository;
Widget.nestedEditable = NestedEditable;
} )();
/**
* An event fired when a widget definition is registered by the {@link CKEDITOR.plugins.widget.repository#add} method.
* It is possible to modify the definition being registered.
*
* @event widgetDefinition
* @member CKEDITOR.editor
* @param {CKEDITOR.plugins.widget.definition} data Widget definition.
*/
/**
* This is an abstract class that describes the definition of a widget.
* It is a type of {@link CKEDITOR.plugins.widget.repository#add} method's second argument.
*
* Widget instances inherit from registered widget definitions, although not in a prototypal way.
* They are simply extended with corresponding widget definitions. Note that not all properties of
* the widget definition become properties of a widget. Some, like {@link #data} or {@link #edit}, become
* widget's events listeners.
*
* @class CKEDITOR.plugins.widget.definition
* @abstract
* @mixins CKEDITOR.feature
*/
/**
* Widget definition name. It is automatically set when the definition is
* {@link CKEDITOR.plugins.widget.repository#add registered}.
*
* @property {String} name
*/
/**
* The method executed while initializing a widget, after a widget instance
* is created, but before it is ready. It is executed before the first
* {@link CKEDITOR.plugins.widget#event-data} is fired so it is common to
* use the `init` method to populate widget data with information loaded from
* the DOM, like for exmaple:
*
* init: function() {
* this.setData( 'width', this.element.getStyle( 'width' ) );
*
* if ( this.parts.caption.getStyle( 'display' ) != 'none' )
* this.setData( 'showCaption', true );
* }
*
* @property {Function} init
*/
/**
* The function to be used to upcast an element to this widget or a
* comma-separated list of upcast methods from the {@link #upcasts} object.
*
* The upcast function **is not** executed in the widget context (because the widget
* does not exist yet) and two arguments are passed:
*
* * `element` ({@link CKEDITOR.htmlParser.element}) – The element to be checked.
* * `data` (`Object`) – The object which can be extended with data which will then be passed to the widget.
*
* An element will be upcasted if a function returned `true` or an instance of
* a {@link CKEDITOR.htmlParser.element} if upcasting meant DOM structure changes
* (in this case the widget will be initialized on the returned element).
*
* @property {String/Function} upcast
*/
/**
* The object containing functions which can be used to upcast this widget.
* Only those pointed by the {@link #upcast} property will be used.
*
* In most cases it is appropriate to use {@link #upcast} directly,
* because majority of widgets need just one method.
* However, in some cases the widget author may want to expose more than one variant
* and then this property may be used.
*
* upcasts: {
* // This function may upcast only figure elements.
* figure: function() {
* // ...
* },
* // This function may upcast only image elements.
* image: function() {
* // ...
* },
* // More variants...
* }
*
* // Then, widget user may choose which upcast methods will be enabled.
* editor.on( 'widgetDefinition', function( evt ) {
* if ( evt.data.name == 'image' )
* evt.data.upcast = 'figure,image'; // Use both methods.
* } );
*
* @property {Object} upcasts
*/
/**
* The {@link #upcast} method(s) priority. The upcast with a lower priority number will be called before
* the one with a higher number. The default priority is `10`.
*
* @since 4.5
* @property {Number} [upcastPriority=10]
*/
/**
* The function to be used to downcast this widget or
* a name of the downcast option from the {@link #downcasts} object.
*
* The downcast funciton will be executed in the {@link CKEDITOR.plugins.widget} context
* and with `widgetElement` ({@link CKEDITOR.htmlParser.element}) argument which is
* the widget's main element.
*
* The function may return an instance of the {@link CKEDITOR.htmlParser.node} class if the widget
* needs to be downcasted to a different node than the widget's main element.
*
* @property {String/Function} downcast
*/
/**
* The object containing functions which can be used to downcast this widget.
* Only the one pointed by the {@link #downcast} property will be used.
*
* In most cases it is appropriate to use {@link #downcast} directly,
* because majority of widgets have just one variant of downcasting (or none at all).
* However, in some cases the widget author may want to expose more than one variant
* and then this property may be used.
*
* downcasts: {
* // This downcast may transform the widget into the figure element.
* figure: function() {
* // ...
* },
* // This downcast may transform the widget into the image element with data-* attributes.
* image: function() {
* // ...
* }
* }
*
* // Then, the widget user may choose one of the downcast options when setting up his editor.
* editor.on( 'widgetDefinition', function( evt ) {
* if ( evt.data.name == 'image' )
* evt.data.downcast = 'figure';
* } );
*
* @property downcasts
*/
/**
* If set, it will be added as the {@link CKEDITOR.plugins.widget#event-edit} event listener.
* This means that it will be executed when a widget is being edited.
* See the {@link CKEDITOR.plugins.widget#method-edit} method.
*
* @property {Function} edit
*/
/**
* If set, it will be added as the {@link CKEDITOR.plugins.widget#event-data} event listener.
* This means that it will be executed every time the {@link CKEDITOR.plugins.widget#property-data widget data} changes.
*
* @property {Function} data
*/
/**
* The method to be executed when the widget's command is executed in order to insert a new widget
* (widget of this type is not focused). If not defined, then the default action will be
* performed which means that:
*
* * An instance of the widget will be created in a detached {@link CKEDITOR.dom.documentFragment document fragment},
* * The {@link CKEDITOR.plugins.widget#method-edit} method will be called to trigger widget editing,
* * The widget element will be inserted into DOM.
*
* @property {Function} insert
*/
/**
* The name of a dialog window which will be opened on {@link CKEDITOR.plugins.widget#method-edit}.
* If not defined, then the {@link CKEDITOR.plugins.widget#method-edit} method will not perform any action and
* widget's command will insert a new widget without opening a dialog window first.
*
* @property {String} dialog
*/
/**
* The template which will be used to create a new widget element (when the widget's command is executed).
* This string is populated with {@link #defaults default values} by using the {@link CKEDITOR.template} format.
* Therefore it has to be a valid {@link CKEDITOR.template} argument.
*
* @property {String} template
*/
/**
* The data object which will be used to populate the data of a newly created widget.
* See {@link CKEDITOR.plugins.widget#property-data}.
*
* defaults: {
* showCaption: true,
* align: 'none'
* }
*
* @property defaults
*/
/**
* An object containing definitions of widget components (part name => CSS selector).
*
* parts: {
* image: 'img',
* caption: 'div.caption'
* }
*
* @property parts
*/
/**
* An object containing definitions of nested editables (editable name => {@link CKEDITOR.plugins.widget.nestedEditable.definition}).
* Note that editables *have to* be defined in the same order as they are in DOM / {@link CKEDITOR.plugins.widget.definition#template template}.
* Otherwise errors will occur when nesting widgets inside each other.
*
* editables: {
* header: 'h1',
* content: {
* selector: 'div.content',
* allowedContent: 'p strong em; a[!href]'
* }
* }
*
* @property editables
*/
/**
* Widget name displayed in elements path.
*
* @property {String} pathName
*/
/**
* If set to `true`, the widget's element will be covered with a transparent mask.
* This will prevent its content from being clickable, which matters in case
* of special elements like embedded Flash or iframes that generate a separate "context".
*
* @property {Boolean} mask
*/
/**
* If set to `true/false`, it will force the widget to be either an inline or a block widget.
* If not set, the widget type will be determined from the widget element.
*
* Widget type influences whether a block (`div`) or an inline (`span`) element is used
* for the wrapper.
*
* @property {Boolean} inline
*/
/**
* The label for the widget toolbar button.
*
* editor.widgets.add( 'simplebox', {
* button: 'Create a simple box'
* } );
*
* editor.widgets.add( 'simplebox', {
* button: editor.lang.simplebox.title
* } );
*
* @property {String} button
*/
/**
* Whether widget should be draggable. Defaults to `true`.
* If set to `false` drag handler will not be displayed when hovering widget.
*
* @property {Boolean} draggable
*/
/**
* Names of element(s) (separated by spaces) for which the {@link CKEDITOR.filter} should allow classes
* defined in the widget styles. For example if your widget is upcasted from a simple `<div>`
* element, then in order to make it styleable you can set:
*
* editor.widgets.add( 'customWidget', {
* upcast: function( element ) {
* return element.name == 'div';
* },
*
* // ...
*
* styleableElements: 'div'
* } );
*
* Then, when the following style is defined:
*
* {
* name: 'Thick border', type: 'widget', widget: 'customWidget',
* attributes: { 'class': 'thickBorder' }
* }
*
* a rule allowing the `thickBorder` class for `div` elements will be registered in the {@link CKEDITOR.filter}.
*
* If you need to have more freedom when transforming widget style to allowed content rules,
* you can use the {@link #styleToAllowedContentRules} callback.
*
* @since 4.4
* @property {String} styleableElements
*/
/**
* Function transforming custom widget's {@link CKEDITOR.style} instance into
* {@link CKEDITOR.filter.allowedContentRules}. It may be used when a static
* {@link #styleableElements} property is not enough to inform the {@link CKEDITOR.filter}
* what HTML features should be enabled when allowing the given style.
*
* In most cases, when style's classes just have to be added to element name(s) used by
* the widget element, it is recommended to use simpler {@link #styleableElements} property.
*
* In order to get parsed classes from the style definition you can use
* {@link CKEDITOR.style.customHandlers.widget#getClassesArray}.
*
* For example, if you want to use the [object format of allowed content rules](#!/guide/dev_allowed_content_rules-section-object-format),
* to specify `match` validator, your implementation could look like this:
*
* editor.widgets.add( 'customWidget', {
* // ...
*
* styleToAllowedContentRules: funciton( style ) {
* // Retrieve classes defined in the style.
* var classes = style.getClassesArray();
*
* // Do something crazy - for example return allowed content rules in object format,
* // with custom match property and propertiesOnly flag.
* return {
* h1: {
* match: isWidgetElement,
* propertiesOnly: true,
* classes: classes
* }
* };
* }
* } );
*
* @since 4.4
* @property {Function} styleToAllowedContentRules
* @param {CKEDITOR.style.customHandlers.widget} style The style to be transformed.
* @returns {CKEDITOR.filter.allowedContentRules}
*/
/**
* This is an abstract class that describes the definition of a widget's nested editable.
* It is a type of values in the {@link CKEDITOR.plugins.widget.definition#editables} object.
*
* In the simplest case the definition is a string which is a CSS selector used to
* find an element that will become a nested editable inside the widget. Note that
* the widget element can be a nested editable, too.
*
* In the more advanced case a definition is an object with a required `selector` property.
*
* editables: {
* header: 'h1',
* content: {
* selector: 'div.content',
* allowedContent: 'p strong em; a[!href]'
* }
* }
*
* @class CKEDITOR.plugins.widget.nestedEditable.definition
* @abstract
*/
/**
* The CSS selector used to find an element which will become a nested editable.
*
* @property {String} selector
*/
/**
* The [Advanced Content Filter](#!/guide/dev_advanced_content_filter) rules
* which will be used to limit the content allowed in this nested editable.
* This option is similar to {@link CKEDITOR.config#allowedContent} and one can
* use it to limit the editor features available in the nested editable.
*
* @property {CKEDITOR.filter.allowedContentRules} allowedContent
*/
/**
* Nested editable name displayed in elements path.
*
* @property {String} pathName
*/