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