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 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
384 var isBogus = CKEDITOR.dom.walker.bogus();
\r
385 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
\r
386 // text node and non-empty elements unless it's being bookmark text.
\r
387 function elementBoundaryEval( checkStart )
\r
389 return function( node )
\r
391 // Tolerant bogus br when checking at the end of block.
\r
392 // Reject any text node unless it's being bookmark
\r
394 // Reject any element unless it's being invisible empty. (#3883)
\r
395 return !checkStart && isBogus( node ) ||
\r
396 ( node.type == CKEDITOR.NODE_TEXT ?
\r
397 !CKEDITOR.tools.trim( node.getText() ) || !!node.getParent().data( 'cke-bookmark' )
\r
398 : node.getName() in CKEDITOR.dtd.$removeEmpty );
\r
402 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
\r
403 bookmarkEval = new CKEDITOR.dom.walker.bookmark();
\r
405 function nonWhitespaceOrBookmarkEval( node )
\r
407 // Whitespaces and bookmark nodes are to be ignored.
\r
408 return !whitespaceEval( node ) && !bookmarkEval( node );
\r
411 CKEDITOR.dom.range.prototype =
\r
415 var clone = new CKEDITOR.dom.range( this.document );
\r
417 clone.startContainer = this.startContainer;
\r
418 clone.startOffset = this.startOffset;
\r
419 clone.endContainer = this.endContainer;
\r
420 clone.endOffset = this.endOffset;
\r
421 clone.collapsed = this.collapsed;
\r
426 collapse : function( toStart )
\r
430 this.endContainer = this.startContainer;
\r
431 this.endOffset = this.startOffset;
\r
435 this.startContainer = this.endContainer;
\r
436 this.startOffset = this.endOffset;
\r
439 this.collapsed = true;
\r
443 * The content nodes of the range are cloned and added to a document fragment, which is returned.
\r
444 * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting).
\r
446 cloneContents : function()
\r
448 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
450 if ( !this.collapsed )
\r
451 execContentsAction( this, 2, docFrag );
\r
457 * Deletes the content nodes of the range permanently from the DOM tree.
\r
458 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
460 deleteContents : function( mergeThen )
\r
462 if ( this.collapsed )
\r
465 execContentsAction( this, 0, null, mergeThen );
\r
469 * The content nodes of the range are cloned and added to a document fragment,
\r
470 * meanwhile they're removed permanently from the DOM tree.
\r
471 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
473 extractContents : function( mergeThen )
\r
475 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
477 if ( !this.collapsed )
\r
478 execContentsAction( this, 1, docFrag, mergeThen );
\r
484 * Creates a bookmark object, which can be later used to restore the
\r
485 * range by using the moveToBookmark function.
\r
486 * This is an "intrusive" way to create a bookmark. It includes <span> tags
\r
487 * in the range boundaries. The advantage of it is that it is possible to
\r
488 * handle DOM mutations when moving back to the bookmark.
\r
489 * Attention: the inclusion of nodes in the DOM is a design choice and
\r
490 * should not be changed as there are other points in the code that may be
\r
491 * using those nodes to perform operations. See GetBookmarkNode.
\r
492 * @param {Boolean} [serializable] Indicates that the bookmark nodes
\r
493 * must contain ids, which can be used to restore the range even
\r
494 * when these nodes suffer mutations (like a clonation or innerHTML
\r
496 * @returns {Object} And object representing a bookmark.
\r
498 createBookmark : function( serializable )
\r
500 var startNode, endNode;
\r
503 var collapsed = this.collapsed;
\r
505 startNode = this.document.createElement( 'span' );
\r
506 startNode.data( 'cke-bookmark', 1 );
\r
507 startNode.setStyle( 'display', 'none' );
\r
509 // For IE, it must have something inside, otherwise it may be
\r
510 // removed during DOM operations.
\r
511 startNode.setHtml( ' ' );
\r
513 if ( serializable )
\r
515 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
\r
516 startNode.setAttribute( 'id', baseId + 'S' );
\r
519 // If collapsed, the endNode will not be created.
\r
522 endNode = startNode.clone();
\r
523 endNode.setHtml( ' ' );
\r
525 if ( serializable )
\r
526 endNode.setAttribute( 'id', baseId + 'E' );
\r
528 clone = this.clone();
\r
530 clone.insertNode( endNode );
\r
533 clone = this.clone();
\r
534 clone.collapse( true );
\r
535 clone.insertNode( startNode );
\r
537 // Update the range position.
\r
540 this.setStartAfter( startNode );
\r
541 this.setEndBefore( endNode );
\r
544 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
\r
547 startNode : serializable ? baseId + 'S' : startNode,
\r
548 endNode : serializable ? baseId + 'E' : endNode,
\r
549 serializable : serializable,
\r
550 collapsed : collapsed
\r
555 * Creates a "non intrusive" and "mutation sensible" bookmark. This
\r
556 * kind of bookmark should be used only when the DOM is supposed to
\r
557 * remain stable after its creation.
\r
558 * @param {Boolean} [normalized] Indicates that the bookmark must
\r
559 * normalized. When normalized, the successive text nodes are
\r
560 * considered a single node. To sucessful load a normalized
\r
561 * bookmark, the DOM tree must be also normalized before calling
\r
563 * @returns {Object} An object representing the bookmark.
\r
565 createBookmark2 : function( normalized )
\r
567 var startContainer = this.startContainer,
\r
568 endContainer = this.endContainer;
\r
570 var startOffset = this.startOffset,
\r
571 endOffset = this.endOffset;
\r
573 var collapsed = this.collapsed;
\r
575 var child, previous;
\r
577 // If there is no range then get out of here.
\r
578 // It happens on initial load in Safari #962 and if the editor it's
\r
579 // hidden also in Firefox
\r
580 if ( !startContainer || !endContainer )
\r
581 return { start : 0, end : 0 };
\r
585 // Find out if the start is pointing to a text node that will
\r
587 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
\r
589 child = startContainer.getChild( startOffset );
\r
591 // In this case, move the start information to that text
\r
593 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
594 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
596 startContainer = child;
\r
600 // Get the normalized offset.
\r
601 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
602 startOffset = child.getIndex( 1 );
\r
605 // Normalize the start.
\r
606 while ( startContainer.type == CKEDITOR.NODE_TEXT
\r
607 && ( previous = startContainer.getPrevious() )
\r
608 && previous.type == CKEDITOR.NODE_TEXT )
\r
610 startContainer = previous;
\r
611 startOffset += previous.getLength();
\r
614 // Process the end only if not normalized.
\r
617 // Find out if the start is pointing to a text node that
\r
618 // will be normalized.
\r
619 if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
\r
621 child = endContainer.getChild( endOffset );
\r
623 // In this case, move the start information to that
\r
625 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
626 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
628 endContainer = child;
\r
632 // Get the normalized offset.
\r
633 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
634 endOffset = child.getIndex( 1 );
\r
637 // Normalize the end.
\r
638 while ( endContainer.type == CKEDITOR.NODE_TEXT
\r
639 && ( previous = endContainer.getPrevious() )
\r
640 && previous.type == CKEDITOR.NODE_TEXT )
\r
642 endContainer = previous;
\r
643 endOffset += previous.getLength();
\r
649 start : startContainer.getAddress( normalized ),
\r
650 end : collapsed ? null : endContainer.getAddress( normalized ),
\r
651 startOffset : startOffset,
\r
652 endOffset : endOffset,
\r
653 normalized : normalized,
\r
654 collapsed : collapsed,
\r
655 is2 : true // It's a createBookmark2 bookmark.
\r
659 moveToBookmark : function( bookmark )
\r
661 if ( bookmark.is2 ) // Created with createBookmark2().
\r
663 // Get the start information.
\r
664 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
\r
665 startOffset = bookmark.startOffset;
\r
667 // Get the end information.
\r
668 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
\r
669 endOffset = bookmark.endOffset;
\r
671 // Set the start boundary.
\r
672 this.setStart( startContainer, startOffset );
\r
674 // Set the end boundary. If not available, collapse it.
\r
675 if ( endContainer )
\r
676 this.setEnd( endContainer, endOffset );
\r
678 this.collapse( true );
\r
680 else // Created with createBookmark().
\r
682 var serializable = bookmark.serializable,
\r
683 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
\r
684 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
\r
686 // Set the range start at the bookmark start node position.
\r
687 this.setStartBefore( startNode );
\r
689 // Remove it, because it may interfere in the setEndBefore call.
\r
690 startNode.remove();
\r
692 // Set the range end at the bookmark end node position, or simply
\r
693 // collapse it if it is not available.
\r
696 this.setEndBefore( endNode );
\r
700 this.collapse( true );
\r
704 getBoundaryNodes : function()
\r
706 var startNode = this.startContainer,
\r
707 endNode = this.endContainer,
\r
708 startOffset = this.startOffset,
\r
709 endOffset = this.endOffset,
\r
712 if ( startNode.type == CKEDITOR.NODE_ELEMENT )
\r
714 childCount = startNode.getChildCount();
\r
715 if ( childCount > startOffset )
\r
716 startNode = startNode.getChild( startOffset );
\r
717 else if ( childCount < 1 )
\r
718 startNode = startNode.getPreviousSourceNode();
\r
719 else // startOffset > childCount but childCount is not 0
\r
721 // Try to take the node just after the current position.
\r
722 startNode = startNode.$;
\r
723 while ( startNode.lastChild )
\r
724 startNode = startNode.lastChild;
\r
725 startNode = new CKEDITOR.dom.node( startNode );
\r
727 // Normally we should take the next node in DFS order. But it
\r
728 // is also possible that we've already reached the end of
\r
730 startNode = startNode.getNextSourceNode() || startNode;
\r
733 if ( endNode.type == CKEDITOR.NODE_ELEMENT )
\r
735 childCount = endNode.getChildCount();
\r
736 if ( childCount > endOffset )
\r
737 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
\r
738 else if ( childCount < 1 )
\r
739 endNode = endNode.getPreviousSourceNode();
\r
740 else // endOffset > childCount but childCount is not 0
\r
742 // Try to take the node just before the current position.
\r
743 endNode = endNode.$;
\r
744 while ( endNode.lastChild )
\r
745 endNode = endNode.lastChild;
\r
746 endNode = new CKEDITOR.dom.node( endNode );
\r
750 // Sometimes the endNode will come right before startNode for collapsed
\r
751 // ranges. Fix it. (#3780)
\r
752 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
\r
753 startNode = endNode;
\r
755 return { startNode : startNode, endNode : endNode };
\r
759 * Find the node which fully contains the range.
\r
760 * @param includeSelf
\r
761 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.
\r
763 getCommonAncestor : function( includeSelf , ignoreTextNode )
\r
765 var start = this.startContainer,
\r
766 end = this.endContainer,
\r
769 if ( start.equals( end ) )
\r
772 && start.type == CKEDITOR.NODE_ELEMENT
\r
773 && this.startOffset == this.endOffset - 1 )
\r
774 ancestor = start.getChild( this.startOffset );
\r
779 ancestor = start.getCommonAncestor( end );
\r
781 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
\r
785 * Transforms the startContainer and endContainer properties from text
\r
786 * nodes to element nodes, whenever possible. This is actually possible
\r
787 * if either of the boundary containers point to a text node, and its
\r
788 * offset is set to zero, or after the last char in the node.
\r
790 optimize : function()
\r
792 var container = this.startContainer;
\r
793 var offset = this.startOffset;
\r
795 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
798 this.setStartBefore( container );
\r
799 else if ( offset >= container.getLength() )
\r
800 this.setStartAfter( container );
\r
803 container = this.endContainer;
\r
804 offset = this.endOffset;
\r
806 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
809 this.setEndBefore( container );
\r
810 else if ( offset >= container.getLength() )
\r
811 this.setEndAfter( container );
\r
816 * Move the range out of bookmark nodes if they'd been the container.
\r
818 optimizeBookmark: function()
\r
820 var startNode = this.startContainer,
\r
821 endNode = this.endContainer;
\r
823 if ( startNode.is && startNode.is( 'span' )
\r
824 && startNode.data( 'cke-bookmark' ) )
\r
825 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
\r
826 if ( endNode && endNode.is && endNode.is( 'span' )
\r
827 && endNode.data( 'cke-bookmark' ) )
\r
828 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
\r
831 trim : function( ignoreStart, ignoreEnd )
\r
833 var startContainer = this.startContainer,
\r
834 startOffset = this.startOffset,
\r
835 collapsed = this.collapsed;
\r
836 if ( ( !ignoreStart || collapsed )
\r
837 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
839 // If the offset is zero, we just insert the new node before
\r
841 if ( !startOffset )
\r
843 startOffset = startContainer.getIndex();
\r
844 startContainer = startContainer.getParent();
\r
846 // If the offset is at the end, we'll insert it after the text
\r
848 else if ( startOffset >= startContainer.getLength() )
\r
850 startOffset = startContainer.getIndex() + 1;
\r
851 startContainer = startContainer.getParent();
\r
853 // In other case, we split the text node and insert the new
\r
854 // node at the split point.
\r
857 var nextText = startContainer.split( startOffset );
\r
859 startOffset = startContainer.getIndex() + 1;
\r
860 startContainer = startContainer.getParent();
\r
862 // Check all necessity of updating the end boundary.
\r
863 if ( this.startContainer.equals( this.endContainer ) )
\r
864 this.setEnd( nextText, this.endOffset - this.startOffset );
\r
865 else if ( startContainer.equals( this.endContainer ) )
\r
866 this.endOffset += 1;
\r
869 this.setStart( startContainer, startOffset );
\r
873 this.collapse( true );
\r
878 var endContainer = this.endContainer;
\r
879 var endOffset = this.endOffset;
\r
881 if ( !( ignoreEnd || collapsed )
\r
882 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
884 // If the offset is zero, we just insert the new node before
\r
888 endOffset = endContainer.getIndex();
\r
889 endContainer = endContainer.getParent();
\r
891 // If the offset is at the end, we'll insert it after the text
\r
893 else if ( endOffset >= endContainer.getLength() )
\r
895 endOffset = endContainer.getIndex() + 1;
\r
896 endContainer = endContainer.getParent();
\r
898 // In other case, we split the text node and insert the new
\r
899 // node at the split point.
\r
902 endContainer.split( endOffset );
\r
904 endOffset = endContainer.getIndex() + 1;
\r
905 endContainer = endContainer.getParent();
\r
908 this.setEnd( endContainer, endOffset );
\r
913 * Expands the range so that partial units are completely contained.
\r
914 * @param unit {Number} The unit type to expand with.
\r
915 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
\r
917 enlarge : function( unit, excludeBrs )
\r
921 case CKEDITOR.ENLARGE_ELEMENT :
\r
923 if ( this.collapsed )
\r
926 // Get the common ancestor.
\r
927 var commonAncestor = this.getCommonAncestor();
\r
929 var body = this.document.getBody();
\r
931 // For each boundary
\r
932 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
\r
933 // 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
935 var startTop, endTop;
\r
937 var enlargeable, sibling, commonReached;
\r
939 // Indicates that the node can be added only if whitespace
\r
940 // is available before it.
\r
941 var needsWhiteSpace = false;
\r
945 // Process the start boundary.
\r
947 var container = this.startContainer;
\r
948 var offset = this.startOffset;
\r
950 if ( container.type == CKEDITOR.NODE_TEXT )
\r
954 // Check if there is any non-space text before the
\r
955 // offset. Otherwise, container is null.
\r
956 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
\r
958 // If we found only whitespace in the node, it
\r
959 // means that we'll need more whitespace to be able
\r
960 // to expand. For example, <i> can be expanded in
\r
961 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
962 needsWhiteSpace = !!container;
\r
967 if ( !( sibling = container.getPrevious() ) )
\r
968 enlargeable = container.getParent();
\r
973 // If we have offset, get the node preceeding it as the
\r
974 // first sibling to be checked.
\r
976 sibling = container.getChild( offset - 1 ) || container.getLast();
\r
978 // If there is no sibling, mark the container to be
\r
981 enlargeable = container;
\r
984 while ( enlargeable || sibling )
\r
986 if ( enlargeable && !sibling )
\r
988 // If we reached the common ancestor, mark the flag
\r
990 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
991 commonReached = true;
\r
993 if ( !body.contains( enlargeable ) )
\r
996 // If we don't need space or this element breaks
\r
997 // the line, then enlarge it.
\r
998 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1000 needsWhiteSpace = false;
\r
1002 // If the common ancestor has been reached,
\r
1003 // we'll not enlarge it immediately, but just
\r
1004 // mark it to be enlarged later if the end
\r
1005 // boundary also enlarges it.
\r
1006 if ( commonReached )
\r
1007 startTop = enlargeable;
\r
1009 this.setStartBefore( enlargeable );
\r
1012 sibling = enlargeable.getPrevious();
\r
1015 // Check all sibling nodes preceeding the enlargeable
\r
1016 // node. The node wil lbe enlarged only if none of them
\r
1020 // This flag indicates that this node has
\r
1021 // whitespaces at the end.
\r
1022 isWhiteSpace = false;
\r
1024 if ( sibling.type == CKEDITOR.NODE_COMMENT )
\r
1026 sibling = sibling.getPrevious();
\r
1029 else if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1031 siblingText = sibling.getText();
\r
1033 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1036 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
\r
1040 // If this is a visible element.
\r
1041 // We need to check for the bookmark attribute because IE insists on
\r
1042 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1043 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1044 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1046 // We'll accept it only if we need
\r
1047 // whitespace, and this is an inline
\r
1048 // element with whitespace only.
\r
1049 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1051 // It must contains spaces and inline elements only.
\r
1053 siblingText = sibling.getText();
\r
1055 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
\r
1059 var allChildren = sibling.$.getElementsByTagName( '*' );
\r
1060 for ( var i = 0, child ; child = allChildren[ i++ ] ; )
\r
1062 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1071 isWhiteSpace = !!siblingText.length;
\r
1078 // A node with whitespaces has been found.
\r
1079 if ( isWhiteSpace )
\r
1081 // Enlarge the last enlargeable node, if we
\r
1082 // were waiting for spaces.
\r
1083 if ( needsWhiteSpace )
\r
1085 if ( commonReached )
\r
1086 startTop = enlargeable;
\r
1087 else if ( enlargeable )
\r
1088 this.setStartBefore( enlargeable );
\r
1091 needsWhiteSpace = true;
\r
1096 var next = sibling.getPrevious();
\r
1098 if ( !enlargeable && !next )
\r
1100 // Set the sibling as enlargeable, so it's
\r
1101 // parent will be get later outside this while.
\r
1102 enlargeable = sibling;
\r
1111 // If sibling has been set to null, then we
\r
1112 // need to stop enlarging.
\r
1113 enlargeable = null;
\r
1117 if ( enlargeable )
\r
1118 enlargeable = enlargeable.getParent();
\r
1121 // Process the end boundary. This is basically the same
\r
1122 // code used for the start boundary, with small changes to
\r
1123 // make it work in the oposite side (to the right). This
\r
1124 // makes it difficult to reuse the code here. So, fixes to
\r
1125 // the above code are likely to be replicated here.
\r
1127 container = this.endContainer;
\r
1128 offset = this.endOffset;
\r
1130 // Reset the common variables.
\r
1131 enlargeable = sibling = null;
\r
1132 commonReached = needsWhiteSpace = false;
\r
1134 if ( container.type == CKEDITOR.NODE_TEXT )
\r
1136 // Check if there is any non-space text after the
\r
1137 // offset. Otherwise, container is null.
\r
1138 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;
\r
1140 // If we found only whitespace in the node, it
\r
1141 // means that we'll need more whitespace to be able
\r
1142 // to expand. For example, <i> can be expanded in
\r
1143 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
1144 needsWhiteSpace = !( container && container.getLength() );
\r
1148 if ( !( sibling = container.getNext() ) )
\r
1149 enlargeable = container.getParent();
\r
1154 // Get the node right after the boudary to be checked
\r
1156 sibling = container.getChild( offset );
\r
1159 enlargeable = container;
\r
1162 while ( enlargeable || sibling )
\r
1164 if ( enlargeable && !sibling )
\r
1166 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1167 commonReached = true;
\r
1169 if ( !body.contains( enlargeable ) )
\r
1172 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1174 needsWhiteSpace = false;
\r
1176 if ( commonReached )
\r
1177 endTop = enlargeable;
\r
1178 else if ( enlargeable )
\r
1179 this.setEndAfter( enlargeable );
\r
1182 sibling = enlargeable.getNext();
\r
1187 isWhiteSpace = false;
\r
1189 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1191 siblingText = sibling.getText();
\r
1193 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1196 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
\r
1198 else if ( sibling.type == CKEDITOR.NODE_ELEMENT )
\r
1200 // If this is a visible element.
\r
1201 // We need to check for the bookmark attribute because IE insists on
\r
1202 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1203 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1204 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1206 // We'll accept it only if we need
\r
1207 // whitespace, and this is an inline
\r
1208 // element with whitespace only.
\r
1209 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1211 // It must contains spaces and inline elements only.
\r
1213 siblingText = sibling.getText();
\r
1215 if ( (/[^\s\ufeff]/).test( siblingText ) )
\r
1219 allChildren = sibling.$.getElementsByTagName( '*' );
\r
1220 for ( i = 0 ; child = allChildren[ i++ ] ; )
\r
1222 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1231 isWhiteSpace = !!siblingText.length;
\r
1240 if ( isWhiteSpace )
\r
1242 if ( needsWhiteSpace )
\r
1244 if ( commonReached )
\r
1245 endTop = enlargeable;
\r
1247 this.setEndAfter( enlargeable );
\r
1253 next = sibling.getNext();
\r
1255 if ( !enlargeable && !next )
\r
1257 enlargeable = sibling;
\r
1266 // If sibling has been set to null, then we
\r
1267 // need to stop enlarging.
\r
1268 enlargeable = null;
\r
1272 if ( enlargeable )
\r
1273 enlargeable = enlargeable.getParent();
\r
1276 // If the common ancestor can be enlarged by both boundaries, then include it also.
\r
1277 if ( startTop && endTop )
\r
1279 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
\r
1281 this.setStartBefore( commonAncestor );
\r
1282 this.setEndAfter( commonAncestor );
\r
1286 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
\r
1287 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
\r
1289 // Enlarging the start boundary.
\r
1290 var walkerRange = new CKEDITOR.dom.range( this.document );
\r
1292 body = this.document.getBody();
\r
1294 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
1295 walkerRange.setEnd( this.startContainer, this.startOffset );
\r
1297 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1298 blockBoundary, // The node on which the enlarging should stop.
\r
1299 tailBr, // In case BR as block boundary.
\r
1300 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary(
\r
1301 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),
\r
1302 // Record the encountered 'blockBoundary' for later use.
\r
1303 boundaryGuard = function( node )
\r
1305 var retval = notBlockBoundary( node );
\r
1307 blockBoundary = node;
\r
1310 // Record the encounted 'tailBr' for later use.
\r
1311 tailBrGuard = function( node )
\r
1313 var retval = boundaryGuard( node );
\r
1314 if ( !retval && node.is && node.is( 'br' ) )
\r
1319 walker.guard = boundaryGuard;
\r
1321 enlargeable = walker.lastBackward();
\r
1323 // It's the body which stop the enlarging if no block boundary found.
\r
1324 blockBoundary = blockBoundary || body;
\r
1326 // Start the range either after the end of found block (<p>...</p>[text)
\r
1327 // or at the start of block (<p>[text...), by comparing the document position
\r
1328 // with 'enlargeable' node.
\r
1331 !blockBoundary.is( 'br' ) &&
\r
1332 ( !enlargeable && this.checkStartOfBlock()
\r
1333 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1334 CKEDITOR.POSITION_AFTER_START :
\r
1335 CKEDITOR.POSITION_AFTER_END );
\r
1337 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
\r
1338 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )
\r
1340 var theRange = this.clone();
\r
1341 walker = new CKEDITOR.dom.walker( theRange );
\r
1343 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
\r
1344 bookmark = CKEDITOR.dom.walker.bookmark();
\r
1346 walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };
\r
1347 var previous = walker.previous();
\r
1348 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
\r
1353 // Enlarging the end boundary.
\r
1354 walkerRange = this.clone();
\r
1355 walkerRange.collapse();
\r
1356 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
1357 walker = new CKEDITOR.dom.walker( walkerRange );
\r
1359 // tailBrGuard only used for on range end.
\r
1360 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?
\r
1361 tailBrGuard : boundaryGuard;
\r
1362 blockBoundary = null;
\r
1363 // End the range right before the block boundary node.
\r
1365 enlargeable = walker.lastForward();
\r
1367 // It's the body which stop the enlarging if no block boundary found.
\r
1368 blockBoundary = blockBoundary || body;
\r
1370 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
\r
1371 // by comparing the document position with 'enlargeable' node.
\r
1374 ( !enlargeable && this.checkEndOfBlock()
\r
1375 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1376 CKEDITOR.POSITION_BEFORE_END :
\r
1377 CKEDITOR.POSITION_BEFORE_START );
\r
1378 // We must include the <br> at the end of range if there's
\r
1379 // one and we're expanding list item contents
\r
1381 this.setEndAfter( tailBr );
\r
1386 * Descrease the range to make sure that boundaries
\r
1387 * always anchor beside text nodes or innermost element.
\r
1388 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.
\r
1390 * <dt>CKEDITOR.SHRINK_ELEMENT</dt>
\r
1391 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>
\r
1392 * <dt>CKEDITOR.SHRINK_TEXT</dt>
\r
1393 * <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
1395 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
\r
1397 shrink : function( mode, selectContents )
\r
1399 // Unable to shrink a collapsed range.
\r
1400 if ( !this.collapsed )
\r
1402 mode = mode || CKEDITOR.SHRINK_TEXT;
\r
1404 var walkerRange = this.clone();
\r
1406 var startContainer = this.startContainer,
\r
1407 endContainer = this.endContainer,
\r
1408 startOffset = this.startOffset,
\r
1409 endOffset = this.endOffset,
\r
1410 collapsed = this.collapsed;
\r
1412 // Whether the start/end boundary is moveable.
\r
1413 var moveStart = 1,
\r
1416 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1418 if ( !startOffset )
\r
1419 walkerRange.setStartBefore( startContainer );
\r
1420 else if ( startOffset >= startContainer.getLength( ) )
\r
1421 walkerRange.setStartAfter( startContainer );
\r
1424 // Enlarge the range properly to avoid walker making
\r
1425 // DOM changes caused by triming the text nodes later.
\r
1426 walkerRange.setStartBefore( startContainer );
\r
1431 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
1434 walkerRange.setEndBefore( endContainer );
\r
1435 else if ( endOffset >= endContainer.getLength( ) )
\r
1436 walkerRange.setEndAfter( endContainer );
\r
1439 walkerRange.setEndAfter( endContainer );
\r
1444 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1445 isBookmark = CKEDITOR.dom.walker.bookmark();
\r
1447 walker.evaluator = function( node )
\r
1449 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
\r
1450 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
\r
1453 var currentElement;
\r
1454 walker.guard = function( node, movingOut )
\r
1456 if ( isBookmark( node ) )
\r
1459 // Stop when we're shrink in element mode while encountering a text node.
\r
1460 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
\r
1463 // Stop when we've already walked "through" an element.
\r
1464 if ( movingOut && node.equals( currentElement ) )
\r
1467 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
\r
1468 currentElement = node;
\r
1475 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
\r
1476 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
\r
1482 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
\r
1483 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
\r
1486 return !!( moveStart || moveEnd );
\r
1491 * Inserts a node at the start of the range. The range will be expanded
\r
1492 * the contain the node.
\r
1494 insertNode : function( node )
\r
1496 this.optimizeBookmark();
\r
1497 this.trim( false, true );
\r
1499 var startContainer = this.startContainer;
\r
1500 var startOffset = this.startOffset;
\r
1502 var nextNode = startContainer.getChild( startOffset );
\r
1505 node.insertBefore( nextNode );
\r
1507 startContainer.append( node );
\r
1509 // Check if we need to update the end boundary.
\r
1510 if ( node.getParent().equals( this.endContainer ) )
\r
1513 // Expand the range to embrace the new node.
\r
1514 this.setStartBefore( node );
\r
1517 moveToPosition : function( node, position )
\r
1519 this.setStartAt( node, position );
\r
1520 this.collapse( true );
\r
1523 selectNodeContents : function( node )
\r
1525 this.setStart( node, 0 );
\r
1526 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
\r
1530 * Sets the start position of a Range.
\r
1531 * @param {CKEDITOR.dom.node} startNode The node to start the range.
\r
1532 * @param {Number} startOffset An integer greater than or equal to zero
\r
1533 * representing the offset for the start of the range from the start
\r
1536 setStart : function( startNode, startOffset )
\r
1538 // W3C requires a check for the new position. If it is after the end
\r
1539 // boundary, the range should be collapsed to the new start. It seams
\r
1540 // we will not need this check for our use of this class so we can
\r
1541 // ignore it for now.
\r
1543 // Fixing invalid range start inside dtd empty elements.
\r
1544 if( startNode.type == CKEDITOR.NODE_ELEMENT
\r
1545 && CKEDITOR.dtd.$empty[ startNode.getName() ] )
\r
1546 startOffset = startNode.getIndex(), startNode = startNode.getParent();
\r
1548 this.startContainer = startNode;
\r
1549 this.startOffset = startOffset;
\r
1551 if ( !this.endContainer )
\r
1553 this.endContainer = startNode;
\r
1554 this.endOffset = startOffset;
\r
1557 updateCollapsed( this );
\r
1561 * Sets the end position of a Range.
\r
1562 * @param {CKEDITOR.dom.node} endNode The node to end the range.
\r
1563 * @param {Number} endOffset An integer greater than or equal to zero
\r
1564 * representing the offset for the end of the range from the start
\r
1567 setEnd : function( endNode, endOffset )
\r
1569 // W3C requires a check for the new position. If it is before the start
\r
1570 // boundary, the range should be collapsed to the new end. It seams we
\r
1571 // will not need this check for our use of this class so we can ignore
\r
1574 // Fixing invalid range end inside dtd empty elements.
\r
1575 if( endNode.type == CKEDITOR.NODE_ELEMENT
\r
1576 && CKEDITOR.dtd.$empty[ endNode.getName() ] )
\r
1577 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
\r
1579 this.endContainer = endNode;
\r
1580 this.endOffset = endOffset;
\r
1582 if ( !this.startContainer )
\r
1584 this.startContainer = endNode;
\r
1585 this.startOffset = endOffset;
\r
1588 updateCollapsed( this );
\r
1591 setStartAfter : function( node )
\r
1593 this.setStart( node.getParent(), node.getIndex() + 1 );
\r
1596 setStartBefore : function( node )
\r
1598 this.setStart( node.getParent(), node.getIndex() );
\r
1601 setEndAfter : function( node )
\r
1603 this.setEnd( node.getParent(), node.getIndex() + 1 );
\r
1606 setEndBefore : function( node )
\r
1608 this.setEnd( node.getParent(), node.getIndex() );
\r
1611 setStartAt : function( node, position )
\r
1613 switch( position )
\r
1615 case CKEDITOR.POSITION_AFTER_START :
\r
1616 this.setStart( node, 0 );
\r
1619 case CKEDITOR.POSITION_BEFORE_END :
\r
1620 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1621 this.setStart( node, node.getLength() );
\r
1623 this.setStart( node, node.getChildCount() );
\r
1626 case CKEDITOR.POSITION_BEFORE_START :
\r
1627 this.setStartBefore( node );
\r
1630 case CKEDITOR.POSITION_AFTER_END :
\r
1631 this.setStartAfter( node );
\r
1634 updateCollapsed( this );
\r
1637 setEndAt : function( node, position )
\r
1639 switch( position )
\r
1641 case CKEDITOR.POSITION_AFTER_START :
\r
1642 this.setEnd( node, 0 );
\r
1645 case CKEDITOR.POSITION_BEFORE_END :
\r
1646 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1647 this.setEnd( node, node.getLength() );
\r
1649 this.setEnd( node, node.getChildCount() );
\r
1652 case CKEDITOR.POSITION_BEFORE_START :
\r
1653 this.setEndBefore( node );
\r
1656 case CKEDITOR.POSITION_AFTER_END :
\r
1657 this.setEndAfter( node );
\r
1660 updateCollapsed( this );
\r
1663 fixBlock : function( isStart, blockTag )
\r
1665 var bookmark = this.createBookmark(),
\r
1666 fixedBlock = this.document.createElement( blockTag );
\r
1668 this.collapse( isStart );
\r
1670 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
\r
1672 this.extractContents().appendTo( fixedBlock );
\r
1673 fixedBlock.trim();
\r
1675 if ( !CKEDITOR.env.ie )
\r
1676 fixedBlock.appendBogus();
\r
1678 this.insertNode( fixedBlock );
\r
1680 this.moveToBookmark( bookmark );
\r
1682 return fixedBlock;
\r
1685 splitBlock : function( blockTag )
\r
1687 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
\r
1688 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1690 var startBlockLimit = startPath.blockLimit,
\r
1691 endBlockLimit = endPath.blockLimit;
\r
1693 var startBlock = startPath.block,
\r
1694 endBlock = endPath.block;
\r
1696 var elementPath = null;
\r
1697 // Do nothing if the boundaries are in different block limits.
\r
1698 if ( !startBlockLimit.equals( endBlockLimit ) )
\r
1701 // Get or fix current blocks.
\r
1702 if ( blockTag != 'br' )
\r
1704 if ( !startBlock )
\r
1706 startBlock = this.fixBlock( true, blockTag );
\r
1707 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
\r
1711 endBlock = this.fixBlock( false, blockTag );
\r
1714 // Get the range position.
\r
1715 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
\r
1716 isEndOfBlock = endBlock && this.checkEndOfBlock();
\r
1718 // Delete the current contents.
\r
1719 // TODO: Why is 2.x doing CheckIsEmpty()?
\r
1720 this.deleteContents();
\r
1722 if ( startBlock && startBlock.equals( endBlock ) )
\r
1724 if ( isEndOfBlock )
\r
1726 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1727 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
\r
1730 else if ( isStartOfBlock )
\r
1732 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1733 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
\r
1734 startBlock = null;
\r
1738 endBlock = this.splitElement( startBlock );
\r
1740 // In Gecko, the last child node must be a bogus <br>.
\r
1741 // Note: bogus <br> added under <ul> or <ol> would cause
\r
1742 // lists to be incorrectly rendered.
\r
1743 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )
\r
1744 startBlock.appendBogus() ;
\r
1749 previousBlock : startBlock,
\r
1750 nextBlock : endBlock,
\r
1751 wasStartOfBlock : isStartOfBlock,
\r
1752 wasEndOfBlock : isEndOfBlock,
\r
1753 elementPath : elementPath
\r
1758 * Branch the specified element from the collapsed range position and
\r
1759 * place the caret between the two result branches.
\r
1760 * Note: The range must be collapsed and been enclosed by this element.
\r
1761 * @param {CKEDITOR.dom.element} element
\r
1762 * @return {CKEDITOR.dom.element} Root element of the new branch after the split.
\r
1764 splitElement : function( toSplit )
\r
1766 if ( !this.collapsed )
\r
1769 // Extract the contents of the block from the selection point to the end
\r
1770 // of its contents.
\r
1771 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
\r
1772 var documentFragment = this.extractContents();
\r
1774 // Duplicate the element after it.
\r
1775 var clone = toSplit.clone( false );
\r
1777 // Place the extracted contents into the duplicated element.
\r
1778 documentFragment.appendTo( clone );
\r
1779 clone.insertAfter( toSplit );
\r
1780 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
\r
1785 * Check whether a range boundary is at the inner boundary of a given
\r
1787 * @param {CKEDITOR.dom.element} element The target element to check.
\r
1788 * @param {Number} checkType The boundary to check for both the range
\r
1789 * and the element. It can be CKEDITOR.START or CKEDITOR.END.
\r
1790 * @returns {Boolean} "true" if the range boundary is at the inner
\r
1791 * boundary of the element.
\r
1793 checkBoundaryOfElement : function( element, checkType )
\r
1795 var checkStart = ( checkType == CKEDITOR.START );
\r
1797 // Create a copy of this range, so we can manipulate it for our checks.
\r
1798 var walkerRange = this.clone();
\r
1800 // Collapse the range at the proper size.
\r
1801 walkerRange.collapse( checkStart );
\r
1803 // Expand the range to element boundary.
\r
1804 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
\r
1805 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
\r
1807 // Create the walker, which will check if we have anything useful
\r
1809 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1810 walker.evaluator = elementBoundaryEval( checkStart );
\r
1812 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
\r
1815 // Calls to this function may produce changes to the DOM. The range may
\r
1816 // be updated to reflect such changes.
\r
1817 checkStartOfBlock : function()
\r
1819 var startContainer = this.startContainer,
\r
1820 startOffset = this.startOffset;
\r
1822 // If the starting node is a text node, and non-empty before the offset,
\r
1823 // then we're surely not at the start of block.
\r
1824 if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1826 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
\r
1827 if ( textBefore.length )
\r
1831 // Antecipate the trim() call here, so the walker will not make
\r
1832 // changes to the DOM, which would not get reflected into this
\r
1833 // range otherwise.
\r
1836 // We need to grab the block element holding the start boundary, so
\r
1837 // let's use an element path for it.
\r
1838 var path = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1840 // Creates a range starting at the block start until the range start.
\r
1841 var walkerRange = this.clone();
\r
1842 walkerRange.collapse( true );
\r
1843 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
\r
1845 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1846 walker.evaluator = getCheckStartEndBlockEvalFunction( true );
\r
1848 return walker.checkBackward();
\r
1851 checkEndOfBlock : function()
\r
1853 var endContainer = this.endContainer,
\r
1854 endOffset = this.endOffset;
\r
1856 // If the ending node is a text node, and non-empty after the offset,
\r
1857 // then we're surely not at the end of block.
\r
1858 if ( endContainer.type == CKEDITOR.NODE_TEXT )
\r
1860 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
\r
1861 if ( textAfter.length )
\r
1865 // Antecipate the trim() call here, so the walker will not make
\r
1866 // changes to the DOM, which would not get reflected into this
\r
1867 // range otherwise.
\r
1870 // We need to grab the block element holding the start boundary, so
\r
1871 // let's use an element path for it.
\r
1872 var path = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1874 // Creates a range starting at the block start until the range start.
\r
1875 var walkerRange = this.clone();
\r
1876 walkerRange.collapse( false );
\r
1877 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
\r
1879 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1880 walker.evaluator = getCheckStartEndBlockEvalFunction( false );
\r
1882 return walker.checkForward();
\r
1886 * Check if elements at which the range boundaries anchor are read-only,
\r
1887 * with respect to "contenteditable" attribute.
\r
1889 checkReadOnly : ( function()
\r
1891 function checkNodesEditable( node, anotherEnd )
\r
1895 if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
1897 if ( node.getAttribute( 'contentEditable' ) == 'false'
\r
1898 && !node.data( 'cke-editable' ) )
\r
1902 // Range enclosed entirely in an editable element.
\r
1903 else if ( node.is( 'html' )
\r
1904 || node.getAttribute( 'contentEditable' ) == 'true'
\r
1905 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
\r
1910 node = node.getParent();
\r
1918 var startNode = this.startContainer,
\r
1919 endNode = this.endContainer;
\r
1921 // Check if elements path at both boundaries are editable.
\r
1922 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
\r
1927 * Moves the range boundaries to the first/end editing point inside an
\r
1928 * element. For example, in an element tree like
\r
1929 * "<p><b><i></i></b> Text</p>", the start editing point is
\r
1930 * "<p><b><i>^</i></b> Text</p>" (inside <i>).
\r
1931 * @param {CKEDITOR.dom.element} el The element into which look for the
\r
1933 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
\r
1935 moveToElementEditablePosition : function( el, isMoveToEnd )
\r
1937 function nextDFS( node, childOnly )
\r
1941 if ( node.type == CKEDITOR.NODE_ELEMENT
\r
1942 && node.isEditable( false )
\r
1943 && !CKEDITOR.dtd.$nonEditable[ node.getName() ] )
\r
1945 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
\r
1948 if ( !childOnly && !next )
\r
1949 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
\r
1958 // Stop immediately if we've found a text node.
\r
1959 if ( el.type == CKEDITOR.NODE_TEXT )
\r
1961 this.moveToPosition( el, isMoveToEnd ?
\r
1962 CKEDITOR.POSITION_AFTER_END :
\r
1963 CKEDITOR.POSITION_BEFORE_START );
\r
1968 // If an editable element is found, move inside it, but not stop the searching.
\r
1969 if ( el.type == CKEDITOR.NODE_ELEMENT )
\r
1971 if ( el.isEditable() )
\r
1973 this.moveToPosition( el, isMoveToEnd ?
\r
1974 CKEDITOR.POSITION_BEFORE_END :
\r
1975 CKEDITOR.POSITION_AFTER_START );
\r
1980 el = nextDFS( el, found );
\r
1987 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
1989 moveToElementEditStart : function( target )
\r
1991 return this.moveToElementEditablePosition( target );
\r
1995 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
1997 moveToElementEditEnd : function( target )
\r
1999 return this.moveToElementEditablePosition( target, true );
\r
2003 * Get the single node enclosed within the range if there's one.
\r
2005 getEnclosedNode : function()
\r
2007 var walkerRange = this.clone();
\r
2009 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
\r
2010 walkerRange.optimize();
\r
2011 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT
\r
2012 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
\r
2015 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
2016 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),
\r
2017 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
\r
2018 evaluator = function( node )
\r
2020 return isNotWhitespaces( node ) && isNotBookmarks( node );
\r
2022 walkerRange.evaluator = evaluator;
\r
2023 var node = walker.next();
\r
2025 return node && node.equals( walker.previous() ) ? node : null;
\r
2028 getTouchedStartNode : function()
\r
2030 var container = this.startContainer ;
\r
2032 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2033 return container ;
\r
2035 return container.getChild( this.startOffset ) || container ;
\r
2038 getTouchedEndNode : function()
\r
2040 var container = this.endContainer ;
\r
2042 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2043 return container ;
\r
2045 return container.getChild( this.endOffset - 1 ) || container ;
\r
2050 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text"
\r
2051 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^"
\r
2052 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text"
\r
2053 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text"
\r
2055 CKEDITOR.ENLARGE_ELEMENT = 1;
\r
2056 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
\r
2057 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
\r
2059 // Check boundary types.
\r
2060 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
\r
2061 CKEDITOR.START = 1;
\r
2063 CKEDITOR.STARTEND = 3;
\r
2065 // Shrink range types.
\r
2066 // @see CKEDITOR.dom.range.prototype.shrink
\r
2067 CKEDITOR.SHRINK_ELEMENT = 1;
\r
2068 CKEDITOR.SHRINK_TEXT = 2;
\r