Skip to content

Commit

Permalink
Merge branch 't/12491'
Browse files Browse the repository at this point in the history
  • Loading branch information
Reinmar committed Sep 29, 2014
2 parents d6b338a + 5bb8c38 commit 958d7c9
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 35 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Expand Up @@ -3,6 +3,10 @@ CKEditor 4 Changelog

## CKEditor 4.4.6

Fixed Issues:

* [#12489](http://dev.ckeditor.com/ticket/12423) and [#12491](http://dev.ckeditor.com/ticket/12423): Fixed: Various issues related to restoring selection after making operations on filler char. See the [fixed cases](http://dev.ckeditor.com/ticket/12491#comment:4).

## CKEditor 4.4.5

New Features:
Expand Down
71 changes: 36 additions & 35 deletions core/selection.js
Expand Up @@ -149,25 +149,20 @@

// Text selection position might get mangled by
// subsequent dom modification, save it now for restoring. (#8617)
if ( keepSelection !== false )
{
if ( keepSelection !== false ) {
var bm,
doc = element.getDocument(),
sel = doc.getSelection().getNative(),
sel = element.getDocument().getSelection().getNative(),
// Be error proof.
range = sel && sel.type != 'None' && sel.getRangeAt( 0 );

if ( fillingChar.getLength() > 1 && range && range.intersectsNode( fillingChar.$ ) ) {
bm = [ sel.anchorOffset, sel.focusOffset ];
bm = createNativeSelectionBookmark( sel );

// Anticipate the offset change brought by the removed char.
var startAffected = sel.anchorNode == fillingChar.$ && sel.anchorOffset > 0,
endAffected = sel.focusNode == fillingChar.$ && sel.focusOffset > 0;
startAffected && bm[ 0 ]--;
endAffected && bm[ 1 ]--;

// Revert the bookmark order on reverse selection.
isReversedSelection( sel ) && bm.unshift( bm.pop() );
startAffected && bm[ 0 ].offset--;
endAffected && bm[ 1 ].offset--;
}
}

Expand All @@ -176,13 +171,9 @@
// invisible char from it.
fillingChar.setText( replaceFillingChar( fillingChar.getText() ) );

// Restore the bookmark.
// Restore the bookmark preserving selection's direction.
if ( bm ) {
var rng = sel.getRangeAt( 0 );
rng.setStart( rng.startContainer, bm[ 0 ] );
rng.setEnd( rng.startContainer, bm[ 1 ] );
sel.removeAllRanges();
sel.addRange( rng );
moveNativeSelectionToBookmark( element.getDocument().$, bm );
}
}
}
Expand All @@ -194,14 +185,22 @@
} );
}

function isReversedSelection( sel ) {
if ( !sel.isCollapsed ) {
var range = sel.getRangeAt( 0 );
// Potentially alter an reversed selection range.
range.setStart( sel.anchorNode, sel.anchorOffset );
range.setEnd( sel.focusNode, sel.focusOffset );
return range.collapsed;
}
function createNativeSelectionBookmark( sel ) {
return [
{ node: sel.anchorNode, offset: sel.anchorOffset },
{ node: sel.focusNode, offset: sel.focusOffset }
];
}

function moveNativeSelectionToBookmark( document, bm ) {
var sel = document.getSelection(),
range = document.createRange();

range.setStart( bm[ 0 ].node, bm[ 0 ].offset );
range.collapse( true );
sel.removeAllRanges();
sel.addRange( range );
sel.extend( bm[ 1 ].node, bm[ 1 ].offset );
}

// Read the comments in selection constructor.
Expand Down Expand Up @@ -840,7 +839,9 @@
} );

CKEDITOR.on( 'instanceReady', function( evt ) {
var editor = evt.editor;
var editor = evt.editor,
fillingCharBefore,
selectionBookmark;

// On WebKit only, we need a special "filling" char on some situations
// (#1272). Here we set the events that should invalidate that char.
Expand All @@ -852,8 +853,6 @@
removeFillingChar( editor.editable() );
}, null, null, -1 );

var fillingCharBefore, resetSelection;

editor.on( 'beforeUndoImage', beforeData );
editor.on( 'afterUndoImage', afterData );
editor.on( 'beforeGetData', beforeData, null, null, 0 );
Expand All @@ -868,11 +867,13 @@
var fillingChar = getFillingChar( editable );

