/** * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ 'use strict'; (function() { var DRAG_HANDLER_SIZE = 14; CKEDITOR.plugins.add( 'widget', { 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;' + 'opacity:0.75;' + 'transition:height 0s 0.2s' + // Delay hiding drag handler. '}' + '.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' + '}' + '.cke_widget_mask{' + 'position:absolute;' + 'top:0;' + 'left:0;' + 'width:100%;' + 'height:100%' + '}' ); }, beforeInit: function( editor ) { editor.widgets = new Repository( editor ); }, afterInit: function( editor ) { addWidgetButtons( editor ); setupContextMenu( editor ); } } ); /** * @class CKEDITOR.plugins.widget.repository * @mixins CKEDITOR.event */ function Repository( editor ) { this.editor = editor; this.registered = {}; this.instances = {}; this.selected = []; this.focused = null; this.widgetHoldingFocusedEditable = null; this._ = { nextId: 0, upcasts: [], filters: {} }; setupDataProcessing( this ); setupWidgetsObserver( this ); setupSelectionObserver( this ); setupMouseObserver( this ); setupKeyboardObserver( this ); setupDragAndDrop( this ); } Repository.prototype = { /** * Minimum interval between selection checks. * * @private */ MIN_SELECTION_CHECK_INTERVAL: 500, /** * Minimum interval between widgets checks. * * @private */ MIN_WIDGETS_CHECK_INTERVAL: 1000, /** * Adds widget definition to the repository. * * @param {String} name * @param {CKEDITOR.plugins.widget.definition} widgetDef * @returns {CKEDITOR.plugins.widget.registeredDefinition} */ add: function( name, widgetDef ) { // Create prototyped copy of original widget defintion, so we won't modify it. widgetDef = CKEDITOR.tools.prototypedCopy( widgetDef ); widgetDef.name = name; widgetDef.repository = this; widgetDef.definition = widgetDef; widgetDef._ = widgetDef._ || {}; this.editor.fire( 'widgetDefinition', widgetDef ); if ( widgetDef.template ) widgetDef.template = new CKEDITOR.template( widgetDef.template ); addWidgetCommand( this.editor, widgetDef ); addWidgetProcessors( this, widgetDef ); // Register widget automatically if it does not have a button. if ( !widgetDef.button ) this.editor.addFeature( widgetDef ); this.registered[ name ] = widgetDef; return widgetDef; }, /** * Checks selection to update widgets states (select and focus). * * This method is triggered by {@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 = isWidgetWrapper2; while ( ( wrapper = walker.next() ) ) updater.select( this.getByElement( wrapper ) ); updater.commit(); }, /** * Checks if all widgets instances are still present in DOM. * Destroys those which are not. * * This method is triggered by {@link #event-checkWidigets} event. */ checkWidgets: function() { if ( this.editor.mode != 'wysiwyg' ) return; var toBeDestroyed = [], editable = this.editor.editable(), instances = this.instances, id; if ( !editable ) return; for ( id in instances ) { if ( !editable.contains( instances[ id ].wrapper ) ) this.destroy( instances[ id ], true ); } }, 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 ); }, /** * Removes and destroys widget instance. * * @param {CKEDITOR.plugins.widget} widget * @param {Boolean} [offline] Whether widget is offline (detached from DOM tree) - * in this case 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 ); }, /** * Removes and destroys all widgets instances. * * @param {Boolean} [offline] Whether widgets are offline (detached from DOM tree) - * in this case DOM (attributes, classes, etc.) will not be cleaned up. */ destroyAll: function( offline ) { var instances = this.instances, widget; for ( var id in instances ) { widget = instances[ id ]; this.destroy( widget, offline ); } }, /** * Gets widget instance by element which may be * widget's wrapper or any of its children. * * @param {CKEDITOR.dom.element} element * @param {Boolean} [checkWrapperOnly] Check only if `element` equals wrapper. * @returns {CKEDITOR.plugins.widget} Widget instance or `null`. */ getByElement: function( element, checkWrapperOnly ) { if ( !element ) return null; var wrapper; for ( var id in this.instances ) { wrapper = this.instances[ id ].wrapper; if ( wrapper.equals( element ) || ( !checkWrapperOnly && wrapper.contains( element ) ) ) return this.instances[ id ]; } return null; }, /** * Initializes widget on given element if widget hasn't * been initialzed on it yet. * * @param {CKEDITOR.dom.element} element * @param {String/CKEDITOR.plugins.widget.definition} widgetDef Name of a widget type or a widget definition. * Widget definition should be previously registered by {@link CKEDITOR.plugins.widget.repository#add}. * @param startupData Widget's startup data (has precedence over defaults one). * @returns {CKEDITOR.plugins.widget} The widget instance or null if there's no widget for 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 initialzed 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 widget by element 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 * haven't been initialized yet. * * @param {CKEDITOR.dom.element} [container=editor.editable()] Container which will be checked for not * initialized widgets. Defaults to editor's editable element. * @returns {CKEDITOR.plugins.widget[]} Array of widget instances which have been initialized. */ 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( isWidgetElement2 ) ); if ( instance ) newInstances.push( instance ); } return newInstances; }, /** * Wraps element with a widget container. * * If this method is called on {@link CKEDITOR.htmlParser.element}, then it will * also take care of fixing DOM after wrapping (wrapper may not be allowed in element's parent). * * @param {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} The widget element to be wrapperd. * @param {String} [widgetName] * @returns {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} The wrapper element or `null` if * widget of this type 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 ) ); // 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 ) ); 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; } // %REMOVE_START% // Expose for tests. , getNestedEditable: getNestedEditable, createEditableFilter: createEditableFilter // %REMOVE_END% }; CKEDITOR.event.implementOn( Repository.prototype ); /** * @class CKEDITOR.plugins.widget * @mixins CKEDITOR.event */ 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, inline: element.getParent().getName() == 'span', /** * Widget's main element. * * @readonly * @property {CKEDITOR.dom.element} */ element: element, /** * Widget's data object. * * Data can only be set by {@link #setData} method. * * @readonly */ data: CKEDITOR.tools.extend( {}, typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults ), /** * Is data ready. Set to `true` when data from all sources * ({@link CKEDITOR.plugins.widget.definition#defaults}, set * in {@link #init} method and loaded from widget's element) * are finally loaded. This is immediately followed by first {@link #event-data}. * * @readonly */ dataReady: false, // Revert what widgetDef could override (automatic #edit listener). edit: Widget.prototype.edit, /** * Contains nested editable which currently holds focus. * * @readonly * @property {CKEDITOR.dom.element} */ focusedEditable: null, // 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 ); /** * Object of widget's component elements. * * For every `partName => selector` pair in {@link CKEDITOR.plugins.widget.definition#parts} * one `partName => element` pair is added to this object during * widget initialization. * * @property {Object} parts */ 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 = { /** * Destroys this widget instance. * * Use {@link CKEDITOR.plugins.widget.repository#destroy} when possible instead of this method. * * This method fires {#event-destroy} event. * * @param {Boolean} [offline] Whether widget is offline (detached from DOM tree) - * in this case DOM (attributes, classes, etc.) will not be cleaned up. */ destroy: function( offline ) { var editor = this.editor; 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 nested editable. * * @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 ) { editable.removeClass( 'cke_widget_editable' ); editable.removeClass( 'cke_widget_editable_focused' ); editable.removeAttributes( [ 'contenteditable', 'data-cke-widget-editable' ] ); } delete this.editables[ editableName ]; }, /** * Starts widget editing. * * This method fires {@link CKEDITOR.plugins.widget#event-edit} event * which may be cancelled in order to prevent from opening dialog. * * Dialog name is obtained from event's data `dialog` property or * from {@link CKEDITOR.plugins.widget.definition#dialog}. */ edit: function() { var evtData = { dialog: this.dialog }, that = this; // Edit event was blocked, but there's no dialog to be automatically opened. if ( !this.fire( 'edit', evtData ) || !evtData.dialog ) return; this.editor.openDialog( evtData.dialog, function( dialog ) { var showListener, okListener; // Allow to add a custom dialog handler. if ( !that.fire( 'dialog', dialog ) ) return; // Make widget accessible beyond setup and commit. dialog._.widget = that; 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(); } ); } ); }, /** * Initializes nested editable. * * **Note**: only elements from {@link CKEDITOR.dtd#$editable} may become editables. * * @param {String} editableName The nested editable name. * @param {CKEDITOR.plugins.widget.nestedEditableDefinition} definition The definition of nested editable. * @returns {Boolean} Whether an editable was successfully initialized. */ initEditable: function( editableName, definition ) { var editable = this.wrapper.findOne( 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 } ); 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' ); 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.setData( editable.getHtml() ); return true; } return false; }, /** * Checks if widget has already been initialized. This means, for example, * that widget has mask, element styles have been transferred to wrapper etc. * * @returns {Boolean} */ isInited: function() { return !!( this.wrapper && this.inited ); }, /** * TODO * * @returns {Boolean} */ isReady: function() { return this.isInited() && this.ready; }, /** * Focuses widget by selecting it. */ focus: function() { var sel = this.editor.getSelection(); if ( sel ) sel.fake( this.wrapper ); // Always focus editor (not only when focusManger.hasFocus is false) (because of #10483). this.editor.focus(); }, /** * Sets widget value(s) in {@link #propeorty-data} object. * If given value(s) modifies current ones {@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 JSON string, so therefore {@link #property-data} should contain * only serializable data. * * @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 widget's focus state. Usually executed automatically after * widget has been focused by {@link #focus} method or selection was moved * out of widget. * * @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 widget's select state. Usually executed automatically after * widget has been selected by {@link #focus} method or selection was moved * out of widget. * * @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; } }; CKEDITOR.event.implementOn( Widget.prototype ); /** * Wrapper class for editable elements inside widgets. * * Don't 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.filter = config.filter; } NestedEditable.prototype = CKEDITOR.tools.extend( CKEDITOR.tools.prototypedCopy( CKEDITOR.dom.element.prototype ), { setData: function( data ) { data = this.editor.dataProcessor.toHtml( data, { context: this.getName(), filter: this.filter, enterMode: this.filter ? this.filter.getAllowedEnterMode() : this.editor.enterMode } ); this.setHtml( data ); }, getData: function() { return this.editor.dataProcessor.toDataFormat( this.getHtml(), { context: this.getName(), filter: this.filter, enterMode: this.filter ? this.filter.getAllowedEnterMode() : this.editor.enterMode } ); } } ); // // 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.registeredDefinition} widgetDef function addWidgetCommand( editor, widgetDef ) { editor.addCommand( widgetDef.name, { exec: function() { 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() ), editWasCanceled = true; // 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 ); // 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 ) { editWasCanceled = false; 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() { editor.widgets.destroy( instance, true ); } ); dialog.once( 'hide', function() { okListener.removeListener(); cancelListener.removeListener(); } ); } ); } // Dialog hasn't been set, so insert widget now. else finalizeCreation(); }, null, null, 999 ); instance.edit(); // Remove listener in case someone canceled it before this // listener was executed. editListener.removeListener(); // In case edit was canceled - finalize creation here which should happen anyway (just without // initial edit). if ( editWasCanceled ) finalizeCreation(); } function finalizeCreation() { var wrapper = temp.getFirst(); if ( wrapper && isWidgetWrapper2( wrapper ) ) { editor.insertElement( wrapper ); var widget = editor.widgets.getByElement( wrapper ); // Fire postponed #ready event. widget.ready = true; widget.fire( 'ready' ); widget.focus(); } } }, refresh: function( editor, path ) { // Disable widgets' commands inside nested editables - // check if blockLimit is a nested editable or a descendant of any. this.setState( getNestedEditable( editor.editable(), path.blockLimit ) ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF ); }, // A hack to force command refreshing on context change. context: 'div', allowedContent: widgetDef.allowedContent, requiredContent: widgetDef.requiredContent, contentForms: widgetDef.contentForms, contentTransformations: widgetDef.contentTransformations } ); } function addWidgetProcessors( widgetsRepo, widgetDef ) { var upcast = widgetDef.upcast, upcasts; if ( !upcast ) return; // Multiple upcasts defined in string. if ( typeof upcast == 'string' ) { upcasts = upcast.split( ',' ); while ( upcasts.length ) widgetsRepo._.upcasts.push( [ widgetDef.upcasts[ upcasts.pop() ], widgetDef.name ] ); } // Single rule which is automatically activated. else widgetsRepo._.upcasts.push( [ upcast, widgetDef.name ] ); } function blurWidget( widgetsRepo, widget ) { widgetsRepo.focused = null; if ( widget.isInited() ) { // Widget could be destroyed in the meantime - e.g. data could be set. widgetsRepo.fire( 'widgetBlurred', { widget: widget } ); widget.setFocused( false ); } } // 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( isWidgetElement2 ); // 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 ); } // Otherwise - something is wrong... clean this up. else 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; } // Gets nested editable if node is its descendant or the editable itself. // // @param {CKEDITOR.dom.element} guard Stop ancestor search on this node (usually editor's editable). // @param {CKEDITOR.dom.node} node Start search from this node. // @returns {CKEDITOR.dom.element} Element or null. function getNestedEditable( guard, node ) { if ( !node || node.equals( guard ) ) return null; if ( isNestedEditable2( node ) ) return node; return getNestedEditable( guard, node.getParent() ); } 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 ); } // @param {CKEDITOR.htmlParser.element} function isWidgetElement( element ) { return element.type == CKEDITOR.NODE_ELEMENT && !!element.attributes[ 'data-widget' ]; } // @param {CKEDITOR.dom.element} function isWidgetElement2( element ) { return element.type == CKEDITOR.NODE_ELEMENT && element.hasAttribute( 'data-widget' ); } // Whether for this definition and element widget should be created in inline or block mode. function isWidgetInline( widgetDef, elementName ) { return typeof widgetDef.inline == 'boolean' ? widgetDef.inline : !!CKEDITOR.dtd.$inline[ elementName ]; } // @param {CKEDITOR.htmlParser.element} function isWidgetWrapper( element ) { return element.type == CKEDITOR.NODE_ELEMENT && element.attributes[ 'data-cke-widget-wrapper' ]; } // @param {CKEDITOR.dom.element} function isWidgetWrapper2( element ) { return element.type == CKEDITOR.NODE_ELEMENT && element.hasAttribute( 'data-cke-widget-wrapper' ); } // @param {CKEDITOR.dom.element} function isNestedEditable2( node ) { return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-editable' ); } function moveSelectionToDropPosition( editor, dropEvt ) { var $evt = dropEvt.data.$, $range, range = editor.createRange(); // Make testing possible. if ( dropEvt.data.testRange ) { dropEvt.data.testRange.select(); return; } // Webkits. if ( document.caretRangeFromPoint ) { $range = editor.document.$.caretRangeFromPoint( $evt.clientX, $evt.clientY ); range.setStart( CKEDITOR.dom.node( $range.startContainer ), $range.startOffset ); range.collapse( true ); } // FF. else if ( $evt.rangeParent ) { range.setStart( CKEDITOR.dom.node( $evt.rangeParent ), $evt.rangeOffset ); range.collapse( true ); } // IEs. else if ( document.body.createTextRange ) { $range = editor.document.getBody().$.createTextRange(); $range.moveToPoint( $evt.clientX, $evt.clientY ); var id = 'cke-temp-' + ( new Date() ).getTime(); $range.pasteHTML( '\u200b' ); var span = editor.document.getById( id ); range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START ); span.remove(); } range.select(); } function moveWidget( editor, sourceWidget ) { var widgetHtml = sourceWidget.wrapper.getOuterHtml(); sourceWidget.wrapper.remove(); editor.widgets.destroy( sourceWidget, true ); // Create snapshot for the removed widget. editor.fire( 'saveSnapshot' ); // Lock snapshot while pasting to merge those changes with the previous snapshot. // This way we are grouping all changes done by moveWidget into one snapshot. editor.fire( 'lockSnapshot' ); editor.execCommand( 'paste', widgetHtml ); editor.fire( 'unlockSnapshot' ); } 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 ) { widgetsRepo.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 ) widgetsRepo.editor.setActiveFilter( editableInstance.filter ); } else { if ( !offline ) widget.focusedEditable.removeClass( 'cke_widget_editable_focused' ); widget.focusedEditable = null; widgetsRepo.widgetHoldingFocusedEditable = null; widgetsRepo.editor.setActiveFilter( null ); } widgetsRepo.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', {} ); } ); } // Set up data processing like: // * toHtml/toDataFormat, // * pasting handling, // * undo/redo handling. function setupDataProcessing( widgetsRepo ) { var editor = widgetsRepo.editor; setupUpcasting( widgetsRepo ); setupDowncasting( widgetsRepo ); editor.on( 'contentDomUnload', function() { widgetsRepo.destroyAll( true ); } ); // Handle pasted single widget. editor.on( 'paste', function( evt ) { evt.data.dataValue = evt.data.dataValue.replace( /^(?: