2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license
\r
9 CKEDITOR.dom.range = function( document )
\r
11 this.startContainer = null;
\r
12 this.startOffset = null;
\r
13 this.endContainer = null;
\r
14 this.endOffset = null;
\r
15 this.collapsed = true;
\r
17 this.document = document;
\r
22 // Updates the "collapsed" property for the given range object.
\r
23 var updateCollapsed = function( range )
\r
26 range.startContainer &&
\r
27 range.endContainer &&
\r
28 range.startContainer.equals( range.endContainer ) &&
\r
29 range.startOffset == range.endOffset );
\r
32 // This is a shared function used to delete, extract and clone the range
\r
35 var execContentsAction = function( range, action, docFrag, mergeThen )
\r
37 range.optimizeBookmark();
\r
39 var startNode = range.startContainer;
\r
40 var endNode = range.endContainer;
\r
42 var startOffset = range.startOffset;
\r
43 var endOffset = range.endOffset;
\r
45 var removeStartNode;
\r
48 // For text containers, we must simply split the node and point to the
\r
49 // second part. The removal will be handled by the rest of the code .
\r
50 if ( endNode.type == CKEDITOR.NODE_TEXT )
\r
51 endNode = endNode.split( endOffset );
\r
54 // If the end container has children and the offset is pointing
\r
55 // to a child, then we should start from it.
\r
56 if ( endNode.getChildCount() > 0 )
\r
58 // If the offset points after the last node.
\r
59 if ( endOffset >= endNode.getChildCount() )
\r
61 // Let's create a temporary node and mark it for removal.
\r
62 endNode = endNode.append( range.document.createText( '' ) );
\r
63 removeEndNode = true;
\r
66 endNode = endNode.getChild( endOffset );
\r
70 // For text containers, we must simply split the node. The removal will
\r
71 // be handled by the rest of the code .
\r
72 if ( startNode.type == CKEDITOR.NODE_TEXT )
\r
74 startNode.split( startOffset );
\r
76 // In cases the end node is the same as the start node, the above
\r
77 // splitting will also split the end, so me must move the end to
\r
78 // the second part of the split.
\r
79 if ( startNode.equals( endNode ) )
\r
80 endNode = startNode.getNext();
\r
84 // If the start container has children and the offset is pointing
\r
85 // to a child, then we should start from its previous sibling.
\r
87 // If the offset points to the first node, we don't have a
\r
88 // sibling, so let's use the first one, but mark it for removal.
\r
91 // Let's create a temporary node and mark it for removal.
\r
92 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) );
\r
93 removeStartNode = true;
\r
95 else if ( startOffset >= startNode.getChildCount() )
\r
97 // Let's create a temporary node and mark it for removal.
\r
98 startNode = startNode.append( range.document.createText( '' ) );
\r
99 removeStartNode = true;
\r
102 startNode = startNode.getChild( startOffset ).getPrevious();
\r
105 // Get the parent nodes tree for the start and end boundaries.
\r
106 var startParents = startNode.getParents();
\r
107 var endParents = endNode.getParents();
\r
109 // Compare them, to find the top most siblings.
\r
110 var i, topStart, topEnd;
\r
112 for ( i = 0 ; i < startParents.length ; i++ )
\r
114 topStart = startParents[ i ];
\r
115 topEnd = endParents[ i ];
\r
117 // The compared nodes will match until we find the top most
\r
118 // siblings (different nodes that have the same parent).
\r
119 // "i" will hold the index in the parents array for the top
\r
121 if ( !topStart.equals( topEnd ) )
\r
125 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling;
\r
127 // Remove all successive sibling nodes for every node in the
\r
128 // startParents tree.
\r
129 for ( var j = i ; j < startParents.length ; j++ )
\r
131 levelStartNode = startParents[j];
\r
133 // For Extract and Clone, we must clone this level.
\r
134 if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete
\r
135 levelClone = clone.append( levelStartNode.clone() );
\r
137 currentNode = levelStartNode.getNext();
\r
139 while ( currentNode )
\r
141 // Stop processing when the current node matches a node in the
\r
142 // endParents tree or if it is the endNode.
\r
143 if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) )
\r
146 // Cache the next sibling.
\r
147 currentSibling = currentNode.getNext();
\r
149 // If cloning, just clone it.
\r
150 if ( action == 2 ) // 2 = Clone
\r
151 clone.append( currentNode.clone( true ) );
\r
154 // Both Delete and Extract will remove the node.
\r
155 currentNode.remove();
\r
157 // When Extracting, move the removed node to the docFrag.
\r
158 if ( action == 1 ) // 1 = Extract
\r
159 clone.append( currentNode );
\r
162 currentNode = currentSibling;
\r
166 clone = levelClone;
\r
171 // Remove all previous sibling nodes for every node in the
\r
172 // endParents tree.
\r
173 for ( var k = i ; k < endParents.length ; k++ )
\r
175 levelStartNode = endParents[ k ];
\r
177 // For Extract and Clone, we must clone this level.
\r
178 if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete
\r
179 levelClone = clone.append( levelStartNode.clone() );
\r
181 // The processing of siblings may have already been done by the parent.
\r
182 if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode )
\r
184 currentNode = levelStartNode.getPrevious();
\r
186 while ( currentNode )
\r
188 // Stop processing when the current node matches a node in the
\r
189 // startParents tree or if it is the startNode.
\r
190 if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) )
\r
193 // Cache the next sibling.
\r
194 currentSibling = currentNode.getPrevious();
\r
196 // If cloning, just clone it.
\r
197 if ( action == 2 ) // 2 = Clone
\r
198 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ;
\r
201 // Both Delete and Extract will remove the node.
\r
202 currentNode.remove();
\r
204 // When Extracting, mode the removed node to the docFrag.
\r
205 if ( action == 1 ) // 1 = Extract
\r
206 clone.$.insertBefore( currentNode.$, clone.$.firstChild );
\r
209 currentNode = currentSibling;
\r
214 clone = levelClone;
\r
217 if ( action == 2 ) // 2 = Clone.
\r
219 // No changes in the DOM should be done, so fix the split text (if any).
\r
221 var startTextNode = range.startContainer;
\r
222 if ( startTextNode.type == CKEDITOR.NODE_TEXT )
\r
224 startTextNode.$.data += startTextNode.$.nextSibling.data;
\r
225 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );
\r
228 var endTextNode = range.endContainer;
\r
229 if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling )
\r
231 endTextNode.$.data += endTextNode.$.nextSibling.data;
\r
232 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );
\r
237 // Collapse the range.
\r
239 // If a node has been partially selected, collapse the range between
\r
240 // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).
\r
241 if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) )
\r
243 var endIndex = topEnd.getIndex();
\r
245 // If the start node is to be removed, we must correct the
\r
246 // index to reflect the removal.
\r
247 if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )
\r
250 // Merge splitted parents.
\r
251 if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT )
\r
253 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
\r
254 'data-cke-bookmark="1" style="display:none"> </span>', range.document );
\r
255 span.insertAfter( topStart );
\r
256 topStart.mergeSiblings( false );
\r
257 range.moveToBookmark( { startNode : span } );
\r
260 range.setStart( topEnd.getParent(), endIndex );
\r
263 // Collapse it to the start.
\r
264 range.collapse( true );
\r
267 // Cleanup any marked node.
\r
268 if ( removeStartNode )
\r
269 startNode.remove();
\r
271 if ( removeEndNode && endNode.$.parentNode )
\r
275 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
277 // Creates the appropriate node evaluator for the dom walker used inside
\r
278 // check(Start|End)OfBlock.
\r
279 function getCheckStartEndBlockEvalFunction( isStart )
\r
281 var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );
\r
282 return function( node )
\r
284 // First ignore bookmark nodes.
\r
285 if ( bookmarkEvaluator( node ) )
\r
288 if ( node.type == CKEDITOR.NODE_TEXT )
\r
290 // If there's any visible text, then we're not at the start.
\r
291 if ( CKEDITOR.tools.trim( node.getText() ).length )
\r
294 else if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
296 // If there are non-empty inline elements (e.g. <img />), then we're not
\r
298 if ( !inlineChildReqElements[ node.getName() ] )
\r
300 // If we're working at the end-of-block, forgive the first <br /> in non-IE
\r
302 if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )
\r
312 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
\r
313 // text node and non-empty elements unless it's being bookmark text.
\r
314 function elementBoundaryEval( node )
\r
316 // Reject any text node unless it's being bookmark
\r
317 // OR it's spaces. (#3883)
\r
318 return node.type != CKEDITOR.NODE_TEXT
\r
319 && node.getName() in CKEDITOR.dtd.$removeEmpty
\r
320 || !CKEDITOR.tools.trim( node.getText() )
\r
321 || !!node.getParent().data( 'cke-bookmark' );
\r
324 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
\r
325 bookmarkEval = new CKEDITOR.dom.walker.bookmark();
\r
327 function nonWhitespaceOrBookmarkEval( node )
\r
329 // Whitespaces and bookmark nodes are to be ignored.
\r
330 return !whitespaceEval( node ) && !bookmarkEval( node );
\r
333 CKEDITOR.dom.range.prototype =
\r
337 var clone = new CKEDITOR.dom.range( this.document );
\r
339 clone.startContainer = this.startContainer;
\r
340 clone.startOffset = this.startOffset;
\r
341 clone.endContainer = this.endContainer;
\r
342 clone.endOffset = this.endOffset;
\r
343 clone.collapsed = this.collapsed;
\r
348 collapse : function( toStart )
\r
352 this.endContainer = this.startContainer;
\r
353 this.endOffset = this.startOffset;
\r
357 this.startContainer = this.endContainer;
\r
358 this.startOffset = this.endOffset;
\r
361 this.collapsed = true;
\r
365 * The content nodes of the range are cloned and added to a document fragment, which is returned.
\r
366 * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting).
\r
368 cloneContents : function()
\r
370 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
372 if ( !this.collapsed )
\r
373 execContentsAction( this, 2, docFrag );
\r
379 * Deletes the content nodes of the range permanently from the DOM tree.
\r
380 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
382 deleteContents : function( mergeThen )
\r
384 if ( this.collapsed )
\r
387 execContentsAction( this, 0, null, mergeThen );
\r
391 * The content nodes of the range are cloned and added to a document fragment,
\r
392 * meanwhile they're removed permanently from the DOM tree.
\r
393 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
\r
395 extractContents : function( mergeThen )
\r
397 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
\r
399 if ( !this.collapsed )
\r
400 execContentsAction( this, 1, docFrag, mergeThen );
\r
406 * Creates a bookmark object, which can be later used to restore the
\r
407 * range by using the moveToBookmark function.
\r
408 * This is an "intrusive" way to create a bookmark. It includes <span> tags
\r
409 * in the range boundaries. The advantage of it is that it is possible to
\r
410 * handle DOM mutations when moving back to the bookmark.
\r
411 * Attention: the inclusion of nodes in the DOM is a design choice and
\r
412 * should not be changed as there are other points in the code that may be
\r
413 * using those nodes to perform operations. See GetBookmarkNode.
\r
414 * @param {Boolean} [serializable] Indicates that the bookmark nodes
\r
415 * must contain ids, which can be used to restore the range even
\r
416 * when these nodes suffer mutations (like a clonation or innerHTML
\r
418 * @returns {Object} And object representing a bookmark.
\r
420 createBookmark : function( serializable )
\r
422 var startNode, endNode;
\r
425 var collapsed = this.collapsed;
\r
427 startNode = this.document.createElement( 'span' );
\r
428 startNode.data( 'cke-bookmark', 1 );
\r
429 startNode.setStyle( 'display', 'none' );
\r
431 // For IE, it must have something inside, otherwise it may be
\r
432 // removed during DOM operations.
\r
433 startNode.setHtml( ' ' );
\r
435 if ( serializable )
\r
437 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
\r
438 startNode.setAttribute( 'id', baseId + 'S' );
\r
441 // If collapsed, the endNode will not be created.
\r
444 endNode = startNode.clone();
\r
445 endNode.setHtml( ' ' );
\r
447 if ( serializable )
\r
448 endNode.setAttribute( 'id', baseId + 'E' );
\r
450 clone = this.clone();
\r
452 clone.insertNode( endNode );
\r
455 clone = this.clone();
\r
456 clone.collapse( true );
\r
457 clone.insertNode( startNode );
\r
459 // Update the range position.
\r
462 this.setStartAfter( startNode );
\r
463 this.setEndBefore( endNode );
\r
466 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
\r
469 startNode : serializable ? baseId + 'S' : startNode,
\r
470 endNode : serializable ? baseId + 'E' : endNode,
\r
471 serializable : serializable,
\r
472 collapsed : collapsed
\r
477 * Creates a "non intrusive" and "mutation sensible" bookmark. This
\r
478 * kind of bookmark should be used only when the DOM is supposed to
\r
479 * remain stable after its creation.
\r
480 * @param {Boolean} [normalized] Indicates that the bookmark must
\r
481 * normalized. When normalized, the successive text nodes are
\r
482 * considered a single node. To sucessful load a normalized
\r
483 * bookmark, the DOM tree must be also normalized before calling
\r
485 * @returns {Object} An object representing the bookmark.
\r
487 createBookmark2 : function( normalized )
\r
489 var startContainer = this.startContainer,
\r
490 endContainer = this.endContainer;
\r
492 var startOffset = this.startOffset,
\r
493 endOffset = this.endOffset;
\r
495 var collapsed = this.collapsed;
\r
497 var child, previous;
\r
499 // If there is no range then get out of here.
\r
500 // It happens on initial load in Safari #962 and if the editor it's
\r
501 // hidden also in Firefox
\r
502 if ( !startContainer || !endContainer )
\r
503 return { start : 0, end : 0 };
\r
507 // Find out if the start is pointing to a text node that will
\r
509 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
\r
511 child = startContainer.getChild( startOffset );
\r
513 // In this case, move the start information to that text
\r
515 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
516 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
518 startContainer = child;
\r
523 // Normalize the start.
\r
524 while ( startContainer.type == CKEDITOR.NODE_TEXT
\r
525 && ( previous = startContainer.getPrevious() )
\r
526 && previous.type == CKEDITOR.NODE_TEXT )
\r
528 startContainer = previous;
\r
529 startOffset += previous.getLength();
\r
532 // Process the end only if not normalized.
\r
535 // Find out if the start is pointing to a text node that
\r
536 // will be normalized.
\r
537 if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
\r
539 child = endContainer.getChild( endOffset );
\r
541 // In this case, move the start information to that
\r
543 if ( child && child.type == CKEDITOR.NODE_TEXT
\r
544 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
\r
546 endContainer = child;
\r
551 // Normalize the end.
\r
552 while ( endContainer.type == CKEDITOR.NODE_TEXT
\r
553 && ( previous = endContainer.getPrevious() )
\r
554 && previous.type == CKEDITOR.NODE_TEXT )
\r
556 endContainer = previous;
\r
557 endOffset += previous.getLength();
\r
563 start : startContainer.getAddress( normalized ),
\r
564 end : collapsed ? null : endContainer.getAddress( normalized ),
\r
565 startOffset : startOffset,
\r
566 endOffset : endOffset,
\r
567 normalized : normalized,
\r
568 collapsed : collapsed,
\r
569 is2 : true // It's a createBookmark2 bookmark.
\r
573 moveToBookmark : function( bookmark )
\r
575 if ( bookmark.is2 ) // Created with createBookmark2().
\r
577 // Get the start information.
\r
578 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
\r
579 startOffset = bookmark.startOffset;
\r
581 // Get the end information.
\r
582 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
\r
583 endOffset = bookmark.endOffset;
\r
585 // Set the start boundary.
\r
586 this.setStart( startContainer, startOffset );
\r
588 // Set the end boundary. If not available, collapse it.
\r
589 if ( endContainer )
\r
590 this.setEnd( endContainer, endOffset );
\r
592 this.collapse( true );
\r
594 else // Created with createBookmark().
\r
596 var serializable = bookmark.serializable,
\r
597 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
\r
598 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
\r
600 // Set the range start at the bookmark start node position.
\r
601 this.setStartBefore( startNode );
\r
603 // Remove it, because it may interfere in the setEndBefore call.
\r
604 startNode.remove();
\r
606 // Set the range end at the bookmark end node position, or simply
\r
607 // collapse it if it is not available.
\r
610 this.setEndBefore( endNode );
\r
614 this.collapse( true );
\r
618 getBoundaryNodes : function()
\r
620 var startNode = this.startContainer,
\r
621 endNode = this.endContainer,
\r
622 startOffset = this.startOffset,
\r
623 endOffset = this.endOffset,
\r
626 if ( startNode.type == CKEDITOR.NODE_ELEMENT )
\r
628 childCount = startNode.getChildCount();
\r
629 if ( childCount > startOffset )
\r
630 startNode = startNode.getChild( startOffset );
\r
631 else if ( childCount < 1 )
\r
632 startNode = startNode.getPreviousSourceNode();
\r
633 else // startOffset > childCount but childCount is not 0
\r
635 // Try to take the node just after the current position.
\r
636 startNode = startNode.$;
\r
637 while ( startNode.lastChild )
\r
638 startNode = startNode.lastChild;
\r
639 startNode = new CKEDITOR.dom.node( startNode );
\r
641 // Normally we should take the next node in DFS order. But it
\r
642 // is also possible that we've already reached the end of
\r
644 startNode = startNode.getNextSourceNode() || startNode;
\r
647 if ( endNode.type == CKEDITOR.NODE_ELEMENT )
\r
649 childCount = endNode.getChildCount();
\r
650 if ( childCount > endOffset )
\r
651 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
\r
652 else if ( childCount < 1 )
\r
653 endNode = endNode.getPreviousSourceNode();
\r
654 else // endOffset > childCount but childCount is not 0
\r
656 // Try to take the node just before the current position.
\r
657 endNode = endNode.$;
\r
658 while ( endNode.lastChild )
\r
659 endNode = endNode.lastChild;
\r
660 endNode = new CKEDITOR.dom.node( endNode );
\r
664 // Sometimes the endNode will come right before startNode for collapsed
\r
665 // ranges. Fix it. (#3780)
\r
666 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
\r
667 startNode = endNode;
\r
669 return { startNode : startNode, endNode : endNode };
\r
673 * Find the node which fully contains the range.
\r
674 * @param includeSelf
\r
675 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.
\r
677 getCommonAncestor : function( includeSelf , ignoreTextNode )
\r
679 var start = this.startContainer,
\r
680 end = this.endContainer,
\r
683 if ( start.equals( end ) )
\r
686 && start.type == CKEDITOR.NODE_ELEMENT
\r
687 && this.startOffset == this.endOffset - 1 )
\r
688 ancestor = start.getChild( this.startOffset );
\r
693 ancestor = start.getCommonAncestor( end );
\r
695 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
\r
699 * Transforms the startContainer and endContainer properties from text
\r
700 * nodes to element nodes, whenever possible. This is actually possible
\r
701 * if either of the boundary containers point to a text node, and its
\r
702 * offset is set to zero, or after the last char in the node.
\r
704 optimize : function()
\r
706 var container = this.startContainer;
\r
707 var offset = this.startOffset;
\r
709 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
712 this.setStartBefore( container );
\r
713 else if ( offset >= container.getLength() )
\r
714 this.setStartAfter( container );
\r
717 container = this.endContainer;
\r
718 offset = this.endOffset;
\r
720 if ( container.type != CKEDITOR.NODE_ELEMENT )
\r
723 this.setEndBefore( container );
\r
724 else if ( offset >= container.getLength() )
\r
725 this.setEndAfter( container );
\r
730 * Move the range out of bookmark nodes if they'd been the container.
\r
732 optimizeBookmark: function()
\r
734 var startNode = this.startContainer,
\r
735 endNode = this.endContainer;
\r
737 if ( startNode.is && startNode.is( 'span' )
\r
738 && startNode.data( 'cke-bookmark' ) )
\r
739 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
\r
740 if ( endNode && endNode.is && endNode.is( 'span' )
\r
741 && endNode.data( 'cke-bookmark' ) )
\r
742 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
\r
745 trim : function( ignoreStart, ignoreEnd )
\r
747 var startContainer = this.startContainer,
\r
748 startOffset = this.startOffset,
\r
749 collapsed = this.collapsed;
\r
750 if ( ( !ignoreStart || collapsed )
\r
751 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
753 // If the offset is zero, we just insert the new node before
\r
755 if ( !startOffset )
\r
757 startOffset = startContainer.getIndex();
\r
758 startContainer = startContainer.getParent();
\r
760 // If the offset is at the end, we'll insert it after the text
\r
762 else if ( startOffset >= startContainer.getLength() )
\r
764 startOffset = startContainer.getIndex() + 1;
\r
765 startContainer = startContainer.getParent();
\r
767 // In other case, we split the text node and insert the new
\r
768 // node at the split point.
\r
771 var nextText = startContainer.split( startOffset );
\r
773 startOffset = startContainer.getIndex() + 1;
\r
774 startContainer = startContainer.getParent();
\r
776 // Check all necessity of updating the end boundary.
\r
777 if ( this.startContainer.equals( this.endContainer ) )
\r
778 this.setEnd( nextText, this.endOffset - this.startOffset );
\r
779 else if ( startContainer.equals( this.endContainer ) )
\r
780 this.endOffset += 1;
\r
783 this.setStart( startContainer, startOffset );
\r
787 this.collapse( true );
\r
792 var endContainer = this.endContainer;
\r
793 var endOffset = this.endOffset;
\r
795 if ( !( ignoreEnd || collapsed )
\r
796 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
798 // If the offset is zero, we just insert the new node before
\r
802 endOffset = endContainer.getIndex();
\r
803 endContainer = endContainer.getParent();
\r
805 // If the offset is at the end, we'll insert it after the text
\r
807 else if ( endOffset >= endContainer.getLength() )
\r
809 endOffset = endContainer.getIndex() + 1;
\r
810 endContainer = endContainer.getParent();
\r
812 // In other case, we split the text node and insert the new
\r
813 // node at the split point.
\r
816 endContainer.split( endOffset );
\r
818 endOffset = endContainer.getIndex() + 1;
\r
819 endContainer = endContainer.getParent();
\r
822 this.setEnd( endContainer, endOffset );
\r
827 * Expands the range so that partial units are completely contained.
\r
828 * @param unit {Number} The unit type to expand with.
\r
829 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
\r
831 enlarge : function( unit, excludeBrs )
\r
835 case CKEDITOR.ENLARGE_ELEMENT :
\r
837 if ( this.collapsed )
\r
840 // Get the common ancestor.
\r
841 var commonAncestor = this.getCommonAncestor();
\r
843 var body = this.document.getBody();
\r
845 // For each boundary
\r
846 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
\r
847 // 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
849 var startTop, endTop;
\r
851 var enlargeable, sibling, commonReached;
\r
853 // Indicates that the node can be added only if whitespace
\r
854 // is available before it.
\r
855 var needsWhiteSpace = false;
\r
859 // Process the start boundary.
\r
861 var container = this.startContainer;
\r
862 var offset = this.startOffset;
\r
864 if ( container.type == CKEDITOR.NODE_TEXT )
\r
868 // Check if there is any non-space text before the
\r
869 // offset. Otherwise, container is null.
\r
870 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
\r
872 // If we found only whitespace in the node, it
\r
873 // means that we'll need more whitespace to be able
\r
874 // to expand. For example, <i> can be expanded in
\r
875 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
876 needsWhiteSpace = !!container;
\r
881 if ( !( sibling = container.getPrevious() ) )
\r
882 enlargeable = container.getParent();
\r
887 // If we have offset, get the node preceeding it as the
\r
888 // first sibling to be checked.
\r
890 sibling = container.getChild( offset - 1 ) || container.getLast();
\r
892 // If there is no sibling, mark the container to be
\r
895 enlargeable = container;
\r
898 while ( enlargeable || sibling )
\r
900 if ( enlargeable && !sibling )
\r
902 // If we reached the common ancestor, mark the flag
\r
904 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
905 commonReached = true;
\r
907 if ( !body.contains( enlargeable ) )
\r
910 // If we don't need space or this element breaks
\r
911 // the line, then enlarge it.
\r
912 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
914 needsWhiteSpace = false;
\r
916 // If the common ancestor has been reached,
\r
917 // we'll not enlarge it immediately, but just
\r
918 // mark it to be enlarged later if the end
\r
919 // boundary also enlarges it.
\r
920 if ( commonReached )
\r
921 startTop = enlargeable;
\r
923 this.setStartBefore( enlargeable );
\r
926 sibling = enlargeable.getPrevious();
\r
929 // Check all sibling nodes preceeding the enlargeable
\r
930 // node. The node wil lbe enlarged only if none of them
\r
934 // This flag indicates that this node has
\r
935 // whitespaces at the end.
\r
936 isWhiteSpace = false;
\r
938 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
940 siblingText = sibling.getText();
\r
942 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
945 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
\r
949 // If this is a visible element.
\r
950 // We need to check for the bookmark attribute because IE insists on
\r
951 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
952 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
953 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
955 // We'll accept it only if we need
\r
956 // whitespace, and this is an inline
\r
957 // element with whitespace only.
\r
958 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
960 // It must contains spaces and inline elements only.
\r
962 siblingText = sibling.getText();
\r
964 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
\r
968 var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
\r
969 for ( var i = 0, child ; child = allChildren[ i++ ] ; )
\r
971 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
980 isWhiteSpace = !!siblingText.length;
\r
987 // A node with whitespaces has been found.
\r
988 if ( isWhiteSpace )
\r
990 // Enlarge the last enlargeable node, if we
\r
991 // were waiting for spaces.
\r
992 if ( needsWhiteSpace )
\r
994 if ( commonReached )
\r
995 startTop = enlargeable;
\r
996 else if ( enlargeable )
\r
997 this.setStartBefore( enlargeable );
\r
1000 needsWhiteSpace = true;
\r
1005 var next = sibling.getPrevious();
\r
1007 if ( !enlargeable && !next )
\r
1009 // Set the sibling as enlargeable, so it's
\r
1010 // parent will be get later outside this while.
\r
1011 enlargeable = sibling;
\r
1020 // If sibling has been set to null, then we
\r
1021 // need to stop enlarging.
\r
1022 enlargeable = null;
\r
1026 if ( enlargeable )
\r
1027 enlargeable = enlargeable.getParent();
\r
1030 // Process the end boundary. This is basically the same
\r
1031 // code used for the start boundary, with small changes to
\r
1032 // make it work in the oposite side (to the right). This
\r
1033 // makes it difficult to reuse the code here. So, fixes to
\r
1034 // the above code are likely to be replicated here.
\r
1036 container = this.endContainer;
\r
1037 offset = this.endOffset;
\r
1039 // Reset the common variables.
\r
1040 enlargeable = sibling = null;
\r
1041 commonReached = needsWhiteSpace = false;
\r
1043 if ( container.type == CKEDITOR.NODE_TEXT )
\r
1045 // Check if there is any non-space text after the
\r
1046 // offset. Otherwise, container is null.
\r
1047 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;
\r
1049 // If we found only whitespace in the node, it
\r
1050 // means that we'll need more whitespace to be able
\r
1051 // to expand. For example, <i> can be expanded in
\r
1052 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
\r
1053 needsWhiteSpace = !( container && container.getLength() );
\r
1057 if ( !( sibling = container.getNext() ) )
\r
1058 enlargeable = container.getParent();
\r
1063 // Get the node right after the boudary to be checked
\r
1065 sibling = container.getChild( offset );
\r
1068 enlargeable = container;
\r
1071 while ( enlargeable || sibling )
\r
1073 if ( enlargeable && !sibling )
\r
1075 if ( !commonReached && enlargeable.equals( commonAncestor ) )
\r
1076 commonReached = true;
\r
1078 if ( !body.contains( enlargeable ) )
\r
1081 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
\r
1083 needsWhiteSpace = false;
\r
1085 if ( commonReached )
\r
1086 endTop = enlargeable;
\r
1087 else if ( enlargeable )
\r
1088 this.setEndAfter( enlargeable );
\r
1091 sibling = enlargeable.getNext();
\r
1096 isWhiteSpace = false;
\r
1098 if ( sibling.type == CKEDITOR.NODE_TEXT )
\r
1100 siblingText = sibling.getText();
\r
1102 if ( /[^\s\ufeff]/.test( siblingText ) )
\r
1105 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
\r
1109 // If this is a visible element.
\r
1110 // We need to check for the bookmark attribute because IE insists on
\r
1111 // rendering the display:none nodes we use for bookmarks. (#3363)
\r
1112 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
\r
1113 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) )
\r
1115 // We'll accept it only if we need
\r
1116 // whitespace, and this is an inline
\r
1117 // element with whitespace only.
\r
1118 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
\r
1120 // It must contains spaces and inline elements only.
\r
1122 siblingText = sibling.getText();
\r
1124 if ( (/[^\s\ufeff]/).test( siblingText ) )
\r
1128 allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
\r
1129 for ( i = 0 ; child = allChildren[ i++ ] ; )
\r
1131 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
\r
1140 isWhiteSpace = !!siblingText.length;
\r
1147 if ( isWhiteSpace )
\r
1149 if ( needsWhiteSpace )
\r
1151 if ( commonReached )
\r
1152 endTop = enlargeable;
\r
1154 this.setEndAfter( enlargeable );
\r
1160 next = sibling.getNext();
\r
1162 if ( !enlargeable && !next )
\r
1164 enlargeable = sibling;
\r
1173 // If sibling has been set to null, then we
\r
1174 // need to stop enlarging.
\r
1175 enlargeable = null;
\r
1179 if ( enlargeable )
\r
1180 enlargeable = enlargeable.getParent();
\r
1183 // If the common ancestor can be enlarged by both boundaries, then include it also.
\r
1184 if ( startTop && endTop )
\r
1186 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
\r
1188 this.setStartBefore( commonAncestor );
\r
1189 this.setEndAfter( commonAncestor );
\r
1193 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
\r
1194 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
\r
1196 // Enlarging the start boundary.
\r
1197 var walkerRange = new CKEDITOR.dom.range( this.document );
\r
1199 body = this.document.getBody();
\r
1201 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
1202 walkerRange.setEnd( this.startContainer, this.startOffset );
\r
1204 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1205 blockBoundary, // The node on which the enlarging should stop.
\r
1206 tailBr, // In case BR as block boundary.
\r
1207 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary(
\r
1208 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),
\r
1209 // Record the encountered 'blockBoundary' for later use.
\r
1210 boundaryGuard = function( node )
\r
1212 var retval = notBlockBoundary( node );
\r
1214 blockBoundary = node;
\r
1217 // Record the encounted 'tailBr' for later use.
\r
1218 tailBrGuard = function( node )
\r
1220 var retval = boundaryGuard( node );
\r
1221 if ( !retval && node.is && node.is( 'br' ) )
\r
1226 walker.guard = boundaryGuard;
\r
1228 enlargeable = walker.lastBackward();
\r
1230 // It's the body which stop the enlarging if no block boundary found.
\r
1231 blockBoundary = blockBoundary || body;
\r
1233 // Start the range either after the end of found block (<p>...</p>[text)
\r
1234 // or at the start of block (<p>[text...), by comparing the document position
\r
1235 // with 'enlargeable' node.
\r
1238 !blockBoundary.is( 'br' ) &&
\r
1239 ( !enlargeable && this.checkStartOfBlock()
\r
1240 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1241 CKEDITOR.POSITION_AFTER_START :
\r
1242 CKEDITOR.POSITION_AFTER_END );
\r
1244 // Enlarging the end boundary.
\r
1245 walkerRange = this.clone();
\r
1246 walkerRange.collapse();
\r
1247 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
1248 walker = new CKEDITOR.dom.walker( walkerRange );
\r
1250 // tailBrGuard only used for on range end.
\r
1251 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?
\r
1252 tailBrGuard : boundaryGuard;
\r
1253 blockBoundary = null;
\r
1254 // End the range right before the block boundary node.
\r
1256 enlargeable = walker.lastForward();
\r
1258 // It's the body which stop the enlarging if no block boundary found.
\r
1259 blockBoundary = blockBoundary || body;
\r
1261 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
\r
1262 // by comparing the document position with 'enlargeable' node.
\r
1265 ( !enlargeable && this.checkEndOfBlock()
\r
1266 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
\r
1267 CKEDITOR.POSITION_BEFORE_END :
\r
1268 CKEDITOR.POSITION_BEFORE_START );
\r
1269 // We must include the <br> at the end of range if there's
\r
1270 // one and we're expanding list item contents
\r
1272 this.setEndAfter( tailBr );
\r
1277 * Descrease the range to make sure that boundaries
\r
1278 * always anchor beside text nodes or innermost element.
\r
1279 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.
\r
1281 * <dt>CKEDITOR.SHRINK_ELEMENT</dt>
\r
1282 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>
\r
1283 * <dt>CKEDITOR.SHRINK_TEXT</dt>
\r
1284 * <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
1286 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
\r
1288 shrink : function( mode, selectContents )
\r
1290 // Unable to shrink a collapsed range.
\r
1291 if ( !this.collapsed )
\r
1293 mode = mode || CKEDITOR.SHRINK_TEXT;
\r
1295 var walkerRange = this.clone();
\r
1297 var startContainer = this.startContainer,
\r
1298 endContainer = this.endContainer,
\r
1299 startOffset = this.startOffset,
\r
1300 endOffset = this.endOffset,
\r
1301 collapsed = this.collapsed;
\r
1303 // Whether the start/end boundary is moveable.
\r
1304 var moveStart = 1,
\r
1307 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1309 if ( !startOffset )
\r
1310 walkerRange.setStartBefore( startContainer );
\r
1311 else if ( startOffset >= startContainer.getLength( ) )
\r
1312 walkerRange.setStartAfter( startContainer );
\r
1315 // Enlarge the range properly to avoid walker making
\r
1316 // DOM changes caused by triming the text nodes later.
\r
1317 walkerRange.setStartBefore( startContainer );
\r
1322 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
\r
1325 walkerRange.setEndBefore( endContainer );
\r
1326 else if ( endOffset >= endContainer.getLength( ) )
\r
1327 walkerRange.setEndAfter( endContainer );
\r
1330 walkerRange.setEndAfter( endContainer );
\r
1335 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1336 isBookmark = CKEDITOR.dom.walker.bookmark();
\r
1338 walker.evaluator = function( node )
\r
1340 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
\r
1341 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
\r
1344 var currentElement;
\r
1345 walker.guard = function( node, movingOut )
\r
1347 if ( isBookmark( node ) )
\r
1350 // Stop when we're shrink in element mode while encountering a text node.
\r
1351 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
\r
1354 // Stop when we've already walked "through" an element.
\r
1355 if ( movingOut && node.equals( currentElement ) )
\r
1358 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
\r
1359 currentElement = node;
\r
1366 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
\r
1367 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
\r
1373 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
\r
1374 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
\r
1377 return !!( moveStart || moveEnd );
\r
1382 * Inserts a node at the start of the range. The range will be expanded
\r
1383 * the contain the node.
\r
1385 insertNode : function( node )
\r
1387 this.optimizeBookmark();
\r
1388 this.trim( false, true );
\r
1390 var startContainer = this.startContainer;
\r
1391 var startOffset = this.startOffset;
\r
1393 var nextNode = startContainer.getChild( startOffset );
\r
1396 node.insertBefore( nextNode );
\r
1398 startContainer.append( node );
\r
1400 // Check if we need to update the end boundary.
\r
1401 if ( node.getParent().equals( this.endContainer ) )
\r
1404 // Expand the range to embrace the new node.
\r
1405 this.setStartBefore( node );
\r
1408 moveToPosition : function( node, position )
\r
1410 this.setStartAt( node, position );
\r
1411 this.collapse( true );
\r
1414 selectNodeContents : function( node )
\r
1416 this.setStart( node, 0 );
\r
1417 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
\r
1421 * Sets the start position of a Range.
\r
1422 * @param {CKEDITOR.dom.node} startNode The node to start the range.
\r
1423 * @param {Number} startOffset An integer greater than or equal to zero
\r
1424 * representing the offset for the start of the range from the start
\r
1427 setStart : function( startNode, startOffset )
\r
1429 // W3C requires a check for the new position. If it is after the end
\r
1430 // boundary, the range should be collapsed to the new start. It seams
\r
1431 // we will not need this check for our use of this class so we can
\r
1432 // ignore it for now.
\r
1434 // Fixing invalid range start inside dtd empty elements.
\r
1435 if( startNode.type == CKEDITOR.NODE_ELEMENT
\r
1436 && CKEDITOR.dtd.$empty[ startNode.getName() ] )
\r
1437 startOffset = startNode.getIndex(), startNode = startNode.getParent();
\r
1439 this.startContainer = startNode;
\r
1440 this.startOffset = startOffset;
\r
1442 if ( !this.endContainer )
\r
1444 this.endContainer = startNode;
\r
1445 this.endOffset = startOffset;
\r
1448 updateCollapsed( this );
\r
1452 * Sets the end position of a Range.
\r
1453 * @param {CKEDITOR.dom.node} endNode The node to end the range.
\r
1454 * @param {Number} endOffset An integer greater than or equal to zero
\r
1455 * representing the offset for the end of the range from the start
\r
1458 setEnd : function( endNode, endOffset )
\r
1460 // W3C requires a check for the new position. If it is before the start
\r
1461 // boundary, the range should be collapsed to the new end. It seams we
\r
1462 // will not need this check for our use of this class so we can ignore
\r
1465 // Fixing invalid range end inside dtd empty elements.
\r
1466 if( endNode.type == CKEDITOR.NODE_ELEMENT
\r
1467 && CKEDITOR.dtd.$empty[ endNode.getName() ] )
\r
1468 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
\r
1470 this.endContainer = endNode;
\r
1471 this.endOffset = endOffset;
\r
1473 if ( !this.startContainer )
\r
1475 this.startContainer = endNode;
\r
1476 this.startOffset = endOffset;
\r
1479 updateCollapsed( this );
\r
1482 setStartAfter : function( node )
\r
1484 this.setStart( node.getParent(), node.getIndex() + 1 );
\r
1487 setStartBefore : function( node )
\r
1489 this.setStart( node.getParent(), node.getIndex() );
\r
1492 setEndAfter : function( node )
\r
1494 this.setEnd( node.getParent(), node.getIndex() + 1 );
\r
1497 setEndBefore : function( node )
\r
1499 this.setEnd( node.getParent(), node.getIndex() );
\r
1502 setStartAt : function( node, position )
\r
1504 switch( position )
\r
1506 case CKEDITOR.POSITION_AFTER_START :
\r
1507 this.setStart( node, 0 );
\r
1510 case CKEDITOR.POSITION_BEFORE_END :
\r
1511 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1512 this.setStart( node, node.getLength() );
\r
1514 this.setStart( node, node.getChildCount() );
\r
1517 case CKEDITOR.POSITION_BEFORE_START :
\r
1518 this.setStartBefore( node );
\r
1521 case CKEDITOR.POSITION_AFTER_END :
\r
1522 this.setStartAfter( node );
\r
1525 updateCollapsed( this );
\r
1528 setEndAt : function( node, position )
\r
1530 switch( position )
\r
1532 case CKEDITOR.POSITION_AFTER_START :
\r
1533 this.setEnd( node, 0 );
\r
1536 case CKEDITOR.POSITION_BEFORE_END :
\r
1537 if ( node.type == CKEDITOR.NODE_TEXT )
\r
1538 this.setEnd( node, node.getLength() );
\r
1540 this.setEnd( node, node.getChildCount() );
\r
1543 case CKEDITOR.POSITION_BEFORE_START :
\r
1544 this.setEndBefore( node );
\r
1547 case CKEDITOR.POSITION_AFTER_END :
\r
1548 this.setEndAfter( node );
\r
1551 updateCollapsed( this );
\r
1554 fixBlock : function( isStart, blockTag )
\r
1556 var bookmark = this.createBookmark(),
\r
1557 fixedBlock = this.document.createElement( blockTag );
\r
1559 this.collapse( isStart );
\r
1561 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
\r
1563 this.extractContents().appendTo( fixedBlock );
\r
1564 fixedBlock.trim();
\r
1566 if ( !CKEDITOR.env.ie )
\r
1567 fixedBlock.appendBogus();
\r
1569 this.insertNode( fixedBlock );
\r
1571 this.moveToBookmark( bookmark );
\r
1573 return fixedBlock;
\r
1576 splitBlock : function( blockTag )
\r
1578 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
\r
1579 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1581 var startBlockLimit = startPath.blockLimit,
\r
1582 endBlockLimit = endPath.blockLimit;
\r
1584 var startBlock = startPath.block,
\r
1585 endBlock = endPath.block;
\r
1587 var elementPath = null;
\r
1588 // Do nothing if the boundaries are in different block limits.
\r
1589 if ( !startBlockLimit.equals( endBlockLimit ) )
\r
1592 // Get or fix current blocks.
\r
1593 if ( blockTag != 'br' )
\r
1595 if ( !startBlock )
\r
1597 startBlock = this.fixBlock( true, blockTag );
\r
1598 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
\r
1602 endBlock = this.fixBlock( false, blockTag );
\r
1605 // Get the range position.
\r
1606 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
\r
1607 isEndOfBlock = endBlock && this.checkEndOfBlock();
\r
1609 // Delete the current contents.
\r
1610 // TODO: Why is 2.x doing CheckIsEmpty()?
\r
1611 this.deleteContents();
\r
1613 if ( startBlock && startBlock.equals( endBlock ) )
\r
1615 if ( isEndOfBlock )
\r
1617 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1618 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
\r
1621 else if ( isStartOfBlock )
\r
1623 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1624 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
\r
1625 startBlock = null;
\r
1629 endBlock = this.splitElement( startBlock );
\r
1631 // In Gecko, the last child node must be a bogus <br>.
\r
1632 // Note: bogus <br> added under <ul> or <ol> would cause
\r
1633 // lists to be incorrectly rendered.
\r
1634 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )
\r
1635 startBlock.appendBogus() ;
\r
1640 previousBlock : startBlock,
\r
1641 nextBlock : endBlock,
\r
1642 wasStartOfBlock : isStartOfBlock,
\r
1643 wasEndOfBlock : isEndOfBlock,
\r
1644 elementPath : elementPath
\r
1649 * Branch the specified element from the collapsed range position and
\r
1650 * place the caret between the two result branches.
\r
1651 * Note: The range must be collapsed and been enclosed by this element.
\r
1652 * @param {CKEDITOR.dom.element} element
\r
1653 * @return {CKEDITOR.dom.element} Root element of the new branch after the split.
\r
1655 splitElement : function( toSplit )
\r
1657 if ( !this.collapsed )
\r
1660 // Extract the contents of the block from the selection point to the end
\r
1661 // of its contents.
\r
1662 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
\r
1663 var documentFragment = this.extractContents();
\r
1665 // Duplicate the element after it.
\r
1666 var clone = toSplit.clone( false );
\r
1668 // Place the extracted contents into the duplicated element.
\r
1669 documentFragment.appendTo( clone );
\r
1670 clone.insertAfter( toSplit );
\r
1671 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
\r
1676 * Check whether a range boundary is at the inner boundary of a given
\r
1678 * @param {CKEDITOR.dom.element} element The target element to check.
\r
1679 * @param {Number} checkType The boundary to check for both the range
\r
1680 * and the element. It can be CKEDITOR.START or CKEDITOR.END.
\r
1681 * @returns {Boolean} "true" if the range boundary is at the inner
\r
1682 * boundary of the element.
\r
1684 checkBoundaryOfElement : function( element, checkType )
\r
1686 var checkStart = ( checkType == CKEDITOR.START );
\r
1688 // Create a copy of this range, so we can manipulate it for our checks.
\r
1689 var walkerRange = this.clone();
\r
1691 // Collapse the range at the proper size.
\r
1692 walkerRange.collapse( checkStart );
\r
1694 // Expand the range to element boundary.
\r
1695 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
\r
1696 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
\r
1698 // Create the walker, which will check if we have anything useful
\r
1700 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1701 walker.evaluator = elementBoundaryEval;
\r
1703 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
\r
1706 // Calls to this function may produce changes to the DOM. The range may
\r
1707 // be updated to reflect such changes.
\r
1708 checkStartOfBlock : function()
\r
1710 var startContainer = this.startContainer,
\r
1711 startOffset = this.startOffset;
\r
1713 // If the starting node is a text node, and non-empty before the offset,
\r
1714 // then we're surely not at the start of block.
\r
1715 if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
\r
1717 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
\r
1718 if ( textBefore.length )
\r
1722 // Antecipate the trim() call here, so the walker will not make
\r
1723 // changes to the DOM, which would not get reflected into this
\r
1724 // range otherwise.
\r
1727 // We need to grab the block element holding the start boundary, so
\r
1728 // let's use an element path for it.
\r
1729 var path = new CKEDITOR.dom.elementPath( this.startContainer );
\r
1731 // Creates a range starting at the block start until the range start.
\r
1732 var walkerRange = this.clone();
\r
1733 walkerRange.collapse( true );
\r
1734 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
\r
1736 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1737 walker.evaluator = getCheckStartEndBlockEvalFunction( true );
\r
1739 return walker.checkBackward();
\r
1742 checkEndOfBlock : function()
\r
1744 var endContainer = this.endContainer,
\r
1745 endOffset = this.endOffset;
\r
1747 // If the ending node is a text node, and non-empty after the offset,
\r
1748 // then we're surely not at the end of block.
\r
1749 if ( endContainer.type == CKEDITOR.NODE_TEXT )
\r
1751 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
\r
1752 if ( textAfter.length )
\r
1756 // Antecipate the trim() call here, so the walker will not make
\r
1757 // changes to the DOM, which would not get reflected into this
\r
1758 // range otherwise.
\r
1761 // We need to grab the block element holding the start boundary, so
\r
1762 // let's use an element path for it.
\r
1763 var path = new CKEDITOR.dom.elementPath( this.endContainer );
\r
1765 // Creates a range starting at the block start until the range start.
\r
1766 var walkerRange = this.clone();
\r
1767 walkerRange.collapse( false );
\r
1768 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
\r
1770 var walker = new CKEDITOR.dom.walker( walkerRange );
\r
1771 walker.evaluator = getCheckStartEndBlockEvalFunction( false );
\r
1773 return walker.checkForward();
\r
1777 * Check if elements at which the range boundaries anchor are read-only,
\r
1778 * with respect to "contenteditable" attribute.
\r
1780 checkReadOnly : ( function()
\r
1782 function checkNodesEditable( node, anotherEnd )
\r
1786 if ( node.type == CKEDITOR.NODE_ELEMENT )
\r
1788 if ( node.getAttribute( 'contentEditable' ) == 'false'
\r
1789 && !node.data( 'cke-editable' ) )
\r
1793 // Range enclosed entirely in an editable element.
\r
1794 else if ( node.is( 'body' )
\r
1795 || node.getAttribute( 'contentEditable' ) == 'true'
\r
1796 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
\r
1801 node = node.getParent();
\r
1809 var startNode = this.startContainer,
\r
1810 endNode = this.endContainer;
\r
1812 // Check if elements path at both boundaries are editable.
\r
1813 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
\r
1818 * Moves the range boundaries to the first/end editing point inside an
\r
1819 * element. For example, in an element tree like
\r
1820 * "<p><b><i></i></b> Text</p>", the start editing point is
\r
1821 * "<p><b><i>^</i></b> Text</p>" (inside <i>).
\r
1822 * @param {CKEDITOR.dom.element} el The element into which look for the
\r
1824 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
\r
1826 moveToElementEditablePosition : function( el, isMoveToEnd )
\r
1830 // Empty elements are rejected.
\r
1831 if ( CKEDITOR.dtd.$empty[ el.getName() ] )
\r
1834 while ( el && el.type == CKEDITOR.NODE_ELEMENT )
\r
1836 isEditable = el.isEditable();
\r
1838 // If an editable element is found, move inside it.
\r
1840 this.moveToPosition( el, isMoveToEnd ?
\r
1841 CKEDITOR.POSITION_BEFORE_END :
\r
1842 CKEDITOR.POSITION_AFTER_START );
\r
1843 // Stop immediately if we've found a non editable inline element (e.g <img>).
\r
1844 else if ( CKEDITOR.dtd.$inline[ el.getName() ] )
\r
1846 this.moveToPosition( el, isMoveToEnd ?
\r
1847 CKEDITOR.POSITION_AFTER_END :
\r
1848 CKEDITOR.POSITION_BEFORE_START );
\r
1852 // Non-editable non-inline elements are to be bypassed, getting the next one.
\r
1853 if ( CKEDITOR.dtd.$empty[ el.getName() ] )
\r
1854 el = el[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
\r
1856 el = el[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
\r
1858 // Stop immediately if we've found a text node.
\r
1859 if ( el && el.type == CKEDITOR.NODE_TEXT )
\r
1861 this.moveToPosition( el, isMoveToEnd ?
\r
1862 CKEDITOR.POSITION_AFTER_END :
\r
1863 CKEDITOR.POSITION_BEFORE_START );
\r
1868 return isEditable;
\r
1872 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
1874 moveToElementEditStart : function( target )
\r
1876 return this.moveToElementEditablePosition( target );
\r
1880 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
\r
1882 moveToElementEditEnd : function( target )
\r
1884 return this.moveToElementEditablePosition( target, true );
\r
1888 * Get the single node enclosed within the range if there's one.
\r
1890 getEnclosedNode : function()
\r
1892 var walkerRange = this.clone();
\r
1894 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
\r
1895 walkerRange.optimize();
\r
1896 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT
\r
1897 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
\r
1900 var walker = new CKEDITOR.dom.walker( walkerRange ),
\r
1901 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),
\r
1902 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
\r
1903 evaluator = function( node )
\r
1905 return isNotWhitespaces( node ) && isNotBookmarks( node );
\r
1907 walkerRange.evaluator = evaluator;
\r
1908 var node = walker.next();
\r
1910 return node && node.equals( walker.previous() ) ? node : null;
\r
1913 getTouchedStartNode : function()
\r
1915 var container = this.startContainer ;
\r
1917 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
1918 return container ;
\r
1920 return container.getChild( this.startOffset ) || container ;
\r
1923 getTouchedEndNode : function()
\r
1925 var container = this.endContainer ;
\r
1927 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
\r
1928 return container ;
\r
1930 return container.getChild( this.endOffset - 1 ) || container ;
\r
1935 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text"
\r
1936 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^"
\r
1937 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text"
\r
1938 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text"
\r
1940 CKEDITOR.ENLARGE_ELEMENT = 1;
\r
1941 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
\r
1942 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
\r
1944 // Check boundary types.
\r
1945 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
\r
1946 CKEDITOR.START = 1;
\r
1948 CKEDITOR.STARTEND = 3;
\r
1950 // Shrink range types.
\r
1951 // @see CKEDITOR.dom.range.prototype.shrink
\r
1952 CKEDITOR.SHRINK_ELEMENT = 1;
\r
1953 CKEDITOR.SHRINK_TEXT = 2;
\r