X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=_source%2Fcore%2Fdom%2Frange.js;h=1aeac003fef1e1849619df7e8be5a7ca4aa8365f;hb=fb481ba0a7d298e3e7b9034fcb9f2afdc6e8e796;hp=c18740001126956db327a676e147ae4eb2da7d91;hpb=66f4ae0bf0280ed56bf7c0f4ab175424dd1d47a0;p=ckeditor.git diff --git a/_source/core/dom/range.js b/_source/core/dom/range.js index c187400..1aeac00 100644 --- a/_source/core/dom/range.js +++ b/_source/core/dom/range.js @@ -1,16 +1,90 @@ /* -Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved. +Copyright (c) 2003-2012, 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(); @@ -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. @@ -263,53 +347,70 @@ CKEDITOR.dom.range = function( document ) // Creates the appropriate node evaluator for the dom walker used inside // check(Start|End)OfBlock. - function getCheckStartEndBlockEvalFunction( isStart ) + function getCheckStartEndBlockEvalFunction() { - var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ); + var skipBogus = false, + whitespaces = CKEDITOR.dom.walker.whitespaces(), + bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ), + isBogus = CKEDITOR.dom.walker.bogus(); + return function( node ) { - // First ignore bookmark nodes. - if ( bookmarkEvaluator( node ) ) + // First skip empty nodes. + if ( bookmarkEvaluator( node ) || whitespaces( node ) ) return true; - if ( node.type == CKEDITOR.NODE_TEXT ) - { - // 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 ) + // Skip the bogus node at the end of block. + if ( isBogus( node ) && + !skipBogus ) { - // If there are non-empty inline elements (e.g. ), then we're not - // at the start. - if ( !inlineChildReqElements[ node.getName() ] ) - { - // If we're working at the end-of-block, forgive the first
in non-IE - // browsers. - if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr ) - hadBr = true; - else - return false; - } + skipBogus = true; + return true; } + + // If there's any visible text, then we're not at the start. + if ( node.type == CKEDITOR.NODE_TEXT && + ( node.hasAscendant( 'pre' ) || + CKEDITOR.tools.trim( node.getText() ).length ) ) + return false; + + // If there are non-empty inline elements (e.g. ), then we're not + // at the start. + if ( node.type == CKEDITOR.NODE_ELEMENT && !inlineChildReqElements[ node.getName() ] ) + return false; + return true; }; } + + var isBogus = CKEDITOR.dom.walker.bogus(); // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any // text node and non-empty elements unless it's being bookmark text. - function elementBoundaryEval( node ) + function elementBoundaryEval( checkStart ) { - // Reject any text node unless it's being bookmark - // OR it's spaces. (#3883) - return node.type != CKEDITOR.NODE_TEXT - && node.getName() in CKEDITOR.dtd.$removeEmpty - || !CKEDITOR.tools.trim( node.getText() ) - || node.getParent().hasAttribute( '_fck_bookmark' ); + var whitespaces = CKEDITOR.dom.walker.whitespaces(), + bookmark = CKEDITOR.dom.walker.bookmark( 1 ); + + return function( node ) + { + // First skip empty nodes. + if ( bookmark( node ) || whitespaces( node ) ) + return true; + + // Tolerant bogus br when checking at the end of block. + // Reject any text node unless it's being bookmark + // OR it's spaces. + // Reject any element unless it's being invisible empty. (#3883) + return !checkStart && isBogus( node ) || + node.type == CKEDITOR.NODE_ELEMENT && + node.getName() in CKEDITOR.dtd.$removeEmpty; + }; } var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(), - bookmarkEval = new CKEDITOR.dom.walker.bookmark(); + bookmarkEval = new CKEDITOR.dom.walker.bookmark(), + nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/; function nonWhitespaceOrBookmarkEval( node ) { @@ -348,7 +449,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 +463,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 +510,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 @@ -409,11 +523,11 @@ CKEDITOR.dom.range = function( document ) if ( serializable ) { baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber(); - startNode.setAttribute( 'id', baseId + 'S' ); + startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) ); } // If collapsed, the endNode will not be created. - if ( !this.collapsed ) + if ( !collapsed ) { endNode = startNode.clone(); endNode.setHtml( ' ' ); @@ -440,9 +554,10 @@ CKEDITOR.dom.range = function( document ) this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END ); return { - startNode : serializable ? baseId + 'S' : startNode, + startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode, endNode : serializable ? baseId + 'E' : endNode, - serializable : serializable + serializable : serializable, + collapsed : collapsed }; }, @@ -465,6 +580,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. @@ -489,6 +606,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. @@ -501,7 +622,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. @@ -517,6 +638,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. @@ -532,10 +657,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 +823,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 +831,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 ); }, @@ -793,7 +919,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 ) { @@ -900,7 +1031,12 @@ CKEDITOR.dom.range = function( document ) // whitespaces at the end. isWhiteSpace = false; - if ( sibling.type == CKEDITOR.NODE_TEXT ) + if ( sibling.type == CKEDITOR.NODE_COMMENT ) + { + sibling = sibling.getPrevious(); + continue; + } + else if ( sibling.type == CKEDITOR.NODE_TEXT ) { siblingText = sibling.getText(); @@ -914,7 +1050,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 @@ -929,7 +1066,7 @@ CKEDITOR.dom.range = function( document ) sibling = null; else { - var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); + var allChildren = sibling.$.getElementsByTagName( '*' ); for ( var i = 0, child ; child = allChildren[ i++ ] ; ) { if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) @@ -1068,12 +1205,13 @@ CKEDITOR.dom.range = function( document ) isWhiteSpace = /^[\s\ufeff]/.test( siblingText ); } - else + else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) { // 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 @@ -1088,7 +1226,7 @@ CKEDITOR.dom.range = function( document ) sibling = null; else { - allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); + allChildren = sibling.$.getElementsByTagName( '*' ); for ( i = 0 ; child = allChildren[ i++ ] ; ) { if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) @@ -1106,6 +1244,8 @@ CKEDITOR.dom.range = function( document ) sibling = null; } } + else + isWhiteSpace = 1; if ( isWhiteSpace ) { @@ -1166,13 +1306,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; @@ -1193,8 +1333,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' ) && @@ -1203,6 +1344,22 @@ CKEDITOR.dom.range = function( document ) CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END ); + // Avoid enlarging the range further when end boundary spans right after the BR. (#7490) + if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) + { + var theRange = this.clone(); + walker = new CKEDITOR.dom.walker( theRange ); + + var whitespaces = CKEDITOR.dom.walker.whitespaces(), + bookmark = CKEDITOR.dom.walker.bookmark(); + + walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); }; + var previous = walker.previous(); + if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) ) + return; + } + + // Enlarging the end boundary. walkerRange = this.clone(); walkerRange.collapse(); @@ -1220,8 +1377,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() @@ -1237,8 +1394,15 @@ CKEDITOR.dom.range = function( document ) /** * Descrease the range to make sure that boundaries - * always anchor beside text nodes or innermost element. + * 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 ) { @@ -1287,7 +1451,8 @@ CKEDITOR.dom.range = function( document ) } } - var walker = new CKEDITOR.dom.walker( walkerRange ); + var walker = new CKEDITOR.dom.walker( walkerRange ), + isBookmark = CKEDITOR.dom.walker.bookmark(); walker.evaluator = function( node ) { @@ -1298,6 +1463,9 @@ CKEDITOR.dom.range = function( document ) 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; @@ -1385,7 +1553,7 @@ CKEDITOR.dom.range = function( document ) // Fixing invalid range start inside dtd empty elements. if( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] ) - startNode = startNode.getParent(), startOffset = startNode.getIndex(); + startOffset = startNode.getIndex(), startNode = startNode.getParent(); this.startContainer = startNode; this.startOffset = startOffset; @@ -1416,7 +1584,7 @@ CKEDITOR.dom.range = function( document ) // Fixing invalid range end inside dtd empty elements. if( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] ) - endNode = endNode.getParent(), endOffset = endNode.getIndex() + 1; + endOffset = endNode.getIndex() + 1, endNode = endNode.getParent(); this.endContainer = endNode; this.endOffset = endOffset; @@ -1624,26 +1792,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; - walker.evaluator = elementBoundaryEval; - return walker[ checkType == CKEDITOR.START ? - 'checkBackward' : 'checkForward' ](); + // Create the walker, which will check if we have anything useful + // in the range. + var walker = new CKEDITOR.dom.walker( walkerRange ); + walker.evaluator = elementBoundaryEval( checkStart ); + + 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() @@ -1651,20 +1829,15 @@ CKEDITOR.dom.range = function( document ) var startContainer = this.startContainer, startOffset = this.startOffset; - // If the starting node is a text node, and non-empty before the offset, - // then we're surely not at the start of block. - if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) + // [IE] Special handling for range start in text with a leading NBSP, + // we it to be isolated, for bogus check. + if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) { var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) ); - if ( textBefore.length ) - return false; + if ( nbspRegExp.test( textBefore ) ) + this.trim( 0, 1 ); } - // Antecipate the trim() call here, so the walker will not make - // changes to the DOM, which would not get reflected into this - // range otherwise. - this.trim(); - // We need to grab the block element holding the start boundary, so // let's use an element path for it. var path = new CKEDITOR.dom.elementPath( this.startContainer ); @@ -1675,7 +1848,7 @@ CKEDITOR.dom.range = function( document ) walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START ); var walker = new CKEDITOR.dom.walker( walkerRange ); - walker.evaluator = getCheckStartEndBlockEvalFunction( true ); + walker.evaluator = getCheckStartEndBlockEvalFunction(); return walker.checkBackward(); }, @@ -1685,20 +1858,15 @@ CKEDITOR.dom.range = function( document ) var endContainer = this.endContainer, endOffset = this.endOffset; - // If the ending node is a text node, and non-empty after the offset, - // then we're surely not at the end of block. - if ( endContainer.type == CKEDITOR.NODE_TEXT ) + // [IE] Special handling for range end in text with a following NBSP, + // we it to be isolated, for bogus check. + if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) { var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) ); - if ( textAfter.length ) - return false; + if ( nbspRegExp.test( textAfter ) ) + this.trim( 1, 0 ); } - // Antecipate the trim() call here, so the walker will not make - // changes to the DOM, which would not get reflected into this - // range otherwise. - this.trim(); - // We need to grab the block element holding the start boundary, so // let's use an element path for it. var path = new CKEDITOR.dom.elementPath( this.endContainer ); @@ -1709,12 +1877,91 @@ CKEDITOR.dom.range = function( document ) walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END ); var walker = new CKEDITOR.dom.walker( walkerRange ); - walker.evaluator = getCheckStartEndBlockEvalFunction( false ); + walker.evaluator = getCheckStartEndBlockEvalFunction(); return walker.checkForward(); }, /** + * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start. + * @param {Function} evaluator Function used as the walker's evaluator. + * @param {Function} [guard] Function used as the walker's guard. + * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited, + * default to the root editable if not defined. + * + * @return {CKEDITOR.dom.element|null} The returned node from the traversal. + */ + getPreviousNode : function( evaluator, guard, boundary ) { + + var walkerRange = this.clone(); + walkerRange.collapse( 1 ); + walkerRange.setStartAt( boundary || this.document.getBody(), CKEDITOR.POSITION_AFTER_START ); + + var walker = new CKEDITOR.dom.walker( walkerRange ); + walker.evaluator = evaluator; + walker.guard = guard; + return walker.previous(); + }, + + /** + * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start. + * @param {Function} evaluator Function used as the walker's evaluator. + * @param {Function} [guard] Function used as the walker's guard. + * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited, + * default to the root editable if not defined. + * + * @return {CKEDITOR.dom.element|null} The returned node from the traversal. + */ + getNextNode: function( evaluator, guard, boundary ) + { + var walkerRange = this.clone(); + walkerRange.collapse(); + walkerRange.setEndAt( boundary || this.document.getBody(), CKEDITOR.POSITION_BEFORE_END ); + + var walker = new CKEDITOR.dom.walker( walkerRange ); + walker.evaluator = evaluator; + walker.guard = guard; + return walker.next(); + }, + + 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( 'html' ) + || 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 @@ -1725,47 +1972,65 @@ CKEDITOR.dom.range = function( document ) */ moveToElementEditablePosition : function( el, isMoveToEnd ) { - var isEditable; + function nextDFS( node, childOnly ) + { + var next; - // Empty elements are rejected. - if ( CKEDITOR.dtd.$empty[ el.getName() ] ) - return false; + if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) ) + next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval ); + + if ( !childOnly && !next ) + next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval ); - while ( el && el.type == CKEDITOR.NODE_ELEMENT ) + return next; + } + + // Handle non-editable element e.g. HR. + if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) { - isEditable = el.isEditable(); - - // If an editable element is found, move inside it. - if ( isEditable ) - this.moveToPosition( el, isMoveToEnd ? - CKEDITOR.POSITION_BEFORE_END : - CKEDITOR.POSITION_AFTER_START ); - // Stop immediately if we've found a non editable inline element (e.g ). - else if ( CKEDITOR.dtd.$inline[ el.getName() ] ) - { - this.moveToPosition( el, isMoveToEnd ? - CKEDITOR.POSITION_AFTER_END : - CKEDITOR.POSITION_BEFORE_START ); - return true; - } + this.moveToPosition( el, isMoveToEnd ? + CKEDITOR.POSITION_AFTER_END : + CKEDITOR.POSITION_BEFORE_START ); + return true; + } - // Non-editable non-inline elements are to be bypassed, getting the next one. - if ( CKEDITOR.dtd.$empty[ el.getName() ] ) - el = el[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval ); - else - el = el[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval ); + var found = 0; + while ( el ) + { // Stop immediately if we've found a text node. - if ( el && el.type == CKEDITOR.NODE_TEXT ) + if ( el.type == CKEDITOR.NODE_TEXT ) { - this.moveToPosition( el, isMoveToEnd ? + // Put cursor before block filler. + if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) ) + this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); + else + this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ); - return true; + found = 1; + break; } + + // If an editable element is found, move inside it, but not stop the searching. + if ( el.type == CKEDITOR.NODE_ELEMENT ) + { + if ( el.isEditable() ) + { + this.moveToPosition( el, isMoveToEnd ? + CKEDITOR.POSITION_BEFORE_END : + CKEDITOR.POSITION_AFTER_START ); + found = 1; + } + // Put cursor before padding block br. + else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() ) + this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); + } + + el = nextDFS( el, found ); } - return isEditable; + return !!found; }, /** @@ -1791,7 +2056,7 @@ CKEDITOR.dom.range = function( document ) { var walkerRange = this.clone(); - // Optimize and analyze the range to avoid DOM destructive nature of walker. (# + // 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 ) @@ -1841,13 +2106,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;