2 Copyright (c) 2003-2012, 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 skipBogus = false,
\r
353 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
\r
354 nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/;
\r
356 return function( node )
\r
358 // First ignore bookmark nodes.
\r
359 if ( bookmarkEvaluator( node ) )
\r
362 if ( node.type == CKEDITOR.NODE_TEXT )
\r
364 // Skip the block filler NBSP.
\r
365 if ( CKEDITOR.env.ie &&
\r
366 nbspRegExp.test( node.getText() ) &&
\r
368 !( isStart && node.getNext() ) )
\r
372 // If there's any visible text, then we're not at the start.
\r
373 else if ( node.hasAscendant( 'pre' ) || CKEDITOR.tools.trim( node.getText() ).length )
\r
376 else if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
378 // If there are non-empty inline elements (e.g. <img />), then we're not
\r
380 if ( !inlineChildReqElements[ node.getName() ] )
\r
382 // Skip the padding block br.
\r
383 if ( !CKEDITOR.env.ie &&
\r
386 !( isStart && node.getNext() ) )
\r
399 var isBogus = CKEDITOR.dom.walker.bogus();
\r
400 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
\r
401 // text node and non-empty elements unless it's being bookmark text.
\r
402 function elementBoundaryEval( checkStart )
\r
404 return function( node )
\r
406 // Tolerant bogus br when checking at the end of block.
\r
407 // Reject any text node unless it's being bookmark
\r
409 // Reject any element unless it's being invisible empty. (#3883)
\r
410 return !checkStart && isBogus( node ) ||
\r
411 ( node.type == CKEDITOR.NODE_TEXT ?
\r
412 !CKEDITOR.tools.trim( node.getText() ) || !!node.getParent().data( 'cke-bookmark' )
\r
413 : node.getName() in CKEDITOR.dtd.$removeEmpty );
\r
417 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
\r
418 bookmarkEval = new CKEDITOR.dom.walker.bookmark();
\r
420 function nonWhitespaceOrBookmarkEval( node )
\r
422 // Whitespaces and bookmark nodes are to be ignored.
\r
423 return !whitespaceEval( node ) && !bookmarkEval( node );
\r
426 CKEDITOR.dom.range.prototype =
\r
430 var clone = new CKEDITOR.dom.range( this.document );
\r
432 clone.startContainer = this.startContainer;
\r
433 clone.startOffset = this.startOffset;
\r
434 clone.endContainer = this.endContainer;
\r
435 clone.endOffset = this.endOffset;
\r
436 clone.collapsed = this.collapsed;
\r
441 collapse : function( toStart )
\r
445 this.endContainer = this.startContainer;
\r
446 this.endOffset = this.startOffset;
\r
450 this.startContainer = this.endContainer;
\r
451 this.startOffset = this.endOffset;
\r
454 this.collapsed = true;
\r
458 * The content nodes of the range are cloned and added to a document fragment, which is returned.
\r
459 * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting).
\r
461 cloneContents : function()
\r
463 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
465 if ( !this.collapsed )
\r
466 execContentsAction( this, 2, docFrag );
\r
472 * Deletes the content nodes of the range permanently from the DOM tree.
\r
473 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
475 deleteContents : function( mergeThen )
\r
477 if ( this.collapsed )
\r
480 execContentsAction( this, 0, null, mergeThen );
\r
484 * The content nodes of the range are cloned and added to a document fragment,
\r
485 * meanwhile they're removed permanently from the DOM tree.
\r
486 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
488 extractContents : function( mergeThen )
\r
490 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
492 if ( !this.collapsed )
\r
493 execContentsAction( this, 1, docFrag, mergeThen );
\r
499 * Creates a bookmark object, which can be later used to restore the
\r
500 * range by using the moveToBookmark function.
\r
501 * This is an "intrusive" way to create a bookmark. It includes <span> tags
\r
502 * in the range boundaries. The advantage of it is that it is possible to
\r
503 * handle DOM mutations when moving back to the bookmark.
\r
504 * Attention: the inclusion of nodes in the DOM is a design choice and
\r
505 * should not be changed as there are other points in the code that may be
\r
506 * using those nodes to perform operations. See GetBookmarkNode.
\r
507 * @param {Boolean} [serializable] Indicates that the bookmark nodes
\r
508 * must contain ids, which can be used to restore the range even
\r
509 * when these nodes suffer mutations (like a clonation or innerHTML
\r
511 * @returns {Object} And object representing a bookmark.
\r
513 createBookmark : function( serializable )
\r
515 var startNode, endNode;
\r
518 var collapsed = this.collapsed;
\r
520 startNode = this.document.createElement( 'span' );
\r
521 startNode.data( 'cke-bookmark', 1 );
\r
522 startNode.setStyle( 'display', 'none' );
\r
524 // For IE, it must have something inside, otherwise it may be
\r
525 // removed during DOM operations.
\r
526 startNode.setHtml( ' ' );
\r
528 if ( serializable )
\r
530 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
\r
531 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
\r
534 // If collapsed, the endNode will not be created.
\r
537 endNode = startNode.clone();
\r
538 endNode.setHtml( ' ' );
\r
540 if ( serializable )
\r
541 endNode.setAttribute( 'id', baseId + 'E' );
\r
543 clone = this.clone();
\r
545 clone.insertNode( endNode );
\r
548 clone = this.clone();
\r
549 clone.collapse( true );
\r
550 clone.insertNode( startNode );
\r
552 // Update the range position.
\r
555 this.setStartAfter( startNode );
\r
556 this.setEndBefore( endNode );
\r
559 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
\r
562 startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
\r
563 endNode : serializable ? baseId + 'E' : endNode,
\r
564 serializable : serializable,
\r
565 collapsed : collapsed
\r
570 * Creates a "non intrusive" and "mutation sensible" bookmark. This
\r
571 * kind of bookmark should be used only when the DOM is supposed to
\r
572 * remain stable after its creation.
\r
573 * @param {Boolean} [normalized] Indicates that the bookmark must
\r
574 * normalized. When normalized, the successive text nodes are
\r
575 * considered a single node. To sucessful load a normalized
\r
576 * bookmark, the DOM tree must be also normalized before calling
\r
578 * @returns {Object} An object representing the bookmark.
\r
580 createBookmark2 : function( normalized )
\r
582 var startContainer = this.startContainer,
\r
583 endContainer = this.endContainer;
\r
585 var startOffset = this.startOffset,
\r
586 endOffset = this.endOffset;
\r
588 var collapsed = this.collapsed;
\r
590 var child, previous;
\r
592 // If there is no range then get out of here.
\r
593 // It happens on initial load in Safari #962 and if the editor it's
\r
594 // hidden also in Firefox
\r
595 if ( !startContainer || !endContainer )
\r
596 return { start : 0, end : 0 };
\r
600 // Find out if the start is pointing to a text node that will
\r
602 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
\r
604 child = startContainer.getChild( startOffset );
\r
606 // In this case, move the start information to that text
\r
608 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
609 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
611 startContainer = child;
\r
615 // Get the normalized offset.
\r
616 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
617 startOffset = child.getIndex( 1 );
\r
620 // Normalize the start.
\r
621 while ( startContainer.type == CKEDITOR.NODE_TEXT
\r
622 && ( previous = startContainer.getPrevious() )
\r
623 && previous.type == CKEDITOR.NODE_TEXT )
\r
625 startContainer = previous;
\r
626 startOffset += previous.getLength();
\r
629 // Process the end only if not normalized.
\r
632 // Find out if the start is pointing to a text node that
\r
633 // will be normalized.
\r
634 if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
\r
636 child = endContainer.getChild( endOffset );
\r
638 // In this case, move the start information to that
\r
640 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
641 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
643 endContainer = child;
\r
647 // Get the normalized offset.
\r
648 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
649 endOffset = child.getIndex( 1 );
\r
652 // Normalize the end.
\r
653 while ( endContainer.type == CKEDITOR.NODE_TEXT
\r
654 && ( previous = endContainer.getPrevious() )
\r
655 && previous.type == CKEDITOR.NODE_TEXT )
\r
657 endContainer = previous;
\r
658 endOffset += previous.getLength();
\r
664 start : startContainer.getAddress( normalized ),
\r
665 end : collapsed ? null : endContainer.getAddress( normalized ),
\r
666 startOffset : startOffset,
\r
667 endOffset : endOffset,
\r
668 normalized : normalized,
\r
669 collapsed : collapsed,
\r
670 is2 : true // It's a createBookmark2 bookmark.
\r
674 moveToBookmark : function( bookmark )
\r
676 if ( bookmark.is2 ) // Created with createBookmark2().
\r
678 // Get the start information.
\r
679 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
\r
680 startOffset = bookmark.startOffset;
\r
682 // Get the end information.
\r
683 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
\r
684 endOffset = bookmark.endOffset;
\r
686 // Set the start boundary.
\r
687 this.setStart( startContainer, startOffset );
\r
689 // Set the end boundary. If not available, collapse it.
\r
690 if ( endContainer )
\r
691 this.setEnd( endContainer, endOffset );
\r
693 this.collapse( true );
\r
695 else // Created with createBookmark().
\r
697 var serializable = bookmark.serializable,
\r
698 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
\r
699 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
\r
701 // Set the range start at the bookmark start node position.
\r
702 this.setStartBefore( startNode );
\r
704 // Remove it, because it may interfere in the setEndBefore call.
\r
705 startNode.remove();
\r
707 // Set the range end at the bookmark end node position, or simply
\r
708 // collapse it if it is not available.
\r
711 this.setEndBefore( endNode );
\r
715 this.collapse( true );
\r
719 getBoundaryNodes : function()
\r
721 var startNode = this.startContainer,
\r
722 endNode = this.endContainer,
\r
723 startOffset = this.startOffset,
\r
724 endOffset = this.endOffset,
\r
727 if ( startNode.type == CKEDITOR.NODE_ELEMENT )
\r
729 childCount = startNode.getChildCount();
\r
730 if ( childCount > startOffset )
\r
731 startNode = startNode.getChild( startOffset );
\r
732 else if ( childCount < 1 )
\r
733 startNode = startNode.getPreviousSourceNode();
\r
734 else // startOffset > childCount but childCount is not 0
\r
736 // Try to take the node just after the current position.
\r
737 startNode = startNode.$;
\r
738 while ( startNode.lastChild )
\r
739 startNode = startNode.lastChild;
\r
740 startNode = new CKEDITOR.dom.node( startNode );
\r
742 // Normally we should take the next node in DFS order. But it
\r
743 // is also possible that we've already reached the end of
\r
745 startNode = startNode.getNextSourceNode() || startNode;
\r
748 if ( endNode.type == CKEDITOR.NODE_ELEMENT )
\r
750 childCount = endNode.getChildCount();
\r
751 if ( childCount > endOffset )
\r
752 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
\r
753 else if ( childCount < 1 )
\r
754 endNode = endNode.getPreviousSourceNode();
\r
755 else // endOffset > childCount but childCount is not 0
\r
757 // Try to take the node just before the current position.
\r
758 endNode = endNode.$;
\r
759 while ( endNode.lastChild )
\r
760 endNode = endNode.lastChild;
\r
761 endNode = new CKEDITOR.dom.node( endNode );
\r
765 // Sometimes the endNode will come right before startNode for collapsed
\r
766 // ranges. Fix it. (#3780)
\r
767 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
\r
768 startNode = endNode;
\r
770 return { startNode : startNode, endNode : endNode };
\r
774 * Find the node which fully contains the range.
\r
775 * @param includeSelf
\r
776 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.
\r
778 getCommonAncestor : function( includeSelf , ignoreTextNode )
\r
780 var start = this.startContainer,
\r
781 end = this.endContainer,
\r
784 if ( start.equals( end ) )
\r
787 && start.type == CKEDITOR.NODE_ELEMENT
\r
788 && this.startOffset == this.endOffset - 1 )
\r
789 ancestor = start.getChild( this.startOffset );
\r
794 ancestor = start.getCommonAncestor( end );
\r
796 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
\r
800 * Transforms the startContainer and endContainer properties from text
\r
801 * nodes to element nodes, whenever possible. This is actually possible
\r
802 * if either of the boundary containers point to a text node, and its
\r
803 * offset is set to zero, or after the last char in the node.
\r
805 optimize : function()
\r
807 var container = this.startContainer;
\r
808 var offset = this.startOffset;
\r
810 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
813 this.setStartBefore( container );
\r
814 else if ( offset >= container.getLength() )
\r
815 this.setStartAfter( container );
\r
818 container = this.endContainer;
\r
819 offset = this.endOffset;
\r
821 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
824 this.setEndBefore( container );
\r
825 else if ( offset >= container.getLength() )
\r
826 this.setEndAfter( container );
\r
831 * Move the range out of bookmark nodes if they'd been the container.
\r
833 optimizeBookmark: function()
\r
835 var startNode = this.startContainer,
\r
836 endNode = this.endContainer;
\r
838 if ( startNode.is && startNode.is( 'span' )
\r
839 && startNode.data( 'cke-bookmark' ) )
\r
840 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
\r
841 if ( endNode && endNode.is && endNode.is( 'span' )
\r
842 && endNode.data( 'cke-bookmark' ) )
\r
843 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
\r
846 trim : function( ignoreStart, ignoreEnd )
\r
848 var startContainer = this.startContainer,
\r
849 startOffset = this.startOffset,
\r
850 collapsed = this.collapsed;
\r
851 if ( ( !ignoreStart || collapsed )
\r
852 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
854 // If the offset is zero, we just insert the new node before
\r
856 if ( !startOffset )
\r
858 startOffset = startContainer.getIndex();
\r
859 startContainer = startContainer.getParent();
\r
861 // If the offset is at the end, we'll insert it after the text
\r
863 else if ( startOffset >= startContainer.getLength() )
\r
865 startOffset = startContainer.getIndex() + 1;
\r
866 startContainer = startContainer.getParent();
\r
868 // In other case, we split the text node and insert the new
\r
869 // node at the split point.
\r
872 var nextText = startContainer.split( startOffset );
\r
874 startOffset = startContainer.getIndex() + 1;
\r
875 startContainer = startContainer.getParent();
\r
877 // Check all necessity of updating the end boundary.
\r
878 if ( this.startContainer.equals( this.endContainer ) )
\r
879 this.setEnd( nextText, this.endOffset - this.startOffset );
\r
880 else if ( startContainer.equals( this.endContainer ) )
\r
881 this.endOffset += 1;
\r
884 this.setStart( startContainer, startOffset );
\r
888 this.collapse( true );
\r
893 var endContainer = this.endContainer;
\r
894 var endOffset = this.endOffset;
\r
896 if ( !( ignoreEnd || collapsed )
\r
897 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
899 // If the offset is zero, we just insert the new node before
\r
903 endOffset = endContainer.getIndex();
\r
904 endContainer = endContainer.getParent();
\r
906 // If the offset is at the end, we'll insert it after the text
\r
908 else if ( endOffset >= endContainer.getLength() )
\r
910 endOffset = endContainer.getIndex() + 1;
\r
911 endContainer = endContainer.getParent();
\r
913 // In other case, we split the text node and insert the new
\r
914 // node at the split point.
\r
917 endContainer.split( endOffset );
\r
919 endOffset = endContainer.getIndex() + 1;
\r
920 endContainer = endContainer.getParent();
\r
923 this.setEnd( endContainer, endOffset );
\r
928 * Expands the range so that partial units are completely contained.
\r
929 * @param unit {Number} The unit type to expand with.
\r
930 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
\r
932 enlarge : function( unit, excludeBrs )
\r
936 case CKEDITOR.ENLARGE_ELEMENT :
\r
938 if ( this.collapsed )
\r
941 // Get the common ancestor.
\r
942 var commonAncestor = this.getCommonAncestor();
\r
944 var body = this.document.getBody();
\r
946 // For each boundary
\r
947 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
\r
948 // 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
950 var startTop, endTop;
\r
952 var enlargeable, sibling, commonReached;
\r
954 // Indicates that the node can be added only if whitespace
\r
955 // is available before it.
\r
956 var needsWhiteSpace = false;
\r
960 // Process the start boundary.
\r
962 var container = this.startContainer;
\r
963 var offset = this.startOffset;
\r
965 if ( container.type == CKEDITOR.NODE_TEXT )
\r
969 // Check if there is any non-space text before the
\r
970 // offset. Otherwise, container is null.
\r
971 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
\r
973 // If we found only whitespace in the node, it
\r
974 // means that we'll need more whitespace to be able
\r
975 // to expand. For example, <i> can be expanded in
\r
976 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
977 needsWhiteSpace = !!container;
\r
982 if ( !( sibling = container.getPrevious() ) )
\r
983 enlargeable = container.getParent();
\r
988 // If we have offset, get the node preceeding it as the
\r
989 // first sibling to be checked.
\r
991 sibling = container.getChild( offset - 1 ) || container.getLast();
\r
993 // If there is no sibling, mark the container to be
\r
996 enlargeable = container;
\r
999 while ( enlargeable || sibling )
\r
1001 if ( enlargeable && !sibling )
\r
1003 // If we reached the common ancestor, mark the flag
\r
1005 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1006 commonReached = true;
\r
1008 if ( !body.contains( enlargeable ) )
\r
1011 // If we don't need space or this element breaks
\r
1012 // the line, then enlarge it.
\r
1013 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1015 needsWhiteSpace = false;
\r
1017 // If the common ancestor has been reached,
\r
1018 // we'll not enlarge it immediately, but just
\r
1019 // mark it to be enlarged later if the end
\r
1020 // boundary also enlarges it.
\r
1021 if ( commonReached )
\r
1022 startTop = enlargeable;
\r
1024 this.setStartBefore( enlargeable );
\r
1027 sibling = enlargeable.getPrevious();
\r
1030 // Check all sibling nodes preceeding the enlargeable
\r
1031 // node. The node wil lbe enlarged only if none of them
\r
1035 // This flag indicates that this node has
\r
1036 // whitespaces at the end.
\r
1037 isWhiteSpace = false;
\r
1039 if ( sibling.type == CKEDITOR.NODE_COMMENT )
\r
1041 sibling = sibling.getPrevious();
\r
1044 else if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1046 siblingText = sibling.getText();
\r
1048 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1051 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
\r
1055 // If this is a visible element.
\r
1056 // We need to check for the bookmark attribute because IE insists on
\r
1057 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1058 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1059 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1061 // We'll accept it only if we need
\r
1062 // whitespace, and this is an inline
\r
1063 // element with whitespace only.
\r
1064 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1066 // It must contains spaces and inline elements only.
\r
1068 siblingText = sibling.getText();
\r
1070 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
\r
1074 var allChildren = sibling.$.getElementsByTagName( '*' );
\r
1075 for ( var i = 0, child ; child = allChildren[ i++ ] ; )
\r
1077 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1086 isWhiteSpace = !!siblingText.length;
\r
1093 // A node with whitespaces has been found.
\r
1094 if ( isWhiteSpace )
\r
1096 // Enlarge the last enlargeable node, if we
\r
1097 // were waiting for spaces.
\r
1098 if ( needsWhiteSpace )
\r
1100 if ( commonReached )
\r
1101 startTop = enlargeable;
\r
1102 else if ( enlargeable )
\r
1103 this.setStartBefore( enlargeable );
\r
1106 needsWhiteSpace = true;
\r
1111 var next = sibling.getPrevious();
\r
1113 if ( !enlargeable && !next )
\r
1115 // Set the sibling as enlargeable, so it's
\r
1116 // parent will be get later outside this while.
\r
1117 enlargeable = sibling;
\r
1126 // If sibling has been set to null, then we
\r
1127 // need to stop enlarging.
\r
1128 enlargeable = null;
\r
1132 if ( enlargeable )
\r
1133 enlargeable = enlargeable.getParent();
\r
1136 // Process the end boundary. This is basically the same
\r
1137 // code used for the start boundary, with small changes to
\r
1138 // make it work in the oposite side (to the right). This
\r
1139 // makes it difficult to reuse the code here. So, fixes to
\r
1140 // the above code are likely to be replicated here.
\r
1142 container = this.endContainer;
\r
1143 offset = this.endOffset;
\r
1145 // Reset the common variables.
\r
1146 enlargeable = sibling = null;
\r
1147 commonReached = needsWhiteSpace = false;
\r
1149 if ( container.type == CKEDITOR.NODE_TEXT )
\r
1151 // Check if there is any non-space text after the
\r
1152 // offset. Otherwise, container is null.
\r
1153 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;
\r
1155 // If we found only whitespace in the node, it
\r
1156 // means that we'll need more whitespace to be able
\r
1157 // to expand. For example, <i> can be expanded in
\r
1158 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
1159 needsWhiteSpace = !( container && container.getLength() );
\r
1163 if ( !( sibling = container.getNext() ) )
\r
1164 enlargeable = container.getParent();
\r
1169 // Get the node right after the boudary to be checked
\r
1171 sibling = container.getChild( offset );
\r
1174 enlargeable = container;
\r
1177 while ( enlargeable || sibling )
\r
1179 if ( enlargeable && !sibling )
\r
1181 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1182 commonReached = true;
\r
1184 if ( !body.contains( enlargeable ) )
\r
1187 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1189 needsWhiteSpace = false;
\r
1191 if ( commonReached )
\r
1192 endTop = enlargeable;
\r
1193 else if ( enlargeable )
\r
1194 this.setEndAfter( enlargeable );
\r
1197 sibling = enlargeable.getNext();
\r
1202 isWhiteSpace = false;
\r
1204 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1206 siblingText = sibling.getText();
\r
1208 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1211 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
\r
1213 else if ( sibling.type == CKEDITOR.NODE_ELEMENT )
\r
1215 // If this is a visible element.
\r
1216 // We need to check for the bookmark attribute because IE insists on
\r
1217 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1218 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1219 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1221 // We'll accept it only if we need
\r
1222 // whitespace, and this is an inline
\r
1223 // element with whitespace only.
\r
1224 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1226 // It must contains spaces and inline elements only.
\r
1228 siblingText = sibling.getText();
\r
1230 if ( (/[^\s\ufeff]/).test( siblingText ) )
\r
1234 allChildren = sibling.$.getElementsByTagName( '*' );
\r
1235 for ( i = 0 ; child = allChildren[ i++ ] ; )
\r
1237 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1246 isWhiteSpace = !!siblingText.length;
\r
1255 if ( isWhiteSpace )
\r
1257 if ( needsWhiteSpace )
\r
1259 if ( commonReached )
\r
1260 endTop = enlargeable;
\r
1262 this.setEndAfter( enlargeable );
\r
1268 next = sibling.getNext();
\r
1270 if ( !enlargeable && !next )
\r
1272 enlargeable = sibling;
\r
1281 // If sibling has been set to null, then we
\r
1282 // need to stop enlarging.
\r
1283 enlargeable = null;
\r
1287 if ( enlargeable )
\r
1288 enlargeable = enlargeable.getParent();
\r
1291 // If the common ancestor can be enlarged by both boundaries, then include it also.
\r
1292 if ( startTop && endTop )
\r
1294 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
\r
1296 this.setStartBefore( commonAncestor );
\r
1297 this.setEndAfter( commonAncestor );
\r
1301 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
\r
1302 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
\r
1304 // Enlarging the start boundary.
\r
1305 var walkerRange = new CKEDITOR.dom.range( this.document );
\r
1307 body = this.document.getBody();
\r
1309 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
1310 walkerRange.setEnd( this.startContainer, this.startOffset );
\r
1312 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1313 blockBoundary, // The node on which the enlarging should stop.
\r
1314 tailBr, // In case BR as block boundary.
\r
1315 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary(
\r
1316 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),
\r
1317 // Record the encountered 'blockBoundary' for later use.
\r
1318 boundaryGuard = function( node )
\r
1320 var retval = notBlockBoundary( node );
\r
1322 blockBoundary = node;
\r
1325 // Record the encounted 'tailBr' for later use.
\r
1326 tailBrGuard = function( node )
\r
1328 var retval = boundaryGuard( node );
\r
1329 if ( !retval && node.is && node.is( 'br' ) )
\r
1334 walker.guard = boundaryGuard;
\r
1336 enlargeable = walker.lastBackward();
\r
1338 // It's the body which stop the enlarging if no block boundary found.
\r
1339 blockBoundary = blockBoundary || body;
\r
1341 // Start the range either after the end of found block (<p>...</p>[text)
\r
1342 // or at the start of block (<p>[text...), by comparing the document position
\r
1343 // with 'enlargeable' node.
\r
1346 !blockBoundary.is( 'br' ) &&
\r
1347 ( !enlargeable && this.checkStartOfBlock()
\r
1348 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1349 CKEDITOR.POSITION_AFTER_START :
\r
1350 CKEDITOR.POSITION_AFTER_END );
\r
1352 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
\r
1353 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )
\r
1355 var theRange = this.clone();
\r
1356 walker = new CKEDITOR.dom.walker( theRange );
\r
1358 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
\r
1359 bookmark = CKEDITOR.dom.walker.bookmark();
\r
1361 walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };
\r
1362 var previous = walker.previous();
\r
1363 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
\r
1368 // Enlarging the end boundary.
\r
1369 walkerRange = this.clone();
\r
1370 walkerRange.collapse();
\r
1371 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
1372 walker = new CKEDITOR.dom.walker( walkerRange );
\r
1374 // tailBrGuard only used for on range end.
\r
1375 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?
\r
1376 tailBrGuard : boundaryGuard;
\r
1377 blockBoundary = null;
\r
1378 // End the range right before the block boundary node.
\r
1380 enlargeable = walker.lastForward();
\r
1382 // It's the body which stop the enlarging if no block boundary found.
\r
1383 blockBoundary = blockBoundary || body;
\r
1385 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
\r
1386 // by comparing the document position with 'enlargeable' node.
\r
1389 ( !enlargeable && this.checkEndOfBlock()
\r
1390 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1391 CKEDITOR.POSITION_BEFORE_END :
\r
1392 CKEDITOR.POSITION_BEFORE_START );
\r
1393 // We must include the <br> at the end of range if there's
\r
1394 // one and we're expanding list item contents
\r
1396 this.setEndAfter( tailBr );
\r
1401 * Descrease the range to make sure that boundaries
\r
1402 * always anchor beside text nodes or innermost element.
\r
1403 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.
\r
1405 * <dt>CKEDITOR.SHRINK_ELEMENT</dt>
\r
1406 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>
\r
1407 * <dt>CKEDITOR.SHRINK_TEXT</dt>
\r
1408 * <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
1410 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
\r
1412 shrink : function( mode, selectContents )
\r
1414 // Unable to shrink a collapsed range.
\r
1415 if ( !this.collapsed )
\r
1417 mode = mode || CKEDITOR.SHRINK_TEXT;
\r
1419 var walkerRange = this.clone();
\r
1421 var startContainer = this.startContainer,
\r
1422 endContainer = this.endContainer,
\r
1423 startOffset = this.startOffset,
\r
1424 endOffset = this.endOffset,
\r
1425 collapsed = this.collapsed;
\r
1427 // Whether the start/end boundary is moveable.
\r
1428 var moveStart = 1,
\r
1431 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1433 if ( !startOffset )
\r
1434 walkerRange.setStartBefore( startContainer );
\r
1435 else if ( startOffset >= startContainer.getLength( ) )
\r
1436 walkerRange.setStartAfter( startContainer );
\r
1439 // Enlarge the range properly to avoid walker making
\r
1440 // DOM changes caused by triming the text nodes later.
\r
1441 walkerRange.setStartBefore( startContainer );
\r
1446 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
1449 walkerRange.setEndBefore( endContainer );
\r
1450 else if ( endOffset >= endContainer.getLength( ) )
\r
1451 walkerRange.setEndAfter( endContainer );
\r
1454 walkerRange.setEndAfter( endContainer );
\r
1459 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1460 isBookmark = CKEDITOR.dom.walker.bookmark();
\r
1462 walker.evaluator = function( node )
\r
1464 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
\r
1465 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
\r
1468 var currentElement;
\r
1469 walker.guard = function( node, movingOut )
\r
1471 if ( isBookmark( node ) )
\r
1474 // Stop when we're shrink in element mode while encountering a text node.
\r
1475 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
\r
1478 // Stop when we've already walked "through" an element.
\r
1479 if ( movingOut && node.equals( currentElement ) )
\r
1482 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
\r
1483 currentElement = node;
\r
1490 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
\r
1491 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
\r
1497 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
\r
1498 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
\r
1501 return !!( moveStart || moveEnd );
\r
1506 * Inserts a node at the start of the range. The range will be expanded
\r
1507 * the contain the node.
\r
1509 insertNode : function( node )
\r
1511 this.optimizeBookmark();
\r
1512 this.trim( false, true );
\r
1514 var startContainer = this.startContainer;
\r
1515 var startOffset = this.startOffset;
\r
1517 var nextNode = startContainer.getChild( startOffset );
\r
1520 node.insertBefore( nextNode );
\r
1522 startContainer.append( node );
\r
1524 // Check if we need to update the end boundary.
\r
1525 if ( node.getParent().equals( this.endContainer ) )
\r
1528 // Expand the range to embrace the new node.
\r
1529 this.setStartBefore( node );
\r
1532 moveToPosition : function( node, position )
\r
1534 this.setStartAt( node, position );
\r
1535 this.collapse( true );
\r
1538 selectNodeContents : function( node )
\r
1540 this.setStart( node, 0 );
\r
1541 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
\r
1545 * Sets the start position of a Range.
\r
1546 * @param {CKEDITOR.dom.node} startNode The node to start the range.
\r
1547 * @param {Number} startOffset An integer greater than or equal to zero
\r
1548 * representing the offset for the start of the range from the start
\r
1551 setStart : function( startNode, startOffset )
\r
1553 // W3C requires a check for the new position. If it is after the end
\r
1554 // boundary, the range should be collapsed to the new start. It seams
\r
1555 // we will not need this check for our use of this class so we can
\r
1556 // ignore it for now.
\r
1558 // Fixing invalid range start inside dtd empty elements.
\r
1559 if( startNode.type == CKEDITOR.NODE_ELEMENT
\r
1560 && CKEDITOR.dtd.$empty[ startNode.getName() ] )
\r
1561 startOffset = startNode.getIndex(), startNode = startNode.getParent();
\r
1563 this.startContainer = startNode;
\r
1564 this.startOffset = startOffset;
\r
1566 if ( !this.endContainer )
\r
1568 this.endContainer = startNode;
\r
1569 this.endOffset = startOffset;
\r
1572 updateCollapsed( this );
\r
1576 * Sets the end position of a Range.
\r
1577 * @param {CKEDITOR.dom.node} endNode The node to end the range.
\r
1578 * @param {Number} endOffset An integer greater than or equal to zero
\r
1579 * representing the offset for the end of the range from the start
\r
1582 setEnd : function( endNode, endOffset )
\r
1584 // W3C requires a check for the new position. If it is before the start
\r
1585 // boundary, the range should be collapsed to the new end. It seams we
\r
1586 // will not need this check for our use of this class so we can ignore
\r
1589 // Fixing invalid range end inside dtd empty elements.
\r
1590 if( endNode.type == CKEDITOR.NODE_ELEMENT
\r
1591 && CKEDITOR.dtd.$empty[ endNode.getName() ] )
\r
1592 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
\r
1594 this.endContainer = endNode;
\r
1595 this.endOffset = endOffset;
\r
1597 if ( !this.startContainer )
\r
1599 this.startContainer = endNode;
\r
1600 this.startOffset = endOffset;
\r
1603 updateCollapsed( this );
\r
1606 setStartAfter : function( node )
\r
1608 this.setStart( node.getParent(), node.getIndex() + 1 );
\r
1611 setStartBefore : function( node )
\r
1613 this.setStart( node.getParent(), node.getIndex() );
\r
1616 setEndAfter : function( node )
\r
1618 this.setEnd( node.getParent(), node.getIndex() + 1 );
\r
1621 setEndBefore : function( node )
\r
1623 this.setEnd( node.getParent(), node.getIndex() );
\r
1626 setStartAt : function( node, position )
\r
1628 switch( position )
\r
1630 case CKEDITOR.POSITION_AFTER_START :
\r
1631 this.setStart( node, 0 );
\r
1634 case CKEDITOR.POSITION_BEFORE_END :
\r
1635 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1636 this.setStart( node, node.getLength() );
\r
1638 this.setStart( node, node.getChildCount() );
\r
1641 case CKEDITOR.POSITION_BEFORE_START :
\r
1642 this.setStartBefore( node );
\r
1645 case CKEDITOR.POSITION_AFTER_END :
\r
1646 this.setStartAfter( node );
\r
1649 updateCollapsed( this );
\r
1652 setEndAt : function( node, position )
\r
1654 switch( position )
\r
1656 case CKEDITOR.POSITION_AFTER_START :
\r
1657 this.setEnd( node, 0 );
\r
1660 case CKEDITOR.POSITION_BEFORE_END :
\r
1661 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1662 this.setEnd( node, node.getLength() );
\r
1664 this.setEnd( node, node.getChildCount() );
\r
1667 case CKEDITOR.POSITION_BEFORE_START :
\r
1668 this.setEndBefore( node );
\r
1671 case CKEDITOR.POSITION_AFTER_END :
\r
1672 this.setEndAfter( node );
\r
1675 updateCollapsed( this );
\r
1678 fixBlock : function( isStart, blockTag )
\r
1680 var bookmark = this.createBookmark(),
\r
1681 fixedBlock = this.document.createElement( blockTag );
\r
1683 this.collapse( isStart );
\r
1685 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
\r
1687 this.extractContents().appendTo( fixedBlock );
\r
1688 fixedBlock.trim();
\r
1690 if ( !CKEDITOR.env.ie )
\r
1691 fixedBlock.appendBogus();
\r
1693 this.insertNode( fixedBlock );
\r
1695 this.moveToBookmark( bookmark );
\r
1697 return fixedBlock;
\r
1700 splitBlock : function( blockTag )
\r
1702 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
\r
1703 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1705 var startBlockLimit = startPath.blockLimit,
\r
1706 endBlockLimit = endPath.blockLimit;
\r
1708 var startBlock = startPath.block,
\r
1709 endBlock = endPath.block;
\r
1711 var elementPath = null;
\r
1712 // Do nothing if the boundaries are in different block limits.
\r
1713 if ( !startBlockLimit.equals( endBlockLimit ) )
\r
1716 // Get or fix current blocks.
\r
1717 if ( blockTag != 'br' )
\r
1719 if ( !startBlock )
\r
1721 startBlock = this.fixBlock( true, blockTag );
\r
1722 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
\r
1726 endBlock = this.fixBlock( false, blockTag );
\r
1729 // Get the range position.
\r
1730 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
\r
1731 isEndOfBlock = endBlock && this.checkEndOfBlock();
\r
1733 // Delete the current contents.
\r
1734 // TODO: Why is 2.x doing CheckIsEmpty()?
\r
1735 this.deleteContents();
\r
1737 if ( startBlock && startBlock.equals( endBlock ) )
\r
1739 if ( isEndOfBlock )
\r
1741 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1742 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
\r
1745 else if ( isStartOfBlock )
\r
1747 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1748 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
\r
1749 startBlock = null;
\r
1753 endBlock = this.splitElement( startBlock );
\r
1755 // In Gecko, the last child node must be a bogus <br>.
\r
1756 // Note: bogus <br> added under <ul> or <ol> would cause
\r
1757 // lists to be incorrectly rendered.
\r
1758 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )
\r
1759 startBlock.appendBogus() ;
\r
1764 previousBlock : startBlock,
\r
1765 nextBlock : endBlock,
\r
1766 wasStartOfBlock : isStartOfBlock,
\r
1767 wasEndOfBlock : isEndOfBlock,
\r
1768 elementPath : elementPath
\r
1773 * Branch the specified element from the collapsed range position and
\r
1774 * place the caret between the two result branches.
\r
1775 * Note: The range must be collapsed and been enclosed by this element.
\r
1776 * @param {CKEDITOR.dom.element} element
\r
1777 * @return {CKEDITOR.dom.element} Root element of the new branch after the split.
\r
1779 splitElement : function( toSplit )
\r
1781 if ( !this.collapsed )
\r
1784 // Extract the contents of the block from the selection point to the end
\r
1785 // of its contents.
\r
1786 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
\r
1787 var documentFragment = this.extractContents();
\r
1789 // Duplicate the element after it.
\r
1790 var clone = toSplit.clone( false );
\r
1792 // Place the extracted contents into the duplicated element.
\r
1793 documentFragment.appendTo( clone );
\r
1794 clone.insertAfter( toSplit );
\r
1795 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
\r
1800 * Check whether a range boundary is at the inner boundary of a given
\r
1802 * @param {CKEDITOR.dom.element} element The target element to check.
\r
1803 * @param {Number} checkType The boundary to check for both the range
\r
1804 * and the element. It can be CKEDITOR.START or CKEDITOR.END.
\r
1805 * @returns {Boolean} "true" if the range boundary is at the inner
\r
1806 * boundary of the element.
\r
1808 checkBoundaryOfElement : function( element, checkType )
\r
1810 var checkStart = ( checkType == CKEDITOR.START );
\r
1812 // Create a copy of this range, so we can manipulate it for our checks.
\r
1813 var walkerRange = this.clone();
\r
1815 // Collapse the range at the proper size.
\r
1816 walkerRange.collapse( checkStart );
\r
1818 // Expand the range to element boundary.
\r
1819 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
\r
1820 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
\r
1822 // Create the walker, which will check if we have anything useful
\r
1824 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1825 walker.evaluator = elementBoundaryEval( checkStart );
\r
1827 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
\r
1830 // Calls to this function may produce changes to the DOM. The range may
\r
1831 // be updated to reflect such changes.
\r
1832 checkStartOfBlock : function()
\r
1834 var startContainer = this.startContainer,
\r
1835 startOffset = this.startOffset;
\r
1837 // If the starting node is a text node, and non-empty before the offset,
\r
1838 // then we're surely not at the start of block.
\r
1839 if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1841 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
\r
1842 if ( textBefore.length )
\r
1846 // We need to grab the block element holding the start boundary, so
\r
1847 // let's use an element path for it.
\r
1848 var path = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1850 // Creates a range starting at the block start until the range start.
\r
1851 var walkerRange = this.clone();
\r
1852 walkerRange.collapse( true );
\r
1853 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
\r
1855 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1856 walker.evaluator = getCheckStartEndBlockEvalFunction( true );
\r
1858 return walker.checkBackward();
\r
1861 checkEndOfBlock : function()
\r
1863 var endContainer = this.endContainer,
\r
1864 endOffset = this.endOffset;
\r
1866 // If the ending node is a text node, and non-empty after the offset,
\r
1867 // then we're surely not at the end of block.
\r
1868 if ( endContainer.type == CKEDITOR.NODE_TEXT )
\r
1870 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
\r
1871 if ( textAfter.length )
\r
1875 // We need to grab the block element holding the start boundary, so
\r
1876 // let's use an element path for it.
\r
1877 var path = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1879 // Creates a range starting at the block start until the range start.
\r
1880 var walkerRange = this.clone();
\r
1881 walkerRange.collapse( false );
\r
1882 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
\r
1884 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1885 walker.evaluator = getCheckStartEndBlockEvalFunction( false );
\r
1887 return walker.checkForward();
\r
1891 * Check if elements at which the range boundaries anchor are read-only,
\r
1892 * with respect to "contenteditable" attribute.
\r
1894 checkReadOnly : ( function()
\r
1896 function checkNodesEditable( node, anotherEnd )
\r
1900 if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
1902 if ( node.getAttribute( 'contentEditable' ) == 'false'
\r
1903 && !node.data( 'cke-editable' ) )
\r
1907 // Range enclosed entirely in an editable element.
\r
1908 else if ( node.is( 'html' )
\r
1909 || node.getAttribute( 'contentEditable' ) == 'true'
\r
1910 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
\r
1915 node = node.getParent();
\r
1923 var startNode = this.startContainer,
\r
1924 endNode = this.endContainer;
\r
1926 // Check if elements path at both boundaries are editable.
\r
1927 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
\r
1932 * Moves the range boundaries to the first/end editing point inside an
\r
1933 * element. For example, in an element tree like
\r
1934 * "<p><b><i></i></b> Text</p>", the start editing point is
\r
1935 * "<p><b><i>^</i></b> Text</p>" (inside <i>).
\r
1936 * @param {CKEDITOR.dom.element} el The element into which look for the
\r
1938 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
\r
1940 moveToElementEditablePosition : function( el, isMoveToEnd )
\r
1942 var nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/;
\r
1944 function nextDFS( node, childOnly )
\r
1948 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
\r
1949 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
\r
1951 if ( !childOnly && !next )
\r
1952 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
\r
1957 // Handle non-editable element e.g. HR.
\r
1958 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) )
\r
1960 this.moveToPosition( el, isMoveToEnd ?
\r
1961 CKEDITOR.POSITION_AFTER_END :
\r
1962 CKEDITOR.POSITION_BEFORE_START );
\r
1970 // Stop immediately if we've found a text node.
\r
1971 if ( el.type == CKEDITOR.NODE_TEXT )
\r
1973 // Put cursor before block filler.
\r
1974 if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
\r
1975 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
\r
1977 this.moveToPosition( el, isMoveToEnd ?
\r
1978 CKEDITOR.POSITION_AFTER_END :
\r
1979 CKEDITOR.POSITION_BEFORE_START );
\r
1984 // If an editable element is found, move inside it, but not stop the searching.
\r
1985 if ( el.type == CKEDITOR.NODE_ELEMENT )
\r
1987 if ( el.isEditable() )
\r
1989 this.moveToPosition( el, isMoveToEnd ?
\r
1990 CKEDITOR.POSITION_BEFORE_END :
\r
1991 CKEDITOR.POSITION_AFTER_START );
\r
1994 // Put cursor before padding block br.
\r
1995 else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() )
\r
1996 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
\r
1999 el = nextDFS( el, found );
\r
2006 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
2008 moveToElementEditStart : function( target )
\r
2010 return this.moveToElementEditablePosition( target );
\r
2014 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
2016 moveToElementEditEnd : function( target )
\r
2018 return this.moveToElementEditablePosition( target, true );
\r
2022 * Get the single node enclosed within the range if there's one.
\r
2024 getEnclosedNode : function()
\r
2026 var walkerRange = this.clone();
\r
2028 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
\r
2029 walkerRange.optimize();
\r
2030 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT
\r
2031 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
\r
2034 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
2035 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),
\r
2036 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
\r
2037 evaluator = function( node )
\r
2039 return isNotWhitespaces( node ) && isNotBookmarks( node );
\r
2041 walkerRange.evaluator = evaluator;
\r
2042 var node = walker.next();
\r
2044 return node && node.equals( walker.previous() ) ? node : null;
\r
2047 getTouchedStartNode : function()
\r
2049 var container = this.startContainer ;
\r
2051 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2052 return container ;
\r
2054 return container.getChild( this.startOffset ) || container ;
\r
2057 getTouchedEndNode : function()
\r
2059 var container = this.endContainer ;
\r
2061 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2062 return container ;
\r
2064 return container.getChild( this.endOffset - 1 ) || container ;
\r
2069 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text"
\r
2070 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^"
\r
2071 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text"
\r
2072 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text"
\r
2074 CKEDITOR.ENLARGE_ELEMENT = 1;
\r
2075 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
\r
2076 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
\r
2078 // Check boundary types.
\r
2079 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
\r
2080 CKEDITOR.START = 1;
\r
2082 CKEDITOR.STARTEND = 3;
\r
2084 // Shrink range types.
\r
2085 // @see CKEDITOR.dom.range.prototype.shrink
\r
2086 CKEDITOR.SHRINK_ELEMENT = 1;
\r
2087 CKEDITOR.SHRINK_TEXT = 2;
\r