/*\r
-Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.\r
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.\r
For licensing, see LICENSE.html or http://ckeditor.com/license\r
*/\r
\r
+/**\r
+ * @class\r
+ */\r
CKEDITOR.dom.range = function( document )\r
{\r
this.startContainer = null;\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
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
// 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
+ }\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
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
+ || !!node.getParent().data( 'cke-bookmark' );\r
}\r
\r
var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),\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
}\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
return {\r
startNode : serializable ? baseId + '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
}\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
\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
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
},\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
// 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
// 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
}\r
\r
- var walker = new CKEDITOR.dom.walker( walkerRange );\r
+ var walker = new CKEDITOR.dom.walker( walkerRange ),\r
+ isBookmark = CKEDITOR.dom.walker.bookmark();\r
\r
walker.evaluator = function( node )\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
// 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
+ startOffset = startNode.getIndex(), startNode = startNode.getParent();\r
\r
this.startContainer = startNode;\r
this.startOffset = startOffset;\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
+ endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();\r
\r
this.endContainer = endNode;\r
this.endOffset = endOffset;\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
+ // 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;\r
- return walker[ checkType == CKEDITOR.START ?\r
- 'checkBackward' : 'checkForward' ]();\r
+\r
+ return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();\r
},\r
\r
// Calls to this function may produce changes to the DOM. The range may\r
},\r
\r
/**\r
+ * Check if elements at which the range boundaries anchor are read-only,\r
+ * with respect to "contenteditable" attribute.\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( 'body' )\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
CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;\r
CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;\r
\r
-/**\r
- * Check boundary types.\r
- * @see CKEDITOR.dom.range.prototype.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
-/**\r
- * Shrink range types.\r
- * @see CKEDITOR.dom.range.prototype.shrink\r
- */\r
+// Shrink range types.\r
+// @see CKEDITOR.dom.range.prototype.shrink\r
CKEDITOR.SHRINK_ELEMENT = 1;\r
CKEDITOR.SHRINK_TEXT = 2;\r