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()
\r
352 var skipBogus = false,
\r
353 whitespaces = CKEDITOR.dom.walker.whitespaces(),
\r
354 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
\r
355 isBogus = CKEDITOR.dom.walker.bogus();
\r
357 return function( node )
\r
359 // First skip empty nodes.
\r
360 if ( bookmarkEvaluator( node ) || whitespaces( node ) )
\r
363 // Skip the bogus node at the end of block.
\r
364 if ( isBogus( node ) &&
\r
371 // If there's any visible text, then we're not at the start.
\r
372 if ( node.type == CKEDITOR.NODE_TEXT &&
\r
373 ( node.hasAscendant( 'pre' ) ||
\r
374 CKEDITOR.tools.trim( node.getText() ).length ) )
\r
377 // If there are non-empty inline elements (e.g. <img />), then we're not
\r
379 if ( node.type == CKEDITOR.NODE_ELEMENT && !inlineChildReqElements[ node.getName() ] )
\r
387 var isBogus = CKEDITOR.dom.walker.bogus();
\r
388 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
\r
389 // text node and non-empty elements unless it's being bookmark text.
\r
390 function elementBoundaryEval( checkStart )
\r
392 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
\r
393 bookmark = CKEDITOR.dom.walker.bookmark( 1 );
\r
395 return function( node )
\r
397 // First skip empty nodes.
\r
398 if ( bookmark( node ) || whitespaces( node ) )
\r
401 // Tolerant bogus br when checking at the end of block.
\r
402 // Reject any text node unless it's being bookmark
\r
404 // Reject any element unless it's being invisible empty. (#3883)
\r
405 return !checkStart && isBogus( node ) ||
\r
406 node.type == CKEDITOR.NODE_ELEMENT &&
\r
407 node.getName() in CKEDITOR.dtd.$removeEmpty;
\r
411 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
\r
412 bookmarkEval = new CKEDITOR.dom.walker.bookmark(),
\r
413 nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/;
\r
415 function nonWhitespaceOrBookmarkEval( node )
\r
417 // Whitespaces and bookmark nodes are to be ignored.
\r
418 return !whitespaceEval( node ) && !bookmarkEval( node );
\r
421 CKEDITOR.dom.range.prototype =
\r
425 var clone = new CKEDITOR.dom.range( this.document );
\r
427 clone.startContainer = this.startContainer;
\r
428 clone.startOffset = this.startOffset;
\r
429 clone.endContainer = this.endContainer;
\r
430 clone.endOffset = this.endOffset;
\r
431 clone.collapsed = this.collapsed;
\r
436 collapse : function( toStart )
\r
440 this.endContainer = this.startContainer;
\r
441 this.endOffset = this.startOffset;
\r
445 this.startContainer = this.endContainer;
\r
446 this.startOffset = this.endOffset;
\r
449 this.collapsed = true;
\r
453 * The content nodes of the range are cloned and added to a document fragment, which is returned.
\r
454 * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting).
\r
456 cloneContents : function()
\r
458 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
460 if ( !this.collapsed )
\r
461 execContentsAction( this, 2, docFrag );
\r
467 * Deletes the content nodes of the range permanently from the DOM tree.
\r
468 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
470 deleteContents : function( mergeThen )
\r
472 if ( this.collapsed )
\r
475 execContentsAction( this, 0, null, mergeThen );
\r
479 * The content nodes of the range are cloned and added to a document fragment,
\r
480 * meanwhile they're removed permanently from the DOM tree.
\r
481 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
483 extractContents : function( mergeThen )
\r
485 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
487 if ( !this.collapsed )
\r
488 execContentsAction( this, 1, docFrag, mergeThen );
\r
494 * Creates a bookmark object, which can be later used to restore the
\r
495 * range by using the moveToBookmark function.
\r
496 * This is an "intrusive" way to create a bookmark. It includes <span> tags
\r
497 * in the range boundaries. The advantage of it is that it is possible to
\r
498 * handle DOM mutations when moving back to the bookmark.
\r
499 * Attention: the inclusion of nodes in the DOM is a design choice and
\r
500 * should not be changed as there are other points in the code that may be
\r
501 * using those nodes to perform operations. See GetBookmarkNode.
\r
502 * @param {Boolean} [serializable] Indicates that the bookmark nodes
\r
503 * must contain ids, which can be used to restore the range even
\r
504 * when these nodes suffer mutations (like a clonation or innerHTML
\r
506 * @returns {Object} And object representing a bookmark.
\r
508 createBookmark : function( serializable )
\r
510 var startNode, endNode;
\r
513 var collapsed = this.collapsed;
\r
515 startNode = this.document.createElement( 'span' );
\r
516 startNode.data( 'cke-bookmark', 1 );
\r
517 startNode.setStyle( 'display', 'none' );
\r
519 // For IE, it must have something inside, otherwise it may be
\r
520 // removed during DOM operations.
\r
521 startNode.setHtml( ' ' );
\r
523 if ( serializable )
\r
525 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
\r
526 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
\r
529 // If collapsed, the endNode will not be created.
\r
532 endNode = startNode.clone();
\r
533 endNode.setHtml( ' ' );
\r
535 if ( serializable )
\r
536 endNode.setAttribute( 'id', baseId + 'E' );
\r
538 clone = this.clone();
\r
540 clone.insertNode( endNode );
\r
543 clone = this.clone();
\r
544 clone.collapse( true );
\r
545 clone.insertNode( startNode );
\r
547 // Update the range position.
\r
550 this.setStartAfter( startNode );
\r
551 this.setEndBefore( endNode );
\r
554 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
\r
557 startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
\r
558 endNode : serializable ? baseId + 'E' : endNode,
\r
559 serializable : serializable,
\r
560 collapsed : collapsed
\r
565 * Creates a "non intrusive" and "mutation sensible" bookmark. This
\r
566 * kind of bookmark should be used only when the DOM is supposed to
\r
567 * remain stable after its creation.
\r
568 * @param {Boolean} [normalized] Indicates that the bookmark must
\r
569 * normalized. When normalized, the successive text nodes are
\r
570 * considered a single node. To sucessful load a normalized
\r
571 * bookmark, the DOM tree must be also normalized before calling
\r
573 * @returns {Object} An object representing the bookmark.
\r
575 createBookmark2 : function( normalized )
\r
577 var startContainer = this.startContainer,
\r
578 endContainer = this.endContainer;
\r
580 var startOffset = this.startOffset,
\r
581 endOffset = this.endOffset;
\r
583 var collapsed = this.collapsed;
\r
585 var child, previous;
\r
587 // If there is no range then get out of here.
\r
588 // It happens on initial load in Safari #962 and if the editor it's
\r
589 // hidden also in Firefox
\r
590 if ( !startContainer || !endContainer )
\r
591 return { start : 0, end : 0 };
\r
595 // Find out if the start is pointing to a text node that will
\r
597 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
\r
599 child = startContainer.getChild( startOffset );
\r
601 // In this case, move the start information to that text
\r
603 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
604 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
606 startContainer = child;
\r
610 // Get the normalized offset.
\r
611 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
612 startOffset = child.getIndex( 1 );
\r
615 // Normalize the start.
\r
616 while ( startContainer.type == CKEDITOR.NODE_TEXT
\r
617 && ( previous = startContainer.getPrevious() )
\r
618 && previous.type == CKEDITOR.NODE_TEXT )
\r
620 startContainer = previous;
\r
621 startOffset += previous.getLength();
\r
624 // Process the end only if not normalized.
\r
627 // Find out if the start is pointing to a text node that
\r
628 // will be normalized.
\r
629 if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
\r
631 child = endContainer.getChild( endOffset );
\r
633 // In this case, move the start information to that
\r
635 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
636 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
638 endContainer = child;
\r
642 // Get the normalized offset.
\r
643 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
\r
644 endOffset = child.getIndex( 1 );
\r
647 // Normalize the end.
\r
648 while ( endContainer.type == CKEDITOR.NODE_TEXT
\r
649 && ( previous = endContainer.getPrevious() )
\r
650 && previous.type == CKEDITOR.NODE_TEXT )
\r
652 endContainer = previous;
\r
653 endOffset += previous.getLength();
\r
659 start : startContainer.getAddress( normalized ),
\r
660 end : collapsed ? null : endContainer.getAddress( normalized ),
\r
661 startOffset : startOffset,
\r
662 endOffset : endOffset,
\r
663 normalized : normalized,
\r
664 collapsed : collapsed,
\r
665 is2 : true // It's a createBookmark2 bookmark.
\r
669 moveToBookmark : function( bookmark )
\r
671 if ( bookmark.is2 ) // Created with createBookmark2().
\r
673 // Get the start information.
\r
674 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
\r
675 startOffset = bookmark.startOffset;
\r
677 // Get the end information.
\r
678 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
\r
679 endOffset = bookmark.endOffset;
\r
681 // Set the start boundary.
\r
682 this.setStart( startContainer, startOffset );
\r
684 // Set the end boundary. If not available, collapse it.
\r
685 if ( endContainer )
\r
686 this.setEnd( endContainer, endOffset );
\r
688 this.collapse( true );
\r
690 else // Created with createBookmark().
\r
692 var serializable = bookmark.serializable,
\r
693 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
\r
694 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
\r
696 // Set the range start at the bookmark start node position.
\r
697 this.setStartBefore( startNode );
\r
699 // Remove it, because it may interfere in the setEndBefore call.
\r
700 startNode.remove();
\r
702 // Set the range end at the bookmark end node position, or simply
\r
703 // collapse it if it is not available.
\r
706 this.setEndBefore( endNode );
\r
710 this.collapse( true );
\r
714 getBoundaryNodes : function()
\r
716 var startNode = this.startContainer,
\r
717 endNode = this.endContainer,
\r
718 startOffset = this.startOffset,
\r
719 endOffset = this.endOffset,
\r
722 if ( startNode.type == CKEDITOR.NODE_ELEMENT )
\r
724 childCount = startNode.getChildCount();
\r
725 if ( childCount > startOffset )
\r
726 startNode = startNode.getChild( startOffset );
\r
727 else if ( childCount < 1 )
\r
728 startNode = startNode.getPreviousSourceNode();
\r
729 else // startOffset > childCount but childCount is not 0
\r
731 // Try to take the node just after the current position.
\r
732 startNode = startNode.$;
\r
733 while ( startNode.lastChild )
\r
734 startNode = startNode.lastChild;
\r
735 startNode = new CKEDITOR.dom.node( startNode );
\r
737 // Normally we should take the next node in DFS order. But it
\r
738 // is also possible that we've already reached the end of
\r
740 startNode = startNode.getNextSourceNode() || startNode;
\r
743 if ( endNode.type == CKEDITOR.NODE_ELEMENT )
\r
745 childCount = endNode.getChildCount();
\r
746 if ( childCount > endOffset )
\r
747 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
\r
748 else if ( childCount < 1 )
\r
749 endNode = endNode.getPreviousSourceNode();
\r
750 else // endOffset > childCount but childCount is not 0
\r
752 // Try to take the node just before the current position.
\r
753 endNode = endNode.$;
\r
754 while ( endNode.lastChild )
\r
755 endNode = endNode.lastChild;
\r
756 endNode = new CKEDITOR.dom.node( endNode );
\r
760 // Sometimes the endNode will come right before startNode for collapsed
\r
761 // ranges. Fix it. (#3780)
\r
762 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
\r
763 startNode = endNode;
\r
765 return { startNode : startNode, endNode : endNode };
\r
769 * Find the node which fully contains the range.
\r
770 * @param includeSelf
\r
771 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.
\r
773 getCommonAncestor : function( includeSelf , ignoreTextNode )
\r
775 var start = this.startContainer,
\r
776 end = this.endContainer,
\r
779 if ( start.equals( end ) )
\r
782 && start.type == CKEDITOR.NODE_ELEMENT
\r
783 && this.startOffset == this.endOffset - 1 )
\r
784 ancestor = start.getChild( this.startOffset );
\r
789 ancestor = start.getCommonAncestor( end );
\r
791 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
\r
795 * Transforms the startContainer and endContainer properties from text
\r
796 * nodes to element nodes, whenever possible. This is actually possible
\r
797 * if either of the boundary containers point to a text node, and its
\r
798 * offset is set to zero, or after the last char in the node.
\r
800 optimize : function()
\r
802 var container = this.startContainer;
\r
803 var offset = this.startOffset;
\r
805 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
808 this.setStartBefore( container );
\r
809 else if ( offset >= container.getLength() )
\r
810 this.setStartAfter( container );
\r
813 container = this.endContainer;
\r
814 offset = this.endOffset;
\r
816 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
819 this.setEndBefore( container );
\r
820 else if ( offset >= container.getLength() )
\r
821 this.setEndAfter( container );
\r
826 * Move the range out of bookmark nodes if they'd been the container.
\r
828 optimizeBookmark: function()
\r
830 var startNode = this.startContainer,
\r
831 endNode = this.endContainer;
\r
833 if ( startNode.is && startNode.is( 'span' )
\r
834 && startNode.data( 'cke-bookmark' ) )
\r
835 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
\r
836 if ( endNode && endNode.is && endNode.is( 'span' )
\r
837 && endNode.data( 'cke-bookmark' ) )
\r
838 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
\r
841 trim : function( ignoreStart, ignoreEnd )
\r
843 var startContainer = this.startContainer,
\r
844 startOffset = this.startOffset,
\r
845 collapsed = this.collapsed;
\r
846 if ( ( !ignoreStart || collapsed )
\r
847 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
849 // If the offset is zero, we just insert the new node before
\r
851 if ( !startOffset )
\r
853 startOffset = startContainer.getIndex();
\r
854 startContainer = startContainer.getParent();
\r
856 // If the offset is at the end, we'll insert it after the text
\r
858 else if ( startOffset >= startContainer.getLength() )
\r
860 startOffset = startContainer.getIndex() + 1;
\r
861 startContainer = startContainer.getParent();
\r
863 // In other case, we split the text node and insert the new
\r
864 // node at the split point.
\r
867 var nextText = startContainer.split( startOffset );
\r
869 startOffset = startContainer.getIndex() + 1;
\r
870 startContainer = startContainer.getParent();
\r
872 // Check all necessity of updating the end boundary.
\r
873 if ( this.startContainer.equals( this.endContainer ) )
\r
874 this.setEnd( nextText, this.endOffset - this.startOffset );
\r
875 else if ( startContainer.equals( this.endContainer ) )
\r
876 this.endOffset += 1;
\r
879 this.setStart( startContainer, startOffset );
\r
883 this.collapse( true );
\r
888 var endContainer = this.endContainer;
\r
889 var endOffset = this.endOffset;
\r
891 if ( !( ignoreEnd || collapsed )
\r
892 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
894 // If the offset is zero, we just insert the new node before
\r
898 endOffset = endContainer.getIndex();
\r
899 endContainer = endContainer.getParent();
\r
901 // If the offset is at the end, we'll insert it after the text
\r
903 else if ( endOffset >= endContainer.getLength() )
\r
905 endOffset = endContainer.getIndex() + 1;
\r
906 endContainer = endContainer.getParent();
\r
908 // In other case, we split the text node and insert the new
\r
909 // node at the split point.
\r
912 endContainer.split( endOffset );
\r
914 endOffset = endContainer.getIndex() + 1;
\r
915 endContainer = endContainer.getParent();
\r
918 this.setEnd( endContainer, endOffset );
\r
923 * Expands the range so that partial units are completely contained.
\r
924 * @param unit {Number} The unit type to expand with.
\r
925 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
\r
927 enlarge : function( unit, excludeBrs )
\r
931 case CKEDITOR.ENLARGE_ELEMENT :
\r
933 if ( this.collapsed )
\r
936 // Get the common ancestor.
\r
937 var commonAncestor = this.getCommonAncestor();
\r
939 var body = this.document.getBody();
\r
941 // For each boundary
\r
942 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
\r
943 // 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
945 var startTop, endTop;
\r
947 var enlargeable, sibling, commonReached;
\r
949 // Indicates that the node can be added only if whitespace
\r
950 // is available before it.
\r
951 var needsWhiteSpace = false;
\r
955 // Process the start boundary.
\r
957 var container = this.startContainer;
\r
958 var offset = this.startOffset;
\r
960 if ( container.type == CKEDITOR.NODE_TEXT )
\r
964 // Check if there is any non-space text before the
\r
965 // offset. Otherwise, container is null.
\r
966 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
\r
968 // If we found only whitespace in the node, it
\r
969 // means that we'll need more whitespace to be able
\r
970 // to expand. For example, <i> can be expanded in
\r
971 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
972 needsWhiteSpace = !!container;
\r
977 if ( !( sibling = container.getPrevious() ) )
\r
978 enlargeable = container.getParent();
\r
983 // If we have offset, get the node preceeding it as the
\r
984 // first sibling to be checked.
\r
986 sibling = container.getChild( offset - 1 ) || container.getLast();
\r
988 // If there is no sibling, mark the container to be
\r
991 enlargeable = container;
\r
994 while ( enlargeable || sibling )
\r
996 if ( enlargeable && !sibling )
\r
998 // If we reached the common ancestor, mark the flag
\r
1000 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1001 commonReached = true;
\r
1003 if ( !body.contains( enlargeable ) )
\r
1006 // If we don't need space or this element breaks
\r
1007 // the line, then enlarge it.
\r
1008 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1010 needsWhiteSpace = false;
\r
1012 // If the common ancestor has been reached,
\r
1013 // we'll not enlarge it immediately, but just
\r
1014 // mark it to be enlarged later if the end
\r
1015 // boundary also enlarges it.
\r
1016 if ( commonReached )
\r
1017 startTop = enlargeable;
\r
1019 this.setStartBefore( enlargeable );
\r
1022 sibling = enlargeable.getPrevious();
\r
1025 // Check all sibling nodes preceeding the enlargeable
\r
1026 // node. The node wil lbe enlarged only if none of them
\r
1030 // This flag indicates that this node has
\r
1031 // whitespaces at the end.
\r
1032 isWhiteSpace = false;
\r
1034 if ( sibling.type == CKEDITOR.NODE_COMMENT )
\r
1036 sibling = sibling.getPrevious();
\r
1039 else if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1041 siblingText = sibling.getText();
\r
1043 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1046 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
\r
1050 // If this is a visible element.
\r
1051 // We need to check for the bookmark attribute because IE insists on
\r
1052 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1053 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1054 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1056 // We'll accept it only if we need
\r
1057 // whitespace, and this is an inline
\r
1058 // element with whitespace only.
\r
1059 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1061 // It must contains spaces and inline elements only.
\r
1063 siblingText = sibling.getText();
\r
1065 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
\r
1069 var allChildren = sibling.$.getElementsByTagName( '*' );
\r
1070 for ( var i = 0, child ; child = allChildren[ i++ ] ; )
\r
1072 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1081 isWhiteSpace = !!siblingText.length;
\r
1088 // A node with whitespaces has been found.
\r
1089 if ( isWhiteSpace )
\r
1091 // Enlarge the last enlargeable node, if we
\r
1092 // were waiting for spaces.
\r
1093 if ( needsWhiteSpace )
\r
1095 if ( commonReached )
\r
1096 startTop = enlargeable;
\r
1097 else if ( enlargeable )
\r
1098 this.setStartBefore( enlargeable );
\r
1101 needsWhiteSpace = true;
\r
1106 var next = sibling.getPrevious();
\r
1108 if ( !enlargeable && !next )
\r
1110 // Set the sibling as enlargeable, so it's
\r
1111 // parent will be get later outside this while.
\r
1112 enlargeable = sibling;
\r
1121 // If sibling has been set to null, then we
\r
1122 // need to stop enlarging.
\r
1123 enlargeable = null;
\r
1127 if ( enlargeable )
\r
1128 enlargeable = enlargeable.getParent();
\r
1131 // Process the end boundary. This is basically the same
\r
1132 // code used for the start boundary, with small changes to
\r
1133 // make it work in the oposite side (to the right). This
\r
1134 // makes it difficult to reuse the code here. So, fixes to
\r
1135 // the above code are likely to be replicated here.
\r
1137 container = this.endContainer;
\r
1138 offset = this.endOffset;
\r
1140 // Reset the common variables.
\r
1141 enlargeable = sibling = null;
\r
1142 commonReached = needsWhiteSpace = false;
\r
1144 if ( container.type == CKEDITOR.NODE_TEXT )
\r
1146 // Check if there is any non-space text after the
\r
1147 // offset. Otherwise, container is null.
\r
1148 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;
\r
1150 // If we found only whitespace in the node, it
\r
1151 // means that we'll need more whitespace to be able
\r
1152 // to expand. For example, <i> can be expanded in
\r
1153 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
1154 needsWhiteSpace = !( container && container.getLength() );
\r
1158 if ( !( sibling = container.getNext() ) )
\r
1159 enlargeable = container.getParent();
\r
1164 // Get the node right after the boudary to be checked
\r
1166 sibling = container.getChild( offset );
\r
1169 enlargeable = container;
\r
1172 while ( enlargeable || sibling )
\r
1174 if ( enlargeable && !sibling )
\r
1176 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1177 commonReached = true;
\r
1179 if ( !body.contains( enlargeable ) )
\r
1182 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1184 needsWhiteSpace = false;
\r
1186 if ( commonReached )
\r
1187 endTop = enlargeable;
\r
1188 else if ( enlargeable )
\r
1189 this.setEndAfter( enlargeable );
\r
1192 sibling = enlargeable.getNext();
\r
1197 isWhiteSpace = false;
\r
1199 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1201 siblingText = sibling.getText();
\r
1203 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1206 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
\r
1208 else if ( sibling.type == CKEDITOR.NODE_ELEMENT )
\r
1210 // If this is a visible element.
\r
1211 // We need to check for the bookmark attribute because IE insists on
\r
1212 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1213 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1214 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1216 // We'll accept it only if we need
\r
1217 // whitespace, and this is an inline
\r
1218 // element with whitespace only.
\r
1219 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1221 // It must contains spaces and inline elements only.
\r
1223 siblingText = sibling.getText();
\r
1225 if ( (/[^\s\ufeff]/).test( siblingText ) )
\r
1229 allChildren = sibling.$.getElementsByTagName( '*' );
\r
1230 for ( i = 0 ; child = allChildren[ i++ ] ; )
\r
1232 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1241 isWhiteSpace = !!siblingText.length;
\r
1250 if ( isWhiteSpace )
\r
1252 if ( needsWhiteSpace )
\r
1254 if ( commonReached )
\r
1255 endTop = enlargeable;
\r
1257 this.setEndAfter( enlargeable );
\r
1263 next = sibling.getNext();
\r
1265 if ( !enlargeable && !next )
\r
1267 enlargeable = sibling;
\r
1276 // If sibling has been set to null, then we
\r
1277 // need to stop enlarging.
\r
1278 enlargeable = null;
\r
1282 if ( enlargeable )
\r
1283 enlargeable = enlargeable.getParent();
\r
1286 // If the common ancestor can be enlarged by both boundaries, then include it also.
\r
1287 if ( startTop && endTop )
\r
1289 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
\r
1291 this.setStartBefore( commonAncestor );
\r
1292 this.setEndAfter( commonAncestor );
\r
1296 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
\r
1297 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
\r
1299 // Enlarging the start boundary.
\r
1300 var walkerRange = new CKEDITOR.dom.range( this.document );
\r
1302 body = this.document.getBody();
\r
1304 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
1305 walkerRange.setEnd( this.startContainer, this.startOffset );
\r
1307 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1308 blockBoundary, // The node on which the enlarging should stop.
\r
1309 tailBr, // In case BR as block boundary.
\r
1310 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary(
\r
1311 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),
\r
1312 // Record the encountered 'blockBoundary' for later use.
\r
1313 boundaryGuard = function( node )
\r
1315 var retval = notBlockBoundary( node );
\r
1317 blockBoundary = node;
\r
1320 // Record the encounted 'tailBr' for later use.
\r
1321 tailBrGuard = function( node )
\r
1323 var retval = boundaryGuard( node );
\r
1324 if ( !retval && node.is && node.is( 'br' ) )
\r
1329 walker.guard = boundaryGuard;
\r
1331 enlargeable = walker.lastBackward();
\r
1333 // It's the body which stop the enlarging if no block boundary found.
\r
1334 blockBoundary = blockBoundary || body;
\r
1336 // Start the range either after the end of found block (<p>...</p>[text)
\r
1337 // or at the start of block (<p>[text...), by comparing the document position
\r
1338 // with 'enlargeable' node.
\r
1341 !blockBoundary.is( 'br' ) &&
\r
1342 ( !enlargeable && this.checkStartOfBlock()
\r
1343 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1344 CKEDITOR.POSITION_AFTER_START :
\r
1345 CKEDITOR.POSITION_AFTER_END );
\r
1347 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
\r
1348 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )
\r
1350 var theRange = this.clone();
\r
1351 walker = new CKEDITOR.dom.walker( theRange );
\r
1353 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
\r
1354 bookmark = CKEDITOR.dom.walker.bookmark();
\r
1356 walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };
\r
1357 var previous = walker.previous();
\r
1358 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
\r
1363 // Enlarging the end boundary.
\r
1364 walkerRange = this.clone();
\r
1365 walkerRange.collapse();
\r
1366 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
1367 walker = new CKEDITOR.dom.walker( walkerRange );
\r
1369 // tailBrGuard only used for on range end.
\r
1370 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?
\r
1371 tailBrGuard : boundaryGuard;
\r
1372 blockBoundary = null;
\r
1373 // End the range right before the block boundary node.
\r
1375 enlargeable = walker.lastForward();
\r
1377 // It's the body which stop the enlarging if no block boundary found.
\r
1378 blockBoundary = blockBoundary || body;
\r
1380 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
\r
1381 // by comparing the document position with 'enlargeable' node.
\r
1384 ( !enlargeable && this.checkEndOfBlock()
\r
1385 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1386 CKEDITOR.POSITION_BEFORE_END :
\r
1387 CKEDITOR.POSITION_BEFORE_START );
\r
1388 // We must include the <br> at the end of range if there's
\r
1389 // one and we're expanding list item contents
\r
1391 this.setEndAfter( tailBr );
\r
1396 * Descrease the range to make sure that boundaries
\r
1397 * always anchor beside text nodes or innermost element.
\r
1398 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.
\r
1400 * <dt>CKEDITOR.SHRINK_ELEMENT</dt>
\r
1401 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>
\r
1402 * <dt>CKEDITOR.SHRINK_TEXT</dt>
\r
1403 * <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
1405 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
\r
1407 shrink : function( mode, selectContents )
\r
1409 // Unable to shrink a collapsed range.
\r
1410 if ( !this.collapsed )
\r
1412 mode = mode || CKEDITOR.SHRINK_TEXT;
\r
1414 var walkerRange = this.clone();
\r
1416 var startContainer = this.startContainer,
\r
1417 endContainer = this.endContainer,
\r
1418 startOffset = this.startOffset,
\r
1419 endOffset = this.endOffset,
\r
1420 collapsed = this.collapsed;
\r
1422 // Whether the start/end boundary is moveable.
\r
1423 var moveStart = 1,
\r
1426 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1428 if ( !startOffset )
\r
1429 walkerRange.setStartBefore( startContainer );
\r
1430 else if ( startOffset >= startContainer.getLength( ) )
\r
1431 walkerRange.setStartAfter( startContainer );
\r
1434 // Enlarge the range properly to avoid walker making
\r
1435 // DOM changes caused by triming the text nodes later.
\r
1436 walkerRange.setStartBefore( startContainer );
\r
1441 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
1444 walkerRange.setEndBefore( endContainer );
\r
1445 else if ( endOffset >= endContainer.getLength( ) )
\r
1446 walkerRange.setEndAfter( endContainer );
\r
1449 walkerRange.setEndAfter( endContainer );
\r
1454 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1455 isBookmark = CKEDITOR.dom.walker.bookmark();
\r
1457 walker.evaluator = function( node )
\r
1459 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
\r
1460 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
\r
1463 var currentElement;
\r
1464 walker.guard = function( node, movingOut )
\r
1466 if ( isBookmark( node ) )
\r
1469 // Stop when we're shrink in element mode while encountering a text node.
\r
1470 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
\r
1473 // Stop when we've already walked "through" an element.
\r
1474 if ( movingOut && node.equals( currentElement ) )
\r
1477 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
\r
1478 currentElement = node;
\r
1485 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
\r
1486 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
\r
1492 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
\r
1493 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
\r
1496 return !!( moveStart || moveEnd );
\r
1501 * Inserts a node at the start of the range. The range will be expanded
\r
1502 * the contain the node.
\r
1504 insertNode : function( node )
\r
1506 this.optimizeBookmark();
\r
1507 this.trim( false, true );
\r
1509 var startContainer = this.startContainer;
\r
1510 var startOffset = this.startOffset;
\r
1512 var nextNode = startContainer.getChild( startOffset );
\r
1515 node.insertBefore( nextNode );
\r
1517 startContainer.append( node );
\r
1519 // Check if we need to update the end boundary.
\r
1520 if ( node.getParent().equals( this.endContainer ) )
\r
1523 // Expand the range to embrace the new node.
\r
1524 this.setStartBefore( node );
\r
1527 moveToPosition : function( node, position )
\r
1529 this.setStartAt( node, position );
\r
1530 this.collapse( true );
\r
1533 selectNodeContents : function( node )
\r
1535 this.setStart( node, 0 );
\r
1536 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
\r
1540 * Sets the start position of a Range.
\r
1541 * @param {CKEDITOR.dom.node} startNode The node to start the range.
\r
1542 * @param {Number} startOffset An integer greater than or equal to zero
\r
1543 * representing the offset for the start of the range from the start
\r
1546 setStart : function( startNode, startOffset )
\r
1548 // W3C requires a check for the new position. If it is after the end
\r
1549 // boundary, the range should be collapsed to the new start. It seams
\r
1550 // we will not need this check for our use of this class so we can
\r
1551 // ignore it for now.
\r
1553 // Fixing invalid range start inside dtd empty elements.
\r
1554 if( startNode.type == CKEDITOR.NODE_ELEMENT
\r
1555 && CKEDITOR.dtd.$empty[ startNode.getName() ] )
\r
1556 startOffset = startNode.getIndex(), startNode = startNode.getParent();
\r
1558 this.startContainer = startNode;
\r
1559 this.startOffset = startOffset;
\r
1561 if ( !this.endContainer )
\r
1563 this.endContainer = startNode;
\r
1564 this.endOffset = startOffset;
\r
1567 updateCollapsed( this );
\r
1571 * Sets the end position of a Range.
\r
1572 * @param {CKEDITOR.dom.node} endNode The node to end the range.
\r
1573 * @param {Number} endOffset An integer greater than or equal to zero
\r
1574 * representing the offset for the end of the range from the start
\r
1577 setEnd : function( endNode, endOffset )
\r
1579 // W3C requires a check for the new position. If it is before the start
\r
1580 // boundary, the range should be collapsed to the new end. It seams we
\r
1581 // will not need this check for our use of this class so we can ignore
\r
1584 // Fixing invalid range end inside dtd empty elements.
\r
1585 if( endNode.type == CKEDITOR.NODE_ELEMENT
\r
1586 && CKEDITOR.dtd.$empty[ endNode.getName() ] )
\r
1587 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
\r
1589 this.endContainer = endNode;
\r
1590 this.endOffset = endOffset;
\r
1592 if ( !this.startContainer )
\r
1594 this.startContainer = endNode;
\r
1595 this.startOffset = endOffset;
\r
1598 updateCollapsed( this );
\r
1601 setStartAfter : function( node )
\r
1603 this.setStart( node.getParent(), node.getIndex() + 1 );
\r
1606 setStartBefore : function( node )
\r
1608 this.setStart( node.getParent(), node.getIndex() );
\r
1611 setEndAfter : function( node )
\r
1613 this.setEnd( node.getParent(), node.getIndex() + 1 );
\r
1616 setEndBefore : function( node )
\r
1618 this.setEnd( node.getParent(), node.getIndex() );
\r
1621 setStartAt : function( node, position )
\r
1623 switch( position )
\r
1625 case CKEDITOR.POSITION_AFTER_START :
\r
1626 this.setStart( node, 0 );
\r
1629 case CKEDITOR.POSITION_BEFORE_END :
\r
1630 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1631 this.setStart( node, node.getLength() );
\r
1633 this.setStart( node, node.getChildCount() );
\r
1636 case CKEDITOR.POSITION_BEFORE_START :
\r
1637 this.setStartBefore( node );
\r
1640 case CKEDITOR.POSITION_AFTER_END :
\r
1641 this.setStartAfter( node );
\r
1644 updateCollapsed( this );
\r
1647 setEndAt : function( node, position )
\r
1649 switch( position )
\r
1651 case CKEDITOR.POSITION_AFTER_START :
\r
1652 this.setEnd( node, 0 );
\r
1655 case CKEDITOR.POSITION_BEFORE_END :
\r
1656 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1657 this.setEnd( node, node.getLength() );
\r
1659 this.setEnd( node, node.getChildCount() );
\r
1662 case CKEDITOR.POSITION_BEFORE_START :
\r
1663 this.setEndBefore( node );
\r
1666 case CKEDITOR.POSITION_AFTER_END :
\r
1667 this.setEndAfter( node );
\r
1670 updateCollapsed( this );
\r
1673 fixBlock : function( isStart, blockTag )
\r
1675 var bookmark = this.createBookmark(),
\r
1676 fixedBlock = this.document.createElement( blockTag );
\r
1678 this.collapse( isStart );
\r
1680 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
\r
1682 this.extractContents().appendTo( fixedBlock );
\r
1683 fixedBlock.trim();
\r
1685 if ( !CKEDITOR.env.ie )
\r
1686 fixedBlock.appendBogus();
\r
1688 this.insertNode( fixedBlock );
\r
1690 this.moveToBookmark( bookmark );
\r
1692 return fixedBlock;
\r
1695 splitBlock : function( blockTag )
\r
1697 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
\r
1698 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1700 var startBlockLimit = startPath.blockLimit,
\r
1701 endBlockLimit = endPath.blockLimit;
\r
1703 var startBlock = startPath.block,
\r
1704 endBlock = endPath.block;
\r
1706 var elementPath = null;
\r
1707 // Do nothing if the boundaries are in different block limits.
\r
1708 if ( !startBlockLimit.equals( endBlockLimit ) )
\r
1711 // Get or fix current blocks.
\r
1712 if ( blockTag != 'br' )
\r
1714 if ( !startBlock )
\r
1716 startBlock = this.fixBlock( true, blockTag );
\r
1717 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
\r
1721 endBlock = this.fixBlock( false, blockTag );
\r
1724 // Get the range position.
\r
1725 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
\r
1726 isEndOfBlock = endBlock && this.checkEndOfBlock();
\r
1728 // Delete the current contents.
\r
1729 // TODO: Why is 2.x doing CheckIsEmpty()?
\r
1730 this.deleteContents();
\r
1732 if ( startBlock && startBlock.equals( endBlock ) )
\r
1734 if ( isEndOfBlock )
\r
1736 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1737 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
\r
1740 else if ( isStartOfBlock )
\r
1742 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1743 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
\r
1744 startBlock = null;
\r
1748 endBlock = this.splitElement( startBlock );
\r
1750 // In Gecko, the last child node must be a bogus <br>.
\r
1751 // Note: bogus <br> added under <ul> or <ol> would cause
\r
1752 // lists to be incorrectly rendered.
\r
1753 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )
\r
1754 startBlock.appendBogus() ;
\r
1759 previousBlock : startBlock,
\r
1760 nextBlock : endBlock,
\r
1761 wasStartOfBlock : isStartOfBlock,
\r
1762 wasEndOfBlock : isEndOfBlock,
\r
1763 elementPath : elementPath
\r
1768 * Branch the specified element from the collapsed range position and
\r
1769 * place the caret between the two result branches.
\r
1770 * Note: The range must be collapsed and been enclosed by this element.
\r
1771 * @param {CKEDITOR.dom.element} element
\r
1772 * @return {CKEDITOR.dom.element} Root element of the new branch after the split.
\r
1774 splitElement : function( toSplit )
\r
1776 if ( !this.collapsed )
\r
1779 // Extract the contents of the block from the selection point to the end
\r
1780 // of its contents.
\r
1781 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
\r
1782 var documentFragment = this.extractContents();
\r
1784 // Duplicate the element after it.
\r
1785 var clone = toSplit.clone( false );
\r
1787 // Place the extracted contents into the duplicated element.
\r
1788 documentFragment.appendTo( clone );
\r
1789 clone.insertAfter( toSplit );
\r
1790 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
\r
1795 * Check whether a range boundary is at the inner boundary of a given
\r
1797 * @param {CKEDITOR.dom.element} element The target element to check.
\r
1798 * @param {Number} checkType The boundary to check for both the range
\r
1799 * and the element. It can be CKEDITOR.START or CKEDITOR.END.
\r
1800 * @returns {Boolean} "true" if the range boundary is at the inner
\r
1801 * boundary of the element.
\r
1803 checkBoundaryOfElement : function( element, checkType )
\r
1805 var checkStart = ( checkType == CKEDITOR.START );
\r
1807 // Create a copy of this range, so we can manipulate it for our checks.
\r
1808 var walkerRange = this.clone();
\r
1810 // Collapse the range at the proper size.
\r
1811 walkerRange.collapse( checkStart );
\r
1813 // Expand the range to element boundary.
\r
1814 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
\r
1815 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
\r
1817 // Create the walker, which will check if we have anything useful
\r
1819 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1820 walker.evaluator = elementBoundaryEval( checkStart );
\r
1822 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
\r
1825 // Calls to this function may produce changes to the DOM. The range may
\r
1826 // be updated to reflect such changes.
\r
1827 checkStartOfBlock : function()
\r
1829 var startContainer = this.startContainer,
\r
1830 startOffset = this.startOffset;
\r
1832 // [IE] Special handling for range start in text with a leading NBSP,
\r
1833 // we it to be isolated, for bogus check.
\r
1834 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1836 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
\r
1837 if ( nbspRegExp.test( textBefore ) )
\r
1838 this.trim( 0, 1 );
\r
1841 // We need to grab the block element holding the start boundary, so
\r
1842 // let's use an element path for it.
\r
1843 var path = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1845 // Creates a range starting at the block start until the range start.
\r
1846 var walkerRange = this.clone();
\r
1847 walkerRange.collapse( true );
\r
1848 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
\r
1850 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1851 walker.evaluator = getCheckStartEndBlockEvalFunction();
\r
1853 return walker.checkBackward();
\r
1856 checkEndOfBlock : function()
\r
1858 var endContainer = this.endContainer,
\r
1859 endOffset = this.endOffset;
\r
1861 // [IE] Special handling for range end in text with a following NBSP,
\r
1862 // we it to be isolated, for bogus check.
\r
1863 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT )
\r
1865 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
\r
1866 if ( nbspRegExp.test( textAfter ) )
\r
1867 this.trim( 1, 0 );
\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();
\r
1882 return walker.checkForward();
\r
1886 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
\r
1887 * @param {Function} evaluator Function used as the walker's evaluator.
\r
1888 * @param {Function} [guard] Function used as the walker's guard.
\r
1889 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
\r
1890 * default to the root editable if not defined.
\r
1892 * @return {CKEDITOR.dom.element|null} The returned node from the traversal.
\r
1894 getPreviousNode : function( evaluator, guard, boundary ) {
\r
1896 var walkerRange = this.clone();
\r
1897 walkerRange.collapse( 1 );
\r
1898 walkerRange.setStartAt( boundary || this.document.getBody(), CKEDITOR.POSITION_AFTER_START );
\r
1900 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1901 walker.evaluator = evaluator;
\r
1902 walker.guard = guard;
\r
1903 return walker.previous();
\r
1907 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
\r
1908 * @param {Function} evaluator Function used as the walker's evaluator.
\r
1909 * @param {Function} [guard] Function used as the walker's guard.
\r
1910 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
\r
1911 * default to the root editable if not defined.
\r
1913 * @return {CKEDITOR.dom.element|null} The returned node from the traversal.
\r
1915 getNextNode: function( evaluator, guard, boundary )
\r
1917 var walkerRange = this.clone();
\r
1918 walkerRange.collapse();
\r
1919 walkerRange.setEndAt( boundary || this.document.getBody(), CKEDITOR.POSITION_BEFORE_END );
\r
1921 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1922 walker.evaluator = evaluator;
\r
1923 walker.guard = guard;
\r
1924 return walker.next();
\r
1927 checkReadOnly : ( function()
\r
1929 function checkNodesEditable( node, anotherEnd )
\r
1933 if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
1935 if ( node.getAttribute( 'contentEditable' ) == 'false'
\r
1936 && !node.data( 'cke-editable' ) )
\r
1940 // Range enclosed entirely in an editable element.
\r
1941 else if ( node.is( 'html' )
\r
1942 || node.getAttribute( 'contentEditable' ) == 'true'
\r
1943 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
\r
1948 node = node.getParent();
\r
1956 var startNode = this.startContainer,
\r
1957 endNode = this.endContainer;
\r
1959 // Check if elements path at both boundaries are editable.
\r
1960 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
\r
1965 * Moves the range boundaries to the first/end editing point inside an
\r
1966 * element. For example, in an element tree like
\r
1967 * "<p><b><i></i></b> Text</p>", the start editing point is
\r
1968 * "<p><b><i>^</i></b> Text</p>" (inside <i>).
\r
1969 * @param {CKEDITOR.dom.element} el The element into which look for the
\r
1971 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
\r
1973 moveToElementEditablePosition : function( el, isMoveToEnd )
\r
1975 function nextDFS( node, childOnly )
\r
1979 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
\r
1980 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
\r
1982 if ( !childOnly && !next )
\r
1983 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
\r
1988 // Handle non-editable element e.g. HR.
\r
1989 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) )
\r
1991 this.moveToPosition( el, isMoveToEnd ?
\r
1992 CKEDITOR.POSITION_AFTER_END :
\r
1993 CKEDITOR.POSITION_BEFORE_START );
\r
2001 // Stop immediately if we've found a text node.
\r
2002 if ( el.type == CKEDITOR.NODE_TEXT )
\r
2004 // Put cursor before block filler.
\r
2005 if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
\r
2006 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
\r
2008 this.moveToPosition( el, isMoveToEnd ?
\r
2009 CKEDITOR.POSITION_AFTER_END :
\r
2010 CKEDITOR.POSITION_BEFORE_START );
\r
2015 // If an editable element is found, move inside it, but not stop the searching.
\r
2016 if ( el.type == CKEDITOR.NODE_ELEMENT )
\r
2018 if ( el.isEditable() )
\r
2020 this.moveToPosition( el, isMoveToEnd ?
\r
2021 CKEDITOR.POSITION_BEFORE_END :
\r
2022 CKEDITOR.POSITION_AFTER_START );
\r
2025 // Put cursor before padding block br.
\r
2026 else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() )
\r
2027 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
\r
2030 el = nextDFS( el, found );
\r
2037 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
2039 moveToElementEditStart : function( target )
\r
2041 return this.moveToElementEditablePosition( target );
\r
2045 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
2047 moveToElementEditEnd : function( target )
\r
2049 return this.moveToElementEditablePosition( target, true );
\r
2053 * Get the single node enclosed within the range if there's one.
\r
2055 getEnclosedNode : function()
\r
2057 var walkerRange = this.clone();
\r
2059 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
\r
2060 walkerRange.optimize();
\r
2061 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT
\r
2062 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
\r
2065 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
2066 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),
\r
2067 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
\r
2068 evaluator = function( node )
\r
2070 return isNotWhitespaces( node ) && isNotBookmarks( node );
\r
2072 walkerRange.evaluator = evaluator;
\r
2073 var node = walker.next();
\r
2075 return node && node.equals( walker.previous() ) ? node : null;
\r
2078 getTouchedStartNode : function()
\r
2080 var container = this.startContainer ;
\r
2082 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2083 return container ;
\r
2085 return container.getChild( this.startOffset ) || container ;
\r
2088 getTouchedEndNode : function()
\r
2090 var container = this.endContainer ;
\r
2092 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
2093 return container ;
\r
2095 return container.getChild( this.endOffset - 1 ) || container ;
\r
2100 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text"
\r
2101 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^"
\r
2102 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text"
\r
2103 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text"
\r
2105 CKEDITOR.ENLARGE_ELEMENT = 1;
\r
2106 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
\r
2107 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
\r
2109 // Check boundary types.
\r
2110 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
\r
2111 CKEDITOR.START = 1;
\r
2113 CKEDITOR.STARTEND = 3;
\r
2115 // Shrink range types.
\r
2116 // @see CKEDITOR.dom.range.prototype.shrink
\r
2117 CKEDITOR.SHRINK_ELEMENT = 1;
\r
2118 CKEDITOR.SHRINK_TEXT = 2;
\r