Index: _source/core/dom/document.js =================================================================== --- _source/core/dom/document.js (revision 3058) +++ _source/core/dom/document.js (working copy) @@ -87,6 +87,47 @@ return $ ? new CKEDITOR.dom.element( $ ) : null; }, + getByAddress : function( address, normalized ) + { + var $ = this.$.documentElement; + + for ( var i = 0 ; $ && i < address.length ; i++ ) + { + var target = address[ i ]; + + if ( !normalized ) + { + $ = $.childNodes[ target ]; + continue; + } + + var currentIndex = -1; + + for (var j = 0 ; j < $.childNodes.length ; j++ ) + { + var candidate = $.childNodes[ j ]; + + if ( normalized === true && + candidate.nodeType == 3 && + candidate.previousSibling && + candidate.previousSibling.nodeType == 3 ) + { + continue; + } + + currentIndex++; + + if ( currentIndex == target ) + { + $ = candidate; + break; + } + } + } + + return $ ? new CKEDITOR.dom.node( $ ) : null; + }, + /** * Gets the <head> element for this document. * @returns {CKEDITOR.dom.element} The <head> element. Index: _source/core/dom/node.js =================================================================== --- _source/core/dom/node.js (revision 3058) +++ _source/core/dom/node.js (working copy) @@ -149,6 +149,57 @@ }, /** + * Retrieves a uniquely identifiable tree address for this node. + * The tree address returns is an array of integers, with each integer + * indicating a child index of a DOM node, starting from + * document.documentElement. + * + * For example, assuming
is the second child from ( + * being the first), and we'd like to address the third child under the + * fourth child of body, the tree address returned would be: + * [1, 3, 2] + * + * The tree address cannot be used for finding back the DOM tree node once + * the DOM tree structure has been modified. + */ + getAddress : function( normalized ) + { + var address = []; + var $documentElement = this.getDocument().$.documentElement; + var node = this.$; + + while ( node && node != $documentElement ) + { + var parentNode = node.parentNode; + var currentIndex = -1; + + for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) + { + var candidate = parentNode.childNodes[i]; + + if ( normalized && + candidate.nodeType == 3 && + candidate.previousSibling && + candidate.previousSibling.nodeType == 3 ) + { + continue; + } + + currentIndex++; + + if ( candidate == node ) + break; + } + + address.unshift( currentIndex ); + + node = node.parentNode; + } + + return address; + }, + + /** * Gets a DOM tree descendant under the current node. * @param {Array|Number} indices The child index or array of child indices under the node. * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist. Index: _source/core/dom/range.js =================================================================== --- _source/core/dom/range.js (revision 3058) +++ _source/core/dom/range.js (working copy) @@ -409,24 +409,137 @@ }; }, + /** + * Creates a "non intrusive" and "mutation sensible" bookmark. This + * kind of bookmark should be used only when the DOM is supposed to + * remain stable after its creation. + * @param {Boolean} [normalized] Indicates that the bookmark must + * normalized. When normalized, the successive text nodes are + * considered a single node. To sucessful load a normalized + * bookmark, the DOM tree must be also normalized before calling + * moveToBookmark. + * @returns {Object} An object representing the bookmark. + */ + createBookmark2 : function( normalized ) + { + var startContainer = this.startContainer, + endContainer = this.endContainer; + + var startOffset = this.startOffset, + endOffset = this.endOffset; + + var child, previous; + + // If there is no range then get out of here. + // It happens on initial load in Safari #962 and if the editor it's + // hidden also in Firefox + if ( !startContainer || !endContainer ) + return { start : 0, end : 0 }; + + if ( normalized ) + { + // Find out if the start is pointing to a text node that will + // be normalized. + if ( startContainer.type == CKEDITOR.NODE_ELEMENT ) + { + var child = startContainer.getChild( startOffset ); + + // In this case, move the start information to that text + // node. + if ( child && child.type == CKEDITOR.NODE_TEXT && child.getPrevious().type == CKEDITOR.NODE_TEXT ) + { + startContainer = child; + startOffset = 0; + } + } + + // Normalize the start. + while ( startContainer.type == CKEDITOR.NODE_TEXT + && ( previous = startContainer.getPrevious() ) + && previous.type == CKEDITOR.NODE_TEXT ) + { + startContainer = previous; + startOffset += previous.getLength(); + } + + // Process the end only if not normalized. + if ( !this.isCollapsed ) + { + // Find out if the start is pointing to a text node that + // will be normalized. + if ( endContainer.type == CKEDITOR.NODE_ELEMENT ) + { + child = endContainer.getChild( endOffset ); + + // In this case, move the start information to that + // text node. + if ( child && child.type == CKEDITOR.NODE_TEXT && child.getPrevious().type == CKEDITOR.NODE_TEXT ) + { + endContainer = child; + endOffset = 0; + } + } + + // Normalize the end. + while ( endContainer.type == CKEDITOR.NODE_TEXT + && ( previous = endContainer.getPrevious() ) + && previous.type == CKEDITOR.NODE_TEXT ) + { + endContainer = previous; + endOffset += previous.getLength(); + } + } + } + + return { + start : startContainer.getAddress( normalized ), + end : this.isCollapsed ? null : endContainer.getAddress( normalized ), + startOffset : startOffset, + endOffset : endOffset, + is2 : true // It's a createBookmark2 bookmark. + }; + }, + moveToBookmark : function( bookmark ) { - // Set the range start at the bookmark start node position. - this.setStartBefore( bookmark.startNode ); + if ( bookmark.is2 ) // Created with createBookmark2(). + { + // Get the start information. + var startContainer = this.document.getByAddress( bookmark.start, true ), + startOffset = bookmark.startOffset; - // Remove it, because it may interfere in the setEndBefore call. - bookmark.startNode.remove(); + // Get the end information. + var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, true ), + endOffset = bookmark.endOffset; - // Set the range end at the bookmark end node position, or simply - // collapse it if it is not available. - var endNode = bookmark.endNode; - if ( endNode ) + // Set the start boundary. + this.setStart( startContainer, startOffset ); + + // Set the end boundary. If not available, collapse it. + if ( endContainer ) + this.setEnd( endContainer, endOffset ); + else + this.collapse( true ); + } + else // Created with createBookmark(). { - this.setEndBefore( endNode ); - endNode.remove(); + // Set the range start at the bookmark start node position. + this.setStartBefore( bookmark.startNode ); + + // Remove it, because it may interfere in the setEndBefore call. + bookmark.startNode.remove(); + + // Set the range end at the bookmark end node position, or simply + // collapse it if it is not available. + var endNode = bookmark.endNode; + if ( endNode ) + { + this.setEndBefore( endNode ); + endNode.remove(); + } + else + this.collapse( true ); } - else - this.collapse( true ); }, getBoundaryNodes : function() Index: _source/tests/core/dom/range.html =================================================================== --- _source/tests/core/dom/range.html (revision 3058) +++ _source/tests/core/dom/range.html (working copy) @@ -1,4 +1,4 @@ - +A B C D E
'; + + doc.getById( 'playground' ).setHtml( html ); + + var p = doc.getById( 'P' ); + + // Split the text nodes. + p.getFirst().split( 2 ); // Right before "B" + p.getChild( 3 ).split( 2 ); // Right before "E" + + assert.areSame( 5, p.getChildCount(), 'The number of nodes after split doesn\'t match' ); + + var range = new CKEDITOR.dom.range( doc ); + + // Create a range that enbraces "E". + range.setStartBefore( p.getChild( 4 ) ); + range.setEndAfter( p.getChild( 4 ) ); + + var bookmark = range.createBookmark2(); + + range = new CKEDITOR.dom.range( doc ); + range.moveToBookmark( bookmark ); + + assert.areSame( document.getElementById('P'), range.startContainer.$, 'range.startContainer' ); + assert.areSame( 4, range.startOffset, 'range.startOffset' ); + assert.areSame( document.getElementById('P'), range.endContainer.$, 'range.endContainer' ); + assert.areSame( 5, range.endOffset, 'range.endOffset' ); + assert.isFalse( range.collapsed, 'range.collapsed' ); + }, + + test_createBookmark2_3 : function() + { + var html = 'A B C D E
'; + + doc.getById( 'playground' ).setHtml( html ); + + var p = doc.getById( 'P' ); + + // Split the text nodes. + p.getFirst().split( 2 ); // Right before "B" + p.getChild( 3 ).split( 2 ); // Right before "E" + + assert.areSame( 5, p.getChildCount(), 'The number of nodes after split doesn\'t match' ); + + var range = new CKEDITOR.dom.range( doc ); + + // Create a range that enbraces "E". + range.setStartBefore( p.getChild( 4 ) ); + range.setEndAfter( p.getChild( 4 ) ); + + var bookmark = range.createBookmark2( true ); + + // Normalize the contents. + doc.getById( 'playground' ).setHtml( html ); + + range = new CKEDITOR.dom.range( doc ); + range.moveToBookmark( bookmark ); + + assert.areSame( document.getElementById('P').childNodes[2], range.startContainer.$, 'range.startContainer' ); + assert.areSame( 2, range.startOffset, 'range.startOffset' ); + assert.areSame( document.getElementById('P'), range.endContainer.$, 'range.endContainer' ); + + // Note that the endOffset doesn't get normalized as it's not + // needed. Any offset pointing over the container size is meant to + // be at the end of it. + assert.areSame( 5, range.endOffset, 'range.endOffset' ); + assert.isFalse( range.collapsed, 'range.collapsed' ); + }, + ///////////// setUp : function() @@ -1524,19 +1619,7 @@ //window.onload = function() //{ -// // Local references. -// var assert = CKEDITOR.test.assert; -// var getInnerHtml = CKEDITOR.test.getInnerHtml; - -// var doc = new CKEDITOR.dom.document( document ); - -// doc.getById( '_EnlargeP' ).setHtml( 'this is some sample text' ); - -// var range = new CKEDITOR.dom.range( doc ); -// range.setStart( doc.getById( '_EnlargeP' ), 0 ); -// range.setEnd( doc.getById( '_EnlargeP' ).getChild( 1 ), 0 ); - -// range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); +// tests.test_createBookmark2_3(); //} //]]>