X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=_source%2Fcore%2Fdom%2Frange.js;h=4c0067b856558a04050cf8eae0b6ed0c54cb3f88;hb=4e90e78dc97789709ee7404359a5517540c27553;hp=5b1681f850c41a5118ac98bb844b7e584a1c97fb;hpb=ea7e3453c7b0f023b050aca6d9f83ab372860d91;p=ckeditor.git diff --git a/_source/core/dom/range.js b/_source/core/dom/range.js index 5b1681f..4c0067b 100644 --- a/_source/core/dom/range.js +++ b/_source/core/dom/range.js @@ -1,16 +1,90 @@ /* -Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. +Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.html or http://ckeditor.com/license */ +/** + * Creates a CKEDITOR.dom.range instance that can be used inside a specific + * DOM Document. + * @class Represents a delimited piece of content in a DOM Document. + * It is contiguous in the sense that it can be characterized as selecting all + * of the content between a pair of boundary-points.
+ *
+ * This class shares much of the W3C + * Document Object Model Range + * ideas and features, adding several range manipulation tools to it, but it's + * not intended to be compatible with it. + * @param {CKEDITOR.dom.document} document The document into which the range + * features will be available. + * @example + * // Create a range for the entire contents of the editor document body. + * var range = new CKEDITOR.dom.range( editor.document ); + * range.selectNodeContents( editor.document.getBody() ); + * // Delete the contents. + * range.deleteContents(); + */ CKEDITOR.dom.range = function( document ) { + /** + * Node within which the range begins. + * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT} + * @example + * var range = new CKEDITOR.dom.range( editor.document ); + * range.selectNodeContents( editor.document.getBody() ); + * alert( range.startContainer.getName() ); // "body" + */ this.startContainer = null; + + /** + * Offset within the starting node of the range. + * @type {Number} + * @example + * var range = new CKEDITOR.dom.range( editor.document ); + * range.selectNodeContents( editor.document.getBody() ); + * alert( range.startOffset ); // "0" + */ this.startOffset = null; + + /** + * Node within which the range ends. + * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT} + * @example + * var range = new CKEDITOR.dom.range( editor.document ); + * range.selectNodeContents( editor.document.getBody() ); + * alert( range.endContainer.getName() ); // "body" + */ this.endContainer = null; + + /** + * Offset within the ending node of the range. + * @type {Number} + * @example + * var range = new CKEDITOR.dom.range( editor.document ); + * range.selectNodeContents( editor.document.getBody() ); + * alert( range.endOffset ); // == editor.document.getBody().getChildCount() + */ this.endOffset = null; + + /** + * Indicates that this is a collapsed range. A collapsed range has it's + * start and end boudaries at the very same point so nothing is contained + * in it. + * @example + * var range = new CKEDITOR.dom.range( editor.document ); + * range.selectNodeContents( editor.document.getBody() ); + * alert( range.collapsed ); // "false" + * range.collapse(); + * alert( range.collapsed ); // "true" + */ this.collapsed = true; + /** + * The document within which the range can be used. + * @type {CKEDITOR.dom.document} + * @example + * // Selects the body contents of the range document. + * range.selectNodeContents( range.document.getBody() ); + */ this.document = document; }; @@ -29,7 +103,7 @@ CKEDITOR.dom.range = function( document ) // This is a shared function used to delete, extract and clone the range // contents. // V2 - var execContentsAction = function( range, action, docFrag ) + var execContentsAction = function( range, action, docFrag, mergeThen ) { range.optimizeBookmark(); @@ -133,7 +207,7 @@ CKEDITOR.dom.range = function( document ) currentNode = levelStartNode.getNext(); - while( currentNode ) + while ( currentNode ) { // Stop processing when the current node matches a node in the // endParents tree or if it is the endNode. @@ -180,7 +254,7 @@ CKEDITOR.dom.range = function( document ) { currentNode = levelStartNode.getPrevious(); - while( currentNode ) + while ( currentNode ) { // Stop processing when the current node matches a node in the // startParents tree or if it is the startNode. @@ -244,7 +318,17 @@ CKEDITOR.dom.range = function( document ) if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode ) endIndex--; - range.setStart( topEnd.getParent(), endIndex ); + // Merge splitted parents. + if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT ) + { + var span = CKEDITOR.dom.element.createFromHtml( ' ', range.document ); + span.insertAfter( topStart ); + topStart.mergeSiblings( false ); + range.moveToBookmark( { startNode : span } ); + } + else + range.setStart( topEnd.getParent(), endIndex ); } // Collapse it to the start. @@ -252,10 +336,10 @@ CKEDITOR.dom.range = function( document ) } // Cleanup any marked node. - if( removeStartNode ) + if ( removeStartNode ) startNode.remove(); - if( removeEndNode && endNode.$.parentNode ) + if ( removeEndNode && endNode.$.parentNode ) endNode.remove(); }; @@ -277,8 +361,8 @@ CKEDITOR.dom.range = function( document ) // If there's any visible text, then we're not at the start. if ( CKEDITOR.tools.trim( node.getText() ).length ) return false; - } - else + } + else if ( node.type == CKEDITOR.NODE_ELEMENT ) { // If there are non-empty inline elements (e.g. ), then we're not // at the start. @@ -305,7 +389,16 @@ CKEDITOR.dom.range = function( document ) return node.type != CKEDITOR.NODE_TEXT && node.getName() in CKEDITOR.dtd.$removeEmpty || !CKEDITOR.tools.trim( node.getText() ) - || node.getParent().hasAttribute( '_fck_bookmark' ); + || !!node.getParent().data( 'cke-bookmark' ); + } + + var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(), + bookmarkEval = new CKEDITOR.dom.walker.bookmark(); + + function nonWhitespaceOrBookmarkEval( node ) + { + // Whitespaces and bookmark nodes are to be ignored. + return !whitespaceEval( node ) && !bookmarkEval( node ); } CKEDITOR.dom.range.prototype = @@ -339,7 +432,10 @@ CKEDITOR.dom.range = function( document ) this.collapsed = true; }, - // The selection may be lost when cloning (due to the splitText() call). + /** + * The content nodes of the range are cloned and added to a document fragment, which is returned. + * Note: Text selection may lost after invoking this method. (caused by text node splitting). + */ cloneContents : function() { var docFrag = new CKEDITOR.dom.documentFragment( this.document ); @@ -350,20 +446,29 @@ CKEDITOR.dom.range = function( document ) return docFrag; }, - deleteContents : function() + /** + * Deletes the content nodes of the range permanently from the DOM tree. + * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection. + */ + deleteContents : function( mergeThen ) { if ( this.collapsed ) return; - execContentsAction( this, 0 ); + execContentsAction( this, 0, null, mergeThen ); }, - extractContents : function() + /** + * The content nodes of the range are cloned and added to a document fragment, + * meanwhile they're removed permanently from the DOM tree. + * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection. + */ + extractContents : function( mergeThen ) { var docFrag = new CKEDITOR.dom.documentFragment( this.document ); if ( !this.collapsed ) - execContentsAction( this, 1, docFrag ); + execContentsAction( this, 1, docFrag, mergeThen ); return docFrag; }, @@ -388,9 +493,10 @@ CKEDITOR.dom.range = function( document ) var startNode, endNode; var baseId; var clone; + var collapsed = this.collapsed; startNode = this.document.createElement( 'span' ); - startNode.setAttribute( '_fck_bookmark', 1 ); + startNode.data( 'cke-bookmark', 1 ); startNode.setStyle( 'display', 'none' ); // For IE, it must have something inside, otherwise it may be @@ -404,7 +510,7 @@ CKEDITOR.dom.range = function( document ) } // If collapsed, the endNode will not be created. - if ( !this.collapsed ) + if ( !collapsed ) { endNode = startNode.clone(); endNode.setHtml( ' ' ); @@ -433,7 +539,8 @@ CKEDITOR.dom.range = function( document ) return { startNode : serializable ? baseId + 'S' : startNode, endNode : serializable ? baseId + 'E' : endNode, - serializable : serializable + serializable : serializable, + collapsed : collapsed }; }, @@ -456,6 +563,8 @@ CKEDITOR.dom.range = function( document ) var startOffset = this.startOffset, endOffset = this.endOffset; + var collapsed = this.collapsed; + var child, previous; // If there is no range then get out of here. @@ -480,6 +589,10 @@ CKEDITOR.dom.range = function( document ) startContainer = child; startOffset = 0; } + + // Get the normalized offset. + if ( child && child.type == CKEDITOR.NODE_ELEMENT ) + startOffset = child.getIndex( 1 ); } // Normalize the start. @@ -492,7 +605,7 @@ CKEDITOR.dom.range = function( document ) } // Process the end only if not normalized. - if ( !this.isCollapsed ) + if ( !collapsed ) { // Find out if the start is pointing to a text node that // will be normalized. @@ -508,6 +621,10 @@ CKEDITOR.dom.range = function( document ) endContainer = child; endOffset = 0; } + + // Get the normalized offset. + if ( child && child.type == CKEDITOR.NODE_ELEMENT ) + endOffset = child.getIndex( 1 ); } // Normalize the end. @@ -523,10 +640,11 @@ CKEDITOR.dom.range = function( document ) return { start : startContainer.getAddress( normalized ), - end : this.isCollapsed ? null : endContainer.getAddress( normalized ), + end : collapsed ? null : endContainer.getAddress( normalized ), startOffset : startOffset, endOffset : endOffset, normalized : normalized, + collapsed : collapsed, is2 : true // It's a createBookmark2 bookmark. }; }, @@ -688,7 +806,7 @@ CKEDITOR.dom.range = function( document ) }, /** - * Move the range out of bookmark nodes if they're been the container. + * Move the range out of bookmark nodes if they'd been the container. */ optimizeBookmark: function() { @@ -696,10 +814,10 @@ CKEDITOR.dom.range = function( document ) endNode = this.endContainer; if ( startNode.is && startNode.is( 'span' ) - && startNode.hasAttribute( '_fck_bookmark' ) ) + && startNode.data( 'cke-bookmark' ) ) this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START ); if ( endNode && endNode.is && endNode.is( 'span' ) - && endNode.hasAttribute( '_fck_bookmark' ) ) + && endNode.data( 'cke-bookmark' ) ) this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END ); }, @@ -733,15 +851,21 @@ CKEDITOR.dom.range = function( document ) startOffset = startContainer.getIndex() + 1; startContainer = startContainer.getParent(); - // Check if it is necessary to update the end boundary. - if ( !collapsed && this.startContainer.equals( this.endContainer ) ) + + // Check all necessity of updating the end boundary. + if ( this.startContainer.equals( this.endContainer ) ) this.setEnd( nextText, this.endOffset - this.startOffset ); + else if ( startContainer.equals( this.endContainer ) ) + this.endOffset += 1; } this.setStart( startContainer, startOffset ); if ( collapsed ) + { this.collapse( true ); + return; + } } var endContainer = this.endContainer; @@ -778,7 +902,12 @@ CKEDITOR.dom.range = function( document ) } }, - enlarge : function( unit ) + /** + * Expands the range so that partial units are completely contained. + * @param unit {Number} The unit type to expand with. + * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding. + */ + enlarge : function( unit, excludeBrs ) { switch ( unit ) { @@ -899,7 +1028,8 @@ CKEDITOR.dom.range = function( document ) // If this is a visible element. // We need to check for the bookmark attribute because IE insists on // rendering the display:none nodes we use for bookmarks. (#3363) - if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) ) + // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) + if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) { // We'll accept it only if we need // whitespace, and this is an inline @@ -910,7 +1040,7 @@ CKEDITOR.dom.range = function( document ) siblingText = sibling.getText(); - if ( !(/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) + if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) sibling = null; else { @@ -1058,7 +1188,8 @@ CKEDITOR.dom.range = function( document ) // If this is a visible element. // We need to check for the bookmark attribute because IE insists on // rendering the display:none nodes we use for bookmarks. (#3363) - if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) ) + // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) + if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) { // We'll accept it only if we need // whitespace, and this is an inline @@ -1069,7 +1200,7 @@ CKEDITOR.dom.range = function( document ) siblingText = sibling.getText(); - if ( !(/[^\s\ufeff]/).test( siblingText ) ) + if ( (/[^\s\ufeff]/).test( siblingText ) ) sibling = null; else { @@ -1151,13 +1282,13 @@ CKEDITOR.dom.range = function( document ) var walker = new CKEDITOR.dom.walker( walkerRange ), blockBoundary, // The node on which the enlarging should stop. - tailBr, // - defaultGuard = CKEDITOR.dom.walker.blockBoundary( + tailBr, // In case BR as block boundary. + notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ), // Record the encountered 'blockBoundary' for later use. boundaryGuard = function( node ) { - var retval = defaultGuard( node ); + var retval = notBlockBoundary( node ); if ( !retval ) blockBoundary = node; return retval; @@ -1178,12 +1309,14 @@ CKEDITOR.dom.range = function( document ) // It's the body which stop the enlarging if no block boundary found. blockBoundary = blockBoundary || body; - // Start the range at different position by comparing - // the document position of it with 'enlargeable' node. + // Start the range either after the end of found block (

