477197ef053ced2de6cf8cd9d9a66d0be11bf83e
[ckeditor.git] / _source / core / dom / range.js
1 /*
2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
4 */
5
6 /**
7 * Creates a CKEDITOR.dom.range instance that can be used inside a specific
8 * DOM Document.
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>
12 * <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.
19 * @example
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();
25 */
26 CKEDITOR.dom.range = function( document )
27 {
28 /**
29 * Node within which the range begins.
30 * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}
31 * @example
32 * var range = new CKEDITOR.dom.range( editor.document );
33 * range.selectNodeContents( editor.document.getBody() );
34 * alert( range.startContainer.getName() ); // "body"
35 */
36 this.startContainer = null;
37
38 /**
39 * Offset within the starting node of the range.
40 * @type {Number}
41 * @example
42 * var range = new CKEDITOR.dom.range( editor.document );
43 * range.selectNodeContents( editor.document.getBody() );
44 * alert( range.startOffset ); // "0"
45 */
46 this.startOffset = null;
47
48 /**
49 * Node within which the range ends.
50 * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}
51 * @example
52 * var range = new CKEDITOR.dom.range( editor.document );
53 * range.selectNodeContents( editor.document.getBody() );
54 * alert( range.endContainer.getName() ); // "body"
55 */
56 this.endContainer = null;
57
58 /**
59 * Offset within the ending node of the range.
60 * @type {Number}
61 * @example
62 * var range = new CKEDITOR.dom.range( editor.document );
63 * range.selectNodeContents( editor.document.getBody() );
64 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
65 */
66 this.endOffset = null;
67
68 /**
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
71 * in it.
72 * @example
73 * var range = new CKEDITOR.dom.range( editor.document );
74 * range.selectNodeContents( editor.document.getBody() );
75 * alert( range.collapsed ); // "false"
76 * range.collapse();
77 * alert( range.collapsed ); // "true"
78 */
79 this.collapsed = true;
80
81 /**
82 * The document within which the range can be used.
83 * @type {CKEDITOR.dom.document}
84 * @example
85 * // Selects the body contents of the range document.
86 * range.selectNodeContents( range.document.getBody() );
87 */
88 this.document = document;
89 };
90
91 (function()
92 {
93 // Updates the "collapsed" property for the given range object.
94 var updateCollapsed = function( range )
95 {
96 range.collapsed = (
97 range.startContainer &&
98 range.endContainer &&
99 range.startContainer.equals( range.endContainer ) &&
100 range.startOffset == range.endOffset );
101 };
102
103 // This is a shared function used to delete, extract and clone the range
104 // contents.
105 // V2
106 var execContentsAction = function( range, action, docFrag, mergeThen )
107 {
108 range.optimizeBookmark();
109
110 var startNode = range.startContainer;
111 var endNode = range.endContainer;
112
113 var startOffset = range.startOffset;
114 var endOffset = range.endOffset;
115
116 var removeStartNode;
117 var removeEndNode;
118
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 );
123 else
124 {
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 )
128 {
129 // If the offset points after the last node.
130 if ( endOffset >= endNode.getChildCount() )
131 {
132 // Let's create a temporary node and mark it for removal.
133 endNode = endNode.append( range.document.createText( '' ) );
134 removeEndNode = true;
135 }
136 else
137 endNode = endNode.getChild( endOffset );
138 }
139 }
140
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 )
144 {
145 startNode.split( startOffset );
146
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();
152 }
153 else
154 {
155 // If the start container has children and the offset is pointing
156 // to a child, then we should start from its previous sibling.
157
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.
160 if ( !startOffset )
161 {
162 // Let's create a temporary node and mark it for removal.
163 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) );
164 removeStartNode = true;
165 }
166 else if ( startOffset >= startNode.getChildCount() )
167 {
168 // Let's create a temporary node and mark it for removal.
169 startNode = startNode.append( range.document.createText( '' ) );
170 removeStartNode = true;
171 }
172 else
173 startNode = startNode.getChild( startOffset ).getPrevious();
174 }
175
176 // Get the parent nodes tree for the start and end boundaries.
177 var startParents = startNode.getParents();
178 var endParents = endNode.getParents();
179
180 // Compare them, to find the top most siblings.
181 var i, topStart, topEnd;
182
183 for ( i = 0 ; i < startParents.length ; i++ )
184 {
185 topStart = startParents[ i ];
186 topEnd = endParents[ i ];
187
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
191 // most element.
192 if ( !topStart.equals( topEnd ) )
193 break;
194 }
195
196 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling;
197
198 // Remove all successive sibling nodes for every node in the
199 // startParents tree.
200 for ( var j = i ; j < startParents.length ; j++ )
201 {
202 levelStartNode = startParents[j];
203
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() );
207
208 currentNode = levelStartNode.getNext();
209
210 while ( currentNode )
211 {
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 ) )
215 break;
216
217 // Cache the next sibling.
218 currentSibling = currentNode.getNext();
219
220 // If cloning, just clone it.
221 if ( action == 2 ) // 2 = Clone
222 clone.append( currentNode.clone( true ) );
223 else
224 {
225 // Both Delete and Extract will remove the node.
226 currentNode.remove();
227
228 // When Extracting, move the removed node to the docFrag.
229 if ( action == 1 ) // 1 = Extract
230 clone.append( currentNode );
231 }
232
233 currentNode = currentSibling;
234 }
235
236 if ( clone )
237 clone = levelClone;
238 }
239
240 clone = docFrag;
241
242 // Remove all previous sibling nodes for every node in the
243 // endParents tree.
244 for ( var k = i ; k < endParents.length ; k++ )
245 {
246 levelStartNode = endParents[ k ];
247
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() );
251
252 // The processing of siblings may have already been done by the parent.
253 if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode )
254 {
255 currentNode = levelStartNode.getPrevious();
256
257 while ( currentNode )
258 {
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 ) )
262 break;
263
264 // Cache the next sibling.
265 currentSibling = currentNode.getPrevious();
266
267 // If cloning, just clone it.
268 if ( action == 2 ) // 2 = Clone
269 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ;
270 else
271 {
272 // Both Delete and Extract will remove the node.
273 currentNode.remove();
274
275 // When Extracting, mode the removed node to the docFrag.
276 if ( action == 1 ) // 1 = Extract
277 clone.$.insertBefore( currentNode.$, clone.$.firstChild );
278 }
279
280 currentNode = currentSibling;
281 }
282 }
283
284 if ( clone )
285 clone = levelClone;
286 }
287
288 if ( action == 2 ) // 2 = Clone.
289 {
290 // No changes in the DOM should be done, so fix the split text (if any).
291
292 var startTextNode = range.startContainer;
293 if ( startTextNode.type == CKEDITOR.NODE_TEXT )
294 {
295 startTextNode.$.data += startTextNode.$.nextSibling.data;
296 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );
297 }
298
299 var endTextNode = range.endContainer;
300 if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling )
301 {
302 endTextNode.$.data += endTextNode.$.nextSibling.data;
303 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );
304 }
305 }
306 else
307 {
308 // Collapse the range.
309
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 ) )
313 {
314 var endIndex = topEnd.getIndex();
315
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 )
319 endIndex--;
320
321 // Merge splitted parents.
322 if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT )
323 {
324 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
325 'data-cke-bookmark="1" style="display:none">&nbsp;</span>', range.document );
326 span.insertAfter( topStart );
327 topStart.mergeSiblings( false );
328 range.moveToBookmark( { startNode : span } );
329 }
330 else
331 range.setStart( topEnd.getParent(), endIndex );
332 }
333
334 // Collapse it to the start.
335 range.collapse( true );
336 }
337
338 // Cleanup any marked node.
339 if ( removeStartNode )
340 startNode.remove();
341
342 if ( removeEndNode && endNode.$.parentNode )
343 endNode.remove();
344 };
345
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 };
347
348 // Creates the appropriate node evaluator for the dom walker used inside
349 // check(Start|End)OfBlock.
350 function getCheckStartEndBlockEvalFunction( isStart )
351 {
352 var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );
353 return function( node )
354 {
355 // First ignore bookmark nodes.
356 if ( bookmarkEvaluator( node ) )
357 return true;
358
359 if ( node.type == CKEDITOR.NODE_TEXT )
360 {
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 )
363 return false;
364 }
365 else if ( node.type == CKEDITOR.NODE_ELEMENT )
366 {
367 // If there are non-empty inline elements (e.g. <img />), then we're not
368 // at the start.
369 if ( !inlineChildReqElements[ node.getName() ] )
370 {
371 // If we're working at the end-of-block, forgive the first <br /> in non-IE
372 // browsers.
373 if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )
374 hadBr = true;
375 else
376 return false;
377 }
378 }
379 return true;
380 };
381 }
382
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 )
386 {
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' );
393 }
394
395 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
396 bookmarkEval = new CKEDITOR.dom.walker.bookmark();
397
398 function nonWhitespaceOrBookmarkEval( node )
399 {
400 // Whitespaces and bookmark nodes are to be ignored.
401 return !whitespaceEval( node ) && !bookmarkEval( node );
402 }
403
404 CKEDITOR.dom.range.prototype =
405 {
406 clone : function()
407 {
408 var clone = new CKEDITOR.dom.range( this.document );
409
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;
415
416 return clone;
417 },
418
419 collapse : function( toStart )
420 {
421 if ( toStart )
422 {
423 this.endContainer = this.startContainer;
424 this.endOffset = this.startOffset;
425 }
426 else
427 {
428 this.startContainer = this.endContainer;
429 this.startOffset = this.endOffset;
430 }
431
432 this.collapsed = true;
433 },
434
435 /**
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).
438 */
439 cloneContents : function()
440 {
441 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
442
443 if ( !this.collapsed )
444 execContentsAction( this, 2, docFrag );
445
446 return docFrag;
447 },
448
449 /**
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.
452 */
453 deleteContents : function( mergeThen )
454 {
455 if ( this.collapsed )
456 return;
457
458 execContentsAction( this, 0, null, mergeThen );
459 },
460
461 /**
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.
465 */
466 extractContents : function( mergeThen )
467 {
468 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
469
470 if ( !this.collapsed )
471 execContentsAction( this, 1, docFrag, mergeThen );
472
473 return docFrag;
474 },
475
476 /**
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
488 * change).
489 * @returns {Object} And object representing a bookmark.
490 */
491 createBookmark : function( serializable )
492 {
493 var startNode, endNode;
494 var baseId;
495 var clone;
496 var collapsed = this.collapsed;
497
498 startNode = this.document.createElement( 'span' );
499 startNode.data( 'cke-bookmark', 1 );
500 startNode.setStyle( 'display', 'none' );
501
502 // For IE, it must have something inside, otherwise it may be
503 // removed during DOM operations.
504 startNode.setHtml( '&nbsp;' );
505
506 if ( serializable )
507 {
508 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
509 startNode.setAttribute( 'id', baseId + 'S' );
510 }
511
512 // If collapsed, the endNode will not be created.
513 if ( !collapsed )
514 {
515 endNode = startNode.clone();
516 endNode.setHtml( '&nbsp;' );
517
518 if ( serializable )
519 endNode.setAttribute( 'id', baseId + 'E' );
520
521 clone = this.clone();
522 clone.collapse();
523 clone.insertNode( endNode );
524 }
525
526 clone = this.clone();
527 clone.collapse( true );
528 clone.insertNode( startNode );
529
530 // Update the range position.
531 if ( endNode )
532 {
533 this.setStartAfter( startNode );
534 this.setEndBefore( endNode );
535 }
536 else
537 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
538
539 return {
540 startNode : serializable ? baseId + 'S' : startNode,
541 endNode : serializable ? baseId + 'E' : endNode,
542 serializable : serializable,
543 collapsed : collapsed
544 };
545 },
546
547 /**
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
555 * moveToBookmark.
556 * @returns {Object} An object representing the bookmark.
557 */
558 createBookmark2 : function( normalized )
559 {
560 var startContainer = this.startContainer,
561 endContainer = this.endContainer;
562
563 var startOffset = this.startOffset,
564 endOffset = this.endOffset;
565
566 var collapsed = this.collapsed;
567
568 var child, previous;
569
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 };
575
576 if ( normalized )
577 {
578 // Find out if the start is pointing to a text node that will
579 // be normalized.
580 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
581 {
582 child = startContainer.getChild( startOffset );
583
584 // In this case, move the start information to that text
585 // node.
586 if ( child && child.type == CKEDITOR.NODE_TEXT
587 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
588 {
589 startContainer = child;
590 startOffset = 0;
591 }
592
593 // Get the normalized offset.
594 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
595 startOffset = child.getIndex( 1 );
596 }
597
598 // Normalize the start.
599 while ( startContainer.type == CKEDITOR.NODE_TEXT
600 && ( previous = startContainer.getPrevious() )
601 && previous.type == CKEDITOR.NODE_TEXT )
602 {
603 startContainer = previous;
604 startOffset += previous.getLength();
605 }
606
607 // Process the end only if not normalized.
608 if ( !collapsed )
609 {
610 // Find out if the start is pointing to a text node that
611 // will be normalized.
612 if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
613 {
614 child = endContainer.getChild( endOffset );
615
616 // In this case, move the start information to that
617 // text node.
618 if ( child && child.type == CKEDITOR.NODE_TEXT
619 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )
620 {
621 endContainer = child;
622 endOffset = 0;
623 }
624
625 // Get the normalized offset.
626 if ( child && child.type == CKEDITOR.NODE_ELEMENT )
627 endOffset = child.getIndex( 1 );
628 }
629
630 // Normalize the end.
631 while ( endContainer.type == CKEDITOR.NODE_TEXT
632 && ( previous = endContainer.getPrevious() )
633 && previous.type == CKEDITOR.NODE_TEXT )
634 {
635 endContainer = previous;
636 endOffset += previous.getLength();
637 }
638 }
639 }
640
641 return {
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.
649 };
650 },
651
652 moveToBookmark : function( bookmark )
653 {
654 if ( bookmark.is2 ) // Created with createBookmark2().
655 {
656 // Get the start information.
657 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
658 startOffset = bookmark.startOffset;
659
660 // Get the end information.
661 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
662 endOffset = bookmark.endOffset;
663
664 // Set the start boundary.
665 this.setStart( startContainer, startOffset );
666
667 // Set the end boundary. If not available, collapse it.
668 if ( endContainer )
669 this.setEnd( endContainer, endOffset );
670 else
671 this.collapse( true );
672 }
673 else // Created with createBookmark().
674 {
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;
678
679 // Set the range start at the bookmark start node position.
680 this.setStartBefore( startNode );
681
682 // Remove it, because it may interfere in the setEndBefore call.
683 startNode.remove();
684
685 // Set the range end at the bookmark end node position, or simply
686 // collapse it if it is not available.
687 if ( endNode )
688 {
689 this.setEndBefore( endNode );
690 endNode.remove();
691 }
692 else
693 this.collapse( true );
694 }
695 },
696
697 getBoundaryNodes : function()
698 {
699 var startNode = this.startContainer,
700 endNode = this.endContainer,
701 startOffset = this.startOffset,
702 endOffset = this.endOffset,
703 childCount;
704
705 if ( startNode.type == CKEDITOR.NODE_ELEMENT )
706 {
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
713 {
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 );
719
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
722 // document.
723 startNode = startNode.getNextSourceNode() || startNode;
724 }
725 }
726 if ( endNode.type == CKEDITOR.NODE_ELEMENT )
727 {
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
734 {
735 // Try to take the node just before the current position.
736 endNode = endNode.$;
737 while ( endNode.lastChild )
738 endNode = endNode.lastChild;
739 endNode = new CKEDITOR.dom.node( endNode );
740 }
741 }
742
743 // Sometimes the endNode will come right before startNode for collapsed
744 // ranges. Fix it. (#3780)
745 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
746 startNode = endNode;
747
748 return { startNode : startNode, endNode : endNode };
749 },
750
751 /**
752 * Find the node which fully contains the range.
753 * @param includeSelf
754 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.
755 */
756 getCommonAncestor : function( includeSelf , ignoreTextNode )
757 {
758 var start = this.startContainer,
759 end = this.endContainer,
760 ancestor;
761
762 if ( start.equals( end ) )
763 {
764 if ( includeSelf
765 && start.type == CKEDITOR.NODE_ELEMENT
766 && this.startOffset == this.endOffset - 1 )
767 ancestor = start.getChild( this.startOffset );
768 else
769 ancestor = start;
770 }
771 else
772 ancestor = start.getCommonAncestor( end );
773
774 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
775 },
776
777 /**
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.
782 */
783 optimize : function()
784 {
785 var container = this.startContainer;
786 var offset = this.startOffset;
787
788 if ( container.type != CKEDITOR.NODE_ELEMENT )
789 {
790 if ( !offset )
791 this.setStartBefore( container );
792 else if ( offset >= container.getLength() )
793 this.setStartAfter( container );
794 }
795
796 container = this.endContainer;
797 offset = this.endOffset;
798
799 if ( container.type != CKEDITOR.NODE_ELEMENT )
800 {
801 if ( !offset )
802 this.setEndBefore( container );
803 else if ( offset >= container.getLength() )
804 this.setEndAfter( container );
805 }
806 },
807
808 /**
809 * Move the range out of bookmark nodes if they'd been the container.
810 */
811 optimizeBookmark: function()
812 {
813 var startNode = this.startContainer,
814 endNode = this.endContainer;
815
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 );
822 },
823
824 trim : function( ignoreStart, ignoreEnd )
825 {
826 var startContainer = this.startContainer,
827 startOffset = this.startOffset,
828 collapsed = this.collapsed;
829 if ( ( !ignoreStart || collapsed )
830 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
831 {
832 // If the offset is zero, we just insert the new node before
833 // the start.
834 if ( !startOffset )
835 {
836 startOffset = startContainer.getIndex();
837 startContainer = startContainer.getParent();
838 }
839 // If the offset is at the end, we'll insert it after the text
840 // node.
841 else if ( startOffset >= startContainer.getLength() )
842 {
843 startOffset = startContainer.getIndex() + 1;
844 startContainer = startContainer.getParent();
845 }
846 // In other case, we split the text node and insert the new
847 // node at the split point.
848 else
849 {
850 var nextText = startContainer.split( startOffset );
851
852 startOffset = startContainer.getIndex() + 1;
853 startContainer = startContainer.getParent();
854
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 ) )
859 this.endOffset += 1;
860 }
861
862 this.setStart( startContainer, startOffset );
863
864 if ( collapsed )
865 {
866 this.collapse( true );
867 return;
868 }
869 }
870
871 var endContainer = this.endContainer;
872 var endOffset = this.endOffset;
873
874 if ( !( ignoreEnd || collapsed )
875 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
876 {
877 // If the offset is zero, we just insert the new node before
878 // the start.
879 if ( !endOffset )
880 {
881 endOffset = endContainer.getIndex();
882 endContainer = endContainer.getParent();
883 }
884 // If the offset is at the end, we'll insert it after the text
885 // node.
886 else if ( endOffset >= endContainer.getLength() )
887 {
888 endOffset = endContainer.getIndex() + 1;
889 endContainer = endContainer.getParent();
890 }
891 // In other case, we split the text node and insert the new
892 // node at the split point.
893 else
894 {
895 endContainer.split( endOffset );
896
897 endOffset = endContainer.getIndex() + 1;
898 endContainer = endContainer.getParent();
899 }
900
901 this.setEnd( endContainer, endOffset );
902 }
903 },
904
905 /**
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.
909 */
910 enlarge : function( unit, excludeBrs )
911 {
912 switch ( unit )
913 {
914 case CKEDITOR.ENLARGE_ELEMENT :
915
916 if ( this.collapsed )
917 return;
918
919 // Get the common ancestor.
920 var commonAncestor = this.getCommonAncestor();
921
922 var body = this.document.getBody();
923
924 // For each boundary
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.
927
928 var startTop, endTop;
929
930 var enlargeable, sibling, commonReached;
931
932 // Indicates that the node can be added only if whitespace
933 // is available before it.
934 var needsWhiteSpace = false;
935 var isWhiteSpace;
936 var siblingText;
937
938 // Process the start boundary.
939
940 var container = this.startContainer;
941 var offset = this.startOffset;
942
943 if ( container.type == CKEDITOR.NODE_TEXT )
944 {
945 if ( offset )
946 {
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;
950
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;
956 }
957
958 if ( container )
959 {
960 if ( !( sibling = container.getPrevious() ) )
961 enlargeable = container.getParent();
962 }
963 }
964 else
965 {
966 // If we have offset, get the node preceeding it as the
967 // first sibling to be checked.
968 if ( offset )
969 sibling = container.getChild( offset - 1 ) || container.getLast();
970
971 // If there is no sibling, mark the container to be
972 // enlarged.
973 if ( !sibling )
974 enlargeable = container;
975 }
976
977 while ( enlargeable || sibling )
978 {
979 if ( enlargeable && !sibling )
980 {
981 // If we reached the common ancestor, mark the flag
982 // for it.
983 if ( !commonReached && enlargeable.equals( commonAncestor ) )
984 commonReached = true;
985
986 if ( !body.contains( enlargeable ) )
987 break;
988
989 // If we don't need space or this element breaks
990 // the line, then enlarge it.
991 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
992 {
993 needsWhiteSpace = false;
994
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.
999 if ( commonReached )
1000 startTop = enlargeable;
1001 else
1002 this.setStartBefore( enlargeable );
1003 }
1004
1005 sibling = enlargeable.getPrevious();
1006 }
1007
1008 // Check all sibling nodes preceeding the enlargeable
1009 // node. The node wil lbe enlarged only if none of them
1010 // blocks it.
1011 while ( sibling )
1012 {
1013 // This flag indicates that this node has
1014 // whitespaces at the end.
1015 isWhiteSpace = false;
1016
1017 if ( sibling.type == CKEDITOR.NODE_TEXT )
1018 {
1019 siblingText = sibling.getText();
1020
1021 if ( /[^\s\ufeff]/.test( siblingText ) )
1022 sibling = null;
1023
1024 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1025 }
1026 else
1027 {
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' ) )
1033 {
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() ] )
1038 {
1039 // It must contains spaces and inline elements only.
1040
1041 siblingText = sibling.getText();
1042
1043 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1044 sibling = null;
1045 else
1046 {
1047 var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
1048 for ( var i = 0, child ; child = allChildren[ i++ ] ; )
1049 {
1050 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
1051 {
1052 sibling = null;
1053 break;
1054 }
1055 }
1056 }
1057
1058 if ( sibling )
1059 isWhiteSpace = !!siblingText.length;
1060 }
1061 else
1062 sibling = null;
1063 }
1064 }
1065
1066 // A node with whitespaces has been found.
1067 if ( isWhiteSpace )
1068 {
1069 // Enlarge the last enlargeable node, if we
1070 // were waiting for spaces.
1071 if ( needsWhiteSpace )
1072 {
1073 if ( commonReached )
1074 startTop = enlargeable;
1075 else if ( enlargeable )
1076 this.setStartBefore( enlargeable );
1077 }
1078 else
1079 needsWhiteSpace = true;
1080 }
1081
1082 if ( sibling )
1083 {
1084 var next = sibling.getPrevious();
1085
1086 if ( !enlargeable && !next )
1087 {
1088 // Set the sibling as enlargeable, so it's
1089 // parent will be get later outside this while.
1090 enlargeable = sibling;
1091 sibling = null;
1092 break;
1093 }
1094
1095 sibling = next;
1096 }
1097 else
1098 {
1099 // If sibling has been set to null, then we
1100 // need to stop enlarging.
1101 enlargeable = null;
1102 }
1103 }
1104
1105 if ( enlargeable )
1106 enlargeable = enlargeable.getParent();
1107 }
1108
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.
1114
1115 container = this.endContainer;
1116 offset = this.endOffset;
1117
1118 // Reset the common variables.
1119 enlargeable = sibling = null;
1120 commonReached = needsWhiteSpace = false;
1121
1122 if ( container.type == CKEDITOR.NODE_TEXT )
1123 {
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;
1127
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() );
1133
1134 if ( container )
1135 {
1136 if ( !( sibling = container.getNext() ) )
1137 enlargeable = container.getParent();
1138 }
1139 }
1140 else
1141 {
1142 // Get the node right after the boudary to be checked
1143 // first.
1144 sibling = container.getChild( offset );
1145
1146 if ( !sibling )
1147 enlargeable = container;
1148 }
1149
1150 while ( enlargeable || sibling )
1151 {
1152 if ( enlargeable && !sibling )
1153 {
1154 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1155 commonReached = true;
1156
1157 if ( !body.contains( enlargeable ) )
1158 break;
1159
1160 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
1161 {
1162 needsWhiteSpace = false;
1163
1164 if ( commonReached )
1165 endTop = enlargeable;
1166 else if ( enlargeable )
1167 this.setEndAfter( enlargeable );
1168 }
1169
1170 sibling = enlargeable.getNext();
1171 }
1172
1173 while ( sibling )
1174 {
1175 isWhiteSpace = false;
1176
1177 if ( sibling.type == CKEDITOR.NODE_TEXT )
1178 {
1179 siblingText = sibling.getText();
1180
1181 if ( /[^\s\ufeff]/.test( siblingText ) )
1182 sibling = null;
1183
1184 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1185 }
1186 else
1187 {
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' ) )
1193 {
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() ] )
1198 {
1199 // It must contains spaces and inline elements only.
1200
1201 siblingText = sibling.getText();
1202
1203 if ( (/[^\s\ufeff]/).test( siblingText ) )
1204 sibling = null;
1205 else
1206 {
1207 allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
1208 for ( i = 0 ; child = allChildren[ i++ ] ; )
1209 {
1210 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
1211 {
1212 sibling = null;
1213 break;
1214 }
1215 }
1216 }
1217
1218 if ( sibling )
1219 isWhiteSpace = !!siblingText.length;
1220 }
1221 else
1222 sibling = null;
1223 }
1224 }
1225
1226 if ( isWhiteSpace )
1227 {
1228 if ( needsWhiteSpace )
1229 {
1230 if ( commonReached )
1231 endTop = enlargeable;
1232 else
1233 this.setEndAfter( enlargeable );
1234 }
1235 }
1236
1237 if ( sibling )
1238 {
1239 next = sibling.getNext();
1240
1241 if ( !enlargeable && !next )
1242 {
1243 enlargeable = sibling;
1244 sibling = null;
1245 break;
1246 }
1247
1248 sibling = next;
1249 }
1250 else
1251 {
1252 // If sibling has been set to null, then we
1253 // need to stop enlarging.
1254 enlargeable = null;
1255 }
1256 }
1257
1258 if ( enlargeable )
1259 enlargeable = enlargeable.getParent();
1260 }
1261
1262 // If the common ancestor can be enlarged by both boundaries, then include it also.
1263 if ( startTop && endTop )
1264 {
1265 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1266
1267 this.setStartBefore( commonAncestor );
1268 this.setEndAfter( commonAncestor );
1269 }
1270 break;
1271
1272 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1273 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1274
1275 // Enlarging the start boundary.
1276 var walkerRange = new CKEDITOR.dom.range( this.document );
1277
1278 body = this.document.getBody();
1279
1280 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
1281 walkerRange.setEnd( this.startContainer, this.startOffset );
1282
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 )
1290 {
1291 var retval = notBlockBoundary( node );
1292 if ( !retval )
1293 blockBoundary = node;
1294 return retval;
1295 },
1296 // Record the encounted 'tailBr' for later use.
1297 tailBrGuard = function( node )
1298 {
1299 var retval = boundaryGuard( node );
1300 if ( !retval && node.is && node.is( 'br' ) )
1301 tailBr = node;
1302 return retval;
1303 };
1304
1305 walker.guard = boundaryGuard;
1306
1307 enlargeable = walker.lastBackward();
1308
1309 // It's the body which stop the enlarging if no block boundary found.
1310 blockBoundary = blockBoundary || body;
1311
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.
1315 this.setStartAt(
1316 blockBoundary,
1317 !blockBoundary.is( 'br' ) &&
1318 ( !enlargeable && this.checkStartOfBlock()
1319 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
1320 CKEDITOR.POSITION_AFTER_START :
1321 CKEDITOR.POSITION_AFTER_END );
1322
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 );
1328
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.
1334
1335 enlargeable = walker.lastForward();
1336
1337 // It's the body which stop the enlarging if no block boundary found.
1338 blockBoundary = blockBoundary || body;
1339
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.
1342 this.setEndAt(
1343 blockBoundary,
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
1350 if ( tailBr )
1351 this.setEndAfter( tailBr );
1352 }
1353 },
1354
1355 /**
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.
1359 * <dl>
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>
1364 * </dl>
1365 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1366 */
1367 shrink : function( mode, selectContents )
1368 {
1369 // Unable to shrink a collapsed range.
1370 if ( !this.collapsed )
1371 {
1372 mode = mode || CKEDITOR.SHRINK_TEXT;
1373
1374 var walkerRange = this.clone();
1375
1376 var startContainer = this.startContainer,
1377 endContainer = this.endContainer,
1378 startOffset = this.startOffset,
1379 endOffset = this.endOffset,
1380 collapsed = this.collapsed;
1381
1382 // Whether the start/end boundary is moveable.
1383 var moveStart = 1,
1384 moveEnd = 1;
1385
1386 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
1387 {
1388 if ( !startOffset )
1389 walkerRange.setStartBefore( startContainer );
1390 else if ( startOffset >= startContainer.getLength( ) )
1391 walkerRange.setStartAfter( startContainer );
1392 else
1393 {
1394 // Enlarge the range properly to avoid walker making
1395 // DOM changes caused by triming the text nodes later.
1396 walkerRange.setStartBefore( startContainer );
1397 moveStart = 0;
1398 }
1399 }
1400
1401 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
1402 {
1403 if ( !endOffset )
1404 walkerRange.setEndBefore( endContainer );
1405 else if ( endOffset >= endContainer.getLength( ) )
1406 walkerRange.setEndAfter( endContainer );
1407 else
1408 {
1409 walkerRange.setEndAfter( endContainer );
1410 moveEnd = 0;
1411 }
1412 }
1413
1414 var walker = new CKEDITOR.dom.walker( walkerRange ),
1415 isBookmark = CKEDITOR.dom.walker.bookmark();
1416
1417 walker.evaluator = function( node )
1418 {
1419 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
1420 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1421 };
1422
1423 var currentElement;
1424 walker.guard = function( node, movingOut )
1425 {
1426 if ( isBookmark( node ) )
1427 return true;
1428
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 )
1431 return false;
1432
1433 // Stop when we've already walked "through" an element.
1434 if ( movingOut && node.equals( currentElement ) )
1435 return false;
1436
1437 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1438 currentElement = node;
1439
1440 return true;
1441 };
1442
1443 if ( moveStart )
1444 {
1445 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
1446 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1447 }
1448
1449 if ( moveEnd )
1450 {
1451 walker.reset();
1452 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
1453 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1454 }
1455
1456 return !!( moveStart || moveEnd );
1457 }
1458 },
1459
1460 /**
1461 * Inserts a node at the start of the range. The range will be expanded
1462 * the contain the node.
1463 */
1464 insertNode : function( node )
1465 {
1466 this.optimizeBookmark();
1467 this.trim( false, true );
1468
1469 var startContainer = this.startContainer;
1470 var startOffset = this.startOffset;
1471
1472 var nextNode = startContainer.getChild( startOffset );
1473
1474 if ( nextNode )
1475 node.insertBefore( nextNode );
1476 else
1477 startContainer.append( node );
1478
1479 // Check if we need to update the end boundary.
1480 if ( node.getParent().equals( this.endContainer ) )
1481 this.endOffset++;
1482
1483 // Expand the range to embrace the new node.
1484 this.setStartBefore( node );
1485 },
1486
1487 moveToPosition : function( node, position )
1488 {
1489 this.setStartAt( node, position );
1490 this.collapse( true );
1491 },
1492
1493 selectNodeContents : function( node )
1494 {
1495 this.setStart( node, 0 );
1496 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1497 },
1498
1499 /**
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
1504 * of startNode.
1505 */
1506 setStart : function( startNode, startOffset )
1507 {
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.
1512
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();
1517
1518 this.startContainer = startNode;
1519 this.startOffset = startOffset;
1520
1521 if ( !this.endContainer )
1522 {
1523 this.endContainer = startNode;
1524 this.endOffset = startOffset;
1525 }
1526
1527 updateCollapsed( this );
1528 },
1529
1530 /**
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
1535 * of endNode.
1536 */
1537 setEnd : function( endNode, endOffset )
1538 {
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
1542 // it for now.
1543
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();
1548
1549 this.endContainer = endNode;
1550 this.endOffset = endOffset;
1551
1552 if ( !this.startContainer )
1553 {
1554 this.startContainer = endNode;
1555 this.startOffset = endOffset;
1556 }
1557
1558 updateCollapsed( this );
1559 },
1560
1561 setStartAfter : function( node )
1562 {
1563 this.setStart( node.getParent(), node.getIndex() + 1 );
1564 },
1565
1566 setStartBefore : function( node )
1567 {
1568 this.setStart( node.getParent(), node.getIndex() );
1569 },
1570
1571 setEndAfter : function( node )
1572 {
1573 this.setEnd( node.getParent(), node.getIndex() + 1 );
1574 },
1575
1576 setEndBefore : function( node )
1577 {
1578 this.setEnd( node.getParent(), node.getIndex() );
1579 },
1580
1581 setStartAt : function( node, position )
1582 {
1583 switch( position )
1584 {
1585 case CKEDITOR.POSITION_AFTER_START :
1586 this.setStart( node, 0 );
1587 break;
1588
1589 case CKEDITOR.POSITION_BEFORE_END :
1590 if ( node.type == CKEDITOR.NODE_TEXT )
1591 this.setStart( node, node.getLength() );
1592 else
1593 this.setStart( node, node.getChildCount() );
1594 break;
1595
1596 case CKEDITOR.POSITION_BEFORE_START :
1597 this.setStartBefore( node );
1598 break;
1599
1600 case CKEDITOR.POSITION_AFTER_END :
1601 this.setStartAfter( node );
1602 }
1603
1604 updateCollapsed( this );
1605 },
1606
1607 setEndAt : function( node, position )
1608 {
1609 switch( position )
1610 {
1611 case CKEDITOR.POSITION_AFTER_START :
1612 this.setEnd( node, 0 );
1613 break;
1614
1615 case CKEDITOR.POSITION_BEFORE_END :
1616 if ( node.type == CKEDITOR.NODE_TEXT )
1617 this.setEnd( node, node.getLength() );
1618 else
1619 this.setEnd( node, node.getChildCount() );
1620 break;
1621
1622 case CKEDITOR.POSITION_BEFORE_START :
1623 this.setEndBefore( node );
1624 break;
1625
1626 case CKEDITOR.POSITION_AFTER_END :
1627 this.setEndAfter( node );
1628 }
1629
1630 updateCollapsed( this );
1631 },
1632
1633 fixBlock : function( isStart, blockTag )
1634 {
1635 var bookmark = this.createBookmark(),
1636 fixedBlock = this.document.createElement( blockTag );
1637
1638 this.collapse( isStart );
1639
1640 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
1641
1642 this.extractContents().appendTo( fixedBlock );
1643 fixedBlock.trim();
1644
1645 if ( !CKEDITOR.env.ie )
1646 fixedBlock.appendBogus();
1647
1648 this.insertNode( fixedBlock );
1649
1650 this.moveToBookmark( bookmark );
1651
1652 return fixedBlock;
1653 },
1654
1655 splitBlock : function( blockTag )
1656 {
1657 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
1658 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
1659
1660 var startBlockLimit = startPath.blockLimit,
1661 endBlockLimit = endPath.blockLimit;
1662
1663 var startBlock = startPath.block,
1664 endBlock = endPath.block;
1665
1666 var elementPath = null;
1667 // Do nothing if the boundaries are in different block limits.
1668 if ( !startBlockLimit.equals( endBlockLimit ) )
1669 return null;
1670
1671 // Get or fix current blocks.
1672 if ( blockTag != 'br' )
1673 {
1674 if ( !startBlock )
1675 {
1676 startBlock = this.fixBlock( true, blockTag );
1677 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
1678 }
1679
1680 if ( !endBlock )
1681 endBlock = this.fixBlock( false, blockTag );
1682 }
1683
1684 // Get the range position.
1685 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
1686 isEndOfBlock = endBlock && this.checkEndOfBlock();
1687
1688 // Delete the current contents.
1689 // TODO: Why is 2.x doing CheckIsEmpty()?
1690 this.deleteContents();
1691
1692 if ( startBlock && startBlock.equals( endBlock ) )
1693 {
1694 if ( isEndOfBlock )
1695 {
1696 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
1697 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
1698 endBlock = null;
1699 }
1700 else if ( isStartOfBlock )
1701 {
1702 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
1703 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
1704 startBlock = null;
1705 }
1706 else
1707 {
1708 endBlock = this.splitElement( startBlock );
1709
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() ;
1715 }
1716 }
1717
1718 return {
1719 previousBlock : startBlock,
1720 nextBlock : endBlock,
1721 wasStartOfBlock : isStartOfBlock,
1722 wasEndOfBlock : isEndOfBlock,
1723 elementPath : elementPath
1724 };
1725 },
1726
1727 /**
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.
1733 */
1734 splitElement : function( toSplit )
1735 {
1736 if ( !this.collapsed )
1737 return null;
1738
1739 // Extract the contents of the block from the selection point to the end
1740 // of its contents.
1741 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
1742 var documentFragment = this.extractContents();
1743
1744 // Duplicate the element after it.
1745 var clone = toSplit.clone( false );
1746
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 );
1751 return clone;
1752 },
1753
1754 /**
1755 * Check whether a range boundary is at the inner boundary of a given
1756 * element.
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.
1762 */
1763 checkBoundaryOfElement : function( element, checkType )
1764 {
1765 var checkStart = ( checkType == CKEDITOR.START );
1766
1767 // Create a copy of this range, so we can manipulate it for our checks.
1768 var walkerRange = this.clone();
1769
1770 // Collapse the range at the proper size.
1771 walkerRange.collapse( checkStart );
1772
1773 // Expand the range to element boundary.
1774 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
1775 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
1776
1777 // Create the walker, which will check if we have anything useful
1778 // in the range.
1779 var walker = new CKEDITOR.dom.walker( walkerRange );
1780 walker.evaluator = elementBoundaryEval;
1781
1782 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
1783 },
1784
1785 // Calls to this function may produce changes to the DOM. The range may
1786 // be updated to reflect such changes.
1787 checkStartOfBlock : function()
1788 {
1789 var startContainer = this.startContainer,
1790 startOffset = this.startOffset;
1791
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 )
1795 {
1796 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
1797 if ( textBefore.length )
1798 return false;
1799 }
1800
1801 // Antecipate the trim() call here, so the walker will not make
1802 // changes to the DOM, which would not get reflected into this
1803 // range otherwise.
1804 this.trim();
1805
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 );
1809
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 );
1814
1815 var walker = new CKEDITOR.dom.walker( walkerRange );
1816 walker.evaluator = getCheckStartEndBlockEvalFunction( true );
1817
1818 return walker.checkBackward();
1819 },
1820
1821 checkEndOfBlock : function()
1822 {
1823 var endContainer = this.endContainer,
1824 endOffset = this.endOffset;
1825
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 )
1829 {
1830 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
1831 if ( textAfter.length )
1832 return false;
1833 }
1834
1835 // Antecipate the trim() call here, so the walker will not make
1836 // changes to the DOM, which would not get reflected into this
1837 // range otherwise.
1838 this.trim();
1839
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 );
1843
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 );
1848
1849 var walker = new CKEDITOR.dom.walker( walkerRange );
1850 walker.evaluator = getCheckStartEndBlockEvalFunction( false );
1851
1852 return walker.checkForward();
1853 },
1854
1855 /**
1856 * Check if elements at which the range boundaries anchor are read-only,
1857 * with respect to "contenteditable" attribute.
1858 */
1859 checkReadOnly : ( function()
1860 {
1861 function checkNodesEditable( node, anotherEnd )
1862 {
1863 while( node )
1864 {
1865 if ( node.type == CKEDITOR.NODE_ELEMENT )
1866 {
1867 if ( node.getAttribute( 'contentEditable' ) == 'false'
1868 && !node.data( 'cke-editable' ) )
1869 {
1870 return 0;
1871 }
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 ) ) )
1876 {
1877 break;
1878 }
1879 }
1880 node = node.getParent();
1881 }
1882
1883 return 1;
1884 }
1885
1886 return function()
1887 {
1888 var startNode = this.startContainer,
1889 endNode = this.endContainer;
1890
1891 // Check if elements path at both boundaries are editable.
1892 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
1893 };
1894 })(),
1895
1896 /**
1897 * Moves the range boundaries to the first/end editing point inside an
1898 * element. For example, in an element tree like
1899 * "&lt;p&gt;&lt;b&gt;&lt;i&gt;&lt;/i&gt;&lt;/b&gt; Text&lt;/p&gt;", the start editing point is
1900 * "&lt;p&gt;&lt;b&gt;&lt;i&gt;^&lt;/i&gt;&lt;/b&gt; Text&lt;/p&gt;" (inside &lt;i&gt;).
1901 * @param {CKEDITOR.dom.element} el The element into which look for the
1902 * editing spot.
1903 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
1904 */
1905 moveToElementEditablePosition : function( el, isMoveToEnd )
1906 {
1907 var isEditable;
1908
1909 // Empty elements are rejected.
1910 if ( CKEDITOR.dtd.$empty[ el.getName() ] )
1911 return false;
1912
1913 while ( el && el.type == CKEDITOR.NODE_ELEMENT )
1914 {
1915 isEditable = el.isEditable();
1916
1917 // If an editable element is found, move inside it.
1918 if ( isEditable )
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() ] )
1924 {
1925 this.moveToPosition( el, isMoveToEnd ?
1926 CKEDITOR.POSITION_AFTER_END :
1927 CKEDITOR.POSITION_BEFORE_START );
1928 return true;
1929 }
1930
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 );
1934 else
1935 el = el[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
1936
1937 // Stop immediately if we've found a text node.
1938 if ( el && el.type == CKEDITOR.NODE_TEXT )
1939 {
1940 this.moveToPosition( el, isMoveToEnd ?
1941 CKEDITOR.POSITION_AFTER_END :
1942 CKEDITOR.POSITION_BEFORE_START );
1943 return true;
1944 }
1945 }
1946
1947 return isEditable;
1948 },
1949
1950 /**
1951 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
1952 */
1953 moveToElementEditStart : function( target )
1954 {
1955 return this.moveToElementEditablePosition( target );
1956 },
1957
1958 /**
1959 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
1960 */
1961 moveToElementEditEnd : function( target )
1962 {
1963 return this.moveToElementEditablePosition( target, true );
1964 },
1965
1966 /**
1967 * Get the single node enclosed within the range if there's one.
1968 */
1969 getEnclosedNode : function()
1970 {
1971 var walkerRange = this.clone();
1972
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 )
1977 return null;
1978
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 )
1983 {
1984 return isNotWhitespaces( node ) && isNotBookmarks( node );
1985 };
1986 walkerRange.evaluator = evaluator;
1987 var node = walker.next();
1988 walker.reset();
1989 return node && node.equals( walker.previous() ) ? node : null;
1990 },
1991
1992 getTouchedStartNode : function()
1993 {
1994 var container = this.startContainer ;
1995
1996 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
1997 return container ;
1998
1999 return container.getChild( this.startOffset ) || container ;
2000 },
2001
2002 getTouchedEndNode : function()
2003 {
2004 var container = this.endContainer ;
2005
2006 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2007 return container ;
2008
2009 return container.getChild( this.endOffset - 1 ) || container ;
2010 }
2011 };
2012 })();
2013
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"
2018
2019 CKEDITOR.ENLARGE_ELEMENT = 1;
2020 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2021 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2022
2023 // Check boundary types.
2024 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
2025 CKEDITOR.START = 1;
2026 CKEDITOR.END = 2;
2027 CKEDITOR.STARTEND = 3;
2028
2029 // Shrink range types.
2030 // @see CKEDITOR.dom.range.prototype.shrink
2031 CKEDITOR.SHRINK_ELEMENT = 1;
2032 CKEDITOR.SHRINK_TEXT = 2;