/*\r
-Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.\r
+Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.\r
For licensing, see LICENSE.html or http://ckeditor.com/license\r
*/\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
}\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
if ( CKEDITOR.tools.trim( node.getText() ).length )\r
return false;\r
}\r
- else\r
+ else if ( node.type == CKEDITOR.NODE_ELEMENT )\r
{\r
// If there are non-empty inline elements (e.g. <img />), then we're not\r
// at the start.\r
|| node.getParent().hasAttribute( '_fck_bookmark' );\r
}\r
\r
+ var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),\r
+ bookmarkEval = new CKEDITOR.dom.walker.bookmark();\r
+\r
+ function nonWhitespaceOrBookmarkEval( node )\r
+ {\r
+ // Whitespaces and bookmark nodes are to be ignored.\r
+ return !whitespaceEval( node ) && !bookmarkEval( node );\r
+ }\r
+\r
CKEDITOR.dom.range.prototype =\r
{\r
clone : function()\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
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
\r
siblingText = sibling.getText();\r
\r
- if ( !(/[^\s\ufeff]/).test( siblingText ) )\r
+ if ( (/[^\s\ufeff]/).test( siblingText ) )\r
sibling = null;\r
else\r
{\r
this.setStartAt(\r
blockBoundary,\r
!blockBoundary.is( 'br' ) &&\r
- ( !enlargeable || blockBoundary.contains( enlargeable ) ) ?\r
+ ( !enlargeable && this.checkStartOfBlock()\r
+ || enlargeable && blockBoundary.contains( enlargeable ) ) ?\r
CKEDITOR.POSITION_AFTER_START :\r
CKEDITOR.POSITION_AFTER_END );\r
\r
// the document position of it with 'enlargeable' node.\r
this.setEndAt(\r
blockBoundary,\r
- !blockBoundary.is( 'br' ) &&\r
- ( !enlargeable || blockBoundary.contains( enlargeable ) ) ?\r
+ ( !enlargeable && this.checkEndOfBlock()\r
+ || enlargeable && blockBoundary.contains( enlargeable ) ) ?\r
CKEDITOR.POSITION_BEFORE_END :\r
CKEDITOR.POSITION_BEFORE_START );\r
// We must include the <br> at the end of range if there's\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
+ */\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
+\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
+ // 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
+ startNode = startNode.getParent(), startOffset = startNode.getIndex();\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
+ endNode = endNode.getParent(), endOffset = endNode.getIndex() + 1;\r
+\r
this.endContainer = endNode;\r
this.endOffset = endOffset;\r
\r
}\r
else\r
{\r
- // Extract the contents of the block from the selection point to the end\r
- // of its contents.\r
- this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END );\r
- var documentFragment = this.extractContents();\r
-\r
- // Duplicate the block element after it.\r
- endBlock = startBlock.clone( false );\r
-\r
- // Place the extracted contents into the duplicated block.\r
- documentFragment.appendTo( endBlock );\r
- endBlock.insertAfter( startBlock );\r
- this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END );\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
},\r
\r
/**\r
+ * Branch the specified element from the collapsed range position and\r
+ * place the caret between the two result branches.\r
+ * Note: The range must be collapsed and been enclosed by this element.\r
+ * @param {CKEDITOR.dom.element} element\r
+ * @return {CKEDITOR.dom.element} Root element of the new branch after the split.\r
+ */\r
+ splitElement : function( toSplit )\r
+ {\r
+ if ( !this.collapsed )\r
+ return null;\r
+\r
+ // Extract the contents of the block from the selection point to the end\r
+ // of its contents.\r
+ this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );\r
+ var documentFragment = this.extractContents();\r
+\r
+ // Duplicate the element after it.\r
+ var clone = toSplit.clone( false );\r
+\r
+ // Place the extracted contents into the duplicated element.\r
+ documentFragment.appendTo( clone );\r
+ clone.insertAfter( toSplit );\r
+ this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );\r
+ return clone;\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
* @param {CKEDITOR.dom.element} element The target element to check.\r
},\r
\r
/**\r
- * Moves the range boundaries to the first editing point inside an\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} targetElement The element into which\r
- * look for the editing spot.\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( targetElement )\r
+ moveToElementEditablePosition : function( el, isMoveToEnd )\r
{\r
- var editableElement;\r
+ var isEditable;\r
\r
- while ( targetElement && targetElement.type == CKEDITOR.NODE_ELEMENT )\r
+ // Empty elements are rejected.\r
+ if ( CKEDITOR.dtd.$empty[ el.getName() ] )\r
+ return false;\r
+\r
+ while ( el && el.type == CKEDITOR.NODE_ELEMENT )\r
{\r
- if ( targetElement.isEditable() )\r
- editableElement = targetElement;\r
- else if ( editableElement )\r
- break ; // If we already found an editable element, stop the loop.\r
+ isEditable = el.isEditable();\r
+\r
+ // If an editable element is found, move inside it.\r
+ if ( isEditable )\r
+ this.moveToPosition( el, isMoveToEnd ?\r
+ CKEDITOR.POSITION_BEFORE_END :\r
+ 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
+ {\r
+ this.moveToPosition( el, isMoveToEnd ?\r
+ CKEDITOR.POSITION_AFTER_END :\r
+ CKEDITOR.POSITION_BEFORE_START );\r
+ return true;\r
+ }\r
\r
- targetElement = targetElement.getFirst();\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[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );\r
+ else\r
+ el = el[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );\r
\r
- if ( editableElement )\r
- {\r
- this.moveToPosition(editableElement, CKEDITOR.POSITION_AFTER_START);\r
- return true;\r
+ // Stop immediately if we've found a text node.\r
+ if ( el && el.type == CKEDITOR.NODE_TEXT )\r
+ {\r
+ this.moveToPosition( el, isMoveToEnd ?\r
+ CKEDITOR.POSITION_AFTER_END :\r
+ CKEDITOR.POSITION_BEFORE_START );\r
+ return true;\r
+ }\r
}\r
- else\r
- return false;\r
+\r
+ return isEditable;\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. (#\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.START = 1;\r
CKEDITOR.END = 2;\r
CKEDITOR.STARTEND = 3;\r
+\r
+CKEDITOR.SHRINK_ELEMENT = 1;\r
+CKEDITOR.SHRINK_TEXT = 2;\r