X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=_source%2Fcore%2Fdom%2Frange.js;h=fedbfb97e45684a2612f0d548a5921c3e1fe1fe6;hb=48b1db88210b4160dce439c6e3e32e14af8c106b;hp=264ee4eff190acd6baa08a3c2e12023fbd05a62f;hpb=941b0a9ba4e673e292510d80a5a86806994b8ea6;p=ckeditor.git diff --git a/_source/core/dom/range.js b/_source/core/dom/range.js index 264ee4e..fedbfb9 100644 --- a/_source/core/dom/range.js +++ b/_source/core/dom/range.js @@ -1,8 +1,11 @@ /* -Copyright (c) 2003-2010, 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 */ +/** + * @class + */ CKEDITOR.dom.range = function( document ) { this.startContainer = null; @@ -29,7 +32,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 +136,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 +183,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 +247,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 +265,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 +290,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 if( node.type == CKEDITOR.NODE_ELEMENT ) + } + 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 +318,7 @@ 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(), @@ -348,7 +361,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 ); @@ -359,20 +375,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; }, @@ -397,9 +422,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 @@ -413,7 +439,7 @@ CKEDITOR.dom.range = function( document ) } // If collapsed, the endNode will not be created. - if ( !this.collapsed ) + if ( !collapsed ) { endNode = startNode.clone(); endNode.setHtml( ' ' ); @@ -442,7 +468,8 @@ CKEDITOR.dom.range = function( document ) return { startNode : serializable ? baseId + 'S' : startNode, endNode : serializable ? baseId + 'E' : endNode, - serializable : serializable + serializable : serializable, + collapsed : collapsed }; }, @@ -465,6 +492,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. @@ -501,7 +530,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. @@ -532,10 +561,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. }; }, @@ -697,7 +727,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() { @@ -705,10 +735,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 ); }, @@ -742,15 +772,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; @@ -787,7 +823,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 ) { @@ -908,7 +949,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 @@ -919,7 +961,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 { @@ -1067,7 +1109,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 @@ -1078,7 +1121,7 @@ CKEDITOR.dom.range = function( document ) siblingText = sibling.getText(); - if ( !(/[^\s\ufeff]/).test( siblingText ) ) + if ( (/[^\s\ufeff]/).test( siblingText ) ) sibling = null; else { @@ -1160,13 +1203,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; @@ -1187,8 +1230,9 @@ 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' ) && @@ -1214,8 +1258,8 @@ 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, ( !enlargeable && this.checkEndOfBlock() @@ -1230,6 +1274,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. */ @@ -1282,6 +1431,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; @@ -1308,6 +1462,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; @@ -1514,26 +1673,36 @@ CKEDITOR.dom.range = function( document ) }, /** - * Check whether current range is on the inner edge of the specified element. - * @param {Number} checkType ( CKEDITOR.START | CKEDITOR.END ) The checking side. + * Check whether a range boundary is at the inner boundary of a given + * element. * @param {CKEDITOR.dom.element} element The target element to check. + * @param {Number} checkType The boundary to check for both the range + * and the element. It can be CKEDITOR.START or CKEDITOR.END. + * @returns {Boolean} "true" if the range boundary is at the inner + * boundary of the element. */ checkBoundaryOfElement : function( element, checkType ) { + var checkStart = ( checkType == CKEDITOR.START ); + + // Create a copy of this range, so we can manipulate it for our checks. var walkerRange = this.clone(); + + // Collapse the range at the proper size. + walkerRange.collapse( checkStart ); + // Expand the range to element boundary. - walkerRange[ checkType == CKEDITOR.START ? - 'setStartAt' : 'setEndAt' ] - ( element, checkType == CKEDITOR.START ? - CKEDITOR.POSITION_AFTER_START - : CKEDITOR.POSITION_BEFORE_END ); + walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ] + ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END ); - var walker = new CKEDITOR.dom.walker( walkerRange ), - retval = false; + // Create the walker, which will check if we have anything useful + // in the range. + var walker = new CKEDITOR.dom.walker( walkerRange ); walker.evaluator = elementBoundaryEval; - return walker[ checkType == CKEDITOR.START ? - 'checkBackward' : 'checkForward' ](); + + return walker[ checkStart ? 'checkBackward' : 'checkForward' ](); }, + // Calls to this function may produce changes to the DOM. The range may // be updated to reflect such changes. checkStartOfBlock : function() @@ -1605,6 +1774,47 @@ CKEDITOR.dom.range = function( document ) }, /** + * Check if elements at which the range boundaries anchor are read-only, + * with respect to "contenteditable" attribute. + */ + checkReadOnly : ( function() + { + function checkNodesEditable( node, anotherEnd ) + { + while( node ) + { + if ( node.type == CKEDITOR.NODE_ELEMENT ) + { + if ( node.getAttribute( 'contentEditable' ) == 'false' + && !node.data( 'cke-editable' ) ) + { + return 0; + } + // Range enclosed entirely in an editable element. + else if ( node.is( 'body' ) + || node.getAttribute( 'contentEditable' ) == 'true' + && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) ) + { + break; + } + } + node = node.getParent(); + } + + return 1; + } + + return function() + { + var startNode = this.startContainer, + endNode = this.endContainer; + + // Check if elements path at both boundaries are editable. + return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) ); + }; + })(), + + /** * Moves the range boundaries to the first/end editing point inside an * element. For example, in an element tree like * "<p><b><i></i></b> Text</p>", the start editing point is @@ -1617,6 +1827,10 @@ CKEDITOR.dom.range = function( document ) { var isEditable; + // Empty elements are rejected. + if ( CKEDITOR.dtd.$empty[ el.getName() ] ) + return false; + while ( el && el.type == CKEDITOR.NODE_ELEMENT ) { isEditable = el.isEditable(); @@ -1675,8 +1889,15 @@ CKEDITOR.dom.range = function( document ) */ getEnclosedNode : function() { - var walkerRange = this.clone(), - walker = new CKEDITOR.dom.walker( walkerRange ), + var walkerRange = this.clone(); + + // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780) + walkerRange.optimize(); + if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT + || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT ) + return null; + + var walker = new CKEDITOR.dom.walker( walkerRange ), isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ), isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), evaluator = function( node ) @@ -1720,10 +1941,13 @@ CKEDITOR.ENLARGE_ELEMENT = 1; CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2; CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3; -/** - * Check boundary types. - * @see CKEDITOR.dom.range::checkBoundaryOfElement - */ +// Check boundary types. +// @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement CKEDITOR.START = 1; CKEDITOR.END = 2; CKEDITOR.STARTEND = 3; + +// Shrink range types. +// @see CKEDITOR.dom.range.prototype.shrink +CKEDITOR.SHRINK_ELEMENT = 1; +CKEDITOR.SHRINK_TEXT = 2;