]> git.immae.eu Git - perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git/blame - sources/core/dom/range.js
Initial commit
[perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git] / sources / core / dom / range.js
CommitLineData
7adcb81e
IB
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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.
10 *
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.
15 *
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();
21 *
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}.
24 *
25 * var range = editor.createRange();
26 * range.root.equals( editor.editable() ); // -> true
27 *
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.
32 *
33 * ### Selection
34 *
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.
40 *
41 * The editor selection may be retrieved using the {@link CKEDITOR.editor#getSelection} method:
42 *
43 * var sel = editor.getSelection(),
44 * ranges = sel.getRanges(); // CKEDITOR.dom.rangeList instance.
45 *
46 * var range = ranges[ 0 ];
47 * range.root; // -> editor's editable element.
48 *
49 * A range can also be selected:
50 *
51 * var range = editor.createRange();
52 * range.selectNodeContents( editor.editable() );
53 * sel.selectRanges( [ range ] );
54 *
55 * @class
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.
60 */
61CKEDITOR.dom.range = function( root ) {
62 /**
63 * Node within which the range begins.
64 *
65 * var range = new CKEDITOR.dom.range( editor.document );
66 * range.selectNodeContents( editor.document.getBody() );
67 * alert( range.startContainer.getName() ); // 'body'
68 *
69 * @readonly
70 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
71 */
72 this.startContainer = null;
73
74 /**
75 * Offset within the starting node of the range.
76 *
77 * var range = new CKEDITOR.dom.range( editor.document );
78 * range.selectNodeContents( editor.document.getBody() );
79 * alert( range.startOffset ); // 0
80 *
81 * @readonly
82 * @property {Number}
83 */
84 this.startOffset = null;
85
86 /**
87 * Node within which the range ends.
88 *
89 * var range = new CKEDITOR.dom.range( editor.document );
90 * range.selectNodeContents( editor.document.getBody() );
91 * alert( range.endContainer.getName() ); // 'body'
92 *
93 * @readonly
94 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
95 */
96 this.endContainer = null;
97
98 /**
99 * Offset within the ending node of the range.
100 *
101 * var range = new CKEDITOR.dom.range( editor.document );
102 * range.selectNodeContents( editor.document.getBody() );
103 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
104 *
105 * @readonly
106 * @property {Number}
107 */
108 this.endOffset = null;
109
110 /**
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
113 * in it.
114 *
115 * var range = new CKEDITOR.dom.range( editor.document );
116 * range.selectNodeContents( editor.document.getBody() );
117 * alert( range.collapsed ); // false
118 * range.collapse();
119 * alert( range.collapsed ); // true
120 *
121 * @readonly
122 */
123 this.collapsed = true;
124
125 var isDocRoot = root instanceof CKEDITOR.dom.document;
126 /**
127 * The document within which the range can be used.
128 *
129 * // Selects the body contents of the range document.
130 * range.selectNodeContents( range.document.getBody() );
131 *
132 * @readonly
133 * @property {CKEDITOR.dom.document}
134 */
135 this.document = isDocRoot ? root : root.getDocument();
136
137 /**
138 * The ancestor DOM element within which the range manipulation are limited.
139 *
140 * @readonly
141 * @property {CKEDITOR.dom.element}
142 */
143 this.root = isDocRoot ? root.getBody() : root;
144};
145
146( function() {
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 );
150 }
151
152 // This is a shared function used to delete, extract and clone the range content.
153 //
154 // The outline of the algorithm:
155 //
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.
174 // 4. Clean up.
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 ) {
179 'use strict';
180
181 range.optimizeBookmark();
182
183 var isDelete = action === 0;
184 var isExtract = action == 1;
185 var isClone = action == 2;
186 var doClone = isClone || isExtract;
187
188 var startNode = range.startContainer;
189 var endNode = range.endContainer;
190
191 var startOffset = range.startOffset;
192 var endOffset = range.endOffset;
193
194 var cloneStartNode;
195 var cloneEndNode;
196
197 var doNotRemoveStartNode;
198 var doNotRemoveEndNode;
199
200 var cloneStartText;
201 var cloneEndText;
202
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.
207 if ( isClone && endNode.type == CKEDITOR.NODE_TEXT && startNode.equals( endNode ) ) {
208 startNode = range.document.createText( startNode.substring( startOffset, endOffset ) );
209 docFrag.append( startNode );
210 return;
211 }
212
213 // For text containers, we must simply split the node and point to the
214 // second part. The removal will be handled by the rest of the code.
215 if ( endNode.type == CKEDITOR.NODE_TEXT ) {
216 // If Extract or Delete we can split the text node,
217 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark the text node for cloning.
218 if ( !isClone ) {
219 endNode = endNode.split( endOffset );
220 } else {
221 cloneEndText = true;
222 }
223 } else {
224 // If there's no node after the range boundary we set endNode to the previous node
225 // and mark it to be cloned.
226 if ( endNode.getChildCount() > 0 ) {
227 // If the offset points after the last node.
228 if ( endOffset >= endNode.getChildCount() ) {
229 endNode = endNode.getChild( endOffset - 1 );
230 cloneEndNode = true;
231 } else {
232 endNode = endNode.getChild( endOffset );
233 }
234 }
235 // The end container is empty (<h1>]</h1>), but we want to clone it, although not remove.
236 else {
237 cloneEndNode = true;
238 doNotRemoveEndNode = true;
239 }
240 }
241
242 // For text containers, we must simply split the node. The removal will
243 // be handled by the rest of the code .
244 if ( startNode.type == CKEDITOR.NODE_TEXT ) {
245 // If Extract or Delete we can split the text node,
246 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark
247 // the text node for cloning.
248 if ( !isClone ) {
249 startNode.split( startOffset );
250 } else {
251 cloneStartText = true;
252 }
253 } else {
254 // If there's no node before the range boundary we set startNode to the next node
255 // and mark it to be cloned.
256 if ( startNode.getChildCount() > 0 ) {
257 if ( startOffset === 0 ) {
258 startNode = startNode.getChild( startOffset );
259 cloneStartNode = true;
260 } else {
261 startNode = startNode.getChild( startOffset - 1 );
262 }
263 }
264 // The start container is empty (<h1>[</h1>), but we want to clone it, although not remove.
265 else {
266 cloneStartNode = true;
267 doNotRemoveStartNode = true;
268 }
269 }
270
271 // Get the parent nodes tree for the start and end boundaries.
272 var startParents = startNode.getParents(),
273 endParents = endNode.getParents(),
274 // Level at which start and end boundaries diverged.
275 minLevel = findMinLevel(),
276 maxLevelLeft = startParents.length - 1,
277 maxLevelRight = endParents.length - 1,
278 // Keeps the frag/element which is parent of the level that we are currently cloning.
279 levelParent = docFrag,
280 nextLevelParent,
281 leftNode,
282 rightNode,
283 nextSibling,
284 // Keeps track of the last connected level (on which left and right branches are connected)
285 // Usually this is minLevel, but not always.
286 lastConnectedLevel = -1;
287
288 // THE LEFT BRANCH.
289 for ( var level = minLevel; level <= maxLevelLeft; level++ ) {
290 leftNode = startParents[ level ];
291 nextSibling = leftNode.getNext();
292
293 // 1.
294 // The first step is to handle partial selection of the left branch.
295
296 // Max depth of the left branch. It means that ( leftSibling == endNode ).
297 // We also check if the leftNode isn't only partially selected, because in this case
298 // we want to make a shallow clone of it (the else part).
299 if ( level == maxLevelLeft && !( leftNode.equals( endParents[ level ] ) && maxLevelLeft < maxLevelRight ) ) {
300 if ( cloneStartNode ) {
301 consume( leftNode, levelParent, false, doNotRemoveStartNode );
302 } else if ( cloneStartText ) {
303 levelParent.append( range.document.createText( leftNode.substring( startOffset ) ) );
304 }
305 } else if ( doClone ) {
306 nextLevelParent = levelParent.append( leftNode.clone( 0, cloneId ) );
307 }
308
309 // 2.
310 // The second step is to handle full selection of the content between the left branch and the right branch.
311
312 while ( nextSibling ) {
313 // We can't clone entire endParent just like we can't clone entire startParent -
314 // - they are not fully selected with the range. Partial endParent selection
315 // will be cloned in the next loop.
316 if ( nextSibling.equals( endParents[ level ] ) ) {
317 lastConnectedLevel = level;
318 break;
319 }
320
321 nextSibling = consume( nextSibling, levelParent );
322 }
323
324 levelParent = nextLevelParent;
325 }
326
327 // Reset levelParent, because we reset the level.
328 levelParent = docFrag;
329
330 // THE RIGHT BRANCH.
331 for ( level = minLevel; level <= maxLevelRight; level++ ) {
332 rightNode = endParents[ level ];
333 nextSibling = rightNode.getPrevious();
334
335 // Do not process this node if it is shared with the left branch
336 // because it was already processed.
337 //
338 // Note: Don't worry about text nodes selection - if the entire range was placed in a single text node
339 // it was handled as a special case at the beginning. In other cases when startNode == endNode
340 // or when on this level leftNode == rightNode (so rightNode.equals( startParents[ level ] ))
341 // this node was handled by the previous loop.
342 if ( !rightNode.equals( startParents[ level ] ) ) {
343 // 1.
344 // The first step is to handle partial selection of the right branch.
345
346 // Max depth of the right branch. It means that ( rightNode == endNode ).
347 // We also check if the rightNode isn't only partially selected, because in this case
348 // we want to make a shallow clone of it (the else part).
349 if ( level == maxLevelRight && !( rightNode.equals( startParents[ level ] ) && maxLevelRight < maxLevelLeft ) ) {
350 if ( cloneEndNode ) {
351 consume( rightNode, levelParent, false, doNotRemoveEndNode );
352 } else if ( cloneEndText ) {
353 levelParent.append( range.document.createText( rightNode.substring( 0, endOffset ) ) );
354 }
355 } else if ( doClone ) {
356 nextLevelParent = levelParent.append( rightNode.clone( 0, cloneId ) );
357 }
358
359 // 2.
360 // The second step is to handle all left (selected) siblings of the rightNode which
361 // have not yet been handled. If the level branches were connected, the previous loop
362 // already copied all siblings (except the current rightNode).
363 if ( level > lastConnectedLevel ) {
364 while ( nextSibling ) {
365 nextSibling = consume( nextSibling, levelParent, true );
366 }
367 }
368
369 levelParent = nextLevelParent;
370 } else if ( doClone ) {
371 // If this is "shared" node and we are in cloning mode we have to update levelParent to
372 // reflect that we visited the node (even though we didn't process it).
373 // If we don't do that, in next iterations nodes will be appended to wrong parent.
374 //
375 // We can just take first child because the algorithm guarantees
376 // that this will be the only child on this level. (#13568)
377 levelParent = levelParent.getChild( 0 );
378 }
379 }
380
381 // Delete or Extract.
382 // We need to update the range and if mergeThen was passed do it.
383 if ( !isClone ) {
384 mergeAndUpdate();
385 }
386
387 // Depending on an action:
388 // * clones node and adds to new parent,
389 // * removes node,
390 // * moves node to the new parent.
391 function consume( node, newParent, toStart, forceClone ) {
392 var nextSibling = toStart ? node.getPrevious() : node.getNext();
393
394 // We do not clone if we are only deleting, so do nothing.
395 if ( forceClone && isDelete ) {
396 return nextSibling;
397 }
398
399 // If cloning, just clone it.
400 if ( isClone || forceClone ) {
401 newParent.append( node.clone( true, cloneId ), toStart );
402 } else {
403 // Both Delete and Extract will remove the node.
404 node.remove();
405
406 // When Extracting, move the removed node to the docFrag.
407 if ( isExtract ) {
408 newParent.append( node );
409 }
410 }
411
412 return nextSibling;
413 }
414
415 // Finds a level number on which both branches starts diverging.
416 // If such level does not exist, return the last on which both branches have nodes.
417 function findMinLevel() {
418 // Compare them, to find the top most siblings.
419 var i, topStart, topEnd,
420 maxLevel = Math.min( startParents.length, endParents.length );
421
422 for ( i = 0; i < maxLevel; i++ ) {
423 topStart = startParents[ i ];
424 topEnd = endParents[ i ];
425
426 // The compared nodes will match until we find the top most siblings (different nodes that have the same parent).
427 // "i" will hold the index in the parents array for the top most element.
428 if ( !topStart.equals( topEnd ) ) {
429 return i;
430 }
431 }
432
433 // When startNode == endNode.
434 return i - 1;
435 }
436
437 // Executed only when deleting or extracting to update range position
438 // and perform the merge operation.
439 function mergeAndUpdate() {
440 var commonLevel = minLevel - 1,
441 boundariesInEmptyNode = doNotRemoveStartNode && doNotRemoveEndNode && !startNode.equals( endNode );
442
443 // If a node has been partially selected, collapse the range between
444 // startParents[ minLevel + 1 ] and endParents[ minLevel + 1 ] (the first diverged elements).
445 // Otherwise, simply collapse it to the start. (W3C specs).
446 //
447 // All clear, right?
448 //
449 // It took me few hours to truly understand a previous version of this condition.
450 // Mine seems to be more straightforward (even if it doesn't look so) and I could leave you here
451 // without additional comments, but I'm not that mean so here goes the explanation.
452 //
453 // We want to know if both ends of the range are anchored in the same element. Really. It's this simple.
454 // But why? Because we need to differentiate situations like:
455 //
456 // <p>foo[<b>x</b>bar]y</p> (commonLevel = p, maxLL = "foo", maxLR = "y")
457 // from:
458 // <p>foo<b>x[</b>bar]y</p> (commonLevel = p, maxLL = "x", maxLR = "y")
459 //
460 // In the first case we can collapse the range to the left, because simply everything between range's
461 // boundaries was removed.
462 // In the second case we must place the range after </b>, because <b> was only **partially selected**.
463 //
464 // * <b> is our startParents[ commonLevel + 1 ]
465 // * "y" is our endParents[ commonLevel + 1 ].
466 //
467 // By now "bar" is removed from the DOM so <b> is a direct sibling of "y":
468 // <p>foo<b>x</b>y</p>
469 //
470 // Therefore it's enough to place the range between <b> and "y".
471 //
472 // Now, what does the comparison mean? Why not just taking startNode and endNode and checking
473 // their parents? Because the tree is already changed and they may be gone. Plus, thanks to
474 // cloneStartNode and cloneEndNode, that would be reaaaaly tricky.
475 //
476 // So we play with levels which can give us the same information:
477 // * commonLevel - the level of common ancestor,
478 // * maxLevel - 1 - the level of range boundary parent (range boundary is here like a bookmark span).
479 // * commonLevel < maxLevel - 1 - whether the range boundary is not a child of common ancestor.
480 //
481 // There's also an edge case in which both range boundaries were placed in empty nodes like:
482 // <p>[</p><p>]</p>
483 // Those boundaries were not removed, but in this case start and end nodes are child of the common ancestor.
484 // We handle this edge case separately.
485 if ( commonLevel < ( maxLevelLeft - 1 ) || commonLevel < ( maxLevelRight - 1 ) || boundariesInEmptyNode ) {
486 if ( boundariesInEmptyNode ) {
487 range.moveToPosition( endNode, CKEDITOR.POSITION_BEFORE_START );
488 } else if ( ( maxLevelRight == commonLevel + 1 ) && cloneEndNode ) {
489 // The maxLevelRight + 1 element could be already removed so we use the fact that
490 // we know that it was the last element in its parent.
491 range.moveToPosition( endParents[ commonLevel ], CKEDITOR.POSITION_BEFORE_END );
492 } else {
493 range.moveToPosition( endParents[ commonLevel + 1 ], CKEDITOR.POSITION_BEFORE_START );
494 }
495
496 // Merge split parents.
497 if ( mergeThen ) {
498 // Find the first diverged node in the left branch.
499 var topLeft = startParents[ commonLevel + 1 ];
500
501 // TopLeft may simply not exist if commonLevel == maxLevel or may be a text node.
502 if ( topLeft && topLeft.type == CKEDITOR.NODE_ELEMENT ) {
503 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
504 'data-cke-bookmark="1" style="display:none">&nbsp;</span>', range.document );
505 span.insertAfter( topLeft );
506 topLeft.mergeSiblings( false );
507 range.moveToBookmark( { startNode: span } );
508 }
509 }
510 } else {
511 // Collapse it to the start.
512 range.collapse( true );
513 }
514 }
515 }
516
517 var inlineChildReqElements = {
518 abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
519 dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
520 strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1
521 };
522
523 // Creates the appropriate node evaluator for the dom walker used inside
524 // check(Start|End)OfBlock.
525 function getCheckStartEndBlockEvalFunction() {
526 var skipBogus = false,
527 whitespaces = CKEDITOR.dom.walker.whitespaces(),
528 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
529 isBogus = CKEDITOR.dom.walker.bogus();
530
531 return function( node ) {
532 // First skip empty nodes
533 if ( bookmarkEvaluator( node ) || whitespaces( node ) )
534 return true;
535
536 // Skip the bogus node at the end of block.
537 if ( isBogus( node ) && !skipBogus ) {
538 skipBogus = true;
539 return true;
540 }
541
542 // If there's any visible text, then we're not at the start.
543 if ( node.type == CKEDITOR.NODE_TEXT &&
544 ( node.hasAscendant( 'pre' ) ||
545 CKEDITOR.tools.trim( node.getText() ).length ) ) {
546 return false;
547 }
548
549 // If there are non-empty inline elements (e.g. <img />), then we're not
550 // at the start.
551 if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( inlineChildReqElements ) )
552 return false;
553
554 return true;
555 };
556 }
557
558 var isBogus = CKEDITOR.dom.walker.bogus(),
559 nbspRegExp = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
560 editableEval = CKEDITOR.dom.walker.editable(),
561 notIgnoredEval = CKEDITOR.dom.walker.ignored( true );
562
563 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
564 // text node and non-empty elements unless it's being bookmark text.
565 function elementBoundaryEval( checkStart ) {
566 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
567 bookmark = CKEDITOR.dom.walker.bookmark( 1 );
568
569 return function( node ) {
570 // First skip empty nodes.
571 if ( bookmark( node ) || whitespaces( node ) )
572 return true;
573
574 // Tolerant bogus br when checking at the end of block.
575 // Reject any text node unless it's being bookmark
576 // OR it's spaces.
577 // Reject any element unless it's being invisible empty. (#3883)
578 return !checkStart && isBogus( node ) ||
579 node.type == CKEDITOR.NODE_ELEMENT &&
580 node.is( CKEDITOR.dtd.$removeEmpty );
581 };
582 }
583
584 function getNextEditableNode( isPrevious ) {
585 return function() {
586 var first;
587
588 return this[ isPrevious ? 'getPreviousNode' : 'getNextNode' ]( function( node ) {
589 // Cache first not ignorable node.
590 if ( !first && notIgnoredEval( node ) )
591 first = node;
592
593 // Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
594 return editableEval( node ) && !( isBogus( node ) && node.equals( first ) );
595 } );
596 };
597 }
598
599 CKEDITOR.dom.range.prototype = {
600 /**
601 * Clones this range.
602 *
603 * @returns {CKEDITOR.dom.range}
604 */
605 clone: function() {
606 var clone = new CKEDITOR.dom.range( this.root );
607
608 clone._setStartContainer( this.startContainer );
609 clone.startOffset = this.startOffset;
610 clone._setEndContainer( this.endContainer );
611 clone.endOffset = this.endOffset;
612 clone.collapsed = this.collapsed;
613
614 return clone;
615 },
616
617 /**
618 * Makes the range collapsed by moving its start point (or end point if `toStart==true`)
619 * to the second end.
620 *
621 * @param {Boolean} toStart Collapse range "to start".
622 */
623 collapse: function( toStart ) {
624 if ( toStart ) {
625 this._setEndContainer( this.startContainer );
626 this.endOffset = this.startOffset;
627 } else {
628 this._setStartContainer( this.endContainer );
629 this.startOffset = this.endOffset;
630 }
631
632 this.collapsed = true;
633 },
634
635 /**
636 * Clones content nodes of the range and adds them to a document fragment, which is returned.
637 *
638 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the clone.
639 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing a clone of range's content.
640 */
641 cloneContents: function( cloneId ) {
642 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
643
644 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
645
646 if ( !this.collapsed )
647 execContentsAction( this, 2, docFrag, false, cloneId );
648
649 return docFrag;
650 },
651
652 /**
653 * Deletes the content nodes of the range permanently from the DOM tree.
654 *
655 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
656 */
657 deleteContents: function( mergeThen ) {
658 if ( this.collapsed )
659 return;
660
661 execContentsAction( this, 0, null, mergeThen );
662 },
663
664 /**
665 * The content nodes of the range are cloned and added to a document fragment,
666 * meanwhile they are removed permanently from the DOM tree.
667 *
668 * **Note:** Setting the `cloneId` parameter to `false` works for **partially** selected elements only.
669 * If an element with an ID attribute is **fully enclosed** in a range, it will keep the ID attribute
670 * regardless of the `cloneId` parameter value, because it is not cloned &mdash; it is moved to the returned
671 * document fragment.
672 *
673 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
674 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the extracted content.
675 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
676 */
677 extractContents: function( mergeThen, cloneId ) {
678 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
679
680 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
681
682 if ( !this.collapsed )
683 execContentsAction( this, 1, docFrag, mergeThen, cloneId );
684
685 return docFrag;
686 },
687
688 /**
689 * Creates a bookmark object, which can be later used to restore the
690 * range by using the {@link #moveToBookmark} function.
691 *
692 * This is an "intrusive" way to create a bookmark. It includes `<span>` tags
693 * in the range boundaries. The advantage of it is that it is possible to
694 * handle DOM mutations when moving back to the bookmark.
695 *
696 * **Note:** The inclusion of nodes in the DOM is a design choice and
697 * should not be changed as there are other points in the code that may be
698 * using those nodes to perform operations.
699 *
700 * @param {Boolean} [serializable] Indicates that the bookmark nodes
701 * must contain IDs, which can be used to restore the range even
702 * when these nodes suffer mutations (like cloning or `innerHTML` change).
703 * @returns {Object} And object representing a bookmark.
704 * @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
705 * @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
706 * @returns {Boolean} return.serializable
707 * @returns {Boolean} return.collapsed
708 */
709 createBookmark: function( serializable ) {
710 var startNode, endNode;
711 var baseId;
712 var clone;
713 var collapsed = this.collapsed;
714
715 startNode = this.document.createElement( 'span' );
716 startNode.data( 'cke-bookmark', 1 );
717 startNode.setStyle( 'display', 'none' );
718
719 // For IE, it must have something inside, otherwise it may be
720 // removed during DOM operations.
721 startNode.setHtml( '&nbsp;' );
722
723 if ( serializable ) {
724 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
725 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
726 }
727
728 // If collapsed, the endNode will not be created.
729 if ( !collapsed ) {
730 endNode = startNode.clone();
731 endNode.setHtml( '&nbsp;' );
732
733 if ( serializable )
734 endNode.setAttribute( 'id', baseId + 'E' );
735
736 clone = this.clone();
737 clone.collapse();
738 clone.insertNode( endNode );
739 }
740
741 clone = this.clone();
742 clone.collapse( true );
743 clone.insertNode( startNode );
744
745 // Update the range position.
746 if ( endNode ) {
747 this.setStartAfter( startNode );
748 this.setEndBefore( endNode );
749 } else {
750 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
751 }
752
753 return {
754 startNode: serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
755 endNode: serializable ? baseId + 'E' : endNode,
756 serializable: serializable,
757 collapsed: collapsed
758 };
759 },
760
761 /**
762 * Creates a "non intrusive" and "mutation sensible" bookmark. This
763 * kind of bookmark should be used only when the DOM is supposed to
764 * remain stable after its creation.
765 *
766 * @param {Boolean} [normalized] Indicates that the bookmark must
767 * be normalized. When normalized, the successive text nodes are
768 * considered a single node. To successfully load a normalized
769 * bookmark, the DOM tree must also be normalized before calling
770 * {@link #moveToBookmark}.
771 * @returns {Object} An object representing the bookmark.
772 * @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
773 * @returns {Array} return.end Start container's address.
774 * @returns {Number} return.startOffset
775 * @returns {Number} return.endOffset
776 * @returns {Boolean} return.collapsed
777 * @returns {Boolean} return.normalized
778 * @returns {Boolean} return.is2 This is "bookmark2".
779 */
780 createBookmark2: ( function() {
781 var isNotText = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_TEXT, true );
782
783 // Returns true for limit anchored in element and placed between text nodes.
784 //
785 // v
786 // <p>[text node] [text node]</p> -> true
787 //
788 // v
789 // <p> [text node]</p> -> false
790 //
791 // v
792 // <p>[text node][text node]</p> -> false (limit is anchored in text node)
793 function betweenTextNodes( container, offset ) {
794 // Not anchored in element or limit is on the edge.
795 if ( container.type != CKEDITOR.NODE_ELEMENT || offset === 0 || offset == container.getChildCount() )
796 return 0;
797
798 return container.getChild( offset - 1 ).type == CKEDITOR.NODE_TEXT &&
799 container.getChild( offset ).type == CKEDITOR.NODE_TEXT;
800 }
801
802 // Sums lengths of all preceding text nodes.
803 function getLengthOfPrecedingTextNodes( node ) {
804 var sum = 0;
805
806 while ( ( node = node.getPrevious() ) && node.type == CKEDITOR.NODE_TEXT )
807 sum += node.getLength();
808
809 return sum;
810 }
811
812 function normalize( limit ) {
813 var container = limit.container,
814 offset = limit.offset;
815
816 // If limit is between text nodes move it to the end of preceding one,
817 // because they will be merged.
818 if ( betweenTextNodes( container, offset ) ) {
819 container = container.getChild( offset - 1 );
820 offset = container.getLength();
821 }
822
823 // Now, if limit is anchored in element and has at least two nodes before it,
824 // it may happen that some of them will be merged. Normalize the offset
825 // by setting it to normalized index of its preceding node.
826 if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 1 )
827 offset = container.getChild( offset - 1 ).getIndex( true ) + 1;
828
829 // The last step - fix the offset inside text node by adding
830 // lengths of preceding text nodes which will be merged with container.
831 if ( container.type == CKEDITOR.NODE_TEXT ) {
832 var precedingLength = getLengthOfPrecedingTextNodes( container );
833
834 // Normal case - text node is not empty.
835 if ( container.getText() ) {
836 offset += precedingLength;
837
838 // Awful case - the text node is empty and thus will be totally lost.
839 // In this case we are trying to normalize the limit to the left:
840 // * either to the preceding text node,
841 // * or to the "gap" after the preceding element.
842 } else {
843 // Find the closest non-text sibling.
844 var precedingContainer = container.getPrevious( isNotText );
845
846 // If there are any characters on the left, that means that we can anchor
847 // there, because this text node will not be lost.
848 if ( precedingLength ) {
849 offset = precedingLength;
850
851 if ( precedingContainer ) {
852 // The text node is the first node after the closest non-text sibling.
853 container = precedingContainer.getNext();
854 } else {
855 // But if there was no non-text sibling, then the text node is the first child.
856 container = container.getParent().getFirst();
857 }
858
859 // If there are no characters on the left, then anchor after the previous non-text node.
860 // E.g. (see tests for a legend :D):
861 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
862 } else {
863 container = container.getParent();
864 offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0;
865 }
866 }
867 }
868
869 limit.container = container;
870 limit.offset = offset;
871 }
872
873 return function( normalized ) {
874 var collapsed = this.collapsed,
875 bmStart = {
876 container: this.startContainer,
877 offset: this.startOffset
878 },
879 bmEnd = {
880 container: this.endContainer,
881 offset: this.endOffset
882 };
883
884 if ( normalized ) {
885 normalize( bmStart );
886
887 if ( !collapsed )
888 normalize( bmEnd );
889 }
890
891 return {
892 start: bmStart.container.getAddress( normalized ),
893 end: collapsed ? null : bmEnd.container.getAddress( normalized ),
894 startOffset: bmStart.offset,
895 endOffset: bmEnd.offset,
896 normalized: normalized,
897 collapsed: collapsed,
898 is2: true // It's a createBookmark2 bookmark.
899 };
900 };
901 } )(),
902
903 /**
904 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
905 *
906 * If serializable bookmark passed, then its `<span>` markers will be removed.
907 *
908 * @param {Object} bookmark
909 */
910 moveToBookmark: function( bookmark ) {
911 // Created with createBookmark2().
912 if ( bookmark.is2 ) {
913 // Get the start information.
914 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
915 startOffset = bookmark.startOffset;
916
917 // Get the end information.
918 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
919 endOffset = bookmark.endOffset;
920
921 // Set the start boundary.
922 this.setStart( startContainer, startOffset );
923
924 // Set the end boundary. If not available, collapse it.
925 if ( endContainer )
926 this.setEnd( endContainer, endOffset );
927 else
928 this.collapse( true );
929 }
930 // Created with createBookmark().
931 else {
932 var serializable = bookmark.serializable,
933 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
934 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
935
936 // Set the range start at the bookmark start node position.
937 this.setStartBefore( startNode );
938
939 // Remove it, because it may interfere in the setEndBefore call.
940 startNode.remove();
941
942 // Set the range end at the bookmark end node position, or simply
943 // collapse it if it is not available.
944 if ( endNode ) {
945 this.setEndBefore( endNode );
946 endNode.remove();
947 } else {
948 this.collapse( true );
949 }
950 }
951 },
952
953 /**
954 * Returns two nodes which are on the boundaries of this range.
955 *
956 * @returns {Object}
957 * @returns {CKEDITOR.dom.node} return.startNode
958 * @returns {CKEDITOR.dom.node} return.endNode
959 * @todo precise desc/algorithm
960 */
961 getBoundaryNodes: function() {
962 var startNode = this.startContainer,
963 endNode = this.endContainer,
964 startOffset = this.startOffset,
965 endOffset = this.endOffset,
966 childCount;
967
968 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
969 childCount = startNode.getChildCount();
970 if ( childCount > startOffset ) {
971 startNode = startNode.getChild( startOffset );
972 } else if ( childCount < 1 ) {
973 startNode = startNode.getPreviousSourceNode();
974 }
975 // startOffset > childCount but childCount is not 0
976 else {
977 // Try to take the node just after the current position.
978 startNode = startNode.$;
979 while ( startNode.lastChild )
980 startNode = startNode.lastChild;
981 startNode = new CKEDITOR.dom.node( startNode );
982
983 // Normally we should take the next node in DFS order. But it
984 // is also possible that we've already reached the end of
985 // document.
986 startNode = startNode.getNextSourceNode() || startNode;
987 }
988 }
989
990 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
991 childCount = endNode.getChildCount();
992 if ( childCount > endOffset ) {
993 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
994 } else if ( childCount < 1 ) {
995 endNode = endNode.getPreviousSourceNode();
996 }
997 // endOffset > childCount but childCount is not 0.
998 else {
999 // Try to take the node just before the current position.
1000 endNode = endNode.$;
1001 while ( endNode.lastChild )
1002 endNode = endNode.lastChild;
1003 endNode = new CKEDITOR.dom.node( endNode );
1004 }
1005 }
1006
1007 // Sometimes the endNode will come right before startNode for collapsed
1008 // ranges. Fix it. (#3780)
1009 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1010 startNode = endNode;
1011
1012 return { startNode: startNode, endNode: endNode };
1013 },
1014
1015 /**
1016 * Find the node which fully contains the range.
1017 *
1018 * @param {Boolean} [includeSelf=false]
1019 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1020 * @returns {CKEDITOR.dom.element}
1021 */
1022 getCommonAncestor: function( includeSelf, ignoreTextNode ) {
1023 var start = this.startContainer,
1024 end = this.endContainer,
1025 ancestor;
1026
1027 if ( start.equals( end ) ) {
1028 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
1029 ancestor = start.getChild( this.startOffset );
1030 else
1031 ancestor = start;
1032 } else {
1033 ancestor = start.getCommonAncestor( end );
1034 }
1035
1036 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
1037 },
1038
1039 /**
1040 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1041 * nodes to element nodes, whenever possible. This is actually possible
1042 * if either of the boundary containers point to a text node, and its
1043 * offset is set to zero, or after the last char in the node.
1044 */
1045 optimize: function() {
1046 var container = this.startContainer;
1047 var offset = this.startOffset;
1048
1049 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1050 if ( !offset )
1051 this.setStartBefore( container );
1052 else if ( offset >= container.getLength() )
1053 this.setStartAfter( container );
1054 }
1055
1056 container = this.endContainer;
1057 offset = this.endOffset;
1058
1059 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1060 if ( !offset )
1061 this.setEndBefore( container );
1062 else if ( offset >= container.getLength() )
1063 this.setEndAfter( container );
1064 }
1065 },
1066
1067 /**
1068 * Move the range out of bookmark nodes if they'd been the container.
1069 */
1070 optimizeBookmark: function() {
1071 var startNode = this.startContainer,
1072 endNode = this.endContainer;
1073
1074 if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
1075 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
1076 if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
1077 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
1078 },
1079
1080 /**
1081 * @param {Boolean} [ignoreStart=false]
1082 * @param {Boolean} [ignoreEnd=false]
1083 * @todo precise desc/algorithm
1084 */
1085 trim: function( ignoreStart, ignoreEnd ) {
1086 var startContainer = this.startContainer,
1087 startOffset = this.startOffset,
1088 collapsed = this.collapsed;
1089 if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1090 // If the offset is zero, we just insert the new node before
1091 // the start.
1092 if ( !startOffset ) {
1093 startOffset = startContainer.getIndex();
1094 startContainer = startContainer.getParent();
1095 }
1096 // If the offset is at the end, we'll insert it after the text
1097 // node.
1098 else if ( startOffset >= startContainer.getLength() ) {
1099 startOffset = startContainer.getIndex() + 1;
1100 startContainer = startContainer.getParent();
1101 }
1102 // In other case, we split the text node and insert the new
1103 // node at the split point.
1104 else {
1105 var nextText = startContainer.split( startOffset );
1106
1107 startOffset = startContainer.getIndex() + 1;
1108 startContainer = startContainer.getParent();
1109
1110 // Check all necessity of updating the end boundary.
1111 if ( this.startContainer.equals( this.endContainer ) )
1112 this.setEnd( nextText, this.endOffset - this.startOffset );
1113 else if ( startContainer.equals( this.endContainer ) )
1114 this.endOffset += 1;
1115 }
1116
1117 this.setStart( startContainer, startOffset );
1118
1119 if ( collapsed ) {
1120 this.collapse( true );
1121 return;
1122 }
1123 }
1124
1125 var endContainer = this.endContainer;
1126 var endOffset = this.endOffset;
1127
1128 if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1129 // If the offset is zero, we just insert the new node before
1130 // the start.
1131 if ( !endOffset ) {
1132 endOffset = endContainer.getIndex();
1133 endContainer = endContainer.getParent();
1134 }
1135 // If the offset is at the end, we'll insert it after the text
1136 // node.
1137 else if ( endOffset >= endContainer.getLength() ) {
1138 endOffset = endContainer.getIndex() + 1;
1139 endContainer = endContainer.getParent();
1140 }
1141 // In other case, we split the text node and insert the new
1142 // node at the split point.
1143 else {
1144 endContainer.split( endOffset );
1145
1146 endOffset = endContainer.getIndex() + 1;
1147 endContainer = endContainer.getParent();
1148 }
1149
1150 this.setEnd( endContainer, endOffset );
1151 }
1152 },
1153
1154 /**
1155 * Expands the range so that partial units are completely contained.
1156 *
1157 * @param unit {Number} The unit type to expand with.
1158 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1159 */
1160 enlarge: function( unit, excludeBrs ) {
1161 var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
1162
1163 switch ( unit ) {
1164 case CKEDITOR.ENLARGE_INLINE:
1165 var enlargeInlineOnly = 1;
1166
1167 /* falls through */
1168 case CKEDITOR.ENLARGE_ELEMENT:
1169
1170 if ( this.collapsed )
1171 return;
1172
1173 // Get the common ancestor.
1174 var commonAncestor = this.getCommonAncestor();
1175
1176 var boundary = this.root;
1177
1178 // For each boundary
1179 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1180 // if not available, to be enlarge.
1181 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1182 // common ancestor is not reached. After reaching the common ancestor, just save the
1183 // enlargeable node to be used later.
1184
1185 var startTop, endTop;
1186
1187 var enlargeable, sibling, commonReached;
1188
1189 // Indicates that the node can be added only if whitespace
1190 // is available before it.
1191 var needsWhiteSpace = false;
1192 var isWhiteSpace;
1193 var siblingText;
1194
1195 // Process the start boundary.
1196
1197 var container = this.startContainer;
1198 var offset = this.startOffset;
1199
1200 if ( container.type == CKEDITOR.NODE_TEXT ) {
1201 if ( offset ) {
1202 // Check if there is any non-space text before the
1203 // offset. Otherwise, container is null.
1204 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
1205
1206 // If we found only whitespace in the node, it
1207 // means that we'll need more whitespace to be able
1208 // to expand. For example, <i> can be expanded in
1209 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1210 needsWhiteSpace = !!container;
1211 }
1212
1213 if ( container ) {
1214 if ( !( sibling = container.getPrevious() ) )
1215 enlargeable = container.getParent();
1216 }
1217 } else {
1218 // If we have offset, get the node preceeding it as the
1219 // first sibling to be checked.
1220 if ( offset )
1221 sibling = container.getChild( offset - 1 ) || container.getLast();
1222
1223 // If there is no sibling, mark the container to be
1224 // enlarged.
1225 if ( !sibling )
1226 enlargeable = container;
1227 }
1228
1229 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1230 enlargeable = getValidEnlargeable( enlargeable );
1231
1232 while ( enlargeable || sibling ) {
1233 if ( enlargeable && !sibling ) {
1234 // If we reached the common ancestor, mark the flag
1235 // for it.
1236 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1237 commonReached = true;
1238
1239 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1240 break;
1241
1242 // If we don't need space or this element breaks
1243 // the line, then enlarge it.
1244 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1245 needsWhiteSpace = false;
1246
1247 // If the common ancestor has been reached,
1248 // we'll not enlarge it immediately, but just
1249 // mark it to be enlarged later if the end
1250 // boundary also enlarges it.
1251 if ( commonReached )
1252 startTop = enlargeable;
1253 else
1254 this.setStartBefore( enlargeable );
1255 }
1256
1257 sibling = enlargeable.getPrevious();
1258 }
1259
1260 // Check all sibling nodes preceeding the enlargeable
1261 // node. The node wil lbe enlarged only if none of them
1262 // blocks it.
1263 while ( sibling ) {
1264 // This flag indicates that this node has
1265 // whitespaces at the end.
1266 isWhiteSpace = false;
1267
1268 if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
1269 sibling = sibling.getPrevious();
1270 continue;
1271 } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1272 siblingText = sibling.getText();
1273
1274 if ( leadingWhitespaceRegex.test( siblingText ) )
1275 sibling = null;
1276
1277 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1278 } else {
1279 // #12221 (Chrome) plus #11111 (Safari).
1280 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1281
1282 // If this is a visible element.
1283 // We need to check for the bookmark attribute because IE insists on
1284 // rendering the display:none nodes we use for bookmarks. (#3363)
1285 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1286 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1287 // We'll accept it only if we need
1288 // whitespace, and this is an inline
1289 // element with whitespace only.
1290 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1291 // It must contains spaces and inline elements only.
1292
1293 siblingText = sibling.getText();
1294
1295 if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1296 sibling = null;
1297 else {
1298 var allChildren = sibling.$.getElementsByTagName( '*' );
1299 for ( var i = 0, child; child = allChildren[ i++ ]; ) {
1300 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1301 sibling = null;
1302 break;
1303 }
1304 }
1305 }
1306
1307 if ( sibling )
1308 isWhiteSpace = !!siblingText.length;
1309 } else {
1310 sibling = null;
1311 }
1312 }
1313 }
1314
1315 // A node with whitespaces has been found.
1316 if ( isWhiteSpace ) {
1317 // Enlarge the last enlargeable node, if we
1318 // were waiting for spaces.
1319 if ( needsWhiteSpace ) {
1320 if ( commonReached )
1321 startTop = enlargeable;
1322 else if ( enlargeable )
1323 this.setStartBefore( enlargeable );
1324 } else {
1325 needsWhiteSpace = true;
1326 }
1327 }
1328
1329 if ( sibling ) {
1330 var next = sibling.getPrevious();
1331
1332 if ( !enlargeable && !next ) {
1333 // Set the sibling as enlargeable, so it's
1334 // parent will be get later outside this while.
1335 enlargeable = sibling;
1336 sibling = null;
1337 break;
1338 }
1339
1340 sibling = next;
1341 } else {
1342 // If sibling has been set to null, then we
1343 // need to stop enlarging.
1344 enlargeable = null;
1345 }
1346 }
1347
1348 if ( enlargeable )
1349 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1350 }
1351
1352 // Process the end boundary. This is basically the same
1353 // code used for the start boundary, with small changes to
1354 // make it work in the oposite side (to the right). This
1355 // makes it difficult to reuse the code here. So, fixes to
1356 // the above code are likely to be replicated here.
1357
1358 container = this.endContainer;
1359 offset = this.endOffset;
1360
1361 // Reset the common variables.
1362 enlargeable = sibling = null;
1363 commonReached = needsWhiteSpace = false;
1364
1365 // Function check if there are only whitespaces from the given starting point
1366 // (startContainer and startOffset) till the end on block.
1367 // Examples ("[" is the start point):
1368 // - <p>foo[ </p> - will return true,
1369 // - <p><b>foo[ </b> </p> - will return true,
1370 // - <p>foo[ bar</p> - will return false,
1371 // - <p><b>foo[ </b>bar</p> - will return false,
1372 // - <p>foo[ <b></b></p> - will return false.
1373 function onlyWhiteSpaces( startContainer, startOffset ) {
1374 // We need to enlarge range if there is white space at the end of the block,
1375 // because it is not displayed in WYSIWYG mode and user can not select it. So
1376 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1377 // we should do nothing if we are not at the end of the block, so this should not
1378 // be changed: "<p><i>[foo] </i>bar</p>".
1379 var walkerRange = new CKEDITOR.dom.range( boundary );
1380 walkerRange.setStart( startContainer, startOffset );
1381 // The guard will find the end of range so I put boundary here.
1382 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1383
1384 var walker = new CKEDITOR.dom.walker( walkerRange ),
1385 node;
1386
1387 walker.guard = function( node ) {
1388 // Stop if you exit block.
1389 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
1390 };
1391
1392 while ( ( node = walker.next() ) ) {
1393 if ( node.type != CKEDITOR.NODE_TEXT ) {
1394 // Stop if you enter to any node (walker.next() will return node only
1395 // it goes out, not if it is go into node).
1396 return false;
1397 } else {
1398 // Trim the first node to startOffset.
1399 if ( node != startContainer )
1400 siblingText = node.getText();
1401 else
1402 siblingText = node.substring( startOffset );
1403
1404 // Check if it is white space.
1405 if ( leadingWhitespaceRegex.test( siblingText ) )
1406 return false;
1407 }
1408 }
1409
1410 return true;
1411 }
1412
1413 if ( container.type == CKEDITOR.NODE_TEXT ) {
1414 // Check if there is only white space after the offset.
1415 if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
1416 // If we found only whitespace in the node, it
1417 // means that we'll need more whitespace to be able
1418 // to expand. For example, <i> can be expanded in
1419 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1420 needsWhiteSpace = true;
1421 } else {
1422 needsWhiteSpace = !container.getLength();
1423
1424 if ( offset == container.getLength() ) {
1425 // If we are at the end of container and this is the last text node,
1426 // we should enlarge end to the parent.
1427 if ( !( sibling = container.getNext() ) )
1428 enlargeable = container.getParent();
1429 } else {
1430 // If we are in the middle on text node and there are only whitespaces
1431 // till the end of block, we should enlarge element.
1432 if ( onlyWhiteSpaces( container, offset ) )
1433 enlargeable = container.getParent();
1434 }
1435 }
1436 } else {
1437 // Get the node right after the boudary to be checked
1438 // first.
1439 sibling = container.getChild( offset );
1440
1441 if ( !sibling )
1442 enlargeable = container;
1443 }
1444
1445 while ( enlargeable || sibling ) {
1446 if ( enlargeable && !sibling ) {
1447 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1448 commonReached = true;
1449
1450 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1451 break;
1452
1453 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1454 needsWhiteSpace = false;
1455
1456 if ( commonReached )
1457 endTop = enlargeable;
1458 else if ( enlargeable )
1459 this.setEndAfter( enlargeable );
1460 }
1461
1462 sibling = enlargeable.getNext();
1463 }
1464
1465 while ( sibling ) {
1466 isWhiteSpace = false;
1467
1468 if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1469 siblingText = sibling.getText();
1470
1471 // Check if there are not whitespace characters till the end of editable.
1472 // If so stop expanding.
1473 if ( !onlyWhiteSpaces( sibling, 0 ) )
1474 sibling = null;
1475
1476 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1477 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1478 // If this is a visible element.
1479 // We need to check for the bookmark attribute because IE insists on
1480 // rendering the display:none nodes we use for bookmarks. (#3363)
1481 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1482 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1483 // We'll accept it only if we need
1484 // whitespace, and this is an inline
1485 // element with whitespace only.
1486 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1487 // It must contains spaces and inline elements only.
1488
1489 siblingText = sibling.getText();
1490
1491 if ( leadingWhitespaceRegex.test( siblingText ) )
1492 sibling = null;
1493 else {
1494 allChildren = sibling.$.getElementsByTagName( '*' );
1495 for ( i = 0; child = allChildren[ i++ ]; ) {
1496 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1497 sibling = null;
1498 break;
1499 }
1500 }
1501 }
1502
1503 if ( sibling )
1504 isWhiteSpace = !!siblingText.length;
1505 } else {
1506 sibling = null;
1507 }
1508 }
1509 } else {
1510 isWhiteSpace = 1;
1511 }
1512
1513 if ( isWhiteSpace ) {
1514 if ( needsWhiteSpace ) {
1515 if ( commonReached )
1516 endTop = enlargeable;
1517 else
1518 this.setEndAfter( enlargeable );
1519 }
1520 }
1521
1522 if ( sibling ) {
1523 next = sibling.getNext();
1524
1525 if ( !enlargeable && !next ) {
1526 enlargeable = sibling;
1527 sibling = null;
1528 break;
1529 }
1530
1531 sibling = next;
1532 } else {
1533 // If sibling has been set to null, then we
1534 // need to stop enlarging.
1535 enlargeable = null;
1536 }
1537 }
1538
1539 if ( enlargeable )
1540 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1541 }
1542
1543 // If the common ancestor can be enlarged by both boundaries, then include it also.
1544 if ( startTop && endTop ) {
1545 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1546
1547 this.setStartBefore( commonAncestor );
1548 this.setEndAfter( commonAncestor );
1549 }
1550 break;
1551
1552 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1553 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1554
1555 // Enlarging the start boundary.
1556 var walkerRange = new CKEDITOR.dom.range( this.root );
1557
1558 boundary = this.root;
1559
1560 walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
1561 walkerRange.setEnd( this.startContainer, this.startOffset );
1562
1563 var walker = new CKEDITOR.dom.walker( walkerRange ),
1564 blockBoundary, // The node on which the enlarging should stop.
1565 tailBr, // In case BR as block boundary.
1566 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
1567 inNonEditable = null,
1568 // Record the encountered 'blockBoundary' for later use.
1569 boundaryGuard = function( node ) {
1570 // We should not check contents of non-editable elements. It may happen
1571 // that inline widget has display:table child which should not block range#enlarge.
1572 // When encoutered non-editable element...
1573 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1574 if ( inNonEditable ) {
1575 // ... in which we already were, reset it (because we're leaving it) and return.
1576 if ( inNonEditable.equals( node ) ) {
1577 inNonEditable = null;
1578 return;
1579 }
1580 // ... which we're entering, remember it but check it (no return).
1581 } else {
1582 inNonEditable = node;
1583 }
1584 // When we are in non-editable element, do not check if current node is a block boundary.
1585 } else if ( inNonEditable ) {
1586 return;
1587 }
1588
1589 var retval = notBlockBoundary( node );
1590 if ( !retval )
1591 blockBoundary = node;
1592 return retval;
1593 },
1594 // Record the encounted 'tailBr' for later use.
1595 tailBrGuard = function( node ) {
1596 var retval = boundaryGuard( node );
1597 if ( !retval && node.is && node.is( 'br' ) )
1598 tailBr = node;
1599 return retval;
1600 };
1601
1602 walker.guard = boundaryGuard;
1603
1604 enlargeable = walker.lastBackward();
1605
1606 // It's the body which stop the enlarging if no block boundary found.
1607 blockBoundary = blockBoundary || boundary;
1608
1609 // Start the range either after the end of found block (<p>...</p>[text)
1610 // or at the start of block (<p>[text...), by comparing the document position
1611 // with 'enlargeable' node.
1612 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1613 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1614
1615 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1616 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1617 var theRange = this.clone();
1618 walker = new CKEDITOR.dom.walker( theRange );
1619
1620 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1621 bookmark = CKEDITOR.dom.walker.bookmark();
1622
1623 walker.evaluator = function( node ) {
1624 return !whitespaces( node ) && !bookmark( node );
1625 };
1626 var previous = walker.previous();
1627 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1628 return;
1629 }
1630
1631 // Enlarging the end boundary.
1632 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1633
1634 walkerRange = this.clone();
1635 walkerRange.collapse();
1636 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1637 walker = new CKEDITOR.dom.walker( walkerRange );
1638
1639 // tailBrGuard only used for on range end.
1640 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
1641 blockBoundary = inNonEditable = tailBr = null;
1642
1643 // End the range right before the block boundary node.
1644 enlargeable = walker.lastForward();
1645
1646 // It's the body which stop the enlarging if no block boundary found.
1647 blockBoundary = blockBoundary || boundary;
1648
1649 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1650 // by comparing the document position with 'enlargeable' node.
1651 this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() ||
1652 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
1653 // We must include the <br> at the end of range if there's
1654 // one and we're expanding list item contents
1655 if ( tailBr ) {
1656 this.setEndAfter( tailBr );
1657 }
1658 }
1659
1660 // Ensures that returned element can be enlarged by selection, null otherwise.
1661 // @param {CKEDITOR.dom.element} enlargeable
1662 // @returns {CKEDITOR.dom.element/null}
1663 function getValidEnlargeable( enlargeable ) {
1664 return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ?
1665 null : enlargeable;
1666 }
1667 },
1668
1669 /**
1670 * Descrease the range to make sure that boundaries
1671 * always anchor beside text nodes or innermost element.
1672 *
1673 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1674 *
1675 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
1676 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
1677 * node, range remains if there's no text nodes on boundaries at all.
1678 *
1679 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1680 */
1681 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
1682 // Unable to shrink a collapsed range.
1683 if ( !this.collapsed ) {
1684 mode = mode || CKEDITOR.SHRINK_TEXT;
1685
1686 var walkerRange = this.clone();
1687
1688 var startContainer = this.startContainer,
1689 endContainer = this.endContainer,
1690 startOffset = this.startOffset,
1691 endOffset = this.endOffset;
1692
1693 // Whether the start/end boundary is moveable.
1694 var moveStart = 1,
1695 moveEnd = 1;
1696
1697 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1698 if ( !startOffset )
1699 walkerRange.setStartBefore( startContainer );
1700 else if ( startOffset >= startContainer.getLength() )
1701 walkerRange.setStartAfter( startContainer );
1702 else {
1703 // Enlarge the range properly to avoid walker making
1704 // DOM changes caused by triming the text nodes later.
1705 walkerRange.setStartBefore( startContainer );
1706 moveStart = 0;
1707 }
1708 }
1709
1710 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1711 if ( !endOffset )
1712 walkerRange.setEndBefore( endContainer );
1713 else if ( endOffset >= endContainer.getLength() )
1714 walkerRange.setEndAfter( endContainer );
1715 else {
1716 walkerRange.setEndAfter( endContainer );
1717 moveEnd = 0;
1718 }
1719 }
1720
1721 var walker = new CKEDITOR.dom.walker( walkerRange ),
1722 isBookmark = CKEDITOR.dom.walker.bookmark();
1723
1724 walker.evaluator = function( node ) {
1725 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1726 };
1727
1728 var currentElement;
1729 walker.guard = function( node, movingOut ) {
1730 if ( isBookmark( node ) )
1731 return true;
1732
1733 // Stop when we're shrink in element mode while encountering a text node.
1734 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1735 return false;
1736
1737 // Stop when we've already walked "through" an element.
1738 if ( movingOut && node.equals( currentElement ) )
1739 return false;
1740
1741 if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
1742 return false;
1743
1744 // Stop shrinking when encountering an editable border.
1745 if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
1746 return false;
1747
1748 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1749 currentElement = node;
1750
1751 return true;
1752 };
1753
1754 if ( moveStart ) {
1755 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
1756 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1757 }
1758
1759 if ( moveEnd ) {
1760 walker.reset();
1761 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
1762 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1763 }
1764
1765 return !!( moveStart || moveEnd );
1766 }
1767 },
1768
1769 /**
1770 * Inserts a node at the start of the range. The range will be expanded
1771 * the contain the node.
1772 *
1773 * @param {CKEDITOR.dom.node} node
1774 */
1775 insertNode: function( node ) {
1776 this.optimizeBookmark();
1777 this.trim( false, true );
1778
1779 var startContainer = this.startContainer;
1780 var startOffset = this.startOffset;
1781
1782 var nextNode = startContainer.getChild( startOffset );
1783
1784 if ( nextNode )
1785 node.insertBefore( nextNode );
1786 else
1787 startContainer.append( node );
1788
1789 // Check if we need to update the end boundary.
1790 if ( node.getParent() && node.getParent().equals( this.endContainer ) )
1791 this.endOffset++;
1792
1793 // Expand the range to embrace the new node.
1794 this.setStartBefore( node );
1795 },
1796
1797 /**
1798 * Moves the range to given position according to specified node.
1799 *
1800 * // HTML: <p>Foo <b>bar</b></p>
1801 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1802 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1803 *
1804 * See also {@link #setStartAt} and {@link #setEndAt}.
1805 *
1806 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1807 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1808 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1809 * {@link CKEDITOR#POSITION_AFTER_END}.
1810 */
1811 moveToPosition: function( node, position ) {
1812 this.setStartAt( node, position );
1813 this.collapse( true );
1814 },
1815
1816 /**
1817 * Moves the range to the exact position of the specified range.
1818 *
1819 * @param {CKEDITOR.dom.range} range
1820 */
1821 moveToRange: function( range ) {
1822 this.setStart( range.startContainer, range.startOffset );
1823 this.setEnd( range.endContainer, range.endOffset );
1824 },
1825
1826 /**
1827 * Select nodes content. Range will start and end inside this node.
1828 *
1829 * @param {CKEDITOR.dom.node} node
1830 */
1831 selectNodeContents: function( node ) {
1832 this.setStart( node, 0 );
1833 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1834 },
1835
1836 /**
1837 * Sets the start position of a range.
1838 *
1839 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1840 * @param {Number} startOffset An integer greater than or equal to zero
1841 * representing the offset for the start of the range from the start
1842 * of `startNode`.
1843 */
1844 setStart: function( startNode, startOffset ) {
1845 // W3C requires a check for the new position. If it is after the end
1846 // boundary, the range should be collapsed to the new start. It seams
1847 // we will not need this check for our use of this class so we can
1848 // ignore it for now.
1849
1850 // Fixing invalid range start inside dtd empty elements.
1851 if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1852 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1853
1854 this._setStartContainer( startNode );
1855 this.startOffset = startOffset;
1856
1857 if ( !this.endContainer ) {
1858 this._setEndContainer( startNode );
1859 this.endOffset = startOffset;
1860 }
1861
1862 updateCollapsed( this );
1863 },
1864
1865 /**
1866 * Sets the end position of a Range.
1867 *
1868 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1869 * @param {Number} endOffset An integer greater than or equal to zero
1870 * representing the offset for the end of the range from the start
1871 * of `endNode`.
1872 */
1873 setEnd: function( endNode, endOffset ) {
1874 // W3C requires a check for the new position. If it is before the start
1875 // boundary, the range should be collapsed to the new end. It seams we
1876 // will not need this check for our use of this class so we can ignore
1877 // it for now.
1878
1879 // Fixing invalid range end inside dtd empty elements.
1880 if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1881 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1882
1883 this._setEndContainer( endNode );
1884 this.endOffset = endOffset;
1885
1886 if ( !this.startContainer ) {
1887 this._setStartContainer( endNode );
1888 this.startOffset = endOffset;
1889 }
1890
1891 updateCollapsed( this );
1892 },
1893
1894 /**
1895 * Sets start of this range after the specified node.
1896 *
1897 * // Range: <p>foo<b>bar</b>^</p>
1898 * range.setStartAfter( textFoo );
1899 * // The range will be changed to:
1900 * // <p>foo[<b>bar</b>]</p>
1901 *
1902 * @param {CKEDITOR.dom.node} node
1903 */
1904 setStartAfter: function( node ) {
1905 this.setStart( node.getParent(), node.getIndex() + 1 );
1906 },
1907
1908 /**
1909 * Sets start of this range after the specified node.
1910 *
1911 * // Range: <p>foo<b>bar</b>^</p>
1912 * range.setStartBefore( elB );
1913 * // The range will be changed to:
1914 * // <p>foo[<b>bar</b>]</p>
1915 *
1916 * @param {CKEDITOR.dom.node} node
1917 */
1918 setStartBefore: function( node ) {
1919 this.setStart( node.getParent(), node.getIndex() );
1920 },
1921
1922 /**
1923 * Sets end of this range after the specified node.
1924 *
1925 * // Range: <p>foo^<b>bar</b></p>
1926 * range.setEndAfter( elB );
1927 * // The range will be changed to:
1928 * // <p>foo[<b>bar</b>]</p>
1929 *
1930 * @param {CKEDITOR.dom.node} node
1931 */
1932 setEndAfter: function( node ) {
1933 this.setEnd( node.getParent(), node.getIndex() + 1 );
1934 },
1935
1936 /**
1937 * Sets end of this range before the specified node.
1938 *
1939 * // Range: <p>^foo<b>bar</b></p>
1940 * range.setStartAfter( textBar );
1941 * // The range will be changed to:
1942 * // <p>[foo<b>]bar</b></p>
1943 *
1944 * @param {CKEDITOR.dom.node} node
1945 */
1946 setEndBefore: function( node ) {
1947 this.setEnd( node.getParent(), node.getIndex() );
1948 },
1949
1950 /**
1951 * Moves the start of this range to given position according to specified node.
1952 *
1953 * // HTML: <p>Foo <b>bar</b>^</p>
1954 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
1955 * // The range will be changed to:
1956 * // <p>Foo <b>[bar</b>]</p>
1957 *
1958 * See also {@link #setEndAt} and {@link #moveToPosition}.
1959 *
1960 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1961 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1962 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1963 * {@link CKEDITOR#POSITION_AFTER_END}.
1964 */
1965 setStartAt: function( node, position ) {
1966 switch ( position ) {
1967 case CKEDITOR.POSITION_AFTER_START:
1968 this.setStart( node, 0 );
1969 break;
1970
1971 case CKEDITOR.POSITION_BEFORE_END:
1972 if ( node.type == CKEDITOR.NODE_TEXT )
1973 this.setStart( node, node.getLength() );
1974 else
1975 this.setStart( node, node.getChildCount() );
1976 break;
1977
1978 case CKEDITOR.POSITION_BEFORE_START:
1979 this.setStartBefore( node );
1980 break;
1981
1982 case CKEDITOR.POSITION_AFTER_END:
1983 this.setStartAfter( node );
1984 }
1985
1986 updateCollapsed( this );
1987 },
1988
1989 /**
1990 * Moves the end of this range to given position according to specified node.
1991 *
1992 * // HTML: <p>^Foo <b>bar</b></p>
1993 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
1994 * // The range will be changed to:
1995 * // <p>[Foo <b>]bar</b></p>
1996 *
1997 * See also {@link #setStartAt} and {@link #moveToPosition}.
1998 *
1999 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2000 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2001 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2002 * {@link CKEDITOR#POSITION_AFTER_END}.
2003 */
2004 setEndAt: function( node, position ) {
2005 switch ( position ) {
2006 case CKEDITOR.POSITION_AFTER_START:
2007 this.setEnd( node, 0 );
2008 break;
2009
2010 case CKEDITOR.POSITION_BEFORE_END:
2011 if ( node.type == CKEDITOR.NODE_TEXT )
2012 this.setEnd( node, node.getLength() );
2013 else
2014 this.setEnd( node, node.getChildCount() );
2015 break;
2016
2017 case CKEDITOR.POSITION_BEFORE_START:
2018 this.setEndBefore( node );
2019 break;
2020
2021 case CKEDITOR.POSITION_AFTER_END:
2022 this.setEndAfter( node );
2023 }
2024
2025 updateCollapsed( this );
2026 },
2027
2028 /**
2029 * Wraps inline content found around the range's start or end boundary
2030 * with a block element.
2031 *
2032 * // Assuming the following range:
2033 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2034 * // The result of executing:
2035 * range.fixBlock( true, 'p' );
2036 * // will be:
2037 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2038 *
2039 * Non-collapsed range:
2040 *
2041 * // Assuming the following range:
2042 * // ba[r<p>foo</p>bo]m
2043 * // The result of executing:
2044 * range.fixBlock( false, 'p' );
2045 * // will be:
2046 * // ba[r<p>foo</p><p>bo]m</p>
2047 *
2048 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2049 * @param {String} blockTag The name of a block element in which content will be wrapped.
2050 * For example: `'p'`.
2051 * @returns {CKEDITOR.dom.element} Created block wrapper.
2052 */
2053 fixBlock: function( isStart, blockTag ) {
2054 var bookmark = this.createBookmark(),
2055 fixedBlock = this.document.createElement( blockTag );
2056
2057 this.collapse( isStart );
2058
2059 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
2060
2061 this.extractContents().appendTo( fixedBlock );
2062 fixedBlock.trim();
2063
2064 this.insertNode( fixedBlock );
2065
2066 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2067 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2068 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2069 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2070 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2071 // is placed before the bookmark.
2072 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2073 // We remove incorrectly placed one and add a brand new one. (#13001)
2074 var bogus = fixedBlock.getBogus();
2075 if ( bogus ) {
2076 bogus.remove();
2077 }
2078 fixedBlock.appendBogus();
2079
2080 this.moveToBookmark( bookmark );
2081
2082 return fixedBlock;
2083 },
2084
2085 /**
2086 * @todo
2087 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2088 */
2089 splitBlock: function( blockTag, cloneId ) {
2090 var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
2091 endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2092
2093 var startBlockLimit = startPath.blockLimit,
2094 endBlockLimit = endPath.blockLimit;
2095
2096 var startBlock = startPath.block,
2097 endBlock = endPath.block;
2098
2099 var elementPath = null;
2100 // Do nothing if the boundaries are in different block limits.
2101 if ( !startBlockLimit.equals( endBlockLimit ) )
2102 return null;
2103
2104 // Get or fix current blocks.
2105 if ( blockTag != 'br' ) {
2106 if ( !startBlock ) {
2107 startBlock = this.fixBlock( true, blockTag );
2108 endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
2109 }
2110
2111 if ( !endBlock )
2112 endBlock = this.fixBlock( false, blockTag );
2113 }
2114
2115 // Get the range position.
2116 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
2117 isEndOfBlock = endBlock && this.checkEndOfBlock();
2118
2119 // Delete the current contents.
2120 // TODO: Why is 2.x doing CheckIsEmpty()?
2121 this.deleteContents();
2122
2123 if ( startBlock && startBlock.equals( endBlock ) ) {
2124 if ( isEndOfBlock ) {
2125 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2126 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
2127 endBlock = null;
2128 } else if ( isStartOfBlock ) {
2129 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2130 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
2131 startBlock = null;
2132 } else {
2133 endBlock = this.splitElement( startBlock, cloneId || false );
2134
2135 // In Gecko, the last child node must be a bogus <br>.
2136 // Note: bogus <br> added under <ul> or <ol> would cause
2137 // lists to be incorrectly rendered.
2138 if ( !startBlock.is( 'ul', 'ol' ) )
2139 startBlock.appendBogus();
2140 }
2141 }
2142
2143 return {
2144 previousBlock: startBlock,
2145 nextBlock: endBlock,
2146 wasStartOfBlock: isStartOfBlock,
2147 wasEndOfBlock: isEndOfBlock,
2148 elementPath: elementPath
2149 };
2150 },
2151
2152 /**
2153 * Branch the specified element from the collapsed range position and
2154 * place the caret between the two result branches.
2155 *
2156 * **Note:** The range must be collapsed and been enclosed by this element.
2157 *
2158 * @param {CKEDITOR.dom.element} element
2159 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2160 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2161 */
2162 splitElement: function( toSplit, cloneId ) {
2163 if ( !this.collapsed )
2164 return null;
2165
2166 // Extract the contents of the block from the selection point to the end
2167 // of its contents.
2168 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
2169 var documentFragment = this.extractContents( false, cloneId || false );
2170
2171 // Duplicate the element after it.
2172 var clone = toSplit.clone( false, cloneId || false );
2173
2174 // Place the extracted contents into the duplicated element.
2175 documentFragment.appendTo( clone );
2176 clone.insertAfter( toSplit );
2177 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
2178 return clone;
2179 },
2180
2181 /**
2182 * Recursively remove any empty path blocks at the range boundary.
2183 *
2184 * @method
2185 * @param {Boolean} atEnd Removal to perform at the end boundary,
2186 * otherwise to perform at the start.
2187 */
2188 removeEmptyBlocksAtEnd: ( function() {
2189
2190 var whitespace = CKEDITOR.dom.walker.whitespaces(),
2191 bookmark = CKEDITOR.dom.walker.bookmark( false );
2192
2193 function childEval( parent ) {
2194 return function( node ) {
2195 // Whitespace, bookmarks, empty inlines.
2196 if ( whitespace( node ) || bookmark( node ) ||
2197 node.type == CKEDITOR.NODE_ELEMENT &&
2198 node.isEmptyInlineRemoveable() ) {
2199 return false;
2200 } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) {
2201 return false;
2202 }
2203
2204 return true;
2205 };
2206 }
2207
2208 return function( atEnd ) {
2209
2210 var bm = this.createBookmark();
2211 var path = this[ atEnd ? 'endPath' : 'startPath' ]();
2212 var block = path.block || path.blockLimit, parent;
2213
2214 // Remove any childless block, including list and table.
2215 while ( block && !block.equals( path.root ) &&
2216 !block.getFirst( childEval( block ) ) ) {
2217 parent = block.getParent();
2218 this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
2219 block.remove( 1 );
2220 block = parent;
2221 }
2222
2223 this.moveToBookmark( bm );
2224 };
2225
2226 } )(),
2227
2228 /**
2229 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2230 *
2231 * @returns {CKEDITOR.dom.elementPath}
2232 */
2233 startPath: function() {
2234 return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2235 },
2236
2237 /**
2238 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2239 *
2240 * @returns {CKEDITOR.dom.elementPath}
2241 */
2242 endPath: function() {
2243 return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2244 },
2245
2246 /**
2247 * Check whether a range boundary is at the inner boundary of a given
2248 * element.
2249 *
2250 * @param {CKEDITOR.dom.element} element The target element to check.
2251 * @param {Number} checkType The boundary to check for both the range
2252 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2253 * @returns {Boolean} `true` if the range boundary is at the inner
2254 * boundary of the element.
2255 */
2256 checkBoundaryOfElement: function( element, checkType ) {
2257 var checkStart = ( checkType == CKEDITOR.START );
2258
2259 // Create a copy of this range, so we can manipulate it for our checks.
2260 var walkerRange = this.clone();
2261
2262 // Collapse the range at the proper size.
2263 walkerRange.collapse( checkStart );
2264
2265 // Expand the range to element boundary.
2266 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2267
2268 // Create the walker, which will check if we have anything useful
2269 // in the range.
2270 var walker = new CKEDITOR.dom.walker( walkerRange );
2271 walker.evaluator = elementBoundaryEval( checkStart );
2272
2273 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
2274 },
2275
2276 /**
2277 * **Note:** Calls to this function may produce changes to the DOM. The range may
2278 * be updated to reflect such changes.
2279 *
2280 * @returns {Boolean}
2281 * @todo
2282 */
2283 checkStartOfBlock: function() {
2284 var startContainer = this.startContainer,
2285 startOffset = this.startOffset;
2286
2287 // [IE] Special handling for range start in text with a leading NBSP,
2288 // we it to be isolated, for bogus check.
2289 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) {
2290 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
2291 if ( nbspRegExp.test( textBefore ) )
2292 this.trim( 0, 1 );
2293 }
2294
2295 // Antecipate the trim() call here, so the walker will not make
2296 // changes to the DOM, which would not get reflected into this
2297 // range otherwise.
2298 this.trim();
2299
2300 // We need to grab the block element holding the start boundary, so
2301 // let's use an element path for it.
2302 var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2303
2304 // Creates a range starting at the block start until the range start.
2305 var walkerRange = this.clone();
2306 walkerRange.collapse( true );
2307 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
2308
2309 var walker = new CKEDITOR.dom.walker( walkerRange );
2310 walker.evaluator = getCheckStartEndBlockEvalFunction();
2311
2312 return walker.checkBackward();
2313 },
2314
2315 /**
2316 * **Note:** Calls to this function may produce changes to the DOM. The range may
2317 * be updated to reflect such changes.
2318 *
2319 * @returns {Boolean}
2320 * @todo
2321 */
2322 checkEndOfBlock: function() {
2323 var endContainer = this.endContainer,
2324 endOffset = this.endOffset;
2325
2326 // [IE] Special handling for range end in text with a following NBSP,
2327 // we it to be isolated, for bogus check.
2328 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) {
2329 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
2330 if ( nbspRegExp.test( textAfter ) )
2331 this.trim( 1, 0 );
2332 }
2333
2334 // Antecipate the trim() call here, so the walker will not make
2335 // changes to the DOM, which would not get reflected into this
2336 // range otherwise.
2337 this.trim();
2338
2339 // We need to grab the block element holding the start boundary, so
2340 // let's use an element path for it.
2341 var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2342
2343 // Creates a range starting at the block start until the range start.
2344 var walkerRange = this.clone();
2345 walkerRange.collapse( false );
2346 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
2347
2348 var walker = new CKEDITOR.dom.walker( walkerRange );
2349 walker.evaluator = getCheckStartEndBlockEvalFunction();
2350
2351 return walker.checkForward();
2352 },
2353
2354 /**
2355 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2356 *
2357 * @param {Function} evaluator Function used as the walker's evaluator.
2358 * @param {Function} [guard] Function used as the walker's guard.
2359 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2360 * default to the root editable if not defined.
2361 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2362 */
2363 getPreviousNode: function( evaluator, guard, boundary ) {
2364 var walkerRange = this.clone();
2365 walkerRange.collapse( 1 );
2366 walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
2367
2368 var walker = new CKEDITOR.dom.walker( walkerRange );
2369 walker.evaluator = evaluator;
2370 walker.guard = guard;
2371 return walker.previous();
2372 },
2373
2374 /**
2375 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2376 *
2377 * @param {Function} evaluator Function used as the walker's evaluator.
2378 * @param {Function} [guard] Function used as the walker's guard.
2379 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2380 * default to the root editable if not defined.
2381 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2382 */
2383 getNextNode: function( evaluator, guard, boundary ) {
2384 var walkerRange = this.clone();
2385 walkerRange.collapse();
2386 walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
2387
2388 var walker = new CKEDITOR.dom.walker( walkerRange );
2389 walker.evaluator = evaluator;
2390 walker.guard = guard;
2391 return walker.next();
2392 },
2393
2394 /**
2395 * Check if elements at which the range boundaries anchor are read-only,
2396 * with respect to `contenteditable` attribute.
2397 *
2398 * @returns {Boolean}
2399 */
2400 checkReadOnly: ( function() {
2401 function checkNodesEditable( node, anotherEnd ) {
2402 while ( node ) {
2403 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2404 if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
2405 return 0;
2406
2407 // Range enclosed entirely in an editable element.
2408 else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
2409 break;
2410
2411 }
2412 node = node.getParent();
2413 }
2414
2415 return 1;
2416 }
2417
2418 return function() {
2419 var startNode = this.startContainer,
2420 endNode = this.endContainer;
2421
2422 // Check if elements path at both boundaries are editable.
2423 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
2424 };
2425 } )(),
2426
2427 /**
2428 * Moves the range boundaries to the first/end editing point inside an
2429 * element.
2430 *
2431 * For example, in an element tree like
2432 * `<p><b><i></i></b> Text</p>`, the start editing point is
2433 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2434 *
2435 * @param {CKEDITOR.dom.element} el The element into which look for the
2436 * editing spot.
2437 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2438 * @returns {Boolean} Whether range was moved.
2439 */
2440 moveToElementEditablePosition: function( el, isMoveToEnd ) {
2441
2442 function nextDFS( node, childOnly ) {
2443 var next;
2444
2445 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
2446 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
2447
2448 if ( !childOnly && !next )
2449 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
2450
2451 return next;
2452 }
2453
2454 // Handle non-editable element e.g. HR.
2455 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
2456 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2457 return true;
2458 }
2459
2460 var found = 0;
2461
2462 while ( el ) {
2463 // Stop immediately if we've found a text node.
2464 if ( el.type == CKEDITOR.NODE_TEXT ) {
2465 // Put cursor before block filler.
2466 if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
2467 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2468 else
2469 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2470 found = 1;
2471 break;
2472 }
2473
2474 // If an editable element is found, move inside it, but not stop the searching.
2475 if ( el.type == CKEDITOR.NODE_ELEMENT ) {
2476 if ( el.isEditable() ) {
2477 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
2478 found = 1;
2479 }
2480 // Put cursor before padding block br.
2481 else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
2482 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2483 // Special case - non-editable block. Select entire element, because it does not make sense
2484 // to place collapsed selection next to it, because browsers can't handle that.
2485 else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
2486 this.setStartBefore( el );
2487 this.setEndAfter( el );
2488 return true;
2489 }
2490 }
2491
2492 el = nextDFS( el, found );
2493 }
2494
2495 return !!found;
2496 },
2497
2498 /**
2499 * Moves the range boundaries to the closest editing point after/before an
2500 * element or the current range position (depends on whether the element was specified).
2501 *
2502 * For example, if the start element has `id="start"`,
2503 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2504 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2505 *
2506 * See also: {@link #moveToElementEditablePosition}.
2507 *
2508 * @since 4.3
2509 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2510 * position will be used.
2511 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2512 * @returns {Boolean} Whether the range was moved.
2513 */
2514 moveToClosestEditablePosition: function( element, isMoveForward ) {
2515 // We don't want to modify original range if there's no editable position.
2516 var range,
2517 found = 0,
2518 sibling,
2519 isElement,
2520 positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
2521
2522 if ( element ) {
2523 // Set collapsed range at one of ends of element.
2524 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2525 range = new CKEDITOR.dom.range( this.root );
2526 range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] );
2527 } else {
2528 range = this.clone();
2529 }
2530
2531 // Start element isn't a block, so we can automatically place range
2532 // next to it.
2533 if ( element && !element.is( CKEDITOR.dtd.$block ) )
2534 found = 1;
2535 else {
2536 // Look for first node that fulfills eval function and place range next to it.
2537 sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2538 if ( sibling ) {
2539 found = 1;
2540 isElement = sibling.type == CKEDITOR.NODE_ELEMENT;
2541
2542 // Special case - eval accepts block element only if it's a non-editable block,
2543 // which we want to select, not place collapsed selection next to it (which browsers
2544 // can't handle).
2545 if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
2546 range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
2547 range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
2548 }
2549 // Handle empty blocks which can be selection containers on old IEs.
2550 else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) {
2551 range.setEnd( sibling, 0 );
2552 range.collapse();
2553 } else {
2554 range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] );
2555 }
2556 }
2557 }
2558
2559 if ( found )
2560 this.moveToRange( range );
2561
2562 return !!found;
2563 },
2564
2565 /**
2566 * See {@link #moveToElementEditablePosition}.
2567 *
2568 * @returns {Boolean} Whether range was moved.
2569 */
2570 moveToElementEditStart: function( target ) {
2571 return this.moveToElementEditablePosition( target );
2572 },
2573
2574 /**
2575 * See {@link #moveToElementEditablePosition}.
2576 *
2577 * @returns {Boolean} Whether range was moved.
2578 */
2579 moveToElementEditEnd: function( target ) {
2580 return this.moveToElementEditablePosition( target, true );
2581 },
2582
2583 /**
2584 * Get the single node enclosed within the range if there's one.
2585 *
2586 * @returns {CKEDITOR.dom.node}
2587 */
2588 getEnclosedNode: function() {
2589 var walkerRange = this.clone();
2590
2591 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
2592 walkerRange.optimize();
2593 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2594 return null;
2595
2596 var walker = new CKEDITOR.dom.walker( walkerRange ),
2597 isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
2598 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
2599
2600 walker.evaluator = function( node ) {
2601 return isNotWhitespaces( node ) && isNotBookmarks( node );
2602 };
2603 var node = walker.next();
2604 walker.reset();
2605 return node && node.equals( walker.previous() ) ? node : null;
2606 },
2607
2608 /**
2609 * Get the node adjacent to the range start or {@link #startContainer}.
2610 *
2611 * @returns {CKEDITOR.dom.node}
2612 */
2613 getTouchedStartNode: function() {
2614 var container = this.startContainer;
2615
2616 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2617 return container;
2618
2619 return container.getChild( this.startOffset ) || container;
2620 },
2621
2622 /**
2623 * Get the node adjacent to the range end or {@link #endContainer}.
2624 *
2625 * @returns {CKEDITOR.dom.node}
2626 */
2627 getTouchedEndNode: function() {
2628 var container = this.endContainer;
2629
2630 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2631 return container;
2632
2633 return container.getChild( this.endOffset - 1 ) || container;
2634 },
2635
2636 /**
2637 * Gets next node which can be a container of a selection.
2638 * This methods mimics a behavior of right/left arrow keys in case of
2639 * collapsed selection. It does not return an exact position (with offset) though,
2640 * but just a selection's container.
2641 *
2642 * Note: use this method on a collapsed range.
2643 *
2644 * @since 4.3
2645 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2646 */
2647 getNextEditableNode: getNextEditableNode(),
2648
2649 /**
2650 * See {@link #getNextEditableNode}.
2651 *
2652 * @since 4.3
2653 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2654 */
2655 getPreviousEditableNode: getNextEditableNode( 1 ),
2656
2657 /**
2658 * Scrolls the start of current range into view.
2659 */
2660 scrollIntoView: function() {
2661
2662 // The reference element contains a zero-width space to avoid
2663 // a premature removal. The view is to be scrolled with respect
2664 // to this element.
2665 var reference = new CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', this.document ),
2666 afterCaretNode, startContainerText, isStartText;
2667
2668 var range = this.clone();
2669
2670 // Work with the range to obtain a proper caret position.
2671 range.optimize();
2672
2673 // Currently in a text node, so we need to split it into two
2674 // halves and put the reference between.
2675 if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
2676 // Keep the original content. It will be restored.
2677 startContainerText = range.startContainer.getText();
2678
2679 // Split the startContainer at the this position.
2680 afterCaretNode = range.startContainer.split( range.startOffset );
2681
2682 // Insert the reference between two text nodes.
2683 reference.insertAfter( range.startContainer );
2684 }
2685
2686 // If not in a text node, simply insert the reference into the range.
2687 else {
2688 range.insertNode( reference );
2689 }
2690
2691 // Scroll with respect to the reference element.
2692 reference.scrollIntoView();
2693
2694 // Get rid of split parts if "in a text node" case.
2695 // Revert the original text of the startContainer.
2696 if ( isStartText ) {
2697 range.startContainer.setText( startContainerText );
2698 afterCaretNode.remove();
2699 }
2700
2701 // Get rid of the reference node. It is no longer necessary.
2702 reference.remove();
2703 },
2704
2705 /**
2706 * Setter for the {@link #startContainer}.
2707 *
2708 * @since 4.4.6
2709 * @private
2710 * @param {CKEDITOR.dom.element} startContainer
2711 */
2712 _setStartContainer: function( startContainer ) {
2713 // %REMOVE_START%
2714 var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer );
2715
2716 if ( !isRootAscendantOrSelf ) {
2717 CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } );
2718 }
2719 // %REMOVE_END%
2720 this.startContainer = startContainer;
2721 },
2722
2723 /**
2724 * Setter for the {@link #endContainer}.
2725 *
2726 * @since 4.4.6
2727 * @private
2728 * @param {CKEDITOR.dom.element} endContainer
2729 */
2730 _setEndContainer: function( endContainer ) {
2731 // %REMOVE_START%
2732 var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer );
2733
2734 if ( !isRootAscendantOrSelf ) {
2735 CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } );
2736 }
2737 // %REMOVE_END%
2738 this.endContainer = endContainer;
2739 }
2740 };
2741
2742
2743} )();
2744
2745/**
2746 * Indicates a position after start of a node.
2747 *
2748 * // When used according to an element:
2749 * // <element>^contents</element>
2750 *
2751 * // When used according to a text node:
2752 * // "^text" (range is anchored in the text node)
2753 *
2754 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2755 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2756 *
2757 * @readonly
2758 * @member CKEDITOR
2759 * @property {Number} [=1]
2760 */
2761CKEDITOR.POSITION_AFTER_START = 1;
2762
2763/**
2764 * Indicates a position before end of a node.
2765 *
2766 * // When used according to an element:
2767 * // <element>contents^</element>
2768 *
2769 * // When used according to a text node:
2770 * // "text^" (range is anchored in the text node)
2771 *
2772 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2773 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2774 *
2775 * @readonly
2776 * @member CKEDITOR
2777 * @property {Number} [=2]
2778 */
2779CKEDITOR.POSITION_BEFORE_END = 2;
2780
2781/**
2782 * Indicates a position before start of a node.
2783 *
2784 * // When used according to an element:
2785 * // ^<element>contents</element> (range is anchored in element's parent)
2786 *
2787 * // When used according to a text node:
2788 * // ^"text" (range is anchored in text node's parent)
2789 *
2790 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2791 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2792 *
2793 * @readonly
2794 * @member CKEDITOR
2795 * @property {Number} [=3]
2796 */
2797CKEDITOR.POSITION_BEFORE_START = 3;
2798
2799/**
2800 * Indicates a position after end of a node.
2801 *
2802 * // When used according to an element:
2803 * // <element>contents</element>^ (range is anchored in element's parent)
2804 *
2805 * // When used according to a text node:
2806 * // "text"^ (range is anchored in text node's parent)
2807 *
2808 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2809 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2810 *
2811 * @readonly
2812 * @member CKEDITOR
2813 * @property {Number} [=4]
2814 */
2815CKEDITOR.POSITION_AFTER_END = 4;
2816
2817CKEDITOR.ENLARGE_ELEMENT = 1;
2818CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2819CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2820CKEDITOR.ENLARGE_INLINE = 4;
2821
2822// Check boundary types.
2823
2824/**
2825 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2826 *
2827 * @readonly
2828 * @member CKEDITOR
2829 * @property {Number} [=1]
2830 */
2831CKEDITOR.START = 1;
2832
2833/**
2834 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2835 *
2836 * @readonly
2837 * @member CKEDITOR
2838 * @property {Number} [=2]
2839 */
2840CKEDITOR.END = 2;
2841
2842// Shrink range types.
2843
2844/**
2845 * See {@link CKEDITOR.dom.range#shrink}.
2846 *
2847 * @readonly
2848 * @member CKEDITOR
2849 * @property {Number} [=1]
2850 */
2851CKEDITOR.SHRINK_ELEMENT = 1;
2852
2853/**
2854 * See {@link CKEDITOR.dom.range#shrink}.
2855 *
2856 * @readonly
2857 * @member CKEDITOR
2858 * @property {Number} [=2]
2859 */
2860CKEDITOR.SHRINK_TEXT = 2;