Skip to content

Commit ef06fd5

Browse files
committed
Merge branch 't/5217' into major
2 parents 00f129f + 77569a2 commit ef06fd5

File tree

8 files changed

+152
-52
lines changed

8 files changed

+152
-52
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ CKEditor 4 Changelog
99

1010
New Features:
1111

12+
* [#5217](http://dev.ckeditor.com/ticket/5217): Setting data (including switching between modes) creates new undo snapshot. Besides that:
13+
* Introduced the [`editable.status`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-property-status) property.
14+
* Introduced new option `forceUpdate` for the [`editor.lockSnapshot`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-lockSnapshot) event.
15+
* Fixed: selection not being unlocked in inline editor after setting data ([#15000](http://dev.ckeditor.com/ticket/11500)).
1216
* [#10202](http://dev.ckeditor.com/ticket/10202): Introduced wildcards support in the [Allowed Content Rules](http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules) format.
1317
* [#11532](http://dev.ckeditor.com/ticket/11532): Introduced the [`editor.addContentsCss()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-addContentsCss) method that can be used for adding custom CSS files.
1418
* [#11536](http://dev.ckeditor.com/ticket/11536): Added the [`CKEDITOR.tools.htmlDecode`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-htmlDecode) method for decoding HTML entities.

core/creators/themedui.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,20 +151,43 @@ CKEDITOR.replaceClass = 'ckeditor';
151151
editor.fire( 'beforeSetMode', newMode );
152152

153153
if ( editor.mode ) {
154-
var isDirty = editor.checkDirty();
155-
156-
editor._.previousMode = editor.mode;
154+
var isDirty = editor.checkDirty(),
155+
previousModeData = editor._.previousModeData,
156+
currentData,
157+
unlockSnapshot = 0;
157158

158159
editor.fire( 'beforeModeUnload' );
159160

160-
// Detach the current editable.
161+
// Detach the current editable. While detaching editable will set
162+
// cached editor's data (with internal setData call). We use this
163+
// data below to avoid two getData() calls in a row.
161164
editor.editable( 0 );
162165

166+
editor._.previousMode = editor.mode;
167+
// Get cached data, which was set while detaching editable.
168+
editor._.previousModeData = currentData = editor.getData( 1 );
169+
170+
// If data has not been modified in the mode which we are currently leaving,
171+
// avoid making snapshot right after initializing new mode.
172+
// http://dev.ckeditor.com/ticket/5217#comment:20
173+
// Tested by:
174+
// 'test switch mode with unrecoreded, inner HTML specific content (boguses)'
175+
// 'test switch mode with unrecoreded, inner HTML specific content (boguses) plus changes in source mode'
176+
if ( editor.mode == 'source' && previousModeData == currentData ) {
177+
// We need to make sure that unlockSnapshot will update the last snapshot
178+
// (will not create new one) if lockSnapshot is not called on outdated snapshots stack.
179+
// Additionally, forceUpdate prevents from making content image now, which is useless
180+
// (because it equals editor data not inner HTML).
181+
editor.fire( 'lockSnapshot', { forceUpdate: true } );
182+
unlockSnapshot = 1;
183+
}
184+
163185
// Clear up the mode space.
164186
editor.ui.space( 'contents' ).setHtml( '' );
165187

166188
editor.mode = '';
167-
}
189+
} else
190+
editor._.previousModeData = editor.getData( 1 );
168191

169192
// Fire the mode handler.
170193
this._.modes[ newMode ]( function() {
@@ -174,6 +197,14 @@ CKEDITOR.replaceClass = 'ckeditor';
174197
if ( isDirty !== undefined )
175198
!isDirty && editor.resetDirty();
176199

200+
if ( unlockSnapshot )
201+
editor.fire( 'unlockSnapshot' );
202+
// Since snapshot made on dataReady (which normally catches changes done by setData)
203+
// won't work because editor.mode was not set yet (it's set in this function), we need
204+
// to make special snapshot for changes done in source mode here.
205+
else if ( newMode == 'wysiwyg' )
206+
editor.fire( 'saveSnapshot' );
207+
177208
// Delay to avoid race conditions (setMode inside setMode).
178209
setTimeout( function() {
179210
editor.fire( 'mode' );

core/editable.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@
2828

2929
this.editor = editor;
3030

31+
/**
32+
* Indicates editable initialization status. The following statuses are available:
33+
*
34+
* * **unloaded**: the initial state; editable's instance has been created but
35+
* is not fully loaded (in particular has no data),
36+
* * **ready**: editable is fully initialized; `ready` status is set after
37+
* first {@link CKEDITOR.editor#method-setData} has been called.
38+
* * **detached**: the editable has been detached.
39+
*
40+
* @since 4.3.3
41+
* @readonly
42+
* @property {String}
43+
*/
44+
this.status = 'unloaded';
45+
3146
/**
3247
* Indicate whether the editable element has gained focus.
3348
*
@@ -398,6 +413,11 @@
398413
data = this.editor.dataProcessor.toHtml( data );
399414

400415
this.setHtml( data );
416+
417+
// Editable is ready after first setData.
418+
if ( this.status == 'unloaded' )
419+
this.status = 'ready';
420+
401421
this.editor.fire( 'dataReady' );
402422
},
403423

@@ -429,6 +449,8 @@
429449
// Cleanup the element.
430450
this.removeClass( 'cke_editable' );
431451

452+
this.status = 'detached';
453+
432454
// Save the editor reference which will be lost after
433455
// calling detach from super class.
434456
var editor = this.editor;

core/editor.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -935,10 +935,15 @@
935935
* @param {Boolean} internal Whether to suppress any event firing when copying data internally inside the editor.
936936
*/
937937
setData: function( data, callback, internal ) {
938-
if ( callback ) {
939-
this.on( 'dataReady', function( evt ) {
940-
evt.removeListener();
941-
callback.call( evt.editor );
938+
!internal && this.fire( 'saveSnapshot' );
939+
940+
if ( callback || !internal ) {
941+
this.once( 'dataReady', function( evt ) {
942+
if ( !internal )
943+
this.fire( 'saveSnapshot' );
944+
945+
if ( callback )
946+
callback.call( evt.editor );
942947
} );
943948
}
944949

core/selection.js

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,26 @@
773773
editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
774774
} );
775775

776-
// Clear the cached range path before unload. (#7174)
777-
editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor );
776+
editor.on( 'setData', function() {
777+
// Invalidate locked selection when unloading DOM.
778+
// (#9521, #5217#comment:32 and #11500#comment:11)
779+
editor.unlockSelection();
780+
781+
// Webkit's selection will mess up after the data loading.
782+
if ( CKEDITOR.env.webkit )
783+
clearSelection();
784+
} );
785+
786+
// Catch all the cases which above setData listener couldn't catch.
787+
// For example: switching to source mode and destroying editor.
788+
editor.on( 'contentDomUnload', function() {
789+
editor.unlockSelection();
790+
} );
791+
792+
// IE9 might cease to work if there's an object selection inside the iframe (#7639).
793+
if ( CKEDITOR.env.ie9Compat )
794+
editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
795+
778796
// Check selection change on data reload.
779797
editor.on( 'dataReady', function() {
780798
// Clean up fake selection after setting data.
@@ -783,6 +801,7 @@
783801

784802
editor.selectionChange( 1 );
785803
} );
804+
786805
// When loaded data are ready check whether hidden selection container was not loaded.
787806
editor.on( 'loadSnapshot', function() {
788807
// TODO replace with el.find() which will be introduced in #9764,
@@ -795,24 +814,6 @@
795814
el.remove();
796815
}, null, null, 100 );
797816

798-
function clearSelection() {
799-
var sel = editor.getSelection();
800-
sel && sel.removeAllRanges();
801-
}
802-
803-
// Clear dom selection before editable destroying to fix some browser
804-
// craziness.
805-
806-
// IE9 might cease to work if there's an object selection inside the iframe (#7639).
807-
CKEDITOR.env.ie9Compat && editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
808-
// Webkit's selection will mess up after the data loading.
809-
CKEDITOR.env.webkit && editor.on( 'setData', clearSelection );
810-
811-
// Invalidate locked selection when unloading DOM (e.g. after setData). (#9521)
812-
editor.on( 'contentDomUnload', function() {
813-
editor.unlockSelection();
814-
} );
815-
816817
editor.on( 'key', function( evt ) {
817818
if ( editor.mode != 'wysiwyg' )
818819
return;
@@ -825,6 +826,11 @@
825826
if ( handler )
826827
return handler( { editor: editor, selected: sel.getSelectedElement(), selection: sel, keyEvent: evt } );
827828
} );
829+
830+
function clearSelection() {
831+
var sel = editor.getSelection();
832+
sel && sel.removeAllRanges();
833+
}
828834
} );
829835

830836
CKEDITOR.on( 'instanceReady', function( evt ) {

plugins/sourcearea/plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
proto: {
9595
setData: function( data ) {
9696
this.setValue( data );
97+
this.status = 'ready';
9798
this.editor.fire( 'dataReady' );
9899
},
99100

plugins/undo/plugin.js

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,12 @@
160160
* @param data
161161
* @param {Boolean} [data.dontUpdate] When set to `true` the last snapshot will not be updated
162162
* with the current contents and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
163+
* @param {Boolean} [data.forceUpdate] When set to `true` the last snapshot will always be updated
164+
* with the current contents and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
163165
*/
164166
editor.on( 'lockSnapshot', function( evt ) {
165-
undoManager.lock( evt.data && evt.data.dontUpdate );
167+
var data = evt.data;
168+
undoManager.lock( data && data.dontUpdate, data && data.forceUpdate );
166169
} );
167170

168171
/**
@@ -414,15 +417,21 @@
414417
* Saves a snapshot of the document image for later retrieval.
415418
*/
416419
save: function( onContentOnly, image, autoFireChange ) {
417-
// Do not change snapshots stack when locked.
418-
if ( this.locked )
420+
var editor = this.editor;
421+
// Do not change snapshots stack when locked, editor is not ready,
422+
// editable is not ready or when editor is in mode difference than 'wysiwyg'.
423+
if ( this.locked || editor.status != 'ready' || editor.mode != 'wysiwyg' )
424+
return false;
425+
426+
var editable = editor.editable();
427+
if ( !editable || editable.status != 'ready' )
419428
return false;
420429

421430
var snapshots = this.snapshots;
422431

423432
// Get a content image.
424433
if ( !image )
425-
image = new Image( this.editor );
434+
image = new Image( editor );
426435

427436
// Do nothing if it was not possible to retrieve an image.
428437
if ( image.contents === false )
@@ -437,8 +446,7 @@
437446
if ( image.equalsSelection( this.currentImage ) )
438447
return false;
439448
} else
440-
this.editor.fire( 'change' );
441-
449+
editor.fire( 'change' );
442450
}
443451

444452
// Drop future snapshots.
@@ -627,25 +635,37 @@
627635
* done during the lock will be merged into the previous snapshot or the next one. Use this option to gain
628636
* more control over this behavior. For example, it is possible to group changes done during the lock into
629637
* a separate snapshot.
638+
* @param {Boolean} [forceUpdate] When set to `true` the last snapshot will always be updated with the
639+
* current contents and selection regardless of the current state of undo manager.
640+
* When not set the last snapshot will be updated only if undo manager was up to date when locking.
641+
* Additionally, this option makes it possible to lock snapshot when editor is not in `wysiwyg` mode,
642+
* because when it's passed snapshots will not need to be compared.
630643
*/
631-
lock: function( dontUpdate ) {
644+
lock: function( dontUpdate, forceUpdate ) {
632645
if ( !this.locked ) {
633646
if ( dontUpdate )
634647
this.locked = { level: 1 };
635648
else {
636-
// Make a contents image. Don't include bookmarks, because:
637-
// * we don't compare them,
638-
// * there's a chance that DOM has been changed since
639-
// locked (e.g. fake) selection was made, so createBookmark2 could fail.
640-
// http://dev.ckeditor.com/ticket/11027#comment:3
641-
var imageBefore = new Image( this.editor, true );
642-
643-
// If current editor content matches the tip of snapshot stack,
644-
// the stack tip must be updated by unlock, to include any changes made
645-
// during this period.
646-
var matchedTip = this.currentImage && this.currentImage.equalsContent( imageBefore );
647-
648-
this.locked = { update: matchedTip ? imageBefore : null, level: 1 };
649+
var update = null;
650+
651+
if ( forceUpdate )
652+
update = true;
653+
else {
654+
// Make a contents image. Don't include bookmarks, because:
655+
// * we don't compare them,
656+
// * there's a chance that DOM has been changed since
657+
// locked (e.g. fake) selection was made, so createBookmark2 could fail.
658+
// http://dev.ckeditor.com/ticket/11027#comment:3
659+
var imageBefore = new Image( this.editor, true );
660+
661+
// If current editor content matches the tip of snapshot stack,
662+
// the stack tip must be updated by unlock, to include any changes made
663+
// during this period.
664+
if ( this.currentImage && this.currentImage.equalsContent( imageBefore ) )
665+
update = imageBefore;
666+
}
667+
668+
this.locked = { update: update, level: 1 };
649669
}
650670
}
651671
// Increase the level of lock.
@@ -664,13 +684,20 @@
664684
if ( this.locked ) {
665685
// Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
666686
if ( !--this.locked.level ) {
667-
var updateImage = this.locked.update,
668-
newImage = updateImage && new Image( this.editor, true );
687+
var update = this.locked.update;
669688

670689
this.locked = null;
671690

672-
if ( updateImage && !updateImage.equalsContent( newImage ) )
691+
// forceUpdate was passed to lock().
692+
if ( update === true )
673693
this.update();
694+
// update is instance of Image.
695+
else if ( update ) {
696+
var newImage = new Image( this.editor, true );
697+
698+
if ( !update.equalsContent( newImage ) )
699+
this.update();
700+
}
674701
}
675702
}
676703
}

plugins/wysiwygarea/plugin.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@
299299
editor.document.$.title = this._.docTitle;
300300

301301
CKEDITOR.tools.setTimeout( function() {
302+
// Editable is ready after first setData.
303+
if ( this.status == 'unloaded' )
304+
this.status = 'ready';
305+
302306
editor.fire( 'contentDom' );
303307

304308
if ( this._.isPendingFocus ) {

0 commit comments

Comments
 (0)