2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 var isMSSelection
= typeof window
.getSelection
!= 'function',
9 // http://dev.ckeditor.com/ticket/13816
10 fillingCharSequence
= CKEDITOR
.tools
.repeat( '\u200b', 7 ),
11 fillingCharSequenceRegExp
= new RegExp( fillingCharSequence
+ '( )?', 'g' ),
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 ) {
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
,
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' ) ) {
37 selected
= range
.cloneContents();
39 // Empty selection is still partially selected.
40 if ( !selected
.getFirst() ) {
44 return trim( selected
.getFirst().getText() ) !== trim( startCell
.getText() );
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 ] ) ) ) {
53 for ( i
= 0; i
< ranges
.length
; i
++ ) {
54 node
= ranges
[ i
]._getTableElement();
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' );
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
);
88 return isSameTable( table
, fakeTable
) && ( isMenuOpen
|| isInTable
);
91 function isSameTable( table
, fakeTable
) {
92 if ( !table
|| !fakeTable
) {
96 return table
.equals( fakeTable
) || fakeTable
.contains( table
);
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
);
111 function getSelectedCells( ranges
) {
116 function getCellsFromElement( element
) {
117 var cells
= element
.find( 'td, th' ),
121 for ( i
= 0; i
< cells
.count(); i
++ ) {
122 cellsArray
.push( cells
.getItem( i
) );
128 for ( i
= 0; i
< ranges
.length
; i
++ ) {
129 node
= ranges
[ i
]._getTableElement();
131 if ( node
.is
&& node
.is( { td: 1, th: 1 } ) ) {
134 cells
= cells
.concat( getCellsFromElement( node
) );
141 // Cells in the same row are separated by tab and the rows are separated by new line, e.g.
144 function getTextFromSelectedCells( ranges
) {
145 var cells
= getSelectedCells( ranges
),
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' );
156 } else if ( i
=== 0 ) {
157 lastRow
= cells
[ i
].getAscendant( 'tr' );
160 currentRow
.push( cells
[ i
].getText() );
163 txt
+= currentRow
.join( '\t' );
168 function performFakeTableSelection( ranges
) {
169 var editor
= this.root
.editor
,
170 realSelection
= editor
.getSelection( 1 ),
173 // Cleanup after previous selection - e.g. remove hidden sel container.
176 // Indicate that the table is being fake-selected to prevent infinite loop
177 // inside `selectRanges`.
178 isSelectingTable
= true;
180 // Cancel selectionchange for the real selection.
181 realSelection
.root
.once( 'selectionchange', function( evt
) {
185 // Move real selection to the first selected range.
186 realSelection
.selectRanges( [ ranges
[ 0 ] ] );
188 cache
= this._
.cache
;
190 // Caches given ranges.
191 cache
.ranges
= new CKEDITOR
.dom
.rangeList( ranges
);
192 cache
.type
= CKEDITOR
.SELECTION_TEXT
;
193 cache
.selectedElement
= ranges
[ 0 ]._getTableElement();
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
);
199 // Properties that will not be available when isFake.
200 cache
.nativeSel
= null;
203 this.rev
= nextRev
++;
205 // Save this selection, so it can be returned by editor.getSelection().
206 editor
._
.fakeSelection
= this;
208 isSelectingTable
= false;
210 // Fire selectionchange, just like a normal selection.
211 this.root
.fire( 'selectionchange' );
213 // #### table selection : END
215 // #### checkSelectionChange : START
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
,
226 realSel
= this.getSelection( 1 );
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
) ) ) {
230 // Remove the cache from fake-selection references in use elsewhere.
233 // Have the code using the native selection.
238 // If not fake-selection is available then get the native selection.
240 sel
= realSel
|| this.getSelection( 1 );
242 // Editor may have no selection at all.
243 if ( !sel
|| sel
.getType() == CKEDITOR
.SELECTION_NONE
)
247 this.fire( 'selectionCheck', sel
);
249 var currentPath
= this.elementPath();
250 if ( !currentPath
.compare( this._
.selectionPreviousPath
) ) {
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)
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();
257 this._
.selectionPreviousPath
= currentPath
;
258 this.fire( 'selectionChange', { selection: sel
, path: currentPath
} );
262 var checkSelectionChangeTimer
, checkSelectionChangeTimeoutPending
;
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.
269 checkSelectionChangeTimeoutPending
= true;
271 if ( checkSelectionChangeTimer
)
274 checkSelectionChangeTimeoutExec
.call( this );
276 checkSelectionChangeTimer
= CKEDITOR
.tools
.setTimeout( checkSelectionChangeTimeoutExec
, 200, this );
279 function checkSelectionChangeTimeoutExec() {
280 checkSelectionChangeTimer
= null;
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
287 CKEDITOR
.tools
.setTimeout( checkSelectionChange
, 0, this );
289 checkSelectionChangeTimeoutPending
= false;
293 // #### checkSelectionChange : END
295 var isVisible
= CKEDITOR
.dom
.walker
.invisible( 1 );
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).
301 // See http://dev.ckeditor.com/ticket/12621.
302 function mayAbsorbCaret( node
) {
303 if ( isVisible( node
) )
306 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& !node
.is( CKEDITOR
.dtd
.$empty
) )
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
)
321 var testRng
= range
.clone();
322 return testRng
[ 'moveToElementEdit' + ( isAtEnd
? 'End' : 'Start' ) ]( node
);
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
) )
330 var ct
= range
.startContainer
;
332 var previous
= range
.getPreviousNode( mayAbsorbCaret
, null, ct
),
333 next
= range
.getNextNode( mayAbsorbCaret
, null, ct
);
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 ) )
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.
344 if ( !( previous
|| next
) && !( ct
.type
== CKEDITOR
.NODE_ELEMENT
&& ct
.isBlockBoundary() && ct
.getBogus() ) )
350 function createFillingCharSequenceNode( editable
) {
351 removeFillingCharSequenceNode( editable
, false );
353 var fillingChar
= editable
.getDocument().createText( fillingCharSequence
);
354 editable
.setCustomData( 'cke-fillingChar', fillingChar
);
359 // Checks if a filling char has been used, eventually removing it (http://dev.ckeditor.com/ticket/1272).
360 function checkFillingCharSequenceNodeReady( editable
) {
361 var fillingChar
= editable
.getCustomData( 'cke-fillingChar' );
364 // Use this flag to avoid removing the filling char right after
366 if ( fillingChar
.getCustomData( 'ready' ) ) {
367 removeFillingCharSequenceNode( editable
);
368 editable
.editor
.fire( 'selectionCheck' );
370 fillingChar
.setCustomData( 'ready', 1 );
375 function removeFillingCharSequenceNode( editable
, keepSelection
) {
376 var fillingChar
= editable
&& editable
.removeCustomData( 'cke-fillingChar' );
379 // Text selection position might get mangled by
380 // subsequent dom modification, save it now for restoring. (http://dev.ckeditor.com/ticket/8617)
381 if ( keepSelection
!== false ) {
382 var sel
= editable
.getDocument().getSelection().getNative(),
384 range
= sel
&& sel
.type
!= 'None' && sel
.getRangeAt( 0 ),
385 fillingCharSeqLength
= fillingCharSequence
.length
;
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
);
392 // Correct start offset anticipating the removal of FC.
393 if ( sel
.anchorNode
== fillingChar
.$ && sel
.anchorOffset
> fillingCharSeqLength
) {
394 bm
[ 0 ].offset
-= fillingCharSeqLength
;
397 // Correct end offset anticipating the removal of FC.
398 if ( sel
.focusNode
== fillingChar
.$ && sel
.focusOffset
> fillingCharSeqLength
) {
399 bm
[ 1 ].offset
-= fillingCharSeqLength
;
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 ) );
409 // Restore the bookmark preserving selection's direction.
411 moveNativeSelectionToBookmark( editable
.getDocument().$, bm
);
416 // http://dev.ckeditor.com/ticket/13816
417 function removeFillingCharSequenceString( str
, nbspAware
) {
419 return str
.replace( fillingCharSequenceRegExp
, function( m
, p
) {
420 // http://dev.ckeditor.com/ticket/10291 if filling char is followed by a space replace it with NBSP.
421 return p
? '\xa0' : '';
424 return str
.replace( fillingCharSequence
, '' );
428 function createNativeSelectionBookmark( sel
) {
430 { node: sel
.anchorNode
, offset: sel
.anchorOffset
},
431 { node: sel
.focusNode
, offset: sel
.focusOffset
}
435 function moveNativeSelectionToBookmark( document
, bm
) {
436 var sel
= document
.getSelection(),
437 range
= document
.createRange();
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
);
446 // Creates cke_hidden_sel container and puts real selection there.
447 function hideSelection( editor
, ariaLabel
) {
448 var content
= ariaLabel
|| ' ',
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>',
454 editor
.fire( 'lockSnapshot' );
456 editor
.editable().append( hiddenEl
);
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
) {
466 range
.setStartAt( hiddenEl
, CKEDITOR
.POSITION_AFTER_START
);
467 range
.setEndAt( hiddenEl
, CKEDITOR
.POSITION_BEFORE_END
);
468 sel
.selectRanges( [ range
] );
470 listener
.removeListener();
472 editor
.fire( 'unlockSnapshot' );
474 // Set this value at the end, so reset() executed by selectRanges()
475 // will clean up old hidden selection container.
476 editor
._
.hiddenSelectionContainer
= hiddenEl
;
479 function removeHiddenSelectionContainer( editor
) {
480 var hiddenEl
= editor
._
.hiddenSelectionContainer
;
483 var isDirty
= editor
.checkDirty();
485 editor
.fire( 'lockSnapshot' );
487 editor
.fire( 'unlockSnapshot' );
489 !isDirty
&& editor
.resetDirty();
492 delete editor
._
.hiddenSelectionContainer
;
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();
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
] );
511 function del( right
) {
512 return function( evt
) {
513 var editor
= evt
.editor
,
514 range
= editor
.createRange(),
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
);
523 editor
.getSelection().selectRanges( [ range
] );
525 // Save the state before removing selected element.
526 editor
.fire( 'saveSnapshot' );
528 evt
.selected
.remove();
530 // Haven't found any editable space before removing element,
531 // try to place the caret anywhere (most likely, in empty editable).
533 range
.moveToElementEditablePosition( editor
.editable() );
534 editor
.getSelection().selectRanges( [ range
] );
537 editor
.fire( 'saveSnapshot' );
544 var leaveLeft
= leave(),
545 leaveRight
= leave( 1 );
548 37: leaveLeft
, // LEFT
550 39: leaveRight
, // RIGHT
551 40: leaveRight
, // DOWN
552 8: del(), // BACKSPACE
553 46: del( 1 ) // DELETE
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 };
562 return function( evt
) {
563 var keystroke
= evt
.data
.getKeystroke();
565 // Handle only left/right/del/bspace keys.
566 if ( !keystrokes
[ keystroke
] )
569 var sel
= editor
.getSelection(),
570 ranges
= sel
.getRanges(),
573 // Handle only single range and it has to be collapsed.
574 if ( ranges
.length
!= 1 || !range
.collapsed
)
577 var next
= range
[ keystroke
< 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ]();
579 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.getAttribute( 'contenteditable' ) == 'false' ) {
580 editor
.getSelection().fake( next
);
581 evt
.data
.preventDefault();
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
;
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
596 // innermost element (http://dev.ckeditor.com/ticket/11114).
597 clone
= range
.clone();
598 clone
.shrink( CKEDITOR
.SHRINK_ELEMENT
, true );
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
;
604 if ( enclosedNode
.getAttribute( 'contenteditable' ) == 'false' )
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
) {
614 for ( var i
= 0; i
< ranges
.length
; ++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() );
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
];
630 // Drop range spans inside one ready-only node.
631 var parent
= range
.getCommonAncestor();
632 if ( parent
.isReadOnly() )
633 ranges
.splice( i
, 1 );
635 if ( range
.collapsed
)
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
,
645 isElement
= current
.type
== CKEDITOR
.NODE_ELEMENT
;
647 if ( ( isElement
&& current
.is( 'body' ) ) || !current
.isReadOnly() )
650 if ( isElement
&& current
.getAttribute( 'contentEditable' ) == 'false' )
651 range
.setStartAfter( current
);
653 current
= current
.getParent();
657 var startContainer
= range
.startContainer
,
658 endContainer
= range
.endContainer
,
659 startOffset
= range
.startOffset
,
660 endOffset
= range
.endOffset
,
661 walkerRange
= range
.clone();
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
);
670 walkerRange
.setStartBefore( startContainer
);
673 if ( endContainer
&& endContainer
.type
== CKEDITOR
.NODE_TEXT
) {
675 walkerRange
.setEndBefore( endContainer
);
677 walkerRange
.setEndAfter( endContainer
);
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
);
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 );
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
);
712 // Setup all editor instances for the necessary selection hooks.
713 CKEDITOR
.on( 'instanceCreated', function( ev
) {
714 var editor
= ev
.editor
;
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();
723 var isInline
= editable
.isInline();
728 // Give the editable an initial selection on first focus,
729 // put selection at a consistent position at the start
730 // of the contents. (http://dev.ckeditor.com/ticket/9507)
731 if ( CKEDITOR
.env
.gecko
) {
732 editable
.attachListener( editable
, 'focus', function( evt
) {
733 evt
.removeListener();
735 if ( restoreSel
!== 0 ) {
736 var nativ
= editor
.getSelection().getNative();
737 // Do it only if the native selection is at an unwanted
738 // place (at the very start of the editable). http://dev.ckeditor.com/ticket/10119
739 if ( nativ
&& nativ
.isCollapsed
&& nativ
.anchorNode
== editable
.$ ) {
740 var rng
= editor
.createRange();
741 rng
.moveToElementEditStart( editable
);
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() );
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
758 // position should be restored after focusing back editable area. (http://dev.ckeditor.com/ticket/14659)
759 if ( restoreSel
&& editor
._
.previousScrollTop
!= null && editor
._
.previousScrollTop
!= editable
.$.scrollTop
) {
760 editable
.$.scrollTop
= editor
._
.previousScrollTop
;
764 editor
.unlockSelection( restoreSel
);
768 // Disable selection restoring when clicking in.
769 editable
.attachListener( editable
, 'mousedown', function() {
773 // Save a cloned version of current selection.
775 lastSel
= new CKEDITOR
.dom
.selection( editor
.getSelection() );
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.
786 editable
.attachListener( editable
, 'beforedeactivate', saveSel
, null, null, -1 );
788 editable
.attachListener( editor
, 'selectionCheck', saveSel
, null, null, -1 );
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
);
798 // Disable selection restoring when clicking in.
799 editable
.attachListener( editable
, 'mousedown', function() {
804 // The following selection-related fixes only apply to classic (`iframe`-based) editable.
805 if ( CKEDITOR
.env
.ie
&& !isInline
) {
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
810 // position and revert it before context menu opens. (http://dev.ckeditor.com/ticket/5778)
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();
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
;
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
829 // the normal behavior on old IEs. (http://dev.ckeditor.com/ticket/1659, http://dev.ckeditor.com/ticket/7932)
830 if ( doc
.$.compatMode
!= 'BackCompat' ) {
831 if ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.ie6Compat
) {
835 html
.on( 'mousedown', function( evt
) {
838 // Expand the text range along with mouse move.
839 function onHover( evt
) {
842 // Read the current cursor.
843 var rngEnd
= body
.$.createTextRange();
845 moveRangeToPoint( rngEnd
, evt
.clientX
, evt
.clientY
);
847 // Handle drag directions.
849 startRng
.compareEndPoints( 'StartToStart', rngEnd
) < 0 ?
850 'EndToEnd' : 'StartToStart', rngEnd
);
852 // Update selection with new range.
857 function removeListeners() {
858 outerDoc
.removeListener( 'mouseup', onSelectEnd
);
859 html
.removeListener( 'mouseup', onSelectEnd
);
862 function onSelectEnd() {
863 html
.removeListener( 'mousemove', onHover
);
866 // Make it in effect on mouse up. (http://dev.ckeditor.com/ticket/9022)
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
);
880 // Records the dragging start of the above text range.
881 startRng
= textRng
.duplicate();
883 html
.on( 'mousemove', onHover
);
884 outerDoc
.on( 'mouseup', onSelectEnd
);
885 html
.on( 'mouseup', onSelectEnd
);
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' ) ) {
895 // Limit the text selection mouse move inside of editable. (http://dev.ckeditor.com/ticket/9715)
896 outerDoc
.on( 'mouseup', onSelectEnd
);
897 html
.on( 'mouseup', onSelectEnd
);
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
);
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();
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 );
926 // http://dev.ckeditor.com/ticket/9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
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
) ) {
931 editable
.attachListener( editable
, 'mousedown', function() {
934 editable
.attachListener( doc
.getDocumentElement(), 'mouseup', function() {
936 checkSelectionChangeTimeout
.call( editor
);
940 // In all other cases listen on simple mouseup over editable, as we did before http://dev.ckeditor.com/ticket/9699.
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
944 // of the editor (http://dev.ckeditor.com/ticket/9851).
946 editable
.attachListener( CKEDITOR
.env
.ie
? editable : doc
.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout
, editor
);
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.
961 case 37: // LEFT-ARROW
962 case 39: // RIGHT-ARROW
966 if ( editable
.hasFocus
) {
967 removeFillingCharSequenceNode( editable
);
974 editable
.attachListener( editable
, 'keydown', getOnKeyDownListener( editor
), null, null, -1 );
976 function moveRangeToPoint( range
, x
, y
) {
977 // Error prune in IE7. (http://dev.ckeditor.com/ticket/9034, http://dev.ckeditor.com/ticket/9110)
979 range
.moveToPoint( x
, y
);
983 function removeListeners() {
984 outerDoc
.removeListener( 'mouseup', onSelectEnd
);
985 html
.removeListener( 'mouseup', onSelectEnd
);
988 function onSelectEnd() {
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();
997 // The selection range is reported on host, but actually it should applies to the content doc.
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
.$ )
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' );
1011 return range
&& ascendant
.getAttribute( 'contenteditable' ) == 'false' && ascendant
;
1016 editor
.on( 'setData', function() {
1017 // Invalidate locked selection when unloading DOM.
1018 // (http://dev.ckeditor.com/ticket/9521, http://dev.ckeditor.com/ticket/5217#comment:32 and http://dev.ckeditor.com/ticket/11500#comment:11)
1019 editor
.unlockSelection();
1021 // Webkit's selection will mess up after the data loading.
1022 if ( CKEDITOR
.env
.webkit
)
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();
1032 // IE9 might cease to work if there's an object selection inside the iframe (http://dev.ckeditor.com/ticket/7639).
1033 if ( CKEDITOR
.env
.ie9Compat
)
1034 editor
.on( 'beforeDestroy', clearSelection
, null, null, 9 );
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
;
1042 editor
.selectionChange( 1 );
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
),
1048 // TODO replace with el.find() which will be introduced in http://dev.ckeditor.com/ticket/9764,
1049 // because it may happen that hidden sel container won't be the last element.
1050 last
= editor
.editable().getLast( isElement
);
1052 if ( last
&& last
.hasAttribute( 'data-cke-hidden-sel' ) ) {
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
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.
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' ) ) {
1073 }, null, null, 100 );
1075 editor
.on( 'key', function( evt
) {
1076 if ( editor
.mode
!= 'wysiwyg' )
1079 var sel
= editor
.getSelection();
1083 var handler
= fakeSelectionDefaultKeystrokeHandlers
[ evt
.data
.keyCode
];
1085 return handler( { editor: editor
, selected: sel
.getSelectedElement(), selection: sel
, keyEvent: evt
} );
1088 function clearSelection() {
1089 var sel
= editor
.getSelection();
1090 sel
&& sel
.removeAllRanges();
1094 // On WebKit only, we need a special "filling" char on some situations
1095 // (http://dev.ckeditor.com/ticket/1272). Here we set the events that should invalidate that char.
1096 if ( CKEDITOR
.env
.webkit
) {
1097 CKEDITOR
.on( 'instanceReady', function( evt
) {
1098 var editor
= evt
.editor
;
1100 editor
.on( 'selectionChange', function() {
1101 checkFillingCharSequenceNodeReady( editor
.editable() );
1102 }, null, null, -1 );
1104 editor
.on( 'beforeSetMode', function() {
1105 removeFillingCharSequenceNode( editor
.editable() );
1106 }, null, null, -1 );
1108 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
1109 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
1110 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (http://dev.ckeditor.com/ticket/13816).
1111 editor
.on( 'getSnapshot', function( evt
) {
1113 evt
.data
= removeFillingCharSequenceString( evt
.data
);
1115 }, editor
, null, 20 );
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,
1119 // which should be pruned by the dataProcessor (http://dev.ckeditor.com/ticket/13816).
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
);
1129 * Check the selection change in editor and potentially fires
1130 * the {@link CKEDITOR.editor#event-selectionChange} event.
1133 * @member CKEDITOR.editor
1134 * @param {Boolean} [checkNow=false] Force the check to happen immediately
1135 * instead of coming with a timeout delay (default).
1137 CKEDITOR
.editor
.prototype.selectionChange = function( checkNow
) {
1138 ( checkNow
? checkSelectionChange : checkSelectionChangeTimeout
).call( this );
1142 * Retrieve the editor selection in scope of editable element.
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.
1149 * var selection = CKEDITOR.instances.editor1.getSelection();
1150 * alert( selection.getType() );
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.
1157 CKEDITOR
.editor
.prototype.getSelection = function( forceRealSelection
) {
1159 // Check if there exists a locked or fake selection.
1160 if ( ( this._
.savedSelection
|| this._
.fakeSelection
) && !forceRealSelection
)
1161 return this._
.savedSelection
|| this._
.fakeSelection
;
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;
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.
1175 * @member CKEDITOR.editor
1176 * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked.
1177 * @returns {Boolean} `true` if selection was locked.
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
;
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.
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.
1200 CKEDITOR
.editor
.prototype.unlockSelection = function( restore
) {
1201 var sel
= this._
.savedSelection
;
1203 sel
.unlock( restore
);
1204 delete this._
.savedSelection
;
1213 * @member CKEDITOR.editor
1216 CKEDITOR
.editor
.prototype.forceNextSelectionCheck = function() {
1217 delete this._
.selectionPreviousPath
;
1221 * Gets the current selection in context of the document's body element.
1223 * var selection = CKEDITOR.instances.editor1.document.getSelection();
1224 * alert( selection.getType() );
1227 * @member CKEDITOR.dom.document
1228 * @returns {CKEDITOR.dom.selection} A selection object.
1230 CKEDITOR
.dom
.document
.prototype.getSelection = function() {
1231 return new CKEDITOR
.dom
.selection( this );
1235 * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}.
1238 * @returns {CKEDITOR.dom.selection}
1239 * @member CKEDITOR.dom.range
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
);
1244 sel
.selectRanges( [ this ] );
1252 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
1253 * alert( 'Nothing is selected' );
1256 * @property {Number} [=1]
1259 CKEDITOR
.SELECTION_NONE
= 1;
1262 * A text or a collapsed selection.
1264 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1265 * alert( 'A text is selected' );
1268 * @property {Number} [=2]
1271 CKEDITOR
.SELECTION_TEXT
= 2;
1274 * Element selection.
1276 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
1277 * alert( 'An element is selected' );
1280 * @property {Number} [=3]
1283 CKEDITOR
.SELECTION_ELEMENT
= 3;
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.
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:
1292 * var sel = editor.getSelection();
1294 * See {@link CKEDITOR.editor#getSelection}.
1297 * @constructor Creates a selection class instance.
1299 * // Selection scoped in document.
1300 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document );
1302 * // Selection scoped in element with 'editable' id.
1303 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document.getById( 'editable' ) );
1305 * // Cloning selection.
1306 * var clone = new CKEDITOR.dom.selection( sel );
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.
1312 * If {@link CKEDITOR.dom.selection} is passed, then its clone will be created.
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
;
1321 var isElement
= target
instanceof CKEDITOR
.dom
.element
,
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();
1334 CKEDITOR
.tools
.extend( this._
.cache
, selection
._
.cache
);
1335 this.isFake
= selection
.isFake
;
1336 this.isLocked
= selection
.isLocked
;
1340 // Check whether browser focus is really inside of the editable element.
1342 var nativeSel
= this.getNative(),
1347 if ( nativeSel
.getRangeAt
) {
1348 range
= nativeSel
.rangeCount
&& nativeSel
.getRangeAt( 0 );
1349 rangeParent
= range
&& new CKEDITOR
.dom
.node( range
.commonAncestorContainer
);
1353 // Sometimes, mostly when selection is close to the table or hr,
1354 // IE throws "Unspecified error".
1356 range
= nativeSel
.createRange();
1358 rangeParent
= range
&& CKEDITOR
.dom
.element
.get( range
.item
&& range
.item( 0 ) || range
.parentElement() );
1362 // Selection out of concerned range, empty the selection.
1363 // TODO check whether this condition cannot be reverted to its old
1364 // form (commented out) after we closed http://dev.ckeditor.com/ticket/10438.
1365 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1368 ( rangeParent
.type
== CKEDITOR
.NODE_ELEMENT
|| rangeParent
.type
== CKEDITOR
.NODE_TEXT
) &&
1369 ( this.root
.equals( rangeParent
) || this.root
.contains( rangeParent
) )
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();
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 };
1385 CKEDITOR
.tools
.extend( CKEDITOR
.dom
.selection
, {
1386 _removeFillingCharSequenceString: removeFillingCharSequenceString
,
1387 _createFillingCharSequenceNode: createFillingCharSequenceNode
,
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).
1395 * @property {String}
1397 FILLING_CHAR_SEQUENCE: fillingCharSequence
1400 CKEDITOR
.dom
.selection
.prototype = {
1402 * Gets the native selection object from the browser.
1404 * var selection = editor.getSelection().getNative();
1406 * @returns {Object} The native browser selection object or null if this is a fake selection.
1408 getNative: function() {
1409 if ( this._
.cache
.nativeSel
!== undefined )
1410 return this._
.cache
.nativeSel
;
1412 return ( this._
.cache
.nativeSel
= isMSSelection
? this.document
.$.selection : this.document
.getWindow().$.getSelection() );
1416 * Gets the type of the current selection. The following values are
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.
1425 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1426 * alert( 'A text is selected' );
1429 * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE},
1430 * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}.
1432 getType: isMSSelection
?
1434 var cache
= this._
.cache
;
1438 var type
= CKEDITOR
.SELECTION_NONE
;
1441 var sel
= this.getNative(),
1444 if ( ieType
== 'Text' )
1445 type
= CKEDITOR
.SELECTION_TEXT
;
1447 if ( ieType
== 'Control' )
1448 type
= CKEDITOR
.SELECTION_ELEMENT
;
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
;
1458 return ( cache
.type
= type
);
1460 var cache
= this._
.cache
;
1464 var type
= CKEDITOR
.SELECTION_TEXT
;
1466 var sel
= this.getNative();
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...).
1474 var range
= sel
.getRangeAt( 0 ),
1475 startContainer
= range
.startContainer
;
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
;
1485 return ( cache
.type
= type
);
1489 * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection.
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.
1494 * var ranges = selection.getRanges();
1495 * alert( ranges.length );
1498 * @param {Boolean} [onlyEditables] If set to `true`, this function retrieves editable ranges only.
1499 * @returns {Array} Range instances that represent the current selection.
1501 getRanges: ( function() {
1502 var func
= isMSSelection
? ( function() {
1503 function getNodeIndex( node
) {
1504 return new CKEDITOR
.dom
.node( node
).getIndex();
1507 // Finds the container and offset for a specific boundary
1509 var getBoundaryInformation = function( range
, start
) {
1510 // Creates a collapsed range at the requested boundary.
1511 range
= range
.duplicate();
1512 range
.collapse( start
);
1514 // Gets the element that encloses the range entirely.
1515 var parent
= range
.parentElement();
1517 // Empty parent element, e.g. <i>^</i>
1518 if ( !parent
.hasChildNodes() )
1519 return { container: parent
, offset: 0 };
1521 var siblings
= parent
.children
,
1523 testRange
= range
.duplicate(),
1525 endIndex
= siblings
.length
- 1,
1527 position
, distance
, container
;
1529 // Binary search over all element children to test the range to see whether
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
);
1538 endIndex
= index
- 1;
1539 else if ( position
< 0 )
1540 startIndex
= index
+ 1;
1542 return { container: parent
, offset: getNodeIndex( child
) };
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)
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
);
1552 // IE report line break as CRLF with range.text but
1553 // only LF with textnode.nodeValue, normalize them to avoid
1554 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1555 distance
= testRange
.text
.replace( /(\r\n|\r)/g, '\n' ).length
;
1557 siblings
= parent
.childNodes
;
1559 // Actual range anchor right beside test range at the boundary of text node.
1561 child
= siblings
[ siblings
.length
- 1 ];
1563 if ( child
.nodeType
!= CKEDITOR
.NODE_TEXT
)
1564 return { container: parent
, offset: siblings
.length
};
1566 return { container: child
, offset: child
.nodeValue
.length
};
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
;
1579 return { container: container
, offset: -distance
};
1581 // Test range was one offset beyond OR behind the anchored text node.
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
);
1588 // IE report line break as CRLF with range.text but
1589 // only LF with textnode.nodeValue, normalize them to avoid
1590 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1591 distance
= testRange
.text
.replace( /(\r\n|\r)/g, '\n' ).length
;
1593 // Actual range anchor right beside test range at the inner boundary of text node.
1595 return { container: parent
, offset: getNodeIndex( child
) + ( position
> 0 ? 0 : 1 ) };
1597 // Start the measuring until distance overflows, meanwhile count the text nodes.
1598 while ( distance
> 0 ) {
1600 sibling
= child
[ position
> 0 ? 'previousSibling' : 'nextSibling' ];
1601 if ( sibling
.nodeType
== CKEDITOR
.NODE_TEXT
) {
1602 distance
-= sibling
.nodeValue
.length
;
1603 container
= sibling
;
1607 // Measurement in IE could be sometimes wrong because of <select> element. (http://dev.ckeditor.com/ticket/4611)
1609 return { container: parent
, offset: getNodeIndex( child
) };
1613 return { container: container
, offset: position
> 0 ? -distance : container
.nodeValue
.length
+ distance
};
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.
1622 var sel
= this.getNative(),
1623 nativeRange
= sel
&& sel
.createRange(),
1624 type
= this.getType(),
1630 if ( type
== CKEDITOR
.SELECTION_TEXT
) {
1631 range
= new CKEDITOR
.dom
.range( this.root
);
1633 var boundaryInfo
= getBoundaryInformation( nativeRange
, true );
1634 range
.setStart( new CKEDITOR
.dom
.node( boundaryInfo
.container
), boundaryInfo
.offset
);
1636 boundaryInfo
= getBoundaryInformation( nativeRange
);
1637 range
.setEnd( new CKEDITOR
.dom
.node( boundaryInfo
.container
), boundaryInfo
.offset
);
1639 // Correct an invalid IE range case on empty list item. (http://dev.ckeditor.com/ticket/5850)
1640 if ( range
.endContainer
.getPosition( range
.startContainer
) & CKEDITOR
.POSITION_PRECEDING
&& range
.endOffset
<= range
.startContainer
.getIndex() )
1644 } else if ( type
== CKEDITOR
.SELECTION_ELEMENT
) {
1647 for ( var i
= 0; i
< nativeRange
.length
; i
++ ) {
1648 var element
= nativeRange
.item( i
),
1649 parentElement
= element
.parentNode
,
1652 range
= new CKEDITOR
.dom
.range( this.root
);
1654 for ( ; j
< parentElement
.childNodes
.length
&& parentElement
.childNodes
[ j
] != element
; j
++ ) {
1658 range
.setStart( new CKEDITOR
.dom
.node( parentElement
), j
);
1659 range
.setEnd( new CKEDITOR
.dom
.node( parentElement
), j
+ 1 );
1660 retval
.push( range
);
1670 // On browsers implementing the W3C range, we simply
1671 // transform the native ranges in CKEDITOR.dom.range
1676 sel
= this.getNative();
1681 for ( var i
= 0; i
< sel
.rangeCount
; i
++ ) {
1682 var nativeRange
= sel
.getRangeAt( i
);
1684 range
= new CKEDITOR
.dom
.range( this.root
);
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
);
1693 return function( onlyEditables
) {
1694 var cache
= this._
.cache
,
1695 ranges
= cache
.ranges
;
1698 cache
.ranges
= ranges
= new CKEDITOR
.dom
.rangeList( func
.call( this ) );
1700 if ( !onlyEditables
)
1703 // Split range into multiple by read-only nodes.
1704 // Clone ranges array to avoid changing cached ranges (http://dev.ckeditor.com/ticket/11493).
1705 return extractEditableRanges( new CKEDITOR
.dom
.rangeList( ranges
.slice() ) );
1710 * Gets the DOM element in which the selection starts.
1712 * var element = editor.getSelection().getStartElement();
1713 * alert( element.getName() );
1715 * @returns {CKEDITOR.dom.element} The element at the beginning of the selection.
1717 getStartElement: function() {
1718 var cache
= this._
.cache
;
1719 if ( cache
.startElement
!== undefined )
1720 return cache
.startElement
;
1724 switch ( this.getType() ) {
1725 case CKEDITOR
.SELECTION_ELEMENT:
1726 return this.getSelectedElement();
1728 case CKEDITOR
.SELECTION_TEXT:
1730 var range
= this.getRanges()[ 0 ];
1733 if ( !range
.collapsed
) {
1736 // Decrease the range content to exclude particial
1737 // selected node on the start which doesn't have
1738 // visual impact. ( http://dev.ckeditor.com/ticket/3231 )
1740 var startContainer
= range
.startContainer
,
1741 startOffset
= range
.startOffset
;
1742 // Limit the fix only to non-block elements.(http://dev.ckeditor.com/ticket/3950)
1743 if ( startOffset
== ( startContainer
.getChildCount
? startContainer
.getChildCount() : startContainer
.getLength() ) && !startContainer
.isBlockBoundary() )
1744 range
.setStartAfter( startContainer
);
1749 node
= range
.startContainer
;
1751 if ( node
.type
!= CKEDITOR
.NODE_ELEMENT
)
1752 return node
.getParent();
1754 node
= node
.getChild( range
.startOffset
);
1756 if ( !node
|| node
.type
!= CKEDITOR
.NODE_ELEMENT
)
1757 node
= range
.startContainer
;
1759 var child
= node
.getFirst();
1760 while ( child
&& child
.type
== CKEDITOR
.NODE_ELEMENT
) {
1762 child
= child
.getFirst();
1766 node
= range
.startContainer
;
1767 if ( node
.type
!= CKEDITOR
.NODE_ELEMENT
)
1768 node
= node
.getParent();
1775 return cache
.startElement
= ( node
? new CKEDITOR
.dom
.element( node
) : null );
1779 * Gets the currently selected element.
1781 * var element = editor.getSelection().getSelectedElement();
1782 * alert( element.getName() );
1784 * @returns {CKEDITOR.dom.element/null} The selected element. `null` if no
1785 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1787 getSelectedElement: function() {
1788 var cache
= this._
.cache
;
1789 if ( cache
.selectedElement
!== undefined )
1790 return cache
.selectedElement
;
1794 var node
= CKEDITOR
.tools
.tryThese(
1795 // Is it native IE control type selection?
1797 return self
.getNative().createRange().item( 0 );
1799 // Figure it out by checking if there's a single enclosed
1800 // node of the range.
1802 var range
= self
.getRanges()[ 0 ].clone(),
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
);
1811 return selected
&& selected
.$;
1815 return cache
.selectedElement
= ( node
? new CKEDITOR
.dom
.element( node
) : null );
1819 * Retrieves the text contained within the range. An empty string is returned for non-text selection.
1821 * var text = editor.getSelection().getSelectedText();
1825 * @returns {String} A string of text within the current selection.
1827 getSelectedText: function() {
1828 var cache
= this._
.cache
;
1829 if ( cache
.selectedText
!== undefined )
1830 return cache
.selectedText
;
1832 var nativeSel
= this.getNative(),
1833 text
= isMSSelection
? nativeSel
.type
== 'Control' ? '' : nativeSel
.createRange().text : nativeSel
.toString();
1835 return ( cache
.selectedText
= text
);
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.
1843 * editor.getSelection().lock();
1846 // Call all cacheable function.
1848 this.getStartElement();
1849 this.getSelectedElement();
1850 this.getSelectedText();
1852 // The native selection is not available when locked.
1853 this._
.cache
.nativeSel
= null;
1861 unlock: function( restore
) {
1862 if ( !this.isLocked
)
1866 var selectedElement
= this.getSelectedElement(),
1867 ranges
= this.getRanges(),
1868 faked
= this.isFake
;
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 ) ) )
1881 if ( isTableSelection( ranges
) ) {
1882 // Tables have it's own selection method.
1883 performFakeTableSelection
.call( this, ranges
);
1885 this.fake( selectedElement
);
1886 else if ( selectedElement
)
1887 this.selectElement( selectedElement
);
1889 this.selectRanges( ranges
);
1894 * Clears the selection cache.
1896 * editor.getSelection().reset();
1902 var editor
= this.root
.editor
;
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
;
1911 removeHiddenSelectionContainer( editor
);
1914 CKEDITOR
.warn( 'selection-fake-reset' );
1918 this.rev
= nextRev
++;
1922 * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element.
1924 * var element = editor.document.getById( 'sampleElement' );
1925 * editor.getSelection().selectElement( element );
1927 * @param {CKEDITOR.dom.element} element The element to enclose in the selection.
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
] );
1937 * Clears the original selection and adds the specified ranges to the document selection.
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 ] );
1944 * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances
1945 * representing ranges to be added to the document.
1947 selectRanges: function( ranges
) {
1948 var editor
= this.root
.editor
,
1949 hadHiddenSelectionContainer
= editor
&& editor
._
.hiddenSelectionContainer
;
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
1956 // may need new endOffset (one element less within the range) (http://dev.ckeditor.com/ticket/11021 http://dev.ckeditor.com/ticket/11393).
1957 if ( hadHiddenSelectionContainer
)
1958 fixRangesAfterHiddenSelectionContainer( ranges
, this.root
);
1960 if ( !ranges
.length
)
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();
1969 this.selectRanges( ranges
);
1971 // Return to the previously focused element.
1972 focused
&& !focused
.equals( this.root
) && focused
.focus();
1976 // Handle special case - automatic fake selection on non-editable elements.
1977 var receiver
= getNonEditableFakeSelectionReceiver( ranges
);
1980 this.fake( receiver
);
1984 // Handle special case - fake selection of table cells.
1985 if ( editor
&& editor
.plugins
.tableselection
&&
1986 CKEDITOR
.plugins
.tableselection
.isSupportedEnvironment
&&
1987 isTableSelection( ranges
) && !isSelectingTable
1989 performFakeTableSelection
.call( this, ranges
);
1993 if ( isMSSelection
) {
1994 var notWhitespaces
= CKEDITOR
.dom
.walker
.whitespaces( true ),
1995 fillerTextRegex
= /\ufeff|\u00a0/,
1996 nonCells
= { table: 1, tbody: 1, tr: 1 };
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
);
2004 var range
= ranges
[ 0 ];
2005 var collapsed
= range
.collapsed
,
2006 isStartMarkerAlone
, dummySpan
, ieRange
;
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() ) ) {
2014 ieRange
= selected
.$.createControlRange();
2015 ieRange
.addElement( selected
.$ );
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 );
2026 // The range might get collapsed (http://dev.ckeditor.com/ticket/7975). Update cached variable.
2027 collapsed
= range
.collapsed
;
2030 var bookmark
= range
.createBookmark();
2032 // Create marker tags for the start and end boundaries.
2033 var startNode
= bookmark
.startNode
;
2037 endNode
= bookmark
.endNode
;
2039 // Create the main range which will be used for the selection.
2040 ieRange
= range
.document
.$.body
.createTextRange();
2042 // Position the range at the start boundary.
2043 ieRange
.moveToElementText( startNode
.$ );
2044 ieRange
.moveStart( 'character', 1 );
2047 // Create a tool range for the end.
2048 var ieRangeEnd
= range
.document
.$.body
.createTextRange();
2050 // Position the tool range at the end.
2051 ieRangeEnd
.moveToElementText( endNode
.$ );
2053 // Move the end boundary of the main range to match the tool range.
2054 ieRange
.setEndPoint( 'EndToEnd', ieRangeEnd
);
2055 ieRange
.moveEnd( 'character', -1 );
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' ) ) ) );
2066 // Append a temporary <span></span> before the selection.
2067 // This is needed to avoid IE destroying selections inside empty
2068 // inline elements, like <b></b> (http://dev.ckeditor.com/ticket/253).
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' );
2072 dummySpan
.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See http://dev.ckeditor.com/ticket/1359.
2073 dummySpan
.insertBefore( startNode
);
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
2079 // \ufeff = Zero Width No-Break Space (U+FEFF). (http://dev.ckeditor.com/ticket/1359)
2080 range
.document
.createText( '\ufeff' ).insertBefore( startNode
);
2084 // Remove the markers (reset the position, because of the changes in the DOM tree).
2085 range
.setStartBefore( startNode
);
2089 if ( isStartMarkerAlone
) {
2090 // Move the selection start to include the temporary \ufeff.
2091 ieRange
.moveStart( 'character', -1 );
2095 // Remove our temporary stuff.
2096 range
.document
.$.selection
.clear();
2101 range
.moveToPosition( dummySpan
, CKEDITOR
.POSITION_BEFORE_START
);
2104 range
.setEndBefore( endNode
);
2109 var sel
= this.getNative();
2111 // getNative() returns null if iframe is "display:none" in FF. (http://dev.ckeditor.com/ticket/6577)
2115 this.removeAllRanges();
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
);
2127 // Don't confused by Firefox adjacent multi-ranges
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();
2134 // The following cases has to be considered:
2135 // 1. <span contenteditable="false">[placeholder]</span>
2136 // 2. <input contenteditable="false" type="radio"/> (http://dev.ckeditor.com/ticket/6621)
2137 if ( ancestor
.isReadOnly() || enclosed
&& enclosed
.isReadOnly() ) {
2138 right
.setStart( left
.startContainer
, left
.startOffset
);
2139 ranges
.splice( i
--, 1 );
2145 range
= ranges
[ i
];
2147 var nativeRange
= this.document
.$.createRange();
2149 if ( range
.collapsed
&& CKEDITOR
.env
.webkit
&& rangeRequiresFix( range
) ) {
2150 // Append a zero-width space so WebKit will not try to
2151 // move the selection by itself (http://dev.ckeditor.com/ticket/1272).
2152 var fillingChar
= createFillingCharSequenceNode( this.root
);
2153 range
.insertNode( fillingChar
);
2155 next
= fillingChar
.getNext();
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
);
2164 range
.moveToPosition( fillingChar
, CKEDITOR
.POSITION_AFTER_END
);
2168 nativeRange
.setStart( range
.startContainer
.$, range
.startOffset
);
2171 nativeRange
.setEnd( range
.endContainer
.$, range
.endOffset
);
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
);
2184 // Select the range.
2185 sel
.addRange( nativeRange
);
2191 // Fakes the IE DOM event "selectionchange" on editable.
2192 this.root
.fire( 'selectionchange' );
2196 * Makes a "fake selection" of an element.
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.
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.
2207 fake: function( element
, ariaLabel
) {
2208 var editor
= this.root
.editor
;
2210 // Attempt to retrieve aria-label if possible (http://dev.ckeditor.com/ticket/14539).
2211 if ( ariaLabel
=== undefined && element
.hasAttribute( 'aria-label' ) ) {
2212 ariaLabel
= element
.getAttribute( 'aria-label' );
2215 // Cleanup after previous selection - e.g. remove hidden sel container.
2218 hideSelection( editor
, ariaLabel
);
2220 // Set this value after executing hiseSelection, because it may
2221 // cause reset() which overwrites cache.
2222 var cache
= this._
.cache
;
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
);
2230 // Put this element in the cache.
2231 cache
.selectedElement
= cache
.startElement
= element
;
2232 cache
.type
= CKEDITOR
.SELECTION_ELEMENT
;
2234 // Properties that will not be available when isFake.
2235 cache
.selectedText
= cache
.nativeSel
= null;
2238 this.rev
= nextRev
++;
2240 // Save this selection, so it can be returned by editor.getSelection().
2241 editor
._
.fakeSelection
= this;
2243 // Fire selectionchange, just like a normal selection.
2244 this.root
.fire( 'selectionchange' );
2248 * Checks whether selection is placed in hidden element.
2250 * This method is to be used to verify whether fake selection
2251 * (see {@link #fake}) is still hidden.
2253 * **Note:** this method should be executed on real selection - e.g.:
2255 * editor.getSelection( true ).isHidden();
2257 * @returns {Boolean}
2259 isHidden: function() {
2260 var el
= this.getCommonAncestor();
2262 if ( el
&& el
.type
== CKEDITOR
.NODE_TEXT
)
2263 el
= el
.getParent();
2265 return !!( el
&& el
.data( 'cke-hidden-sel' ) );
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).
2273 * editor.getSelection().isInTable();
2276 * @param {Boolean} [allowPartialSelection=false] Whether a partial cell selection should be included.
2278 * @returns {Boolean}
2280 isInTable: function( allowPartialSelection
) {
2281 return isTableSelection( this.getRanges(), allowPartialSelection
);
2285 * Checks if the selection contains only one range which is collapsed.
2287 * if ( editor.getSelection().isCollapsed() ) {
2288 * // Do something when the selection is collapsed.
2292 * @returns {Boolean}
2294 isCollapsed: function() {
2295 var ranges
= this.getRanges();
2297 return ranges
.length
=== 1 && ranges
[ 0 ].collapsed
;
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.
2306 * var bookmarks = editor.getSelection().createBookmarks();
2308 * @returns {Array} Array of bookmarks for each range.
2310 createBookmarks: function( serializable
) {
2311 var bookmark
= this.getRanges().createBookmarks( serializable
);
2312 this.isFake
&& ( bookmark
.isFake
= 1 );
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.
2322 * var bookmarks = editor.getSelection().createBookmarks2();
2324 * @returns {Array} Array of bookmarks for each range.
2326 createBookmarks2: function( normalized
) {
2327 var bookmark
= this.getRanges().createBookmarks2( normalized
);
2328 this.isFake
&& ( bookmark
.isFake
= 1 );
2333 * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}.
2335 * var bookmarks = editor.getSelection().createBookmarks();
2336 * editor.getSelection().selectBookmarks( bookmarks );
2338 * @param {Array} bookmarks The bookmarks representing ranges to be selected.
2339 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected.
2341 selectBookmarks: function( bookmarks
) {
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
);
2351 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2352 if ( bookmarks
.isFake
) {
2353 node
= isTableSelection( ranges
) ? ranges
[ 0 ]._getTableElement() : ranges
[ 0 ].getEnclosedNode();
2355 if ( !node
|| node
.type
!= CKEDITOR
.NODE_ELEMENT
) {
2356 CKEDITOR
.warn( 'selection-not-fake' );
2357 bookmarks
.isFake
= 0;
2361 if ( bookmarks
.isFake
&& !isTableSelection( ranges
) ) {
2364 this.selectRanges( ranges
);
2371 * Retrieves the common ancestor node of the first range and the last range.
2373 * var ancestor = editor.getSelection().getCommonAncestor();
2375 * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty.
2377 getCommonAncestor: function() {
2378 var ranges
= this.getRanges();
2379 if ( !ranges
.length
)
2382 var startNode
= ranges
[ 0 ].startContainer
,
2383 endNode
= ranges
[ ranges
.length
- 1 ].endContainer
;
2384 return startNode
.getCommonAncestor( endNode
);
2388 * Moves the scrollbar to the starting position of the current selection.
2390 * editor.getSelection().scrollIntoView();
2392 scrollIntoView: function() {
2393 // Scrolls the first range into view.
2394 if ( this.type
!= CKEDITOR
.SELECTION_NONE
)
2395 this.getRanges()[ 0 ].scrollIntoView();
2399 * Remove all the selection ranges from the document.
2401 removeAllRanges: function() {
2402 // Don't clear selection outside this selection's root (http://dev.ckeditor.com/ticket/11500).
2403 if ( this.getType() == CKEDITOR
.SELECTION_NONE
)
2406 var nativ
= this.getNative();
2409 nativ
&& nativ
[ isMSSelection
? 'empty' : 'removeAllRanges' ]();
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.
2426 * @event selectionChange
2427 * @member CKEDITOR.editor
2428 * @param {CKEDITOR.editor} editor This editor instance.
2430 * @param {CKEDITOR.dom.selection} data.selection
2431 * @param {CKEDITOR.dom.elementPath} data.path
2435 * Selection's revision. This value is incremented every time new
2436 * selection is created or existing one is modified.
2440 * @property {Number} rev
2444 * Document in which selection is anchored.
2447 * @property {CKEDITOR.dom.document} document
2451 * Selection's root element.
2454 * @property {CKEDITOR.dom.element} root
2458 * Whether selection is locked (cannot be modified).
2460 * See {@link #lock} and {@link #unlock} methods.
2463 * @property {Boolean} isLocked
2467 * Whether selection is a fake selection.
2469 * See {@link #fake} method.
2472 * @property {Boolean} isFake