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