source: trunk/admin/inc/ckeditor/_source/core/dom/range.js@ 239

Last change on this file since 239 was 239, checked in by luc, 9 years ago

Admin: correzione visulaizzazione immissione dati spoglio per Chrome e Safari - Aggiornamento dell'editor da FCKeditor a CKeditor , accessibili anche a Chrome e Safari.

  • Property svn:executable set to *
File size: 63.0 KB
Line 
1/*
2Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3For 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 */
26CKEDITOR.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 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1324 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )
1325 {
1326 var theRange = this.clone();
1327 walker = new CKEDITOR.dom.walker( theRange );
1328
1329 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1330 bookmark = CKEDITOR.dom.walker.bookmark();
1331
1332 walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };
1333 var previous = walker.previous();
1334 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1335 return;
1336 }
1337
1338
1339 // Enlarging the end boundary.
1340 walkerRange = this.clone();
1341 walkerRange.collapse();
1342 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
1343 walker = new CKEDITOR.dom.walker( walkerRange );
1344
1345 // tailBrGuard only used for on range end.
1346 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?
1347 tailBrGuard : boundaryGuard;
1348 blockBoundary = null;
1349 // End the range right before the block boundary node.
1350
1351 enlargeable = walker.lastForward();
1352
1353 // It's the body which stop the enlarging if no block boundary found.
1354 blockBoundary = blockBoundary || body;
1355
1356 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1357 // by comparing the document position with 'enlargeable' node.
1358 this.setEndAt(
1359 blockBoundary,
1360 ( !enlargeable && this.checkEndOfBlock()
1361 || enlargeable && blockBoundary.contains( enlargeable ) ) ?
1362 CKEDITOR.POSITION_BEFORE_END :
1363 CKEDITOR.POSITION_BEFORE_START );
1364 // We must include the <br> at the end of range if there's
1365 // one and we're expanding list item contents
1366 if ( tailBr )
1367 this.setEndAfter( tailBr );
1368 }
1369 },
1370
1371 /**
1372 * Descrease the range to make sure that boundaries
1373 * always anchor beside text nodes or innermost element.
1374 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode.
1375 * <dl>
1376 * <dt>CKEDITOR.SHRINK_ELEMENT</dt>
1377 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd>
1378 * <dt>CKEDITOR.SHRINK_TEXT</dt>
1379 * <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>
1380 * </dl>
1381 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1382 */
1383 shrink : function( mode, selectContents )
1384 {
1385 // Unable to shrink a collapsed range.
1386 if ( !this.collapsed )
1387 {
1388 mode = mode || CKEDITOR.SHRINK_TEXT;
1389
1390 var walkerRange = this.clone();
1391
1392 var startContainer = this.startContainer,
1393 endContainer = this.endContainer,
1394 startOffset = this.startOffset,
1395 endOffset = this.endOffset,
1396 collapsed = this.collapsed;
1397
1398 // Whether the start/end boundary is moveable.
1399 var moveStart = 1,
1400 moveEnd = 1;
1401
1402 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
1403 {
1404 if ( !startOffset )
1405 walkerRange.setStartBefore( startContainer );
1406 else if ( startOffset >= startContainer.getLength( ) )
1407 walkerRange.setStartAfter( startContainer );
1408 else
1409 {
1410 // Enlarge the range properly to avoid walker making
1411 // DOM changes caused by triming the text nodes later.
1412 walkerRange.setStartBefore( startContainer );
1413 moveStart = 0;
1414 }
1415 }
1416
1417 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT )
1418 {
1419 if ( !endOffset )
1420 walkerRange.setEndBefore( endContainer );
1421 else if ( endOffset >= endContainer.getLength( ) )
1422 walkerRange.setEndAfter( endContainer );
1423 else
1424 {
1425 walkerRange.setEndAfter( endContainer );
1426 moveEnd = 0;
1427 }
1428 }
1429
1430 var walker = new CKEDITOR.dom.walker( walkerRange ),
1431 isBookmark = CKEDITOR.dom.walker.bookmark();
1432
1433 walker.evaluator = function( node )
1434 {
1435 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ?
1436 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1437 };
1438
1439 var currentElement;
1440 walker.guard = function( node, movingOut )
1441 {
1442 if ( isBookmark( node ) )
1443 return true;
1444
1445 // Stop when we're shrink in element mode while encountering a text node.
1446 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1447 return false;
1448
1449 // Stop when we've already walked "through" an element.
1450 if ( movingOut && node.equals( currentElement ) )
1451 return false;
1452
1453 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1454 currentElement = node;
1455
1456 return true;
1457 };
1458
1459 if ( moveStart )
1460 {
1461 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next']();
1462 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1463 }
1464
1465 if ( moveEnd )
1466 {
1467 walker.reset();
1468 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
1469 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1470 }
1471
1472 return !!( moveStart || moveEnd );
1473 }
1474 },
1475
1476 /**
1477 * Inserts a node at the start of the range. The range will be expanded
1478 * the contain the node.
1479 */
1480 insertNode : function( node )
1481 {
1482 this.optimizeBookmark();
1483 this.trim( false, true );
1484
1485 var startContainer = this.startContainer;
1486 var startOffset = this.startOffset;
1487
1488 var nextNode = startContainer.getChild( startOffset );
1489
1490 if ( nextNode )
1491 node.insertBefore( nextNode );
1492 else
1493 startContainer.append( node );
1494
1495 // Check if we need to update the end boundary.
1496 if ( node.getParent().equals( this.endContainer ) )
1497 this.endOffset++;
1498
1499 // Expand the range to embrace the new node.
1500 this.setStartBefore( node );
1501 },
1502
1503 moveToPosition : function( node, position )
1504 {
1505 this.setStartAt( node, position );
1506 this.collapse( true );
1507 },
1508
1509 selectNodeContents : function( node )
1510 {
1511 this.setStart( node, 0 );
1512 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1513 },
1514
1515 /**
1516 * Sets the start position of a Range.
1517 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1518 * @param {Number} startOffset An integer greater than or equal to zero
1519 * representing the offset for the start of the range from the start
1520 * of startNode.
1521 */
1522 setStart : function( startNode, startOffset )
1523 {
1524 // W3C requires a check for the new position. If it is after the end
1525 // boundary, the range should be collapsed to the new start. It seams
1526 // we will not need this check for our use of this class so we can
1527 // ignore it for now.
1528
1529 // Fixing invalid range start inside dtd empty elements.
1530 if( startNode.type == CKEDITOR.NODE_ELEMENT
1531 && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1532 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1533
1534 this.startContainer = startNode;
1535 this.startOffset = startOffset;
1536
1537 if ( !this.endContainer )
1538 {
1539 this.endContainer = startNode;
1540 this.endOffset = startOffset;
1541 }
1542
1543 updateCollapsed( this );
1544 },
1545
1546 /**
1547 * Sets the end position of a Range.
1548 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1549 * @param {Number} endOffset An integer greater than or equal to zero
1550 * representing the offset for the end of the range from the start
1551 * of endNode.
1552 */
1553 setEnd : function( endNode, endOffset )
1554 {
1555 // W3C requires a check for the new position. If it is before the start
1556 // boundary, the range should be collapsed to the new end. It seams we
1557 // will not need this check for our use of this class so we can ignore
1558 // it for now.
1559
1560 // Fixing invalid range end inside dtd empty elements.
1561 if( endNode.type == CKEDITOR.NODE_ELEMENT
1562 && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1563 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1564
1565 this.endContainer = endNode;
1566 this.endOffset = endOffset;
1567
1568 if ( !this.startContainer )
1569 {
1570 this.startContainer = endNode;
1571 this.startOffset = endOffset;
1572 }
1573
1574 updateCollapsed( this );
1575 },
1576
1577 setStartAfter : function( node )
1578 {
1579 this.setStart( node.getParent(), node.getIndex() + 1 );
1580 },
1581
1582 setStartBefore : function( node )
1583 {
1584 this.setStart( node.getParent(), node.getIndex() );
1585 },
1586
1587 setEndAfter : function( node )
1588 {
1589 this.setEnd( node.getParent(), node.getIndex() + 1 );
1590 },
1591
1592 setEndBefore : function( node )
1593 {
1594 this.setEnd( node.getParent(), node.getIndex() );
1595 },
1596
1597 setStartAt : function( node, position )
1598 {
1599 switch( position )
1600 {
1601 case CKEDITOR.POSITION_AFTER_START :
1602 this.setStart( node, 0 );
1603 break;
1604
1605 case CKEDITOR.POSITION_BEFORE_END :
1606 if ( node.type == CKEDITOR.NODE_TEXT )
1607 this.setStart( node, node.getLength() );
1608 else
1609 this.setStart( node, node.getChildCount() );
1610 break;
1611
1612 case CKEDITOR.POSITION_BEFORE_START :
1613 this.setStartBefore( node );
1614 break;
1615
1616 case CKEDITOR.POSITION_AFTER_END :
1617 this.setStartAfter( node );
1618 }
1619
1620 updateCollapsed( this );
1621 },
1622
1623 setEndAt : function( node, position )
1624 {
1625 switch( position )
1626 {
1627 case CKEDITOR.POSITION_AFTER_START :
1628 this.setEnd( node, 0 );
1629 break;
1630
1631 case CKEDITOR.POSITION_BEFORE_END :
1632 if ( node.type == CKEDITOR.NODE_TEXT )
1633 this.setEnd( node, node.getLength() );
1634 else
1635 this.setEnd( node, node.getChildCount() );
1636 break;
1637
1638 case CKEDITOR.POSITION_BEFORE_START :
1639 this.setEndBefore( node );
1640 break;
1641
1642 case CKEDITOR.POSITION_AFTER_END :
1643 this.setEndAfter( node );
1644 }
1645
1646 updateCollapsed( this );
1647 },
1648
1649 fixBlock : function( isStart, blockTag )
1650 {
1651 var bookmark = this.createBookmark(),
1652 fixedBlock = this.document.createElement( blockTag );
1653
1654 this.collapse( isStart );
1655
1656 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
1657
1658 this.extractContents().appendTo( fixedBlock );
1659 fixedBlock.trim();
1660
1661 if ( !CKEDITOR.env.ie )
1662 fixedBlock.appendBogus();
1663
1664 this.insertNode( fixedBlock );
1665
1666 this.moveToBookmark( bookmark );
1667
1668 return fixedBlock;
1669 },
1670
1671 splitBlock : function( blockTag )
1672 {
1673 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
1674 endPath = new CKEDITOR.dom.elementPath( this.endContainer );
1675
1676 var startBlockLimit = startPath.blockLimit,
1677 endBlockLimit = endPath.blockLimit;
1678
1679 var startBlock = startPath.block,
1680 endBlock = endPath.block;
1681
1682 var elementPath = null;
1683 // Do nothing if the boundaries are in different block limits.
1684 if ( !startBlockLimit.equals( endBlockLimit ) )
1685 return null;
1686
1687 // Get or fix current blocks.
1688 if ( blockTag != 'br' )
1689 {
1690 if ( !startBlock )
1691 {
1692 startBlock = this.fixBlock( true, blockTag );
1693 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;
1694 }
1695
1696 if ( !endBlock )
1697 endBlock = this.fixBlock( false, blockTag );
1698 }
1699
1700 // Get the range position.
1701 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
1702 isEndOfBlock = endBlock && this.checkEndOfBlock();
1703
1704 // Delete the current contents.
1705 // TODO: Why is 2.x doing CheckIsEmpty()?
1706 this.deleteContents();
1707
1708 if ( startBlock && startBlock.equals( endBlock ) )
1709 {
1710 if ( isEndOfBlock )
1711 {
1712 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
1713 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
1714 endBlock = null;
1715 }
1716 else if ( isStartOfBlock )
1717 {
1718 elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
1719 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
1720 startBlock = null;
1721 }
1722 else
1723 {
1724 endBlock = this.splitElement( startBlock );
1725
1726 // In Gecko, the last child node must be a bogus <br>.
1727 // Note: bogus <br> added under <ul> or <ol> would cause
1728 // lists to be incorrectly rendered.
1729 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )
1730 startBlock.appendBogus() ;
1731 }
1732 }
1733
1734 return {
1735 previousBlock : startBlock,
1736 nextBlock : endBlock,
1737 wasStartOfBlock : isStartOfBlock,
1738 wasEndOfBlock : isEndOfBlock,
1739 elementPath : elementPath
1740 };
1741 },
1742
1743 /**
1744 * Branch the specified element from the collapsed range position and
1745 * place the caret between the two result branches.
1746 * Note: The range must be collapsed and been enclosed by this element.
1747 * @param {CKEDITOR.dom.element} element
1748 * @return {CKEDITOR.dom.element} Root element of the new branch after the split.
1749 */
1750 splitElement : function( toSplit )
1751 {
1752 if ( !this.collapsed )
1753 return null;
1754
1755 // Extract the contents of the block from the selection point to the end
1756 // of its contents.
1757 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
1758 var documentFragment = this.extractContents();
1759
1760 // Duplicate the element after it.
1761 var clone = toSplit.clone( false );
1762
1763 // Place the extracted contents into the duplicated element.
1764 documentFragment.appendTo( clone );
1765 clone.insertAfter( toSplit );
1766 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
1767 return clone;
1768 },
1769
1770 /**
1771 * Check whether a range boundary is at the inner boundary of a given
1772 * element.
1773 * @param {CKEDITOR.dom.element} element The target element to check.
1774 * @param {Number} checkType The boundary to check for both the range
1775 * and the element. It can be CKEDITOR.START or CKEDITOR.END.
1776 * @returns {Boolean} "true" if the range boundary is at the inner
1777 * boundary of the element.
1778 */
1779 checkBoundaryOfElement : function( element, checkType )
1780 {
1781 var checkStart = ( checkType == CKEDITOR.START );
1782
1783 // Create a copy of this range, so we can manipulate it for our checks.
1784 var walkerRange = this.clone();
1785
1786 // Collapse the range at the proper size.
1787 walkerRange.collapse( checkStart );
1788
1789 // Expand the range to element boundary.
1790 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
1791 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
1792
1793 // Create the walker, which will check if we have anything useful
1794 // in the range.
1795 var walker = new CKEDITOR.dom.walker( walkerRange );
1796 walker.evaluator = elementBoundaryEval;
1797
1798 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
1799 },
1800
1801 // Calls to this function may produce changes to the DOM. The range may
1802 // be updated to reflect such changes.
1803 checkStartOfBlock : function()
1804 {
1805 var startContainer = this.startContainer,
1806 startOffset = this.startOffset;
1807
1808 // If the starting node is a text node, and non-empty before the offset,
1809 // then we're surely not at the start of block.
1810 if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
1811 {
1812 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
1813 if ( textBefore.length )
1814 return false;
1815 }
1816
1817 // Antecipate the trim() call here, so the walker will not make
1818 // changes to the DOM, which would not get reflected into this
1819 // range otherwise.
1820 this.trim();
1821
1822 // We need to grab the block element holding the start boundary, so
1823 // let's use an element path for it.
1824 var path = new CKEDITOR.dom.elementPath( this.startContainer );
1825
1826 // Creates a range starting at the block start until the range start.
1827 var walkerRange = this.clone();
1828 walkerRange.collapse( true );
1829 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
1830
1831 var walker = new CKEDITOR.dom.walker( walkerRange );
1832 walker.evaluator = getCheckStartEndBlockEvalFunction( true );
1833
1834 return walker.checkBackward();
1835 },
1836
1837 checkEndOfBlock : function()
1838 {
1839 var endContainer = this.endContainer,
1840 endOffset = this.endOffset;
1841
1842 // If the ending node is a text node, and non-empty after the offset,
1843 // then we're surely not at the end of block.
1844 if ( endContainer.type == CKEDITOR.NODE_TEXT )
1845 {
1846 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
1847 if ( textAfter.length )
1848 return false;
1849 }
1850
1851 // Antecipate the trim() call here, so the walker will not make
1852 // changes to the DOM, which would not get reflected into this
1853 // range otherwise.
1854 this.trim();
1855
1856 // We need to grab the block element holding the start boundary, so
1857 // let's use an element path for it.
1858 var path = new CKEDITOR.dom.elementPath( this.endContainer );
1859
1860 // Creates a range starting at the block start until the range start.
1861 var walkerRange = this.clone();
1862 walkerRange.collapse( false );
1863 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
1864
1865 var walker = new CKEDITOR.dom.walker( walkerRange );
1866 walker.evaluator = getCheckStartEndBlockEvalFunction( false );
1867
1868 return walker.checkForward();
1869 },
1870
1871 /**
1872 * Check if elements at which the range boundaries anchor are read-only,
1873 * with respect to "contenteditable" attribute.
1874 */
1875 checkReadOnly : ( function()
1876 {
1877 function checkNodesEditable( node, anotherEnd )
1878 {
1879 while( node )
1880 {
1881 if ( node.type == CKEDITOR.NODE_ELEMENT )
1882 {
1883 if ( node.getAttribute( 'contentEditable' ) == 'false'
1884 && !node.data( 'cke-editable' ) )
1885 {
1886 return 0;
1887 }
1888 // Range enclosed entirely in an editable element.
1889 else if ( node.is( 'html' )
1890 || node.getAttribute( 'contentEditable' ) == 'true'
1891 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
1892 {
1893 break;
1894 }
1895 }
1896 node = node.getParent();
1897 }
1898
1899 return 1;
1900 }
1901
1902 return function()
1903 {
1904 var startNode = this.startContainer,
1905 endNode = this.endContainer;
1906
1907 // Check if elements path at both boundaries are editable.
1908 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
1909 };
1910 })(),
1911
1912 /**
1913 * Moves the range boundaries to the first/end editing point inside an
1914 * element. For example, in an element tree like
1915 * "&lt;p&gt;&lt;b&gt;&lt;i&gt;&lt;/i&gt;&lt;/b&gt; Text&lt;/p&gt;", the start editing point is
1916 * "&lt;p&gt;&lt;b&gt;&lt;i&gt;^&lt;/i&gt;&lt;/b&gt; Text&lt;/p&gt;" (inside &lt;i&gt;).
1917 * @param {CKEDITOR.dom.element} el The element into which look for the
1918 * editing spot.
1919 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
1920 */
1921 moveToElementEditablePosition : function( el, isMoveToEnd )
1922 {
1923 function nextDFS( node, childOnly )
1924 {
1925 var next;
1926
1927 if ( node.type == CKEDITOR.NODE_ELEMENT
1928 && node.isEditable( false )
1929 && !CKEDITOR.dtd.$nonEditable[ node.getName() ] )
1930 {
1931 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
1932 }
1933
1934 if ( !childOnly && !next )
1935 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
1936
1937 return next;
1938 }
1939
1940 var found = 0;
1941
1942 while ( el )
1943 {
1944 // Stop immediately if we've found a text node.
1945 if ( el.type == CKEDITOR.NODE_TEXT )
1946 {
1947 this.moveToPosition( el, isMoveToEnd ?
1948 CKEDITOR.POSITION_AFTER_END :
1949 CKEDITOR.POSITION_BEFORE_START );
1950 found = 1;
1951 break;
1952 }
1953
1954 // If an editable element is found, move inside it, but not stop the searching.
1955 if ( el.type == CKEDITOR.NODE_ELEMENT )
1956 {
1957 if ( el.isEditable() )
1958 {
1959 this.moveToPosition( el, isMoveToEnd ?
1960 CKEDITOR.POSITION_BEFORE_END :
1961 CKEDITOR.POSITION_AFTER_START );
1962 found = 1;
1963 }
1964 }
1965
1966 el = nextDFS( el, found );
1967 }
1968
1969 return !!found;
1970 },
1971
1972 /**
1973 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
1974 */
1975 moveToElementEditStart : function( target )
1976 {
1977 return this.moveToElementEditablePosition( target );
1978 },
1979
1980 /**
1981 *@see {CKEDITOR.dom.range.moveToElementEditablePosition}
1982 */
1983 moveToElementEditEnd : function( target )
1984 {
1985 return this.moveToElementEditablePosition( target, true );
1986 },
1987
1988 /**
1989 * Get the single node enclosed within the range if there's one.
1990 */
1991 getEnclosedNode : function()
1992 {
1993 var walkerRange = this.clone();
1994
1995 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
1996 walkerRange.optimize();
1997 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT
1998 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
1999 return null;
2000
2001 var walker = new CKEDITOR.dom.walker( walkerRange ),
2002 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),
2003 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
2004 evaluator = function( node )
2005 {
2006 return isNotWhitespaces( node ) && isNotBookmarks( node );
2007 };
2008 walkerRange.evaluator = evaluator;
2009 var node = walker.next();
2010 walker.reset();
2011 return node && node.equals( walker.previous() ) ? node : null;
2012 },
2013
2014 getTouchedStartNode : function()
2015 {
2016 var container = this.startContainer ;
2017
2018 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2019 return container ;
2020
2021 return container.getChild( this.startOffset ) || container ;
2022 },
2023
2024 getTouchedEndNode : function()
2025 {
2026 var container = this.endContainer ;
2027
2028 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2029 return container ;
2030
2031 return container.getChild( this.endOffset - 1 ) || container ;
2032 }
2033 };
2034})();
2035
2036CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text"
2037CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^"
2038CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text"
2039CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text"
2040
2041CKEDITOR.ENLARGE_ELEMENT = 1;
2042CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2043CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2044
2045// Check boundary types.
2046// @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement
2047CKEDITOR.START = 1;
2048CKEDITOR.END = 2;
2049CKEDITOR.STARTEND = 3;
2050
2051// Shrink range types.
2052// @see CKEDITOR.dom.range.prototype.shrink
2053CKEDITOR.SHRINK_ELEMENT = 1;
2054CKEDITOR.SHRINK_TEXT = 2;
Note: See TracBrowser for help on using the repository browser.