From bc1d2957562754b017e419e52706e80629f8cbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Mon, 2 Dec 2013 13:55:46 +0100 Subject: [PATCH 1/8] Stop firing checkWidgets on keyup. --- plugins/widget/plugin.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index a11d1899ef3..a1a155de92c 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -192,13 +192,6 @@ */ MIN_SELECTION_CHECK_INTERVAL: 500, - /** - * Minimum interval between widget checks. - * - * @private - */ - MIN_WIDGETS_CHECK_INTERVAL: 1000, - /** * 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. @@ -2281,26 +2274,11 @@ } // Setup observer which will trigger checkWidgets on: - // * keyup. + // * contentDomInvalidated. function setupWidgetsObserver( widgetsRepo ) { - var editor = widgetsRepo.editor, - buffer = CKEDITOR.tools.eventsBuffer( widgetsRepo.MIN_WIDGETS_CHECK_INTERVAL, checkWidgets ), - ignoredKeys = { 16:1,17:1,18:1,37:1,38:1,39:1,40:1,225:1 }; // SHIFT,CTRL,ALT,LEFT,UP,RIGHT,DOWN,RIGHT ALT(FF) - - editor.on( 'contentDom', function() { - var editable = editor.editable(); - - // Schedule check on keyup, but not more often than once per MIN_CHECK_DELAY. - editable.attachListener( editable.isInline() ? editable : editor.document, 'keyup', function( evt ) { - if ( !( evt.data.getKey() in ignoredKeys ) ) - buffer.input(); - }, null, null, 999 ); - } ); - - editor.on( 'contentDomUnload', buffer.reset ); + var editor = widgetsRepo.editor; widgetsRepo.on( 'checkWidgets', widgetsRepo.checkWidgets, widgetsRepo ); - editor.on( 'contentDomInvalidated', checkWidgets ); function checkWidgets() { From 2645ca369c39ea558dab0c4cd7eb571a0212e134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Mon, 2 Dec 2013 14:34:09 +0100 Subject: [PATCH 2/8] Check widgets on insertText and insertHtml. --- plugins/widget/plugin.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index a1a155de92c..d967a918eb3 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -2280,10 +2280,17 @@ widgetsRepo.on( 'checkWidgets', widgetsRepo.checkWidgets, widgetsRepo ); editor.on( 'contentDomInvalidated', checkWidgets ); + // Listen with high priority to check widgets after data was inserted. + editor.on( 'insertText', checkNewWidgets, null, null, 999 ); + editor.on( 'insertHtml', checkNewWidgets, null, null, 999 ); function checkWidgets() { widgetsRepo.fire( 'checkWidgets' ); } + + function checkNewWidgets() { + widgetsRepo.fire( 'checkWidgets', { initOnlyNew: true } ); + } } // Helper for coordinating which widgets should be From 488b4b615ee94e81d117a7f1bc36a59df216866b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Mon, 2 Dec 2013 16:04:09 +0100 Subject: [PATCH 3/8] Added initOnlyNew option for checkWidgets. --- plugins/widget/plugin.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index d967a918eb3..85b4316911d 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -263,9 +263,16 @@ * Reinitializes widgets on widget wrappers for which widget instances * cannot be found. * - * This method is triggered by the {@link #event-checkWidgets} event. + * This method is triggered by the {@link #event-checkWidgets} event **and should + * not be called directly**. + * + * @param [options] + * @param {Boolean} [options.initOnlyNew] Init widgets only on newly created + * widgets (those which still have `cke_widget_new` class). When this option is + * set to `true` widgets which were invalidated (e.g. by replacing by clone), will not be reinitialized. + * That makes the check faster. */ - checkWidgets: function() { + checkWidgets: function( options ) { if ( this.editor.mode != 'wysiwyg' ) return; @@ -282,6 +289,12 @@ this.destroy( instances[ i ], true ); } + // Init on all (new) if initOnlyNew option was passed. + if ( options && options.initOnlyNew ) { + this.initOnAll(); + return; + } + var wrappers = editable.find( '.cke_widget_wrapper' ); // Create widgets on existing wrappers if they do not exists. @@ -2274,20 +2287,22 @@ } // Setup observer which will trigger checkWidgets on: - // * contentDomInvalidated. + // * contentDomInvalidated, + // * insertText, + // * insertHtml. function setupWidgetsObserver( widgetsRepo ) { var editor = widgetsRepo.editor; - widgetsRepo.on( 'checkWidgets', widgetsRepo.checkWidgets, widgetsRepo ); - editor.on( 'contentDomInvalidated', checkWidgets ); + widgetsRepo.on( 'checkWidgets', function( evt ) { + widgetsRepo.checkWidgets( evt.data ); + } ); + editor.on( 'contentDomInvalidated', function() { + widgetsRepo.fire( 'checkWidgets' ); + } ); // Listen with high priority to check widgets after data was inserted. editor.on( 'insertText', checkNewWidgets, null, null, 999 ); editor.on( 'insertHtml', checkNewWidgets, null, null, 999 ); - function checkWidgets() { - widgetsRepo.fire( 'checkWidgets' ); - } - function checkNewWidgets() { widgetsRepo.fire( 'checkWidgets', { initOnlyNew: true } ); } From 5e6aaa2616a34d7d9b4a3a1016cfed67ef765168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Mon, 2 Dec 2013 17:10:19 +0100 Subject: [PATCH 4/8] Code reorganisation - no logic change. --- plugins/widget/plugin.js | 210 +++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 107 deletions(-) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index 85b4316911d..2b7b90ed1f6 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -175,8 +175,7 @@ filters: {} }; - setupDataProcessing( this ); - setupWidgetsObserver( this ); + setupWidgetsLifecycle( this ); setupSelectionObserver( this ); setupMouseObserver( this ); setupKeyboardObserver( this ); @@ -1820,103 +1819,6 @@ return CKEDITOR.tools.trim( wrapperHtml ); } - // 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( pasteReplaceRegex, pasteReplaceFn ); - } ); - } - - function setupDowncasting( widgetsRepo ) { - var editor = widgetsRepo.editor, - downcastingSessions = {}, - nestedEditableScope = false; - - // 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( isWidgetElement ); - toBeDowncasted.push( { - wrapper: element, - element: widgetElement, - widget: widget - } ); - - // 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 ) { - delete attrs[ 'contenteditable' ]; - - // Replace nested editable's content with its output data. - var editable = toBeDowncasted[ toBeDowncasted.length - 1 ].widget.editables[ attrs[ 'data-cke-widget-editable' ] ]; - element.setHtml( editable.getData() ); - - // 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 ); - }, 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; - - while ( ( toBe = toBeDowncasted.shift() ) ) { - widget = toBe.widget; - widgetElement = toBe.element; - retElement = widget._.downcastFn && widget._.downcastFn.call( widget, widgetElement ); - - // Returned element always defaults to widgetElement. - if ( !retElement ) - retElement = widgetElement; - - toBe.wrapper.replaceWith( retElement ); - } - }, null, null, 13 ); - } - function setupDragAndDrop( widgetsRepo ) { var editor = widgetsRepo.editor, lineutils = CKEDITOR.plugins.lineutils; @@ -2174,7 +2076,100 @@ } ); } - function setupUpcasting( widgetsRepo ) { + // Set up data processing like: + // * toHtml/toDataFormat, + // * pasting handling, + // * insertion handling, + // * undo/redo handling, + // * editable reload handling (setData, mode switch). + function setupWidgetsLifecycle( widgetsRepo ) { + setupWidgetsLifecycleStart( widgetsRepo ); + setupWidgetsLifecycleEnd( widgetsRepo ); + } + + function setupWidgetsLifecycleEnd( widgetsRepo ) { + var editor = widgetsRepo.editor, + downcastingSessions = {}, + nestedEditableScope = false; + + // 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( isWidgetElement ); + toBeDowncasted.push( { + wrapper: element, + element: widgetElement, + widget: widget + } ); + + // 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 ) { + delete attrs[ 'contenteditable' ]; + + // Replace nested editable's content with its output data. + var editable = toBeDowncasted[ toBeDowncasted.length - 1 ].widget.editables[ attrs[ 'data-cke-widget-editable' ] ]; + element.setHtml( editable.getData() ); + + // 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 ); + }, 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; + + while ( ( toBe = toBeDowncasted.shift() ) ) { + widget = toBe.widget; + widgetElement = toBe.element; + retElement = widget._.downcastFn && widget._.downcastFn.call( widget, widgetElement ); + + // 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, upcasts = widgetsRepo._.upcasts, processedWidgetOnly, @@ -2284,15 +2279,16 @@ processedWidgetOnly = evt.data.dataValue.children.length == 1 && isWidgetWrapper( evt.data.dataValue.children[ 0 ] ); }, null, null, 8 ); - } - // Setup observer which will trigger checkWidgets on: - // * contentDomInvalidated, - // * insertText, - // * insertHtml. - function setupWidgetsObserver( widgetsRepo ) { - var editor = widgetsRepo.editor; + // Handle pasted single widget. + editor.on( 'paste', function( evt ) { + evt.data.dataValue = evt.data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn ); + } ); + // Setup observer which will trigger checkWidgets on: + // * contentDomInvalidated, + // * insertText, + // * insertHtml. widgetsRepo.on( 'checkWidgets', function( evt ) { widgetsRepo.checkWidgets( evt.data ); } ); From 7d13df4108157b4136e4b71d11627eb01f2a24c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Mon, 2 Dec 2013 17:57:36 +0100 Subject: [PATCH 5/8] Further reorganisation. --- plugins/widget/plugin.js | 198 ++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 94 deletions(-) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index 2b7b90ed1f6..c45b4c6a3bc 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -1555,6 +1555,66 @@ 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; + + return { + toBeWrapped: toBeWrapped, + + iterator: function( element ) { + // 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( isWidgetElement ); + + 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 ( upcasts.length ) { + var upcast, upcasted, + data, + i = 0, + l = upcasts.length; + + for ( ; i < l; ++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' ] = JSON.stringify( data ); + + toBeWrapped.push( [ element, upcast[ 1 ] ] ); + + // Do not iterate over descendants. + return false; + } + } + } + } + }; + } + // Finds a first parent that matches query. // // @param {CKEDITOR.dom.element} element @@ -2076,15 +2136,23 @@ } ); } - // Set up data processing like: - // * toHtml/toDataFormat, + // Set up actions like: + // * processing in toHtml/toDataFormat, // * pasting handling, // * insertion handling, - // * undo/redo handling, - // * editable reload handling (setData, mode switch). + // * editable reload handling (setData, mode switch, undo/redo), + // * DOM invalidation handling, + // * widgets checks. function setupWidgetsLifecycle( widgetsRepo ) { setupWidgetsLifecycleStart( widgetsRepo ); setupWidgetsLifecycleEnd( widgetsRepo ); + + widgetsRepo.on( 'checkWidgets', function( evt ) { + widgetsRepo.checkWidgets( evt.data ); + } ); + widgetsRepo.editor.on( 'contentDomInvalidated', function() { + widgetsRepo.fire( 'checkWidgets' ); + } ); } function setupWidgetsLifecycleEnd( widgetsRepo ) { @@ -2171,10 +2239,28 @@ function setupWidgetsLifecycleStart( widgetsRepo ) { var editor = widgetsRepo.editor, - upcasts = widgetsRepo._.upcasts, 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 ); + + // 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. + processedWidgetOnly = evt.data.dataValue.children.length == 1 && + isWidgetWrapper( evt.data.dataValue.children[ 0 ] ); + }, null, null, 8 ); + editor.on( 'dataReady', function() { // Clean up all widgets loaded from snapshot. if ( snapshotLoaded ) @@ -2189,20 +2275,6 @@ widgetsRepo.initOnAll(); } ); - editor.on( 'afterPaste', function() { - editor.fire( 'lockSnapshot' ); - - // Init is enough (no clean up needed), - // because inserted widgets were cleaned up by toHtml. - var newInstances = widgetsRepo.initOnAll(); - - // If just a widget was pasted and nothing more focus it. - if ( processedWidgetOnly && newInstances.length == 1 ) - newInstances[ 0 ].focus(); - - editor.fire( 'unlockSnapshot' ); - } ); - // Set flag so dataReady will know that additional // cleanup is needed, because snapshot containing widgets was loaded. editor.on( 'loadSnapshot', function( evt ) { @@ -2214,87 +2286,25 @@ widgetsRepo.destroyAll( true ); }, null, null, 9 ); - // 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 toBeWrapped = [], - toBe, - element; - - evt.data.dataValue.forEach( function( element ) { - // 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( isWidgetElement ); - - 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 ( upcasts.length ) { - var upcast, upcasted, - data, - i = 0, - l = upcasts.length; - - for ( ; i < l; ++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' ] = JSON.stringify( data ); - - toBeWrapped.push( [ element, upcast[ 1 ] ] ); - - // Do not iterate over descendants. - return false; - } - } - } - }, CKEDITOR.NODE_ELEMENT ); - - // Clean up and wrap all queued elements. - while ( ( toBe = toBeWrapped.pop() ) ) { - cleanUpWidgetElement( toBe[ 0 ] ); - widgetsRepo.wrapElement( toBe[ 0 ], toBe[ 1 ] ); - } - - // Used to determine whether only widget was pasted. - processedWidgetOnly = evt.data.dataValue.children.length == 1 && - isWidgetWrapper( evt.data.dataValue.children[ 0 ] ); - }, null, null, 8 ); - // Handle pasted single widget. editor.on( 'paste', function( evt ) { evt.data.dataValue = evt.data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn ); } ); - // Setup observer which will trigger checkWidgets on: - // * contentDomInvalidated, - // * insertText, - // * insertHtml. - widgetsRepo.on( 'checkWidgets', function( evt ) { - widgetsRepo.checkWidgets( evt.data ); - } ); - editor.on( 'contentDomInvalidated', function() { - widgetsRepo.fire( 'checkWidgets' ); + editor.on( 'afterPaste', function() { + editor.fire( 'lockSnapshot' ); + + // Init is enough (no clean up needed), + // because inserted widgets were cleaned up by toHtml. + var newInstances = widgetsRepo.initOnAll(); + + // If just a widget was pasted and nothing more focus it. + if ( processedWidgetOnly && newInstances.length == 1 ) + newInstances[ 0 ].focus(); + + editor.fire( 'unlockSnapshot' ); } ); + // Listen with high priority to check widgets after data was inserted. editor.on( 'insertText', checkNewWidgets, null, null, 999 ); editor.on( 'insertHtml', checkNewWidgets, null, null, 999 ); From d208d946e9d50fa0e537e695aff507f83b7a7887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 3 Dec 2013 10:36:20 +0100 Subject: [PATCH 6/8] Introduced focusInited option for checkWidgets. --- plugins/widget/plugin.js | 61 ++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index c45b4c6a3bc..738ba59e1ba 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -265,11 +265,13 @@ * This method is triggered by the {@link #event-checkWidgets} event **and should * not be called directly**. * - * @param [options] + * @param [options] The options object. * @param {Boolean} [options.initOnlyNew] Init widgets only on newly created * widgets (those which still have `cke_widget_new` class). When this option is * set to `true` widgets which were invalidated (e.g. by replacing by clone), will not be reinitialized. * That makes the check faster. + * @param {Boolean} [options.focusInited] If only one widget will be initialized by + * the method, then it will be focused. */ checkWidgets: function( options ) { if ( this.editor.mode != 'wysiwyg' ) @@ -277,7 +279,7 @@ var editable = this.editor.editable(), instances = this.instances, - i, count, wrapper; + newInstances, i, count, wrapper; if ( !editable ) return; @@ -289,26 +291,30 @@ } // Init on all (new) if initOnlyNew option was passed. - if ( options && options.initOnlyNew ) { - this.initOnAll(); - return; - } - - var wrappers = editable.find( '.cke_widget_wrapper' ); - - // Create widgets on existing wrappers if they do not exists. - for ( i = 0, count = wrappers.count(); i < count; i++ ) { - wrapper = wrappers.getItem( i ); - - // Check if there's no instance for this widget and that - // wrapper is not inside some temporary element like copybin (#11088). - if ( !this.getByElement( wrapper, true ) && !findParent( wrapper, isTemp2 ) ) { - // Add cke_widget_new class because otherwise - // widget will not be created on such wrapper. - wrapper.addClass( 'cke_widget_new' ); - this.initOn( wrapper.getFirst( isWidgetElement2 ) ); + 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 ); + + // Check if there's no instance for this widget and that + // wrapper is not inside some temporary element like copybin (#11088). + if ( !this.getByElement( wrapper, true ) && !findParent( wrapper, isTemp2 ) ) { + // 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( isWidgetElement2 ) ) ); + } } } + + // If only single widget was initialized and focusInited was passed, focus it. + if ( options && options.focusInited && newInstances.length == 1 ) + newInstances[ 0 ].focus(); }, /** @@ -634,9 +640,22 @@ /** * An event fired to trigger the widgets check. * - * See {@link #method-checkWidgets} method. + * See the {@link #method-checkWidgets} method. + * + * // Will destroy old widgets and initialize new ones. + * editor.widgets.fire( 'checkWidgets' ); + * + * // Initialized widget will be focused. + * editor.widgets.fire( 'checkWidgets', { focusInited: true } ); * * @event checkWidgets + * @param [data] + * @param {Boolean} [data.initOnlyNew] Init widgets only on newly created + * widgets (those which still have `cke_widget_new` class). When this option is + * set to `true` widgets which were invalidated (e.g. by replacing by clone), will not be reinitialized. + * That makes the check faster. + * @param {Boolean} [data.focusInited] If only one widget will be initialized by + * the method, then it will be focused. */ From 3f9a384a5c38973064300e5f24a3295a108b8366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 3 Dec 2013 11:00:39 +0100 Subject: [PATCH 7/8] Handle afterPaste in insertHtml and reuse checkWidgets' new option. --- plugins/widget/plugin.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/plugins/widget/plugin.js b/plugins/widget/plugin.js index 738ba59e1ba..6ce50c1402b 100644 --- a/plugins/widget/plugin.js +++ b/plugins/widget/plugin.js @@ -2310,26 +2310,18 @@ evt.data.dataValue = evt.data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn ); } ); - editor.on( 'afterPaste', function() { - editor.fire( 'lockSnapshot' ); - - // Init is enough (no clean up needed), - // because inserted widgets were cleaned up by toHtml. - var newInstances = widgetsRepo.initOnAll(); - - // If just a widget was pasted and nothing more focus it. - if ( processedWidgetOnly && newInstances.length == 1 ) - newInstances[ 0 ].focus(); - - editor.fire( 'unlockSnapshot' ); - } ); - // Listen with high priority to check widgets after data was inserted. editor.on( 'insertText', checkNewWidgets, null, null, 999 ); editor.on( 'insertHtml', checkNewWidgets, null, null, 999 ); function checkNewWidgets() { - widgetsRepo.fire( 'checkWidgets', { initOnlyNew: true } ); + editor.fire( 'lockSnapshot' ); + + // Init only new for performance reason. + // Focus inited if only widget was processed. + widgetsRepo.fire( 'checkWidgets', { initOnlyNew: true, focusInited: processedWidgetOnly } ); + + editor.fire( 'unlockSnapshot' ); } } From abbd0911574fddf80f69bc1dd0f1435c11b8f574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 3 Dec 2013 16:37:51 +0100 Subject: [PATCH 8/8] Changelog entry. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 1e600930145..741f96ecde9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ CKEditor 4 Changelog Fixed Issues: +* [#11171](http://dev.ckeditor.com/ticket/11171): Fixed: [`editor.insertElement`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertElement) and [`editor.insertText`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertText) methods do not call the [`widget.repository.checkWidgets`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-checkWidgets) method. * [#11159](http://dev.ckeditor.com/ticket/11159): [Enhanced Image](http://ckeditor.com/addon/image2): Fixed buggy discovery of image dimensions in IE9 and IE10. * [#11101](http://dev.ckeditor.com/ticket/11101): Richcombo no longer breaks when given double quotes. * [#11077](http://dev.ckeditor.com/ticket/11077): [Enhanced Image](http://ckeditor.com/addon/image2): Empty undo step recorded when resizing the image.