diff --git a/CHANGES.md b/CHANGES.md index ac22e95c21a..6eb1b30c84f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ Fixed Issues: * [#12546](http://dev.ckeditor.com/ticket/12546): Fixed: Preview tab in docprops dialog is always disabled. * [#12300](http://dev.ckeditor.com/ticket/12300): Fixed: The [`editor.change`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-change) event fired on first navigation key press after typing. * [#12141](http://dev.ckeditor.com/ticket/12141): Fixed: List items are lost when indenting a list item with a content wrapped with a block element. +* [#12515](http://dev.ckeditor.com/ticket/12515): Fixed: Cursor is in the wrong position when executing undo command after adding image and typing some text. Other Changes: * [#12550](http://dev.ckeditor.com/ticket/12550): Added CKEDITOR.dtd.main. diff --git a/core/dom/node.js b/core/dom/node.js index 6803ca139ae..826a24f4ecc 100644 --- a/core/dom/node.js +++ b/core/dom/node.js @@ -265,18 +265,24 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, { /** * Gets the index of a node in an array of its `parent.childNodes`. + * Returns `-1` if node does not have a parent or when `normalized` argument is set to `true` + * and the text node is empty and will be removed while normalization. * * Let us assume having the following `childNodes` array: * - * [ emptyText, element1, text, text, element2 ] - * element1.getIndex(); // 1 - * element1.getIndex( true ); // 0 - * element2.getIndex(); // 4 - * element2.getIndex( true ); // 2 + * [ emptyText, element1, text, text, element2, emptyText2 ] * - * @param {Boolean} normalized When `true`, empty text nodes and one followed - * by another one text node are not counted in. - * @returns {Number} Index of a node. + * emptyText.getIndex() // 0 + * emptyText.getIndex( true ) // -1 + * element1.getIndex(); // 1 + * element1.getIndex( true ); // 0 + * element2.getIndex(); // 4 + * element2.getIndex( true ); // 2 + * emptyText2.getIndex(); // 5 + * emptyText2.getIndex( true ); // -1 + * + * @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed. + * @returns {Number} Index of a node or `-1` if node does not have a parent or is removed while normalization. */ getIndex: function( normalized ) { // Attention: getAddress depends on this.$ @@ -287,7 +293,17 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, { isNormalizing; if ( !this.$.parentNode ) - return index; + return -1; + + // The idea is - all empty text nodes will be virtually merged into their adjacent text nodes. + // If an empty text node does not have an adjacent non-empty text node we can return -1 straight away, + // because it and all its sibling text nodes will be merged into an empty text node and then totally ignored. + if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && !current.nodeValue ) { + var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true ); + + if ( !adjacent ) + return -1; + } do { // Bypass blank node and adjacent text nodes. @@ -300,6 +316,18 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, { while ( ( current = current.previousSibling ) ); return index; + + function getAdjacentNonEmptyTextNode( node, lookForward ) { + var sibling = lookForward ? node.nextSibling : node.previousSibling; + + if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) { + return null; + } + + // If found a non-empty text node, then return it. + // If not, then continue search. + return sibling.nodeValue ? sibling : getAdjacentNonEmptyTextNode( sibling, lookForward ); + } }, /** diff --git a/core/dom/range.js b/core/dom/range.js index 1c6aab568fd..f4a4c3f8944 100644 --- a/core/dom/range.js +++ b/core/dom/range.js @@ -624,6 +624,8 @@ CKEDITOR.dom.range = function( root ) { * @returns {Boolean} return.is2 This is "bookmark2". */ createBookmark2: ( function() { + var isNotText = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_TEXT, true ); + // Returns true for limit anchored in element and placed between text nodes. // // v @@ -672,8 +674,43 @@ CKEDITOR.dom.range = function( root ) { // The last step - fix the offset inside text node by adding // lengths of preceding text nodes which will be merged with container. - if ( container.type == CKEDITOR.NODE_TEXT ) - offset += getLengthOfPrecedingTextNodes( container ); + if ( container.type == CKEDITOR.NODE_TEXT ) { + var precedingLength = getLengthOfPrecedingTextNodes( container ); + + // Normal case - text node is not empty. + if ( container.getText() ) { + offset += precedingLength; + + // Awful case - the text node is empty and thus will be totally lost. + // In this case we are trying to normalize the limit to the left: + // * either to the preceding text node, + // * or to the "gap" after the preceding element. + } else { + // Find the closest non-text sibling. + var precedingContainer = container.getPrevious( isNotText ); + + // If there are any characters on the left, that means that we can anchor + // there, because this text node will not be lost. + if ( precedingLength ) { + offset = precedingLength; + + if ( precedingContainer ) { + // The text node is the first node after the closest non-text sibling. + container = precedingContainer.getNext(); + } else { + // But if there was no non-text sibling, then the text node is the first child. + container = container.getParent().getFirst(); + } + + // If there are no characters on the left, then anchor after the previous non-text node. + // E.g. (see tests for a legend :D): + // x(foo)({}bar) -> x[](foo)(bar) + } else { + container = container.getParent(); + offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0; + } + } + } limit.container = container; limit.offset = offset; diff --git a/tests/core/dom/node.js b/tests/core/dom/node.js index 0eda5dc305a..5d8314a6092 100644 --- a/tests/core/dom/node.js +++ b/tests/core/dom/node.js @@ -3,8 +3,7 @@ ( function() { 'use strict'; - var getInnerHtml = bender.tools.getInnerHtml, - getOuterHtml = function( element ) { + var getOuterHtml = function( element ) { return bender.tools.fixHtml( element.getOuterHtml() ); }, $ = function( id ) { @@ -51,7 +50,7 @@ bender.test( { - test_$ : function() { + test_$: function() { var t = newTextNode( 'text' ), c = newComment( 'comment' ), e = newElement( 'span' ); @@ -61,76 +60,76 @@ assert.isTrue( e instanceof CKEDITOR.dom.element, 'Should be an instanceof dom.element' ); }, - test_getPosition1 : function() { + test_getPosition1: function() { var node1 = getNodeByTagName( 'h1' ); var node2 = getNodeByTagName( 'p' ); assert.areSame( CKEDITOR.POSITION_PRECEDING, node1.getPosition( node2 ) ); }, - test_getPosition2 : function() { + test_getPosition2: function() { var node1 = getNodeByTagName( 'h1' ); var node2 = getNodeByTagName( 'p' ); assert.areSame( CKEDITOR.POSITION_FOLLOWING, node2.getPosition( node1 ) ); }, - test_getPosition3 : function() { + test_getPosition3: function() { var node1 = getNodeByTagName( 'p' ); var node2 = getNodeByTagName( 'b' ); assert.areSame( CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING, node1.getPosition( node2 ) ); }, - test_getPosition4 : function() { + test_getPosition4: function() { var node1 = getNodeByTagName( 'p' ); var node2 = getNodeByTagName( 'b' ); assert.areSame( CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING, node2.getPosition( node1 ) ); }, - test_getPosition5 : function() { + test_getPosition5: function() { var node1 = getNodeByTagName( 'div' ); var node2 = getNodeByTagName( 'div' ); assert.areSame( CKEDITOR.POSITION_IDENTICAL, node1.getPosition( node2 ) ); }, - test_getPosition6 : function() { + test_getPosition6: function() { var node1 = getNodeByTagName( 'h1' ); var node2 = newNode( $tn( 'h1' )[ 0 ].firstChild ); assert.areSame( CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING, node1.getPosition( node2 ) ); }, - test_getPosition7 : function() { + test_getPosition7: function() { var node1 = getNodeByTagName( 'h1' ); var node2 = newNode( $tn( 'h1' )[ 0 ].firstChild ); assert.areSame( CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING, node2.getPosition( node1 ) ); }, - test_getPosition8 : function() { + test_getPosition8: function() { var node1 = newNode( $tn( 'h1' )[ 0 ].firstChild ); var node2 = newNode( $tn( 'b' )[ 0 ].firstChild ); assert.areSame( CKEDITOR.POSITION_PRECEDING, node1.getPosition( node2 ) ); }, - test_getPosition9 : function() { + test_getPosition9: function() { var node1 = newNode( $tn( 'h1' )[ 0 ].firstChild ); var node2 = newNode( $tn( 'b' )[ 0 ].firstChild ); assert.areSame( CKEDITOR.POSITION_FOLLOWING, node2.getPosition( node1 ) ); }, - test_getPosition10 : function() { + test_getPosition10: function() { var node1 = newNode( $tn( 'b' )[ 0 ] ); var node2 = newNode( $tn( 'i' )[ 0 ] ); assert.areSame( CKEDITOR.POSITION_PRECEDING, node1.getPosition( node2 ) ); }, - test_getPosition11 : function() { + test_getPosition11: function() { var node1 = newNode( $tn( 'b' )[ 0 ] ); var node2 = newNode( $tn( 'i' )[ 0 ] ); @@ -148,7 +147,7 @@ }, // Test get previous non-spaces node. - test_getPrevious : function() { + test_getPrevious: function() { var element = $( 'append' ); var span1 = newElement( 'span' ); element.append( span1 ); @@ -159,17 +158,16 @@ assert.areSame( span1.$, previous.$ ); }, - test_getPrevious2 : function() { + test_getPrevious2: function() { var node = $( 'getNSN6' ); assert.areSame( CKEDITOR.NODE_TEXT, node.getPrevious().type ); assert.areSame( $( 'getNSN5' ), node.getPrevious( function( node ) { - return node.type === CKEDITOR.NODE_ELEMENT; - } - ) ); + return node.type === CKEDITOR.NODE_ELEMENT; + } ) ); }, // Test get next non-spaces node. - test_getNext : function() { + test_getNext: function() { var element = $( 'append' ); var span1 = newElement( 'span' ); element.append( span1 ); @@ -180,17 +178,16 @@ assert.areSame( span2.$, next.$ ); }, - test_getNext2 : function() { + test_getNext2: function() { var node = $( 'getNSN1' ); assert.areSame( CKEDITOR.NODE_TEXT, node.getNext().type ); assert.areSame( $( 'getNSN2' ), node.getNext( function( node ) { - return node.type === CKEDITOR.NODE_ELEMENT; - } - ) ); + return node.type === CKEDITOR.NODE_ELEMENT; + } ) ); }, // element::isReadOnly tests. - test_isReadOnly : function() { + test_isReadOnly: function() { var target = $( 'editable' ), body = target.getParent(); assert.isTrue( body.isReadOnly(), 'Body is not editable' ); assert.isFalse( target.isReadOnly(), 'Element specify itself as editable.' ); @@ -208,7 +205,7 @@ assert.isFalse( target.getFirst().isReadOnly(), 'Element marked as "cke-editable" is not ready-only.' ); }, - test_appendTo : function() { + test_appendTo: function() { var p = newElement( 'p' ), t = newTextNode( 'text' ), c = newComment( 'comment' ), @@ -224,7 +221,7 @@ assert.areSame( CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING, b.getPosition( p ) ); }, - test_clone1 : function() { + test_clone1: function() { var a = $( 'clone1' ); assert.areSame( '

', getOuterHtml( a.clone() ) ); @@ -237,7 +234,7 @@ ); }, - test_clone2 : function() { + test_clone2: function() { var t = newTextNode( 'text' ), c = newComment( 'comment' ); @@ -245,12 +242,12 @@ assert.areSame( '', c.clone().getOuterHtml() ); }, - test_clone3 : function() { + test_clone3: function() { var a = $( 'clone1' ); assert.areNotEqual( a.getUniqueId(), a.clone().getUniqueId() ); }, - test_clone4 : function() { + test_clone4: function() { var t = new CKEDITOR.dom.text( 'text' ), c = new CKEDITOR.dom.comment( 'comment' ), e = $( 'clone' ); @@ -260,7 +257,7 @@ assert.isTrue( e.clone() instanceof CKEDITOR.dom.element ); }, - test_hasNext : function() { + test_hasNext: function() { var node1 = getNodeByTagName( 'b' ), node2 = getNodeByTagName( 'i' ); @@ -268,7 +265,7 @@ assert.isFalse( node2.hasNext() ); }, - test_hasPrevious : function() { + test_hasPrevious: function() { var node1 = getNodeByTagName( 'b' ), node2 = getNodeByTagName( 'i' ); @@ -276,7 +273,7 @@ assert.isTrue( node2.hasPrevious() ); }, - test_insertAfter1 : function() { + test_insertAfter1: function() { var c = newNode( $( 'insertAfter' ).$.firstChild ), t = newNode( $( 'insertAfter' ).$.lastChild ), e1 = newElement( 'i' ), @@ -292,7 +289,7 @@ }, // test if other types of nodes can be inserted too - test_insertAfter2 : function() { + test_insertAfter2: function() { var node = newNode( $( 'insertAfter' ).$.lastChild ), c = newComment( 'comment' ), t = newTextNode( 'text' ); @@ -304,7 +301,7 @@ assert.areSame( t, node.getNext() ); }, - test_insertBefore : function() { + test_insertBefore: function() { var c = newNode( $( 'insertBefore' ).$.firstChild ), t = newNode( $( 'insertBefore' ).$.lastChild ), e1 = newElement( 'i' ), @@ -320,7 +317,7 @@ }, // test if other types of nodes can be inserted too - test_insertBefore2 : function() { + test_insertBefore2: function() { var node = newNode( $( 'insertBefore' ).$.firstChild ), c = newComment( 'comment' ), t = newTextNode( 'text' ); @@ -332,7 +329,7 @@ assert.areSame( t, node.getPrevious() ); }, - test_insertBeforeMe : function() { + test_insertBeforeMe: function() { var node1 = newElement( 'span' ), node2 = $( 'insertBefore' ); @@ -341,7 +338,7 @@ assert.areSame( node1.getNext(), node2 ); }, - test_getAddress : function() { + test_getAddress: function() { // slice (2) - removes body>div part var address1 = $( 'getAddress1' ).getAddress().slice( 2 ), address2 = $( 'getAddress2' ).getAddress().slice( 2 ); @@ -363,7 +360,7 @@ */ }, - test_getDocument : function() { + test_getDocument: function() { var doc = CKEDITOR.document, iframe = $( 'getDocument' ), docIframe = iframe.getFrameDocument(); @@ -379,7 +376,7 @@ assert.areSame( childContext.document, docIframe.$ ); }, - 'getIndex - single node' : function() { + 'getIndex - single node': function() { var wrapper = createGetIndexTest( 'el' ), node = wrapper.getFirst(); @@ -387,7 +384,7 @@ assert.areEqual( 0, node.getIndex( true ) ); }, - 'getIndex - two elements' : function() { + 'getIndex - two elements': function() { var wrapper = createGetIndexTest( 'el,el' ), node2 = wrapper.getChild( 1 ); @@ -395,7 +392,7 @@ assert.areEqual( 1, node2.getIndex( true ) ); }, - 'getIndex - element after text node' : function() { + 'getIndex - element after text node': function() { var wrapper = createGetIndexTest( 'tn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ); @@ -406,29 +403,18 @@ assert.areEqual( 1, node2.getIndex( true ) ); }, - 'getIndex - element after empty text node1' : function() { + 'getIndex - element after empty text node1': function() { var wrapper = createGetIndexTest( 'etn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ); assert.areEqual( 0, node1.getIndex() ); - assert.areEqual( 0, node1.getIndex( true ) ); + assert.areEqual( -1, node1.getIndex( true ) ); assert.areEqual( 1, node2.getIndex() ); assert.areEqual( 0, node2.getIndex( true ) ); }, - 'getIndex - element after empty text node2' : function() { - var wrapper = createGetIndexTest( 'etn,el' ), - node1 = wrapper.getChild( 0 ), - node2 = wrapper.getChild( 1 ); - - assert.areEqual( 0, node1.getIndex() ); - assert.areEqual( 0, node1.getIndex( true ) ); - assert.areEqual( 1, node2.getIndex() ); - assert.areEqual( 0, node2.getIndex( true ) ); - }, - - 'getIndex - etn, tn, el' : function() { + 'getIndex - etn, tn, el': function() { var wrapper = createGetIndexTest( 'etn,tn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ), @@ -442,7 +428,7 @@ assert.areEqual( 1, node3.getIndex( true ) ); }, - 'getIndex - tn, etn, el' : function() { + 'getIndex - tn, etn, el': function() { var wrapper = createGetIndexTest( 'tn,etn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ), @@ -456,21 +442,21 @@ assert.areEqual( 1, node3.getIndex( true ) ); }, - 'getIndex - etn, etn, el' : function() { + 'getIndex - etn, etn, el': function() { var wrapper = createGetIndexTest( 'etn,etn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ), node3 = wrapper.getChild( 2 ); assert.areEqual( 0, node1.getIndex() ); - assert.areEqual( 0, node1.getIndex( true ) ); + assert.areEqual( -1, node1.getIndex( true ) ); assert.areEqual( 1, node2.getIndex() ); - assert.areEqual( 0, node2.getIndex( true ) ); + assert.areEqual( -1, node2.getIndex( true ) ); assert.areEqual( 2, node3.getIndex() ); assert.areEqual( 0, node3.getIndex( true ) ); }, - 'getIndex - etn, tn, etn, el' : function() { + 'getIndex - etn, tn, etn, el': function() { var wrapper = createGetIndexTest( 'etn,tn,etn,el' ), node2 = wrapper.getChild( 1 ), node3 = wrapper.getChild( 2 ), @@ -484,7 +470,7 @@ assert.areEqual( 1, node4.getIndex( true ) ); }, - 'getIndex - el, tn, el' : function() { + 'getIndex - el, tn, el': function() { var wrapper = createGetIndexTest( 'el,tn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ), @@ -498,7 +484,7 @@ assert.areEqual( 2, node3.getIndex( true ) ); }, - 'getIndex - el, etn, el' : function() { + 'getIndex - el, etn, el': function() { var wrapper = createGetIndexTest( 'el,etn,el' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ), @@ -507,12 +493,12 @@ assert.areEqual( 0, node1.getIndex() ); assert.areEqual( 0, node1.getIndex( true ) ); assert.areEqual( 1, node2.getIndex() ); - assert.areEqual( 1, node2.getIndex( true ) ); + assert.areEqual( -1, node2.getIndex( true ) ); assert.areEqual( 2, node3.getIndex() ); assert.areEqual( 1, node3.getIndex( true ) ); }, - 'getIndex - el, etn, tn, el' : function() { + 'getIndex - el, etn, tn, el': function() { var wrapper = createGetIndexTest( 'el,etn,tn,el' ), node2 = wrapper.getChild( 1 ), node3 = wrapper.getChild( 2 ), @@ -526,7 +512,7 @@ assert.areEqual( 2, node4.getIndex( true ) ); }, - 'getIndex - el, tn, etn, el' : function() { + 'getIndex - el, tn, etn, el': function() { var wrapper = createGetIndexTest( 'el,tn,etn,el' ), node2 = wrapper.getChild( 1 ), node3 = wrapper.getChild( 2 ), @@ -540,18 +526,54 @@ assert.areEqual( 2, node4.getIndex( true ) ); }, - 'getIndex - el, etn' : function() { + 'getIndex - el, etn': function() { var wrapper = createGetIndexTest( 'el,etn' ), node1 = wrapper.getChild( 0 ), node2 = wrapper.getChild( 1 ); assert.areEqual( 0, node1.getIndex() ); assert.areEqual( 0, node1.getIndex( true ) ); + assert.areEqual( 1, node2.getIndex() ); + assert.areEqual( -1, node2.getIndex( true ) ); + }, + + 'getIndex - tn, el, etn': function() { + var wrapper = createGetIndexTest( 'tn,el,etn' ), + node3 = wrapper.getChild( 2 ); + + assert.areEqual( 2, node3.getIndex() ); + assert.areEqual( -1, node3.getIndex( true ) ); + }, + + 'getIndex - tn, el, etn 2': function() { + var wrapper = createGetIndexTest( 'tn,el,etn' ), + node2 = wrapper.getChild( 1 ), + node3 = wrapper.getChild( 2 ); + + node2.setText( 's' ); + + assert.areEqual( 2, node3.getIndex() ); + assert.areEqual( -1, node3.getIndex( true ) ); + }, + + 'getIndex - el, tn, etn, etn, etn, el': function() { + var wrapper = createGetIndexTest( 'el,tn,etn,etn,etn,el' ), + node2 = wrapper.getChild( 1 ), + node4 = wrapper.getChild( 3 ), + node5 = wrapper.getChild( 4 ), + node6 = wrapper.getChild( 5 ); + assert.areEqual( 1, node2.getIndex() ); assert.areEqual( 1, node2.getIndex( true ) ); + assert.areEqual( 3, node4.getIndex() ); + assert.areEqual( 1, node4.getIndex( true ) ); + assert.areEqual( 4, node5.getIndex() ); + assert.areEqual( 1, node5.getIndex( true ) ); + assert.areEqual( 5, node6.getIndex() ); + assert.areEqual( 2, node6.getIndex( true ) ); }, - test_getNextSourceNode : function() { + test_getNextSourceNode: function() { var node = $( 'getNSN1' ); assert.areSame( CKEDITOR.NODE_TEXT, node.getNextSourceNode( true ).type ); assert.areSame( $( 'getNSN2' ), node.getNextSourceNode( true, CKEDITOR.NODE_ELEMENT ) ); @@ -559,7 +581,7 @@ assert.isNull( node.getNextSourceNode( true, CKEDITOR.NODE_COMMENT, $( 'getNSN3' ) ) ); }, - test_getPreviousSourceNode : function() { + test_getPreviousSourceNode: function() { var node = $( 'getNSN6' ); assert.areSame( CKEDITOR.NODE_TEXT, node.getPreviousSourceNode( true ).type ); assert.areSame( $( 'getNSN5' ), node.getPreviousSourceNode( true, CKEDITOR.NODE_ELEMENT ) ); @@ -567,24 +589,24 @@ assert.isNull( node.getPreviousSourceNode( true, CKEDITOR.NODE_COMMENT, $( 'getNSN4' ) ) ); }, - test_getParent : function() { + test_getParent: function() { var node = $( 'getNSN1' ); assert.areSame( $( 'getNSN' ), node.getParent() ); assert.isNull( newElement( document.body ).getParent().getParent() ); }, - test_getParents : function() { + test_getParents: function() { var node = getNodeByTagName( 'div' ); assert.areSame( 3, node.getParents().length ); assert.areSame( node.getParents()[ 0 ], node.getParents( true )[ 2 ] ); }, - test_getCommonAncestor : function() { + test_getCommonAncestor: function() { assert.areSame( newElement( document.body ), $( 'getNSN1' ).getCommonAncestor( $( 'getAddress2' ) ) ); }, - test_getAscendant : function() { + test_getAscendant: function() { var node = $( 'getNSN1' ); assert.areSame( $( 'getNSN' ), node.getAscendant( 'div' ) ); @@ -593,45 +615,45 @@ assert.isNull( null, node.getAscendant( 'i' ) ); }, - test_getAscendantFuncCheck_callsNumber : function() { + test_getAscendantFuncCheck_callsNumber: function() { var node = $( 'getAscendantFuncCheck' ), calls = 0; - node.getAscendant( function( elem ) { + node.getAscendant( function() { calls++; }, true ); assert.isTrue( calls > 0, 'Should be called at least once.' ); }, - test_getAscendantFuncCheck_findFirstOne : function() { + test_getAscendantFuncCheck_findFirstOne: function() { var node = $( 'getAscendantFuncCheck' ), - found = node.getAscendant( function( el ) { + found = node.getAscendant( function() { return true; }, true ); assert.areSame( node, found, 'First one match.' ); }, - test_getAscendantFuncCheck_findFirstAncestor : function() { + test_getAscendantFuncCheck_findFirstAncestor: function() { var node = $( 'getAscendantFuncCheck' ), - found = node.getAscendant( function( el ) { + found = node.getAscendant( function() { return true; } ); assert.areSame( node.getParent(), found, 'First ancestor match.' ); }, - test_getAscendantFuncCheckFindNothing : function() { + test_getAscendantFuncCheckFindNothing: function() { var node = $( 'getAscendantFuncCheck' ), - found = node.getAscendant( function( el ) { + found = node.getAscendant( function() { return false; } ); assert.isNull( found, 'Nothing found.' ); }, - test_getAscendantFuncCheck_findFirstWithClassDeep2 : function() { + test_getAscendantFuncCheck_findFirstWithClassDeep2: function() { var node = $( 'getAscendantFuncCheck' ), found = node.getAscendant( function( el ) { return el.hasClass( 'deep2' ); @@ -640,7 +662,7 @@ assert.areSame( $( 'deep2' ), found, 'Found element which has class deep2' ); }, - test_hasAscendant : function() { + test_hasAscendant: function() { var node = $( 'getNSN1' ); assert.isTrue( node.hasAscendant( 'div' ) ); @@ -649,7 +671,7 @@ assert.isTrue( node.hasAscendant( 'i', true ) ); }, - test_move : function() { + test_move: function() { var parent = $( 'move' ), node = $( 'move1' ); @@ -662,14 +684,14 @@ assert.areSame( 'move1', parent.$.childNodes[ 0 ].id ); }, - test_remove : function() { + test_remove: function() { $( 'remove1' ).remove( true ); assert.areSame( '
text
', getOuterHtml( $( 'remove' ) ) ); $( 'remove' ).remove(); assert.areSame( null, document.getElementById( 'remove' ) ); }, - test_replace : function() { + test_replace: function() { $( 'replace1' ).replace( $( 'replace2' ) ); assert.areSame( '
12

3
', getOuterHtml( $( 'replace' ) ) ); diff --git a/tests/core/dom/range/bookmarks.html b/tests/core/dom/range/bookmarks.html deleted file mode 100644 index ad24fd032fd..00000000000 --- a/tests/core/dom/range/bookmarks.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/tests/core/dom/range/bookmarks.js b/tests/core/dom/range/bookmarks.js index 46429cc9097..0f1c9e36ede 100644 --- a/tests/core/dom/range/bookmarks.js +++ b/tests/core/dom/range/bookmarks.js @@ -1,22 +1,50 @@ -/* bender-tags: editor,unit,dom,range,jquery */ +/* bender-tags: editor,unit,dom,range */ 'use strict'; var doc = CKEDITOR.document; function createPlayground( html ) { - var playground = doc.getById( 'playground' ); + var playground = doc.createElement( 'div' ); + CKEDITOR.document.getBody().append( playground ); // Replace dots with elements and then remove all of them leaving // split text nodes. html = html.replace( /\./g, '' ); + + // Creating empty elements... + html = html.replace( /\((\w+)\)/g, function( match, $0 ) { + return ''; + } ); + playground.setHtml( html ); + // ... and then replacing then with empty text nodes. + var empty = playground.find( '.empty' ), + split = playground.find( '.split' ), + i; + + // ... but IE8 doesn't support custom data on text nodes, so we must ignore these tests. + if ( empty.count() && CKEDITOR.env.ie && CKEDITOR.env.version == 8 ) { + assert.ignore(); + } + + for ( i = 0; i < empty.count(); i++ ) { + var current = empty.getItem( i ), + emptyTextNode = new CKEDITOR.dom.text( '' ); + + // Setting custom id to have reference for later usage. + emptyTextNode.setCustomData( 'id', current.getAttribute( 'data-id' ) ); + emptyTextNode.replace( current ); + } + // Hack to avoid merging text nodes by IE 8. // We are leaving references to them, so IE won't merge them. findNode( playground, '#weLoveIE8' ); - $( '#playground .split' ).remove(); + for ( i = 0; i < split.count(); i++ ) { + split.getItem( i ).remove(); + } return playground; } @@ -85,19 +113,27 @@ function findNode( container, query ) { if ( query == 'root' ) return container; - var textQuery = query.indexOf( '#' ) == 0 ? query.slice( 1 ) : false, + var textQuery = query.indexOf( '#' ) === 0 ? query.slice( 1 ) : false, + emptyTextQuery = query.match( /^\(\w+\)$/g ), range = new CKEDITOR.dom.range( container ), node, walker; + if ( emptyTextQuery ) { + query = query.replace( /^\(|\)$/g, '' ); + } + range.selectNodeContents( container ); walker = new CKEDITOR.dom.walker( range ); while ( ( node = walker.next() ) ) { - if ( textQuery && node.type == CKEDITOR.NODE_TEXT && node.getText() == textQuery ) + if ( textQuery && node.type == CKEDITOR.NODE_TEXT && node.getText() == textQuery ) { + return node; + } else if ( !textQuery && node.type == CKEDITOR.NODE_ELEMENT && node.is( query ) ) { return node; - else if ( !textQuery && node.type == CKEDITOR.NODE_ELEMENT && node.is( query ) ) + } else if ( emptyTextQuery && node.type == CKEDITOR.NODE_TEXT && node.getCustomData( 'id' ) == query ) { return node; + } } } @@ -121,7 +157,9 @@ var tcs = { }; // TC format: -// 0 - HTML to be tested. Note that text nodes are split in place of '.' characters. +// 0 - HTML to be tested. +// * '.' means that text nodes are split at that position, +// * '(foo)' means an empty text node identified as 'foo'. // 1 - Input range - 'sc' means startContainer and it's passed through findNode(), 'so' means startOffset. // If 'ec' and 'eo' are not passed range is collapsed to start. // 2 - Output range (the same format as input). @@ -155,6 +193,16 @@ addBookmark2TCs( tcs, { 'm offset 1': [ 'i.jkl.m', { sc: '#m', so: 1 }, { sc: '#lm', so: 2 } ] }, + 'collapsed in empty text nodes': { + 'ab(foo) - range in foo': [ 'ab(foo)', { sc: '(foo)', so: 0 }, { sc: '#ab', so: 2 } ], + 'ab(foo) - range in foo': [ 'ab(foo)', { sc: '(foo)', so: 0 }, { sc: 'root', so: 2 } ], + '(foo)ab - range in foo': [ '(foo)ab', { sc: '(foo)', so: 0 }, { sc: 'root', so: 0 } ], + '(foo)ab(bar) - range in foo': [ '(foo)ab(bar)', { sc: '(foo)', so: 0 }, { sc: 'root', so: 0 } ], + '(foo)ab(bar) - range in bar': [ '(foo)ab(bar)', { sc: '(bar)', so: 0 }, { sc: '#ab', so: 2 } ], + 'a(foo)(bar)b - range in bar': [ 'a(foo)(bar)b', { sc: '(bar)', so: 0 }, { sc: 'root', so: 1 } ], + '(foo)a - range in foo': [ '(foo)a', { sc: '(foo)', so: 0 }, { sc: 'root', so: 0 } ] + }, + 'collapsed in element': { 'a root offset 0': [ 'a', { sc: 'root', so: 0 }, { sc: 'root', so: 0 } ], 'a root offset 1': [ 'a', { sc: 'root', so: 1 }, { sc: 'root', so: 1 } ],