]> git.immae.eu Git - perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git/blob - sources/core/selection.js
Upgrade to 4.5.7 and add some plugin
[perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git] / sources / core / selection.js
1 /**
2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 ( function() {
7 // #### checkSelectionChange : START
8
9 // The selection change check basically saves the element parent tree of
10 // the current node and check it on successive requests. If there is any
11 // change on the tree, then the selectionChange event gets fired.
12 function checkSelectionChange() {
13 // A possibly available fake-selection.
14 var sel = this._.fakeSelection,
15 realSel;
16
17 if ( sel ) {
18 realSel = this.getSelection( 1 );
19
20 // If real (not locked/stored) selection was moved from hidden container,
21 // then the fake-selection must be invalidated.
22 if ( !realSel || !realSel.isHidden() ) {
23 // Remove the cache from fake-selection references in use elsewhere.
24 sel.reset();
25
26 // Have the code using the native selection.
27 sel = 0;
28 }
29 }
30
31 // If not fake-selection is available then get the native selection.
32 if ( !sel ) {
33 sel = realSel || this.getSelection( 1 );
34
35 // Editor may have no selection at all.
36 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
37 return;
38 }
39
40 this.fire( 'selectionCheck', sel );
41
42 var currentPath = this.elementPath();
43 if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
44 // Cache the active element, which we'll eventually lose on Webkit.
45 if ( CKEDITOR.env.webkit )
46 this._.previousActive = this.document.getActive();
47
48 this._.selectionPreviousPath = currentPath;
49 this.fire( 'selectionChange', { selection: sel, path: currentPath } );
50 }
51 }
52
53 var checkSelectionChangeTimer, checkSelectionChangeTimeoutPending;
54
55 function checkSelectionChangeTimeout() {
56 // Firing the "OnSelectionChange" event on every key press started to
57 // be too slow. This function guarantees that there will be at least
58 // 200ms delay between selection checks.
59
60 checkSelectionChangeTimeoutPending = true;
61
62 if ( checkSelectionChangeTimer )
63 return;
64
65 checkSelectionChangeTimeoutExec.call( this );
66
67 checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this );
68 }
69
70 function checkSelectionChangeTimeoutExec() {
71 checkSelectionChangeTimer = null;
72
73 if ( checkSelectionChangeTimeoutPending ) {
74 // Call this with a timeout so the browser properly moves the
75 // selection after the mouseup. It happened that the selection was
76 // being moved after the mouseup when clicking inside selected text
77 // with Firefox.
78 CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this );
79
80 checkSelectionChangeTimeoutPending = false;
81 }
82 }
83
84 // #### checkSelectionChange : END
85
86 var isVisible = CKEDITOR.dom.walker.invisible( 1 );
87
88 // May absorb the caret if:
89 // * is a visible node,
90 // * is a non-empty element (this rule will accept elements like <strong></strong> because they
91 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
92 // See #12621.
93 function mayAbsorbCaret( node ) {
94 if ( isVisible( node ) )
95 return true;
96
97 if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( CKEDITOR.dtd.$empty ) )
98 return true;
99
100 return false;
101 }
102
103 function rangeRequiresFix( range ) {
104 // Whether we must prevent from absorbing caret by this context node.
105 // Also checks whether there's an editable position next to that node.
106 function ctxRequiresFix( node, isAtEnd ) {
107 // It's ok for us if a text node absorbs the caret, because
108 // the caret container element isn't changed then.
109 if ( !node || node.type == CKEDITOR.NODE_TEXT )
110 return false;
111
112 var testRng = range.clone();
113 return testRng[ 'moveToElementEdit' + ( isAtEnd ? 'End' : 'Start' ) ]( node );
114 }
115
116 // Range root must be the editable element, it's to avoid creating filler char
117 // on any temporary internal selection.
118 if ( !( range.root instanceof CKEDITOR.editable ) )
119 return false;
120
121 var ct = range.startContainer;
122
123 var previous = range.getPreviousNode( mayAbsorbCaret, null, ct ),
124 next = range.getNextNode( mayAbsorbCaret, null, ct );
125
126 // Any adjacent text container may absorb the caret, e.g.
127 // <p><strong>text</strong>^foo</p>
128 // <p>foo^<strong>text</strong></p>
129 // <div>^<p>foo</p></div>
130 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
131 return true;
132
133 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222)
134 // If you found this line confusing check #12655.
135 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) )
136 return true;
137
138 return false;
139 }
140
141 function createFillingCharSequenceNode( editable ) {
142 removeFillingCharSequenceNode( editable, false );
143
144 var fillingChar = editable.getDocument().createText( fillingCharSequence );
145 editable.setCustomData( 'cke-fillingChar', fillingChar );
146
147 return fillingChar;
148 }
149
150 // Checks if a filling char has been used, eventualy removing it (#1272).
151 function checkFillingCharSequenceNodeReady( editable ) {
152 var fillingChar = editable.getCustomData( 'cke-fillingChar' );
153
154 if ( fillingChar ) {
155 // Use this flag to avoid removing the filling char right after
156 // creating it.
157 if ( fillingChar.getCustomData( 'ready' ) ) {
158 removeFillingCharSequenceNode( editable );
159 } else {
160 fillingChar.setCustomData( 'ready', 1 );
161 }
162 }
163 }
164
165 function removeFillingCharSequenceNode( editable, keepSelection ) {
166 var fillingChar = editable && editable.removeCustomData( 'cke-fillingChar' );
167
168 if ( fillingChar ) {
169 // Text selection position might get mangled by
170 // subsequent dom modification, save it now for restoring. (#8617)
171 if ( keepSelection !== false ) {
172 var sel = editable.getDocument().getSelection().getNative(),
173 // Be error proof.
174 range = sel && sel.type != 'None' && sel.getRangeAt( 0 ),
175 fillingCharSeqLength = fillingCharSequence.length;
176
177 // If there's some text other than the sequence in the FC text node and the range
178 // intersects with that node...
179 if ( fillingChar.getLength() > fillingCharSeqLength && range && range.intersectsNode( fillingChar.$ ) ) {
180 var bm = createNativeSelectionBookmark( sel );
181
182 // Correct start offset anticipating the removal of FC.
183 if ( sel.anchorNode == fillingChar.$ && sel.anchorOffset > fillingCharSeqLength ) {
184 bm[ 0 ].offset -= fillingCharSeqLength;
185 }
186
187 // Correct end offset anticipating the removal of FC.
188 if ( sel.focusNode == fillingChar.$ && sel.focusOffset > fillingCharSeqLength ) {
189 bm[ 1 ].offset -= fillingCharSeqLength;
190 }
191 }
192 }
193
194 // We can't simply remove the filling node because the user
195 // will actually enlarge it when typing, so we just remove the
196 // invisible char from it.
197 fillingChar.setText( removeFillingCharSequenceString( fillingChar.getText(), 1 ) );
198
199 // Restore the bookmark preserving selection's direction.
200 if ( bm ) {
201 moveNativeSelectionToBookmark( editable.getDocument().$, bm );
202 }
203 }
204 }
205
206 // #13816
207 function removeFillingCharSequenceString( str, nbspAware ) {
208 if ( nbspAware ) {
209 return str.replace( fillingCharSequenceRegExp, function( m, p ) {
210 // #10291 if filling char is followed by a space replace it with NBSP.
211 return p ? '\xa0' : '';
212 } );
213 } else {
214 return str.replace( fillingCharSequence, '' );
215 }
216 }
217
218 function createNativeSelectionBookmark( sel ) {
219 return [
220 { node: sel.anchorNode, offset: sel.anchorOffset },
221 { node: sel.focusNode, offset: sel.focusOffset }
222 ];
223 }
224
225 function moveNativeSelectionToBookmark( document, bm ) {
226 var sel = document.getSelection(),
227 range = document.createRange();
228
229 range.setStart( bm[ 0 ].node, bm[ 0 ].offset );
230 range.collapse( true );
231 sel.removeAllRanges();
232 sel.addRange( range );
233 sel.extend( bm[ 1 ].node, bm[ 1 ].offset );
234 }
235
236 // Creates cke_hidden_sel container and puts real selection there.
237 function hideSelection( editor ) {
238 var style = CKEDITOR.env.ie ? 'display:none' : 'position:fixed;top:0;left:-1000px',
239 hiddenEl = CKEDITOR.dom.element.createFromHtml(
240 '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">&nbsp;</div>',
241 editor.document );
242
243 editor.fire( 'lockSnapshot' );
244
245 editor.editable().append( hiddenEl );
246
247 // Always use real selection to avoid overriding locked one (http://dev.ckeditor.com/ticket/11104#comment:13).
248 var sel = editor.getSelection( 1 ),
249 range = editor.createRange(),
250 // Cancel selectionchange fired by selectRanges - prevent from firing selectionChange.
251 listener = sel.root.on( 'selectionchange', function( evt ) {
252 evt.cancel();
253 }, null, null, 0 );
254
255 range.setStartAt( hiddenEl, CKEDITOR.POSITION_AFTER_START );
256 range.setEndAt( hiddenEl, CKEDITOR.POSITION_BEFORE_END );
257 sel.selectRanges( [ range ] );
258
259 listener.removeListener();
260
261 editor.fire( 'unlockSnapshot' );
262
263 // Set this value at the end, so reset() executed by selectRanges()
264 // will clean up old hidden selection container.
265 editor._.hiddenSelectionContainer = hiddenEl;
266 }
267
268 function removeHiddenSelectionContainer( editor ) {
269 var hiddenEl = editor._.hiddenSelectionContainer;
270
271 if ( hiddenEl ) {
272 var isDirty = editor.checkDirty();
273
274 editor.fire( 'lockSnapshot' );
275 hiddenEl.remove();
276 editor.fire( 'unlockSnapshot' );
277
278 !isDirty && editor.resetDirty();
279 }
280
281 delete editor._.hiddenSelectionContainer;
282 }
283
284 // Object containing keystroke handlers for fake selection.
285 var fakeSelectionDefaultKeystrokeHandlers = ( function() {
286 function leave( right ) {
287 return function( evt ) {
288 var range = evt.editor.createRange();
289
290 // Move selection only if there's a editable place for it.
291 // It no, then do nothing (keystroke will be blocked, widget selection kept).
292 if ( range.moveToClosestEditablePosition( evt.selected, right ) )
293 evt.editor.getSelection().selectRanges( [ range ] );
294
295 // Prevent default.
296 return false;
297 };
298 }
299
300 function del( right ) {
301 return function( evt ) {
302 var editor = evt.editor,
303 range = editor.createRange(),
304 found;
305
306 // If haven't found place for caret on the default side,
307 // try to find it on the other side.
308 if ( !( found = range.moveToClosestEditablePosition( evt.selected, right ) ) )
309 found = range.moveToClosestEditablePosition( evt.selected, !right );
310
311 if ( found )
312 editor.getSelection().selectRanges( [ range ] );
313
314 // Save the state before removing selected element.
315 editor.fire( 'saveSnapshot' );
316
317 evt.selected.remove();
318
319 // Haven't found any editable space before removing element,
320 // try to place the caret anywhere (most likely, in empty editable).
321 if ( !found ) {
322 range.moveToElementEditablePosition( editor.editable() );
323 editor.getSelection().selectRanges( [ range ] );
324 }
325
326 editor.fire( 'saveSnapshot' );
327
328 // Prevent default.
329 return false;
330 };
331 }
332
333 var leaveLeft = leave(),
334 leaveRight = leave( 1 );
335
336 return {
337 37: leaveLeft, // LEFT
338 38: leaveLeft, // UP
339 39: leaveRight, // RIGHT
340 40: leaveRight, // DOWN
341 8: del(), // BACKSPACE
342 46: del( 1 ) // DELETE
343 };
344 } )();
345
346 // Handle left, right, delete and backspace keystrokes next to non-editable elements
347 // by faking selection on them.
348 function getOnKeyDownListener( editor ) {
349 var keystrokes = { 37: 1, 39: 1, 8: 1, 46: 1 };
350
351 return function( evt ) {
352 var keystroke = evt.data.getKeystroke();
353
354 // Handle only left/right/del/bspace keys.
355 if ( !keystrokes[ keystroke ] )
356 return;
357
358 var sel = editor.getSelection(),
359 ranges = sel.getRanges(),
360 range = ranges[ 0 ];
361
362 // Handle only single range and it has to be collapsed.
363 if ( ranges.length != 1 || !range.collapsed )
364 return;
365
366 var next = range[ keystroke < 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ]();
367
368 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getAttribute( 'contenteditable' ) == 'false' ) {
369 editor.getSelection().fake( next );
370 evt.data.preventDefault();
371 evt.cancel();
372 }
373 };
374 }
375
376 // If fake selection should be applied this function will return instance of
377 // CKEDITOR.dom.element which should gain fake selection.
378 function getNonEditableFakeSelectionReceiver( ranges ) {
379 var enclosedNode, shrinkedNode, clone, range;
380
381 if ( ranges.length == 1 && !( range = ranges[ 0 ] ).collapsed &&
382 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
383 // So far we can't say that enclosed element is non-editable. Before checking,
384 // we'll shrink range (clone). Shrinking will stop on non-editable range, or
385 // innermost element (#11114).
386 clone = range.clone();
387 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
388
389 // If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements).
390 if ( ( shrinkedNode = clone.getEnclosedNode() ) && shrinkedNode.type == CKEDITOR.NODE_ELEMENT )
391 enclosedNode = shrinkedNode;
392
393 if ( enclosedNode.getAttribute( 'contenteditable' ) == 'false' )
394 return enclosedNode;
395 }
396 }
397
398 // Fix ranges which may end after hidden selection container.
399 // Note: this function may only be used if hidden selection container
400 // is not in DOM any more.
401 function fixRangesAfterHiddenSelectionContainer( ranges, root ) {
402 var range;
403 for ( var i = 0; i < ranges.length; ++i ) {
404 range = ranges[ i ];
405 if ( range.endContainer.equals( root ) ) {
406 // We can use getChildCount() because hidden selection container is not in DOM.
407 range.endOffset = Math.min( range.endOffset, root.getChildCount() );
408 }
409 }
410 }
411
412 // Extract only editable part or ranges.
413 // Note: this function modifies ranges list!
414 // @param {CKEDITOR.dom.rangeList} ranges
415 function extractEditableRanges( ranges ) {
416 for ( var i = 0; i < ranges.length; i++ ) {
417 var range = ranges[ i ];
418
419 // Drop range spans inside one ready-only node.
420 var parent = range.getCommonAncestor();
421 if ( parent.isReadOnly() )
422 ranges.splice( i, 1 );
423
424 if ( range.collapsed )
425 continue;
426
427 // Range may start inside a non-editable element,
428 // replace the range start after it.
429 if ( range.startContainer.isReadOnly() ) {
430 var current = range.startContainer,
431 isElement;
432
433 while ( current ) {
434 isElement = current.type == CKEDITOR.NODE_ELEMENT;
435
436 if ( ( isElement && current.is( 'body' ) ) || !current.isReadOnly() )
437 break;
438
439 if ( isElement && current.getAttribute( 'contentEditable' ) == 'false' )
440 range.setStartAfter( current );
441
442 current = current.getParent();
443 }
444 }
445
446 var startContainer = range.startContainer,
447 endContainer = range.endContainer,
448 startOffset = range.startOffset,
449 endOffset = range.endOffset,
450 walkerRange = range.clone();
451
452 // Enlarge range start/end with text node to avoid walker
453 // being DOM destructive, it doesn't interfere our checking
454 // of elements below as well.
455 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
456 if ( startOffset >= startContainer.getLength() )
457 walkerRange.setStartAfter( startContainer );
458 else
459 walkerRange.setStartBefore( startContainer );
460 }
461
462 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
463 if ( !endOffset )
464 walkerRange.setEndBefore( endContainer );
465 else
466 walkerRange.setEndAfter( endContainer );
467 }
468
469 // Looking for non-editable element inside the range.
470 var walker = new CKEDITOR.dom.walker( walkerRange );
471 walker.evaluator = function( node ) {
472 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isReadOnly() ) {
473 var newRange = range.clone();
474 range.setEndBefore( node );
475
476 // Drop collapsed range around read-only elements,
477 // it make sure the range list empty when selecting
478 // only non-editable elements.
479 if ( range.collapsed )
480 ranges.splice( i--, 1 );
481
482 // Avoid creating invalid range.
483 if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) {
484 newRange.setStartAfter( node );
485 if ( !newRange.collapsed )
486 ranges.splice( i + 1, 0, newRange );
487 }
488
489 return true;
490 }
491
492 return false;
493 };
494
495 walker.next();
496 }
497
498 return ranges;
499 }
500
501 // Setup all editor instances for the necessary selection hooks.
502 CKEDITOR.on( 'instanceCreated', function( ev ) {
503 var editor = ev.editor;
504
505 editor.on( 'contentDom', function() {
506 var doc = editor.document,
507 outerDoc = CKEDITOR.document,
508 editable = editor.editable(),
509 body = doc.getBody(),
510 html = doc.getDocumentElement();
511
512 var isInline = editable.isInline();
513
514 var restoreSel,
515 lastSel;
516
517 // Give the editable an initial selection on first focus,
518 // put selection at a consistent position at the start
519 // of the contents. (#9507)
520 if ( CKEDITOR.env.gecko ) {
521 editable.attachListener( editable, 'focus', function( evt ) {
522 evt.removeListener();
523
524 if ( restoreSel !== 0 ) {
525 var nativ = editor.getSelection().getNative();
526 // Do it only if the native selection is at an unwanted
527 // place (at the very start of the editable). #10119
528 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
529 var rng = editor.createRange();
530 rng.moveToElementEditStart( editable );
531 rng.select();
532 }
533 }
534 }, null, null, -2 );
535 }
536
537 // Plays the magic here to restore/save dom selection on editable focus/blur.
538 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
539 // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
540 // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
541 // for the same active element, what will e.g. mean restoring after displaying dialog.
542 if ( restoreSel && CKEDITOR.env.webkit )
543 restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() );
544
545 editor.unlockSelection( restoreSel );
546 restoreSel = 0;
547 }, null, null, -1 );
548
549 // Disable selection restoring when clicking in.
550 editable.attachListener( editable, 'mousedown', function() {
551 restoreSel = 0;
552 } );
553
554 // Save a cloned version of current selection.
555 function saveSel() {
556 lastSel = new CKEDITOR.dom.selection( editor.getSelection() );
557 lastSel.lock();
558 }
559
560 // Browsers could loose the selection once the editable lost focus,
561 // in such case we need to reproduce it by saving a locked selection
562 // and restoring it upon focus gain.
563 if ( CKEDITOR.env.ie || isInline ) {
564 // For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event.
565 // For the rest, a more frequent check is required for each selection change made.
566 if ( isMSSelection )
567 editable.attachListener( editable, 'beforedeactivate', saveSel, null, null, -1 );
568 else
569 editable.attachListener( editor, 'selectionCheck', saveSel, null, null, -1 );
570
571 // Lock the selection and mark it to be restored.
572 // On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be
573 // fired when nested editable is blurred.
574 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusOut' : 'blur', function() {
575 editor.lockSelection( lastSel );
576 restoreSel = 1;
577 }, null, null, -1 );
578
579 // Disable selection restoring when clicking in.
580 editable.attachListener( editable, 'mousedown', function() {
581 restoreSel = 0;
582 } );
583 }
584
585 // The following selection-related fixes only apply to classic (`iframe`-based) editable.
586 if ( CKEDITOR.env.ie && !isInline ) {
587 var scroll;
588 editable.attachListener( editable, 'mousedown', function( evt ) {
589 // IE scrolls document to top on right mousedown
590 // when editor has no focus, remember this scroll
591 // position and revert it before context menu opens. (#5778)
592 if ( evt.data.$.button == 2 ) {
593 var sel = editor.document.getSelection();
594 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
595 scroll = editor.window.getScrollPosition();
596 }
597 } );
598
599 editable.attachListener( editable, 'mouseup', function( evt ) {
600 // Restore recorded scroll position when needed on right mouseup.
601 if ( evt.data.$.button == 2 && scroll ) {
602 editor.document.$.documentElement.scrollLeft = scroll.x;
603 editor.document.$.documentElement.scrollTop = scroll.y;
604 }
605 scroll = null;
606 } );
607
608 // When content doc is in standards mode, IE doesn't focus the editor when
609 // clicking at the region below body (on html element) content, we emulate
610 // the normal behavior on old IEs. (#1659, #7932)
611 if ( doc.$.compatMode != 'BackCompat' ) {
612 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
613 html.on( 'mousedown', function( evt ) {
614 evt = evt.data;
615
616 // Expand the text range along with mouse move.
617 function onHover( evt ) {
618 evt = evt.data.$;
619 if ( textRng ) {
620 // Read the current cursor.
621 var rngEnd = body.$.createTextRange();
622
623 moveRangeToPoint( rngEnd, evt.clientX, evt.clientY );
624
625 // Handle drag directions.
626 textRng.setEndPoint(
627 startRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ?
628 'EndToEnd' : 'StartToStart', rngEnd );
629
630 // Update selection with new range.
631 textRng.select();
632 }
633 }
634
635 function removeListeners() {
636 outerDoc.removeListener( 'mouseup', onSelectEnd );
637 html.removeListener( 'mouseup', onSelectEnd );
638 }
639
640 function onSelectEnd() {
641 html.removeListener( 'mousemove', onHover );
642 removeListeners();
643
644 // Make it in effect on mouse up. (#9022)
645 textRng.select();
646 }
647
648
649 // We're sure that the click happens at the region
650 // below body, but not on scrollbar.
651 if ( evt.getTarget().is( 'html' ) &&
652 evt.$.y < html.$.clientHeight &&
653 evt.$.x < html.$.clientWidth ) {
654 // Start to build the text range.
655 var textRng = body.$.createTextRange();
656 moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY );
657
658 // Records the dragging start of the above text range.
659 var startRng = textRng.duplicate();
660
661 html.on( 'mousemove', onHover );
662 outerDoc.on( 'mouseup', onSelectEnd );
663 html.on( 'mouseup', onSelectEnd );
664 }
665 } );
666 }
667
668 // It's much simpler for IE8+, we just need to reselect the reported range.
669 // This hack does not work on IE>=11 because there's no old selection&range APIs.
670 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
671 html.on( 'mousedown', function( evt ) {
672 if ( evt.data.getTarget().is( 'html' ) ) {
673 // Limit the text selection mouse move inside of editable. (#9715)
674 outerDoc.on( 'mouseup', onSelectEnd );
675 html.on( 'mouseup', onSelectEnd );
676 }
677 } );
678 }
679 }
680 }
681
682 // We check the selection change:
683 // 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code)
684 // 2. After the accomplish of keyboard and mouse events.
685 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
686 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
687 // Always fire the selection change on focus gain.
688 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
689 // we need synchronization between those listeners to not lost cached editor._.previousActive property
690 // (which is updated on selectionCheck).
691 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
692 editor.forceNextSelectionCheck();
693 editor.selectionChange( 1 );
694 } );
695
696 // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
697 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
698 // has to be initialized in editable, but for mouseup we listen on document element.
699 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
700 var mouseDown;
701 editable.attachListener( editable, 'mousedown', function() {
702 mouseDown = 1;
703 } );
704 editable.attachListener( doc.getDocumentElement(), 'mouseup', function() {
705 if ( mouseDown )
706 checkSelectionChangeTimeout.call( editor );
707 mouseDown = 0;
708 } );
709 }
710 // In all other cases listen on simple mouseup over editable, as we did before #9699.
711 //
712 // Use document instead of editable in non-IEs for observing mouseup
713 // since editable won't fire the event if selection process started within iframe and ended out
714 // of the editor (#9851).
715 else {
716 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
717 }
718
719 if ( CKEDITOR.env.webkit ) {
720 // Before keystroke is handled by editor, check to remove the filling char.
721 editable.attachListener( doc, 'keydown', function( evt ) {
722 var key = evt.data.getKey();
723 // Remove the filling char before some keys get
724 // executed, so they'll not get blocked by it.
725 switch ( key ) {
726 case 13: // ENTER
727 case 33: // PAGEUP
728 case 34: // PAGEDOWN
729 case 35: // HOME
730 case 36: // END
731 case 37: // LEFT-ARROW
732 case 39: // RIGHT-ARROW
733 case 8: // BACKSPACE
734 case 45: // INS
735 case 46: // DEl
736 removeFillingCharSequenceNode( editable );
737 }
738
739 }, null, null, -1 );
740 }
741
742 // Automatically select non-editable element when navigating into
743 // it by left/right or backspace/del keys.
744 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
745
746 function moveRangeToPoint( range, x, y ) {
747 // Error prune in IE7. (#9034, #9110)
748 try {
749 range.moveToPoint( x, y );
750 } catch ( e ) {}
751 }
752
753 function removeListeners() {
754 outerDoc.removeListener( 'mouseup', onSelectEnd );
755 html.removeListener( 'mouseup', onSelectEnd );
756 }
757
758 function onSelectEnd() {
759 removeListeners();
760
761 // The event is not fired when clicking on the scrollbars,
762 // so we can safely check the following to understand
763 // whether the empty space following <body> has been clicked.
764 var sel = CKEDITOR.document.$.selection,
765 range = sel.createRange();
766
767 // The selection range is reported on host, but actually it should applies to the content doc.
768 if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ )
769 range.select();
770 }
771 } );
772
773 editor.on( 'setData', function() {
774 // Invalidate locked selection when unloading DOM.
775 // (#9521, #5217#comment:32 and #11500#comment:11)
776 editor.unlockSelection();
777
778 // Webkit's selection will mess up after the data loading.
779 if ( CKEDITOR.env.webkit )
780 clearSelection();
781 } );
782
783 // Catch all the cases which above setData listener couldn't catch.
784 // For example: switching to source mode and destroying editor.
785 editor.on( 'contentDomUnload', function() {
786 editor.unlockSelection();
787 } );
788
789 // IE9 might cease to work if there's an object selection inside the iframe (#7639).
790 if ( CKEDITOR.env.ie9Compat )
791 editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
792
793 // Check selection change on data reload.
794 editor.on( 'dataReady', function() {
795 // Clean up fake selection after setting data.
796 delete editor._.fakeSelection;
797 delete editor._.hiddenSelectionContainer;
798
799 editor.selectionChange( 1 );
800 } );
801
802 // When loaded data are ready check whether hidden selection container was not loaded.
803 editor.on( 'loadSnapshot', function() {
804 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
805 // TODO replace with el.find() which will be introduced in #9764,
806 // because it may happen that hidden sel container won't be the last element.
807 last = editor.editable().getLast( isElement );
808
809 if ( last && last.hasAttribute( 'data-cke-hidden-sel' ) ) {
810 last.remove();
811
812 // Firefox does a very unfortunate thing. When a non-editable element is the only
813 // element in the editable, when we remove the hidden selection container, Firefox
814 // will insert a bogus <br> at the beginning of the editable...
815 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201
816 //
817 // This behavior is never desired because this <br> pushes the content lower, but in
818 // this case it is especially dangerous, because it happens when a bookmark is being restored.
819 // Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2
820 // what results in errors.
821 //
822 // So... let's revert what Firefox broke.
823 if ( CKEDITOR.env.gecko ) {
824 var first = editor.editable().getFirst( isElement );
825 if ( first && first.is( 'br' ) && first.getAttribute( '_moz_editor_bogus_node' ) ) {
826 first.remove();
827 }
828 }
829 }
830 }, null, null, 100 );
831
832 editor.on( 'key', function( evt ) {
833 if ( editor.mode != 'wysiwyg' )
834 return;
835
836 var sel = editor.getSelection();
837 if ( !sel.isFake )
838 return;
839
840 var handler = fakeSelectionDefaultKeystrokeHandlers[ evt.data.keyCode ];
841 if ( handler )
842 return handler( { editor: editor, selected: sel.getSelectedElement(), selection: sel, keyEvent: evt } );
843 } );
844
845 function clearSelection() {
846 var sel = editor.getSelection();
847 sel && sel.removeAllRanges();
848 }
849 } );
850
851 // On WebKit only, we need a special "filling" char on some situations
852 // (#1272). Here we set the events that should invalidate that char.
853 if ( CKEDITOR.env.webkit ) {
854 CKEDITOR.on( 'instanceReady', function( evt ) {
855 var editor = evt.editor;
856
857 editor.on( 'selectionChange', function() {
858 checkFillingCharSequenceNodeReady( editor.editable() );
859 }, null, null, -1 );
860
861 editor.on( 'beforeSetMode', function() {
862 removeFillingCharSequenceNode( editor.editable() );
863 }, null, null, -1 );
864
865 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
866 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
867 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816).
868 editor.on( 'getSnapshot', function( evt ) {
869 if ( evt.data ) {
870 evt.data = removeFillingCharSequenceString( evt.data );
871 }
872 }, editor, null, 20 );
873
874 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
875 // instead of #getData because once removed, FCSeq may leave an empty element,
876 // which should be pruned by the dataProcessor (#13816).
877 // Note: Used low priority to filter when dataProcessor works on strings,
878 // not pseudo–DOM.
879 editor.on( 'toDataFormat', function( evt ) {
880 evt.data.dataValue = removeFillingCharSequenceString( evt.data.dataValue );
881 }, null, null, 0 );
882 } );
883 }
884
885 /**
886 * Check the selection change in editor and potentially fires
887 * the {@link CKEDITOR.editor#event-selectionChange} event.
888 *
889 * @method
890 * @member CKEDITOR.editor
891 * @param {Boolean} [checkNow=false] Force the check to happen immediately
892 * instead of coming with a timeout delay (default).
893 */
894 CKEDITOR.editor.prototype.selectionChange = function( checkNow ) {
895 ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this );
896 };
897
898 /**
899 * Retrieve the editor selection in scope of editable element.
900 *
901 * **Note:** Since the native browser selection provides only one single
902 * selection at a time per document, so if editor's editable element has lost focus,
903 * this method will return a null value unless the {@link CKEDITOR.editor#lockSelection}
904 * has been called beforehand so the saved selection is retrieved.
905 *
906 * var selection = CKEDITOR.instances.editor1.getSelection();
907 * alert( selection.getType() );
908 *
909 * @method
910 * @member CKEDITOR.editor
911 * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one.
912 * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment.
913 */
914 CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) {
915
916 // Check if there exists a locked or fake selection.
917 if ( ( this._.savedSelection || this._.fakeSelection ) && !forceRealSelection )
918 return this._.savedSelection || this._.fakeSelection;
919
920 // Editable element might be absent or editor might not be in a wysiwyg mode.
921 var editable = this.editable();
922 return editable && this.mode == 'wysiwyg' ? new CKEDITOR.dom.selection( editable ) : null;
923 };
924
925 /**
926 * Locks the selection made in the editor in order to make it possible to
927 * manipulate it without browser interference. A locked selection is
928 * cached and remains unchanged until it is released with the
929 * {@link CKEDITOR.editor#unlockSelection} method.
930 *
931 * @method
932 * @member CKEDITOR.editor
933 * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked.
934 * @returns {Boolean} `true` if selection was locked.
935 */
936 CKEDITOR.editor.prototype.lockSelection = function( sel ) {
937 sel = sel || this.getSelection( 1 );
938 if ( sel.getType() != CKEDITOR.SELECTION_NONE ) {
939 !sel.isLocked && sel.lock();
940 this._.savedSelection = sel;
941 return true;
942 }
943 return false;
944 };
945
946 /**
947 * Unlocks the selection made in the editor and locked with the
948 * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection
949 * is no longer cached and can be changed.
950 *
951 * @method
952 * @member CKEDITOR.editor
953 * @param {Boolean} [restore] If set to `true`, the selection is
954 * restored back to the selection saved earlier by using the
955 * {@link CKEDITOR.dom.selection#lock} method.
956 */
957 CKEDITOR.editor.prototype.unlockSelection = function( restore ) {
958 var sel = this._.savedSelection;
959 if ( sel ) {
960 sel.unlock( restore );
961 delete this._.savedSelection;
962 return true;
963 }
964
965 return false;
966 };
967
968 /**
969 * @method
970 * @member CKEDITOR.editor
971 * @todo
972 */
973 CKEDITOR.editor.prototype.forceNextSelectionCheck = function() {
974 delete this._.selectionPreviousPath;
975 };
976
977 /**
978 * Gets the current selection in context of the document's body element.
979 *
980 * var selection = CKEDITOR.instances.editor1.document.getSelection();
981 * alert( selection.getType() );
982 *
983 * @method
984 * @member CKEDITOR.dom.document
985 * @returns {CKEDITOR.dom.selection} A selection object.
986 */
987 CKEDITOR.dom.document.prototype.getSelection = function() {
988 return new CKEDITOR.dom.selection( this );
989 };
990
991 /**
992 * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}.
993 *
994 * @method
995 * @returns {CKEDITOR.dom.selection}
996 * @member CKEDITOR.dom.range
997 */
998 CKEDITOR.dom.range.prototype.select = function() {
999 var sel = this.root instanceof CKEDITOR.editable ? this.root.editor.getSelection() : new CKEDITOR.dom.selection( this.root );
1000
1001 sel.selectRanges( [ this ] );
1002
1003 return sel;
1004 };
1005
1006 /**
1007 * No selection.
1008 *
1009 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
1010 * alert( 'Nothing is selected' );
1011 *
1012 * @readonly
1013 * @property {Number} [=1]
1014 * @member CKEDITOR
1015 */
1016 CKEDITOR.SELECTION_NONE = 1;
1017
1018 /**
1019 * A text or a collapsed selection.
1020 *
1021 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1022 * alert( 'A text is selected' );
1023 *
1024 * @readonly
1025 * @property {Number} [=2]
1026 * @member CKEDITOR
1027 */
1028 CKEDITOR.SELECTION_TEXT = 2;
1029
1030 /**
1031 * Element selection.
1032 *
1033 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
1034 * alert( 'An element is selected' );
1035 *
1036 * @readonly
1037 * @property {Number} [=3]
1038 * @member CKEDITOR
1039 */
1040 CKEDITOR.SELECTION_ELEMENT = 3;
1041
1042 var isMSSelection = typeof window.getSelection != 'function',
1043 nextRev = 1;
1044
1045 /**
1046 * Manipulates the selection within a DOM element. If the current browser selection
1047 * spans outside of the element, an empty selection object is returned.
1048 *
1049 * Despite the fact that selection's constructor allows to create selection instances,
1050 * usually it's better to get selection from the editor instance:
1051 *
1052 * var sel = editor.getSelection();
1053 *
1054 * See {@link CKEDITOR.editor#getSelection}.
1055 *
1056 * @class
1057 * @constructor Creates a selection class instance.
1058 *
1059 * // Selection scoped in document.
1060 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document );
1061 *
1062 * // Selection scoped in element with 'editable' id.
1063 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document.getById( 'editable' ) );
1064 *
1065 * // Cloning selection.
1066 * var clone = new CKEDITOR.dom.selection( sel );
1067 *
1068 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element/CKEDITOR.dom.selection} target
1069 * The DOM document/element that the DOM selection is restrained to. Only selection which spans
1070 * within the target element is considered as valid.
1071 *
1072 * If {@link CKEDITOR.dom.selection} is passed, then its clone will be created.
1073 */
1074 CKEDITOR.dom.selection = function( target ) {
1075 // Target is a selection - clone it.
1076 if ( target instanceof CKEDITOR.dom.selection ) {
1077 var selection = target;
1078 target = target.root;
1079 }
1080
1081 var isElement = target instanceof CKEDITOR.dom.element,
1082 root;
1083
1084 this.rev = selection ? selection.rev : nextRev++;
1085 this.document = target instanceof CKEDITOR.dom.document ? target : target.getDocument();
1086 this.root = root = isElement ? target : this.document.getBody();
1087 this.isLocked = 0;
1088 this._ = {
1089 cache: {}
1090 };
1091
1092 // Clone selection.
1093 if ( selection ) {
1094 CKEDITOR.tools.extend( this._.cache, selection._.cache );
1095 this.isFake = selection.isFake;
1096 this.isLocked = selection.isLocked;
1097 return this;
1098 }
1099
1100 // Check whether browser focus is really inside of the editable element.
1101
1102 var nativeSel = this.getNative(),
1103 rangeParent,
1104 range;
1105
1106 if ( nativeSel ) {
1107 if ( nativeSel.getRangeAt ) {
1108 range = nativeSel.rangeCount && nativeSel.getRangeAt( 0 );
1109 rangeParent = range && new CKEDITOR.dom.node( range.commonAncestorContainer );
1110 }
1111 // For old IEs.
1112 else {
1113 // Sometimes, mostly when selection is close to the table or hr,
1114 // IE throws "Unspecified error".
1115 try {
1116 range = nativeSel.createRange();
1117 } catch ( err ) {}
1118 rangeParent = range && CKEDITOR.dom.element.get( range.item && range.item( 0 ) || range.parentElement() );
1119 }
1120 }
1121
1122 // Selection out of concerned range, empty the selection.
1123 // TODO check whether this condition cannot be reverted to its old
1124 // form (commented out) after we closed #10438.
1125 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1126 if ( !(
1127 rangeParent &&
1128 ( rangeParent.type == CKEDITOR.NODE_ELEMENT || rangeParent.type == CKEDITOR.NODE_TEXT ) &&
1129 ( this.root.equals( rangeParent ) || this.root.contains( rangeParent ) )
1130 ) ) {
1131
1132 this._.cache.type = CKEDITOR.SELECTION_NONE;
1133 this._.cache.startElement = null;
1134 this._.cache.selectedElement = null;
1135 this._.cache.selectedText = '';
1136 this._.cache.ranges = new CKEDITOR.dom.rangeList();
1137 }
1138
1139 return this;
1140 };
1141
1142 var styleObjectElements = { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1,
1143 a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 };
1144
1145 // #13816
1146 var fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
1147 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' );
1148
1149 CKEDITOR.tools.extend( CKEDITOR.dom.selection, {
1150 _removeFillingCharSequenceString: removeFillingCharSequenceString,
1151 _createFillingCharSequenceNode: createFillingCharSequenceNode,
1152
1153 /**
1154 * The sequence used in a WebKit-based browser to create a Filling Character. By default it is
1155 * a string of 7 zero-width space characters (U+200B).
1156 *
1157 * @since 4.5.7
1158 * @readonly
1159 * @property {String}
1160 */
1161 FILLING_CHAR_SEQUENCE: fillingCharSequence
1162 } );
1163
1164 CKEDITOR.dom.selection.prototype = {
1165 /**
1166 * Gets the native selection object from the browser.
1167 *
1168 * var selection = editor.getSelection().getNative();
1169 *
1170 * @returns {Object} The native browser selection object.
1171 */
1172 getNative: function() {
1173 if ( this._.cache.nativeSel !== undefined )
1174 return this._.cache.nativeSel;
1175
1176 return ( this._.cache.nativeSel = isMSSelection ? this.document.$.selection : this.document.getWindow().$.getSelection() );
1177 },
1178
1179 /**
1180 * Gets the type of the current selection. The following values are
1181 * available:
1182 *
1183 * * {@link CKEDITOR#SELECTION_NONE} (1): No selection.
1184 * * {@link CKEDITOR#SELECTION_TEXT} (2): A text or a collapsed selection is selected.
1185 * * {@link CKEDITOR#SELECTION_ELEMENT} (3): An element is selected.
1186 *
1187 * Example:
1188 *
1189 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1190 * alert( 'A text is selected' );
1191 *
1192 * @method
1193 * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE},
1194 * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}.
1195 */
1196 getType: isMSSelection ?
1197 function() {
1198 var cache = this._.cache;
1199 if ( cache.type )
1200 return cache.type;
1201
1202 var type = CKEDITOR.SELECTION_NONE;
1203
1204 try {
1205 var sel = this.getNative(),
1206 ieType = sel.type;
1207
1208 if ( ieType == 'Text' )
1209 type = CKEDITOR.SELECTION_TEXT;
1210
1211 if ( ieType == 'Control' )
1212 type = CKEDITOR.SELECTION_ELEMENT;
1213
1214 // It is possible that we can still get a text range
1215 // object even when type == 'None' is returned by IE.
1216 // So we'd better check the object returned by
1217 // createRange() rather than by looking at the type.
1218 if ( sel.createRange().parentElement() )
1219 type = CKEDITOR.SELECTION_TEXT;
1220 } catch ( e ) {}
1221
1222 return ( cache.type = type );
1223 } : function() {
1224 var cache = this._.cache;
1225 if ( cache.type )
1226 return cache.type;
1227
1228 var type = CKEDITOR.SELECTION_TEXT;
1229
1230 var sel = this.getNative();
1231
1232 if ( !( sel && sel.rangeCount ) )
1233 type = CKEDITOR.SELECTION_NONE;
1234 else if ( sel.rangeCount == 1 ) {
1235 // Check if the actual selection is a control (IMG,
1236 // TABLE, HR, etc...).
1237
1238 var range = sel.getRangeAt( 0 ),
1239 startContainer = range.startContainer;
1240
1241 if ( startContainer == range.endContainer && startContainer.nodeType == 1 &&
1242 ( range.endOffset - range.startOffset ) == 1 &&
1243 styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) {
1244 type = CKEDITOR.SELECTION_ELEMENT;
1245 }
1246
1247 }
1248
1249 return ( cache.type = type );
1250 },
1251
1252 /**
1253 * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection.
1254 *
1255 * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns
1256 * one range for each table cell when one or more table rows are selected.
1257 *
1258 * var ranges = selection.getRanges();
1259 * alert( ranges.length );
1260 *
1261 * @method
1262 * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only.
1263 * @returns {Array} Range instances that represent the current selection.
1264 */
1265 getRanges: ( function() {
1266 var func = isMSSelection ? ( function() {
1267 function getNodeIndex( node ) {
1268 return new CKEDITOR.dom.node( node ).getIndex();
1269 }
1270
1271 // Finds the container and offset for a specific boundary
1272 // of an IE range.
1273 var getBoundaryInformation = function( range, start ) {
1274 // Creates a collapsed range at the requested boundary.
1275 range = range.duplicate();
1276 range.collapse( start );
1277
1278 // Gets the element that encloses the range entirely.
1279 var parent = range.parentElement();
1280
1281 // Empty parent element, e.g. <i>^</i>
1282 if ( !parent.hasChildNodes() )
1283 return { container: parent, offset: 0 };
1284
1285 var siblings = parent.children,
1286 child, sibling,
1287 testRange = range.duplicate(),
1288 startIndex = 0,
1289 endIndex = siblings.length - 1,
1290 index = -1,
1291 position, distance, container;
1292
1293 // Binary search over all element childs to test the range to see whether
1294 // range is right on the boundary of one element.
1295 while ( startIndex <= endIndex ) {
1296 index = Math.floor( ( startIndex + endIndex ) / 2 );
1297 child = siblings[ index ];
1298 testRange.moveToElementText( child );
1299 position = testRange.compareEndPoints( 'StartToStart', range );
1300
1301 if ( position > 0 )
1302 endIndex = index - 1;
1303 else if ( position < 0 )
1304 startIndex = index + 1;
1305 else
1306 return { container: parent, offset: getNodeIndex( child ) };
1307 }
1308
1309 // All childs are text nodes,
1310 // or to the right hand of test range are all text nodes. (#6992)
1311 if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
1312 // Adapt test range to embrace the entire parent contents.
1313 testRange.moveToElementText( parent );
1314 testRange.setEndPoint( 'StartToStart', range );
1315
1316 // IE report line break as CRLF with range.text but
1317 // only LF with textnode.nodeValue, normalize them to avoid
1318 // breaking character counting logic below. (#3949)
1319 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1320
1321 siblings = parent.childNodes;
1322
1323 // Actual range anchor right beside test range at the boundary of text node.
1324 if ( !distance ) {
1325 child = siblings[ siblings.length - 1 ];
1326
1327 if ( child.nodeType != CKEDITOR.NODE_TEXT )
1328 return { container: parent, offset: siblings.length };
1329 else
1330 return { container: child, offset: child.nodeValue.length };
1331 }
1332
1333 // Start the measuring until distance overflows, meanwhile count the text nodes.
1334 var i = siblings.length;
1335 while ( distance > 0 && i > 0 ) {
1336 sibling = siblings[ --i ];
1337 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
1338 container = sibling;
1339 distance -= sibling.nodeValue.length;
1340 }
1341 }
1342
1343 return { container: container, offset: -distance };
1344 }
1345 // Test range was one offset beyond OR behind the anchored text node.
1346 else {
1347 // Adapt one side of test range to the actual range
1348 // for measuring the offset between them.
1349 testRange.collapse( position > 0 ? true : false );
1350 testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range );
1351
1352 // IE report line break as CRLF with range.text but
1353 // only LF with textnode.nodeValue, normalize them to avoid
1354 // breaking character counting logic below. (#3949)
1355 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1356
1357 // Actual range anchor right beside test range at the inner boundary of text node.
1358 if ( !distance )
1359 return { container: parent, offset: getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) };
1360
1361 // Start the measuring until distance overflows, meanwhile count the text nodes.
1362 while ( distance > 0 ) {
1363 try {
1364 sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ];
1365 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
1366 distance -= sibling.nodeValue.length;
1367 container = sibling;
1368 }
1369 child = sibling;
1370 }
1371 // Measurement in IE could be somtimes wrong because of <select> element. (#4611)
1372 catch ( e ) {
1373 return { container: parent, offset: getNodeIndex( child ) };
1374 }
1375 }
1376
1377 return { container: container, offset: position > 0 ? -distance : container.nodeValue.length + distance };
1378 }
1379 };
1380
1381 return function() {
1382 // IE doesn't have range support (in the W3C way), so we
1383 // need to do some magic to transform selections into
1384 // CKEDITOR.dom.range instances.
1385
1386 var sel = this.getNative(),
1387 nativeRange = sel && sel.createRange(),
1388 type = this.getType(),
1389 range;
1390
1391 if ( !sel )
1392 return [];
1393
1394 if ( type == CKEDITOR.SELECTION_TEXT ) {
1395 range = new CKEDITOR.dom.range( this.root );
1396
1397 var boundaryInfo = getBoundaryInformation( nativeRange, true );
1398 range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1399
1400 boundaryInfo = getBoundaryInformation( nativeRange );
1401 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1402
1403 // Correct an invalid IE range case on empty list item. (#5850)
1404 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
1405 range.collapse();
1406
1407 return [ range ];
1408 } else if ( type == CKEDITOR.SELECTION_ELEMENT ) {
1409 var retval = [];
1410
1411 for ( var i = 0; i < nativeRange.length; i++ ) {
1412 var element = nativeRange.item( i ),
1413 parentElement = element.parentNode,
1414 j = 0;
1415
1416 range = new CKEDITOR.dom.range( this.root );
1417
1418 for ( ; j < parentElement.childNodes.length && parentElement.childNodes[ j ] != element; j++ ) {
1419
1420 }
1421
1422 range.setStart( new CKEDITOR.dom.node( parentElement ), j );
1423 range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 );
1424 retval.push( range );
1425 }
1426
1427 return retval;
1428 }
1429
1430 return [];
1431 };
1432 } )() :
1433 function() {
1434 // On browsers implementing the W3C range, we simply
1435 // tranform the native ranges in CKEDITOR.dom.range
1436 // instances.
1437
1438 var ranges = [],
1439 range,
1440 sel = this.getNative();
1441
1442 if ( !sel )
1443 return ranges;
1444
1445 for ( var i = 0; i < sel.rangeCount; i++ ) {
1446 var nativeRange = sel.getRangeAt( i );
1447
1448 range = new CKEDITOR.dom.range( this.root );
1449
1450 range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset );
1451 range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset );
1452 ranges.push( range );
1453 }
1454 return ranges;
1455 };
1456
1457 return function( onlyEditables ) {
1458 var cache = this._.cache,
1459 ranges = cache.ranges;
1460
1461 if ( !ranges )
1462 cache.ranges = ranges = new CKEDITOR.dom.rangeList( func.call( this ) );
1463
1464 if ( !onlyEditables )
1465 return ranges;
1466
1467 // Split range into multiple by read-only nodes.
1468 // Clone ranges array to avoid changing cached ranges (#11493).
1469 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
1470 };
1471 } )(),
1472
1473 /**
1474 * Gets the DOM element in which the selection starts.
1475 *
1476 * var element = editor.getSelection().getStartElement();
1477 * alert( element.getName() );
1478 *
1479 * @returns {CKEDITOR.dom.element} The element at the beginning of the selection.
1480 */
1481 getStartElement: function() {
1482 var cache = this._.cache;
1483 if ( cache.startElement !== undefined )
1484 return cache.startElement;
1485
1486 var node;
1487
1488 switch ( this.getType() ) {
1489 case CKEDITOR.SELECTION_ELEMENT:
1490 return this.getSelectedElement();
1491
1492 case CKEDITOR.SELECTION_TEXT:
1493
1494 var range = this.getRanges()[ 0 ];
1495
1496 if ( range ) {
1497 if ( !range.collapsed ) {
1498 range.optimize();
1499
1500 // Decrease the range content to exclude particial
1501 // selected node on the start which doesn't have
1502 // visual impact. ( #3231 )
1503 while ( 1 ) {
1504 var startContainer = range.startContainer,
1505 startOffset = range.startOffset;
1506 // Limit the fix only to non-block elements.(#3950)
1507 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
1508 range.setStartAfter( startContainer );
1509 else
1510 break;
1511 }
1512
1513 node = range.startContainer;
1514
1515 if ( node.type != CKEDITOR.NODE_ELEMENT )
1516 return node.getParent();
1517
1518 node = node.getChild( range.startOffset );
1519
1520 if ( !node || node.type != CKEDITOR.NODE_ELEMENT )
1521 node = range.startContainer;
1522 else {
1523 var child = node.getFirst();
1524 while ( child && child.type == CKEDITOR.NODE_ELEMENT ) {
1525 node = child;
1526 child = child.getFirst();
1527 }
1528 }
1529 } else {
1530 node = range.startContainer;
1531 if ( node.type != CKEDITOR.NODE_ELEMENT )
1532 node = node.getParent();
1533 }
1534
1535 node = node.$;
1536 }
1537 }
1538
1539 return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null );
1540 },
1541
1542 /**
1543 * Gets the currently selected element.
1544 *
1545 * var element = editor.getSelection().getSelectedElement();
1546 * alert( element.getName() );
1547 *
1548 * @returns {CKEDITOR.dom.element} The selected element. Null if no
1549 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1550 */
1551 getSelectedElement: function() {
1552 var cache = this._.cache;
1553 if ( cache.selectedElement !== undefined )
1554 return cache.selectedElement;
1555
1556 var self = this;
1557
1558 var node = CKEDITOR.tools.tryThese(
1559 // Is it native IE control type selection?
1560 function() {
1561 return self.getNative().createRange().item( 0 );
1562 },
1563 // Figure it out by checking if there's a single enclosed
1564 // node of the range.
1565 function() {
1566 var range = self.getRanges()[ 0 ].clone(),
1567 enclosed, selected;
1568
1569 // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul>
1570 for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) && styleObjectElements[ enclosed.getName() ] && ( selected = enclosed ) ); i-- ) {
1571 // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>]
1572 range.shrink( CKEDITOR.SHRINK_ELEMENT );
1573 }
1574
1575 return selected && selected.$;
1576 }
1577 );
1578
1579 return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null );
1580 },
1581
1582 /**
1583 * Retrieves the text contained within the range. An empty string is returned for non-text selection.
1584 *
1585 * var text = editor.getSelection().getSelectedText();
1586 * alert( text );
1587 *
1588 * @since 3.6.1
1589 * @returns {String} A string of text within the current selection.
1590 */
1591 getSelectedText: function() {
1592 var cache = this._.cache;
1593 if ( cache.selectedText !== undefined )
1594 return cache.selectedText;
1595
1596 var nativeSel = this.getNative(),
1597 text = isMSSelection ? nativeSel.type == 'Control' ? '' : nativeSel.createRange().text : nativeSel.toString();
1598
1599 return ( cache.selectedText = text );
1600 },
1601
1602 /**
1603 * Locks the selection made in the editor in order to make it possible to
1604 * manipulate it without browser interference. A locked selection is
1605 * cached and remains unchanged until it is released with the {@link #unlock} method.
1606 *
1607 * editor.getSelection().lock();
1608 */
1609 lock: function() {
1610 // Call all cacheable function.
1611 this.getRanges();
1612 this.getStartElement();
1613 this.getSelectedElement();
1614 this.getSelectedText();
1615
1616 // The native selection is not available when locked.
1617 this._.cache.nativeSel = null;
1618
1619 this.isLocked = 1;
1620 },
1621
1622 /**
1623 * @todo
1624 */
1625 unlock: function( restore ) {
1626 if ( !this.isLocked )
1627 return;
1628
1629 if ( restore ) {
1630 var selectedElement = this.getSelectedElement(),
1631 ranges = !selectedElement && this.getRanges(),
1632 faked = this.isFake;
1633 }
1634
1635 this.isLocked = 0;
1636 this.reset();
1637
1638 if ( restore ) {
1639 // Saved selection may be outdated (e.g. anchored in offline nodes).
1640 // Avoid getting broken by such.
1641 var common = selectedElement || ranges[ 0 ] && ranges[ 0 ].getCommonAncestor();
1642 if ( !( common && common.getAscendant( 'body', 1 ) ) )
1643 return;
1644
1645 if ( faked )
1646 this.fake( selectedElement );
1647 else if ( selectedElement )
1648 this.selectElement( selectedElement );
1649 else
1650 this.selectRanges( ranges );
1651 }
1652 },
1653
1654 /**
1655 * Clears the selection cache.
1656 *
1657 * editor.getSelection().reset();
1658 */
1659 reset: function() {
1660 this._.cache = {};
1661 this.isFake = 0;
1662
1663 var editor = this.root.editor;
1664
1665 // Invalidate any fake selection available in the editor.
1666 if ( editor && editor._.fakeSelection ) {
1667 // Test whether this selection is the one that was
1668 // faked or its clone.
1669 if ( this.rev == editor._.fakeSelection.rev ) {
1670 delete editor._.fakeSelection;
1671
1672 removeHiddenSelectionContainer( editor );
1673 }
1674 else {
1675 CKEDITOR.warn( 'selection-fake-reset' );
1676 }
1677 }
1678
1679 this.rev = nextRev++;
1680 },
1681
1682 /**
1683 * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element.
1684 *
1685 * var element = editor.document.getById( 'sampleElement' );
1686 * editor.getSelection().selectElement( element );
1687 *
1688 * @param {CKEDITOR.dom.element} element The element to enclose in the selection.
1689 */
1690 selectElement: function( element ) {
1691 var range = new CKEDITOR.dom.range( this.root );
1692 range.setStartBefore( element );
1693 range.setEndAfter( element );
1694 this.selectRanges( [ range ] );
1695 },
1696
1697 /**
1698 * Clears the original selection and adds the specified ranges to the document selection.
1699 *
1700 * // Move selection to the end of the editable element.
1701 * var range = editor.createRange();
1702 * range.moveToPosition( range.root, CKEDITOR.POSITION_BEFORE_END );
1703 * editor.getSelection().selectRanges( [ ranges ] );
1704 *
1705 * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances
1706 * representing ranges to be added to the document.
1707 */
1708 selectRanges: function( ranges ) {
1709 var editor = this.root.editor,
1710 hadHiddenSelectionContainer = editor && editor._.hiddenSelectionContainer;
1711
1712 this.reset();
1713
1714 // Check if there's a hiddenSelectionContainer in editable at some index.
1715 // Some ranges may be anchored after the hiddenSelectionContainer and,
1716 // once the container is removed while resetting the selection, they
1717 // may need new endOffset (one element less within the range) (#11021 #11393).
1718 if ( hadHiddenSelectionContainer )
1719 fixRangesAfterHiddenSelectionContainer( ranges, this.root );
1720
1721 if ( !ranges.length )
1722 return;
1723
1724 // Refresh the locked selection.
1725 if ( this.isLocked ) {
1726 // making a new DOM selection will force the focus on editable in certain situation,
1727 // we have to save the currently focused element for later recovery.
1728 var focused = CKEDITOR.document.getActive();
1729 this.unlock();
1730 this.selectRanges( ranges );
1731 this.lock();
1732 // Return to the previously focused element.
1733 focused && !focused.equals( this.root ) && focused.focus();
1734 return;
1735 }
1736
1737 // Handle special case - automatic fake selection on non-editable elements.
1738 var receiver = getNonEditableFakeSelectionReceiver( ranges );
1739
1740 if ( receiver ) {
1741 this.fake( receiver );
1742 return;
1743 }
1744
1745 if ( isMSSelection ) {
1746 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
1747 fillerTextRegex = /\ufeff|\u00a0/,
1748 nonCells = { table: 1, tbody: 1, tr: 1 };
1749
1750 if ( ranges.length > 1 ) {
1751 // IE doesn't accept multiple ranges selection, so we join all into one.
1752 var last = ranges[ ranges.length - 1 ];
1753 ranges[ 0 ].setEnd( last.endContainer, last.endOffset );
1754 }
1755
1756 var range = ranges[ 0 ];
1757 var collapsed = range.collapsed,
1758 isStartMarkerAlone, dummySpan, ieRange;
1759
1760 // Try to make a object selection, be careful with selecting phase element in IE
1761 // will breaks the selection in non-framed environment.
1762 var selected = range.getEnclosedNode();
1763 if ( selected && selected.type == CKEDITOR.NODE_ELEMENT && selected.getName() in styleObjectElements &&
1764 !( selected.is( 'a' ) && selected.getText() ) ) {
1765 try {
1766 ieRange = selected.$.createControlRange();
1767 ieRange.addElement( selected.$ );
1768 ieRange.select();
1769 return;
1770 } catch ( er ) {}
1771 }
1772
1773 // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
1774 // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>...
1775 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
1776 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
1777 range.shrink( CKEDITOR.NODE_ELEMENT, true );
1778 // The range might get collapsed (#7975). Update cached variable.
1779 collapsed = range.collapsed;
1780 }
1781
1782 var bookmark = range.createBookmark();
1783
1784 // Create marker tags for the start and end boundaries.
1785 var startNode = bookmark.startNode;
1786
1787 var endNode;
1788 if ( !collapsed )
1789 endNode = bookmark.endNode;
1790
1791 // Create the main range which will be used for the selection.
1792 ieRange = range.document.$.body.createTextRange();
1793
1794 // Position the range at the start boundary.
1795 ieRange.moveToElementText( startNode.$ );
1796 ieRange.moveStart( 'character', 1 );
1797
1798 if ( endNode ) {
1799 // Create a tool range for the end.
1800 var ieRangeEnd = range.document.$.body.createTextRange();
1801
1802 // Position the tool range at the end.
1803 ieRangeEnd.moveToElementText( endNode.$ );
1804
1805 // Move the end boundary of the main range to match the tool range.
1806 ieRange.setEndPoint( 'EndToEnd', ieRangeEnd );
1807 ieRange.moveEnd( 'character', -1 );
1808 } else {
1809 // The isStartMarkerAlone logic comes from V2. It guarantees that the lines
1810 // will expand and that the cursor will be blinking on the right place.
1811 // Actually, we are using this flag just to avoid using this hack in all
1812 // situations, but just on those needed.
1813 var next = startNode.getNext( notWhitespaces );
1814 var inPre = startNode.hasAscendant( 'pre' );
1815 isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) && // already a filler there?
1816 ( inPre || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) );
1817
1818 // Append a temporary <span>&#65279;</span> before the selection.
1819 // This is needed to avoid IE destroying selections inside empty
1820 // inline elements, like <b></b> (#253).
1821 // It is also needed when placing the selection right after an inline
1822 // element to avoid the selection moving inside of it.
1823 dummySpan = range.document.createElement( 'span' );
1824 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359.
1825 dummySpan.insertBefore( startNode );
1826
1827 if ( isStartMarkerAlone ) {
1828 // To expand empty blocks or line spaces after <br>, we need
1829 // instead to have any char, which will be later deleted using the
1830 // selection.
1831 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
1832 range.document.createText( '\ufeff' ).insertBefore( startNode );
1833 }
1834 }
1835
1836 // Remove the markers (reset the position, because of the changes in the DOM tree).
1837 range.setStartBefore( startNode );
1838 startNode.remove();
1839
1840 if ( collapsed ) {
1841 if ( isStartMarkerAlone ) {
1842 // Move the selection start to include the temporary \ufeff.
1843 ieRange.moveStart( 'character', -1 );
1844
1845 ieRange.select();
1846
1847 // Remove our temporary stuff.
1848 range.document.$.selection.clear();
1849 } else {
1850 ieRange.select();
1851 }
1852
1853 range.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START );
1854 dummySpan.remove();
1855 } else {
1856 range.setEndBefore( endNode );
1857 endNode.remove();
1858 ieRange.select();
1859 }
1860 } else {
1861 var sel = this.getNative();
1862
1863 // getNative() returns null if iframe is "display:none" in FF. (#6577)
1864 if ( !sel )
1865 return;
1866
1867 this.removeAllRanges();
1868
1869 for ( var i = 0; i < ranges.length; i++ ) {
1870 // Joining sequential ranges introduced by
1871 // readonly elements protection.
1872 if ( i < ranges.length - 1 ) {
1873 var left = ranges[ i ],
1874 right = ranges[ i + 1 ],
1875 between = left.clone();
1876 between.setStart( left.endContainer, left.endOffset );
1877 between.setEnd( right.startContainer, right.startOffset );
1878
1879 // Don't confused by Firefox adjancent multi-ranges
1880 // introduced by table cells selection.
1881 if ( !between.collapsed ) {
1882 between.shrink( CKEDITOR.NODE_ELEMENT, true );
1883 var ancestor = between.getCommonAncestor(),
1884 enclosed = between.getEnclosedNode();
1885
1886 // The following cases has to be considered:
1887 // 1. <span contenteditable="false">[placeholder]</span>
1888 // 2. <input contenteditable="false" type="radio"/> (#6621)
1889 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
1890 right.setStart( left.startContainer, left.startOffset );
1891 ranges.splice( i--, 1 );
1892 continue;
1893 }
1894 }
1895 }
1896
1897 range = ranges[ i ];
1898
1899 var nativeRange = this.document.$.createRange();
1900
1901 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
1902 // Append a zero-width space so WebKit will not try to
1903 // move the selection by itself (#1272).
1904 var fillingChar = createFillingCharSequenceNode( this.root );
1905 range.insertNode( fillingChar );
1906
1907 next = fillingChar.getNext();
1908
1909 // If the filling char is followed by a <br>, whithout
1910 // having something before it, it'll not blink.
1911 // Let's remove it in this case.
1912 if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) {
1913 removeFillingCharSequenceNode( this.root );
1914 range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START );
1915 } else {
1916 range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END );
1917 }
1918 }
1919
1920 nativeRange.setStart( range.startContainer.$, range.startOffset );
1921
1922 try {
1923 nativeRange.setEnd( range.endContainer.$, range.endOffset );
1924 } catch ( e ) {
1925 // There is a bug in Firefox implementation (it would be too easy
1926 // otherwise). The new start can't be after the end (W3C says it can).
1927 // So, let's create a new range and collapse it to the desired point.
1928 if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) {
1929 range.collapse( 1 );
1930 nativeRange.setEnd( range.endContainer.$, range.endOffset );
1931 } else {
1932 throw e;
1933 }
1934 }
1935
1936 // Select the range.
1937 sel.addRange( nativeRange );
1938 }
1939 }
1940
1941 this.reset();
1942
1943 // Fakes the IE DOM event "selectionchange" on editable.
1944 this.root.fire( 'selectionchange' );
1945 },
1946
1947 /**
1948 * Makes a "fake selection" of an element.
1949 *
1950 * A fake selection does not render UI artifacts over the selected
1951 * element. Additionally, the browser native selection system is not
1952 * aware of the fake selection. In practice, the native selection is
1953 * moved to a hidden place where no native selection UI artifacts are
1954 * displayed to the user.
1955 *
1956 * @param {CKEDITOR.dom.element} element The element to be "selected".
1957 */
1958 fake: function( element ) {
1959 var editor = this.root.editor;
1960
1961 // Cleanup after previous selection - e.g. remove hidden sel container.
1962 this.reset();
1963
1964 hideSelection( editor );
1965
1966 // Set this value after executing hiseSelection, because it may
1967 // cause reset() which overwrites cache.
1968 var cache = this._.cache;
1969
1970 // Caches a range than holds the element.
1971 var range = new CKEDITOR.dom.range( this.root );
1972 range.setStartBefore( element );
1973 range.setEndAfter( element );
1974 cache.ranges = new CKEDITOR.dom.rangeList( range );
1975
1976 // Put this element in the cache.
1977 cache.selectedElement = cache.startElement = element;
1978 cache.type = CKEDITOR.SELECTION_ELEMENT;
1979
1980 // Properties that will not be available when isFake.
1981 cache.selectedText = cache.nativeSel = null;
1982
1983 this.isFake = 1;
1984 this.rev = nextRev++;
1985
1986 // Save this selection, so it can be returned by editor.getSelection().
1987 editor._.fakeSelection = this;
1988
1989 // Fire selectionchange, just like a normal selection.
1990 this.root.fire( 'selectionchange' );
1991 },
1992
1993 /**
1994 * Checks whether selection is placed in hidden element.
1995 *
1996 * This method is to be used to verify whether fake selection
1997 * (see {@link #fake}) is still hidden.
1998 *
1999 * **Note:** this method should be executed on real selection - e.g.:
2000 *
2001 * editor.getSelection( true ).isHidden();
2002 *
2003 * @returns {Boolean}
2004 */
2005 isHidden: function() {
2006 var el = this.getCommonAncestor();
2007
2008 if ( el && el.type == CKEDITOR.NODE_TEXT )
2009 el = el.getParent();
2010
2011 return !!( el && el.data( 'cke-hidden-sel' ) );
2012 },
2013
2014 /**
2015 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2016 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2017 * with extra care taken to avoid interference among those ranges. The arguments
2018 * received are the same as with the underlying range method.
2019 *
2020 * var bookmarks = editor.getSelection().createBookmarks();
2021 *
2022 * @returns {Array} Array of bookmarks for each range.
2023 */
2024 createBookmarks: function( serializable ) {
2025 var bookmark = this.getRanges().createBookmarks( serializable );
2026 this.isFake && ( bookmark.isFake = 1 );
2027 return bookmark;
2028 },
2029
2030 /**
2031 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2032 * by calling the {@link CKEDITOR.dom.range#createBookmark2} method,
2033 * with extra care taken to avoid interference among those ranges. The arguments
2034 * received are the same as with the underlying range method.
2035 *
2036 * var bookmarks = editor.getSelection().createBookmarks2();
2037 *
2038 * @returns {Array} Array of bookmarks for each range.
2039 */
2040 createBookmarks2: function( normalized ) {
2041 var bookmark = this.getRanges().createBookmarks2( normalized );
2042 this.isFake && ( bookmark.isFake = 1 );
2043 return bookmark;
2044 },
2045
2046 /**
2047 * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}.
2048 *
2049 * var bookmarks = editor.getSelection().createBookmarks();
2050 * editor.getSelection().selectBookmarks( bookmarks );
2051 *
2052 * @param {Array} bookmarks The bookmarks representing ranges to be selected.
2053 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected.
2054 */
2055 selectBookmarks: function( bookmarks ) {
2056 var ranges = [],
2057 node;
2058
2059 for ( var i = 0; i < bookmarks.length; i++ ) {
2060 var range = new CKEDITOR.dom.range( this.root );
2061 range.moveToBookmark( bookmarks[ i ] );
2062 ranges.push( range );
2063 }
2064
2065 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2066 if ( bookmarks.isFake ) {
2067 node = ranges[ 0 ].getEnclosedNode();
2068 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
2069 CKEDITOR.warn( 'selection-not-fake' );
2070 bookmarks.isFake = 0;
2071 }
2072 }
2073
2074 if ( bookmarks.isFake )
2075 this.fake( node );
2076 else
2077 this.selectRanges( ranges );
2078
2079 return this;
2080 },
2081
2082 /**
2083 * Retrieves the common ancestor node of the first range and the last range.
2084 *
2085 * var ancestor = editor.getSelection().getCommonAncestor();
2086 *
2087 * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty.
2088 */
2089 getCommonAncestor: function() {
2090 var ranges = this.getRanges();
2091 if ( !ranges.length )
2092 return null;
2093
2094 var startNode = ranges[ 0 ].startContainer,
2095 endNode = ranges[ ranges.length - 1 ].endContainer;
2096 return startNode.getCommonAncestor( endNode );
2097 },
2098
2099 /**
2100 * Moves the scrollbar to the starting position of the current selection.
2101 *
2102 * editor.getSelection().scrollIntoView();
2103 */
2104 scrollIntoView: function() {
2105 // Scrolls the first range into view.
2106 if ( this.type != CKEDITOR.SELECTION_NONE )
2107 this.getRanges()[ 0 ].scrollIntoView();
2108 },
2109
2110 /**
2111 * Remove all the selection ranges from the document.
2112 */
2113 removeAllRanges: function() {
2114 // Don't clear selection outside this selection's root (#11500).
2115 if ( this.getType() == CKEDITOR.SELECTION_NONE )
2116 return;
2117
2118 var nativ = this.getNative();
2119
2120 try {
2121 nativ && nativ[ isMSSelection ? 'empty' : 'removeAllRanges' ]();
2122 } catch ( er ) {}
2123
2124 this.reset();
2125 }
2126 };
2127
2128 } )();
2129
2130
2131 /**
2132 * Fired when selection inside editor has been changed. Note that this event
2133 * is fired only when selection's start element (container of a selecion start)
2134 * changes, not on every possible selection change. Thanks to that `selectionChange`
2135 * is fired less frequently, but on every context
2136 * (the {@link CKEDITOR.editor#elementPath elements path} holding selection's start) change.
2137 *
2138 * @event selectionChange
2139 * @member CKEDITOR.editor
2140 * @param {CKEDITOR.editor} editor This editor instance.
2141 * @param data
2142 * @param {CKEDITOR.dom.selection} data.selection
2143 * @param {CKEDITOR.dom.elementPath} data.path
2144 */
2145
2146 /**
2147 * Selection's revision. This value is incremented every time new
2148 * selection is created or existing one is modified.
2149 *
2150 * @since 4.3
2151 * @readonly
2152 * @property {Number} rev
2153 */
2154
2155 /**
2156 * Document in which selection is anchored.
2157 *
2158 * @readonly
2159 * @property {CKEDITOR.dom.document} document
2160 */
2161
2162 /**
2163 * Selection's root element.
2164 *
2165 * @readonly
2166 * @property {CKEDITOR.dom.element} root
2167 */
2168
2169 /**
2170 * Whether selection is locked (cannot be modified).
2171 *
2172 * See {@link #lock} and {@link #unlock} methods.
2173 *
2174 * @readonly
2175 * @property {Boolean} isLocked
2176 */
2177
2178 /**
2179 * Whether selection is a fake selection.
2180 *
2181 * See {@link #fake} method.
2182 *
2183 * @readonly
2184 * @property {Boolean} isFake
2185 */