Skip to content

Commit 511070d

Browse files
committed
Merge branch 't/12018' into major
2 parents 965630a + 89b013b commit 511070d

File tree

5 files changed

+460
-41
lines changed

5 files changed

+460
-41
lines changed

plugins/widget/plugin.js

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@
278278
* Checks if all widget instances are still present in the DOM.
279279
* Destroys those instances that are not present.
280280
* Reinitializes widgets on widget wrappers for which widget instances
281-
* cannot be found.
281+
* cannot be found. Takes nested widgets into account too.
282282
*
283283
* This method triggers the {@link #event-checkWidgets} event whose listeners
284284
* can cancel the method's execution or modify its options.
@@ -321,7 +321,7 @@
321321
},
322322

323323
/**
324-
* Destroys the widget instance.
324+
* Destroys the widget instance and all its nested widgets (widgets inside its nested editables).
325325
*
326326
* @param {CKEDITOR.plugins.widget} widget The widget instance to be destroyed.
327327
* @param {Boolean} [offline] Whether the widget is offline (detached from the DOM tree) —
@@ -341,8 +341,32 @@
341341
*
342342
* @param {Boolean} [offline] Whether the widgets are offline (detached from the DOM tree) —
343343
* in this case the DOM (attributes, classes, etc.) will not be cleaned up.
344+
* @param {CKEDITOR.dom.element} [container] Container within widgets will be destroyed.
345+
* This option will be ignored if `offline` flag was set to `true`, because in such case
346+
* it is not possible to find widgets within passed block.
344347
*/
345-
destroyAll: function( offline ) {
348+
destroyAll: function( offline, container ) {
349+
if ( container && !offline ) {
350+
var wrappers = container.find( '.cke_widget_wrapper' ),
351+
l = wrappers.count(),
352+
i = 0,
353+
widget;
354+
355+
// Length is constant, because this is not a live node list.
356+
// Note: since querySelectorAll returns nodes in document order,
357+
// outer widgets are always placed before their nested widgets and therefore
358+
// are destroyed before them.
359+
for ( ; i < l; ++i ) {
360+
widget = this.getByElement( wrappers.getItem( i ), true );
361+
// Widget might not be found, because it could be a nested widget,
362+
// which would be destroyed when destroying its parent.
363+
if ( widget )
364+
this.destroy( widget );
365+
}
366+
367+
return;
368+
}
369+
346370
var instances = this.instances,
347371
widget;
348372

@@ -1005,7 +1029,7 @@
10051029
},
10061030

10071031
/**
1008-
* Destroys a nested editable.
1032+
* Destroys a nested editable and all nested widgets.
10091033
*
10101034
* @param {String} editableName Nested editable name.
10111035
* @param {Boolean} [offline] See {@link #method-destroy} method.
@@ -1018,6 +1042,7 @@
10181042
this.editor.focusManager.remove( editable );
10191043

10201044
if ( !offline ) {
1045+
this.repository.destroyAll( false, editable );
10211046
editable.removeClass( 'cke_widget_editable' );
10221047
editable.removeClass( 'cke_widget_editable_focused' );
10231048
editable.removeAttributes( [ 'contenteditable', 'data-cke-widget-editable', 'data-cke-enter-mode' ] );
@@ -1158,6 +1183,7 @@
11581183

11591184
// Finally, process editable's data. This data wasn't processed when loading
11601185
// editor's data, becuase they need to be processed separately, with its own filters and settings.
1186+
editable._.initialSetData = true;
11611187
editable.setData( editable.getHtml() );
11621188

11631189
return true;
@@ -1486,6 +1512,7 @@
14861512
// Call the base constructor.
14871513
CKEDITOR.dom.element.call( this, element.$ );
14881514
this.editor = editor;
1515+
this._ = {};
14891516
var filter = this.filter = config.filter;
14901517

14911518
// If blockless editable - always use BR mode.
@@ -1503,9 +1530,20 @@
15031530
* and the {@link CKEDITOR.editor#filter}. This ensures that the data was filtered and prepared to be
15041531
* edited like the {@link CKEDITOR.editor#method-setData editor data}.
15051532
*
1533+
* Before content is changed all nested widgets are destroyed. Afterwards, after new content is loaded
1534+
* all nested widgets are initialized.
1535+
*
15061536
* @param {String} data
15071537
*/
15081538
setData: function( data ) {
1539+
// For performance reasons don't call destroyAll when initializing nested editable,
1540+
// because there are not widgets inside.
1541+
if ( !this._.initialSetData ) {
1542+
// Destroy all nested widgets before setting data.
1543+
this.editor.widgets.destroyAll( false, this );
1544+
}
1545+
this._.initialSetData = false;
1546+
15091547
data = this.editor.dataProcessor.toHtml( data, {
15101548
context: this.getName(),
15111549
filter: this.filter,
@@ -1715,7 +1753,7 @@
17151753

17161754
var editable = this.editor.editable(),
17171755
instances = this.instances,
1718-
newInstances, i, count, wrapper;
1756+
newInstances, i, count, wrapper, notYetInitialized;
17191757

17201758
if ( !editable )
17211759
return;
@@ -1736,10 +1774,15 @@
17361774
// Create widgets on existing wrappers if they do not exists.
17371775
for ( i = 0, count = wrappers.count(); i < count; i++ ) {
17381776
wrapper = wrappers.getItem( i );
1739-
1740-
// Check if there's no instance for this widget and that
1741-
// wrapper is not inside some temporary element like copybin (#11088).
1742-
if ( !this.getByElement( wrapper, true ) && !findParent( wrapper, isDomTemp ) ) {
1777+
notYetInitialized = !this.getByElement( wrapper, true );
1778+
1779+
// Check if:
1780+
// * there's no instance for this widget
1781+
// * wrapper is not inside some temporary element like copybin (#11088)
1782+
// * it was a nested widget's wrapper which has been detached from DOM,
1783+
// when nested editable has been initialized (it overwrites its innerHTML
1784+
// and initializes nested widgets).
1785+
if ( notYetInitialized && !findParent( wrapper, isDomTemp ) && editable.contains( wrapper ) ) {
17431786
// Add cke_widget_new class because otherwise
17441787
// widget will not be created on such wrapper.
17451788
wrapper.addClass( 'cke_widget_new' );

tests/plugins/widget/_helpers/tools.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,15 @@ var widgetTestsTools = ( function() {
175175
return JSON.parse( decodeURIComponent( widget.element.data( 'cke-widget-data' ) ) );
176176
}
177177

178-
function getWidgetById( editor, id ) {
179-
return editor.widgets.getByElement( editor.document.getById( id ) );
178+
// @param {Boolean} [byElement] If true, the passed id has to be widget element's id.
179+
// Important for nested widgets, so parent widget is not mistakenly found.
180+
function getWidgetById( editor, id, byElement ) {
181+
var widget = editor.widgets.getByElement( editor.document.getById( id ) );
182+
183+
if ( widget && byElement )
184+
return widget.element.$.id == id ? widget : null;
185+
186+
return widget;
180187
}
181188

182189
// Retrives widget by its offset among parsed widgets.

tests/plugins/widget/nestededitables.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
data2Attr = widgetTestsTools.data2Attribute,
3838
getWidgetById = widgetTestsTools.getWidgetById;
3939

40+
function keysLength( obj ) {
41+
return CKEDITOR.tools.objectKeys( obj ).length;
42+
}
43+
4044
function testDelKey( editor, keyName, range, shouldBeBlocked, msg ) {
4145
range.select();
4246

@@ -164,6 +168,52 @@
164168
} );
165169
},
166170

171+
'test #destroyEditable destroys nested widgets': function() {
172+
var editor = this.editor;
173+
174+
editor.widgets.add( 'testmethods3', {
175+
editables: {
176+
foo: '#foo'
177+
}
178+
} );
179+
180+
this.editorBot.setData( '<div data-widget="testmethods3" id="w1"><p id="foo"><span data-widget="testmethods3" id="w2">x</span></p></div>', function() {
181+
var w1 = getWidgetById( editor, 'w1' ),
182+
w2 = getWidgetById( editor, 'w2' );
183+
184+
assert.areEqual( 2, keysLength( editor.widgets.instances ), '2 widgets were initialized' );
185+
186+
w1.destroyEditable( 'foo' );
187+
188+
assert.areEqual( 1, keysLength( editor.widgets.instances ), '1 widget reimained' );
189+
assert.isNull( getWidgetById( editor, 'w2', true ), 'nested widget was destroyed' );
190+
assert.isFalse( w2.element.getParent().hasAttribute( 'data-cke-widget-wrapper' ), 'widget was unwrapped' );
191+
} );
192+
},
193+
194+
// More precise tests can be found in widgetsrepoapi because this
195+
// methods uses repo#destroyAll with specified container.
196+
'test #destroyEditable in offline mode does not destroy nested widgets': function() {
197+
var editor = this.editor;
198+
199+
editor.widgets.add( 'testmethods4', {
200+
editables: {
201+
foo: '#foo'
202+
}
203+
} );
204+
205+
this.editorBot.setData( '<div data-widget="testmethods4" id="w1"><p id="foo"><span data-widget="testmethods4" id="w2">x</span></p></div>', function() {
206+
var w1 = getWidgetById( editor, 'w1' );
207+
208+
assert.areEqual( 2, keysLength( editor.widgets.instances ), '2 widgets were initialized' );
209+
210+
w1.destroyEditable( 'foo', true );
211+
212+
assert.areEqual( 2, keysLength( editor.widgets.instances ), '2 widgets reimained' );
213+
assert.isNotNull( getWidgetById( editor, 'w2', true ), 'nested widget was not destroyed' );
214+
} );
215+
},
216+
167217
'test nestedEditable enter modes are limited by ACF': function() {
168218
var editor = this.editor;
169219

@@ -293,6 +343,44 @@
293343
} );
294344
},
295345

346+
// For performance reasons.
347+
'test nestedEditable.setData - destroyAll(false,editable) is not called on first nestedEditable.setData': function() {
348+
var editor = this.editor;
349+
350+
editor.widgets.add( 'testsetdata3', {} );
351+
352+
this.editorBot.setData(
353+
'<div data-widget="testsetdata3" id="w1">' +
354+
'<p id="foo"></p>' +
355+
'</div>',
356+
function() {
357+
var w1 = getWidgetById( editor, 'w1' ),
358+
ed = editor.document.getById( 'foo' );
359+
360+
ed.setHtml( '<span data-widget="testsetdata3" id="w2">x</span><span data-widget="testsetdata3" id="w3">x</span>' );
361+
362+
assert.areEqual( 1, keysLength( editor.widgets.instances ), '1 widget was initialized' );
363+
364+
var original = editor.widgets.destroyAll,
365+
destroyAllCalls = 0,
366+
revert = bender.tools.replaceMethod( editor.widgets, 'destroyAll', function( offline, container ) {
367+
destroyAllCalls += 1;
368+
original.call( this, offline, container );
369+
} );
370+
371+
w1.initEditable( 'foo', { selector: '#foo' } );
372+
assert.areSame( 0, destroyAllCalls, 'destroyAll is not called on initial nestedEditable.setData' );
373+
assert.areEqual( 3, keysLength( editor.widgets.instances ), '3 widgets were initialized' );
374+
375+
w1.editables.foo.setData( '<span data-widget="testsetdata3" id="w2">x</span>' );
376+
377+
assert.areSame( 1, destroyAllCalls, 'destroyAll is called on 2nd+ nestedEditable.setData' );
378+
assert.areEqual( 2, keysLength( editor.widgets.instances ), '2 widgets reimained' );
379+
revert();
380+
}
381+
);
382+
},
383+
296384
'test nestedEditable.getData - data processor integration': function() {
297385
var editor = this.editor,
298386
data = '<p>Foo</p><div data-widget="testgetdata1" id="w1"><p>A</p><p id="foo">B</p></div>';

tests/plugins/widget/nestedwidgets.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,48 @@
257257
assert.areSame( wn1.wrapper, wn1.dragHandlerContainer.getParent(), '1st nested widget\'s drag handler is directly in the wrapper' );
258258
assert.areSame( wn2.wrapper, wn2.dragHandlerContainer.getParent(), '2nd nested widget\'s drag handler is directly in the wrapper' );
259259
} );
260+
},
261+
262+
'test all widgets are destroyed once when setting editor data': function() {
263+
var editor = this.editors.editor,
264+
bot = this.editorBots.editor,
265+
destroyed = [];
266+
267+
bot.setData( generateWidgetsData( 1 ), function() {
268+
for ( var id in editor.widgets.instances )
269+
editor.widgets.instances[ id ].on( 'destroy', log );
270+
271+
bot.setData( '', function() {
272+
assert.areSame( 'wn-0-0,wn-0-1,wp-0', destroyed.sort().join( ',' ), 'all widgets were destroyed' );
273+
} );
274+
} );
275+
276+
function log() {
277+
destroyed.push( this.element.$.id );
278+
}
279+
},
280+
281+
'test all nested widgets are destroyed when setting nested editable data': function() {
282+
var editor = this.editors.editor,
283+
bot = this.editorBots.editor,
284+
destroyed = [];
285+
286+
bot.setData( generateWidgetsData( 2 ), function() {
287+
var w1 = getWidgetById( editor, 'wp-0' );
288+
289+
for ( var id in editor.widgets.instances )
290+
editor.widgets.instances[ id ].on( 'destroy', log );
291+
292+
w1.editables.ned.setData( '<p>foo</p>' );
293+
assert.areSame( 'wn-0-0,wn-0-1', destroyed.sort().join( ',' ), 'all widgets were destroyed' );
294+
295+
// Clean up widgets in this test, so next won't fire listeners added above.
296+
editor.widgets.destroyAll();
297+
} );
298+
299+
function log() {
300+
destroyed.push( this.element.$.id );
301+
}
260302
}
261303

262304
} );

0 commit comments

Comments
 (0)