if ( fillingChar ) {
// If cursor is right blinking by side of the filler node, save it for restoring,
// as the following text substitution will blind it. (#7437)
var sel = editor.document.$.defaultView.getSelection();
if ( sel.type == 'Caret' && sel.anchorNode == fillingChar.$ )
resetSelection = 1;
// If the selection's focus or anchor is located in the filling char's text node,
// we need to restore the selection in afterData, because it will be lost
// when setting text. Selection's direction must be preserved.
// (#7437, #12489, #12491 comment:3)
var sel = editor.document.$.getSelection();
if ( sel.type != 'None' && ( sel.anchorNode == fillingChar.$ || sel.focusNode == fillingChar.$ ) )
selectionBookmark = createNativeSelectionBookmark( sel );

fillingCharBefore = fillingChar.getText();
fillingChar.setText( replaceFillingChar( fillingCharBefore ) );
Expand All @@ -889,9 +890,9 @@
if ( fillingChar ) {
fillingChar.setText( fillingCharBefore );

if ( resetSelection ) {
editor.document.$.defaultView.getSelection().setPosition( fillingChar.$, fillingChar.getLength() );
resetSelection = 0;
if ( selectionBookmark ) {
moveNativeSelectionToBookmark( editor.document.$, selectionBookmark );
selectionBookmark = null;
}
}
}
Expand Down
226 changes: 226 additions & 0 deletions tests/core/selection/editor.js
Expand Up @@ -37,6 +37,27 @@ bender.test( {
assert.areSame( expected || source, bender.tools.getHtmlWithSelection( ed ) );
},

setSelectionInEmptyInlineElement: function( editor ) {
var editable = editor.editable(),
range = editor.createRange();

editable.setHtml( '<p>x<u></u>x</p>' );

var uEl = editable.findOne( 'u' );

range.moveToPosition( uEl, CKEDITOR.POSITION_AFTER_START );
editor.getSelection().selectRanges( [ range ] );
},

assertFillingChar: function( editable, parent, contents, msg ) {
var fillingChar = editable.getCustomData( 'cke-fillingChar' );
assert.isTrue( !!fillingChar, 'Filling char exists - ' + msg );
assert.areSame( parent, fillingChar.getParent(), 'Filling char parent - ' + msg );
assert.areSame( contents, fillingChar.getText(), 'Filling char contents - ' + msg );

return fillingChar;
},

'test editor selection with no focus' : function() {
var ed = this.editor;

Expand Down Expand Up @@ -444,6 +465,211 @@ bender.test( {
} );
},

'test filling char is removed and restored when taking snapshot': function() {
if ( !CKEDITOR.env.webkit )
assert.ignore();

var editor = this.editor,
bot = this.editorBot,
editable = editor.editable(),
range = editor.createRange();

this.setSelectionInEmptyInlineElement( editor );

var uEl = editable.findOne( 'u' ),
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after set selection' );

editor.fire( 'beforeUndoImage' );
this.assertFillingChar( editable, uEl, '', 'after beforeUndoImage' );

editor.fire( 'afterUndoImage' );
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after afterUndoImage' );

range = editor.getSelection().getRanges()[ 0 ];
assert.areSame( fillingChar, range.startContainer, 'Selection was restored - container' );
assert.areSame( 1, range.startOffset, 'Selection was restored - offset after ZWS' );
},

// #12489
'test filling char is removed and restored when taking snapshot if selection is not right after the filling char': function() {
if ( !CKEDITOR.env.webkit )
assert.ignore();

var editor = this.editor,
bot = this.editorBot,
editable = editor.editable(),
range = editor.createRange();

this.setSelectionInEmptyInlineElement( editor );

var uEl = editable.findOne( 'u' ),
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after set selection' );

// Happens when typing and navigating...
// Setting selection using native API to avoid losing the filling char on selection.setRanges().
fillingChar.setText( fillingChar.getText() + 'abcd' );
editor.document.$.getSelection().setPosition( fillingChar.$, 3 ); // ZWSab^cd

this.assertFillingChar( editable, uEl, '\u200babcd', 'after type' );

editor.fire( 'beforeUndoImage' );
this.assertFillingChar( editable, uEl, 'abcd', 'after beforeUndoImage' );

editor.fire( 'afterUndoImage' );
fillingChar = this.assertFillingChar( editable, uEl, '\u200babcd', 'after afterUndoImage' );

range = editor.getSelection().getRanges()[ 0 ];
assert.areSame( fillingChar, range.startContainer, 'Selection was restored - container' );
assert.areSame( 3, range.startOffset, 'Selection was restored - offset in ZWSab^cd' );
},

// #8617
'test selection is preserved when removing filling char on left-arrow': function() {
if ( !CKEDITOR.env.webkit )
assert.ignore();

var editor = this.editor,
bot = this.editorBot,
editable = editor.editable(),
range = editor.createRange();

this.setSelectionInEmptyInlineElement( editor );

var uEl = editable.findOne( 'u' ),
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after setting selection' );

// Happens when typing and navigating...
// Setting selection using native API to avoid losing the filling char on selection.setRanges().
fillingChar.setText( fillingChar.getText() + 'abc' );
editor.document.$.getSelection().setPosition( fillingChar.$, 4 ); // ZWSabc^

this.assertFillingChar( editable, uEl, '\u200babc', 'after typing' );

// Mock LEFT arrow.
editor.document.fire( 'keydown', new CKEDITOR.dom.event( { keyCode: 37 } ) );

assert.areSame( 'abc', uEl.getHtml(), 'Filling char is removed on left-arrow press' );

range = editor.getSelection().getRanges()[ 0 ];
assert.areSame( uEl.getFirst(), range.startContainer, 'Selection was restored - container' );
assert.areSame( 3, range.startOffset, 'Selection was restored - offset in abc^' );
},

// #12419
'test selection is preserved when removing filling char on select all': function() {
if ( !CKEDITOR.env.webkit )
assert.ignore();

var editor = this.editor,
bot = this.editorBot,
editable = editor.editable(),
range = editor.createRange(),
htmlMatchingOpts = {
compareSelection: true,
normalizeSelection: true
};

this.setSelectionInEmptyInlineElement( editor );

var uEl = editable.findOne( 'u' ),
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after setting selection' );

// Happens when typing and navigating...
// Setting selection using native API to avoid losing the filling char on selection.setRanges().
fillingChar.setText( fillingChar.getText() + 'abc' );
editor.document.$.getSelection().setPosition( fillingChar.$, 4 ); // ZWSabc^

this.assertFillingChar( editable, uEl, '\u200babc', 'after typing' );

// Select all contents.
range.selectNodeContents( editable.findOne( 'p' ) );
editor.getSelection().selectRanges( [ range ] );

assert.areSame( 'abc', uEl.getHtml(), 'Filling char is removed on selection change' );
assert.isInnerHtmlMatching( '<p>[x<u>abc</u>x]</p>', bender.tools.selection.getWithHtml( editor ),
htmlMatchingOpts, 'Selection is correctly set' );
},

'test direction of selection is preserved when removing filling char': function() {
if ( !CKEDITOR.env.webkit )
assert.ignore();

var editor = this.editor,
bot = this.editorBot,
editable = editor.editable(),
range = editor.createRange();

this.setSelectionInEmptyInlineElement( editor );

var uEl = editable.findOne( 'u' ),
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after setting selection' );

// Happens when typing and making selection from right to left...
// Setting selection using native API to avoid losing the filling char on selection.setRanges().
fillingChar.setText( fillingChar.getText() + 'abc' );
range = editor.document.$.createRange();
// ZWSabc]
range.setStart( fillingChar.$, 4 );
var nativeSel = editor.document.$.getSelection();
nativeSel.removeAllRanges();
nativeSel.addRange( range );
// ZWSa[bc
nativeSel.extend( fillingChar.$, 2 );

this.assertFillingChar( editable, uEl, '\u200babc', 'after typing' );

// Mock LEFT arrow.
editor.document.fire( 'keydown', new CKEDITOR.dom.event( { keyCode: 37 } ) );

assert.areSame( 'abc', uEl.getHtml(), 'Filling char is removed on left-arrow press' );

nativeSel = editor.document.$.getSelection();
assert.areSame( 3, nativeSel.anchorOffset, 'sel.anchorOffset' );
assert.areSame( 1, nativeSel.focusOffset, 'sel.focusOffset' );
},

// This particular scenario is reproducible when after typing in an empty inline element
// user tries to select text by mouse from right to left in that element - selection is lost.
// #12491 comment:3
'test direction of selection is preserved when taking snapshot': function() {
if ( !CKEDITOR.env.webkit )
assert.ignore();

var editor = this.editor,
bot = this.editorBot,
editable = editor.editable(),
range = editor.createRange();

this.setSelectionInEmptyInlineElement( editor );

var uEl = editable.findOne( 'u' ),
fillingChar = this.assertFillingChar( editable, uEl, '\u200b', 'after set selection' );

// Happens when typing and making selection from right to left...
// Setting selection using native API to avoid losing the filling char on selection.setRanges().
fillingChar.setText( fillingChar.getText() + 'abc' );
range = editor.document.$.createRange();
// ZWSabc]
range.setStart( fillingChar.$, 4 );
var nativeSel = editor.document.$.getSelection();
nativeSel.removeAllRanges();
nativeSel.addRange( range );
// ZWSa[bc
nativeSel.extend( fillingChar.$, 2 );

this.assertFillingChar( editable, uEl, '\u200babc', 'after type' );

editor.fire( 'beforeUndoImage' );
this.assertFillingChar( editable, uEl, 'abc', 'after beforeUndoImage' );

editor.fire( 'afterUndoImage' );
this.assertFillingChar( editable, uEl, '\u200babc', 'after afterUndoImage' );

nativeSel = editor.document.$.getSelection();
assert.areSame( 4, nativeSel.anchorOffset, 'sel.anchorOffset' );
assert.areSame( 2, nativeSel.focusOffset, 'sel.focusOffset' );
},

'test selection in source mode': function() {
bender.editorBot.create( {
name: 'test_editor_source',
Expand Down

0 comments on commit 958d7c9

Please sign in to comment.