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