...

[text) + // or at the start of block (

[text...), by comparing the document position + // with 'enlargeable' node. this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && - ( !enlargeable || blockBoundary.contains( enlargeable ) ) ? + ( !enlargeable && this.checkStartOfBlock() + || enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END ); @@ -1204,12 +1337,12 @@ CKEDITOR.dom.range = function( document ) // It's the body which stop the enlarging if no block boundary found. blockBoundary = blockBoundary || body; - // Start the range at different position by comparing - // the document position of it with 'enlargeable' node. + // Close the range either before the found block start (text]

...

) or at the block end (...text]

) + // by comparing the document position with 'enlargeable' node. this.setEndAt( blockBoundary, - !blockBoundary.is( 'br' ) && - ( !enlargeable || blockBoundary.contains( enlargeable ) ) ? + ( !enlargeable && this.checkEndOfBlock() + || enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START ); // We must include the
at the end of range if there's @@ -1220,6 +1353,111 @@ CKEDITOR.dom.range = function( document ) }, /** + * Descrease the range to make sure that boundaries + * always anchor beside text nodes or innermost element. + * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode. + *
+ *
CKEDITOR.SHRINK_ELEMENT
+ *
Shrink the range boundaries to the edge of the innermost element.
+ *
CKEDITOR.SHRINK_TEXT
+ *
Shrink the range boudaries to anchor by the side of enclosed text node, range remains if there's no text nodes on boundaries at all.
+ *
+ * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node. + */ + shrink : function( mode, selectContents ) + { + // Unable to shrink a collapsed range. + if ( !this.collapsed ) + { + mode = mode || CKEDITOR.SHRINK_TEXT; + + var walkerRange = this.clone(); + + var startContainer = this.startContainer, + endContainer = this.endContainer, + startOffset = this.startOffset, + endOffset = this.endOffset, + collapsed = this.collapsed; + + // Whether the start/end boundary is moveable. + var moveStart = 1, + moveEnd = 1; + + if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) + { + if ( !startOffset ) + walkerRange.setStartBefore( startContainer ); + else if ( startOffset >= startContainer.getLength( ) ) + walkerRange.setStartAfter( startContainer ); + else + { + // Enlarge the range properly to avoid walker making + // DOM changes caused by triming the text nodes later. + walkerRange.setStartBefore( startContainer ); + moveStart = 0; + } + } + + if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) + { + if ( !endOffset ) + walkerRange.setEndBefore( endContainer ); + else if ( endOffset >= endContainer.getLength( ) ) + walkerRange.setEndAfter( endContainer ); + else + { + walkerRange.setEndAfter( endContainer ); + moveEnd = 0; + } + } + + var walker = new CKEDITOR.dom.walker( walkerRange ), + isBookmark = CKEDITOR.dom.walker.bookmark(); + + walker.evaluator = function( node ) + { + return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? + CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT ); + }; + + var currentElement; + walker.guard = function( node, movingOut ) + { + if ( isBookmark( node ) ) + return true; + + // Stop when we're shrink in element mode while encountering a text node. + if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT ) + return false; + + // Stop when we've already walked "through" an element. + if ( movingOut && node.equals( currentElement ) ) + return false; + + if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT ) + currentElement = node; + + return true; + }; + + if ( moveStart ) + { + var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next'](); + textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START ); + } + + if ( moveEnd ) + { + walker.reset(); + var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous'](); + textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END ); + } + + return !!( moveStart || moveEnd ); + } + }, + + /** * Inserts a node at the start of the range. The range will be expanded * the contain the node. */ @@ -1272,6 +1510,11 @@ CKEDITOR.dom.range = function( document ) // we will not need this check for our use of this class so we can // ignore it for now. + // Fixing invalid range start inside dtd empty elements. + if( startNode.type == CKEDITOR.NODE_ELEMENT + && CKEDITOR.dtd.$empty[ startNode.getName() ] ) + startOffset = startNode.getIndex(), startNode = startNode.getParent(); + this.startContainer = startNode; this.startOffset = startOffset; @@ -1298,6 +1541,11 @@ CKEDITOR.dom.range = function( document ) // will not need this check for our use of this class so we can ignore // it for now. + // Fixing invalid range end inside dtd empty elements. + if( endNode.type == CKEDITOR.NODE_ELEMENT + && CKEDITOR.dtd.$empty[ endNode.getName() ] ) + endOffset = endNode.getIndex() + 1, endNode = endNode.getParent(); + this.endContainer = endNode; this.endOffset = endOffset; @@ -1457,18 +1705,7 @@ CKEDITOR.dom.range = function( document ) } else { - // Extract the contents of the block from the selection point to the end - // of its contents. - this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END ); - var documentFragment = this.extractContents(); - - // Duplicate the block element after it. - endBlock = startBlock.clone( false ); - - // Place the extracted contents into the duplicated block. - documentFragment.appendTo( endBlock ); - endBlock.insertAfter( startBlock ); - this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END ); + endBlock = this.splitElement( startBlock ); // In Gecko, the last child node must be a bogus
. // Note: bogus
added under