/*\r
-Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.\r
+Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
For licensing, see LICENSE.html or http://ckeditor.com/license\r
*/\r
\r
+/**\r
+ * Creates a CKEDITOR.dom.range instance that can be used inside a specific\r
+ * DOM Document.\r
+ * @class Represents a delimited piece of content in a DOM Document.\r
+ * It is contiguous in the sense that it can be characterized as selecting all\r
+ * of the content between a pair of boundary-points.<br>\r
+ * <br>\r
+ * This class shares much of the W3C\r
+ * <a href="http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html">Document Object Model Range</a>\r
+ * ideas and features, adding several range manipulation tools to it, but it's\r
+ * not intended to be compatible with it.\r
+ * @param {CKEDITOR.dom.document} document The document into which the range\r
+ * features will be available.\r
+ * @example\r
+ * // Create a range for the entire contents of the editor document body.\r
+ * var range = new CKEDITOR.dom.range( editor.document );\r
+ * range.selectNodeContents( editor.document.getBody() );\r
+ * // Delete the contents.\r
+ * range.deleteContents();\r
+ */\r
CKEDITOR.dom.range = function( document )\r
{\r
+ /**\r
+ * Node within which the range begins.\r
+ * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}\r
+ * @example\r
+ * var range = new CKEDITOR.dom.range( editor.document );\r
+ * range.selectNodeContents( editor.document.getBody() );\r
+ * alert( range.startContainer.getName() ); // "body"\r
+ */\r
this.startContainer = null;\r
+\r
+ /**\r
+ * Offset within the starting node of the range.\r
+ * @type {Number}\r
+ * @example\r
+ * var range = new CKEDITOR.dom.range( editor.document );\r
+ * range.selectNodeContents( editor.document.getBody() );\r
+ * alert( range.startOffset ); // "0"\r
+ */\r
this.startOffset = null;\r
+\r
+ /**\r
+ * Node within which the range ends.\r
+ * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}\r
+ * @example\r
+ * var range = new CKEDITOR.dom.range( editor.document );\r
+ * range.selectNodeContents( editor.document.getBody() );\r
+ * alert( range.endContainer.getName() ); // "body"\r
+ */\r
this.endContainer = null;\r
+\r
+ /**\r
+ * Offset within the ending node of the range.\r
+ * @type {Number}\r
+ * @example\r
+ * var range = new CKEDITOR.dom.range( editor.document );\r
+ * range.selectNodeContents( editor.document.getBody() );\r
+ * alert( range.endOffset ); // == editor.document.getBody().getChildCount()\r
+ */\r
this.endOffset = null;\r
+\r
+ /**\r
+ * Indicates that this is a collapsed range. A collapsed range has it's\r
+ * start and end boudaries at the very same point so nothing is contained\r
+ * in it.\r
+ * @example\r
+ * var range = new CKEDITOR.dom.range( editor.document );\r
+ * range.selectNodeContents( editor.document.getBody() );\r
+ * alert( range.collapsed ); // "false"\r
+ * range.collapse();\r
+ * alert( range.collapsed ); // "true"\r
+ */\r
this.collapsed = true;\r
\r
+ /**\r
+ * The document within which the range can be used.\r
+ * @type {CKEDITOR.dom.document}\r
+ * @example\r
+ * // Selects the body contents of the range document.\r
+ * range.selectNodeContents( range.document.getBody() );\r
+ */\r
this.document = document;\r
};\r
\r
// This is a shared function used to delete, extract and clone the range\r
// contents.\r
// V2\r
- var execContentsAction = function( range, action, docFrag )\r
+ var execContentsAction = function( range, action, docFrag, mergeThen )\r
{\r
range.optimizeBookmark();\r
\r
\r
currentNode = levelStartNode.getNext();\r
\r
- while( currentNode )\r
+ while ( currentNode )\r
{\r
// Stop processing when the current node matches a node in the\r
// endParents tree or if it is the endNode.\r
{\r
currentNode = levelStartNode.getPrevious();\r
\r
- while( currentNode )\r
+ while ( currentNode )\r
{\r
// Stop processing when the current node matches a node in the\r
// startParents tree or if it is the startNode.\r
if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )\r
endIndex--;\r
\r
- range.setStart( topEnd.getParent(), endIndex );\r
+ // Merge splitted parents.\r
+ if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT )\r
+ {\r
+ var span = CKEDITOR.dom.element.createFromHtml( '<span ' +\r
+ 'data-cke-bookmark="1" style="display:none"> </span>', range.document );\r
+ span.insertAfter( topStart );\r
+ topStart.mergeSiblings( false );\r
+ range.moveToBookmark( { startNode : span } );\r
+ }\r
+ else\r
+ range.setStart( topEnd.getParent(), endIndex );\r
}\r
\r
// Collapse it to the start.\r
}\r
\r
// Cleanup any marked node.\r
- if( removeStartNode )\r
+ if ( removeStartNode )\r
startNode.remove();\r
\r
- if( removeEndNode && endNode.$.parentNode )\r
+ if ( removeEndNode && endNode.$.parentNode )\r
endNode.remove();\r
};\r
\r
\r
// Creates the appropriate node evaluator for the dom walker used inside\r
// check(Start|End)OfBlock.\r
- function getCheckStartEndBlockEvalFunction( isStart )\r
+ function getCheckStartEndBlockEvalFunction()\r
{\r
- var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );\r
+ var skipBogus = false,\r
+ whitespaces = CKEDITOR.dom.walker.whitespaces(),\r
+ bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),\r
+ isBogus = CKEDITOR.dom.walker.bogus();\r
+\r
return function( node )\r
{\r
- // First ignore bookmark nodes.\r
- if ( bookmarkEvaluator( node ) )\r
+ // First skip empty nodes.\r
+ if ( bookmarkEvaluator( node ) || whitespaces( node ) )\r
return true;\r
\r
- if ( node.type == CKEDITOR.NODE_TEXT )\r
- {\r
- // If there's any visible text, then we're not at the start.\r
- if ( CKEDITOR.tools.trim( node.getText() ).length )\r
- return false;\r
- }\r
- else if( node.type == CKEDITOR.NODE_ELEMENT )\r
+ // Skip the bogus node at the end of block.\r
+ if ( isBogus( node ) &&\r
+ !skipBogus )\r
{\r
- // If there are non-empty inline elements (e.g. <img />), then we're not\r
- // at the start.\r
- if ( !inlineChildReqElements[ node.getName() ] )\r
- {\r
- // If we're working at the end-of-block, forgive the first <br /> in non-IE\r
- // browsers.\r
- if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )\r
- hadBr = true;\r
- else\r
- return false;\r
- }\r
+ skipBogus = true;\r
+ return true;\r
}\r
+\r
+ // If there's any visible text, then we're not at the start.\r
+ if ( node.type == CKEDITOR.NODE_TEXT &&\r
+ ( node.hasAscendant( 'pre' ) ||\r
+ CKEDITOR.tools.trim( node.getText() ).length ) )\r
+ return false;\r
+\r
+ // If there are non-empty inline elements (e.g. <img />), then we're not\r
+ // at the start.\r
+ if ( node.type == CKEDITOR.NODE_ELEMENT && !inlineChildReqElements[ node.getName() ] )\r
+ return false;\r
+\r
return true;\r
};\r
}\r
\r
+\r
+ var isBogus = CKEDITOR.dom.walker.bogus();\r
// Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any\r
// text node and non-empty elements unless it's being bookmark text.\r
- function elementBoundaryEval( node )\r
+ function elementBoundaryEval( checkStart )\r
{\r
- // Reject any text node unless it's being bookmark\r
- // OR it's spaces. (#3883)\r
- return node.type != CKEDITOR.NODE_TEXT\r
- && node.getName() in CKEDITOR.dtd.$removeEmpty\r
- || !CKEDITOR.tools.trim( node.getText() )\r
- || node.getParent().hasAttribute( '_fck_bookmark' );\r
+ var whitespaces = CKEDITOR.dom.walker.whitespaces(),\r
+ bookmark = CKEDITOR.dom.walker.bookmark( 1 );\r
+\r
+ return function( node )\r
+ {\r
+ // First skip empty nodes.\r
+ if ( bookmark( node ) || whitespaces( node ) )\r
+ return true;\r
+\r
+ // Tolerant bogus br when checking at the end of block.\r
+ // Reject any text node unless it's being bookmark\r
+ // OR it's spaces.\r
+ // Reject any element unless it's being invisible empty. (#3883)\r
+ return !checkStart && isBogus( node ) ||\r
+ node.type == CKEDITOR.NODE_ELEMENT &&\r
+ node.getName() in CKEDITOR.dtd.$removeEmpty;\r
+ };\r
}\r
\r
var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),\r
- bookmarkEval = new CKEDITOR.dom.walker.bookmark();\r
+ bookmarkEval = new CKEDITOR.dom.walker.bookmark(),\r
+ nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/;\r
\r
function nonWhitespaceOrBookmarkEval( node )\r
{\r
this.collapsed = true;\r
},\r
\r
- // The selection may be lost when cloning (due to the splitText() call).\r
+ /**\r
+ * The content nodes of the range are cloned and added to a document fragment, which is returned.\r
+ * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting).\r
+ */\r
cloneContents : function()\r
{\r
var docFrag = new CKEDITOR.dom.documentFragment( this.document );\r
return docFrag;\r
},\r
\r
- deleteContents : function()\r
+ /**\r
+ * Deletes the content nodes of the range permanently from the DOM tree.\r
+ * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.\r
+ */\r
+ deleteContents : function( mergeThen )\r
{\r
if ( this.collapsed )\r
return;\r
\r
- execContentsAction( this, 0 );\r
+ execContentsAction( this, 0, null, mergeThen );\r
},\r
\r
- extractContents : function()\r
+ /**\r
+ * The content nodes of the range are cloned and added to a document fragment,\r
+ * meanwhile they're removed permanently from the DOM tree.\r
+ * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.\r
+ */\r
+ extractContents : function( mergeThen )\r
{\r
var docFrag = new CKEDITOR.dom.documentFragment( this.document );\r
\r
if ( !this.collapsed )\r
- execContentsAction( this, 1, docFrag );\r
+ execContentsAction( this, 1, docFrag, mergeThen );\r
\r
return docFrag;\r
},\r
var startNode, endNode;\r
var baseId;\r
var clone;\r
+ var collapsed = this.collapsed;\r
\r
startNode = this.document.createElement( 'span' );\r
- startNode.setAttribute( '_fck_bookmark', 1 );\r
+ startNode.data( 'cke-bookmark', 1 );\r
startNode.setStyle( 'display', 'none' );\r
\r
// For IE, it must have something inside, otherwise it may be\r
if ( serializable )\r
{\r
baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();\r
- startNode.setAttribute( 'id', baseId + 'S' );\r
+ startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );\r
}\r
\r
// If collapsed, the endNode will not be created.\r
- if ( !this.collapsed )\r
+ if ( !collapsed )\r
{\r
endNode = startNode.clone();\r
endNode.setHtml( ' ' );\r
this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );\r
\r
return {\r
- startNode : serializable ? baseId + 'S' : startNode,\r
+ startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,\r
endNode : serializable ? baseId + 'E' : endNode,\r
- serializable : serializable\r
+ serializable : serializable,\r
+ collapsed : collapsed\r
};\r
},\r
\r
var startOffset = this.startOffset,\r
endOffset = this.endOffset;\r
\r
+ var collapsed = this.collapsed;\r
+\r
var child, previous;\r
\r
// If there is no range then get out of here.\r
startContainer = child;\r
startOffset = 0;\r
}\r
+\r
+ // Get the normalized offset.\r
+ if ( child && child.type == CKEDITOR.NODE_ELEMENT )\r
+ startOffset = child.getIndex( 1 );\r
}\r
\r
// Normalize the start.\r
}\r
\r
// Process the end only if not normalized.\r
- if ( !this.isCollapsed )\r
+ if ( !collapsed )\r
{\r
// Find out if the start is pointing to a text node that\r
// will be normalized.\r
endContainer = child;\r
endOffset = 0;\r
}\r
+\r
+ // Get the normalized offset.\r
+ if ( child && child.type == CKEDITOR.NODE_ELEMENT )\r
+ endOffset = child.getIndex( 1 );\r
}\r
\r
// Normalize the end.\r
\r
return {\r
start : startContainer.getAddress( normalized ),\r
- end : this.isCollapsed ? null : endContainer.getAddress( normalized ),\r
+ end : collapsed ? null : endContainer.getAddress( normalized ),\r
startOffset : startOffset,\r
endOffset : endOffset,\r
normalized : normalized,\r
+ collapsed : collapsed,\r
is2 : true // It's a createBookmark2 bookmark.\r
};\r
},\r
},\r
\r
/**\r
- * Move the range out of bookmark nodes if they're been the container.\r
+ * Move the range out of bookmark nodes if they'd been the container.\r
*/\r
optimizeBookmark: function()\r
{\r
endNode = this.endContainer;\r
\r
if ( startNode.is && startNode.is( 'span' )\r
- && startNode.hasAttribute( '_fck_bookmark' ) )\r
+ && startNode.data( 'cke-bookmark' ) )\r
this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );\r
if ( endNode && endNode.is && endNode.is( 'span' )\r
- && endNode.hasAttribute( '_fck_bookmark' ) )\r
+ && endNode.data( 'cke-bookmark' ) )\r
this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );\r
},\r
\r
\r
startOffset = startContainer.getIndex() + 1;\r
startContainer = startContainer.getParent();\r
- // Check if it is necessary to update the end boundary.\r
- if ( !collapsed && this.startContainer.equals( this.endContainer ) )\r
+\r
+ // Check all necessity of updating the end boundary.\r
+ if ( this.startContainer.equals( this.endContainer ) )\r
this.setEnd( nextText, this.endOffset - this.startOffset );\r
+ else if ( startContainer.equals( this.endContainer ) )\r
+ this.endOffset += 1;\r
}\r
\r
this.setStart( startContainer, startOffset );\r
\r
if ( collapsed )\r
+ {\r
this.collapse( true );\r
+ return;\r
+ }\r
}\r
\r
var endContainer = this.endContainer;\r
}\r
},\r
\r
- enlarge : function( unit )\r
+ /**\r
+ * Expands the range so that partial units are completely contained.\r
+ * @param unit {Number} The unit type to expand with.\r
+ * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.\r
+ */\r
+ enlarge : function( unit, excludeBrs )\r
{\r
switch ( unit )\r
{\r
// whitespaces at the end.\r
isWhiteSpace = false;\r
\r
- if ( sibling.type == CKEDITOR.NODE_TEXT )\r
+ if ( sibling.type == CKEDITOR.NODE_COMMENT )\r
+ {\r
+ sibling = sibling.getPrevious();\r
+ continue;\r
+ }\r
+ else if ( sibling.type == CKEDITOR.NODE_TEXT )\r
{\r
siblingText = sibling.getText();\r
\r
// If this is a visible element.\r
// We need to check for the bookmark attribute because IE insists on\r
// rendering the display:none nodes we use for bookmarks. (#3363)\r
- if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) )\r
+ // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)\r
+ if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )\r
{\r
// We'll accept it only if we need\r
// whitespace, and this is an inline\r
\r
siblingText = sibling.getText();\r
\r
- if ( !(/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)\r
+ if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)\r
sibling = null;\r
else\r
{\r
- var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );\r
+ var allChildren = sibling.$.getElementsByTagName( '*' );\r
for ( var i = 0, child ; child = allChildren[ i++ ] ; )\r
{\r
if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )\r
\r
isWhiteSpace = /^[\s\ufeff]/.test( siblingText );\r
}\r
- else\r
+ else if ( sibling.type == CKEDITOR.NODE_ELEMENT )\r
{\r
// If this is a visible element.\r
// We need to check for the bookmark attribute because IE insists on\r
// rendering the display:none nodes we use for bookmarks. (#3363)\r
- if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) )\r
+ // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)\r
+ if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )\r
{\r
// We'll accept it only if we need\r
// whitespace, and this is an inline\r
\r
siblingText = sibling.getText();\r
\r
- if ( !(/[^\s\ufeff]/).test( siblingText ) )\r
+ if ( (/[^\s\ufeff]/).test( siblingText ) )\r
sibling = null;\r
else\r
{\r
- allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );\r
+ allChildren = sibling.$.getElementsByTagName( '*' );\r
for ( i = 0 ; child = allChildren[ i++ ] ; )\r
{\r
if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )\r
sibling = null;\r
}\r
}\r
+ else\r
+ isWhiteSpace = 1;\r
\r
if ( isWhiteSpace )\r
{\r
\r
var walker = new CKEDITOR.dom.walker( walkerRange ),\r
blockBoundary, // The node on which the enlarging should stop.\r
- tailBr, //\r
- defaultGuard = CKEDITOR.dom.walker.blockBoundary(\r
+ tailBr, // In case BR as block boundary.\r
+ notBlockBoundary = CKEDITOR.dom.walker.blockBoundary(\r
( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),\r
// Record the encountered 'blockBoundary' for later use.\r
boundaryGuard = function( node )\r
{\r
- var retval = defaultGuard( node );\r
+ var retval = notBlockBoundary( node );\r
if ( !retval )\r
blockBoundary = node;\r
return retval;\r
// It's the body which stop the enlarging if no block boundary found.\r
blockBoundary = blockBoundary || body;\r
\r
- // Start the range at different position by comparing\r
- // the document position of it with 'enlargeable' node.\r
+ // Start the range either after the end of found block (<p>...</p>[text)\r
+ // or at the start of block (<p>[text...), by comparing the document position\r
+ // with 'enlargeable' node.\r
this.setStartAt(\r
blockBoundary,\r
!blockBoundary.is( 'br' ) &&\r
CKEDITOR.POSITION_AFTER_START :\r
CKEDITOR.POSITION_AFTER_END );\r
\r
+ // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)\r
+ if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )\r
+ {\r
+ var theRange = this.clone();\r
+ walker = new CKEDITOR.dom.walker( theRange );\r
+\r
+ var whitespaces = CKEDITOR.dom.walker.whitespaces(),\r
+ bookmark = CKEDITOR.dom.walker.bookmark();\r
+\r
+ walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };\r
+ var previous = walker.previous();\r
+ if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )\r
+ return;\r
+ }\r
+\r
+\r
// Enlarging the end boundary.\r
walkerRange = this.clone();\r
walkerRange.collapse();\r
// It's the body which stop the enlarging if no block boundary found.\r
blockBoundary = blockBoundary || body;\r
\r
- // Start the range at different position by comparing\r
- // the document position of it with 'enlargeable' node.\r
+ // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)\r
+ // by comparing the document position with 'enlargeable' node.\r
this.setEndAt(\r
blockBoundary,\r
( !enlargeable && this.checkEndOfBlock()\r
},\r
\r
/**\r
+ * Descrease the range to make sure that boundaries\r
+ * always anchor beside text nodes or innermost element.\r
+ * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.\r
+ * <dl>\r
+ * <dt>CKEDITOR.SHRINK_ELEMENT</dt>\r
+ * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>\r
+ * <dt>CKEDITOR.SHRINK_TEXT</dt>\r
+ * <dd>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.</dd>\r
+ * </dl>\r
+ * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.\r
+ */\r
+ shrink : function( mode, selectContents )\r
+ {\r
+ // Unable to shrink a collapsed range.\r
+ if ( !this.collapsed )\r
+ {\r
+ mode = mode || CKEDITOR.SHRINK_TEXT;\r
+\r
+ var walkerRange = this.clone();\r
+\r
+ var startContainer = this.startContainer,\r
+ endContainer = this.endContainer,\r
+ startOffset = this.startOffset,\r
+ endOffset = this.endOffset,\r
+ collapsed = this.collapsed;\r
+\r
+ // Whether the start/end boundary is moveable.\r
+ var moveStart = 1,\r
+ moveEnd = 1;\r
+\r
+ if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )\r
+ {\r
+ if ( !startOffset )\r
+ walkerRange.setStartBefore( startContainer );\r
+ else if ( startOffset >= startContainer.getLength( ) )\r
+ walkerRange.setStartAfter( startContainer );\r
+ else\r
+ {\r
+ // Enlarge the range properly to avoid walker making\r
+ // DOM changes caused by triming the text nodes later.\r
+ walkerRange.setStartBefore( startContainer );\r
+ moveStart = 0;\r
+ }\r
+ }\r
+\r
+ if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )\r
+ {\r
+ if ( !endOffset )\r
+ walkerRange.setEndBefore( endContainer );\r
+ else if ( endOffset >= endContainer.getLength( ) )\r
+ walkerRange.setEndAfter( endContainer );\r
+ else\r
+ {\r
+ walkerRange.setEndAfter( endContainer );\r
+ moveEnd = 0;\r
+ }\r
+ }\r
+\r
+ var walker = new CKEDITOR.dom.walker( walkerRange ),\r
+ isBookmark = CKEDITOR.dom.walker.bookmark();\r
+\r
+ walker.evaluator = function( node )\r
+ {\r
+ return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?\r
+ CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );\r
+ };\r
+\r
+ var currentElement;\r
+ walker.guard = function( node, movingOut )\r
+ {\r
+ if ( isBookmark( node ) )\r
+ return true;\r
+\r
+ // Stop when we're shrink in element mode while encountering a text node.\r
+ if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )\r
+ return false;\r
+\r
+ // Stop when we've already walked "through" an element.\r
+ if ( movingOut && node.equals( currentElement ) )\r
+ return false;\r
+\r
+ if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )\r
+ currentElement = node;\r
+\r
+ return true;\r
+ };\r
+\r
+ if ( moveStart )\r
+ {\r
+ var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();\r
+ textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );\r
+ }\r
+\r
+ if ( moveEnd )\r
+ {\r
+ walker.reset();\r
+ var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();\r
+ textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );\r
+ }\r
+\r
+ return !!( moveStart || moveEnd );\r
+ }\r
+ },\r
+\r
+ /**\r
* Inserts a node at the start of the range. The range will be expanded\r
* the contain the node.\r
*/\r
// we will not need this check for our use of this class so we can\r
// ignore it for now.\r
\r
+ // Fixing invalid range start inside dtd empty elements.\r
+ if( startNode.type == CKEDITOR.NODE_ELEMENT\r
+ && CKEDITOR.dtd.$empty[ startNode.getName() ] )\r
+ startOffset = startNode.getIndex(), startNode = startNode.getParent();\r
+\r
this.startContainer = startNode;\r
this.startOffset = startOffset;\r
\r
// will not need this check for our use of this class so we can ignore\r
// it for now.\r
\r
+ // Fixing invalid range end inside dtd empty elements.\r
+ if( endNode.type == CKEDITOR.NODE_ELEMENT\r
+ && CKEDITOR.dtd.$empty[ endNode.getName() ] )\r
+ endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();\r
+\r
this.endContainer = endNode;\r
this.endOffset = endOffset;\r
\r
else\r
{\r
endBlock = this.splitElement( startBlock );\r
+\r
// In Gecko, the last child node must be a bogus <br>.\r
// Note: bogus <br> added under <ul> or <ol> would cause\r
// lists to be incorrectly rendered.\r
},\r
\r
/**\r
- * Check whether current range is on the inner edge of the specified element.\r
- * @param {Number} checkType ( CKEDITOR.START | CKEDITOR.END ) The checking side.\r
+ * Check whether a range boundary is at the inner boundary of a given\r
+ * element.\r
* @param {CKEDITOR.dom.element} element The target element to check.\r
+ * @param {Number} checkType The boundary to check for both the range\r
+ * and the element. It can be CKEDITOR.START or CKEDITOR.END.\r
+ * @returns {Boolean} "true" if the range boundary is at the inner\r
+ * boundary of the element.\r
*/\r
checkBoundaryOfElement : function( element, checkType )\r
{\r
+ var checkStart = ( checkType == CKEDITOR.START );\r
+\r
+ // Create a copy of this range, so we can manipulate it for our checks.\r
var walkerRange = this.clone();\r
+\r
+ // Collapse the range at the proper size.\r
+ walkerRange.collapse( checkStart );\r
+\r
// Expand the range to element boundary.\r
- walkerRange[ checkType == CKEDITOR.START ?\r
- 'setStartAt' : 'setEndAt' ]\r
- ( element, checkType == CKEDITOR.START ?\r
- CKEDITOR.POSITION_AFTER_START\r
- : CKEDITOR.POSITION_BEFORE_END );\r
+ walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]\r
+ ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );\r
\r
- var walker = new CKEDITOR.dom.walker( walkerRange ),\r
- retval = false;\r
- walker.evaluator = elementBoundaryEval;\r
- return walker[ checkType == CKEDITOR.START ?\r
- 'checkBackward' : 'checkForward' ]();\r
+ // Create the walker, which will check if we have anything useful\r
+ // in the range.\r
+ var walker = new CKEDITOR.dom.walker( walkerRange );\r
+ walker.evaluator = elementBoundaryEval( checkStart );\r
+\r
+ return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();\r
},\r
+\r
// Calls to this function may produce changes to the DOM. The range may\r
// be updated to reflect such changes.\r
checkStartOfBlock : function()\r
var startContainer = this.startContainer,\r
startOffset = this.startOffset;\r
\r
- // If the starting node is a text node, and non-empty before the offset,\r
- // then we're surely not at the start of block.\r
- if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )\r
+ // [IE] Special handling for range start in text with a leading NBSP,\r
+ // we it to be isolated, for bogus check.\r
+ if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT )\r
{\r
var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );\r
- if ( textBefore.length )\r
- return false;\r
+ if ( nbspRegExp.test( textBefore ) )\r
+ this.trim( 0, 1 );\r
}\r
\r
- // Antecipate the trim() call here, so the walker will not make\r
- // changes to the DOM, which would not get reflected into this\r
- // range otherwise.\r
- this.trim();\r
-\r
// We need to grab the block element holding the start boundary, so\r
// let's use an element path for it.\r
var path = new CKEDITOR.dom.elementPath( this.startContainer );\r
walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );\r
\r
var walker = new CKEDITOR.dom.walker( walkerRange );\r
- walker.evaluator = getCheckStartEndBlockEvalFunction( true );\r
+ walker.evaluator = getCheckStartEndBlockEvalFunction();\r
\r
return walker.checkBackward();\r
},\r
var endContainer = this.endContainer,\r
endOffset = this.endOffset;\r
\r
- // If the ending node is a text node, and non-empty after the offset,\r
- // then we're surely not at the end of block.\r
- if ( endContainer.type == CKEDITOR.NODE_TEXT )\r
+ // [IE] Special handling for range end in text with a following NBSP,\r
+ // we it to be isolated, for bogus check.\r
+ if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT )\r
{\r
var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );\r
- if ( textAfter.length )\r
- return false;\r
+ if ( nbspRegExp.test( textAfter ) )\r
+ this.trim( 1, 0 );\r
}\r
\r
- // Antecipate the trim() call here, so the walker will not make\r
- // changes to the DOM, which would not get reflected into this\r
- // range otherwise.\r
- this.trim();\r
-\r
// We need to grab the block element holding the start boundary, so\r
// let's use an element path for it.\r
var path = new CKEDITOR.dom.elementPath( this.endContainer );\r
walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );\r
\r
var walker = new CKEDITOR.dom.walker( walkerRange );\r
- walker.evaluator = getCheckStartEndBlockEvalFunction( false );\r
+ walker.evaluator = getCheckStartEndBlockEvalFunction();\r
\r
return walker.checkForward();\r
},\r
\r
/**\r
- * Moves the range boundaries to the first editing point inside an\r
+ * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.\r
+ * @param {Function} evaluator Function used as the walker's evaluator.\r
+ * @param {Function} [guard] Function used as the walker's guard.\r
+ * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,\r
+ * default to the root editable if not defined.\r
+ *\r
+ * @return {CKEDITOR.dom.element|null} The returned node from the traversal.\r
+ */\r
+ getPreviousNode : function( evaluator, guard, boundary ) {\r
+\r
+ var walkerRange = this.clone();\r
+ walkerRange.collapse( 1 );\r
+ walkerRange.setStartAt( boundary || this.document.getBody(), CKEDITOR.POSITION_AFTER_START );\r
+\r
+ var walker = new CKEDITOR.dom.walker( walkerRange );\r
+ walker.evaluator = evaluator;\r
+ walker.guard = guard;\r
+ return walker.previous();\r
+ },\r
+\r
+ /**\r
+ * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.\r
+ * @param {Function} evaluator Function used as the walker's evaluator.\r
+ * @param {Function} [guard] Function used as the walker's guard.\r
+ * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,\r
+ * default to the root editable if not defined.\r
+ *\r
+ * @return {CKEDITOR.dom.element|null} The returned node from the traversal.\r
+ */\r
+ getNextNode: function( evaluator, guard, boundary )\r
+ {\r
+ var walkerRange = this.clone();\r
+ walkerRange.collapse();\r
+ walkerRange.setEndAt( boundary || this.document.getBody(), CKEDITOR.POSITION_BEFORE_END );\r
+\r
+ var walker = new CKEDITOR.dom.walker( walkerRange );\r
+ walker.evaluator = evaluator;\r
+ walker.guard = guard;\r
+ return walker.next();\r
+ },\r
+\r
+ checkReadOnly : ( function()\r
+ {\r
+ function checkNodesEditable( node, anotherEnd )\r
+ {\r
+ while( node )\r
+ {\r
+ if ( node.type == CKEDITOR.NODE_ELEMENT )\r
+ {\r
+ if ( node.getAttribute( 'contentEditable' ) == 'false'\r
+ && !node.data( 'cke-editable' ) )\r
+ {\r
+ return 0;\r
+ }\r
+ // Range enclosed entirely in an editable element.\r
+ else if ( node.is( 'html' )\r
+ || node.getAttribute( 'contentEditable' ) == 'true'\r
+ && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )\r
+ {\r
+ break;\r
+ }\r
+ }\r
+ node = node.getParent();\r
+ }\r
+\r
+ return 1;\r
+ }\r
+\r
+ return function()\r
+ {\r
+ var startNode = this.startContainer,\r
+ endNode = this.endContainer;\r
+\r
+ // Check if elements path at both boundaries are editable.\r
+ return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );\r
+ };\r
+ })(),\r
+\r
+ /**\r
+ * Moves the range boundaries to the first/end editing point inside an\r
* element. For example, in an element tree like\r
* "<p><b><i></i></b> Text</p>", the start editing point is\r
* "<p><b><i>^</i></b> Text</p>" (inside <i>).\r
* @param {CKEDITOR.dom.element} el The element into which look for the\r
* editing spot.\r
+ * @param {Boolean} isMoveToEnd Whether move to the end editable position.\r
*/\r
- moveToElementEditStart : function( el )\r
+ moveToElementEditablePosition : function( el, isMoveToEnd )\r
{\r
- var isEditable;\r
+ function nextDFS( node, childOnly )\r
+ {\r
+ var next;\r
+\r
+ if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )\r
+ next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );\r
+\r
+ if ( !childOnly && !next )\r
+ next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );\r
+\r
+ return next;\r
+ }\r
\r
- while ( el && el.type == CKEDITOR.NODE_ELEMENT )\r
+ // Handle non-editable element e.g. HR.\r
+ if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) )\r
{\r
- isEditable = el.isEditable();\r
+ this.moveToPosition( el, isMoveToEnd ?\r
+ CKEDITOR.POSITION_AFTER_END :\r
+ CKEDITOR.POSITION_BEFORE_START );\r
+ return true;\r
+ }\r
+\r
+ var found = 0;\r
\r
- // If an editable element is found, move inside it.\r
- if ( isEditable )\r
- this.moveToPosition( el, CKEDITOR.POSITION_AFTER_START );\r
- // Stop immediately if we've found a non editable inline element (e.g <img>).\r
- else if ( CKEDITOR.dtd.$inline[ el.getName() ] )\r
+ while ( el )\r
+ {\r
+ // Stop immediately if we've found a text node.\r
+ if ( el.type == CKEDITOR.NODE_TEXT )\r
{\r
- this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );\r
- return true;\r
+ // Put cursor before block filler.\r
+ if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )\r
+ this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );\r
+ else\r
+ this.moveToPosition( el, isMoveToEnd ?\r
+ CKEDITOR.POSITION_AFTER_END :\r
+ CKEDITOR.POSITION_BEFORE_START );\r
+ found = 1;\r
+ break;\r
}\r
\r
- // Non-editable non-inline elements are to be bypassed, getting the next one.\r
- if ( CKEDITOR.dtd.$empty[ el.getName() ] )\r
- el = el.getNext( nonWhitespaceOrBookmarkEval );\r
- else\r
- el = el.getFirst( nonWhitespaceOrBookmarkEval );\r
-\r
- // Stop immediately if we've found a text node.\r
- if ( el && el.type == CKEDITOR.NODE_TEXT )\r
+ // If an editable element is found, move inside it, but not stop the searching.\r
+ if ( el.type == CKEDITOR.NODE_ELEMENT )\r
{\r
- this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );\r
- return true;\r
+ if ( el.isEditable() )\r
+ {\r
+ this.moveToPosition( el, isMoveToEnd ?\r
+ CKEDITOR.POSITION_BEFORE_END :\r
+ CKEDITOR.POSITION_AFTER_START );\r
+ found = 1;\r
+ }\r
+ // Put cursor before padding block br.\r
+ else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() )\r
+ this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );\r
}\r
+\r
+ el = nextDFS( el, found );\r
}\r
\r
- return isEditable;\r
+ return !!found;\r
+ },\r
+\r
+ /**\r
+ *@see {CKEDITOR.dom.range.moveToElementEditablePosition}\r
+ */\r
+ moveToElementEditStart : function( target )\r
+ {\r
+ return this.moveToElementEditablePosition( target );\r
+ },\r
+\r
+ /**\r
+ *@see {CKEDITOR.dom.range.moveToElementEditablePosition}\r
+ */\r
+ moveToElementEditEnd : function( target )\r
+ {\r
+ return this.moveToElementEditablePosition( target, true );\r
},\r
\r
/**\r
*/\r
getEnclosedNode : function()\r
{\r
- var walkerRange = this.clone(),\r
- walker = new CKEDITOR.dom.walker( walkerRange ),\r
+ var walkerRange = this.clone();\r
+\r
+ // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)\r
+ walkerRange.optimize();\r
+ if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT\r
+ || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )\r
+ return null;\r
+\r
+ var walker = new CKEDITOR.dom.walker( walkerRange ),\r
isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),\r
isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),\r
evaluator = function( node )\r
CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;\r
CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;\r
\r
-/**\r
- * Check boundary types.\r
- * @see CKEDITOR.dom.range::checkBoundaryOfElement\r
- */\r
+// Check boundary types.\r
+// @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement\r
CKEDITOR.START = 1;\r
CKEDITOR.END = 2;\r
CKEDITOR.STARTEND = 3;\r
+\r
+// Shrink range types.\r
+// @see CKEDITOR.dom.range.prototype.shrink\r
+CKEDITOR.SHRINK_ELEMENT = 1;\r
+CKEDITOR.SHRINK_TEXT = 2;\r