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