2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license
\r
7 * Creates a CKEDITOR.dom.range instance that can be used inside a specific
\r
9 * @class Represents a delimited piece of content in a DOM Document.
\r
10 * It is contiguous in the sense that it can be characterized as selecting all
\r
11 * of the content between a pair of boundary-points.<br>
\r
13 * This class shares much of the W3C
\r
14 * <a href="http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html">Document Object Model Range</a>
\r
15 * ideas and features, adding several range manipulation tools to it, but it's
\r
16 * not intended to be compatible with it.
\r
17 * @param {CKEDITOR.dom.document} document The document into which the range
\r
18 * features will be available.
\r
20 * // Create a range for the entire contents of the editor document body.
\r
21 * var range = new CKEDITOR.dom.range( editor.document );
\r
22 * range.selectNodeContents( editor.document.getBody() );
\r
23 * // Delete the contents.
\r
24 * range.deleteContents();
\r
26 CKEDITOR.dom.range = function( document )
\r
29 * Node within which the range begins.
\r
30 * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}
\r
32 * var range = new CKEDITOR.dom.range( editor.document );
\r
33 * range.selectNodeContents( editor.document.getBody() );
\r
34 * alert( range.startContainer.getName() ); // "body"
\r
36 this.startContainer = null;
\r
39 * Offset within the starting node of the range.
\r
42 * var range = new CKEDITOR.dom.range( editor.document );
\r
43 * range.selectNodeContents( editor.document.getBody() );
\r
44 * alert( range.startOffset ); // "0"
\r
46 this.startOffset = null;
\r
49 * Node within which the range ends.
\r
50 * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}
\r
52 * var range = new CKEDITOR.dom.range( editor.document );
\r
53 * range.selectNodeContents( editor.document.getBody() );
\r
54 * alert( range.endContainer.getName() ); // "body"
\r
56 this.endContainer = null;
\r
59 * Offset within the ending node of the range.
\r
62 * var range = new CKEDITOR.dom.range( editor.document );
\r
63 * range.selectNodeContents( editor.document.getBody() );
\r
64 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
\r
66 this.endOffset = null;
\r
69 * Indicates that this is a collapsed range. A collapsed range has it's
\r
70 * start and end boudaries at the very same point so nothing is contained
\r
73 * var range = new CKEDITOR.dom.range( editor.document );
\r
74 * range.selectNodeContents( editor.document.getBody() );
\r
75 * alert( range.collapsed ); // "false"
\r
77 * alert( range.collapsed ); // "true"
\r
79 this.collapsed = true;
\r
82 * The document within which the range can be used.
\r
83 * @type {CKEDITOR.dom.document}
\r
85 * // Selects the body contents of the range document.
\r
86 * range.selectNodeContents( range.document.getBody() );
\r
88 this.document = document;
\r
93 // Updates the "collapsed" property for the given range object.
\r
94 var updateCollapsed = function( range )
\r
97 range.startContainer &&
\r
98 range.endContainer &&
\r
99 range.startContainer.equals( range.endContainer ) &&
\r
100 range.startOffset == range.endOffset );
\r
103 // This is a shared function used to delete, extract and clone the range
\r
106 var execContentsAction = function( range, action, docFrag, mergeThen )
\r
108 range.optimizeBookmark();
\r
110 var startNode = range.startContainer;
\r
111 var endNode = range.endContainer;
\r
113 var startOffset = range.startOffset;
\r
114 var endOffset = range.endOffset;
\r
116 var removeStartNode;
\r
119 // For text containers, we must simply split the node and point to the
\r
120 // second part. The removal will be handled by the rest of the code .
\r
121 if ( endNode.type == CKEDITOR.NODE_TEXT )
\r
122 endNode = endNode.split( endOffset );
\r
125 // If the end container has children and the offset is pointing
\r
126 // to a child, then we should start from it.
\r
127 if ( endNode.getChildCount() > 0 )
\r
129 // If the offset points after the last node.
\r
130 if ( endOffset >= endNode.getChildCount() )
\r
132 // Let's create a temporary node and mark it for removal.
\r
133 endNode = endNode.append( range.document.createText( '' ) );
\r
134 removeEndNode = true;
\r
137 endNode = endNode.getChild( endOffset );
\r
141 // For text containers, we must simply split the node. The removal will
\r
142 // be handled by the rest of the code .
\r
143 if ( startNode.type == CKEDITOR.NODE_TEXT )
\r
145 startNode.split( startOffset );
\r
147 // In cases the end node is the same as the start node, the above
\r
148 // splitting will also split the end, so me must move the end to
\r
149 // the second part of the split.
\r
150 if ( startNode.equals( endNode ) )
\r
151 endNode = startNode.getNext();
\r
155 // If the start container has children and the offset is pointing
\r
156 // to a child, then we should start from its previous sibling.
\r
158 // If the offset points to the first node, we don't have a
\r
159 // sibling, so let's use the first one, but mark it for removal.
\r
160 if ( !startOffset )
\r
162 // Let's create a temporary node and mark it for removal.
\r
163 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) );
\r
164 removeStartNode = true;
\r
166 else if ( startOffset >= startNode.getChildCount() )
\r
168 // Let's create a temporary node and mark it for removal.
\r
169 startNode = startNode.append( range.document.createText( '' ) );
\r
170 removeStartNode = true;
\r
173 startNode = startNode.getChild( startOffset ).getPrevious();
\r
176 // Get the parent nodes tree for the start and end boundaries.
\r
177 var startParents = startNode.getParents();
\r
178 var endParents = endNode.getParents();
\r
180 // Compare them, to find the top most siblings.
\r
181 var i, topStart, topEnd;
\r
183 for ( i = 0 ; i < startParents.length ; i++ )
\r
185 topStart = startParents[ i ];
\r
186 topEnd = endParents[ i ];
\r
188 // The compared nodes will match until we find the top most
\r
189 // siblings (different nodes that have the same parent).
\r
190 // "i" will hold the index in the parents array for the top
\r
192 if ( !topStart.equals( topEnd ) )
\r
196 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling;
\r
198 // Remove all successive sibling nodes for every node in the
\r
199 // startParents tree.
\r
200 for ( var j = i ; j < startParents.length ; j++ )
\r
202 levelStartNode = startParents[j];
\r
204 // For Extract and Clone, we must clone this level.
\r
205 if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete
\r
206 levelClone = clone.append( levelStartNode.clone() );
\r
208 currentNode = levelStartNode.getNext();
\r
210 while ( currentNode )
\r
212 // Stop processing when the current node matches a node in the
\r
213 // endParents tree or if it is the endNode.
\r
214 if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) )
\r
217 // Cache the next sibling.
\r
218 currentSibling = currentNode.getNext();
\r
220 // If cloning, just clone it.
\r
221 if ( action == 2 ) // 2 = Clone
\r
222 clone.append( currentNode.clone( true ) );
\r
225 // Both Delete and Extract will remove the node.
\r
226 currentNode.remove();
\r
228 // When Extracting, move the removed node to the docFrag.
\r
229 if ( action == 1 ) // 1 = Extract
\r
230 clone.append( currentNode );
\r
233 currentNode = currentSibling;
\r
237 clone = levelClone;
\r
242 // Remove all previous sibling nodes for every node in the
\r
243 // endParents tree.
\r
244 for ( var k = i ; k < endParents.length ; k++ )
\r
246 levelStartNode = endParents[ k ];
\r
248 // For Extract and Clone, we must clone this level.
\r
249 if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete
\r
250 levelClone = clone.append( levelStartNode.clone() );
\r
252 // The processing of siblings may have already been done by the parent.
\r
253 if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode )
\r
255 currentNode = levelStartNode.getPrevious();
\r
257 while ( currentNode )
\r
259 // Stop processing when the current node matches a node in the
\r
260 // startParents tree or if it is the startNode.
\r
261 if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) )
\r
264 // Cache the next sibling.
\r
265 currentSibling = currentNode.getPrevious();
\r
267 // If cloning, just clone it.
\r
268 if ( action == 2 ) // 2 = Clone
\r
269 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ;
\r
272 // Both Delete and Extract will remove the node.
\r
273 currentNode.remove();
\r
275 // When Extracting, mode the removed node to the docFrag.
\r
276 if ( action == 1 ) // 1 = Extract
\r
277 clone.$.insertBefore( currentNode.$, clone.$.firstChild );
\r
280 currentNode = currentSibling;
\r
285 clone = levelClone;
\r
288 if ( action == 2 ) // 2 = Clone.
\r
290 // No changes in the DOM should be done, so fix the split text (if any).
\r
292 var startTextNode = range.startContainer;
\r
293 if ( startTextNode.type == CKEDITOR.NODE_TEXT )
\r
295 startTextNode.$.data += startTextNode.$.nextSibling.data;
\r
296 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );
\r
299 var endTextNode = range.endContainer;
\r
300 if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling )
\r
302 endTextNode.$.data += endTextNode.$.nextSibling.data;
\r
303 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );
\r
308 // Collapse the range.
\r
310 // If a node has been partially selected, collapse the range between
\r
311 // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).
\r
312 if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) )
\r
314 var endIndex = topEnd.getIndex();
\r
316 // If the start node is to be removed, we must correct the
\r
317 // index to reflect the removal.
\r
318 if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )
\r
321 // Merge splitted parents.
\r
322 if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT )
\r
324 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
\r
325 'data-cke-bookmark="1" style="display:none"> </span>', range.document );
\r
326 span.insertAfter( topStart );
\r
327 topStart.mergeSiblings( false );
\r
328 range.moveToBookmark( { startNode : span } );
\r
331 range.setStart( topEnd.getParent(), endIndex );
\r
334 // Collapse it to the start.
\r
335 range.collapse( true );
\r
338 // Cleanup any marked node.
\r
339 if ( removeStartNode )
\r
340 startNode.remove();
\r
342 if ( removeEndNode && endNode.$.parentNode )
\r
346 var inlineChildReqElements = { abbr:1,acronym:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,'var':1 };
\r
348 // Creates the appropriate node evaluator for the dom walker used inside
\r
349 // check(Start|End)OfBlock.
\r
350 function getCheckStartEndBlockEvalFunction( isStart )
\r
352 var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );
\r
353 return function( node )
\r
355 // First ignore bookmark nodes.
\r
356 if ( bookmarkEvaluator( node ) )
\r
359 if ( node.type == CKEDITOR.NODE_TEXT )
\r
361 // If there's any visible text, then we're not at the start.
\r
362 if ( node.hasAscendant( 'pre' ) || CKEDITOR.tools.trim( node.getText() ).length )
\r
365 else if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
367 // If there are non-empty inline elements (e.g. <img />), then we're not
\r
369 if ( !inlineChildReqElements[ node.getName() ] )
\r
371 // If we're working at the end-of-block, forgive the first <br /> in non-IE
\r
373 if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )
\r
383 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
\r
384 // text node and non-empty elements unless it's being bookmark text.
\r
385 function elementBoundaryEval( node )
\r
387 // Reject any text node unless it's being bookmark
\r
388 // OR it's spaces. (#3883)
\r
389 return node.type != CKEDITOR.NODE_TEXT
\r
390 && node.getName() in CKEDITOR.dtd.$removeEmpty
\r
391 || !CKEDITOR.tools.trim( node.getText() )
\r
392 || !!node.getParent().data( 'cke-bookmark' );
\r
395 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
\r
396 bookmarkEval = new CKEDITOR.dom.walker.bookmark();
\r
398 function nonWhitespaceOrBookmarkEval( node )
\r
400 // Whitespaces and bookmark nodes are to be ignored.
\r
401 return !whitespaceEval( node ) && !bookmarkEval( node );
\r
404 CKEDITOR.dom.range.prototype =
\r
408 var clone = new CKEDITOR.dom.range( this.document );
\r
410 clone.startContainer = this.startContainer;
\r
411 clone.startOffset = this.startOffset;
\r
412 clone.endContainer = this.endContainer;
\r
413 clone.endOffset = this.endOffset;
\r
414 clone.collapsed = this.collapsed;
\r
419 collapse : function( toStart )
\r
423 this.endContainer = this.startContainer;
\r
424 this.endOffset = this.startOffset;
\r
428 this.startContainer = this.endContainer;
\r
429 this.startOffset = this.endOffset;
\r
432 this.collapsed = true;
\r
436 * The content nodes of the range are cloned and added to a document fragment, which is returned.
\r
437 * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting).
\r
439 cloneContents : function()
\r
441 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
443 if ( !this.collapsed )
\r
444 execContentsAction( this, 2, docFrag );
\r
450 * Deletes the content nodes of the range permanently from the DOM tree.
\r
451 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
453 deleteContents : function( mergeThen )
\r
455 if ( this.collapsed )
\r
458 execContentsAction( this, 0, null, mergeThen );
\r
462 * The content nodes of the range are cloned and added to a document fragment,
\r
463 * meanwhile they're removed permanently from the DOM tree.
\r
464 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
466 extractContents : function( mergeThen )
\r
468 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
470 if ( !this.collapsed )
\r
471 execContentsAction( this, 1, docFrag, mergeThen );
\r
477 * Creates a bookmark object, which can be later used to restore the
\r
478 * range by using the moveToBookmark function.
\r
479 * This is an "intrusive" way to create a bookmark. It includes <span> tags
\r
480 * in the range boundaries. The advantage of it is that it is possible to
\r
481 * handle DOM mutations when moving back to the bookmark.
\r
482 * Attention: the inclusion of nodes in the DOM is a design choice and
\r
483 * should not be changed as there are other points in the code that may be
\r
484 * using those nodes to perform operations. See GetBookmarkNode.
\r
485 * @param {Boolean} [serializable] Indicates that the bookmark nodes
\r
486 * must contain ids, which can be used to restore the range even
\r
487 * when these nodes suffer mutations (like a clonation or innerHTML
\r
489 * @returns {Object} And object representing a bookmark.
\r
491 createBookmark : function( serializable )
\r
493 var startNode, endNode;
\r
496 var collapsed = this.collapsed;
\r
498 startNode = this.document.createElement( 'span' );
\r
499 startNode.data( 'cke-bookmark', 1 );
\r
500 startNode.setStyle( 'display', 'none' );
\r
502 // For IE, it must have something inside, otherwise it may be
\r
503 // removed during DOM operations.
\r
504 startNode.setHtml( ' ' );
\r
506 if ( serializable )
\r
508 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
\r
509 startNode.setAttribute( 'id', baseId + 'S' );
\r
512 // If collapsed, the endNode will not be created.
\r
515 endNode = startNode.clone();
\r
516 endNode.setHtml( ' ' );
\r
518 if ( serializable )
\r
519 endNode.setAttribute( 'id', baseId + 'E' );
\r
521 clone = this.clone();
\r
523 clone.insertNode( endNode );
\r
526 clone = this.clone();
\r
527 clone.collapse( true );
\r
528 clone.insertNode( startNode );
\r
530 // Update the range position.
\r
533 this.setStartAfter( startNode );
\r
534 this.setEndBefore( endNode );
\r
537 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
\r
540 startNode : serializable ? baseId + 'S' : startNode,
\r
541 endNode : serializable ? baseId + 'E' : endNode,
\r
542 serializable : serializable,
\r
543 collapsed : collapsed
\r
548 * Creates a "non intrusive" and "mutation sensible" bookmark. This
\r
549 * kind of bookmark should be used only when the DOM is supposed to
\r
550 * remain stable after its creation.
\r
551 * @param {Boolean} [normalized] Indicates that the bookmark must
\r
552 * normalized. When normalized, the successive text nodes are
\r
553 * considered a single node. To sucessful load a normalized
\r
554 * bookmark, the DOM tree must be also normalized before calling
\r
556 * @returns {Object} An object representing the bookmark.
\r
558 createBookmark2 : function( normalized )
\r
560 var startContainer = this.startContainer,
\r
561 endContainer = this.endContainer;
\r
563 var startOffset = this.startOffset,
\r
564 endOffset = this.endOffset;
\r
566 var collapsed = this.collapsed;
\r
568 var child, previous;
\r
570 // If there is no range then get out of here.
\r
571 // It happens on initial load in Safari #962 and if the editor it's
\r
572 // hidden also in Firefox
\r
573 if ( !startContainer || !endContainer )
\r
574 return { start : 0, end : 0 };
\r
578 // Find out if the start is pointing to a text node that will
\r
580 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
\r
582 child = startContainer.getChild( startOffset );
\r
584 // In this case, move the start information to that text
\r
586 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
587 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
589 startContainer = child;
\r
593 // Get the normalized offset.
\r
594 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
595 startOffset = child.getIndex( 1 );
\r
598 // Normalize the start.
\r
599 while ( startContainer.type == CKEDITOR.NODE_TEXT
\r
600 && ( previous = startContainer.getPrevious() )
\r
601 && previous.type == CKEDITOR.NODE_TEXT )
\r
603 startContainer = previous;
\r
604 startOffset += previous.getLength();
\r
607 // Process the end only if not normalized.
\r
610 // Find out if the start is pointing to a text node that
\r
611 // will be normalized.
\r
612 if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
\r
614 child = endContainer.getChild( endOffset );
\r
616 // In this case, move the start information to that
\r
618 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
619 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
621 endContainer = child;
\r
625 // Get the normalized offset.
\r
626 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
627 endOffset = child.getIndex( 1 );
\r
630 // Normalize the end.
\r
631 while ( endContainer.type == CKEDITOR.NODE_TEXT
\r
632 && ( previous = endContainer.getPrevious() )
\r
633 && previous.type == CKEDITOR.NODE_TEXT )
\r
635 endContainer = previous;
\r
636 endOffset += previous.getLength();
\r
642 start : startContainer.getAddress( normalized ),
\r
643 end : collapsed ? null : endContainer.getAddress( normalized ),
\r
644 startOffset : startOffset,
\r
645 endOffset : endOffset,
\r
646 normalized : normalized,
\r
647 collapsed : collapsed,
\r
648 is2 : true // It's a createBookmark2 bookmark.
\r
652 moveToBookmark : function( bookmark )
\r
654 if ( bookmark.is2 ) // Created with createBookmark2().
\r
656 // Get the start information.
\r
657 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
\r
658 startOffset = bookmark.startOffset;
\r
660 // Get the end information.
\r
661 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
\r
662 endOffset = bookmark.endOffset;
\r
664 // Set the start boundary.
\r
665 this.setStart( startContainer, startOffset );
\r
667 // Set the end boundary. If not available, collapse it.
\r
668 if ( endContainer )
\r
669 this.setEnd( endContainer, endOffset );
\r
671 this.collapse( true );
\r
673 else // Created with createBookmark().
\r
675 var serializable = bookmark.serializable,
\r
676 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
\r
677 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
\r
679 // Set the range start at the bookmark start node position.
\r
680 this.setStartBefore( startNode );
\r
682 // Remove it, because it may interfere in the setEndBefore call.
\r
683 startNode.remove();
\r
685 // Set the range end at the bookmark end node position, or simply
\r
686 // collapse it if it is not available.
\r
689 this.setEndBefore( endNode );
\r
693 this.collapse( true );
\r
697 getBoundaryNodes : function()
\r
699 var startNode = this.startContainer,
\r
700 endNode = this.endContainer,
\r
701 startOffset = this.startOffset,
\r
702 endOffset = this.endOffset,
\r
705 if ( startNode.type == CKEDITOR.NODE_ELEMENT )
\r
707 childCount = startNode.getChildCount();
\r
708 if ( childCount > startOffset )
\r
709 startNode = startNode.getChild( startOffset );
\r
710 else if ( childCount < 1 )
\r
711 startNode = startNode.getPreviousSourceNode();
\r
712 else // startOffset > childCount but childCount is not 0
\r
714 // Try to take the node just after the current position.
\r
715 startNode = startNode.$;
\r
716 while ( startNode.lastChild )
\r
717 startNode = startNode.lastChild;
\r
718 startNode = new CKEDITOR.dom.node( startNode );
\r
720 // Normally we should take the next node in DFS order. But it
\r
721 // is also possible that we've already reached the end of
\r
723 startNode = startNode.getNextSourceNode() || startNode;
\r
726 if ( endNode.type == CKEDITOR.NODE_ELEMENT )
\r
728 childCount = endNode.getChildCount();
\r
729 if ( childCount > endOffset )
\r
730 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
\r
731 else if ( childCount < 1 )
\r
732 endNode = endNode.getPreviousSourceNode();
\r
733 else // endOffset > childCount but childCount is not 0
\r
735 // Try to take the node just before the current position.
\r
736 endNode = endNode.$;
\r
737 while ( endNode.lastChild )
\r
738 endNode = endNode.lastChild;
\r
739 endNode = new CKEDITOR.dom.node( endNode );
\r
743 // Sometimes the endNode will come right before startNode for collapsed
\r
744 // ranges. Fix it. (#3780)
\r
745 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
\r
746 startNode = endNode;
\r
748 return { startNode : startNode, endNode : endNode };
\r
752 * Find the node which fully contains the range.
\r
753 * @param includeSelf
\r
754 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.
\r
756 getCommonAncestor : function( includeSelf , ignoreTextNode )
\r
758 var start = this.startContainer,
\r
759 end = this.endContainer,
\r
762 if ( start.equals( end ) )
\r
765 && start.type == CKEDITOR.NODE_ELEMENT
\r
766 && this.startOffset == this.endOffset - 1 )
\r
767 ancestor = start.getChild( this.startOffset );
\r
772 ancestor = start.getCommonAncestor( end );
\r
774 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
\r
778 * Transforms the startContainer and endContainer properties from text
\r
779 * nodes to element nodes, whenever possible. This is actually possible
\r
780 * if either of the boundary containers point to a text node, and its
\r
781 * offset is set to zero, or after the last char in the node.
\r
783 optimize : function()
\r
785 var container = this.startContainer;
\r
786 var offset = this.startOffset;
\r
788 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
791 this.setStartBefore( container );
\r
792 else if ( offset >= container.getLength() )
\r
793 this.setStartAfter( container );
\r
796 container = this.endContainer;
\r
797 offset = this.endOffset;
\r
799 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
802 this.setEndBefore( container );
\r
803 else if ( offset >= container.getLength() )
\r
804 this.setEndAfter( container );
\r
809 * Move the range out of bookmark nodes if they'd been the container.
\r
811 optimizeBookmark: function()
\r
813 var startNode = this.startContainer,
\r
814 endNode = this.endContainer;
\r
816 if ( startNode.is && startNode.is( 'span' )
\r
817 && startNode.data( 'cke-bookmark' ) )
\r
818 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
\r
819 if ( endNode && endNode.is && endNode.is( 'span' )
\r
820 && endNode.data( 'cke-bookmark' ) )
\r
821 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
\r
824 trim : function( ignoreStart, ignoreEnd )
\r
826 var startContainer = this.startContainer,
\r
827 startOffset = this.startOffset,
\r
828 collapsed = this.collapsed;
\r
829 if ( ( !ignoreStart || collapsed )
\r
830 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
832 // If the offset is zero, we just insert the new node before
\r
834 if ( !startOffset )
\r
836 startOffset = startContainer.getIndex();
\r
837 startContainer = startContainer.getParent();
\r
839 // If the offset is at the end, we'll insert it after the text
\r
841 else if ( startOffset >= startContainer.getLength() )
\r
843 startOffset = startContainer.getIndex() + 1;
\r
844 startContainer = startContainer.getParent();
\r
846 // In other case, we split the text node and insert the new
\r
847 // node at the split point.
\r
850 var nextText = startContainer.split( startOffset );
\r
852 startOffset = startContainer.getIndex() + 1;
\r
853 startContainer = startContainer.getParent();
\r
855 // Check all necessity of updating the end boundary.
\r
856 if ( this.startContainer.equals( this.endContainer ) )
\r
857 this.setEnd( nextText, this.endOffset - this.startOffset );
\r
858 else if ( startContainer.equals( this.endContainer ) )
\r
859 this.endOffset += 1;
\r
862 this.setStart( startContainer, startOffset );
\r
866 this.collapse( true );
\r
871 var endContainer = this.endContainer;
\r
872 var endOffset = this.endOffset;
\r
874 if ( !( ignoreEnd || collapsed )
\r
875 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
877 // If the offset is zero, we just insert the new node before
\r
881 endOffset = endContainer.getIndex();
\r
882 endContainer = endContainer.getParent();
\r
884 // If the offset is at the end, we'll insert it after the text
\r
886 else if ( endOffset >= endContainer.getLength() )
\r
888 endOffset = endContainer.getIndex() + 1;
\r
889 endContainer = endContainer.getParent();
\r
891 // In other case, we split the text node and insert the new
\r
892 // node at the split point.
\r
895 endContainer.split( endOffset );
\r
897 endOffset = endContainer.getIndex() + 1;
\r
898 endContainer = endContainer.getParent();
\r
901 this.setEnd( endContainer, endOffset );
\r
906 * Expands the range so that partial units are completely contained.
\r
907 * @param unit {Number} The unit type to expand with.
\r
908 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
\r
910 enlarge : function( unit, excludeBrs )
\r
914 case CKEDITOR.ENLARGE_ELEMENT :
\r
916 if ( this.collapsed )
\r
919 // Get the common ancestor.
\r
920 var commonAncestor = this.getCommonAncestor();
\r
922 var body = this.document.getBody();
\r
924 // For each boundary
\r
925 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
\r
926 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the common ancestor is not reached. After reaching the common ancestor, just save the enlargeable node to be used later.
\r
928 var startTop, endTop;
\r
930 var enlargeable, sibling, commonReached;
\r
932 // Indicates that the node can be added only if whitespace
\r
933 // is available before it.
\r
934 var needsWhiteSpace = false;
\r
938 // Process the start boundary.
\r
940 var container = this.startContainer;
\r
941 var offset = this.startOffset;
\r
943 if ( container.type == CKEDITOR.NODE_TEXT )
\r
947 // Check if there is any non-space text before the
\r
948 // offset. Otherwise, container is null.
\r
949 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
\r
951 // If we found only whitespace in the node, it
\r
952 // means that we'll need more whitespace to be able
\r
953 // to expand. For example, <i> can be expanded in
\r
954 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
955 needsWhiteSpace = !!container;
\r
960 if ( !( sibling = container.getPrevious() ) )
\r
961 enlargeable = container.getParent();
\r
966 // If we have offset, get the node preceeding it as the
\r
967 // first sibling to be checked.
\r
969 sibling = container.getChild( offset - 1 ) || container.getLast();
\r
971 // If there is no sibling, mark the container to be
\r
974 enlargeable = container;
\r
977 while ( enlargeable || sibling )
\r
979 if ( enlargeable && !sibling )
\r
981 // If we reached the common ancestor, mark the flag
\r
983 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
984 commonReached = true;
\r
986 if ( !body.contains( enlargeable ) )
\r
989 // If we don't need space or this element breaks
\r
990 // the line, then enlarge it.
\r
991 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
993 needsWhiteSpace = false;
\r
995 // If the common ancestor has been reached,
\r
996 // we'll not enlarge it immediately, but just
\r
997 // mark it to be enlarged later if the end
\r
998 // boundary also enlarges it.
\r
999 if ( commonReached )
\r
1000 startTop = enlargeable;
\r
1002 this.setStartBefore( enlargeable );
\r
1005 sibling = enlargeable.getPrevious();
\r
1008 // Check all sibling nodes preceeding the enlargeable
\r
1009 // node. The node wil lbe enlarged only if none of them
\r
1013 // This flag indicates that this node has
\r
1014 // whitespaces at the end.
\r
1015 isWhiteSpace = false;
\r
1017 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1019 siblingText = sibling.getText();
\r
1021 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1024 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
\r
1028 // If this is a visible element.
\r
1029 // We need to check for the bookmark attribute because IE insists on
\r
1030 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1031 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1032 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1034 // We'll accept it only if we need
\r
1035 // whitespace, and this is an inline
\r
1036 // element with whitespace only.
\r
1037 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1039 // It must contains spaces and inline elements only.
\r
1041 siblingText = sibling.getText();
\r
1043 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
\r
1047 var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
\r
1048 for ( var i = 0, child ; child = allChildren[ i++ ] ; )
\r
1050 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1059 isWhiteSpace = !!siblingText.length;
\r
1066 // A node with whitespaces has been found.
\r
1067 if ( isWhiteSpace )
\r
1069 // Enlarge the last enlargeable node, if we
\r
1070 // were waiting for spaces.
\r
1071 if ( needsWhiteSpace )
\r
1073 if ( commonReached )
\r
1074 startTop = enlargeable;
\r
1075 else if ( enlargeable )
\r
1076 this.setStartBefore( enlargeable );
\r
1079 needsWhiteSpace = true;
\r
1084 var next = sibling.getPrevious();
\r
1086 if ( !enlargeable && !next )
\r
1088 // Set the sibling as enlargeable, so it's
\r
1089 // parent will be get later outside this while.
\r
1090 enlargeable = sibling;
\r
1099 // If sibling has been set to null, then we
\r
1100 // need to stop enlarging.
\r
1101 enlargeable = null;
\r
1105 if ( enlargeable )
\r
1106 enlargeable = enlargeable.getParent();
\r
1109 // Process the end boundary. This is basically the same
\r
1110 // code used for the start boundary, with small changes to
\r
1111 // make it work in the oposite side (to the right). This
\r
1112 // makes it difficult to reuse the code here. So, fixes to
\r
1113 // the above code are likely to be replicated here.
\r
1115 container = this.endContainer;
\r
1116 offset = this.endOffset;
\r
1118 // Reset the common variables.
\r
1119 enlargeable = sibling = null;
\r
1120 commonReached = needsWhiteSpace = false;
\r
1122 if ( container.type == CKEDITOR.NODE_TEXT )
\r
1124 // Check if there is any non-space text after the
\r
1125 // offset. Otherwise, container is null.
\r
1126 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;
\r
1128 // If we found only whitespace in the node, it
\r
1129 // means that we'll need more whitespace to be able
\r
1130 // to expand. For example, <i> can be expanded in
\r
1131 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
1132 needsWhiteSpace = !( container && container.getLength() );
\r
1136 if ( !( sibling = container.getNext() ) )
\r
1137 enlargeable = container.getParent();
\r
1142 // Get the node right after the boudary to be checked
\r
1144 sibling = container.getChild( offset );
\r
1147 enlargeable = container;
\r
1150 while ( enlargeable || sibling )
\r
1152 if ( enlargeable && !sibling )
\r
1154 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1155 commonReached = true;
\r
1157 if ( !body.contains( enlargeable ) )
\r
1160 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1162 needsWhiteSpace = false;
\r
1164 if ( commonReached )
\r
1165 endTop = enlargeable;
\r
1166 else if ( enlargeable )
\r
1167 this.setEndAfter( enlargeable );
\r
1170 sibling = enlargeable.getNext();
\r
1175 isWhiteSpace = false;
\r
1177 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1179 siblingText = sibling.getText();
\r
1181 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1184 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
\r
1188 // If this is a visible element.
\r
1189 // We need to check for the bookmark attribute because IE insists on
\r
1190 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1191 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1192 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1194 // We'll accept it only if we need
\r
1195 // whitespace, and this is an inline
\r
1196 // element with whitespace only.
\r
1197 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1199 // It must contains spaces and inline elements only.
\r
1201 siblingText = sibling.getText();
\r
1203 if ( (/[^\s\ufeff]/).test( siblingText ) )
\r
1207 allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
\r
1208 for ( i = 0 ; child = allChildren[ i++ ] ; )
\r
1210 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1219 isWhiteSpace = !!siblingText.length;
\r
1226 if ( isWhiteSpace )
\r
1228 if ( needsWhiteSpace )
\r
1230 if ( commonReached )
\r
1231 endTop = enlargeable;
\r
1233 this.setEndAfter( enlargeable );
\r
1239 next = sibling.getNext();
\r
1241 if ( !enlargeable && !next )
\r
1243 enlargeable = sibling;
\r
1252 // If sibling has been set to null, then we
\r
1253 // need to stop enlarging.
\r
1254 enlargeable = null;
\r
1258 if ( enlargeable )
\r
1259 enlargeable = enlargeable.getParent();
\r
1262 // If the common ancestor can be enlarged by both boundaries, then include it also.
\r
1263 if ( startTop && endTop )
\r
1265 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
\r
1267 this.setStartBefore( commonAncestor );
\r
1268 this.setEndAfter( commonAncestor );
\r
1272 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
\r
1273 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
\r
1275 // Enlarging the start boundary.
\r
1276 var walkerRange = new CKEDITOR.dom.range( this.document );
\r
1278 body = this.document.getBody();
\r
1280 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
1281 walkerRange.setEnd( this.startContainer, this.startOffset );
\r
1283 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1284 blockBoundary, // The node on which the enlarging should stop.
\r
1285 tailBr, // In case BR as block boundary.
\r
1286 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary(
\r
1287 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),
\r
1288 // Record the encountered 'blockBoundary' for later use.
\r
1289 boundaryGuard = function( node )
\r
1291 var retval = notBlockBoundary( node );
\r
1293 blockBoundary = node;
\r
1296 // Record the encounted 'tailBr' for later use.
\r
1297 tailBrGuard = function( node )
\r
1299 var retval = boundaryGuard( node );
\r
1300 if ( !retval && node.is && node.is( 'br' ) )
\r
1305 walker.guard = boundaryGuard;
\r
1307 enlargeable = walker.lastBackward();
\r
1309 // It's the body which stop the enlarging if no block boundary found.
\r
1310 blockBoundary = blockBoundary || body;
\r
1312 // Start the range either after the end of found block (<p>...</p>[text)
\r
1313 // or at the start of block (<p>[text...), by comparing the document position
\r
1314 // with 'enlargeable' node.
\r
1317 !blockBoundary.is( 'br' ) &&
\r
1318 ( !enlargeable && this.checkStartOfBlock()
\r
1319 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1320 CKEDITOR.POSITION_AFTER_START :
\r
1321 CKEDITOR.POSITION_AFTER_END );
\r
1323 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
\r
1324 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )
\r
1326 var theRange = this.clone();
\r
1327 walker = new CKEDITOR.dom.walker( theRange );
\r
1329 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
\r
1330 bookmark = CKEDITOR.dom.walker.bookmark();
\r
1332 walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };
\r
1333 var previous = walker.previous();
\r
1334 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
\r
1339 // Enlarging the end boundary.
\r
1340 walkerRange = this.clone();
\r
1341 walkerRange.collapse();
\r
1342 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
1343 walker = new CKEDITOR.dom.walker( walkerRange );
\r
1345 // tailBrGuard only used for on range end.
\r
1346 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?
\r
1347 tailBrGuard : boundaryGuard;
\r
1348 blockBoundary = null;
\r
1349 // End the range right before the block boundary node.
\r
1351 enlargeable = walker.lastForward();
\r
1353 // It's the body which stop the enlarging if no block boundary found.
\r
1354 blockBoundary = blockBoundary || body;
\r
1356 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
\r
1357 // by comparing the document position with 'enlargeable' node.
\r
1360 ( !enlargeable && this.checkEndOfBlock()
\r
1361 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1362 CKEDITOR.POSITION_BEFORE_END :
\r
1363 CKEDITOR.POSITION_BEFORE_START );
\r
1364 // We must include the <br> at the end of range if there's
\r
1365 // one and we're expanding list item contents
\r
1367 this.setEndAfter( tailBr );
\r
1372 * Descrease the range to make sure that boundaries
\r
1373 * always anchor beside text nodes or innermost element.
\r
1374 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.
\r
1376 * <dt>CKEDITOR.SHRINK_ELEMENT</dt>
\r
1377 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>
\r
1378 * <dt>CKEDITOR.SHRINK_TEXT</dt>
\r
1379 * <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
1381 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
\r
1383 shrink : function( mode, selectContents )
\r
1385 // Unable to shrink a collapsed range.
\r
1386 if ( !this.collapsed )
\r
1388 mode = mode || CKEDITOR.SHRINK_TEXT;
\r
1390 var walkerRange = this.clone();
\r
1392 var startContainer = this.startContainer,
\r
1393 endContainer = this.endContainer,
\r
1394 startOffset = this.startOffset,
\r
1395 endOffset = this.endOffset,
\r
1396 collapsed = this.collapsed;
\r
1398 // Whether the start/end boundary is moveable.
\r
1399 var moveStart = 1,
\r
1402 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1404 if ( !startOffset )
\r
1405 walkerRange.setStartBefore( startContainer );
\r
1406 else if ( startOffset >= startContainer.getLength( ) )
\r
1407 walkerRange.setStartAfter( startContainer );
\r
1410 // Enlarge the range properly to avoid walker making
\r
1411 // DOM changes caused by triming the text nodes later.
\r
1412 walkerRange.setStartBefore( startContainer );
\r
1417 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
1420 walkerRange.setEndBefore( endContainer );
\r
1421 else if ( endOffset >= endContainer.getLength( ) )
\r
1422 walkerRange.setEndAfter( endContainer );
\r
1425 walkerRange.setEndAfter( endContainer );
\r
1430 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1431 isBookmark = CKEDITOR.dom.walker.bookmark();
\r
1433 walker.evaluator = function( node )
\r
1435 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
\r
1436 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
\r
1439 var currentElement;
\r
1440 walker.guard = function( node, movingOut )
\r
1442 if ( isBookmark( node ) )
\r
1445 // Stop when we're shrink in element mode while encountering a text node.
\r
1446 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
\r
1449 // Stop when we've already walked "through" an element.
\r
1450 if ( movingOut && node.equals( currentElement ) )
\r
1453 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
\r
1454 currentElement = node;
\r
1461 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
\r
1462 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
\r
1468 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
\r
1469 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
\r
1472 return !!( moveStart || moveEnd );
\r
1477 * Inserts a node at the start of the range. The range will be expanded
\r
1478 * the contain the node.
\r
1480 insertNode : function( node )
\r
1482 this.optimizeBookmark();
\r
1483 this.trim( false, true );
\r
1485 var startContainer = this.startContainer;
\r
1486 var startOffset = this.startOffset;
\r
1488 var nextNode = startContainer.getChild( startOffset );
\r
1491 node.insertBefore( nextNode );
\r
1493 startContainer.append( node );
\r
1495 // Check if we need to update the end boundary.
\r
1496 if ( node.getParent().equals( this.endContainer ) )
\r
1499 // Expand the range to embrace the new node.
\r
1500 this.setStartBefore( node );
\r
1503 moveToPosition : function( node, position )
\r
1505 this.setStartAt( node, position );
\r
1506 this.collapse( true );
\r
1509 selectNodeContents : function( node )
\r
1511 this.setStart( node, 0 );
\r
1512 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
\r
1516 * Sets the start position of a Range.
\r
1517 * @param {CKEDITOR.dom.node} startNode The node to start the range.
\r
1518 * @param {Number} startOffset An integer greater than or equal to zero
\r
1519 * representing the offset for the start of the range from the start
\r
1522 setStart : function( startNode, startOffset )
\r
1524 // W3C requires a check for the new position. If it is after the end
\r
1525 // boundary, the range should be collapsed to the new start. It seams
\r
1526 // we will not need this check for our use of this class so we can
\r
1527 // ignore it for now.
\r
1529 // Fixing invalid range start inside dtd empty elements.
\r
1530 if( startNode.type == CKEDITOR.NODE_ELEMENT
\r
1531 && CKEDITOR.dtd.$empty[ startNode.getName() ] )
\r
1532 startOffset = startNode.getIndex(), startNode = startNode.getParent();
\r
1534 this.startContainer = startNode;
\r
1535 this.startOffset = startOffset;
\r
1537 if ( !this.endContainer )
\r
1539 this.endContainer = startNode;
\r
1540 this.endOffset = startOffset;
\r
1543 updateCollapsed( this );
\r
1547 * Sets the end position of a Range.
\r
1548 * @param {CKEDITOR.dom.node} endNode The node to end the range.
\r
1549 * @param {Number} endOffset An integer greater than or equal to zero
\r
1550 * representing the offset for the end of the range from the start
\r
1553 setEnd : function( endNode, endOffset )
\r
1555 // W3C requires a check for the new position. If it is before the start
\r
1556 // boundary, the range should be collapsed to the new end. It seams we
\r
1557 // will not need this check for our use of this class so we can ignore
\r
1560 // Fixing invalid range end inside dtd empty elements.
\r
1561 if( endNode.type == CKEDITOR.NODE_ELEMENT
\r
1562 && CKEDITOR.dtd.$empty[ endNode.getName() ] )
\r
1563 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
\r
1565 this.endContainer = endNode;
\r
1566 this.endOffset = endOffset;
\r
1568 if ( !this.startContainer )
\r
1570 this.startContainer = endNode;
\r
1571 this.startOffset = endOffset;
\r
1574 updateCollapsed( this );
\r
1577 setStartAfter : function( node )
\r
1579 this.setStart( node.getParent(), node.getIndex() + 1 );
\r
1582 setStartBefore : function( node )
\r
1584 this.setStart( node.getParent(), node.getIndex() );
\r
1587 setEndAfter : function( node )
\r
1589 this.setEnd( node.getParent(), node.getIndex() + 1 );
\r
1592 setEndBefore : function( node )
\r
1594 this.setEnd( node.getParent(), node.getIndex() );
\r
1597 setStartAt : function( node, position )
\r
1599 switch( position )
\r
1601 case CKEDITOR.POSITION_AFTER_START :
\r
1602 this.setStart( node, 0 );
\r
1605 case CKEDITOR.POSITION_BEFORE_END :
\r
1606 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1607 this.setStart( node, node.getLength() );
\r
1609 this.setStart( node, node.getChildCount() );
\r
1612 case CKEDITOR.POSITION_BEFORE_START :
\r
1613 this.setStartBefore( node );
\r
1616 case CKEDITOR.POSITION_AFTER_END :
\r
1617 this.setStartAfter( node );
\r
1620 updateCollapsed( this );
\r
1623 setEndAt : function( node, position )
\r
1625 switch( position )
\r
1627 case CKEDITOR.POSITION_AFTER_START :
\r
1628 this.setEnd( node, 0 );
\r
1631 case CKEDITOR.POSITION_BEFORE_END :
\r
1632 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1633 this.setEnd( node, node.getLength() );
\r
1635 this.setEnd( node, node.getChildCount() );
\r
1638 case CKEDITOR.POSITION_BEFORE_START :
\r
1639 this.setEndBefore( node );
\r
1642 case CKEDITOR.POSITION_AFTER_END :
\r
1643 this.setEndAfter( node );
\r
1646 updateCollapsed( this );
\r
1649 fixBlock : function( isStart, blockTag )
\r
1651 var bookmark = this.createBookmark(),
\r
1652 fixedBlock = this.document.createElement( blockTag );
\r
1654 this.collapse( isStart );
\r
1656 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
\r
1658 this.extractContents().appendTo( fixedBlock );
\r
1659 fixedBlock.trim();
\r
1661 if ( !CKEDITOR.env.ie )
\r
1662 fixedBlock.appendBogus();
\r
1664 this.insertNode( fixedBlock );
\r
1666 this.moveToBookmark( bookmark );
\r
1668 return fixedBlock;
\r
1671 splitBlock : function( blockTag )
\r
1673 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
\r
1674 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1676 var startBlockLimit = startPath.blockLimit,
\r
1677 endBlockLimit = endPath.blockLimit;
\r
1679 var startBlock = startPath.block,
\r
1680 endBlock = endPath.block;
\r
1682 var elementPath = null;
\r
1683 // Do nothing if the boundaries are in different block limits.
\r
1684 if ( !startBlockLimit.equals( endBlockLimit ) )
\r
1687 // Get or fix current blocks.
\r
1688 if ( blockTag != 'br' )
\r
1690 if ( !startBlock )
\r
1692 startBlock = this.fixBlock( true, blockTag );
\r
1693 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
\r
1697 endBlock = this.fixBlock( false, blockTag );
\r
1700 // Get the range position.
\r
1701 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
\r
1702 isEndOfBlock = endBlock && this.checkEndOfBlock();
\r
1704 // Delete the current contents.
\r
1705 // TODO: Why is 2.x doing CheckIsEmpty()?
\r
1706 this.deleteContents();
\r
1708 if ( startBlock && startBlock.equals( endBlock ) )
\r
1710 if ( isEndOfBlock )
\r
1712 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1713 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
\r
1716 else if ( isStartOfBlock )
\r
1718 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1719 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
\r
1720 startBlock = null;
\r
1724 endBlock = this.splitElement( startBlock );
\r
1726 // In Gecko, the last child node must be a bogus <br>.
\r
1727 // Note: bogus <br> added under <ul> or <ol> would cause
\r
1728 // lists to be incorrectly rendered.
\r
1729 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )
\r
1730 startBlock.appendBogus() ;
\r
1735 previousBlock : startBlock,
\r
1736 nextBlock : endBlock,
\r
1737 wasStartOfBlock : isStartOfBlock,
\r
1738 wasEndOfBlock : isEndOfBlock,
\r
1739 elementPath : elementPath
\r
1744 * Branch the specified element from the collapsed range position and
\r
1745 * place the caret between the two result branches.
\r
1746 * Note: The range must be collapsed and been enclosed by this element.
\r
1747 * @param {CKEDITOR.dom.element} element
\r
1748 * @return {CKEDITOR.dom.element} Root element of the new branch after the split.
\r
1750 splitElement : function( toSplit )
\r
1752 if ( !this.collapsed )
\r
1755 // Extract the contents of the block from the selection point to the end
\r
1756 // of its contents.
\r
1757 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
\r
1758 var documentFragment = this.extractContents();
\r
1760 // Duplicate the element after it.
\r
1761 var clone = toSplit.clone( false );
\r
1763 // Place the extracted contents into the duplicated element.
\r
1764 documentFragment.appendTo( clone );
\r
1765 clone.insertAfter( toSplit );
\r
1766 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
\r
1771 * Check whether a range boundary is at the inner boundary of a given
\r
1773 * @param {CKEDITOR.dom.element} element The target element to check.
\r
1774 * @param {Number} checkType The boundary to check for both the range
\r
1775 * and the element. It can be CKEDITOR.START or CKEDITOR.END.
\r
1776 * @returns {Boolean} "true" if the range boundary is at the inner
\r
1777 * boundary of the element.
\r
1779 checkBoundaryOfElement : function( element, checkType )
\r
1781 var checkStart = ( checkType == CKEDITOR.START );
\r
1783 // Create a copy of this range, so we can manipulate it for our checks.
\r
1784 var walkerRange = this.clone();
\r
1786 // Collapse the range at the proper size.
\r
1787 walkerRange.collapse( checkStart );
\r
1789 // Expand the range to element boundary.
\r
1790 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
\r
1791 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
\r
1793 // Create the walker, which will check if we have anything useful
\r
1795 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1796 walker.evaluator = elementBoundaryEval;
\r
1798 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
\r
1801 // Calls to this function may produce changes to the DOM. The range may
\r
1802 // be updated to reflect such changes.
\r
1803 checkStartOfBlock : function()
\r
1805 var startContainer = this.startContainer,
\r
1806 startOffset = this.startOffset;
\r
1808 // If the starting node is a text node, and non-empty before the offset,
\r
1809 // then we're surely not at the start of block.
\r
1810 if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1812 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
\r
1813 if ( textBefore.length )
\r
1817 // Antecipate the trim() call here, so the walker will not make
\r
1818 // changes to the DOM, which would not get reflected into this
\r
1819 // range otherwise.
\r
1822 // We need to grab the block element holding the start boundary, so
\r
1823 // let's use an element path for it.
\r
1824 var path = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1826 // Creates a range starting at the block start until the range start.
\r
1827 var walkerRange = this.clone();
\r
1828 walkerRange.collapse( true );
\r
1829 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
\r
1831 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1832 walker.evaluator = getCheckStartEndBlockEvalFunction( true );
\r
1834 return walker.checkBackward();
\r
1837 checkEndOfBlock : function()
\r
1839 var endContainer = this.endContainer,
\r
1840 endOffset = this.endOffset;
\r
1842 // If the ending node is a text node, and non-empty after the offset,
\r
1843 // then we're surely not at the end of block.
\r
1844 if ( endContainer.type == CKEDITOR.NODE_TEXT )
\r
1846 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
\r
1847 if ( textAfter.length )
\r
1851 // Antecipate the trim() call here, so the walker will not make
\r
1852 // changes to the DOM, which would not get reflected into this
\r
1853 // range otherwise.
\r
1856 // We need to grab the block element holding the start boundary, so
\r
1857 // let's use an element path for it.
\r
1858 var path = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1860 // Creates a range starting at the block start until the range start.
\r
1861 var walkerRange = this.clone();
\r
1862 walkerRange.collapse( false );
\r
1863 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
\r
1865 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1866 walker.evaluator = getCheckStartEndBlockEvalFunction( false );
\r
1868 return walker.checkForward();
\r
1872 * Check if elements at which the range boundaries anchor are read-only,
\r
1873 * with respect to "contenteditable" attribute.
\r
1875 checkReadOnly : ( function()
\r
1877 function checkNodesEditable( node, anotherEnd )
\r
1881 if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
1883 if ( node.getAttribute( 'contentEditable' ) == 'false'
\r
1884 && !node.data( 'cke-editable' ) )
\r
1888 // Range enclosed entirely in an editable element.
\r
1889 else if ( node.is( 'html' )
\r
1890 || node.getAttribute( 'contentEditable' ) == 'true'
\r
1891 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
\r
1896 node = node.getParent();
\r
1904 var startNode = this.startContainer,
\r
1905 endNode = this.endContainer;
\r
1907 // Check if elements path at both boundaries are editable.
\r
1908 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
\r
1913 * Moves the range boundaries to the first/end editing point inside an
\r
1914 * element. For example, in an element tree like
\r
1915 * "<p><b><i></i></b> Text</p>", the start editing point is
\r
1916 * "<p><b><i>^</i></b> Text</p>" (inside <i>).
\r
1917 * @param {CKEDITOR.dom.element} el The element into which look for the
\r
1919 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
\r
1921 moveToElementEditablePosition : function( el, isMoveToEnd )
\r
1923 function nextDFS( node, childOnly )
\r
1927 if ( node.type == CKEDITOR.NODE_ELEMENT
\r
1928 && node.isEditable( false )
\r
1929 && !CKEDITOR.dtd.$nonEditable[ node.getName() ] )
\r
1931 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
\r
1934 if ( !childOnly && !next )
\r
1935 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
\r
1944 // Stop immediately if we've found a text node.
\r
1945 if ( el.type == CKEDITOR.NODE_TEXT )
\r
1947 this.moveToPosition( el, isMoveToEnd ?
\r
1948 CKEDITOR.POSITION_AFTER_END :
\r
1949 CKEDITOR.POSITION_BEFORE_START );
\r
1954 // If an editable element is found, move inside it, but not stop the searching.
\r
1955 if ( el.type == CKEDITOR.NODE_ELEMENT )
\r
1957 if ( el.isEditable() )
\r
1959 this.moveToPosition( el, isMoveToEnd ?
\r
1960 CKEDITOR.POSITION_BEFORE_END :
\r
1961 CKEDITOR.POSITION_AFTER_START );
\r
1966 el = nextDFS( el, found );
\r
1973 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
1975 moveToElementEditStart : function( target )
\r
1977 return this.moveToElementEditablePosition( target );
\r
1981 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
1983 moveToElementEditEnd : function( target )
\r
1985 return this.moveToElementEditablePosition( target, true );
\r
1989 * Get the single node enclosed within the range if there's one.
\r
1991 getEnclosedNode : function()
\r
1993 var walkerRange = this.clone();
\r
1995 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
\r
1996 walkerRange.optimize();
\r
1997 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT
\r
1998 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
\r
2001 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
2002 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),
\r
2003 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
\r
2004 evaluator = function( node )
\r
2006 return isNotWhitespaces( node ) && isNotBookmarks( node );
\r
2008 walkerRange.evaluator = evaluator;
\r
2009 var node = walker.next();
\r
2011 return node && node.equals( walker.previous() ) ? node : null;
\r
2014 getTouchedStartNode : function()
\r
2016 var container = this.startContainer ;
\r
2018 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2019 return container ;
\r
2021 return container.getChild( this.startOffset ) || container ;
\r
2024 getTouchedEndNode : function()
\r
2026 var container = this.endContainer ;
\r
2028 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2029 return container ;
\r
2031 return container.getChild( this.endOffset - 1 ) || container ;
\r
2036 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text"
\r
2037 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^"
\r
2038 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text"
\r
2039 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text"
\r
2041 CKEDITOR.ENLARGE_ELEMENT = 1;
\r
2042 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
\r
2043 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
\r
2045 // Check boundary types.
\r
2046 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
\r
2047 CKEDITOR.START = 1;
\r
2049 CKEDITOR.STARTEND = 3;
\r
2051 // Shrink range types.
\r
2052 // @see CKEDITOR.dom.range.prototype.shrink
\r
2053 CKEDITOR.SHRINK_ELEMENT = 1;
\r
2054 CKEDITOR.SHRINK_TEXT = 2;
\r