2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * Represents a delimited piece of content in a DOM Document.
8 * It is contiguous in the sense that it can be characterized as selecting all
9 * of the content between a pair of boundary-points.
11 * This class shares much of the W3C
12 * [Document Object Model Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
13 * ideas and features, adding several range manipulation tools to it, but it's
14 * not intended to be compatible with it.
16 * // Create a range for the entire contents of the editor document body.
17 * var range = new CKEDITOR.dom.range( editor.document );
18 * range.selectNodeContents( editor.document.getBody() );
19 * // Delete the contents.
20 * range.deleteContents();
22 * Usually you will want to work on a ranges rooted in the editor's {@link CKEDITOR.editable editable}
23 * element. Such ranges can be created with a shorthand method – {@link CKEDITOR.editor#createRange editor.createRange}.
25 * var range = editor.createRange();
26 * range.root.equals( editor.editable() ); // -> true
28 * Note that the {@link #root} of a range is an important property, which limits many
29 * algorithms implemented in range's methods. Therefore it is crucial, especially
30 * when using ranges inside inline editors, to specify correct root, so using
31 * the {@link CKEDITOR.editor#createRange} method is highly recommended.
35 * Range is only a logical representation of a piece of content in a DOM. It should not
36 * be confused with a {@link CKEDITOR.dom.selection selection} which represents "physically
37 * marked" content. It is possible to create unlimited number of various ranges, when
38 * only one real selection may exist at a time in a document. Ranges are used to read position
39 * of selection in the DOM and to move selection to new positions.
41 * The editor selection may be retrieved using the {@link CKEDITOR.editor#getSelection} method:
43 * var sel = editor.getSelection(),
44 * ranges = sel.getRanges(); // CKEDITOR.dom.rangeList instance.
46 * var range = ranges[ 0 ];
47 * range.root; // -> editor's editable element.
49 * A range can also be selected:
51 * var range = editor.createRange();
52 * range.selectNodeContents( editor.editable() );
53 * sel.selectRanges( [ range ] );
56 * @constructor Creates a {@link CKEDITOR.dom.range} instance that can be used inside a specific DOM Document.
57 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element} root The document or element
58 * within which the range will be scoped.
59 * @todo global "TODO" - precise algorithms descriptions needed for the most complex methods like #enlarge.
61 CKEDITOR
.dom
.range = function( root
) {
63 * Node within which the range begins.
65 * var range = new CKEDITOR.dom.range( editor.document );
66 * range.selectNodeContents( editor.document.getBody() );
67 * alert( range.startContainer.getName() ); // 'body'
70 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
72 this.startContainer
= null;
75 * Offset within the starting node of the range.
77 * var range = new CKEDITOR.dom.range( editor.document );
78 * range.selectNodeContents( editor.document.getBody() );
79 * alert( range.startOffset ); // 0
84 this.startOffset
= null;
87 * Node within which the range ends.
89 * var range = new CKEDITOR.dom.range( editor.document );
90 * range.selectNodeContents( editor.document.getBody() );
91 * alert( range.endContainer.getName() ); // 'body'
94 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
96 this.endContainer
= null;
99 * Offset within the ending node of the range.
101 * var range = new CKEDITOR.dom.range( editor.document );
102 * range.selectNodeContents( editor.document.getBody() );
103 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
108 this.endOffset
= null;
111 * Indicates that this is a collapsed range. A collapsed range has its
112 * start and end boundaries at the very same point so nothing is contained
115 * var range = new CKEDITOR.dom.range( editor.document );
116 * range.selectNodeContents( editor.document.getBody() );
117 * alert( range.collapsed ); // false
119 * alert( range.collapsed ); // true
123 this.collapsed
= true;
125 var isDocRoot
= root
instanceof CKEDITOR
.dom
.document
;
127 * The document within which the range can be used.
129 * // Selects the body contents of the range document.
130 * range.selectNodeContents( range.document.getBody() );
133 * @property {CKEDITOR.dom.document}
135 this.document
= isDocRoot
? root : root
.getDocument();
138 * The ancestor DOM element within which the range manipulation are limited.
141 * @property {CKEDITOR.dom.element}
143 this.root
= isDocRoot
? root
.getBody() : root
;
147 // Updates the "collapsed" property for the given range object.
148 function updateCollapsed( range
) {
149 range
.collapsed
= ( range
.startContainer
&& range
.endContainer
&& range
.startContainer
.equals( range
.endContainer
) && range
.startOffset
== range
.endOffset
);
152 // This is a shared function used to delete, extract and clone the range content.
154 // The outline of the algorithm:
156 // 1. Normalization. We handle special cases, split text nodes if we can, find boundary nodes (startNode and endNode).
157 // 2. Gathering data.
158 // * We start by creating two arrays of boundary nodes parents. You can imagine these arrays as lines limiting
159 // the tree from the left and right and thus marking the part which is selected by the range. The both lines
160 // start in the same node which is the range.root and end in startNode and endNode.
161 // * Then we find min level and max levels. Level represents all nodes which are equally far from the range.root.
162 // Min level is the level at which the left and right boundaries diverged (the first diverged level). And max levels
163 // are how deep the start and end nodes are nested.
164 // 3. Cloning/extraction.
165 // * We start iterating over start node parents (left branch) from min level and clone the parent (usually shallow clone,
166 // because we know that it's not fully selected) and its right siblings (deep clone, because they are fully selected).
167 // We iterate over siblings up to meeting end node parent or end of the siblings chain.
168 // * We clone level after level down to the startNode.
169 // * Then we do the same with end node parents (right branch), because it may contains notes we omit during the previous
170 // step, for example if the right branch is deeper then left branch. Things are more complicated here because we have to
171 // watch out for nodes that were already cloned.
172 // * ***Note:** Setting `cloneId` option to `false` for **extraction** works for partially selected elements only.
173 // See range.extractContents to know more.
175 // * There are two things we need to do - updating the range position and perform the action of the "mergeThen"
176 // param (see range.deleteContents or range.extractContents).
177 // See comments in mergeAndUpdate because this is lots of fun too.
178 function execContentsAction( range
, action
, docFrag
, mergeThen
, cloneId
) {
181 range
.optimizeBookmark();
183 var isDelete
= action
=== 0;
184 var isExtract
= action
== 1;
185 var isClone
= action
== 2;
186 var doClone
= isClone
|| isExtract
;
188 var startNode
= range
.startContainer
;
189 var endNode
= range
.endContainer
;
191 var startOffset
= range
.startOffset
;
192 var endOffset
= range
.endOffset
;
197 var doNotRemoveStartNode
;
198 var doNotRemoveEndNode
;
203 // Handle here an edge case where we clone a range which is located in one text node.
204 // This allows us to not think about startNode == endNode case later on.
205 // We do that only when cloning, because in other cases we can safely split this text node
206 // and hence we can easily handle this case as many others.
208 // We need to handle situation when selection startNode is type of NODE_ELEMENT (#426).
210 endNode
.type
== CKEDITOR
.NODE_TEXT
&&
211 ( startNode
.equals( endNode
) || ( startNode
.type
=== CKEDITOR
.NODE_ELEMENT
&& startNode
.getFirst().equals( endNode
) ) ) ) {
213 // Here we should always be inside one text node.
214 docFrag
.append( range
.document
.createText( endNode
.substring( startOffset
, endOffset
) ) );
218 // For text containers, we must simply split the node and point to the
219 // second part. The removal will be handled by the rest of the code.
220 if ( endNode
.type
== CKEDITOR
.NODE_TEXT
) {
221 // If Extract or Delete we can split the text node,
222 // but if Clone (2), then we cannot modify the DOM (http://dev.ckeditor.com/ticket/11586) so we mark the text node for cloning.
224 endNode
= endNode
.split( endOffset
);
229 // If there's no node after the range boundary we set endNode to the previous node
230 // and mark it to be cloned.
231 if ( endNode
.getChildCount() > 0 ) {
232 // If the offset points after the last node.
233 if ( endOffset
>= endNode
.getChildCount() ) {
234 endNode
= endNode
.getChild( endOffset
- 1 );
237 endNode
= endNode
.getChild( endOffset
);
240 // The end container is empty (<h1>]</h1>), but we want to clone it, although not remove.
243 doNotRemoveEndNode
= true;
247 // For text containers, we must simply split the node. The removal will
248 // be handled by the rest of the code .
249 if ( startNode
.type
== CKEDITOR
.NODE_TEXT
) {
250 // If Extract or Delete we can split the text node,
251 // but if Clone (2), then we cannot modify the DOM (http://dev.ckeditor.com/ticket/11586) so we mark
252 // the text node for cloning.
254 startNode
.split( startOffset
);
256 cloneStartText
= true;
259 // If there's no node before the range boundary we set startNode to the next node
260 // and mark it to be cloned.
261 if ( startNode
.getChildCount() > 0 ) {
262 if ( startOffset
=== 0 ) {
263 startNode
= startNode
.getChild( startOffset
);
264 cloneStartNode
= true;
266 startNode
= startNode
.getChild( startOffset
- 1 );
269 // The start container is empty (<h1>[</h1>), but we want to clone it, although not remove.
271 cloneStartNode
= true;
272 doNotRemoveStartNode
= true;
276 // Get the parent nodes tree for the start and end boundaries.
277 var startParents
= startNode
.getParents(),
278 endParents
= endNode
.getParents(),
279 // Level at which start and end boundaries diverged.
280 minLevel
= findMinLevel(),
281 maxLevelLeft
= startParents
.length
- 1,
282 maxLevelRight
= endParents
.length
- 1,
283 // Keeps the frag/element which is parent of the level that we are currently cloning.
284 levelParent
= docFrag
,
289 // Keeps track of the last connected level (on which left and right branches are connected)
290 // Usually this is minLevel, but not always.
291 lastConnectedLevel
= -1;
294 for ( var level
= minLevel
; level
<= maxLevelLeft
; level
++ ) {
295 leftNode
= startParents
[ level
];
296 nextSibling
= leftNode
.getNext();
299 // The first step is to handle partial selection of the left branch.
301 // Max depth of the left branch. It means that ( leftSibling == endNode ).
302 // We also check if the leftNode isn't only partially selected, because in this case
303 // we want to make a shallow clone of it (the else part).
304 if ( level
== maxLevelLeft
&& !( leftNode
.equals( endParents
[ level
] ) && maxLevelLeft
< maxLevelRight
) ) {
305 if ( cloneStartNode
) {
306 consume( leftNode
, levelParent
, false, doNotRemoveStartNode
);
307 } else if ( cloneStartText
) {
308 levelParent
.append( range
.document
.createText( leftNode
.substring( startOffset
) ) );
310 } else if ( doClone
) {
311 nextLevelParent
= levelParent
.append( leftNode
.clone( 0, cloneId
) );
315 // The second step is to handle full selection of the content between the left branch and the right branch.
317 while ( nextSibling
) {
318 // We can't clone entire endParent just like we can't clone entire startParent -
319 // - they are not fully selected with the range. Partial endParent selection
320 // will be cloned in the next loop.
321 if ( nextSibling
.equals( endParents
[ level
] ) ) {
322 lastConnectedLevel
= level
;
326 nextSibling
= consume( nextSibling
, levelParent
);
329 levelParent
= nextLevelParent
;
332 // Reset levelParent, because we reset the level.
333 levelParent
= docFrag
;
336 for ( level
= minLevel
; level
<= maxLevelRight
; level
++ ) {
337 rightNode
= endParents
[ level
];
338 nextSibling
= rightNode
.getPrevious();
340 // Do not process this node if it is shared with the left branch
341 // because it was already processed.
343 // Note: Don't worry about text nodes selection - if the entire range was placed in a single text node
344 // it was handled as a special case at the beginning. In other cases when startNode == endNode
345 // or when on this level leftNode == rightNode (so rightNode.equals( startParents[ level ] ))
346 // this node was handled by the previous loop.
347 if ( !rightNode
.equals( startParents
[ level
] ) ) {
349 // The first step is to handle partial selection of the right branch.
351 // Max depth of the right branch. It means that ( rightNode == endNode ).
352 // We also check if the rightNode isn't only partially selected, because in this case
353 // we want to make a shallow clone of it (the else part).
354 if ( level
== maxLevelRight
&& !( rightNode
.equals( startParents
[ level
] ) && maxLevelRight
< maxLevelLeft
) ) {
355 if ( cloneEndNode
) {
356 consume( rightNode
, levelParent
, false, doNotRemoveEndNode
);
357 } else if ( cloneEndText
) {
358 levelParent
.append( range
.document
.createText( rightNode
.substring( 0, endOffset
) ) );
360 } else if ( doClone
) {
361 nextLevelParent
= levelParent
.append( rightNode
.clone( 0, cloneId
) );
365 // The second step is to handle all left (selected) siblings of the rightNode which
366 // have not yet been handled. If the level branches were connected, the previous loop
367 // already copied all siblings (except the current rightNode).
368 if ( level
> lastConnectedLevel
) {
369 while ( nextSibling
) {
370 nextSibling
= consume( nextSibling
, levelParent
, true );
374 levelParent
= nextLevelParent
;
375 } else if ( doClone
) {
376 // If this is "shared" node and we are in cloning mode we have to update levelParent to
377 // reflect that we visited the node (even though we didn't process it).
378 // If we don't do that, in next iterations nodes will be appended to wrong parent.
380 // We can just take first child because the algorithm guarantees
381 // that this will be the only child on this level. (http://dev.ckeditor.com/ticket/13568)
382 levelParent
= levelParent
.getChild( 0 );
386 // Delete or Extract.
387 // We need to update the range and if mergeThen was passed do it.
392 // Depending on an action:
393 // * clones node and adds to new parent,
395 // * moves node to the new parent.
396 function consume( node
, newParent
, toStart
, forceClone
) {
397 var nextSibling
= toStart
? node
.getPrevious() : node
.getNext();
399 // We do not clone if we are only deleting, so do nothing.
400 if ( forceClone
&& isDelete
) {
404 // If cloning, just clone it.
405 if ( isClone
|| forceClone
) {
406 newParent
.append( node
.clone( true, cloneId
), toStart
);
408 // Both Delete and Extract will remove the node.
411 // When Extracting, move the removed node to the docFrag.
413 newParent
.append( node
, toStart
);
420 // Finds a level number on which both branches starts diverging.
421 // If such level does not exist, return the last on which both branches have nodes.
422 function findMinLevel() {
423 // Compare them, to find the top most siblings.
424 var i
, topStart
, topEnd
,
425 maxLevel
= Math
.min( startParents
.length
, endParents
.length
);
427 for ( i
= 0; i
< maxLevel
; i
++ ) {
428 topStart
= startParents
[ i
];
429 topEnd
= endParents
[ i
];
431 // The compared nodes will match until we find the top most siblings (different nodes that have the same parent).
432 // "i" will hold the index in the parents array for the top most element.
433 if ( !topStart
.equals( topEnd
) ) {
438 // When startNode == endNode.
442 // Executed only when deleting or extracting to update range position
443 // and perform the merge operation.
444 function mergeAndUpdate() {
445 var commonLevel
= minLevel
- 1,
446 boundariesInEmptyNode
= doNotRemoveStartNode
&& doNotRemoveEndNode
&& !startNode
.equals( endNode
);
448 // If a node has been partially selected, collapse the range between
449 // startParents[ minLevel + 1 ] and endParents[ minLevel + 1 ] (the first diverged elements).
450 // Otherwise, simply collapse it to the start. (W3C specs).
454 // It took me few hours to truly understand a previous version of this condition.
455 // Mine seems to be more straightforward (even if it doesn't look so) and I could leave you here
456 // without additional comments, but I'm not that mean so here goes the explanation.
458 // We want to know if both ends of the range are anchored in the same element. Really. It's this simple.
459 // But why? Because we need to differentiate situations like:
461 // <p>foo[<b>x</b>bar]y</p> (commonLevel = p, maxLL = "foo", maxLR = "y")
463 // <p>foo<b>x[</b>bar]y</p> (commonLevel = p, maxLL = "x", maxLR = "y")
465 // In the first case we can collapse the range to the left, because simply everything between range's
466 // boundaries was removed.
467 // In the second case we must place the range after </b>, because <b> was only **partially selected**.
469 // * <b> is our startParents[ commonLevel + 1 ]
470 // * "y" is our endParents[ commonLevel + 1 ].
472 // By now "bar" is removed from the DOM so <b> is a direct sibling of "y":
473 // <p>foo<b>x</b>y</p>
475 // Therefore it's enough to place the range between <b> and "y".
477 // Now, what does the comparison mean? Why not just taking startNode and endNode and checking
478 // their parents? Because the tree is already changed and they may be gone. Plus, thanks to
479 // cloneStartNode and cloneEndNode, that would be reaaaaly tricky.
481 // So we play with levels which can give us the same information:
482 // * commonLevel - the level of common ancestor,
483 // * maxLevel - 1 - the level of range boundary parent (range boundary is here like a bookmark span).
484 // * commonLevel < maxLevel - 1 - whether the range boundary is not a child of common ancestor.
486 // There's also an edge case in which both range boundaries were placed in empty nodes like:
488 // Those boundaries were not removed, but in this case start and end nodes are child of the common ancestor.
489 // We handle this edge case separately.
490 if ( commonLevel
< ( maxLevelLeft
- 1 ) || commonLevel
< ( maxLevelRight
- 1 ) || boundariesInEmptyNode
) {
491 if ( boundariesInEmptyNode
) {
492 range
.moveToPosition( endNode
, CKEDITOR
.POSITION_BEFORE_START
);
493 } else if ( ( maxLevelRight
== commonLevel
+ 1 ) && cloneEndNode
) {
494 // The maxLevelRight + 1 element could be already removed so we use the fact that
495 // we know that it was the last element in its parent.
496 range
.moveToPosition( endParents
[ commonLevel
], CKEDITOR
.POSITION_BEFORE_END
);
498 range
.moveToPosition( endParents
[ commonLevel
+ 1 ], CKEDITOR
.POSITION_BEFORE_START
);
501 // Merge split parents.
503 // Find the first diverged node in the left branch.
504 var topLeft
= startParents
[ commonLevel
+ 1 ];
506 // TopLeft may simply not exist if commonLevel == maxLevel or may be a text node.
507 if ( topLeft
&& topLeft
.type
== CKEDITOR
.NODE_ELEMENT
) {
508 var span
= CKEDITOR
.dom
.element
.createFromHtml( '<span ' +
509 'data-cke-bookmark="1" style="display:none"> </span>', range
.document
);
510 span
.insertAfter( topLeft
);
511 topLeft
.mergeSiblings( false );
512 range
.moveToBookmark( { startNode: span
} );
516 // Collapse it to the start.
517 range
.collapse( true );
522 var inlineChildReqElements
= {
523 abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
524 dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
525 strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1
528 // Creates the appropriate node evaluator for the dom walker used inside
529 // check(Start|End)OfBlock.
530 function getCheckStartEndBlockEvalFunction() {
531 var skipBogus
= false,
532 whitespaces
= CKEDITOR
.dom
.walker
.whitespaces(),
533 bookmarkEvaluator
= CKEDITOR
.dom
.walker
.bookmark( true ),
534 isBogus
= CKEDITOR
.dom
.walker
.bogus();
536 return function( node
) {
537 // First skip empty nodes
538 if ( bookmarkEvaluator( node
) || whitespaces( node
) )
541 // Skip the bogus node at the end of block.
542 if ( isBogus( node
) && !skipBogus
) {
547 // If there's any visible text, then we're not at the start.
548 if ( node
.type
== CKEDITOR
.NODE_TEXT
&&
549 ( node
.hasAscendant( 'pre' ) ||
550 CKEDITOR
.tools
.trim( node
.getText() ).length
) ) {
554 // If there are non-empty inline elements (e.g. <img />), then we're not
556 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& !node
.is( inlineChildReqElements
) )
563 var isBogus
= CKEDITOR
.dom
.walker
.bogus(),
564 nbspRegExp
= /^[\t\r\n ]*(?: 
;|\xa0)$/,
565 editableEval
= CKEDITOR
.dom
.walker
.editable(),
566 notIgnoredEval
= CKEDITOR
.dom
.walker
.ignored( true );
568 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
569 // text node and non-empty elements unless it's being bookmark text.
570 function elementBoundaryEval( checkStart
) {
571 var whitespaces
= CKEDITOR
.dom
.walker
.whitespaces(),
572 bookmark
= CKEDITOR
.dom
.walker
.bookmark( 1 );
574 return function( node
) {
575 // First skip empty nodes.
576 if ( bookmark( node
) || whitespaces( node
) )
579 // Tolerant bogus br when checking at the end of block.
580 // Reject any text node unless it's being bookmark
582 // Reject any element unless it's being invisible empty. (http://dev.ckeditor.com/ticket/3883)
583 return !checkStart
&& isBogus( node
) ||
584 node
.type
== CKEDITOR
.NODE_ELEMENT
&&
585 node
.is( CKEDITOR
.dtd
.$removeEmpty
);
589 function getNextEditableNode( isPrevious
) {
593 return this[ isPrevious
? 'getPreviousNode' : 'getNextNode' ]( function( node
) {
594 // Cache first not ignorable node.
595 if ( !first
&& notIgnoredEval( node
) )
598 // Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
599 return editableEval( node
) && !( isBogus( node
) && node
.equals( first
) );
604 CKEDITOR
.dom
.range
.prototype = {
608 * @returns {CKEDITOR.dom.range}
611 var clone
= new CKEDITOR
.dom
.range( this.root
);
613 clone
._setStartContainer( this.startContainer
);
614 clone
.startOffset
= this.startOffset
;
615 clone
._setEndContainer( this.endContainer
);
616 clone
.endOffset
= this.endOffset
;
617 clone
.collapsed
= this.collapsed
;
623 * Makes the range collapsed by moving its start point (or end point if `toStart==true`)
626 * @param {Boolean} toStart Collapse range "to start".
628 collapse: function( toStart
) {
630 this._setEndContainer( this.startContainer
);
631 this.endOffset
= this.startOffset
;
633 this._setStartContainer( this.endContainer
);
634 this.startOffset
= this.endOffset
;
637 this.collapsed
= true;
641 * Clones content nodes of the range and adds them to a document fragment, which is returned.
643 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the clone.
644 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing a clone of range's content.
646 cloneContents: function( cloneId
) {
647 var docFrag
= new CKEDITOR
.dom
.documentFragment( this.document
);
649 cloneId
= typeof cloneId
== 'undefined' ? true : cloneId
;
651 if ( !this.collapsed
)
652 execContentsAction( this, 2, docFrag
, false, cloneId
);
658 * Deletes the content nodes of the range permanently from the DOM tree.
660 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
662 deleteContents: function( mergeThen
) {
663 if ( this.collapsed
)
666 execContentsAction( this, 0, null, mergeThen
);
670 * The content nodes of the range are cloned and added to a document fragment,
671 * meanwhile they are removed permanently from the DOM tree.
673 * **Note:** Setting the `cloneId` parameter to `false` works for **partially** selected elements only.
674 * If an element with an ID attribute is **fully enclosed** in a range, it will keep the ID attribute
675 * regardless of the `cloneId` parameter value, because it is not cloned — it is moved to the returned
678 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
679 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the extracted content.
680 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
682 extractContents: function( mergeThen
, cloneId
) {
683 var docFrag
= new CKEDITOR
.dom
.documentFragment( this.document
);
685 cloneId
= typeof cloneId
== 'undefined' ? true : cloneId
;
687 if ( !this.collapsed
)
688 execContentsAction( this, 1, docFrag
, mergeThen
, cloneId
);
694 * Creates a bookmark object, which can be later used to restore the
695 * range by using the {@link #moveToBookmark} function.
697 * This is an "intrusive" way to create a bookmark. It includes `<span>` tags
698 * in the range boundaries. The advantage of it is that it is possible to
699 * handle DOM mutations when moving back to the bookmark.
701 * **Note:** The inclusion of nodes in the DOM is a design choice and
702 * should not be changed as there are other points in the code that may be
703 * using those nodes to perform operations.
705 * @param {Boolean} [serializable] Indicates that the bookmark nodes
706 * must contain IDs, which can be used to restore the range even
707 * when these nodes suffer mutations (like cloning or `innerHTML` change).
708 * @returns {Object} And object representing a bookmark.
709 * @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
710 * @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
711 * @returns {Boolean} return.serializable
712 * @returns {Boolean} return.collapsed
714 createBookmark: function( serializable
) {
715 var startNode
, endNode
;
718 var collapsed
= this.collapsed
;
720 startNode
= this.document
.createElement( 'span' );
721 startNode
.data( 'cke-bookmark', 1 );
722 startNode
.setStyle( 'display', 'none' );
724 // For IE, it must have something inside, otherwise it may be
725 // removed during DOM operations.
726 startNode
.setHtml( ' ' );
728 if ( serializable
) {
729 baseId
= 'cke_bm_' + CKEDITOR
.tools
.getNextNumber();
730 startNode
.setAttribute( 'id', baseId
+ ( collapsed
? 'C' : 'S' ) );
733 // If collapsed, the endNode will not be created.
735 endNode
= startNode
.clone();
736 endNode
.setHtml( ' ' );
739 endNode
.setAttribute( 'id', baseId
+ 'E' );
741 clone
= this.clone();
743 clone
.insertNode( endNode
);
746 clone
= this.clone();
747 clone
.collapse( true );
748 clone
.insertNode( startNode
);
750 // Update the range position.
752 this.setStartAfter( startNode
);
753 this.setEndBefore( endNode
);
755 this.moveToPosition( startNode
, CKEDITOR
.POSITION_AFTER_END
);
759 startNode: serializable
? baseId
+ ( collapsed
? 'C' : 'S' ) : startNode
,
760 endNode: serializable
? baseId
+ 'E' : endNode
,
761 serializable: serializable
,
767 * Creates a "non intrusive" and "mutation sensible" bookmark. This
768 * kind of bookmark should be used only when the DOM is supposed to
769 * remain stable after its creation.
771 * @param {Boolean} [normalized] Indicates that the bookmark must
772 * be normalized. When normalized, the successive text nodes are
773 * considered a single node. To successfully load a normalized
774 * bookmark, the DOM tree must also be normalized before calling
775 * {@link #moveToBookmark}.
776 * @returns {Object} An object representing the bookmark.
777 * @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
778 * @returns {Array} return.end Start container's address.
779 * @returns {Number} return.startOffset
780 * @returns {Number} return.endOffset
781 * @returns {Boolean} return.collapsed
782 * @returns {Boolean} return.normalized
783 * @returns {Boolean} return.is2 This is "bookmark2".
785 createBookmark2: ( function() {
786 var isNotText
= CKEDITOR
.dom
.walker
.nodeType( CKEDITOR
.NODE_TEXT
, true );
788 // Returns true for limit anchored in element and placed between text nodes.
791 // <p>[text node] [text node]</p> -> true
794 // <p> [text node]</p> -> false
797 // <p>[text node][text node]</p> -> false (limit is anchored in text node)
798 function betweenTextNodes( container
, offset
) {
799 // Not anchored in element or limit is on the edge.
800 if ( container
.type
!= CKEDITOR
.NODE_ELEMENT
|| offset
=== 0 || offset
== container
.getChildCount() )
803 return container
.getChild( offset
- 1 ).type
== CKEDITOR
.NODE_TEXT
&&
804 container
.getChild( offset
).type
== CKEDITOR
.NODE_TEXT
;
807 // Sums lengths of all preceding text nodes.
808 function getLengthOfPrecedingTextNodes( node
) {
811 while ( ( node
= node
.getPrevious() ) && node
.type
== CKEDITOR
.NODE_TEXT
)
812 sum
+= node
.getText().replace( CKEDITOR
.dom
.selection
.FILLING_CHAR_SEQUENCE
, '' ).length
;
817 function normalizeTextNodes( limit
) {
818 var container
= limit
.container
,
819 offset
= limit
.offset
;
821 // If limit is between text nodes move it to the end of preceding one,
822 // because they will be merged.
823 if ( betweenTextNodes( container
, offset
) ) {
824 container
= container
.getChild( offset
- 1 );
825 offset
= container
.getLength();
828 // Now, if limit is anchored in element and has at least one node before it,
829 // it may happen that some of them will be merged. Normalize the offset
830 // by setting it to normalized index of its preceding, safe node.
831 // (safe == one for which getIndex(true) does not return -1, so one which won't disappear).
832 if ( container
.type
== CKEDITOR
.NODE_ELEMENT
&& offset
> 0 ) {
833 offset
= getPrecedingSafeNodeIndex( container
, offset
) + 1;
836 // The last step - fix the offset inside text node by adding
837 // lengths of preceding text nodes which will be merged with container.
838 if ( container
.type
== CKEDITOR
.NODE_TEXT
) {
839 var precedingLength
= getLengthOfPrecedingTextNodes( container
);
841 // Normal case - text node is not empty.
842 if ( container
.getText() ) {
843 offset
+= precedingLength
;
845 // Awful case - the text node is empty and thus will be totally lost.
846 // In this case we are trying to normalize the limit to the left:
847 // * either to the preceding text node,
848 // * or to the "gap" after the preceding element.
850 // Find the closest non-text sibling.
851 var precedingContainer
= container
.getPrevious( isNotText
);
853 // If there are any characters on the left, that means that we can anchor
854 // there, because this text node will not be lost.
855 if ( precedingLength
) {
856 offset
= precedingLength
;
858 if ( precedingContainer
) {
859 // The text node is the first node after the closest non-text sibling.
860 container
= precedingContainer
.getNext();
862 // But if there was no non-text sibling, then the text node is the first child.
863 container
= container
.getParent().getFirst();
866 // If there are no characters on the left, then anchor after the previous non-text node.
867 // E.g. (see tests for a legend :D):
868 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
870 container
= container
.getParent();
871 offset
= precedingContainer
? ( precedingContainer
.getIndex( true ) + 1 ) : 0;
876 limit
.container
= container
;
877 limit
.offset
= offset
;
880 function normalizeFCSeq( limit
, root
) {
881 var fcseq
= root
.getCustomData( 'cke-fillingChar' );
887 var container
= limit
.container
;
889 if ( fcseq
.equals( container
) ) {
890 limit
.offset
-= CKEDITOR
.dom
.selection
.FILLING_CHAR_SEQUENCE
.length
;
892 // == 0 handles case when limit was at the end of FCS.
893 // < 0 handles all cases where limit was somewhere in the middle or at the beginning.
894 // > 0 (the "else" case) means cases where there are some more characters in the FCS node (FCSabc^def).
895 if ( limit
.offset
<= 0 ) {
896 limit
.offset
= container
.getIndex();
897 limit
.container
= container
.getParent();
902 // And here goes the funny part - all other cases are handled inside node.getAddress() and getIndex() thanks to
903 // node.getIndex() being aware of FCS (handling it as an empty node).
906 // Finds a normalized index of a safe node preceding this one.
907 // Safe == one that will not disappear, so one for which getIndex( true ) does not return -1.
908 // Return -1 if there's no safe preceding node.
909 function getPrecedingSafeNodeIndex( container
, offset
) {
913 index
= container
.getChild( offset
).getIndex( true );
922 return function( normalized
) {
923 var collapsed
= this.collapsed
,
925 container: this.startContainer
,
926 offset: this.startOffset
929 container: this.endContainer
,
930 offset: this.endOffset
934 normalizeTextNodes( bmStart
);
935 normalizeFCSeq( bmStart
, this.root
);
938 normalizeTextNodes( bmEnd
);
939 normalizeFCSeq( bmEnd
, this.root
);
944 start: bmStart
.container
.getAddress( normalized
),
945 end: collapsed
? null : bmEnd
.container
.getAddress( normalized
),
946 startOffset: bmStart
.offset
,
947 endOffset: bmEnd
.offset
,
948 normalized: normalized
,
949 collapsed: collapsed
,
950 is2: true // It's a createBookmark2 bookmark.
956 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
958 * If serializable bookmark passed, then its `<span>` markers will be removed.
960 * @param {Object} bookmark
962 moveToBookmark: function( bookmark
) {
963 // Created with createBookmark2().
964 if ( bookmark
.is2
) {
965 // Get the start information.
966 var startContainer
= this.document
.getByAddress( bookmark
.start
, bookmark
.normalized
),
967 startOffset
= bookmark
.startOffset
;
969 // Get the end information.
970 var endContainer
= bookmark
.end
&& this.document
.getByAddress( bookmark
.end
, bookmark
.normalized
),
971 endOffset
= bookmark
.endOffset
;
973 // Set the start boundary.
974 this.setStart( startContainer
, startOffset
);
976 // Set the end boundary. If not available, collapse it.
978 this.setEnd( endContainer
, endOffset
);
980 this.collapse( true );
982 // Created with createBookmark().
984 var serializable
= bookmark
.serializable
,
985 startNode
= serializable
? this.document
.getById( bookmark
.startNode
) : bookmark
.startNode
,
986 endNode
= serializable
? this.document
.getById( bookmark
.endNode
) : bookmark
.endNode
;
988 // Set the range start at the bookmark start node position.
989 this.setStartBefore( startNode
);
991 // Remove it, because it may interfere in the setEndBefore call.
994 // Set the range end at the bookmark end node position, or simply
995 // collapse it if it is not available.
997 this.setEndBefore( endNode
);
1000 this.collapse( true );
1006 * Returns two nodes which are on the boundaries of this range.
1009 * @returns {CKEDITOR.dom.node} return.startNode
1010 * @returns {CKEDITOR.dom.node} return.endNode
1011 * @todo precise desc/algorithm
1013 getBoundaryNodes: function() {
1014 var startNode
= this.startContainer
,
1015 endNode
= this.endContainer
,
1016 startOffset
= this.startOffset
,
1017 endOffset
= this.endOffset
,
1020 if ( startNode
.type
== CKEDITOR
.NODE_ELEMENT
) {
1021 childCount
= startNode
.getChildCount();
1022 if ( childCount
> startOffset
) {
1023 startNode
= startNode
.getChild( startOffset
);
1024 } else if ( childCount
< 1 ) {
1025 startNode
= startNode
.getPreviousSourceNode();
1027 // startOffset > childCount but childCount is not 0
1029 // Try to take the node just after the current position.
1030 startNode
= startNode
.$;
1031 while ( startNode
.lastChild
)
1032 startNode
= startNode
.lastChild
;
1033 startNode
= new CKEDITOR
.dom
.node( startNode
);
1035 // Normally we should take the next node in DFS order. But it
1036 // is also possible that we've already reached the end of
1038 startNode
= startNode
.getNextSourceNode() || startNode
;
1042 if ( endNode
.type
== CKEDITOR
.NODE_ELEMENT
) {
1043 childCount
= endNode
.getChildCount();
1044 if ( childCount
> endOffset
) {
1045 endNode
= endNode
.getChild( endOffset
).getPreviousSourceNode( true );
1046 } else if ( childCount
< 1 ) {
1047 endNode
= endNode
.getPreviousSourceNode();
1049 // endOffset > childCount but childCount is not 0.
1051 // Try to take the node just before the current position.
1052 endNode
= endNode
.$;
1053 while ( endNode
.lastChild
)
1054 endNode
= endNode
.lastChild
;
1055 endNode
= new CKEDITOR
.dom
.node( endNode
);
1059 // Sometimes the endNode will come right before startNode for collapsed
1060 // ranges. Fix it. (http://dev.ckeditor.com/ticket/3780)
1061 if ( startNode
.getPosition( endNode
) & CKEDITOR
.POSITION_FOLLOWING
)
1062 startNode
= endNode
;
1064 return { startNode: startNode
, endNode: endNode
};
1068 * Find the node which fully contains the range.
1070 * @param {Boolean} [includeSelf=false]
1071 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1072 * @returns {CKEDITOR.dom.element}
1074 getCommonAncestor: function( includeSelf
, ignoreTextNode
) {
1075 var start
= this.startContainer
,
1076 end
= this.endContainer
,
1079 if ( start
.equals( end
) ) {
1080 if ( includeSelf
&& start
.type
== CKEDITOR
.NODE_ELEMENT
&& this.startOffset
== this.endOffset
- 1 )
1081 ancestor
= start
.getChild( this.startOffset
);
1085 ancestor
= start
.getCommonAncestor( end
);
1088 return ignoreTextNode
&& !ancestor
.is
? ancestor
.getParent() : ancestor
;
1092 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1093 * nodes to element nodes, whenever possible. This is actually possible
1094 * if either of the boundary containers point to a text node, and its
1095 * offset is set to zero, or after the last char in the node.
1097 optimize: function() {
1098 var container
= this.startContainer
;
1099 var offset
= this.startOffset
;
1101 if ( container
.type
!= CKEDITOR
.NODE_ELEMENT
) {
1103 this.setStartBefore( container
);
1104 else if ( offset
>= container
.getLength() )
1105 this.setStartAfter( container
);
1108 container
= this.endContainer
;
1109 offset
= this.endOffset
;
1111 if ( container
.type
!= CKEDITOR
.NODE_ELEMENT
) {
1113 this.setEndBefore( container
);
1114 else if ( offset
>= container
.getLength() )
1115 this.setEndAfter( container
);
1120 * Move the range out of bookmark nodes if they'd been the container.
1122 optimizeBookmark: function() {
1123 var startNode
= this.startContainer
,
1124 endNode
= this.endContainer
;
1126 if ( startNode
.is
&& startNode
.is( 'span' ) && startNode
.data( 'cke-bookmark' ) )
1127 this.setStartAt( startNode
, CKEDITOR
.POSITION_BEFORE_START
);
1128 if ( endNode
&& endNode
.is
&& endNode
.is( 'span' ) && endNode
.data( 'cke-bookmark' ) )
1129 this.setEndAt( endNode
, CKEDITOR
.POSITION_AFTER_END
);
1133 * @param {Boolean} [ignoreStart=false]
1134 * @param {Boolean} [ignoreEnd=false]
1135 * @todo precise desc/algorithm
1137 trim: function( ignoreStart
, ignoreEnd
) {
1138 var startContainer
= this.startContainer
,
1139 startOffset
= this.startOffset
,
1140 collapsed
= this.collapsed
;
1141 if ( ( !ignoreStart
|| collapsed
) && startContainer
&& startContainer
.type
== CKEDITOR
.NODE_TEXT
) {
1142 // If the offset is zero, we just insert the new node before
1144 if ( !startOffset
) {
1145 startOffset
= startContainer
.getIndex();
1146 startContainer
= startContainer
.getParent();
1148 // If the offset is at the end, we'll insert it after the text
1150 else if ( startOffset
>= startContainer
.getLength() ) {
1151 startOffset
= startContainer
.getIndex() + 1;
1152 startContainer
= startContainer
.getParent();
1154 // In other case, we split the text node and insert the new
1155 // node at the split point.
1157 var nextText
= startContainer
.split( startOffset
);
1159 startOffset
= startContainer
.getIndex() + 1;
1160 startContainer
= startContainer
.getParent();
1162 // Check all necessity of updating the end boundary.
1163 if ( this.startContainer
.equals( this.endContainer
) )
1164 this.setEnd( nextText
, this.endOffset
- this.startOffset
);
1165 else if ( startContainer
.equals( this.endContainer
) )
1166 this.endOffset
+= 1;
1169 this.setStart( startContainer
, startOffset
);
1172 this.collapse( true );
1177 var endContainer
= this.endContainer
;
1178 var endOffset
= this.endOffset
;
1180 if ( !( ignoreEnd
|| collapsed
) && endContainer
&& endContainer
.type
== CKEDITOR
.NODE_TEXT
) {
1181 // If the offset is zero, we just insert the new node before
1184 endOffset
= endContainer
.getIndex();
1185 endContainer
= endContainer
.getParent();
1187 // If the offset is at the end, we'll insert it after the text
1189 else if ( endOffset
>= endContainer
.getLength() ) {
1190 endOffset
= endContainer
.getIndex() + 1;
1191 endContainer
= endContainer
.getParent();
1193 // In other case, we split the text node and insert the new
1194 // node at the split point.
1196 endContainer
.split( endOffset
);
1198 endOffset
= endContainer
.getIndex() + 1;
1199 endContainer
= endContainer
.getParent();
1202 this.setEnd( endContainer
, endOffset
);
1207 * Expands the range so that partial units are completely contained.
1209 * @param {Number} unit The unit type to expand with. Use one of following values: {@link CKEDITOR#ENLARGE_BLOCK_CONTENTS},
1210 * {@link CKEDITOR#ENLARGE_ELEMENT}, {@link CKEDITOR#ENLARGE_INLINE}, {@link CKEDITOR#ENLARGE_LIST_ITEM_CONTENTS}.
1211 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1213 enlarge: function( unit
, excludeBrs
) {
1214 var leadingWhitespaceRegex
= new RegExp( /[^\s\ufeff]/ );
1217 case CKEDITOR
.ENLARGE_INLINE:
1218 var enlargeInlineOnly
= 1;
1221 case CKEDITOR
.ENLARGE_ELEMENT:
1223 if ( this.collapsed
)
1226 // Get the common ancestor.
1227 var commonAncestor
= this.getCommonAncestor();
1229 var boundary
= this.root
;
1231 // For each boundary
1232 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1233 // if not available, to be enlarge.
1234 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1235 // common ancestor is not reached. After reaching the common ancestor, just save the
1236 // enlargeable node to be used later.
1238 var startTop
, endTop
;
1240 var enlargeable
, sibling
, commonReached
;
1242 // Indicates that the node can be added only if whitespace
1243 // is available before it.
1244 var needsWhiteSpace
= false;
1248 // Process the start boundary.
1250 var container
= this.startContainer
;
1251 var offset
= this.startOffset
;
1253 if ( container
.type
== CKEDITOR
.NODE_TEXT
) {
1255 // Check if there is any non-space text before the
1256 // offset. Otherwise, container is null.
1257 container
= !CKEDITOR
.tools
.trim( container
.substring( 0, offset
) ).length
&& container
;
1259 // If we found only whitespace in the node, it
1260 // means that we'll need more whitespace to be able
1261 // to expand. For example, <i> can be expanded in
1262 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1263 needsWhiteSpace
= !!container
;
1267 if ( !( sibling
= container
.getPrevious() ) )
1268 enlargeable
= container
.getParent();
1271 // If we have offset, get the node preceeding it as the
1272 // first sibling to be checked.
1274 sibling
= container
.getChild( offset
- 1 ) || container
.getLast();
1276 // If there is no sibling, mark the container to be
1279 enlargeable
= container
;
1282 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1283 enlargeable
= getValidEnlargeable( enlargeable
);
1285 while ( enlargeable
|| sibling
) {
1286 if ( enlargeable
&& !sibling
) {
1287 // If we reached the common ancestor, mark the flag
1289 if ( !commonReached
&& enlargeable
.equals( commonAncestor
) )
1290 commonReached
= true;
1292 if ( enlargeInlineOnly
? enlargeable
.isBlockBoundary() : !boundary
.contains( enlargeable
) )
1295 // If we don't need space or this element breaks
1296 // the line, then enlarge it.
1297 if ( !needsWhiteSpace
|| enlargeable
.getComputedStyle( 'display' ) != 'inline' ) {
1298 needsWhiteSpace
= false;
1300 // If the common ancestor has been reached,
1301 // we'll not enlarge it immediately, but just
1302 // mark it to be enlarged later if the end
1303 // boundary also enlarges it.
1304 if ( commonReached
)
1305 startTop
= enlargeable
;
1307 this.setStartBefore( enlargeable
);
1310 sibling
= enlargeable
.getPrevious();
1313 // Check all sibling nodes preceeding the enlargeable
1314 // node. The node wil lbe enlarged only if none of them
1317 // This flag indicates that this node has
1318 // whitespaces at the end.
1319 isWhiteSpace
= false;
1321 if ( sibling
.type
== CKEDITOR
.NODE_COMMENT
) {
1322 sibling
= sibling
.getPrevious();
1324 } else if ( sibling
.type
== CKEDITOR
.NODE_TEXT
) {
1325 siblingText
= sibling
.getText();
1327 if ( leadingWhitespaceRegex
.test( siblingText
) )
1330 isWhiteSpace
= /[\s\ufeff]$/.test( siblingText
);
1332 // http://dev.ckeditor.com/ticket/12221 (Chrome) plus http://dev.ckeditor.com/ticket/11111 (Safari).
1333 var offsetWidth0
= CKEDITOR
.env
.webkit
? 1 : 0;
1335 // If this is a visible element.
1336 // We need to check for the bookmark attribute because IE insists on
1337 // rendering the display:none nodes we use for bookmarks. (http://dev.ckeditor.com/ticket/3363)
1338 // Line-breaks (br) are rendered with zero width, which we don't want to include. (http://dev.ckeditor.com/ticket/7041)
1339 if ( ( sibling
.$.offsetWidth
> offsetWidth0
|| excludeBrs
&& sibling
.is( 'br' ) ) && !sibling
.data( 'cke-bookmark' ) ) {
1340 // We'll accept it only if we need
1341 // whitespace, and this is an inline
1342 // element with whitespace only.
1343 if ( needsWhiteSpace
&& CKEDITOR
.dtd
.$removeEmpty
[ sibling
.getName() ] ) {
1344 // It must contains spaces and inline elements only.
1346 siblingText
= sibling
.getText();
1348 if ( leadingWhitespaceRegex
.test( siblingText
) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1351 var allChildren
= sibling
.$.getElementsByTagName( '*' );
1352 for ( var i
= 0, child
; child
= allChildren
[ i
++ ]; ) {
1353 if ( !CKEDITOR
.dtd
.$removeEmpty
[ child
.nodeName
.toLowerCase() ] ) {
1361 isWhiteSpace
= !!siblingText
.length
;
1368 // A node with whitespaces has been found.
1369 if ( isWhiteSpace
) {
1370 // Enlarge the last enlargeable node, if we
1371 // were waiting for spaces.
1372 if ( needsWhiteSpace
) {
1373 if ( commonReached
)
1374 startTop
= enlargeable
;
1375 else if ( enlargeable
)
1376 this.setStartBefore( enlargeable
);
1378 needsWhiteSpace
= true;
1383 var next
= sibling
.getPrevious();
1385 if ( !enlargeable
&& !next
) {
1386 // Set the sibling as enlargeable, so it's
1387 // parent will be get later outside this while.
1388 enlargeable
= sibling
;
1395 // If sibling has been set to null, then we
1396 // need to stop enlarging.
1402 enlargeable
= getValidEnlargeable( enlargeable
.getParent() );
1405 // Process the end boundary. This is basically the same
1406 // code used for the start boundary, with small changes to
1407 // make it work in the opposite side (to the right). This
1408 // makes it difficult to reuse the code here. So, fixes to
1409 // the above code are likely to be replicated here.
1411 container
= this.endContainer
;
1412 offset
= this.endOffset
;
1414 // Reset the common variables.
1415 enlargeable
= sibling
= null;
1416 commonReached
= needsWhiteSpace
= false;
1418 // Function check if there are only whitespaces from the given starting point
1419 // (startContainer and startOffset) till the end on block.
1420 // Examples ("[" is the start point):
1421 // - <p>foo[ </p> - will return true,
1422 // - <p><b>foo[ </b> </p> - will return true,
1423 // - <p>foo[ bar</p> - will return false,
1424 // - <p><b>foo[ </b>bar</p> - will return false,
1425 // - <p>foo[ <b></b></p> - will return false.
1426 function onlyWhiteSpaces( startContainer
, startOffset
) {
1427 // We need to enlarge range if there is white space at the end of the block,
1428 // because it is not displayed in WYSIWYG mode and user can not select it. So
1429 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1430 // we should do nothing if we are not at the end of the block, so this should not
1431 // be changed: "<p><i>[foo] </i>bar</p>".
1432 var walkerRange
= new CKEDITOR
.dom
.range( boundary
);
1433 walkerRange
.setStart( startContainer
, startOffset
);
1434 // The guard will find the end of range so I put boundary here.
1435 walkerRange
.setEndAt( boundary
, CKEDITOR
.POSITION_BEFORE_END
);
1437 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
1440 walker
.guard = function( node
) {
1441 // Stop if you exit block.
1442 return !( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isBlockBoundary() );
1445 while ( ( node
= walker
.next() ) ) {
1446 if ( node
.type
!= CKEDITOR
.NODE_TEXT
) {
1447 // Stop if you enter to any node (walker.next() will return node only
1448 // it goes out, not if it is go into node).
1451 // Trim the first node to startOffset.
1452 if ( node
!= startContainer
)
1453 siblingText
= node
.getText();
1455 siblingText
= node
.substring( startOffset
);
1457 // Check if it is white space.
1458 if ( leadingWhitespaceRegex
.test( siblingText
) )
1466 if ( container
.type
== CKEDITOR
.NODE_TEXT
) {
1467 // Check if there is only white space after the offset.
1468 if ( CKEDITOR
.tools
.trim( container
.substring( offset
) ).length
) {
1469 // If we found only whitespace in the node, it
1470 // means that we'll need more whitespace to be able
1471 // to expand. For example, <i> can be expanded in
1472 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1473 needsWhiteSpace
= true;
1475 needsWhiteSpace
= !container
.getLength();
1477 if ( offset
== container
.getLength() ) {
1478 // If we are at the end of container and this is the last text node,
1479 // we should enlarge end to the parent.
1480 if ( !( sibling
= container
.getNext() ) )
1481 enlargeable
= container
.getParent();
1483 // If we are in the middle on text node and there are only whitespaces
1484 // till the end of block, we should enlarge element.
1485 if ( onlyWhiteSpaces( container
, offset
) )
1486 enlargeable
= container
.getParent();
1490 // Get the node right after the boundary to be checked
1492 sibling
= container
.getChild( offset
);
1495 enlargeable
= container
;
1498 while ( enlargeable
|| sibling
) {
1499 if ( enlargeable
&& !sibling
) {
1500 if ( !commonReached
&& enlargeable
.equals( commonAncestor
) )
1501 commonReached
= true;
1503 if ( enlargeInlineOnly
? enlargeable
.isBlockBoundary() : !boundary
.contains( enlargeable
) )
1506 if ( !needsWhiteSpace
|| enlargeable
.getComputedStyle( 'display' ) != 'inline' ) {
1507 needsWhiteSpace
= false;
1509 if ( commonReached
)
1510 endTop
= enlargeable
;
1511 else if ( enlargeable
)
1512 this.setEndAfter( enlargeable
);
1515 sibling
= enlargeable
.getNext();
1519 isWhiteSpace
= false;
1521 if ( sibling
.type
== CKEDITOR
.NODE_TEXT
) {
1522 siblingText
= sibling
.getText();
1524 // Check if there are not whitespace characters till the end of editable.
1525 // If so stop expanding.
1526 if ( !onlyWhiteSpaces( sibling
, 0 ) )
1529 isWhiteSpace
= /^[\s\ufeff]/.test( siblingText
);
1530 } else if ( sibling
.type
== CKEDITOR
.NODE_ELEMENT
) {
1531 // If this is a visible element.
1532 // We need to check for the bookmark attribute because IE insists on
1533 // rendering the display:none nodes we use for bookmarks. (http://dev.ckeditor.com/ticket/3363)
1534 // Line-breaks (br) are rendered with zero width, which we don't want to include. (http://dev.ckeditor.com/ticket/7041)
1535 if ( ( sibling
.$.offsetWidth
> 0 || excludeBrs
&& sibling
.is( 'br' ) ) && !sibling
.data( 'cke-bookmark' ) ) {
1536 // We'll accept it only if we need
1537 // whitespace, and this is an inline
1538 // element with whitespace only.
1539 if ( needsWhiteSpace
&& CKEDITOR
.dtd
.$removeEmpty
[ sibling
.getName() ] ) {
1540 // It must contains spaces and inline elements only.
1542 siblingText
= sibling
.getText();
1544 if ( leadingWhitespaceRegex
.test( siblingText
) )
1547 allChildren
= sibling
.$.getElementsByTagName( '*' );
1548 for ( i
= 0; child
= allChildren
[ i
++ ]; ) {
1549 if ( !CKEDITOR
.dtd
.$removeEmpty
[ child
.nodeName
.toLowerCase() ] ) {
1557 isWhiteSpace
= !!siblingText
.length
;
1566 if ( isWhiteSpace
) {
1567 if ( needsWhiteSpace
) {
1568 if ( commonReached
)
1569 endTop
= enlargeable
;
1571 this.setEndAfter( enlargeable
);
1576 next
= sibling
.getNext();
1578 if ( !enlargeable
&& !next
) {
1579 enlargeable
= sibling
;
1586 // If sibling has been set to null, then we
1587 // need to stop enlarging.
1593 enlargeable
= getValidEnlargeable( enlargeable
.getParent() );
1596 // If the common ancestor can be enlarged by both boundaries, then include it also.
1597 if ( startTop
&& endTop
) {
1598 commonAncestor
= startTop
.contains( endTop
) ? endTop : startTop
;
1600 this.setStartBefore( commonAncestor
);
1601 this.setEndAfter( commonAncestor
);
1605 case CKEDITOR
.ENLARGE_BLOCK_CONTENTS:
1606 case CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS:
1608 // Enlarging the start boundary.
1609 var walkerRange
= new CKEDITOR
.dom
.range( this.root
);
1611 boundary
= this.root
;
1613 walkerRange
.setStartAt( boundary
, CKEDITOR
.POSITION_AFTER_START
);
1614 walkerRange
.setEnd( this.startContainer
, this.startOffset
);
1616 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
1617 blockBoundary
, // The node on which the enlarging should stop.
1618 tailBr
, // In case BR as block boundary.
1619 notBlockBoundary
= CKEDITOR
.dom
.walker
.blockBoundary( ( unit
== CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS
) ? { br: 1 } : null ),
1620 inNonEditable
= null,
1621 // Record the encountered 'blockBoundary' for later use.
1622 boundaryGuard = function( node
) {
1623 // We should not check contents of non-editable elements. It may happen
1624 // that inline widget has display:table child which should not block range#enlarge.
1625 // When encountered non-editable element...
1626 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.getAttribute( 'contenteditable' ) == 'false' ) {
1627 if ( inNonEditable
) {
1628 // ... in which we already were, reset it (because we're leaving it) and return.
1629 if ( inNonEditable
.equals( node
) ) {
1630 inNonEditable
= null;
1633 // ... which we're entering, remember it but check it (no return).
1635 inNonEditable
= node
;
1637 // When we are in non-editable element, do not check if current node is a block boundary.
1638 } else if ( inNonEditable
) {
1642 var retval
= notBlockBoundary( node
);
1644 blockBoundary
= node
;
1647 // Record the encountered 'tailBr' for later use.
1648 tailBrGuard = function( node
) {
1649 var retval
= boundaryGuard( node
);
1650 if ( !retval
&& node
.is
&& node
.is( 'br' ) )
1655 walker
.guard
= boundaryGuard
;
1657 enlargeable
= walker
.lastBackward();
1659 // It's the body which stop the enlarging if no block boundary found.
1660 blockBoundary
= blockBoundary
|| boundary
;
1662 // Start the range either after the end of found block (<p>...</p>[text)
1663 // or at the start of block (<p>[text...), by comparing the document position
1664 // with 'enlargeable' node.
1665 this.setStartAt( blockBoundary
, !blockBoundary
.is( 'br' ) && ( !enlargeable
&& this.checkStartOfBlock() ||
1666 enlargeable
&& blockBoundary
.contains( enlargeable
) ) ? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_AFTER_END
);
1668 // Avoid enlarging the range further when end boundary spans right after the BR. (http://dev.ckeditor.com/ticket/7490)
1669 if ( unit
== CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS
) {
1670 var theRange
= this.clone();
1671 walker
= new CKEDITOR
.dom
.walker( theRange
);
1673 var whitespaces
= CKEDITOR
.dom
.walker
.whitespaces(),
1674 bookmark
= CKEDITOR
.dom
.walker
.bookmark();
1676 walker
.evaluator = function( node
) {
1677 return !whitespaces( node
) && !bookmark( node
);
1679 var previous
= walker
.previous();
1680 if ( previous
&& previous
.type
== CKEDITOR
.NODE_ELEMENT
&& previous
.is( 'br' ) )
1684 // Enlarging the end boundary.
1685 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1687 walkerRange
= this.clone();
1688 walkerRange
.collapse();
1689 walkerRange
.setEndAt( boundary
, CKEDITOR
.POSITION_BEFORE_END
);
1690 walker
= new CKEDITOR
.dom
.walker( walkerRange
);
1692 // tailBrGuard only used for on range end.
1693 walker
.guard
= ( unit
== CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS
) ? tailBrGuard : boundaryGuard
;
1694 blockBoundary
= inNonEditable
= tailBr
= null;
1696 // End the range right before the block boundary node.
1697 enlargeable
= walker
.lastForward();
1699 // It's the body which stop the enlarging if no block boundary found.
1700 blockBoundary
= blockBoundary
|| boundary
;
1702 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1703 // by comparing the document position with 'enlargeable' node.
1704 this.setEndAt( blockBoundary
, ( !enlargeable
&& this.checkEndOfBlock() ||
1705 enlargeable
&& blockBoundary
.contains( enlargeable
) ) ? CKEDITOR
.POSITION_BEFORE_END : CKEDITOR
.POSITION_BEFORE_START
);
1706 // We must include the <br> at the end of range if there's
1707 // one and we're expanding list item contents
1709 this.setEndAfter( tailBr
);
1713 // Ensures that returned element can be enlarged by selection, null otherwise.
1714 // @param {CKEDITOR.dom.element} enlargeable
1715 // @returns {CKEDITOR.dom.element/null}
1716 function getValidEnlargeable( enlargeable
) {
1717 return enlargeable
&& enlargeable
.type
== CKEDITOR
.NODE_ELEMENT
&& enlargeable
.hasAttribute( 'contenteditable' ) ?
1723 * Decreases the range to make sure that boundaries
1724 * always anchor beside text nodes or the innermost element.
1726 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1728 * * {@link CKEDITOR#SHRINK_ELEMENT} – Shrinks the range boundaries to the edge of the innermost element.
1729 * * {@link CKEDITOR#SHRINK_TEXT} – Shrinks the range boundaries to anchor by the side of enclosed text
1730 * node. The range remains if there are no text nodes available on boundaries.
1732 * @param {Boolean} [selectContents=false] Whether the resulting range anchors at the inner OR outer boundary of the node.
1733 * @param {Boolean/Object} [options=true] If this parameter is of a Boolean type, it is treated as
1734 * `options.shrinkOnBlockBoundary`. This parameter was added in 4.7.0.
1735 * @param {Boolean} [options.shrinkOnBlockBoundary=true] Whether the block boundary should be included in
1737 * @param {Boolean} [options.skipBogus=false] Whether bogus `<br>` elements should be ignored while
1738 * `mode` is set to {@link CKEDITOR#SHRINK_TEXT}. This option was added in 4.7.0.
1740 shrink: function( mode
, selectContents
, options
) {
1741 var shrinkOnBlockBoundary
= typeof options
=== 'boolean' ? options :
1742 ( options
&& typeof options
.shrinkOnBlockBoundary
=== 'boolean' ? options
.shrinkOnBlockBoundary : true ),
1743 skipBogus
= options
&& options
.skipBogus
;
1745 // Unable to shrink a collapsed range.
1746 if ( !this.collapsed
) {
1747 mode
= mode
|| CKEDITOR
.SHRINK_TEXT
;
1749 var walkerRange
= this.clone();
1751 var startContainer
= this.startContainer
,
1752 endContainer
= this.endContainer
,
1753 startOffset
= this.startOffset
,
1754 endOffset
= this.endOffset
;
1756 // Whether the start/end boundary is moveable.
1760 if ( startContainer
&& startContainer
.type
== CKEDITOR
.NODE_TEXT
) {
1762 walkerRange
.setStartBefore( startContainer
);
1763 else if ( startOffset
>= startContainer
.getLength() )
1764 walkerRange
.setStartAfter( startContainer
);
1766 // Enlarge the range properly to avoid walker making
1767 // DOM changes caused by trimming the text nodes later.
1768 walkerRange
.setStartBefore( startContainer
);
1773 if ( endContainer
&& endContainer
.type
== CKEDITOR
.NODE_TEXT
) {
1775 walkerRange
.setEndBefore( endContainer
);
1776 else if ( endOffset
>= endContainer
.getLength() )
1777 walkerRange
.setEndAfter( endContainer
);
1779 walkerRange
.setEndAfter( endContainer
);
1784 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
1785 isBookmark
= CKEDITOR
.dom
.walker
.bookmark(),
1786 isBogus
= CKEDITOR
.dom
.walker
.bogus();
1788 walker
.evaluator = function( node
) {
1789 return node
.type
== ( mode
== CKEDITOR
.SHRINK_ELEMENT
? CKEDITOR
.NODE_ELEMENT : CKEDITOR
.NODE_TEXT
);
1793 walker
.guard = function( node
, movingOut
) {
1794 // Skipping bogus before other cases (http://dev.ckeditor.com/ticket/17010).
1795 if ( skipBogus
&& isBogus( node
) ) {
1799 if ( isBookmark( node
) )
1802 // Stop when we're shrink in element mode while encountering a text node.
1803 if ( mode
== CKEDITOR
.SHRINK_ELEMENT
&& node
.type
== CKEDITOR
.NODE_TEXT
)
1806 // Stop when we've already walked "through" an element.
1807 if ( movingOut
&& node
.equals( currentElement
) )
1810 if ( shrinkOnBlockBoundary
=== false && node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isBlockBoundary() )
1813 // Stop shrinking when encountering an editable border.
1814 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.hasAttribute( 'contenteditable' ) )
1817 if ( !movingOut
&& node
.type
== CKEDITOR
.NODE_ELEMENT
)
1818 currentElement
= node
;
1824 var textStart
= walker
[ mode
== CKEDITOR
.SHRINK_ELEMENT
? 'lastForward' : 'next' ]();
1825 textStart
&& this.setStartAt( textStart
, selectContents
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_START
);
1830 var textEnd
= walker
[ mode
== CKEDITOR
.SHRINK_ELEMENT
? 'lastBackward' : 'previous' ]();
1831 textEnd
&& this.setEndAt( textEnd
, selectContents
? CKEDITOR
.POSITION_BEFORE_END : CKEDITOR
.POSITION_AFTER_END
);
1834 return !!( moveStart
|| moveEnd
);
1839 * Inserts a node at the start of the range. The range will be expanded
1840 * to contain the node.
1842 * @param {CKEDITOR.dom.node} node
1844 insertNode: function( node
) {
1845 this.optimizeBookmark();
1846 this.trim( false, true );
1848 var startContainer
= this.startContainer
;
1849 var startOffset
= this.startOffset
;
1851 var nextNode
= startContainer
.getChild( startOffset
);
1854 node
.insertBefore( nextNode
);
1856 startContainer
.append( node
);
1858 // Check if we need to update the end boundary.
1859 if ( node
.getParent() && node
.getParent().equals( this.endContainer
) )
1862 // Expand the range to embrace the new node.
1863 this.setStartBefore( node
);
1867 * Moves the range to a given position according to the specified node.
1869 * // HTML: <p>Foo <b>bar</b></p>
1870 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1871 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1873 * See also {@link #setStartAt} and {@link #setEndAt}.
1875 * @param {CKEDITOR.dom.node} node The node according to which the position will be set.
1876 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1877 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1878 * {@link CKEDITOR#POSITION_AFTER_END}.
1880 moveToPosition: function( node
, position
) {
1881 this.setStartAt( node
, position
);
1882 this.collapse( true );
1886 * Moves the range to the exact position of the specified range.
1888 * @param {CKEDITOR.dom.range} range
1890 moveToRange: function( range
) {
1891 this.setStart( range
.startContainer
, range
.startOffset
);
1892 this.setEnd( range
.endContainer
, range
.endOffset
);
1896 * Select nodes content. Range will start and end inside this node.
1898 * @param {CKEDITOR.dom.node} node
1900 selectNodeContents: function( node
) {
1901 this.setStart( node
, 0 );
1902 this.setEnd( node
, node
.type
== CKEDITOR
.NODE_TEXT
? node
.getLength() : node
.getChildCount() );
1906 * Sets the start position of a range.
1908 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1909 * @param {Number} startOffset An integer greater than or equal to zero
1910 * representing the offset for the start of the range from the start
1913 setStart: function( startNode
, startOffset
) {
1914 // W3C requires a check for the new position. If it is after the end
1915 // boundary, the range should be collapsed to the new start. It seams
1916 // we will not need this check for our use of this class so we can
1917 // ignore it for now.
1919 // Fixing invalid range start inside dtd empty elements.
1920 if ( startNode
.type
== CKEDITOR
.NODE_ELEMENT
&& CKEDITOR
.dtd
.$empty
[ startNode
.getName() ] )
1921 startOffset
= startNode
.getIndex(), startNode
= startNode
.getParent();
1923 this._setStartContainer( startNode
);
1924 this.startOffset
= startOffset
;
1926 if ( !this.endContainer
) {
1927 this._setEndContainer( startNode
);
1928 this.endOffset
= startOffset
;
1931 updateCollapsed( this );
1935 * Sets the end position of a Range.
1937 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1938 * @param {Number} endOffset An integer greater than or equal to zero
1939 * representing the offset for the end of the range from the start
1942 setEnd: function( endNode
, endOffset
) {
1943 // W3C requires a check for the new position. If it is before the start
1944 // boundary, the range should be collapsed to the new end. It seams we
1945 // will not need this check for our use of this class so we can ignore
1948 // Fixing invalid range end inside dtd empty elements.
1949 if ( endNode
.type
== CKEDITOR
.NODE_ELEMENT
&& CKEDITOR
.dtd
.$empty
[ endNode
.getName() ] )
1950 endOffset
= endNode
.getIndex() + 1, endNode
= endNode
.getParent();
1952 this._setEndContainer( endNode
);
1953 this.endOffset
= endOffset
;
1955 if ( !this.startContainer
) {
1956 this._setStartContainer( endNode
);
1957 this.startOffset
= endOffset
;
1960 updateCollapsed( this );
1964 * Sets start of this range after the specified node.
1966 * // Range: <p>foo<b>bar</b>^</p>
1967 * range.setStartAfter( textFoo );
1968 * // The range will be changed to:
1969 * // <p>foo[<b>bar</b>]</p>
1971 * @param {CKEDITOR.dom.node} node
1973 setStartAfter: function( node
) {
1974 this.setStart( node
.getParent(), node
.getIndex() + 1 );
1978 * Sets start of this range after the specified node.
1980 * // Range: <p>foo<b>bar</b>^</p>
1981 * range.setStartBefore( elB );
1982 * // The range will be changed to:
1983 * // <p>foo[<b>bar</b>]</p>
1985 * @param {CKEDITOR.dom.node} node
1987 setStartBefore: function( node
) {
1988 this.setStart( node
.getParent(), node
.getIndex() );
1992 * Sets end of this range after the specified node.
1994 * // Range: <p>foo^<b>bar</b></p>
1995 * range.setEndAfter( elB );
1996 * // The range will be changed to:
1997 * // <p>foo[<b>bar</b>]</p>
1999 * @param {CKEDITOR.dom.node} node
2001 setEndAfter: function( node
) {
2002 this.setEnd( node
.getParent(), node
.getIndex() + 1 );
2006 * Sets end of this range before the specified node.
2008 * // Range: <p>^foo<b>bar</b></p>
2009 * range.setStartAfter( textBar );
2010 * // The range will be changed to:
2011 * // <p>[foo<b>]bar</b></p>
2013 * @param {CKEDITOR.dom.node} node
2015 setEndBefore: function( node
) {
2016 this.setEnd( node
.getParent(), node
.getIndex() );
2020 * Moves the start of this range to given position according to specified node.
2022 * // HTML: <p>Foo <b>bar</b>^</p>
2023 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
2024 * // The range will be changed to:
2025 * // <p>Foo <b>[bar</b>]</p>
2027 * See also {@link #setEndAt} and {@link #moveToPosition}.
2029 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2030 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2031 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2032 * {@link CKEDITOR#POSITION_AFTER_END}.
2034 setStartAt: function( node
, position
) {
2035 switch ( position
) {
2036 case CKEDITOR
.POSITION_AFTER_START:
2037 this.setStart( node
, 0 );
2040 case CKEDITOR
.POSITION_BEFORE_END:
2041 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
2042 this.setStart( node
, node
.getLength() );
2044 this.setStart( node
, node
.getChildCount() );
2047 case CKEDITOR
.POSITION_BEFORE_START:
2048 this.setStartBefore( node
);
2051 case CKEDITOR
.POSITION_AFTER_END:
2052 this.setStartAfter( node
);
2055 updateCollapsed( this );
2059 * Moves the end of this range to given position according to specified node.
2061 * // HTML: <p>^Foo <b>bar</b></p>
2062 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
2063 * // The range will be changed to:
2064 * // <p>[Foo <b>]bar</b></p>
2066 * See also {@link #setStartAt} and {@link #moveToPosition}.
2068 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2069 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2070 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2071 * {@link CKEDITOR#POSITION_AFTER_END}.
2073 setEndAt: function( node
, position
) {
2074 switch ( position
) {
2075 case CKEDITOR
.POSITION_AFTER_START:
2076 this.setEnd( node
, 0 );
2079 case CKEDITOR
.POSITION_BEFORE_END:
2080 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
2081 this.setEnd( node
, node
.getLength() );
2083 this.setEnd( node
, node
.getChildCount() );
2086 case CKEDITOR
.POSITION_BEFORE_START:
2087 this.setEndBefore( node
);
2090 case CKEDITOR
.POSITION_AFTER_END:
2091 this.setEndAfter( node
);
2094 updateCollapsed( this );
2098 * Wraps inline content found around the range's start or end boundary
2099 * with a block element.
2101 * // Assuming the following range:
2102 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2103 * // The result of executing:
2104 * range.fixBlock( true, 'p' );
2106 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2108 * Non-collapsed range:
2110 * // Assuming the following range:
2111 * // ba[r<p>foo</p>bo]m
2112 * // The result of executing:
2113 * range.fixBlock( false, 'p' );
2115 * // ba[r<p>foo</p><p>bo]m</p>
2117 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2118 * @param {String} blockTag The name of a block element in which content will be wrapped.
2119 * For example: `'p'`.
2120 * @returns {CKEDITOR.dom.element} Created block wrapper.
2122 fixBlock: function( isStart
, blockTag
) {
2123 var bookmark
= this.createBookmark(),
2124 fixedBlock
= this.document
.createElement( blockTag
);
2126 this.collapse( isStart
);
2128 this.enlarge( CKEDITOR
.ENLARGE_BLOCK_CONTENTS
);
2130 this.extractContents().appendTo( fixedBlock
);
2133 this.insertNode( fixedBlock
);
2135 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2136 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2137 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2138 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2139 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2140 // is placed before the bookmark.
2141 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2142 // We remove incorrectly placed one and add a brand new one. (http://dev.ckeditor.com/ticket/13001)
2143 var bogus
= fixedBlock
.getBogus();
2147 fixedBlock
.appendBogus();
2149 this.moveToBookmark( bookmark
);
2156 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2158 splitBlock: function( blockTag
, cloneId
) {
2159 var startPath
= new CKEDITOR
.dom
.elementPath( this.startContainer
, this.root
),
2160 endPath
= new CKEDITOR
.dom
.elementPath( this.endContainer
, this.root
);
2162 var startBlockLimit
= startPath
.blockLimit
,
2163 endBlockLimit
= endPath
.blockLimit
;
2165 var startBlock
= startPath
.block
,
2166 endBlock
= endPath
.block
;
2168 var elementPath
= null;
2169 // Do nothing if the boundaries are in different block limits.
2170 if ( !startBlockLimit
.equals( endBlockLimit
) )
2173 // Get or fix current blocks.
2174 if ( blockTag
!= 'br' ) {
2175 if ( !startBlock
) {
2176 startBlock
= this.fixBlock( true, blockTag
);
2177 endBlock
= new CKEDITOR
.dom
.elementPath( this.endContainer
, this.root
).block
;
2181 endBlock
= this.fixBlock( false, blockTag
);
2184 // Get the range position.
2185 var isStartOfBlock
= startBlock
&& this.checkStartOfBlock(),
2186 isEndOfBlock
= endBlock
&& this.checkEndOfBlock();
2188 // Delete the current contents.
2189 // TODO: Why is 2.x doing CheckIsEmpty()?
2190 this.deleteContents();
2192 if ( startBlock
&& startBlock
.equals( endBlock
) ) {
2193 if ( isEndOfBlock
) {
2194 elementPath
= new CKEDITOR
.dom
.elementPath( this.startContainer
, this.root
);
2195 this.moveToPosition( endBlock
, CKEDITOR
.POSITION_AFTER_END
);
2197 } else if ( isStartOfBlock
) {
2198 elementPath
= new CKEDITOR
.dom
.elementPath( this.startContainer
, this.root
);
2199 this.moveToPosition( startBlock
, CKEDITOR
.POSITION_BEFORE_START
);
2202 endBlock
= this.splitElement( startBlock
, cloneId
|| false );
2204 // In Gecko, the last child node must be a bogus <br>.
2205 // Note: bogus <br> added under <ul> or <ol> would cause
2206 // lists to be incorrectly rendered.
2207 if ( !startBlock
.is( 'ul', 'ol' ) )
2208 startBlock
.appendBogus();
2213 previousBlock: startBlock
,
2214 nextBlock: endBlock
,
2215 wasStartOfBlock: isStartOfBlock
,
2216 wasEndOfBlock: isEndOfBlock
,
2217 elementPath: elementPath
2222 * Branch the specified element from the collapsed range position and
2223 * place the caret between the two result branches.
2225 * **Note:** The range must be collapsed and been enclosed by this element.
2227 * @param {CKEDITOR.dom.element} element
2228 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2229 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2231 splitElement: function( toSplit
, cloneId
) {
2232 if ( !this.collapsed
)
2235 // Extract the contents of the block from the selection point to the end
2237 this.setEndAt( toSplit
, CKEDITOR
.POSITION_BEFORE_END
);
2238 var documentFragment
= this.extractContents( false, cloneId
|| false );
2240 // Duplicate the element after it.
2241 var clone
= toSplit
.clone( false, cloneId
|| false );
2243 // Place the extracted contents into the duplicated element.
2244 documentFragment
.appendTo( clone
);
2245 clone
.insertAfter( toSplit
);
2246 this.moveToPosition( toSplit
, CKEDITOR
.POSITION_AFTER_END
);
2251 * Recursively remove any empty path blocks at the range boundary.
2254 * @param {Boolean} atEnd Removal to perform at the end boundary,
2255 * otherwise to perform at the start.
2257 removeEmptyBlocksAtEnd: ( function() {
2259 var whitespace
= CKEDITOR
.dom
.walker
.whitespaces(),
2260 bookmark
= CKEDITOR
.dom
.walker
.bookmark( false );
2262 function childEval( parent
) {
2263 return function( node
) {
2264 // Whitespace, bookmarks, empty inlines.
2265 if ( whitespace( node
) || bookmark( node
) ||
2266 node
.type
== CKEDITOR
.NODE_ELEMENT
&&
2267 node
.isEmptyInlineRemoveable() ) {
2269 } else if ( parent
.is( 'table' ) && node
.is( 'caption' ) ) {
2277 return function( atEnd
) {
2279 var bm
= this.createBookmark();
2280 var path
= this[ atEnd
? 'endPath' : 'startPath' ]();
2281 var block
= path
.block
|| path
.blockLimit
, parent
;
2283 // Remove any childless block, including list and table.
2284 while ( block
&& !block
.equals( path
.root
) &&
2285 !block
.getFirst( childEval( block
) ) ) {
2286 parent
= block
.getParent();
2287 this[ atEnd
? 'setEndAt' : 'setStartAt' ]( block
, CKEDITOR
.POSITION_AFTER_END
);
2292 this.moveToBookmark( bm
);
2298 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2300 * @returns {CKEDITOR.dom.elementPath}
2302 startPath: function() {
2303 return new CKEDITOR
.dom
.elementPath( this.startContainer
, this.root
);
2307 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2309 * @returns {CKEDITOR.dom.elementPath}
2311 endPath: function() {
2312 return new CKEDITOR
.dom
.elementPath( this.endContainer
, this.root
);
2316 * Check whether a range boundary is at the inner boundary of a given
2319 * @param {CKEDITOR.dom.element} element The target element to check.
2320 * @param {Number} checkType The boundary to check for both the range
2321 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2322 * @returns {Boolean} `true` if the range boundary is at the inner
2323 * boundary of the element.
2325 checkBoundaryOfElement: function( element
, checkType
) {
2326 var checkStart
= ( checkType
== CKEDITOR
.START
);
2328 // Create a copy of this range, so we can manipulate it for our checks.
2329 var walkerRange
= this.clone();
2331 // Collapse the range at the proper size.
2332 walkerRange
.collapse( checkStart
);
2334 // Expand the range to element boundary.
2335 walkerRange
[ checkStart
? 'setStartAt' : 'setEndAt' ]( element
, checkStart
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_END
);
2337 // Create the walker, which will check if we have anything useful
2339 var walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2340 walker
.evaluator
= elementBoundaryEval( checkStart
);
2342 return walker
[ checkStart
? 'checkBackward' : 'checkForward' ]();
2346 * **Note:** Calls to this function may produce changes to the DOM. The range may
2347 * be updated to reflect such changes.
2349 * @returns {Boolean}
2352 checkStartOfBlock: function() {
2353 var startContainer
= this.startContainer
,
2354 startOffset
= this.startOffset
;
2356 // [IE] Special handling for range start in text with a leading NBSP,
2357 // we it to be isolated, for bogus check.
2358 if ( CKEDITOR
.env
.ie
&& startOffset
&& startContainer
.type
== CKEDITOR
.NODE_TEXT
) {
2359 var textBefore
= CKEDITOR
.tools
.ltrim( startContainer
.substring( 0, startOffset
) );
2360 if ( nbspRegExp
.test( textBefore
) )
2364 // Anticipate the trim() call here, so the walker will not make
2365 // changes to the DOM, which would not get reflected into this
2369 // We need to grab the block element holding the start boundary, so
2370 // let's use an element path for it.
2371 var path
= new CKEDITOR
.dom
.elementPath( this.startContainer
, this.root
);
2373 // Creates a range starting at the block start until the range start.
2374 var walkerRange
= this.clone();
2375 walkerRange
.collapse( true );
2376 walkerRange
.setStartAt( path
.block
|| path
.blockLimit
, CKEDITOR
.POSITION_AFTER_START
);
2378 var walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2379 walker
.evaluator
= getCheckStartEndBlockEvalFunction();
2381 return walker
.checkBackward();
2385 * **Note:** Calls to this function may produce changes to the DOM. The range may
2386 * be updated to reflect such changes.
2388 * @returns {Boolean}
2391 checkEndOfBlock: function() {
2392 var endContainer
= this.endContainer
,
2393 endOffset
= this.endOffset
;
2395 // [IE] Special handling for range end in text with a following NBSP,
2396 // we it to be isolated, for bogus check.
2397 if ( CKEDITOR
.env
.ie
&& endContainer
.type
== CKEDITOR
.NODE_TEXT
) {
2398 var textAfter
= CKEDITOR
.tools
.rtrim( endContainer
.substring( endOffset
) );
2399 if ( nbspRegExp
.test( textAfter
) )
2403 // Anticipate the trim() call here, so the walker will not make
2404 // changes to the DOM, which would not get reflected into this
2408 // We need to grab the block element holding the start boundary, so
2409 // let's use an element path for it.
2410 var path
= new CKEDITOR
.dom
.elementPath( this.endContainer
, this.root
);
2412 // Creates a range starting at the block start until the range start.
2413 var walkerRange
= this.clone();
2414 walkerRange
.collapse( false );
2415 walkerRange
.setEndAt( path
.block
|| path
.blockLimit
, CKEDITOR
.POSITION_BEFORE_END
);
2417 var walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2418 walker
.evaluator
= getCheckStartEndBlockEvalFunction();
2420 return walker
.checkForward();
2424 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2426 * @param {Function} evaluator Function used as the walker's evaluator.
2427 * @param {Function} [guard] Function used as the walker's guard.
2428 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2429 * default to the root editable if not defined.
2430 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2432 getPreviousNode: function( evaluator
, guard
, boundary
) {
2433 var walkerRange
= this.clone();
2434 walkerRange
.collapse( 1 );
2435 walkerRange
.setStartAt( boundary
|| this.root
, CKEDITOR
.POSITION_AFTER_START
);
2437 var walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2438 walker
.evaluator
= evaluator
;
2439 walker
.guard
= guard
;
2440 return walker
.previous();
2444 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2446 * @param {Function} evaluator Function used as the walker's evaluator.
2447 * @param {Function} [guard] Function used as the walker's guard.
2448 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2449 * default to the root editable if not defined.
2450 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2452 getNextNode: function( evaluator
, guard
, boundary
) {
2453 var walkerRange
= this.clone();
2454 walkerRange
.collapse();
2455 walkerRange
.setEndAt( boundary
|| this.root
, CKEDITOR
.POSITION_BEFORE_END
);
2457 var walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2458 walker
.evaluator
= evaluator
;
2459 walker
.guard
= guard
;
2460 return walker
.next();
2464 * Check if elements at which the range boundaries anchor are read-only,
2465 * with respect to `contenteditable` attribute.
2467 * @returns {Boolean}
2469 checkReadOnly: ( function() {
2470 function checkNodesEditable( node
, anotherEnd
) {
2472 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
2473 if ( node
.getAttribute( 'contentEditable' ) == 'false' && !node
.data( 'cke-editable' ) )
2476 // Range enclosed entirely in an editable element.
2477 else if ( node
.is( 'html' ) || node
.getAttribute( 'contentEditable' ) == 'true' && ( node
.contains( anotherEnd
) || node
.equals( anotherEnd
) ) )
2481 node
= node
.getParent();
2488 var startNode
= this.startContainer
,
2489 endNode
= this.endContainer
;
2491 // Check if elements path at both boundaries are editable.
2492 return !( checkNodesEditable( startNode
, endNode
) && checkNodesEditable( endNode
, startNode
) );
2497 * Moves the range boundaries to the first/end editing point inside an
2500 * For example, in an element tree like
2501 * `<p><b><i></i></b> Text</p>`, the start editing point is
2502 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2504 * @param {CKEDITOR.dom.element} el The element into which look for the
2506 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2507 * @returns {Boolean} Whether range was moved.
2509 moveToElementEditablePosition: function( el
, isMoveToEnd
) {
2511 function nextDFS( node
, childOnly
) {
2514 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isEditable( false ) )
2515 next
= node
[ isMoveToEnd
? 'getLast' : 'getFirst' ]( notIgnoredEval
);
2517 if ( !childOnly
&& !next
)
2518 next
= node
[ isMoveToEnd
? 'getPrevious' : 'getNext' ]( notIgnoredEval
);
2523 // Handle non-editable element e.g. HR.
2524 if ( el
.type
== CKEDITOR
.NODE_ELEMENT
&& !el
.isEditable( false ) ) {
2525 this.moveToPosition( el
, isMoveToEnd
? CKEDITOR
.POSITION_AFTER_END : CKEDITOR
.POSITION_BEFORE_START
);
2532 // Stop immediately if we've found a text node.
2533 if ( el
.type
== CKEDITOR
.NODE_TEXT
) {
2534 // Put cursor before block filler.
2535 if ( isMoveToEnd
&& this.endContainer
&& this.checkEndOfBlock() && nbspRegExp
.test( el
.getText() ) )
2536 this.moveToPosition( el
, CKEDITOR
.POSITION_BEFORE_START
);
2538 this.moveToPosition( el
, isMoveToEnd
? CKEDITOR
.POSITION_AFTER_END : CKEDITOR
.POSITION_BEFORE_START
);
2543 // If an editable element is found, move inside it, but not stop the searching.
2544 if ( el
.type
== CKEDITOR
.NODE_ELEMENT
) {
2545 if ( el
.isEditable() ) {
2546 this.moveToPosition( el
, isMoveToEnd
? CKEDITOR
.POSITION_BEFORE_END : CKEDITOR
.POSITION_AFTER_START
);
2549 // Put cursor before padding block br.
2550 else if ( isMoveToEnd
&& el
.is( 'br' ) && this.endContainer
&& this.checkEndOfBlock() )
2551 this.moveToPosition( el
, CKEDITOR
.POSITION_BEFORE_START
);
2552 // Special case - non-editable block. Select entire element, because it does not make sense
2553 // to place collapsed selection next to it, because browsers can't handle that.
2554 else if ( el
.getAttribute( 'contenteditable' ) == 'false' && el
.is( CKEDITOR
.dtd
.$block
) ) {
2555 this.setStartBefore( el
);
2556 this.setEndAfter( el
);
2561 el
= nextDFS( el
, found
);
2568 * Moves the range boundaries to the closest editing point after/before an
2569 * element or the current range position (depends on whether the element was specified).
2571 * For example, if the start element has `id="start"`,
2572 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2573 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2575 * See also: {@link #moveToElementEditablePosition}.
2578 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2579 * position will be used.
2580 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2581 * @returns {Boolean} Whether the range was moved.
2583 moveToClosestEditablePosition: function( element
, isMoveForward
) {
2584 // We don't want to modify original range if there's no editable position.
2589 positions
= [ CKEDITOR
.POSITION_AFTER_END
, CKEDITOR
.POSITION_BEFORE_START
];
2592 // Set collapsed range at one of ends of element.
2593 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2594 range
= new CKEDITOR
.dom
.range( this.root
);
2595 range
.moveToPosition( element
, positions
[ isMoveForward
? 0 : 1 ] );
2597 range
= this.clone();
2600 // Start element isn't a block, so we can automatically place range
2602 if ( element
&& !element
.is( CKEDITOR
.dtd
.$block
) )
2605 // Look for first node that fulfills eval function and place range next to it.
2606 sibling
= range
[ isMoveForward
? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2609 isElement
= sibling
.type
== CKEDITOR
.NODE_ELEMENT
;
2611 // Special case - eval accepts block element only if it's a non-editable block,
2612 // which we want to select, not place collapsed selection next to it (which browsers
2614 if ( isElement
&& sibling
.is( CKEDITOR
.dtd
.$block
) && sibling
.getAttribute( 'contenteditable' ) == 'false' ) {
2615 range
.setStartAt( sibling
, CKEDITOR
.POSITION_BEFORE_START
);
2616 range
.setEndAt( sibling
, CKEDITOR
.POSITION_AFTER_END
);
2618 // Handle empty blocks which can be selection containers on old IEs.
2619 else if ( !CKEDITOR
.env
.needsBrFiller
&& isElement
&& sibling
.is( CKEDITOR
.dom
.walker
.validEmptyBlockContainers
) ) {
2620 range
.setEnd( sibling
, 0 );
2623 range
.moveToPosition( sibling
, positions
[ isMoveForward
? 1 : 0 ] );
2629 this.moveToRange( range
);
2635 * See {@link #moveToElementEditablePosition}.
2637 * @returns {Boolean} Whether range was moved.
2639 moveToElementEditStart: function( target
) {
2640 return this.moveToElementEditablePosition( target
);
2644 * See {@link #moveToElementEditablePosition}.
2646 * @returns {Boolean} Whether range was moved.
2648 moveToElementEditEnd: function( target
) {
2649 return this.moveToElementEditablePosition( target
, true );
2653 * Get the single node enclosed within the range if there's one.
2655 * @returns {CKEDITOR.dom.node}
2657 getEnclosedNode: function() {
2658 var walkerRange
= this.clone();
2660 // Optimize and analyze the range to avoid DOM destructive nature of walker. (http://dev.ckeditor.com/ticket/5780)
2661 walkerRange
.optimize();
2662 if ( walkerRange
.startContainer
.type
!= CKEDITOR
.NODE_ELEMENT
|| walkerRange
.endContainer
.type
!= CKEDITOR
.NODE_ELEMENT
)
2665 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
2666 isNotBookmarks
= CKEDITOR
.dom
.walker
.bookmark( false, true ),
2667 isNotWhitespaces
= CKEDITOR
.dom
.walker
.whitespaces( true );
2669 walker
.evaluator = function( node
) {
2670 return isNotWhitespaces( node
) && isNotBookmarks( node
);
2672 var node
= walker
.next();
2674 return node
&& node
.equals( walker
.previous() ) ? node : null;
2678 * Get the node adjacent to the range start or {@link #startContainer}.
2680 * @returns {CKEDITOR.dom.node}
2682 getTouchedStartNode: function() {
2683 var container
= this.startContainer
;
2685 if ( this.collapsed
|| container
.type
!= CKEDITOR
.NODE_ELEMENT
)
2688 return container
.getChild( this.startOffset
) || container
;
2692 * Get the node adjacent to the range end or {@link #endContainer}.
2694 * @returns {CKEDITOR.dom.node}
2696 getTouchedEndNode: function() {
2697 var container
= this.endContainer
;
2699 if ( this.collapsed
|| container
.type
!= CKEDITOR
.NODE_ELEMENT
)
2702 return container
.getChild( this.endOffset
- 1 ) || container
;
2706 * Gets next node which can be a container of a selection.
2707 * This methods mimics a behavior of right/left arrow keys in case of
2708 * collapsed selection. It does not return an exact position (with offset) though,
2709 * but just a selection's container.
2711 * Note: use this method on a collapsed range.
2714 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2716 getNextEditableNode: getNextEditableNode(),
2719 * See {@link #getNextEditableNode}.
2722 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2724 getPreviousEditableNode: getNextEditableNode( 1 ),
2727 * Returns any table element, like `td`, `tbody`, `table` etc. from a given range. The element
2728 * is returned only if the range is contained within one table (might be a nested
2729 * table, but it cannot be two different tables on the same DOM level).
2733 * @param {Object} [tableElements] Mapping of element names that should be considered.
2734 * @returns {CKEDITOR.dom.element/null}
2736 _getTableElement: function( tableElements
) {
2737 tableElements
= tableElements
|| {
2747 var start
= this.startContainer
,
2748 end
= this.endContainer
,
2749 startTable
= start
.getAscendant( 'table', true ),
2750 endTable
= end
.getAscendant( 'table', true );
2752 // Super weird edge case in Safari: if there is a table with only one cell inside and that cell
2753 // is selected, then the end boundary of the table is moved into editor's editable.
2754 // That case is also present when selecting the last cell inside nested table.
2755 if ( CKEDITOR
.env
.safari
&& startTable
&& end
.equals( this.root
) ) {
2756 return start
.getAscendant( tableElements
, true );
2759 if ( this.getEnclosedNode() ) {
2760 return this.getEnclosedNode().getAscendant( tableElements
, true );
2763 // Ensure that selection starts and ends in the same table or one of the table is inside the other.
2764 if ( startTable
&& endTable
&& ( startTable
.equals( endTable
) || startTable
.contains( endTable
) ||
2765 endTable
.contains( startTable
) ) ) {
2767 return start
.getAscendant( tableElements
, true );
2774 * Scrolls the start of current range into view.
2776 scrollIntoView: function() {
2778 // The reference element contains a zero-width space to avoid
2779 // a premature removal. The view is to be scrolled with respect
2781 var reference
= new CKEDITOR
.dom
.element
.createFromHtml( '<span> </span>', this.document
),
2782 afterCaretNode
, startContainerText
, isStartText
;
2784 var range
= this.clone();
2786 // Work with the range to obtain a proper caret position.
2789 // Currently in a text node, so we need to split it into two
2790 // halves and put the reference between.
2791 if ( isStartText
= range
.startContainer
.type
== CKEDITOR
.NODE_TEXT
) {
2792 // Keep the original content. It will be restored.
2793 startContainerText
= range
.startContainer
.getText();
2795 // Split the startContainer at the this position.
2796 afterCaretNode
= range
.startContainer
.split( range
.startOffset
);
2798 // Insert the reference between two text nodes.
2799 reference
.insertAfter( range
.startContainer
);
2802 // If not in a text node, simply insert the reference into the range.
2804 range
.insertNode( reference
);
2807 // Scroll with respect to the reference element.
2808 reference
.scrollIntoView();
2810 // Get rid of split parts if "in a text node" case.
2811 // Revert the original text of the startContainer.
2812 if ( isStartText
) {
2813 range
.startContainer
.setText( startContainerText
);
2814 afterCaretNode
.remove();
2817 // Get rid of the reference node. It is no longer necessary.
2822 * Setter for the {@link #startContainer}.
2826 * @param {CKEDITOR.dom.element} startContainer
2828 _setStartContainer: function( startContainer
) {
2830 var isRootAscendantOrSelf
= this.root
.equals( startContainer
) || this.root
.contains( startContainer
);
2832 if ( !isRootAscendantOrSelf
) {
2833 CKEDITOR
.warn( 'range-startcontainer', { startContainer: startContainer
, root: this.root
} );
2836 this.startContainer
= startContainer
;
2840 * Setter for the {@link #endContainer}.
2844 * @param {CKEDITOR.dom.element} endContainer
2846 _setEndContainer: function( endContainer
) {
2848 var isRootAscendantOrSelf
= this.root
.equals( endContainer
) || this.root
.contains( endContainer
);
2850 if ( !isRootAscendantOrSelf
) {
2851 CKEDITOR
.warn( 'range-endcontainer', { endContainer: endContainer
, root: this.root
} );
2854 this.endContainer
= endContainer
;
2858 * Looks for elements matching the `query` selector within a range.
2862 * @param {String} query
2863 * @param {Boolean} [includeNonEditables=false] Whether elements with `contenteditable` set to `false` should
2865 * @returns {CKEDITOR.dom.element[]}
2867 _find: function( query
, includeNonEditables
) {
2868 var ancestor
= this.getCommonAncestor(),
2869 boundaries
= this.getBoundaryNodes(),
2870 // Contrary to CKEDITOR.dom.element#find we're returning array, that's because NodeList is immutable, and we need
2871 // to do some filtering in returned list.
2879 if ( ancestor
&& ancestor
.find
) {
2880 initialMatches
= ancestor
.find( query
);
2882 for ( i
= 0; i
< initialMatches
.count(); i
++ ) {
2883 curItem
= initialMatches
.getItem( i
);
2885 // Using isReadOnly() method to filterout non editables. It checks isContentEditable including all browser quirks.
2886 if ( !includeNonEditables
&& curItem
.isReadOnly() ) {
2890 // It's not enough to get elements from common ancestor, because it might contain too many matches.
2891 // We need to ensure that returned items are between boundary points.
2892 isStartGood
= ( curItem
.getPosition( boundaries
.startNode
) & CKEDITOR
.POSITION_FOLLOWING
) || boundaries
.startNode
.equals( curItem
);
2893 isEndGood
= ( curItem
.getPosition( boundaries
.endNode
) & ( CKEDITOR
.POSITION_PRECEDING
+ CKEDITOR
.POSITION_IS_CONTAINED
) ) || boundaries
.endNode
.equals( curItem
);
2895 if ( isStartGood
&& isEndGood
) {
2896 ret
.push( curItem
);
2906 * Merges every subsequent range in given set, returning a smaller array of ranges.
2908 * Note that each range in the returned value will be enlarged with `CKEDITOR.ENLARGE_ELEMENT` value.
2912 * @param {CKEDITOR.dom.range[]} ranges
2913 * @returns {CKEDITOR.dom.range[]} Set of merged ranges.
2914 * @member CKEDITOR.dom.range
2916 CKEDITOR
.dom
.range
.mergeRanges = function( ranges
) {
2917 return CKEDITOR
.tools
.array
.reduce( ranges
, function( ret
, rng
) {
2919 var lastRange
= ret
[ ret
.length
- 1 ],
2920 isContinuation
= false;
2922 // Make a clone, we don't want to modify input.
2924 rng
.enlarge( CKEDITOR
.ENLARGE_ELEMENT
);
2927 // The trick is to create a range spanning the gap between the two ranges. Then iterate over
2928 // each node found in this gap. If it contains anything other than whitespace, then it means it
2929 // is not a continuation.
2930 var gapRange
= new CKEDITOR
.dom
.range( rng
.root
),
2931 walker
= new CKEDITOR
.dom
.walker( gapRange
),
2932 isWhitespace
= CKEDITOR
.dom
.walker
.whitespaces(),
2935 gapRange
.setStart( lastRange
.endContainer
, lastRange
.endOffset
);
2936 gapRange
.setEnd( rng
.startContainer
, rng
.startOffset
);
2938 nodeInBetween
= walker
.next();
2940 while ( isWhitespace( nodeInBetween
) || rng
.endContainer
.equals( nodeInBetween
) ) {
2941 // We don't care about whitespaces, and range container. Also we skip the endContainer,
2942 // as it will also be provided by the iterator (as it visits it's opening tag).
2943 nodeInBetween
= walker
.next();
2946 // Simply, if anything has been found there's a content in between the two.
2947 isContinuation
= !nodeInBetween
;
2950 if ( isContinuation
) {
2951 // If last range ends, where the current range starts, then let's merge it.
2952 lastRange
.setEnd( rng
.endContainer
, rng
.endOffset
);
2954 // In other case just push cur range into the stack.
2965 * Indicates a position after start of a node.
2967 * // When used according to an element:
2968 * // <element>^contents</element>
2970 * // When used according to a text node:
2971 * // "^text" (range is anchored in the text node)
2973 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2974 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2978 * @property {Number} [=1]
2980 CKEDITOR
.POSITION_AFTER_START
= 1;
2983 * Indicates a position before end of a node.
2985 * // When used according to an element:
2986 * // <element>contents^</element>
2988 * // When used according to a text node:
2989 * // "text^" (range is anchored in the text node)
2991 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2992 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2996 * @property {Number} [=2]
2998 CKEDITOR
.POSITION_BEFORE_END
= 2;
3001 * Indicates a position before start of a node.
3003 * // When used according to an element:
3004 * // ^<element>contents</element> (range is anchored in element's parent)
3006 * // When used according to a text node:
3007 * // ^"text" (range is anchored in text node's parent)
3009 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
3010 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
3014 * @property {Number} [=3]
3016 CKEDITOR
.POSITION_BEFORE_START
= 3;
3019 * Indicates a position after end of a node.
3021 * // When used according to an element:
3022 * // <element>contents</element>^ (range is anchored in element's parent)
3024 * // When used according to a text node:
3025 * // "text"^ (range is anchored in text node's parent)
3027 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
3028 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
3032 * @property {Number} [=4]
3034 CKEDITOR
.POSITION_AFTER_END
= 4;
3039 * @property {Number} [=1]
3041 CKEDITOR
.ENLARGE_ELEMENT
= 1;
3046 * @property {Number} [=2]
3048 CKEDITOR
.ENLARGE_BLOCK_CONTENTS
= 2;
3053 * @property {Number} [=3]
3055 CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS
= 3;
3060 * @property {Number} [=4]
3062 CKEDITOR
.ENLARGE_INLINE
= 4;
3064 // Check boundary types.
3067 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
3071 * @property {Number} [=1]
3076 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
3080 * @property {Number} [=2]
3084 // Shrink range types.
3087 * See {@link CKEDITOR.dom.range#shrink}.
3091 * @property {Number} [=1]
3093 CKEDITOR
.SHRINK_ELEMENT
= 1;
3096 * See {@link CKEDITOR.dom.range#shrink}.
3100 * @property {Number} [=2]
3102 CKEDITOR
.SHRINK_TEXT
= 2;