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