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