]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/core/dom/range.js
Validation initiale
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / core / dom / range.js
1 /**
2 * @license Copyright (c) 2003-2017, 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 */
61 CKEDITOR.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.getText().replace( CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE, '' ).length;
808
809 return sum;
810 }
811
812 function normalizeTextNodes( 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 one node 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, safe node.
826 // (safe == one for which getIndex(true) does not return -1, so one which won't disappear).
827 if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 0 ) {
828 offset = getPrecedingSafeNodeIndex( container, offset ) + 1;
829 }
830
831 // The last step - fix the offset inside text node by adding
832 // lengths of preceding text nodes which will be merged with container.
833 if ( container.type == CKEDITOR.NODE_TEXT ) {
834 var precedingLength = getLengthOfPrecedingTextNodes( container );
835
836 // Normal case - text node is not empty.
837 if ( container.getText() ) {
838 offset += precedingLength;
839
840 // Awful case - the text node is empty and thus will be totally lost.
841 // In this case we are trying to normalize the limit to the left:
842 // * either to the preceding text node,
843 // * or to the "gap" after the preceding element.
844 } else {
845 // Find the closest non-text sibling.
846 var precedingContainer = container.getPrevious( isNotText );
847
848 // If there are any characters on the left, that means that we can anchor
849 // there, because this text node will not be lost.
850 if ( precedingLength ) {
851 offset = precedingLength;
852
853 if ( precedingContainer ) {
854 // The text node is the first node after the closest non-text sibling.
855 container = precedingContainer.getNext();
856 } else {
857 // But if there was no non-text sibling, then the text node is the first child.
858 container = container.getParent().getFirst();
859 }
860
861 // If there are no characters on the left, then anchor after the previous non-text node.
862 // E.g. (see tests for a legend :D):
863 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
864 } else {
865 container = container.getParent();
866 offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0;
867 }
868 }
869 }
870
871 limit.container = container;
872 limit.offset = offset;
873 }
874
875 function normalizeFCSeq( limit, root ) {
876 var fcseq = root.getCustomData( 'cke-fillingChar' );
877
878 if ( !fcseq ) {
879 return;
880 }
881
882 var container = limit.container;
883
884 if ( fcseq.equals( container ) ) {
885 limit.offset -= CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE.length;
886
887 // == 0 handles case when limit was at the end of FCS.
888 // < 0 handles all cases where limit was somewhere in the middle or at the beginning.
889 // > 0 (the "else" case) means cases where there are some more characters in the FCS node (FCSabc^def).
890 if ( limit.offset <= 0 ) {
891 limit.offset = container.getIndex();
892 limit.container = container.getParent();
893 }
894 return;
895 }
896
897 // And here goes the funny part - all other cases are handled inside node.getAddress() and getIndex() thanks to
898 // node.getIndex() being aware of FCS (handling it as an empty node).
899 }
900
901 // Finds a normalized index of a safe node preceding this one.
902 // Safe == one that will not disappear, so one for which getIndex( true ) does not return -1.
903 // Return -1 if there's no safe preceding node.
904 function getPrecedingSafeNodeIndex( container, offset ) {
905 var index;
906
907 while ( offset-- ) {
908 index = container.getChild( offset ).getIndex( true );
909
910 if ( index >= 0 )
911 return index;
912 }
913
914 return -1;
915 }
916
917 return function( normalized ) {
918 var collapsed = this.collapsed,
919 bmStart = {
920 container: this.startContainer,
921 offset: this.startOffset
922 },
923 bmEnd = {
924 container: this.endContainer,
925 offset: this.endOffset
926 };
927
928 if ( normalized ) {
929 normalizeTextNodes( bmStart );
930 normalizeFCSeq( bmStart, this.root );
931
932 if ( !collapsed ) {
933 normalizeTextNodes( bmEnd );
934 normalizeFCSeq( bmEnd, this.root );
935 }
936 }
937
938 return {
939 start: bmStart.container.getAddress( normalized ),
940 end: collapsed ? null : bmEnd.container.getAddress( normalized ),
941 startOffset: bmStart.offset,
942 endOffset: bmEnd.offset,
943 normalized: normalized,
944 collapsed: collapsed,
945 is2: true // It's a createBookmark2 bookmark.
946 };
947 };
948 } )(),
949
950 /**
951 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
952 *
953 * If serializable bookmark passed, then its `<span>` markers will be removed.
954 *
955 * @param {Object} bookmark
956 */
957 moveToBookmark: function( bookmark ) {
958 // Created with createBookmark2().
959 if ( bookmark.is2 ) {
960 // Get the start information.
961 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
962 startOffset = bookmark.startOffset;
963
964 // Get the end information.
965 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
966 endOffset = bookmark.endOffset;
967
968 // Set the start boundary.
969 this.setStart( startContainer, startOffset );
970
971 // Set the end boundary. If not available, collapse it.
972 if ( endContainer )
973 this.setEnd( endContainer, endOffset );
974 else
975 this.collapse( true );
976 }
977 // Created with createBookmark().
978 else {
979 var serializable = bookmark.serializable,
980 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
981 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
982
983 // Set the range start at the bookmark start node position.
984 this.setStartBefore( startNode );
985
986 // Remove it, because it may interfere in the setEndBefore call.
987 startNode.remove();
988
989 // Set the range end at the bookmark end node position, or simply
990 // collapse it if it is not available.
991 if ( endNode ) {
992 this.setEndBefore( endNode );
993 endNode.remove();
994 } else {
995 this.collapse( true );
996 }
997 }
998 },
999
1000 /**
1001 * Returns two nodes which are on the boundaries of this range.
1002 *
1003 * @returns {Object}
1004 * @returns {CKEDITOR.dom.node} return.startNode
1005 * @returns {CKEDITOR.dom.node} return.endNode
1006 * @todo precise desc/algorithm
1007 */
1008 getBoundaryNodes: function() {
1009 var startNode = this.startContainer,
1010 endNode = this.endContainer,
1011 startOffset = this.startOffset,
1012 endOffset = this.endOffset,
1013 childCount;
1014
1015 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
1016 childCount = startNode.getChildCount();
1017 if ( childCount > startOffset ) {
1018 startNode = startNode.getChild( startOffset );
1019 } else if ( childCount < 1 ) {
1020 startNode = startNode.getPreviousSourceNode();
1021 }
1022 // startOffset > childCount but childCount is not 0
1023 else {
1024 // Try to take the node just after the current position.
1025 startNode = startNode.$;
1026 while ( startNode.lastChild )
1027 startNode = startNode.lastChild;
1028 startNode = new CKEDITOR.dom.node( startNode );
1029
1030 // Normally we should take the next node in DFS order. But it
1031 // is also possible that we've already reached the end of
1032 // document.
1033 startNode = startNode.getNextSourceNode() || startNode;
1034 }
1035 }
1036
1037 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
1038 childCount = endNode.getChildCount();
1039 if ( childCount > endOffset ) {
1040 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
1041 } else if ( childCount < 1 ) {
1042 endNode = endNode.getPreviousSourceNode();
1043 }
1044 // endOffset > childCount but childCount is not 0.
1045 else {
1046 // Try to take the node just before the current position.
1047 endNode = endNode.$;
1048 while ( endNode.lastChild )
1049 endNode = endNode.lastChild;
1050 endNode = new CKEDITOR.dom.node( endNode );
1051 }
1052 }
1053
1054 // Sometimes the endNode will come right before startNode for collapsed
1055 // ranges. Fix it. (#3780)
1056 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1057 startNode = endNode;
1058
1059 return { startNode: startNode, endNode: endNode };
1060 },
1061
1062 /**
1063 * Find the node which fully contains the range.
1064 *
1065 * @param {Boolean} [includeSelf=false]
1066 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1067 * @returns {CKEDITOR.dom.element}
1068 */
1069 getCommonAncestor: function( includeSelf, ignoreTextNode ) {
1070 var start = this.startContainer,
1071 end = this.endContainer,
1072 ancestor;
1073
1074 if ( start.equals( end ) ) {
1075 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
1076 ancestor = start.getChild( this.startOffset );
1077 else
1078 ancestor = start;
1079 } else {
1080 ancestor = start.getCommonAncestor( end );
1081 }
1082
1083 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
1084 },
1085
1086 /**
1087 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1088 * nodes to element nodes, whenever possible. This is actually possible
1089 * if either of the boundary containers point to a text node, and its
1090 * offset is set to zero, or after the last char in the node.
1091 */
1092 optimize: function() {
1093 var container = this.startContainer;
1094 var offset = this.startOffset;
1095
1096 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1097 if ( !offset )
1098 this.setStartBefore( container );
1099 else if ( offset >= container.getLength() )
1100 this.setStartAfter( container );
1101 }
1102
1103 container = this.endContainer;
1104 offset = this.endOffset;
1105
1106 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1107 if ( !offset )
1108 this.setEndBefore( container );
1109 else if ( offset >= container.getLength() )
1110 this.setEndAfter( container );
1111 }
1112 },
1113
1114 /**
1115 * Move the range out of bookmark nodes if they'd been the container.
1116 */
1117 optimizeBookmark: function() {
1118 var startNode = this.startContainer,
1119 endNode = this.endContainer;
1120
1121 if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
1122 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
1123 if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
1124 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
1125 },
1126
1127 /**
1128 * @param {Boolean} [ignoreStart=false]
1129 * @param {Boolean} [ignoreEnd=false]
1130 * @todo precise desc/algorithm
1131 */
1132 trim: function( ignoreStart, ignoreEnd ) {
1133 var startContainer = this.startContainer,
1134 startOffset = this.startOffset,
1135 collapsed = this.collapsed;
1136 if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1137 // If the offset is zero, we just insert the new node before
1138 // the start.
1139 if ( !startOffset ) {
1140 startOffset = startContainer.getIndex();
1141 startContainer = startContainer.getParent();
1142 }
1143 // If the offset is at the end, we'll insert it after the text
1144 // node.
1145 else if ( startOffset >= startContainer.getLength() ) {
1146 startOffset = startContainer.getIndex() + 1;
1147 startContainer = startContainer.getParent();
1148 }
1149 // In other case, we split the text node and insert the new
1150 // node at the split point.
1151 else {
1152 var nextText = startContainer.split( startOffset );
1153
1154 startOffset = startContainer.getIndex() + 1;
1155 startContainer = startContainer.getParent();
1156
1157 // Check all necessity of updating the end boundary.
1158 if ( this.startContainer.equals( this.endContainer ) )
1159 this.setEnd( nextText, this.endOffset - this.startOffset );
1160 else if ( startContainer.equals( this.endContainer ) )
1161 this.endOffset += 1;
1162 }
1163
1164 this.setStart( startContainer, startOffset );
1165
1166 if ( collapsed ) {
1167 this.collapse( true );
1168 return;
1169 }
1170 }
1171
1172 var endContainer = this.endContainer;
1173 var endOffset = this.endOffset;
1174
1175 if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1176 // If the offset is zero, we just insert the new node before
1177 // the start.
1178 if ( !endOffset ) {
1179 endOffset = endContainer.getIndex();
1180 endContainer = endContainer.getParent();
1181 }
1182 // If the offset is at the end, we'll insert it after the text
1183 // node.
1184 else if ( endOffset >= endContainer.getLength() ) {
1185 endOffset = endContainer.getIndex() + 1;
1186 endContainer = endContainer.getParent();
1187 }
1188 // In other case, we split the text node and insert the new
1189 // node at the split point.
1190 else {
1191 endContainer.split( endOffset );
1192
1193 endOffset = endContainer.getIndex() + 1;
1194 endContainer = endContainer.getParent();
1195 }
1196
1197 this.setEnd( endContainer, endOffset );
1198 }
1199 },
1200
1201 /**
1202 * Expands the range so that partial units are completely contained.
1203 *
1204 * @param {Number} unit The unit type to expand with. Use one of following values: {@link CKEDITOR#ENLARGE_BLOCK_CONTENTS},
1205 * {@link CKEDITOR#ENLARGE_ELEMENT}, {@link CKEDITOR#ENLARGE_INLINE}, {@link CKEDITOR#ENLARGE_LIST_ITEM_CONTENTS}.
1206 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1207 */
1208 enlarge: function( unit, excludeBrs ) {
1209 var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
1210
1211 switch ( unit ) {
1212 case CKEDITOR.ENLARGE_INLINE:
1213 var enlargeInlineOnly = 1;
1214
1215 /* falls through */
1216 case CKEDITOR.ENLARGE_ELEMENT:
1217
1218 if ( this.collapsed )
1219 return;
1220
1221 // Get the common ancestor.
1222 var commonAncestor = this.getCommonAncestor();
1223
1224 var boundary = this.root;
1225
1226 // For each boundary
1227 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1228 // if not available, to be enlarge.
1229 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1230 // common ancestor is not reached. After reaching the common ancestor, just save the
1231 // enlargeable node to be used later.
1232
1233 var startTop, endTop;
1234
1235 var enlargeable, sibling, commonReached;
1236
1237 // Indicates that the node can be added only if whitespace
1238 // is available before it.
1239 var needsWhiteSpace = false;
1240 var isWhiteSpace;
1241 var siblingText;
1242
1243 // Process the start boundary.
1244
1245 var container = this.startContainer;
1246 var offset = this.startOffset;
1247
1248 if ( container.type == CKEDITOR.NODE_TEXT ) {
1249 if ( offset ) {
1250 // Check if there is any non-space text before the
1251 // offset. Otherwise, container is null.
1252 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
1253
1254 // If we found only whitespace in the node, it
1255 // means that we'll need more whitespace to be able
1256 // to expand. For example, <i> can be expanded in
1257 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1258 needsWhiteSpace = !!container;
1259 }
1260
1261 if ( container ) {
1262 if ( !( sibling = container.getPrevious() ) )
1263 enlargeable = container.getParent();
1264 }
1265 } else {
1266 // If we have offset, get the node preceeding it as the
1267 // first sibling to be checked.
1268 if ( offset )
1269 sibling = container.getChild( offset - 1 ) || container.getLast();
1270
1271 // If there is no sibling, mark the container to be
1272 // enlarged.
1273 if ( !sibling )
1274 enlargeable = container;
1275 }
1276
1277 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1278 enlargeable = getValidEnlargeable( enlargeable );
1279
1280 while ( enlargeable || sibling ) {
1281 if ( enlargeable && !sibling ) {
1282 // If we reached the common ancestor, mark the flag
1283 // for it.
1284 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1285 commonReached = true;
1286
1287 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1288 break;
1289
1290 // If we don't need space or this element breaks
1291 // the line, then enlarge it.
1292 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1293 needsWhiteSpace = false;
1294
1295 // If the common ancestor has been reached,
1296 // we'll not enlarge it immediately, but just
1297 // mark it to be enlarged later if the end
1298 // boundary also enlarges it.
1299 if ( commonReached )
1300 startTop = enlargeable;
1301 else
1302 this.setStartBefore( enlargeable );
1303 }
1304
1305 sibling = enlargeable.getPrevious();
1306 }
1307
1308 // Check all sibling nodes preceeding the enlargeable
1309 // node. The node wil lbe enlarged only if none of them
1310 // blocks it.
1311 while ( sibling ) {
1312 // This flag indicates that this node has
1313 // whitespaces at the end.
1314 isWhiteSpace = false;
1315
1316 if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
1317 sibling = sibling.getPrevious();
1318 continue;
1319 } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1320 siblingText = sibling.getText();
1321
1322 if ( leadingWhitespaceRegex.test( siblingText ) )
1323 sibling = null;
1324
1325 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1326 } else {
1327 // #12221 (Chrome) plus #11111 (Safari).
1328 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1329
1330 // If this is a visible element.
1331 // We need to check for the bookmark attribute because IE insists on
1332 // rendering the display:none nodes we use for bookmarks. (#3363)
1333 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1334 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1335 // We'll accept it only if we need
1336 // whitespace, and this is an inline
1337 // element with whitespace only.
1338 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1339 // It must contains spaces and inline elements only.
1340
1341 siblingText = sibling.getText();
1342
1343 if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1344 sibling = null;
1345 else {
1346 var allChildren = sibling.$.getElementsByTagName( '*' );
1347 for ( var i = 0, child; child = allChildren[ i++ ]; ) {
1348 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1349 sibling = null;
1350 break;
1351 }
1352 }
1353 }
1354
1355 if ( sibling )
1356 isWhiteSpace = !!siblingText.length;
1357 } else {
1358 sibling = null;
1359 }
1360 }
1361 }
1362
1363 // A node with whitespaces has been found.
1364 if ( isWhiteSpace ) {
1365 // Enlarge the last enlargeable node, if we
1366 // were waiting for spaces.
1367 if ( needsWhiteSpace ) {
1368 if ( commonReached )
1369 startTop = enlargeable;
1370 else if ( enlargeable )
1371 this.setStartBefore( enlargeable );
1372 } else {
1373 needsWhiteSpace = true;
1374 }
1375 }
1376
1377 if ( sibling ) {
1378 var next = sibling.getPrevious();
1379
1380 if ( !enlargeable && !next ) {
1381 // Set the sibling as enlargeable, so it's
1382 // parent will be get later outside this while.
1383 enlargeable = sibling;
1384 sibling = null;
1385 break;
1386 }
1387
1388 sibling = next;
1389 } else {
1390 // If sibling has been set to null, then we
1391 // need to stop enlarging.
1392 enlargeable = null;
1393 }
1394 }
1395
1396 if ( enlargeable )
1397 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1398 }
1399
1400 // Process the end boundary. This is basically the same
1401 // code used for the start boundary, with small changes to
1402 // make it work in the oposite side (to the right). This
1403 // makes it difficult to reuse the code here. So, fixes to
1404 // the above code are likely to be replicated here.
1405
1406 container = this.endContainer;
1407 offset = this.endOffset;
1408
1409 // Reset the common variables.
1410 enlargeable = sibling = null;
1411 commonReached = needsWhiteSpace = false;
1412
1413 // Function check if there are only whitespaces from the given starting point
1414 // (startContainer and startOffset) till the end on block.
1415 // Examples ("[" is the start point):
1416 // - <p>foo[ </p> - will return true,
1417 // - <p><b>foo[ </b> </p> - will return true,
1418 // - <p>foo[ bar</p> - will return false,
1419 // - <p><b>foo[ </b>bar</p> - will return false,
1420 // - <p>foo[ <b></b></p> - will return false.
1421 function onlyWhiteSpaces( startContainer, startOffset ) {
1422 // We need to enlarge range if there is white space at the end of the block,
1423 // because it is not displayed in WYSIWYG mode and user can not select it. So
1424 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1425 // we should do nothing if we are not at the end of the block, so this should not
1426 // be changed: "<p><i>[foo] </i>bar</p>".
1427 var walkerRange = new CKEDITOR.dom.range( boundary );
1428 walkerRange.setStart( startContainer, startOffset );
1429 // The guard will find the end of range so I put boundary here.
1430 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1431
1432 var walker = new CKEDITOR.dom.walker( walkerRange ),
1433 node;
1434
1435 walker.guard = function( node ) {
1436 // Stop if you exit block.
1437 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
1438 };
1439
1440 while ( ( node = walker.next() ) ) {
1441 if ( node.type != CKEDITOR.NODE_TEXT ) {
1442 // Stop if you enter to any node (walker.next() will return node only
1443 // it goes out, not if it is go into node).
1444 return false;
1445 } else {
1446 // Trim the first node to startOffset.
1447 if ( node != startContainer )
1448 siblingText = node.getText();
1449 else
1450 siblingText = node.substring( startOffset );
1451
1452 // Check if it is white space.
1453 if ( leadingWhitespaceRegex.test( siblingText ) )
1454 return false;
1455 }
1456 }
1457
1458 return true;
1459 }
1460
1461 if ( container.type == CKEDITOR.NODE_TEXT ) {
1462 // Check if there is only white space after the offset.
1463 if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
1464 // If we found only whitespace in the node, it
1465 // means that we'll need more whitespace to be able
1466 // to expand. For example, <i> can be expanded in
1467 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1468 needsWhiteSpace = true;
1469 } else {
1470 needsWhiteSpace = !container.getLength();
1471
1472 if ( offset == container.getLength() ) {
1473 // If we are at the end of container and this is the last text node,
1474 // we should enlarge end to the parent.
1475 if ( !( sibling = container.getNext() ) )
1476 enlargeable = container.getParent();
1477 } else {
1478 // If we are in the middle on text node and there are only whitespaces
1479 // till the end of block, we should enlarge element.
1480 if ( onlyWhiteSpaces( container, offset ) )
1481 enlargeable = container.getParent();
1482 }
1483 }
1484 } else {
1485 // Get the node right after the boudary to be checked
1486 // first.
1487 sibling = container.getChild( offset );
1488
1489 if ( !sibling )
1490 enlargeable = container;
1491 }
1492
1493 while ( enlargeable || sibling ) {
1494 if ( enlargeable && !sibling ) {
1495 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1496 commonReached = true;
1497
1498 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1499 break;
1500
1501 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1502 needsWhiteSpace = false;
1503
1504 if ( commonReached )
1505 endTop = enlargeable;
1506 else if ( enlargeable )
1507 this.setEndAfter( enlargeable );
1508 }
1509
1510 sibling = enlargeable.getNext();
1511 }
1512
1513 while ( sibling ) {
1514 isWhiteSpace = false;
1515
1516 if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1517 siblingText = sibling.getText();
1518
1519 // Check if there are not whitespace characters till the end of editable.
1520 // If so stop expanding.
1521 if ( !onlyWhiteSpaces( sibling, 0 ) )
1522 sibling = null;
1523
1524 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1525 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1526 // If this is a visible element.
1527 // We need to check for the bookmark attribute because IE insists on
1528 // rendering the display:none nodes we use for bookmarks. (#3363)
1529 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1530 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1531 // We'll accept it only if we need
1532 // whitespace, and this is an inline
1533 // element with whitespace only.
1534 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1535 // It must contains spaces and inline elements only.
1536
1537 siblingText = sibling.getText();
1538
1539 if ( leadingWhitespaceRegex.test( siblingText ) )
1540 sibling = null;
1541 else {
1542 allChildren = sibling.$.getElementsByTagName( '*' );
1543 for ( i = 0; child = allChildren[ i++ ]; ) {
1544 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1545 sibling = null;
1546 break;
1547 }
1548 }
1549 }
1550
1551 if ( sibling )
1552 isWhiteSpace = !!siblingText.length;
1553 } else {
1554 sibling = null;
1555 }
1556 }
1557 } else {
1558 isWhiteSpace = 1;
1559 }
1560
1561 if ( isWhiteSpace ) {
1562 if ( needsWhiteSpace ) {
1563 if ( commonReached )
1564 endTop = enlargeable;
1565 else
1566 this.setEndAfter( enlargeable );
1567 }
1568 }
1569
1570 if ( sibling ) {
1571 next = sibling.getNext();
1572
1573 if ( !enlargeable && !next ) {
1574 enlargeable = sibling;
1575 sibling = null;
1576 break;
1577 }
1578
1579 sibling = next;
1580 } else {
1581 // If sibling has been set to null, then we
1582 // need to stop enlarging.
1583 enlargeable = null;
1584 }
1585 }
1586
1587 if ( enlargeable )
1588 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1589 }
1590
1591 // If the common ancestor can be enlarged by both boundaries, then include it also.
1592 if ( startTop && endTop ) {
1593 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1594
1595 this.setStartBefore( commonAncestor );
1596 this.setEndAfter( commonAncestor );
1597 }
1598 break;
1599
1600 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1601 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1602
1603 // Enlarging the start boundary.
1604 var walkerRange = new CKEDITOR.dom.range( this.root );
1605
1606 boundary = this.root;
1607
1608 walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
1609 walkerRange.setEnd( this.startContainer, this.startOffset );
1610
1611 var walker = new CKEDITOR.dom.walker( walkerRange ),
1612 blockBoundary, // The node on which the enlarging should stop.
1613 tailBr, // In case BR as block boundary.
1614 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
1615 inNonEditable = null,
1616 // Record the encountered 'blockBoundary' for later use.
1617 boundaryGuard = function( node ) {
1618 // We should not check contents of non-editable elements. It may happen
1619 // that inline widget has display:table child which should not block range#enlarge.
1620 // When encoutered non-editable element...
1621 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1622 if ( inNonEditable ) {
1623 // ... in which we already were, reset it (because we're leaving it) and return.
1624 if ( inNonEditable.equals( node ) ) {
1625 inNonEditable = null;
1626 return;
1627 }
1628 // ... which we're entering, remember it but check it (no return).
1629 } else {
1630 inNonEditable = node;
1631 }
1632 // When we are in non-editable element, do not check if current node is a block boundary.
1633 } else if ( inNonEditable ) {
1634 return;
1635 }
1636
1637 var retval = notBlockBoundary( node );
1638 if ( !retval )
1639 blockBoundary = node;
1640 return retval;
1641 },
1642 // Record the encounted 'tailBr' for later use.
1643 tailBrGuard = function( node ) {
1644 var retval = boundaryGuard( node );
1645 if ( !retval && node.is && node.is( 'br' ) )
1646 tailBr = node;
1647 return retval;
1648 };
1649
1650 walker.guard = boundaryGuard;
1651
1652 enlargeable = walker.lastBackward();
1653
1654 // It's the body which stop the enlarging if no block boundary found.
1655 blockBoundary = blockBoundary || boundary;
1656
1657 // Start the range either after the end of found block (<p>...</p>[text)
1658 // or at the start of block (<p>[text...), by comparing the document position
1659 // with 'enlargeable' node.
1660 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1661 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1662
1663 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1664 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1665 var theRange = this.clone();
1666 walker = new CKEDITOR.dom.walker( theRange );
1667
1668 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1669 bookmark = CKEDITOR.dom.walker.bookmark();
1670
1671 walker.evaluator = function( node ) {
1672 return !whitespaces( node ) && !bookmark( node );
1673 };
1674 var previous = walker.previous();
1675 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1676 return;
1677 }
1678
1679 // Enlarging the end boundary.
1680 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1681
1682 walkerRange = this.clone();
1683 walkerRange.collapse();
1684 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1685 walker = new CKEDITOR.dom.walker( walkerRange );
1686
1687 // tailBrGuard only used for on range end.
1688 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
1689 blockBoundary = inNonEditable = tailBr = null;
1690
1691 // End the range right before the block boundary node.
1692 enlargeable = walker.lastForward();
1693
1694 // It's the body which stop the enlarging if no block boundary found.
1695 blockBoundary = blockBoundary || boundary;
1696
1697 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1698 // by comparing the document position with 'enlargeable' node.
1699 this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() ||
1700 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
1701 // We must include the <br> at the end of range if there's
1702 // one and we're expanding list item contents
1703 if ( tailBr ) {
1704 this.setEndAfter( tailBr );
1705 }
1706 }
1707
1708 // Ensures that returned element can be enlarged by selection, null otherwise.
1709 // @param {CKEDITOR.dom.element} enlargeable
1710 // @returns {CKEDITOR.dom.element/null}
1711 function getValidEnlargeable( enlargeable ) {
1712 return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ?
1713 null : enlargeable;
1714 }
1715 },
1716
1717 /**
1718 * Descrease the range to make sure that boundaries
1719 * always anchor beside text nodes or innermost element.
1720 *
1721 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1722 *
1723 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
1724 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
1725 * node, range remains if there's no text nodes on boundaries at all.
1726 *
1727 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1728 */
1729 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
1730 // Unable to shrink a collapsed range.
1731 if ( !this.collapsed ) {
1732 mode = mode || CKEDITOR.SHRINK_TEXT;
1733
1734 var walkerRange = this.clone();
1735
1736 var startContainer = this.startContainer,
1737 endContainer = this.endContainer,
1738 startOffset = this.startOffset,
1739 endOffset = this.endOffset;
1740
1741 // Whether the start/end boundary is moveable.
1742 var moveStart = 1,
1743 moveEnd = 1;
1744
1745 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1746 if ( !startOffset )
1747 walkerRange.setStartBefore( startContainer );
1748 else if ( startOffset >= startContainer.getLength() )
1749 walkerRange.setStartAfter( startContainer );
1750 else {
1751 // Enlarge the range properly to avoid walker making
1752 // DOM changes caused by triming the text nodes later.
1753 walkerRange.setStartBefore( startContainer );
1754 moveStart = 0;
1755 }
1756 }
1757
1758 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1759 if ( !endOffset )
1760 walkerRange.setEndBefore( endContainer );
1761 else if ( endOffset >= endContainer.getLength() )
1762 walkerRange.setEndAfter( endContainer );
1763 else {
1764 walkerRange.setEndAfter( endContainer );
1765 moveEnd = 0;
1766 }
1767 }
1768
1769 var walker = new CKEDITOR.dom.walker( walkerRange ),
1770 isBookmark = CKEDITOR.dom.walker.bookmark();
1771
1772 walker.evaluator = function( node ) {
1773 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1774 };
1775
1776 var currentElement;
1777 walker.guard = function( node, movingOut ) {
1778 if ( isBookmark( node ) )
1779 return true;
1780
1781 // Stop when we're shrink in element mode while encountering a text node.
1782 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1783 return false;
1784
1785 // Stop when we've already walked "through" an element.
1786 if ( movingOut && node.equals( currentElement ) )
1787 return false;
1788
1789 if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
1790 return false;
1791
1792 // Stop shrinking when encountering an editable border.
1793 if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
1794 return false;
1795
1796 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1797 currentElement = node;
1798
1799 return true;
1800 };
1801
1802 if ( moveStart ) {
1803 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
1804 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1805 }
1806
1807 if ( moveEnd ) {
1808 walker.reset();
1809 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
1810 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1811 }
1812
1813 return !!( moveStart || moveEnd );
1814 }
1815 },
1816
1817 /**
1818 * Inserts a node at the start of the range. The range will be expanded
1819 * the contain the node.
1820 *
1821 * @param {CKEDITOR.dom.node} node
1822 */
1823 insertNode: function( node ) {
1824 this.optimizeBookmark();
1825 this.trim( false, true );
1826
1827 var startContainer = this.startContainer;
1828 var startOffset = this.startOffset;
1829
1830 var nextNode = startContainer.getChild( startOffset );
1831
1832 if ( nextNode )
1833 node.insertBefore( nextNode );
1834 else
1835 startContainer.append( node );
1836
1837 // Check if we need to update the end boundary.
1838 if ( node.getParent() && node.getParent().equals( this.endContainer ) )
1839 this.endOffset++;
1840
1841 // Expand the range to embrace the new node.
1842 this.setStartBefore( node );
1843 },
1844
1845 /**
1846 * Moves the range to given position according to specified node.
1847 *
1848 * // HTML: <p>Foo <b>bar</b></p>
1849 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1850 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1851 *
1852 * See also {@link #setStartAt} and {@link #setEndAt}.
1853 *
1854 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1855 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1856 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1857 * {@link CKEDITOR#POSITION_AFTER_END}.
1858 */
1859 moveToPosition: function( node, position ) {
1860 this.setStartAt( node, position );
1861 this.collapse( true );
1862 },
1863
1864 /**
1865 * Moves the range to the exact position of the specified range.
1866 *
1867 * @param {CKEDITOR.dom.range} range
1868 */
1869 moveToRange: function( range ) {
1870 this.setStart( range.startContainer, range.startOffset );
1871 this.setEnd( range.endContainer, range.endOffset );
1872 },
1873
1874 /**
1875 * Select nodes content. Range will start and end inside this node.
1876 *
1877 * @param {CKEDITOR.dom.node} node
1878 */
1879 selectNodeContents: function( node ) {
1880 this.setStart( node, 0 );
1881 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1882 },
1883
1884 /**
1885 * Sets the start position of a range.
1886 *
1887 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1888 * @param {Number} startOffset An integer greater than or equal to zero
1889 * representing the offset for the start of the range from the start
1890 * of `startNode`.
1891 */
1892 setStart: function( startNode, startOffset ) {
1893 // W3C requires a check for the new position. If it is after the end
1894 // boundary, the range should be collapsed to the new start. It seams
1895 // we will not need this check for our use of this class so we can
1896 // ignore it for now.
1897
1898 // Fixing invalid range start inside dtd empty elements.
1899 if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1900 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1901
1902 this._setStartContainer( startNode );
1903 this.startOffset = startOffset;
1904
1905 if ( !this.endContainer ) {
1906 this._setEndContainer( startNode );
1907 this.endOffset = startOffset;
1908 }
1909
1910 updateCollapsed( this );
1911 },
1912
1913 /**
1914 * Sets the end position of a Range.
1915 *
1916 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1917 * @param {Number} endOffset An integer greater than or equal to zero
1918 * representing the offset for the end of the range from the start
1919 * of `endNode`.
1920 */
1921 setEnd: function( endNode, endOffset ) {
1922 // W3C requires a check for the new position. If it is before the start
1923 // boundary, the range should be collapsed to the new end. It seams we
1924 // will not need this check for our use of this class so we can ignore
1925 // it for now.
1926
1927 // Fixing invalid range end inside dtd empty elements.
1928 if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1929 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1930
1931 this._setEndContainer( endNode );
1932 this.endOffset = endOffset;
1933
1934 if ( !this.startContainer ) {
1935 this._setStartContainer( endNode );
1936 this.startOffset = endOffset;
1937 }
1938
1939 updateCollapsed( this );
1940 },
1941
1942 /**
1943 * Sets start of this range after the specified node.
1944 *
1945 * // Range: <p>foo<b>bar</b>^</p>
1946 * range.setStartAfter( textFoo );
1947 * // The range will be changed to:
1948 * // <p>foo[<b>bar</b>]</p>
1949 *
1950 * @param {CKEDITOR.dom.node} node
1951 */
1952 setStartAfter: function( node ) {
1953 this.setStart( node.getParent(), node.getIndex() + 1 );
1954 },
1955
1956 /**
1957 * Sets start of this range after the specified node.
1958 *
1959 * // Range: <p>foo<b>bar</b>^</p>
1960 * range.setStartBefore( elB );
1961 * // The range will be changed to:
1962 * // <p>foo[<b>bar</b>]</p>
1963 *
1964 * @param {CKEDITOR.dom.node} node
1965 */
1966 setStartBefore: function( node ) {
1967 this.setStart( node.getParent(), node.getIndex() );
1968 },
1969
1970 /**
1971 * Sets end of this range after the specified node.
1972 *
1973 * // Range: <p>foo^<b>bar</b></p>
1974 * range.setEndAfter( elB );
1975 * // The range will be changed to:
1976 * // <p>foo[<b>bar</b>]</p>
1977 *
1978 * @param {CKEDITOR.dom.node} node
1979 */
1980 setEndAfter: function( node ) {
1981 this.setEnd( node.getParent(), node.getIndex() + 1 );
1982 },
1983
1984 /**
1985 * Sets end of this range before the specified node.
1986 *
1987 * // Range: <p>^foo<b>bar</b></p>
1988 * range.setStartAfter( textBar );
1989 * // The range will be changed to:
1990 * // <p>[foo<b>]bar</b></p>
1991 *
1992 * @param {CKEDITOR.dom.node} node
1993 */
1994 setEndBefore: function( node ) {
1995 this.setEnd( node.getParent(), node.getIndex() );
1996 },
1997
1998 /**
1999 * Moves the start of this range to given position according to specified node.
2000 *
2001 * // HTML: <p>Foo <b>bar</b>^</p>
2002 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
2003 * // The range will be changed to:
2004 * // <p>Foo <b>[bar</b>]</p>
2005 *
2006 * See also {@link #setEndAt} and {@link #moveToPosition}.
2007 *
2008 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2009 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2010 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2011 * {@link CKEDITOR#POSITION_AFTER_END}.
2012 */
2013 setStartAt: function( node, position ) {
2014 switch ( position ) {
2015 case CKEDITOR.POSITION_AFTER_START:
2016 this.setStart( node, 0 );
2017 break;
2018
2019 case CKEDITOR.POSITION_BEFORE_END:
2020 if ( node.type == CKEDITOR.NODE_TEXT )
2021 this.setStart( node, node.getLength() );
2022 else
2023 this.setStart( node, node.getChildCount() );
2024 break;
2025
2026 case CKEDITOR.POSITION_BEFORE_START:
2027 this.setStartBefore( node );
2028 break;
2029
2030 case CKEDITOR.POSITION_AFTER_END:
2031 this.setStartAfter( node );
2032 }
2033
2034 updateCollapsed( this );
2035 },
2036
2037 /**
2038 * Moves the end of this range to given position according to specified node.
2039 *
2040 * // HTML: <p>^Foo <b>bar</b></p>
2041 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
2042 * // The range will be changed to:
2043 * // <p>[Foo <b>]bar</b></p>
2044 *
2045 * See also {@link #setStartAt} and {@link #moveToPosition}.
2046 *
2047 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2048 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2049 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2050 * {@link CKEDITOR#POSITION_AFTER_END}.
2051 */
2052 setEndAt: function( node, position ) {
2053 switch ( position ) {
2054 case CKEDITOR.POSITION_AFTER_START:
2055 this.setEnd( node, 0 );
2056 break;
2057
2058 case CKEDITOR.POSITION_BEFORE_END:
2059 if ( node.type == CKEDITOR.NODE_TEXT )
2060 this.setEnd( node, node.getLength() );
2061 else
2062 this.setEnd( node, node.getChildCount() );
2063 break;
2064
2065 case CKEDITOR.POSITION_BEFORE_START:
2066 this.setEndBefore( node );
2067 break;
2068
2069 case CKEDITOR.POSITION_AFTER_END:
2070 this.setEndAfter( node );
2071 }
2072
2073 updateCollapsed( this );
2074 },
2075
2076 /**
2077 * Wraps inline content found around the range's start or end boundary
2078 * with a block element.
2079 *
2080 * // Assuming the following range:
2081 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2082 * // The result of executing:
2083 * range.fixBlock( true, 'p' );
2084 * // will be:
2085 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2086 *
2087 * Non-collapsed range:
2088 *
2089 * // Assuming the following range:
2090 * // ba[r<p>foo</p>bo]m
2091 * // The result of executing:
2092 * range.fixBlock( false, 'p' );
2093 * // will be:
2094 * // ba[r<p>foo</p><p>bo]m</p>
2095 *
2096 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2097 * @param {String} blockTag The name of a block element in which content will be wrapped.
2098 * For example: `'p'`.
2099 * @returns {CKEDITOR.dom.element} Created block wrapper.
2100 */
2101 fixBlock: function( isStart, blockTag ) {
2102 var bookmark = this.createBookmark(),
2103 fixedBlock = this.document.createElement( blockTag );
2104
2105 this.collapse( isStart );
2106
2107 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
2108
2109 this.extractContents().appendTo( fixedBlock );
2110 fixedBlock.trim();
2111
2112 this.insertNode( fixedBlock );
2113
2114 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2115 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2116 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2117 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2118 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2119 // is placed before the bookmark.
2120 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2121 // We remove incorrectly placed one and add a brand new one. (#13001)
2122 var bogus = fixedBlock.getBogus();
2123 if ( bogus ) {
2124 bogus.remove();
2125 }
2126 fixedBlock.appendBogus();
2127
2128 this.moveToBookmark( bookmark );
2129
2130 return fixedBlock;
2131 },
2132
2133 /**
2134 * @todo
2135 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2136 */
2137 splitBlock: function( blockTag, cloneId ) {
2138 var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
2139 endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2140
2141 var startBlockLimit = startPath.blockLimit,
2142 endBlockLimit = endPath.blockLimit;
2143
2144 var startBlock = startPath.block,
2145 endBlock = endPath.block;
2146
2147 var elementPath = null;
2148 // Do nothing if the boundaries are in different block limits.
2149 if ( !startBlockLimit.equals( endBlockLimit ) )
2150 return null;
2151
2152 // Get or fix current blocks.
2153 if ( blockTag != 'br' ) {
2154 if ( !startBlock ) {
2155 startBlock = this.fixBlock( true, blockTag );
2156 endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
2157 }
2158
2159 if ( !endBlock )
2160 endBlock = this.fixBlock( false, blockTag );
2161 }
2162
2163 // Get the range position.
2164 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
2165 isEndOfBlock = endBlock && this.checkEndOfBlock();
2166
2167 // Delete the current contents.
2168 // TODO: Why is 2.x doing CheckIsEmpty()?
2169 this.deleteContents();
2170
2171 if ( startBlock && startBlock.equals( endBlock ) ) {
2172 if ( isEndOfBlock ) {
2173 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2174 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
2175 endBlock = null;
2176 } else if ( isStartOfBlock ) {
2177 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2178 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
2179 startBlock = null;
2180 } else {
2181 endBlock = this.splitElement( startBlock, cloneId || false );
2182
2183 // In Gecko, the last child node must be a bogus <br>.
2184 // Note: bogus <br> added under <ul> or <ol> would cause
2185 // lists to be incorrectly rendered.
2186 if ( !startBlock.is( 'ul', 'ol' ) )
2187 startBlock.appendBogus();
2188 }
2189 }
2190
2191 return {
2192 previousBlock: startBlock,
2193 nextBlock: endBlock,
2194 wasStartOfBlock: isStartOfBlock,
2195 wasEndOfBlock: isEndOfBlock,
2196 elementPath: elementPath
2197 };
2198 },
2199
2200 /**
2201 * Branch the specified element from the collapsed range position and
2202 * place the caret between the two result branches.
2203 *
2204 * **Note:** The range must be collapsed and been enclosed by this element.
2205 *
2206 * @param {CKEDITOR.dom.element} element
2207 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2208 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2209 */
2210 splitElement: function( toSplit, cloneId ) {
2211 if ( !this.collapsed )
2212 return null;
2213
2214 // Extract the contents of the block from the selection point to the end
2215 // of its contents.
2216 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
2217 var documentFragment = this.extractContents( false, cloneId || false );
2218
2219 // Duplicate the element after it.
2220 var clone = toSplit.clone( false, cloneId || false );
2221
2222 // Place the extracted contents into the duplicated element.
2223 documentFragment.appendTo( clone );
2224 clone.insertAfter( toSplit );
2225 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
2226 return clone;
2227 },
2228
2229 /**
2230 * Recursively remove any empty path blocks at the range boundary.
2231 *
2232 * @method
2233 * @param {Boolean} atEnd Removal to perform at the end boundary,
2234 * otherwise to perform at the start.
2235 */
2236 removeEmptyBlocksAtEnd: ( function() {
2237
2238 var whitespace = CKEDITOR.dom.walker.whitespaces(),
2239 bookmark = CKEDITOR.dom.walker.bookmark( false );
2240
2241 function childEval( parent ) {
2242 return function( node ) {
2243 // Whitespace, bookmarks, empty inlines.
2244 if ( whitespace( node ) || bookmark( node ) ||
2245 node.type == CKEDITOR.NODE_ELEMENT &&
2246 node.isEmptyInlineRemoveable() ) {
2247 return false;
2248 } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) {
2249 return false;
2250 }
2251
2252 return true;
2253 };
2254 }
2255
2256 return function( atEnd ) {
2257
2258 var bm = this.createBookmark();
2259 var path = this[ atEnd ? 'endPath' : 'startPath' ]();
2260 var block = path.block || path.blockLimit, parent;
2261
2262 // Remove any childless block, including list and table.
2263 while ( block && !block.equals( path.root ) &&
2264 !block.getFirst( childEval( block ) ) ) {
2265 parent = block.getParent();
2266 this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
2267 block.remove( 1 );
2268 block = parent;
2269 }
2270
2271 this.moveToBookmark( bm );
2272 };
2273
2274 } )(),
2275
2276 /**
2277 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2278 *
2279 * @returns {CKEDITOR.dom.elementPath}
2280 */
2281 startPath: function() {
2282 return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2283 },
2284
2285 /**
2286 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2287 *
2288 * @returns {CKEDITOR.dom.elementPath}
2289 */
2290 endPath: function() {
2291 return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2292 },
2293
2294 /**
2295 * Check whether a range boundary is at the inner boundary of a given
2296 * element.
2297 *
2298 * @param {CKEDITOR.dom.element} element The target element to check.
2299 * @param {Number} checkType The boundary to check for both the range
2300 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2301 * @returns {Boolean} `true` if the range boundary is at the inner
2302 * boundary of the element.
2303 */
2304 checkBoundaryOfElement: function( element, checkType ) {
2305 var checkStart = ( checkType == CKEDITOR.START );
2306
2307 // Create a copy of this range, so we can manipulate it for our checks.
2308 var walkerRange = this.clone();
2309
2310 // Collapse the range at the proper size.
2311 walkerRange.collapse( checkStart );
2312
2313 // Expand the range to element boundary.
2314 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2315
2316 // Create the walker, which will check if we have anything useful
2317 // in the range.
2318 var walker = new CKEDITOR.dom.walker( walkerRange );
2319 walker.evaluator = elementBoundaryEval( checkStart );
2320
2321 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
2322 },
2323
2324 /**
2325 * **Note:** Calls to this function may produce changes to the DOM. The range may
2326 * be updated to reflect such changes.
2327 *
2328 * @returns {Boolean}
2329 * @todo
2330 */
2331 checkStartOfBlock: function() {
2332 var startContainer = this.startContainer,
2333 startOffset = this.startOffset;
2334
2335 // [IE] Special handling for range start in text with a leading NBSP,
2336 // we it to be isolated, for bogus check.
2337 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) {
2338 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
2339 if ( nbspRegExp.test( textBefore ) )
2340 this.trim( 0, 1 );
2341 }
2342
2343 // Antecipate the trim() call here, so the walker will not make
2344 // changes to the DOM, which would not get reflected into this
2345 // range otherwise.
2346 this.trim();
2347
2348 // We need to grab the block element holding the start boundary, so
2349 // let's use an element path for it.
2350 var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2351
2352 // Creates a range starting at the block start until the range start.
2353 var walkerRange = this.clone();
2354 walkerRange.collapse( true );
2355 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
2356
2357 var walker = new CKEDITOR.dom.walker( walkerRange );
2358 walker.evaluator = getCheckStartEndBlockEvalFunction();
2359
2360 return walker.checkBackward();
2361 },
2362
2363 /**
2364 * **Note:** Calls to this function may produce changes to the DOM. The range may
2365 * be updated to reflect such changes.
2366 *
2367 * @returns {Boolean}
2368 * @todo
2369 */
2370 checkEndOfBlock: function() {
2371 var endContainer = this.endContainer,
2372 endOffset = this.endOffset;
2373
2374 // [IE] Special handling for range end in text with a following NBSP,
2375 // we it to be isolated, for bogus check.
2376 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) {
2377 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
2378 if ( nbspRegExp.test( textAfter ) )
2379 this.trim( 1, 0 );
2380 }
2381
2382 // Antecipate the trim() call here, so the walker will not make
2383 // changes to the DOM, which would not get reflected into this
2384 // range otherwise.
2385 this.trim();
2386
2387 // We need to grab the block element holding the start boundary, so
2388 // let's use an element path for it.
2389 var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2390
2391 // Creates a range starting at the block start until the range start.
2392 var walkerRange = this.clone();
2393 walkerRange.collapse( false );
2394 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
2395
2396 var walker = new CKEDITOR.dom.walker( walkerRange );
2397 walker.evaluator = getCheckStartEndBlockEvalFunction();
2398
2399 return walker.checkForward();
2400 },
2401
2402 /**
2403 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2404 *
2405 * @param {Function} evaluator Function used as the walker's evaluator.
2406 * @param {Function} [guard] Function used as the walker's guard.
2407 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2408 * default to the root editable if not defined.
2409 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2410 */
2411 getPreviousNode: function( evaluator, guard, boundary ) {
2412 var walkerRange = this.clone();
2413 walkerRange.collapse( 1 );
2414 walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
2415
2416 var walker = new CKEDITOR.dom.walker( walkerRange );
2417 walker.evaluator = evaluator;
2418 walker.guard = guard;
2419 return walker.previous();
2420 },
2421
2422 /**
2423 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2424 *
2425 * @param {Function} evaluator Function used as the walker's evaluator.
2426 * @param {Function} [guard] Function used as the walker's guard.
2427 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2428 * default to the root editable if not defined.
2429 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2430 */
2431 getNextNode: function( evaluator, guard, boundary ) {
2432 var walkerRange = this.clone();
2433 walkerRange.collapse();
2434 walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
2435
2436 var walker = new CKEDITOR.dom.walker( walkerRange );
2437 walker.evaluator = evaluator;
2438 walker.guard = guard;
2439 return walker.next();
2440 },
2441
2442 /**
2443 * Check if elements at which the range boundaries anchor are read-only,
2444 * with respect to `contenteditable` attribute.
2445 *
2446 * @returns {Boolean}
2447 */
2448 checkReadOnly: ( function() {
2449 function checkNodesEditable( node, anotherEnd ) {
2450 while ( node ) {
2451 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2452 if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
2453 return 0;
2454
2455 // Range enclosed entirely in an editable element.
2456 else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
2457 break;
2458
2459 }
2460 node = node.getParent();
2461 }
2462
2463 return 1;
2464 }
2465
2466 return function() {
2467 var startNode = this.startContainer,
2468 endNode = this.endContainer;
2469
2470 // Check if elements path at both boundaries are editable.
2471 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
2472 };
2473 } )(),
2474
2475 /**
2476 * Moves the range boundaries to the first/end editing point inside an
2477 * element.
2478 *
2479 * For example, in an element tree like
2480 * `<p><b><i></i></b> Text</p>`, the start editing point is
2481 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2482 *
2483 * @param {CKEDITOR.dom.element} el The element into which look for the
2484 * editing spot.
2485 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2486 * @returns {Boolean} Whether range was moved.
2487 */
2488 moveToElementEditablePosition: function( el, isMoveToEnd ) {
2489
2490 function nextDFS( node, childOnly ) {
2491 var next;
2492
2493 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
2494 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
2495
2496 if ( !childOnly && !next )
2497 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
2498
2499 return next;
2500 }
2501
2502 // Handle non-editable element e.g. HR.
2503 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
2504 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2505 return true;
2506 }
2507
2508 var found = 0;
2509
2510 while ( el ) {
2511 // Stop immediately if we've found a text node.
2512 if ( el.type == CKEDITOR.NODE_TEXT ) {
2513 // Put cursor before block filler.
2514 if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
2515 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2516 else
2517 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2518 found = 1;
2519 break;
2520 }
2521
2522 // If an editable element is found, move inside it, but not stop the searching.
2523 if ( el.type == CKEDITOR.NODE_ELEMENT ) {
2524 if ( el.isEditable() ) {
2525 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
2526 found = 1;
2527 }
2528 // Put cursor before padding block br.
2529 else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
2530 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2531 // Special case - non-editable block. Select entire element, because it does not make sense
2532 // to place collapsed selection next to it, because browsers can't handle that.
2533 else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
2534 this.setStartBefore( el );
2535 this.setEndAfter( el );
2536 return true;
2537 }
2538 }
2539
2540 el = nextDFS( el, found );
2541 }
2542
2543 return !!found;
2544 },
2545
2546 /**
2547 * Moves the range boundaries to the closest editing point after/before an
2548 * element or the current range position (depends on whether the element was specified).
2549 *
2550 * For example, if the start element has `id="start"`,
2551 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2552 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2553 *
2554 * See also: {@link #moveToElementEditablePosition}.
2555 *
2556 * @since 4.3
2557 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2558 * position will be used.
2559 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2560 * @returns {Boolean} Whether the range was moved.
2561 */
2562 moveToClosestEditablePosition: function( element, isMoveForward ) {
2563 // We don't want to modify original range if there's no editable position.
2564 var range,
2565 found = 0,
2566 sibling,
2567 isElement,
2568 positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
2569
2570 if ( element ) {
2571 // Set collapsed range at one of ends of element.
2572 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2573 range = new CKEDITOR.dom.range( this.root );
2574 range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] );
2575 } else {
2576 range = this.clone();
2577 }
2578
2579 // Start element isn't a block, so we can automatically place range
2580 // next to it.
2581 if ( element && !element.is( CKEDITOR.dtd.$block ) )
2582 found = 1;
2583 else {
2584 // Look for first node that fulfills eval function and place range next to it.
2585 sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2586 if ( sibling ) {
2587 found = 1;
2588 isElement = sibling.type == CKEDITOR.NODE_ELEMENT;
2589
2590 // Special case - eval accepts block element only if it's a non-editable block,
2591 // which we want to select, not place collapsed selection next to it (which browsers
2592 // can't handle).
2593 if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
2594 range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
2595 range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
2596 }
2597 // Handle empty blocks which can be selection containers on old IEs.
2598 else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) {
2599 range.setEnd( sibling, 0 );
2600 range.collapse();
2601 } else {
2602 range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] );
2603 }
2604 }
2605 }
2606
2607 if ( found )
2608 this.moveToRange( range );
2609
2610 return !!found;
2611 },
2612
2613 /**
2614 * See {@link #moveToElementEditablePosition}.
2615 *
2616 * @returns {Boolean} Whether range was moved.
2617 */
2618 moveToElementEditStart: function( target ) {
2619 return this.moveToElementEditablePosition( target );
2620 },
2621
2622 /**
2623 * See {@link #moveToElementEditablePosition}.
2624 *
2625 * @returns {Boolean} Whether range was moved.
2626 */
2627 moveToElementEditEnd: function( target ) {
2628 return this.moveToElementEditablePosition( target, true );
2629 },
2630
2631 /**
2632 * Get the single node enclosed within the range if there's one.
2633 *
2634 * @returns {CKEDITOR.dom.node}
2635 */
2636 getEnclosedNode: function() {
2637 var walkerRange = this.clone();
2638
2639 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
2640 walkerRange.optimize();
2641 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2642 return null;
2643
2644 var walker = new CKEDITOR.dom.walker( walkerRange ),
2645 isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
2646 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
2647
2648 walker.evaluator = function( node ) {
2649 return isNotWhitespaces( node ) && isNotBookmarks( node );
2650 };
2651 var node = walker.next();
2652 walker.reset();
2653 return node && node.equals( walker.previous() ) ? node : null;
2654 },
2655
2656 /**
2657 * Get the node adjacent to the range start or {@link #startContainer}.
2658 *
2659 * @returns {CKEDITOR.dom.node}
2660 */
2661 getTouchedStartNode: function() {
2662 var container = this.startContainer;
2663
2664 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2665 return container;
2666
2667 return container.getChild( this.startOffset ) || container;
2668 },
2669
2670 /**
2671 * Get the node adjacent to the range end or {@link #endContainer}.
2672 *
2673 * @returns {CKEDITOR.dom.node}
2674 */
2675 getTouchedEndNode: function() {
2676 var container = this.endContainer;
2677
2678 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2679 return container;
2680
2681 return container.getChild( this.endOffset - 1 ) || container;
2682 },
2683
2684 /**
2685 * Gets next node which can be a container of a selection.
2686 * This methods mimics a behavior of right/left arrow keys in case of
2687 * collapsed selection. It does not return an exact position (with offset) though,
2688 * but just a selection's container.
2689 *
2690 * Note: use this method on a collapsed range.
2691 *
2692 * @since 4.3
2693 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2694 */
2695 getNextEditableNode: getNextEditableNode(),
2696
2697 /**
2698 * See {@link #getNextEditableNode}.
2699 *
2700 * @since 4.3
2701 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2702 */
2703 getPreviousEditableNode: getNextEditableNode( 1 ),
2704
2705 /**
2706 * Scrolls the start of current range into view.
2707 */
2708 scrollIntoView: function() {
2709
2710 // The reference element contains a zero-width space to avoid
2711 // a premature removal. The view is to be scrolled with respect
2712 // to this element.
2713 var reference = new CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', this.document ),
2714 afterCaretNode, startContainerText, isStartText;
2715
2716 var range = this.clone();
2717
2718 // Work with the range to obtain a proper caret position.
2719 range.optimize();
2720
2721 // Currently in a text node, so we need to split it into two
2722 // halves and put the reference between.
2723 if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
2724 // Keep the original content. It will be restored.
2725 startContainerText = range.startContainer.getText();
2726
2727 // Split the startContainer at the this position.
2728 afterCaretNode = range.startContainer.split( range.startOffset );
2729
2730 // Insert the reference between two text nodes.
2731 reference.insertAfter( range.startContainer );
2732 }
2733
2734 // If not in a text node, simply insert the reference into the range.
2735 else {
2736 range.insertNode( reference );
2737 }
2738
2739 // Scroll with respect to the reference element.
2740 reference.scrollIntoView();
2741
2742 // Get rid of split parts if "in a text node" case.
2743 // Revert the original text of the startContainer.
2744 if ( isStartText ) {
2745 range.startContainer.setText( startContainerText );
2746 afterCaretNode.remove();
2747 }
2748
2749 // Get rid of the reference node. It is no longer necessary.
2750 reference.remove();
2751 },
2752
2753 /**
2754 * Setter for the {@link #startContainer}.
2755 *
2756 * @since 4.4.6
2757 * @private
2758 * @param {CKEDITOR.dom.element} startContainer
2759 */
2760 _setStartContainer: function( startContainer ) {
2761 // %REMOVE_START%
2762 var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer );
2763
2764 if ( !isRootAscendantOrSelf ) {
2765 CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } );
2766 }
2767 // %REMOVE_END%
2768 this.startContainer = startContainer;
2769 },
2770
2771 /**
2772 * Setter for the {@link #endContainer}.
2773 *
2774 * @since 4.4.6
2775 * @private
2776 * @param {CKEDITOR.dom.element} endContainer
2777 */
2778 _setEndContainer: function( endContainer ) {
2779 // %REMOVE_START%
2780 var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer );
2781
2782 if ( !isRootAscendantOrSelf ) {
2783 CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } );
2784 }
2785 // %REMOVE_END%
2786 this.endContainer = endContainer;
2787 },
2788
2789 /**
2790 * Looks for elements matching the `query` selector within a range.
2791 *
2792 * @since 4.5.11
2793 * @private
2794 * @param {String} query
2795 * @param {Boolean} [includeNonEditables=false] Whether elements with `contenteditable` set to `false` should
2796 * be included.
2797 * @returns {CKEDITOR.dom.element[]}
2798 */
2799 _find: function( query, includeNonEditables ) {
2800 var ancestor = this.getCommonAncestor(),
2801 boundaries = this.getBoundaryNodes(),
2802 // Contrary to CKEDITOR.dom.element#find we're returning array, that's because NodeList is immutable, and we need
2803 // to do some filtering in returned list.
2804 ret = [],
2805 curItem,
2806 i,
2807 initialMatches,
2808 isStartGood,
2809 isEndGood;
2810
2811 if ( ancestor && ancestor.find ) {
2812 initialMatches = ancestor.find( query );
2813
2814 for ( i = 0; i < initialMatches.count(); i++ ) {
2815 curItem = initialMatches.getItem( i );
2816
2817 // Using isReadOnly() method to filterout non editables. It checks isContentEditable including all browser quirks.
2818 if ( !includeNonEditables && curItem.isReadOnly() ) {
2819 continue;
2820 }
2821
2822 // It's not enough to get elements from common ancestor, because it migth contain too many matches.
2823 // We need to ensure that returned items are between boundary points.
2824 isStartGood = ( curItem.getPosition( boundaries.startNode ) & CKEDITOR.POSITION_FOLLOWING ) || boundaries.startNode.equals( curItem );
2825 isEndGood = ( curItem.getPosition( boundaries.endNode ) & ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IS_CONTAINED ) );
2826
2827 if ( isStartGood && isEndGood ) {
2828 ret.push( curItem );
2829 }
2830 }
2831 }
2832
2833 return ret;
2834 }
2835 };
2836
2837
2838 } )();
2839
2840 /**
2841 * Indicates a position after start of a node.
2842 *
2843 * // When used according to an element:
2844 * // <element>^contents</element>
2845 *
2846 * // When used according to a text node:
2847 * // "^text" (range is anchored in the text node)
2848 *
2849 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2850 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2851 *
2852 * @readonly
2853 * @member CKEDITOR
2854 * @property {Number} [=1]
2855 */
2856 CKEDITOR.POSITION_AFTER_START = 1;
2857
2858 /**
2859 * Indicates a position before end of a node.
2860 *
2861 * // When used according to an element:
2862 * // <element>contents^</element>
2863 *
2864 * // When used according to a text node:
2865 * // "text^" (range is anchored in the text node)
2866 *
2867 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2868 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2869 *
2870 * @readonly
2871 * @member CKEDITOR
2872 * @property {Number} [=2]
2873 */
2874 CKEDITOR.POSITION_BEFORE_END = 2;
2875
2876 /**
2877 * Indicates a position before start of a node.
2878 *
2879 * // When used according to an element:
2880 * // ^<element>contents</element> (range is anchored in element's parent)
2881 *
2882 * // When used according to a text node:
2883 * // ^"text" (range is anchored in text node's parent)
2884 *
2885 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2886 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2887 *
2888 * @readonly
2889 * @member CKEDITOR
2890 * @property {Number} [=3]
2891 */
2892 CKEDITOR.POSITION_BEFORE_START = 3;
2893
2894 /**
2895 * Indicates a position after end of a node.
2896 *
2897 * // When used according to an element:
2898 * // <element>contents</element>^ (range is anchored in element's parent)
2899 *
2900 * // When used according to a text node:
2901 * // "text"^ (range is anchored in text node's parent)
2902 *
2903 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2904 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2905 *
2906 * @readonly
2907 * @member CKEDITOR
2908 * @property {Number} [=4]
2909 */
2910 CKEDITOR.POSITION_AFTER_END = 4;
2911
2912 /**
2913 * @readonly
2914 * @member CKEDITOR
2915 * @property {Number} [=1]
2916 */
2917 CKEDITOR.ENLARGE_ELEMENT = 1;
2918
2919 /**
2920 * @readonly
2921 * @member CKEDITOR
2922 * @property {Number} [=2]
2923 */
2924 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2925
2926 /**
2927 * @readonly
2928 * @member CKEDITOR
2929 * @property {Number} [=3]
2930 */
2931 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2932
2933 /**
2934 * @readonly
2935 * @member CKEDITOR
2936 * @property {Number} [=4]
2937 */
2938 CKEDITOR.ENLARGE_INLINE = 4;
2939
2940 // Check boundary types.
2941
2942 /**
2943 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2944 *
2945 * @readonly
2946 * @member CKEDITOR
2947 * @property {Number} [=1]
2948 */
2949 CKEDITOR.START = 1;
2950
2951 /**
2952 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2953 *
2954 * @readonly
2955 * @member CKEDITOR
2956 * @property {Number} [=2]
2957 */
2958 CKEDITOR.END = 2;
2959
2960 // Shrink range types.
2961
2962 /**
2963 * See {@link CKEDITOR.dom.range#shrink}.
2964 *
2965 * @readonly
2966 * @member CKEDITOR
2967 * @property {Number} [=1]
2968 */
2969 CKEDITOR.SHRINK_ELEMENT = 1;
2970
2971 /**
2972 * See {@link CKEDITOR.dom.range#shrink}.
2973 *
2974 * @readonly
2975 * @member CKEDITOR
2976 * @property {Number} [=2]
2977 */
2978 CKEDITOR.SHRINK_TEXT = 2;