2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 // #### checkSelectionChange : START
9 // The selection change check basically saves the element parent tree of
10 // the current node and check it on successive requests. If there is any
11 // change on the tree, then the selectionChange event gets fired.
12 function checkSelectionChange() {
13 // A possibly available fake-selection.
14 var sel
= this._
.fakeSelection
,
18 realSel
= this.getSelection( 1 );
20 // If real (not locked/stored) selection was moved from hidden container,
21 // then the fake-selection must be invalidated.
22 if ( !realSel
|| !realSel
.isHidden() ) {
23 // Remove the cache from fake-selection references in use elsewhere.
26 // Have the code using the native selection.
31 // If not fake-selection is available then get the native selection.
33 sel
= realSel
|| this.getSelection( 1 );
35 // Editor may have no selection at all.
36 if ( !sel
|| sel
.getType() == CKEDITOR
.SELECTION_NONE
)
40 this.fire( 'selectionCheck', sel
);
42 var currentPath
= this.elementPath();
43 if ( !currentPath
.compare( this._
.selectionPreviousPath
) ) {
44 // Cache the active element, which we'll eventually lose on Webkit.
45 if ( CKEDITOR
.env
.webkit
)
46 this._
.previousActive
= this.document
.getActive();
48 this._
.selectionPreviousPath
= currentPath
;
49 this.fire( 'selectionChange', { selection: sel
, path: currentPath
} );
53 var checkSelectionChangeTimer
, checkSelectionChangeTimeoutPending
;
55 function checkSelectionChangeTimeout() {
56 // Firing the "OnSelectionChange" event on every key press started to
57 // be too slow. This function guarantees that there will be at least
58 // 200ms delay between selection checks.
60 checkSelectionChangeTimeoutPending
= true;
62 if ( checkSelectionChangeTimer
)
65 checkSelectionChangeTimeoutExec
.call( this );
67 checkSelectionChangeTimer
= CKEDITOR
.tools
.setTimeout( checkSelectionChangeTimeoutExec
, 200, this );
70 function checkSelectionChangeTimeoutExec() {
71 checkSelectionChangeTimer
= null;
73 if ( checkSelectionChangeTimeoutPending
) {
74 // Call this with a timeout so the browser properly moves the
75 // selection after the mouseup. It happened that the selection was
76 // being moved after the mouseup when clicking inside selected text
78 CKEDITOR
.tools
.setTimeout( checkSelectionChange
, 0, this );
80 checkSelectionChangeTimeoutPending
= false;
84 // #### checkSelectionChange : END
86 var isVisible
= CKEDITOR
.dom
.walker
.invisible( 1 );
88 // May absorb the caret if:
89 // * is a visible node,
90 // * is a non-empty element (this rule will accept elements like <strong></strong> because they
91 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
93 function mayAbsorbCaret( node
) {
94 if ( isVisible( node
) )
97 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& !node
.is( CKEDITOR
.dtd
.$empty
) )
103 function rangeRequiresFix( range
) {
104 // Whether we must prevent from absorbing caret by this context node.
105 // Also checks whether there's an editable position next to that node.
106 function ctxRequiresFix( node
, isAtEnd
) {
107 // It's ok for us if a text node absorbs the caret, because
108 // the caret container element isn't changed then.
109 if ( !node
|| node
.type
== CKEDITOR
.NODE_TEXT
)
112 var testRng
= range
.clone();
113 return testRng
[ 'moveToElementEdit' + ( isAtEnd
? 'End' : 'Start' ) ]( node
);
116 // Range root must be the editable element, it's to avoid creating filler char
117 // on any temporary internal selection.
118 if ( !( range
.root
instanceof CKEDITOR
.editable
) )
121 var ct
= range
.startContainer
;
123 var previous
= range
.getPreviousNode( mayAbsorbCaret
, null, ct
),
124 next
= range
.getNextNode( mayAbsorbCaret
, null, ct
);
126 // Any adjacent text container may absorb the caret, e.g.
127 // <p><strong>text</strong>^foo</p>
128 // <p>foo^<strong>text</strong></p>
129 // <div>^<p>foo</p></div>
130 if ( ctxRequiresFix( previous
) || ctxRequiresFix( next
, 1 ) )
133 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222)
134 // If you found this line confusing check #12655.
135 if ( !( previous
|| next
) && !( ct
.type
== CKEDITOR
.NODE_ELEMENT
&& ct
.isBlockBoundary() && ct
.getBogus() ) )
141 function createFillingCharSequenceNode( editable
) {
142 removeFillingCharSequenceNode( editable
, false );
144 var fillingChar
= editable
.getDocument().createText( fillingCharSequence
);
145 editable
.setCustomData( 'cke-fillingChar', fillingChar
);
150 // Checks if a filling char has been used, eventualy removing it (#1272).
151 function checkFillingCharSequenceNodeReady( editable
) {
152 var fillingChar
= editable
.getCustomData( 'cke-fillingChar' );
155 // Use this flag to avoid removing the filling char right after
157 if ( fillingChar
.getCustomData( 'ready' ) ) {
158 removeFillingCharSequenceNode( editable
);
160 fillingChar
.setCustomData( 'ready', 1 );
165 function removeFillingCharSequenceNode( editable
, keepSelection
) {
166 var fillingChar
= editable
&& editable
.removeCustomData( 'cke-fillingChar' );
169 // Text selection position might get mangled by
170 // subsequent dom modification, save it now for restoring. (#8617)
171 if ( keepSelection
!== false ) {
172 var sel
= editable
.getDocument().getSelection().getNative(),
174 range
= sel
&& sel
.type
!= 'None' && sel
.getRangeAt( 0 ),
175 fillingCharSeqLength
= fillingCharSequence
.length
;
177 // If there's some text other than the sequence in the FC text node and the range
178 // intersects with that node...
179 if ( fillingChar
.getLength() > fillingCharSeqLength
&& range
&& range
.intersectsNode( fillingChar
.$ ) ) {
180 var bm
= createNativeSelectionBookmark( sel
);
182 // Correct start offset anticipating the removal of FC.
183 if ( sel
.anchorNode
== fillingChar
.$ && sel
.anchorOffset
> fillingCharSeqLength
) {
184 bm
[ 0 ].offset
-= fillingCharSeqLength
;
187 // Correct end offset anticipating the removal of FC.
188 if ( sel
.focusNode
== fillingChar
.$ && sel
.focusOffset
> fillingCharSeqLength
) {
189 bm
[ 1 ].offset
-= fillingCharSeqLength
;
194 // We can't simply remove the filling node because the user
195 // will actually enlarge it when typing, so we just remove the
196 // invisible char from it.
197 fillingChar
.setText( removeFillingCharSequenceString( fillingChar
.getText(), 1 ) );
199 // Restore the bookmark preserving selection's direction.
201 moveNativeSelectionToBookmark( editable
.getDocument().$, bm
);
207 function removeFillingCharSequenceString( str
, nbspAware
) {
209 return str
.replace( fillingCharSequenceRegExp
, function( m
, p
) {
210 // #10291 if filling char is followed by a space replace it with NBSP.
211 return p
? '\xa0' : '';
214 return str
.replace( fillingCharSequence
, '' );
218 function createNativeSelectionBookmark( sel
) {
220 { node: sel
.anchorNode
, offset: sel
.anchorOffset
},
221 { node: sel
.focusNode
, offset: sel
.focusOffset
}
225 function moveNativeSelectionToBookmark( document
, bm
) {
226 var sel
= document
.getSelection(),
227 range
= document
.createRange();
229 range
.setStart( bm
[ 0 ].node
, bm
[ 0 ].offset
);
230 range
.collapse( true );
231 sel
.removeAllRanges();
232 sel
.addRange( range
);
233 sel
.extend( bm
[ 1 ].node
, bm
[ 1 ].offset
);
236 // Creates cke_hidden_sel container and puts real selection there.
237 function hideSelection( editor
) {
238 var style
= CKEDITOR
.env
.ie
? 'display:none' : 'position:fixed;top:0;left:-1000px',
239 hiddenEl
= CKEDITOR
.dom
.element
.createFromHtml(
240 '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style
+ '"> </div>',
243 editor
.fire( 'lockSnapshot' );
245 editor
.editable().append( hiddenEl
);
247 // Always use real selection to avoid overriding locked one (http://dev.ckeditor.com/ticket/11104#comment:13).
248 var sel
= editor
.getSelection( 1 ),
249 range
= editor
.createRange(),
250 // Cancel selectionchange fired by selectRanges - prevent from firing selectionChange.
251 listener
= sel
.root
.on( 'selectionchange', function( evt
) {
255 range
.setStartAt( hiddenEl
, CKEDITOR
.POSITION_AFTER_START
);
256 range
.setEndAt( hiddenEl
, CKEDITOR
.POSITION_BEFORE_END
);
257 sel
.selectRanges( [ range
] );
259 listener
.removeListener();
261 editor
.fire( 'unlockSnapshot' );
263 // Set this value at the end, so reset() executed by selectRanges()
264 // will clean up old hidden selection container.
265 editor
._
.hiddenSelectionContainer
= hiddenEl
;
268 function removeHiddenSelectionContainer( editor
) {
269 var hiddenEl
= editor
._
.hiddenSelectionContainer
;
272 var isDirty
= editor
.checkDirty();
274 editor
.fire( 'lockSnapshot' );
276 editor
.fire( 'unlockSnapshot' );
278 !isDirty
&& editor
.resetDirty();
281 delete editor
._
.hiddenSelectionContainer
;
284 // Object containing keystroke handlers for fake selection.
285 var fakeSelectionDefaultKeystrokeHandlers
= ( function() {
286 function leave( right
) {
287 return function( evt
) {
288 var range
= evt
.editor
.createRange();
290 // Move selection only if there's a editable place for it.
291 // It no, then do nothing (keystroke will be blocked, widget selection kept).
292 if ( range
.moveToClosestEditablePosition( evt
.selected
, right
) )
293 evt
.editor
.getSelection().selectRanges( [ range
] );
300 function del( right
) {
301 return function( evt
) {
302 var editor
= evt
.editor
,
303 range
= editor
.createRange(),
306 // If haven't found place for caret on the default side,
307 // try to find it on the other side.
308 if ( !( found
= range
.moveToClosestEditablePosition( evt
.selected
, right
) ) )
309 found
= range
.moveToClosestEditablePosition( evt
.selected
, !right
);
312 editor
.getSelection().selectRanges( [ range
] );
314 // Save the state before removing selected element.
315 editor
.fire( 'saveSnapshot' );
317 evt
.selected
.remove();
319 // Haven't found any editable space before removing element,
320 // try to place the caret anywhere (most likely, in empty editable).
322 range
.moveToElementEditablePosition( editor
.editable() );
323 editor
.getSelection().selectRanges( [ range
] );
326 editor
.fire( 'saveSnapshot' );
333 var leaveLeft
= leave(),
334 leaveRight
= leave( 1 );
337 37: leaveLeft
, // LEFT
339 39: leaveRight
, // RIGHT
340 40: leaveRight
, // DOWN
341 8: del(), // BACKSPACE
342 46: del( 1 ) // DELETE
346 // Handle left, right, delete and backspace keystrokes next to non-editable elements
347 // by faking selection on them.
348 function getOnKeyDownListener( editor
) {
349 var keystrokes
= { 37: 1, 39: 1, 8: 1, 46: 1 };
351 return function( evt
) {
352 var keystroke
= evt
.data
.getKeystroke();
354 // Handle only left/right/del/bspace keys.
355 if ( !keystrokes
[ keystroke
] )
358 var sel
= editor
.getSelection(),
359 ranges
= sel
.getRanges(),
362 // Handle only single range and it has to be collapsed.
363 if ( ranges
.length
!= 1 || !range
.collapsed
)
366 var next
= range
[ keystroke
< 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ]();
368 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.getAttribute( 'contenteditable' ) == 'false' ) {
369 editor
.getSelection().fake( next
);
370 evt
.data
.preventDefault();
376 // If fake selection should be applied this function will return instance of
377 // CKEDITOR.dom.element which should gain fake selection.
378 function getNonEditableFakeSelectionReceiver( ranges
) {
379 var enclosedNode
, shrinkedNode
, clone
, range
;
381 if ( ranges
.length
== 1 && !( range
= ranges
[ 0 ] ).collapsed
&&
382 ( enclosedNode
= range
.getEnclosedNode() ) && enclosedNode
.type
== CKEDITOR
.NODE_ELEMENT
) {
383 // So far we can't say that enclosed element is non-editable. Before checking,
384 // we'll shrink range (clone). Shrinking will stop on non-editable range, or
385 // innermost element (#11114).
386 clone
= range
.clone();
387 clone
.shrink( CKEDITOR
.SHRINK_ELEMENT
, true );
389 // If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements).
390 if ( ( shrinkedNode
= clone
.getEnclosedNode() ) && shrinkedNode
.type
== CKEDITOR
.NODE_ELEMENT
)
391 enclosedNode
= shrinkedNode
;
393 if ( enclosedNode
.getAttribute( 'contenteditable' ) == 'false' )
398 // Fix ranges which may end after hidden selection container.
399 // Note: this function may only be used if hidden selection container
400 // is not in DOM any more.
401 function fixRangesAfterHiddenSelectionContainer( ranges
, root
) {
403 for ( var i
= 0; i
< ranges
.length
; ++i
) {
405 if ( range
.endContainer
.equals( root
) ) {
406 // We can use getChildCount() because hidden selection container is not in DOM.
407 range
.endOffset
= Math
.min( range
.endOffset
, root
.getChildCount() );
412 // Extract only editable part or ranges.
413 // Note: this function modifies ranges list!
414 // @param {CKEDITOR.dom.rangeList} ranges
415 function extractEditableRanges( ranges
) {
416 for ( var i
= 0; i
< ranges
.length
; i
++ ) {
417 var range
= ranges
[ i
];
419 // Drop range spans inside one ready-only node.
420 var parent
= range
.getCommonAncestor();
421 if ( parent
.isReadOnly() )
422 ranges
.splice( i
, 1 );
424 if ( range
.collapsed
)
427 // Range may start inside a non-editable element,
428 // replace the range start after it.
429 if ( range
.startContainer
.isReadOnly() ) {
430 var current
= range
.startContainer
,
434 isElement
= current
.type
== CKEDITOR
.NODE_ELEMENT
;
436 if ( ( isElement
&& current
.is( 'body' ) ) || !current
.isReadOnly() )
439 if ( isElement
&& current
.getAttribute( 'contentEditable' ) == 'false' )
440 range
.setStartAfter( current
);
442 current
= current
.getParent();
446 var startContainer
= range
.startContainer
,
447 endContainer
= range
.endContainer
,
448 startOffset
= range
.startOffset
,
449 endOffset
= range
.endOffset
,
450 walkerRange
= range
.clone();
452 // Enlarge range start/end with text node to avoid walker
453 // being DOM destructive, it doesn't interfere our checking
454 // of elements below as well.
455 if ( startContainer
&& startContainer
.type
== CKEDITOR
.NODE_TEXT
) {
456 if ( startOffset
>= startContainer
.getLength() )
457 walkerRange
.setStartAfter( startContainer
);
459 walkerRange
.setStartBefore( startContainer
);
462 if ( endContainer
&& endContainer
.type
== CKEDITOR
.NODE_TEXT
) {
464 walkerRange
.setEndBefore( endContainer
);
466 walkerRange
.setEndAfter( endContainer
);
469 // Looking for non-editable element inside the range.
470 var walker
= new CKEDITOR
.dom
.walker( walkerRange
);
471 walker
.evaluator = function( node
) {
472 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isReadOnly() ) {
473 var newRange
= range
.clone();
474 range
.setEndBefore( node
);
476 // Drop collapsed range around read-only elements,
477 // it make sure the range list empty when selecting
478 // only non-editable elements.
479 if ( range
.collapsed
)
480 ranges
.splice( i
--, 1 );
482 // Avoid creating invalid range.
483 if ( !( node
.getPosition( walkerRange
.endContainer
) & CKEDITOR
.POSITION_CONTAINS
) ) {
484 newRange
.setStartAfter( node
);
485 if ( !newRange
.collapsed
)
486 ranges
.splice( i
+ 1, 0, newRange
);
501 // Setup all editor instances for the necessary selection hooks.
502 CKEDITOR
.on( 'instanceCreated', function( ev
) {
503 var editor
= ev
.editor
;
505 editor
.on( 'contentDom', function() {
506 var doc
= editor
.document
,
507 outerDoc
= CKEDITOR
.document
,
508 editable
= editor
.editable(),
509 body
= doc
.getBody(),
510 html
= doc
.getDocumentElement();
512 var isInline
= editable
.isInline();
517 // Give the editable an initial selection on first focus,
518 // put selection at a consistent position at the start
519 // of the contents. (#9507)
520 if ( CKEDITOR
.env
.gecko
) {
521 editable
.attachListener( editable
, 'focus', function( evt
) {
522 evt
.removeListener();
524 if ( restoreSel
!== 0 ) {
525 var nativ
= editor
.getSelection().getNative();
526 // Do it only if the native selection is at an unwanted
527 // place (at the very start of the editable). #10119
528 if ( nativ
&& nativ
.isCollapsed
&& nativ
.anchorNode
== editable
.$ ) {
529 var rng
= editor
.createRange();
530 rng
.moveToElementEditStart( editable
);
537 // Plays the magic here to restore/save dom selection on editable focus/blur.
538 editable
.attachListener( editable
, CKEDITOR
.env
.webkit
? 'DOMFocusIn' : 'focus', function() {
539 // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
540 // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
541 // for the same active element, what will e.g. mean restoring after displaying dialog.
542 if ( restoreSel
&& CKEDITOR
.env
.webkit
)
543 restoreSel
= editor
._
.previousActive
&& editor
._
.previousActive
.equals( doc
.getActive() );
545 editor
.unlockSelection( restoreSel
);
549 // Disable selection restoring when clicking in.
550 editable
.attachListener( editable
, 'mousedown', function() {
554 // Save a cloned version of current selection.
556 lastSel
= new CKEDITOR
.dom
.selection( editor
.getSelection() );
560 // Browsers could loose the selection once the editable lost focus,
561 // in such case we need to reproduce it by saving a locked selection
562 // and restoring it upon focus gain.
563 if ( CKEDITOR
.env
.ie
|| isInline
) {
564 // For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event.
565 // For the rest, a more frequent check is required for each selection change made.
567 editable
.attachListener( editable
, 'beforedeactivate', saveSel
, null, null, -1 );
569 editable
.attachListener( editor
, 'selectionCheck', saveSel
, null, null, -1 );
571 // Lock the selection and mark it to be restored.
572 // On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be
573 // fired when nested editable is blurred.
574 editable
.attachListener( editable
, CKEDITOR
.env
.webkit
? 'DOMFocusOut' : 'blur', function() {
575 editor
.lockSelection( lastSel
);
579 // Disable selection restoring when clicking in.
580 editable
.attachListener( editable
, 'mousedown', function() {
585 // The following selection-related fixes only apply to classic (`iframe`-based) editable.
586 if ( CKEDITOR
.env
.ie
&& !isInline
) {
588 editable
.attachListener( editable
, 'mousedown', function( evt
) {
589 // IE scrolls document to top on right mousedown
590 // when editor has no focus, remember this scroll
591 // position and revert it before context menu opens. (#5778)
592 if ( evt
.data
.$.button
== 2 ) {
593 var sel
= editor
.document
.getSelection();
594 if ( !sel
|| sel
.getType() == CKEDITOR
.SELECTION_NONE
)
595 scroll
= editor
.window
.getScrollPosition();
599 editable
.attachListener( editable
, 'mouseup', function( evt
) {
600 // Restore recorded scroll position when needed on right mouseup.
601 if ( evt
.data
.$.button
== 2 && scroll
) {
602 editor
.document
.$.documentElement
.scrollLeft
= scroll
.x
;
603 editor
.document
.$.documentElement
.scrollTop
= scroll
.y
;
608 // When content doc is in standards mode, IE doesn't focus the editor when
609 // clicking at the region below body (on html element) content, we emulate
610 // the normal behavior on old IEs. (#1659, #7932)
611 if ( doc
.$.compatMode
!= 'BackCompat' ) {
612 if ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.ie6Compat
) {
613 html
.on( 'mousedown', function( evt
) {
616 // Expand the text range along with mouse move.
617 function onHover( evt
) {
620 // Read the current cursor.
621 var rngEnd
= body
.$.createTextRange();
623 moveRangeToPoint( rngEnd
, evt
.clientX
, evt
.clientY
);
625 // Handle drag directions.
627 startRng
.compareEndPoints( 'StartToStart', rngEnd
) < 0 ?
628 'EndToEnd' : 'StartToStart', rngEnd
);
630 // Update selection with new range.
635 function removeListeners() {
636 outerDoc
.removeListener( 'mouseup', onSelectEnd
);
637 html
.removeListener( 'mouseup', onSelectEnd
);
640 function onSelectEnd() {
641 html
.removeListener( 'mousemove', onHover
);
644 // Make it in effect on mouse up. (#9022)
649 // We're sure that the click happens at the region
650 // below body, but not on scrollbar.
651 if ( evt
.getTarget().is( 'html' ) &&
652 evt
.$.y
< html
.$.clientHeight
&&
653 evt
.$.x
< html
.$.clientWidth
) {
654 // Start to build the text range.
655 var textRng
= body
.$.createTextRange();
656 moveRangeToPoint( textRng
, evt
.$.clientX
, evt
.$.clientY
);
658 // Records the dragging start of the above text range.
659 var startRng
= textRng
.duplicate();
661 html
.on( 'mousemove', onHover
);
662 outerDoc
.on( 'mouseup', onSelectEnd
);
663 html
.on( 'mouseup', onSelectEnd
);
668 // It's much simpler for IE8+, we just need to reselect the reported range.
669 // This hack does not work on IE>=11 because there's no old selection&range APIs.
670 if ( CKEDITOR
.env
.version
> 7 && CKEDITOR
.env
.version
< 11 ) {
671 html
.on( 'mousedown', function( evt
) {
672 if ( evt
.data
.getTarget().is( 'html' ) ) {
673 // Limit the text selection mouse move inside of editable. (#9715)
674 outerDoc
.on( 'mouseup', onSelectEnd
);
675 html
.on( 'mouseup', onSelectEnd
);
682 // We check the selection change:
683 // 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code)
684 // 2. After the accomplish of keyboard and mouse events.
685 editable
.attachListener( editable
, 'selectionchange', checkSelectionChange
, editor
);
686 editable
.attachListener( editable
, 'keyup', checkSelectionChangeTimeout
, editor
);
687 // Always fire the selection change on focus gain.
688 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
689 // we need synchronization between those listeners to not lost cached editor._.previousActive property
690 // (which is updated on selectionCheck).
691 editable
.attachListener( editable
, CKEDITOR
.env
.webkit
? 'DOMFocusIn' : 'focus', function() {
692 editor
.forceNextSelectionCheck();
693 editor
.selectionChange( 1 );
696 // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
697 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
698 // has to be initialized in editable, but for mouseup we listen on document element.
699 if ( isInline
&& ( CKEDITOR
.env
.webkit
|| CKEDITOR
.env
.gecko
) ) {
701 editable
.attachListener( editable
, 'mousedown', function() {
704 editable
.attachListener( doc
.getDocumentElement(), 'mouseup', function() {
706 checkSelectionChangeTimeout
.call( editor
);
710 // In all other cases listen on simple mouseup over editable, as we did before #9699.
712 // Use document instead of editable in non-IEs for observing mouseup
713 // since editable won't fire the event if selection process started within iframe and ended out
714 // of the editor (#9851).
716 editable
.attachListener( CKEDITOR
.env
.ie
? editable : doc
.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout
, editor
);
719 if ( CKEDITOR
.env
.webkit
) {
720 // Before keystroke is handled by editor, check to remove the filling char.
721 editable
.attachListener( doc
, 'keydown', function( evt
) {
722 var key
= evt
.data
.getKey();
723 // Remove the filling char before some keys get
724 // executed, so they'll not get blocked by it.
731 case 37: // LEFT-ARROW
732 case 39: // RIGHT-ARROW
736 removeFillingCharSequenceNode( editable
);
742 // Automatically select non-editable element when navigating into
743 // it by left/right or backspace/del keys.
744 editable
.attachListener( editable
, 'keydown', getOnKeyDownListener( editor
), null, null, -1 );
746 function moveRangeToPoint( range
, x
, y
) {
747 // Error prune in IE7. (#9034, #9110)
749 range
.moveToPoint( x
, y
);
753 function removeListeners() {
754 outerDoc
.removeListener( 'mouseup', onSelectEnd
);
755 html
.removeListener( 'mouseup', onSelectEnd
);
758 function onSelectEnd() {
761 // The event is not fired when clicking on the scrollbars,
762 // so we can safely check the following to understand
763 // whether the empty space following <body> has been clicked.
764 var sel
= CKEDITOR
.document
.$.selection
,
765 range
= sel
.createRange();
767 // The selection range is reported on host, but actually it should applies to the content doc.
768 if ( sel
.type
!= 'None' && range
.parentElement().ownerDocument
== doc
.$ )
773 editor
.on( 'setData', function() {
774 // Invalidate locked selection when unloading DOM.
775 // (#9521, #5217#comment:32 and #11500#comment:11)
776 editor
.unlockSelection();
778 // Webkit's selection will mess up after the data loading.
779 if ( CKEDITOR
.env
.webkit
)
783 // Catch all the cases which above setData listener couldn't catch.
784 // For example: switching to source mode and destroying editor.
785 editor
.on( 'contentDomUnload', function() {
786 editor
.unlockSelection();
789 // IE9 might cease to work if there's an object selection inside the iframe (#7639).
790 if ( CKEDITOR
.env
.ie9Compat
)
791 editor
.on( 'beforeDestroy', clearSelection
, null, null, 9 );
793 // Check selection change on data reload.
794 editor
.on( 'dataReady', function() {
795 // Clean up fake selection after setting data.
796 delete editor
._
.fakeSelection
;
797 delete editor
._
.hiddenSelectionContainer
;
799 editor
.selectionChange( 1 );
802 // When loaded data are ready check whether hidden selection container was not loaded.
803 editor
.on( 'loadSnapshot', function() {
804 var isElement
= CKEDITOR
.dom
.walker
.nodeType( CKEDITOR
.NODE_ELEMENT
),
805 // TODO replace with el.find() which will be introduced in #9764,
806 // because it may happen that hidden sel container won't be the last element.
807 last
= editor
.editable().getLast( isElement
);
809 if ( last
&& last
.hasAttribute( 'data-cke-hidden-sel' ) ) {
812 // Firefox does a very unfortunate thing. When a non-editable element is the only
813 // element in the editable, when we remove the hidden selection container, Firefox
814 // will insert a bogus <br> at the beginning of the editable...
815 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201
817 // This behavior is never desired because this <br> pushes the content lower, but in
818 // this case it is especially dangerous, because it happens when a bookmark is being restored.
819 // Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2
820 // what results in errors.
822 // So... let's revert what Firefox broke.
823 if ( CKEDITOR
.env
.gecko
) {
824 var first
= editor
.editable().getFirst( isElement
);
825 if ( first
&& first
.is( 'br' ) && first
.getAttribute( '_moz_editor_bogus_node' ) ) {
830 }, null, null, 100 );
832 editor
.on( 'key', function( evt
) {
833 if ( editor
.mode
!= 'wysiwyg' )
836 var sel
= editor
.getSelection();
840 var handler
= fakeSelectionDefaultKeystrokeHandlers
[ evt
.data
.keyCode
];
842 return handler( { editor: editor
, selected: sel
.getSelectedElement(), selection: sel
, keyEvent: evt
} );
845 function clearSelection() {
846 var sel
= editor
.getSelection();
847 sel
&& sel
.removeAllRanges();
851 // On WebKit only, we need a special "filling" char on some situations
852 // (#1272). Here we set the events that should invalidate that char.
853 if ( CKEDITOR
.env
.webkit
) {
854 CKEDITOR
.on( 'instanceReady', function( evt
) {
855 var editor
= evt
.editor
;
857 editor
.on( 'selectionChange', function() {
858 checkFillingCharSequenceNodeReady( editor
.editable() );
861 editor
.on( 'beforeSetMode', function() {
862 removeFillingCharSequenceNode( editor
.editable() );
865 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
866 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
867 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816).
868 editor
.on( 'getSnapshot', function( evt
) {
870 evt
.data
= removeFillingCharSequenceString( evt
.data
);
872 }, editor
, null, 20 );
874 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
875 // instead of #getData because once removed, FCSeq may leave an empty element,
876 // which should be pruned by the dataProcessor (#13816).
877 // Note: Used low priority to filter when dataProcessor works on strings,
879 editor
.on( 'toDataFormat', function( evt
) {
880 evt
.data
.dataValue
= removeFillingCharSequenceString( evt
.data
.dataValue
);
886 * Check the selection change in editor and potentially fires
887 * the {@link CKEDITOR.editor#event-selectionChange} event.
890 * @member CKEDITOR.editor
891 * @param {Boolean} [checkNow=false] Force the check to happen immediately
892 * instead of coming with a timeout delay (default).
894 CKEDITOR
.editor
.prototype.selectionChange = function( checkNow
) {
895 ( checkNow
? checkSelectionChange : checkSelectionChangeTimeout
).call( this );
899 * Retrieve the editor selection in scope of editable element.
901 * **Note:** Since the native browser selection provides only one single
902 * selection at a time per document, so if editor's editable element has lost focus,
903 * this method will return a null value unless the {@link CKEDITOR.editor#lockSelection}
904 * has been called beforehand so the saved selection is retrieved.
906 * var selection = CKEDITOR.instances.editor1.getSelection();
907 * alert( selection.getType() );
910 * @member CKEDITOR.editor
911 * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one.
912 * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment.
914 CKEDITOR
.editor
.prototype.getSelection = function( forceRealSelection
) {
916 // Check if there exists a locked or fake selection.
917 if ( ( this._
.savedSelection
|| this._
.fakeSelection
) && !forceRealSelection
)
918 return this._
.savedSelection
|| this._
.fakeSelection
;
920 // Editable element might be absent or editor might not be in a wysiwyg mode.
921 var editable
= this.editable();
922 return editable
&& this.mode
== 'wysiwyg' ? new CKEDITOR
.dom
.selection( editable
) : null;
926 * Locks the selection made in the editor in order to make it possible to
927 * manipulate it without browser interference. A locked selection is
928 * cached and remains unchanged until it is released with the
929 * {@link CKEDITOR.editor#unlockSelection} method.
932 * @member CKEDITOR.editor
933 * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked.
934 * @returns {Boolean} `true` if selection was locked.
936 CKEDITOR
.editor
.prototype.lockSelection = function( sel
) {
937 sel
= sel
|| this.getSelection( 1 );
938 if ( sel
.getType() != CKEDITOR
.SELECTION_NONE
) {
939 !sel
.isLocked
&& sel
.lock();
940 this._
.savedSelection
= sel
;
947 * Unlocks the selection made in the editor and locked with the
948 * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection
949 * is no longer cached and can be changed.
952 * @member CKEDITOR.editor
953 * @param {Boolean} [restore] If set to `true`, the selection is
954 * restored back to the selection saved earlier by using the
955 * {@link CKEDITOR.dom.selection#lock} method.
957 CKEDITOR
.editor
.prototype.unlockSelection = function( restore
) {
958 var sel
= this._
.savedSelection
;
960 sel
.unlock( restore
);
961 delete this._
.savedSelection
;
970 * @member CKEDITOR.editor
973 CKEDITOR
.editor
.prototype.forceNextSelectionCheck = function() {
974 delete this._
.selectionPreviousPath
;
978 * Gets the current selection in context of the document's body element.
980 * var selection = CKEDITOR.instances.editor1.document.getSelection();
981 * alert( selection.getType() );
984 * @member CKEDITOR.dom.document
985 * @returns {CKEDITOR.dom.selection} A selection object.
987 CKEDITOR
.dom
.document
.prototype.getSelection = function() {
988 return new CKEDITOR
.dom
.selection( this );
992 * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}.
995 * @returns {CKEDITOR.dom.selection}
996 * @member CKEDITOR.dom.range
998 CKEDITOR
.dom
.range
.prototype.select = function() {
999 var sel
= this.root
instanceof CKEDITOR
.editable
? this.root
.editor
.getSelection() : new CKEDITOR
.dom
.selection( this.root
);
1001 sel
.selectRanges( [ this ] );
1009 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
1010 * alert( 'Nothing is selected' );
1013 * @property {Number} [=1]
1016 CKEDITOR
.SELECTION_NONE
= 1;
1019 * A text or a collapsed selection.
1021 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1022 * alert( 'A text is selected' );
1025 * @property {Number} [=2]
1028 CKEDITOR
.SELECTION_TEXT
= 2;
1031 * Element selection.
1033 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
1034 * alert( 'An element is selected' );
1037 * @property {Number} [=3]
1040 CKEDITOR
.SELECTION_ELEMENT
= 3;
1042 var isMSSelection
= typeof window
.getSelection
!= 'function',
1046 * Manipulates the selection within a DOM element. If the current browser selection
1047 * spans outside of the element, an empty selection object is returned.
1049 * Despite the fact that selection's constructor allows to create selection instances,
1050 * usually it's better to get selection from the editor instance:
1052 * var sel = editor.getSelection();
1054 * See {@link CKEDITOR.editor#getSelection}.
1057 * @constructor Creates a selection class instance.
1059 * // Selection scoped in document.
1060 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document );
1062 * // Selection scoped in element with 'editable' id.
1063 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document.getById( 'editable' ) );
1065 * // Cloning selection.
1066 * var clone = new CKEDITOR.dom.selection( sel );
1068 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element/CKEDITOR.dom.selection} target
1069 * The DOM document/element that the DOM selection is restrained to. Only selection which spans
1070 * within the target element is considered as valid.
1072 * If {@link CKEDITOR.dom.selection} is passed, then its clone will be created.
1074 CKEDITOR
.dom
.selection = function( target
) {
1075 // Target is a selection - clone it.
1076 if ( target
instanceof CKEDITOR
.dom
.selection
) {
1077 var selection
= target
;
1078 target
= target
.root
;
1081 var isElement
= target
instanceof CKEDITOR
.dom
.element
,
1084 this.rev
= selection
? selection
.rev : nextRev
++;
1085 this.document
= target
instanceof CKEDITOR
.dom
.document
? target : target
.getDocument();
1086 this.root
= root
= isElement
? target : this.document
.getBody();
1094 CKEDITOR
.tools
.extend( this._
.cache
, selection
._
.cache
);
1095 this.isFake
= selection
.isFake
;
1096 this.isLocked
= selection
.isLocked
;
1100 // Check whether browser focus is really inside of the editable element.
1102 var nativeSel
= this.getNative(),
1107 if ( nativeSel
.getRangeAt
) {
1108 range
= nativeSel
.rangeCount
&& nativeSel
.getRangeAt( 0 );
1109 rangeParent
= range
&& new CKEDITOR
.dom
.node( range
.commonAncestorContainer
);
1113 // Sometimes, mostly when selection is close to the table or hr,
1114 // IE throws "Unspecified error".
1116 range
= nativeSel
.createRange();
1118 rangeParent
= range
&& CKEDITOR
.dom
.element
.get( range
.item
&& range
.item( 0 ) || range
.parentElement() );
1122 // Selection out of concerned range, empty the selection.
1123 // TODO check whether this condition cannot be reverted to its old
1124 // form (commented out) after we closed #10438.
1125 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1128 ( rangeParent
.type
== CKEDITOR
.NODE_ELEMENT
|| rangeParent
.type
== CKEDITOR
.NODE_TEXT
) &&
1129 ( this.root
.equals( rangeParent
) || this.root
.contains( rangeParent
) )
1132 this._
.cache
.type
= CKEDITOR
.SELECTION_NONE
;
1133 this._
.cache
.startElement
= null;
1134 this._
.cache
.selectedElement
= null;
1135 this._
.cache
.selectedText
= '';
1136 this._
.cache
.ranges
= new CKEDITOR
.dom
.rangeList();
1142 var styleObjectElements
= { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1,
1143 a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 };
1146 var fillingCharSequence
= CKEDITOR
.tools
.repeat( '\u200b', 7 ),
1147 fillingCharSequenceRegExp
= new RegExp( fillingCharSequence
+ '( )?', 'g' );
1149 CKEDITOR
.tools
.extend( CKEDITOR
.dom
.selection
, {
1150 _removeFillingCharSequenceString: removeFillingCharSequenceString
,
1151 _createFillingCharSequenceNode: createFillingCharSequenceNode
,
1154 * The sequence used in a WebKit-based browser to create a Filling Character. By default it is
1155 * a string of 7 zero-width space characters (U+200B).
1159 * @property {String}
1161 FILLING_CHAR_SEQUENCE: fillingCharSequence
1164 CKEDITOR
.dom
.selection
.prototype = {
1166 * Gets the native selection object from the browser.
1168 * var selection = editor.getSelection().getNative();
1170 * @returns {Object} The native browser selection object.
1172 getNative: function() {
1173 if ( this._
.cache
.nativeSel
!== undefined )
1174 return this._
.cache
.nativeSel
;
1176 return ( this._
.cache
.nativeSel
= isMSSelection
? this.document
.$.selection : this.document
.getWindow().$.getSelection() );
1180 * Gets the type of the current selection. The following values are
1183 * * {@link CKEDITOR#SELECTION_NONE} (1): No selection.
1184 * * {@link CKEDITOR#SELECTION_TEXT} (2): A text or a collapsed selection is selected.
1185 * * {@link CKEDITOR#SELECTION_ELEMENT} (3): An element is selected.
1189 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1190 * alert( 'A text is selected' );
1193 * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE},
1194 * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}.
1196 getType: isMSSelection
?
1198 var cache
= this._
.cache
;
1202 var type
= CKEDITOR
.SELECTION_NONE
;
1205 var sel
= this.getNative(),
1208 if ( ieType
== 'Text' )
1209 type
= CKEDITOR
.SELECTION_TEXT
;
1211 if ( ieType
== 'Control' )
1212 type
= CKEDITOR
.SELECTION_ELEMENT
;
1214 // It is possible that we can still get a text range
1215 // object even when type == 'None' is returned by IE.
1216 // So we'd better check the object returned by
1217 // createRange() rather than by looking at the type.
1218 if ( sel
.createRange().parentElement() )
1219 type
= CKEDITOR
.SELECTION_TEXT
;
1222 return ( cache
.type
= type
);
1224 var cache
= this._
.cache
;
1228 var type
= CKEDITOR
.SELECTION_TEXT
;
1230 var sel
= this.getNative();
1232 if ( !( sel
&& sel
.rangeCount
) )
1233 type
= CKEDITOR
.SELECTION_NONE
;
1234 else if ( sel
.rangeCount
== 1 ) {
1235 // Check if the actual selection is a control (IMG,
1236 // TABLE, HR, etc...).
1238 var range
= sel
.getRangeAt( 0 ),
1239 startContainer
= range
.startContainer
;
1241 if ( startContainer
== range
.endContainer
&& startContainer
.nodeType
== 1 &&
1242 ( range
.endOffset
- range
.startOffset
) == 1 &&
1243 styleObjectElements
[ startContainer
.childNodes
[ range
.startOffset
].nodeName
.toLowerCase() ] ) {
1244 type
= CKEDITOR
.SELECTION_ELEMENT
;
1249 return ( cache
.type
= type
);
1253 * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection.
1255 * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns
1256 * one range for each table cell when one or more table rows are selected.
1258 * var ranges = selection.getRanges();
1259 * alert( ranges.length );
1262 * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only.
1263 * @returns {Array} Range instances that represent the current selection.
1265 getRanges: ( function() {
1266 var func
= isMSSelection
? ( function() {
1267 function getNodeIndex( node
) {
1268 return new CKEDITOR
.dom
.node( node
).getIndex();
1271 // Finds the container and offset for a specific boundary
1273 var getBoundaryInformation = function( range
, start
) {
1274 // Creates a collapsed range at the requested boundary.
1275 range
= range
.duplicate();
1276 range
.collapse( start
);
1278 // Gets the element that encloses the range entirely.
1279 var parent
= range
.parentElement();
1281 // Empty parent element, e.g. <i>^</i>
1282 if ( !parent
.hasChildNodes() )
1283 return { container: parent
, offset: 0 };
1285 var siblings
= parent
.children
,
1287 testRange
= range
.duplicate(),
1289 endIndex
= siblings
.length
- 1,
1291 position
, distance
, container
;
1293 // Binary search over all element childs to test the range to see whether
1294 // range is right on the boundary of one element.
1295 while ( startIndex
<= endIndex
) {
1296 index
= Math
.floor( ( startIndex
+ endIndex
) / 2 );
1297 child
= siblings
[ index
];
1298 testRange
.moveToElementText( child
);
1299 position
= testRange
.compareEndPoints( 'StartToStart', range
);
1302 endIndex
= index
- 1;
1303 else if ( position
< 0 )
1304 startIndex
= index
+ 1;
1306 return { container: parent
, offset: getNodeIndex( child
) };
1309 // All childs are text nodes,
1310 // or to the right hand of test range are all text nodes. (#6992)
1311 if ( index
== -1 || index
== siblings
.length
- 1 && position
< 0 ) {
1312 // Adapt test range to embrace the entire parent contents.
1313 testRange
.moveToElementText( parent
);
1314 testRange
.setEndPoint( 'StartToStart', range
);
1316 // IE report line break as CRLF with range.text but
1317 // only LF with textnode.nodeValue, normalize them to avoid
1318 // breaking character counting logic below. (#3949)
1319 distance
= testRange
.text
.replace( /(\r\n|\r)/g, '\n' ).length
;
1321 siblings
= parent
.childNodes
;
1323 // Actual range anchor right beside test range at the boundary of text node.
1325 child
= siblings
[ siblings
.length
- 1 ];
1327 if ( child
.nodeType
!= CKEDITOR
.NODE_TEXT
)
1328 return { container: parent
, offset: siblings
.length
};
1330 return { container: child
, offset: child
.nodeValue
.length
};
1333 // Start the measuring until distance overflows, meanwhile count the text nodes.
1334 var i
= siblings
.length
;
1335 while ( distance
> 0 && i
> 0 ) {
1336 sibling
= siblings
[ --i
];
1337 if ( sibling
.nodeType
== CKEDITOR
.NODE_TEXT
) {
1338 container
= sibling
;
1339 distance
-= sibling
.nodeValue
.length
;
1343 return { container: container
, offset: -distance
};
1345 // Test range was one offset beyond OR behind the anchored text node.
1347 // Adapt one side of test range to the actual range
1348 // for measuring the offset between them.
1349 testRange
.collapse( position
> 0 ? true : false );
1350 testRange
.setEndPoint( position
> 0 ? 'StartToStart' : 'EndToStart', range
);
1352 // IE report line break as CRLF with range.text but
1353 // only LF with textnode.nodeValue, normalize them to avoid
1354 // breaking character counting logic below. (#3949)
1355 distance
= testRange
.text
.replace( /(\r\n|\r)/g, '\n' ).length
;
1357 // Actual range anchor right beside test range at the inner boundary of text node.
1359 return { container: parent
, offset: getNodeIndex( child
) + ( position
> 0 ? 0 : 1 ) };
1361 // Start the measuring until distance overflows, meanwhile count the text nodes.
1362 while ( distance
> 0 ) {
1364 sibling
= child
[ position
> 0 ? 'previousSibling' : 'nextSibling' ];
1365 if ( sibling
.nodeType
== CKEDITOR
.NODE_TEXT
) {
1366 distance
-= sibling
.nodeValue
.length
;
1367 container
= sibling
;
1371 // Measurement in IE could be somtimes wrong because of <select> element. (#4611)
1373 return { container: parent
, offset: getNodeIndex( child
) };
1377 return { container: container
, offset: position
> 0 ? -distance : container
.nodeValue
.length
+ distance
};
1382 // IE doesn't have range support (in the W3C way), so we
1383 // need to do some magic to transform selections into
1384 // CKEDITOR.dom.range instances.
1386 var sel
= this.getNative(),
1387 nativeRange
= sel
&& sel
.createRange(),
1388 type
= this.getType(),
1394 if ( type
== CKEDITOR
.SELECTION_TEXT
) {
1395 range
= new CKEDITOR
.dom
.range( this.root
);
1397 var boundaryInfo
= getBoundaryInformation( nativeRange
, true );
1398 range
.setStart( new CKEDITOR
.dom
.node( boundaryInfo
.container
), boundaryInfo
.offset
);
1400 boundaryInfo
= getBoundaryInformation( nativeRange
);
1401 range
.setEnd( new CKEDITOR
.dom
.node( boundaryInfo
.container
), boundaryInfo
.offset
);
1403 // Correct an invalid IE range case on empty list item. (#5850)
1404 if ( range
.endContainer
.getPosition( range
.startContainer
) & CKEDITOR
.POSITION_PRECEDING
&& range
.endOffset
<= range
.startContainer
.getIndex() )
1408 } else if ( type
== CKEDITOR
.SELECTION_ELEMENT
) {
1411 for ( var i
= 0; i
< nativeRange
.length
; i
++ ) {
1412 var element
= nativeRange
.item( i
),
1413 parentElement
= element
.parentNode
,
1416 range
= new CKEDITOR
.dom
.range( this.root
);
1418 for ( ; j
< parentElement
.childNodes
.length
&& parentElement
.childNodes
[ j
] != element
; j
++ ) {
1422 range
.setStart( new CKEDITOR
.dom
.node( parentElement
), j
);
1423 range
.setEnd( new CKEDITOR
.dom
.node( parentElement
), j
+ 1 );
1424 retval
.push( range
);
1434 // On browsers implementing the W3C range, we simply
1435 // tranform the native ranges in CKEDITOR.dom.range
1440 sel
= this.getNative();
1445 for ( var i
= 0; i
< sel
.rangeCount
; i
++ ) {
1446 var nativeRange
= sel
.getRangeAt( i
);
1448 range
= new CKEDITOR
.dom
.range( this.root
);
1450 range
.setStart( new CKEDITOR
.dom
.node( nativeRange
.startContainer
), nativeRange
.startOffset
);
1451 range
.setEnd( new CKEDITOR
.dom
.node( nativeRange
.endContainer
), nativeRange
.endOffset
);
1452 ranges
.push( range
);
1457 return function( onlyEditables
) {
1458 var cache
= this._
.cache
,
1459 ranges
= cache
.ranges
;
1462 cache
.ranges
= ranges
= new CKEDITOR
.dom
.rangeList( func
.call( this ) );
1464 if ( !onlyEditables
)
1467 // Split range into multiple by read-only nodes.
1468 // Clone ranges array to avoid changing cached ranges (#11493).
1469 return extractEditableRanges( new CKEDITOR
.dom
.rangeList( ranges
.slice() ) );
1474 * Gets the DOM element in which the selection starts.
1476 * var element = editor.getSelection().getStartElement();
1477 * alert( element.getName() );
1479 * @returns {CKEDITOR.dom.element} The element at the beginning of the selection.
1481 getStartElement: function() {
1482 var cache
= this._
.cache
;
1483 if ( cache
.startElement
!== undefined )
1484 return cache
.startElement
;
1488 switch ( this.getType() ) {
1489 case CKEDITOR
.SELECTION_ELEMENT:
1490 return this.getSelectedElement();
1492 case CKEDITOR
.SELECTION_TEXT:
1494 var range
= this.getRanges()[ 0 ];
1497 if ( !range
.collapsed
) {
1500 // Decrease the range content to exclude particial
1501 // selected node on the start which doesn't have
1502 // visual impact. ( #3231 )
1504 var startContainer
= range
.startContainer
,
1505 startOffset
= range
.startOffset
;
1506 // Limit the fix only to non-block elements.(#3950)
1507 if ( startOffset
== ( startContainer
.getChildCount
? startContainer
.getChildCount() : startContainer
.getLength() ) && !startContainer
.isBlockBoundary() )
1508 range
.setStartAfter( startContainer
);
1513 node
= range
.startContainer
;
1515 if ( node
.type
!= CKEDITOR
.NODE_ELEMENT
)
1516 return node
.getParent();
1518 node
= node
.getChild( range
.startOffset
);
1520 if ( !node
|| node
.type
!= CKEDITOR
.NODE_ELEMENT
)
1521 node
= range
.startContainer
;
1523 var child
= node
.getFirst();
1524 while ( child
&& child
.type
== CKEDITOR
.NODE_ELEMENT
) {
1526 child
= child
.getFirst();
1530 node
= range
.startContainer
;
1531 if ( node
.type
!= CKEDITOR
.NODE_ELEMENT
)
1532 node
= node
.getParent();
1539 return cache
.startElement
= ( node
? new CKEDITOR
.dom
.element( node
) : null );
1543 * Gets the currently selected element.
1545 * var element = editor.getSelection().getSelectedElement();
1546 * alert( element.getName() );
1548 * @returns {CKEDITOR.dom.element} The selected element. Null if no
1549 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1551 getSelectedElement: function() {
1552 var cache
= this._
.cache
;
1553 if ( cache
.selectedElement
!== undefined )
1554 return cache
.selectedElement
;
1558 var node
= CKEDITOR
.tools
.tryThese(
1559 // Is it native IE control type selection?
1561 return self
.getNative().createRange().item( 0 );
1563 // Figure it out by checking if there's a single enclosed
1564 // node of the range.
1566 var range
= self
.getRanges()[ 0 ].clone(),
1569 // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul>
1570 for ( var i
= 2; i
&& !( ( enclosed
= range
.getEnclosedNode() ) && ( enclosed
.type
== CKEDITOR
.NODE_ELEMENT
) && styleObjectElements
[ enclosed
.getName() ] && ( selected
= enclosed
) ); i
-- ) {
1571 // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>]
1572 range
.shrink( CKEDITOR
.SHRINK_ELEMENT
);
1575 return selected
&& selected
.$;
1579 return cache
.selectedElement
= ( node
? new CKEDITOR
.dom
.element( node
) : null );
1583 * Retrieves the text contained within the range. An empty string is returned for non-text selection.
1585 * var text = editor.getSelection().getSelectedText();
1589 * @returns {String} A string of text within the current selection.
1591 getSelectedText: function() {
1592 var cache
= this._
.cache
;
1593 if ( cache
.selectedText
!== undefined )
1594 return cache
.selectedText
;
1596 var nativeSel
= this.getNative(),
1597 text
= isMSSelection
? nativeSel
.type
== 'Control' ? '' : nativeSel
.createRange().text : nativeSel
.toString();
1599 return ( cache
.selectedText
= text
);
1603 * Locks the selection made in the editor in order to make it possible to
1604 * manipulate it without browser interference. A locked selection is
1605 * cached and remains unchanged until it is released with the {@link #unlock} method.
1607 * editor.getSelection().lock();
1610 // Call all cacheable function.
1612 this.getStartElement();
1613 this.getSelectedElement();
1614 this.getSelectedText();
1616 // The native selection is not available when locked.
1617 this._
.cache
.nativeSel
= null;
1625 unlock: function( restore
) {
1626 if ( !this.isLocked
)
1630 var selectedElement
= this.getSelectedElement(),
1631 ranges
= !selectedElement
&& this.getRanges(),
1632 faked
= this.isFake
;
1639 // Saved selection may be outdated (e.g. anchored in offline nodes).
1640 // Avoid getting broken by such.
1641 var common
= selectedElement
|| ranges
[ 0 ] && ranges
[ 0 ].getCommonAncestor();
1642 if ( !( common
&& common
.getAscendant( 'body', 1 ) ) )
1646 this.fake( selectedElement
);
1647 else if ( selectedElement
)
1648 this.selectElement( selectedElement
);
1650 this.selectRanges( ranges
);
1655 * Clears the selection cache.
1657 * editor.getSelection().reset();
1663 var editor
= this.root
.editor
;
1665 // Invalidate any fake selection available in the editor.
1666 if ( editor
&& editor
._
.fakeSelection
) {
1667 // Test whether this selection is the one that was
1668 // faked or its clone.
1669 if ( this.rev
== editor
._
.fakeSelection
.rev
) {
1670 delete editor
._
.fakeSelection
;
1672 removeHiddenSelectionContainer( editor
);
1675 CKEDITOR
.warn( 'selection-fake-reset' );
1679 this.rev
= nextRev
++;
1683 * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element.
1685 * var element = editor.document.getById( 'sampleElement' );
1686 * editor.getSelection().selectElement( element );
1688 * @param {CKEDITOR.dom.element} element The element to enclose in the selection.
1690 selectElement: function( element
) {
1691 var range
= new CKEDITOR
.dom
.range( this.root
);
1692 range
.setStartBefore( element
);
1693 range
.setEndAfter( element
);
1694 this.selectRanges( [ range
] );
1698 * Clears the original selection and adds the specified ranges to the document selection.
1700 * // Move selection to the end of the editable element.
1701 * var range = editor.createRange();
1702 * range.moveToPosition( range.root, CKEDITOR.POSITION_BEFORE_END );
1703 * editor.getSelection().selectRanges( [ ranges ] );
1705 * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances
1706 * representing ranges to be added to the document.
1708 selectRanges: function( ranges
) {
1709 var editor
= this.root
.editor
,
1710 hadHiddenSelectionContainer
= editor
&& editor
._
.hiddenSelectionContainer
;
1714 // Check if there's a hiddenSelectionContainer in editable at some index.
1715 // Some ranges may be anchored after the hiddenSelectionContainer and,
1716 // once the container is removed while resetting the selection, they
1717 // may need new endOffset (one element less within the range) (#11021 #11393).
1718 if ( hadHiddenSelectionContainer
)
1719 fixRangesAfterHiddenSelectionContainer( ranges
, this.root
);
1721 if ( !ranges
.length
)
1724 // Refresh the locked selection.
1725 if ( this.isLocked
) {
1726 // making a new DOM selection will force the focus on editable in certain situation,
1727 // we have to save the currently focused element for later recovery.
1728 var focused
= CKEDITOR
.document
.getActive();
1730 this.selectRanges( ranges
);
1732 // Return to the previously focused element.
1733 focused
&& !focused
.equals( this.root
) && focused
.focus();
1737 // Handle special case - automatic fake selection on non-editable elements.
1738 var receiver
= getNonEditableFakeSelectionReceiver( ranges
);
1741 this.fake( receiver
);
1745 if ( isMSSelection
) {
1746 var notWhitespaces
= CKEDITOR
.dom
.walker
.whitespaces( true ),
1747 fillerTextRegex
= /\ufeff|\u00a0/,
1748 nonCells
= { table: 1, tbody: 1, tr: 1 };
1750 if ( ranges
.length
> 1 ) {
1751 // IE doesn't accept multiple ranges selection, so we join all into one.
1752 var last
= ranges
[ ranges
.length
- 1 ];
1753 ranges
[ 0 ].setEnd( last
.endContainer
, last
.endOffset
);
1756 var range
= ranges
[ 0 ];
1757 var collapsed
= range
.collapsed
,
1758 isStartMarkerAlone
, dummySpan
, ieRange
;
1760 // Try to make a object selection, be careful with selecting phase element in IE
1761 // will breaks the selection in non-framed environment.
1762 var selected
= range
.getEnclosedNode();
1763 if ( selected
&& selected
.type
== CKEDITOR
.NODE_ELEMENT
&& selected
.getName() in styleObjectElements
&&
1764 !( selected
.is( 'a' ) && selected
.getText() ) ) {
1766 ieRange
= selected
.$.createControlRange();
1767 ieRange
.addElement( selected
.$ );
1773 // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
1774 // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>...
1775 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.startContainer
.getName() in nonCells
||
1776 range
.endContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.endContainer
.getName() in nonCells
) {
1777 range
.shrink( CKEDITOR
.NODE_ELEMENT
, true );
1778 // The range might get collapsed (#7975). Update cached variable.
1779 collapsed
= range
.collapsed
;
1782 var bookmark
= range
.createBookmark();
1784 // Create marker tags for the start and end boundaries.
1785 var startNode
= bookmark
.startNode
;
1789 endNode
= bookmark
.endNode
;
1791 // Create the main range which will be used for the selection.
1792 ieRange
= range
.document
.$.body
.createTextRange();
1794 // Position the range at the start boundary.
1795 ieRange
.moveToElementText( startNode
.$ );
1796 ieRange
.moveStart( 'character', 1 );
1799 // Create a tool range for the end.
1800 var ieRangeEnd
= range
.document
.$.body
.createTextRange();
1802 // Position the tool range at the end.
1803 ieRangeEnd
.moveToElementText( endNode
.$ );
1805 // Move the end boundary of the main range to match the tool range.
1806 ieRange
.setEndPoint( 'EndToEnd', ieRangeEnd
);
1807 ieRange
.moveEnd( 'character', -1 );
1809 // The isStartMarkerAlone logic comes from V2. It guarantees that the lines
1810 // will expand and that the cursor will be blinking on the right place.
1811 // Actually, we are using this flag just to avoid using this hack in all
1812 // situations, but just on those needed.
1813 var next
= startNode
.getNext( notWhitespaces
);
1814 var inPre
= startNode
.hasAscendant( 'pre' );
1815 isStartMarkerAlone
= ( !( next
&& next
.getText
&& next
.getText().match( fillerTextRegex
) ) && // already a filler there?
1816 ( inPre
|| !startNode
.hasPrevious() || ( startNode
.getPrevious().is
&& startNode
.getPrevious().is( 'br' ) ) ) );
1818 // Append a temporary <span></span> before the selection.
1819 // This is needed to avoid IE destroying selections inside empty
1820 // inline elements, like <b></b> (#253).
1821 // It is also needed when placing the selection right after an inline
1822 // element to avoid the selection moving inside of it.
1823 dummySpan
= range
.document
.createElement( 'span' );
1824 dummySpan
.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359.
1825 dummySpan
.insertBefore( startNode
);
1827 if ( isStartMarkerAlone
) {
1828 // To expand empty blocks or line spaces after <br>, we need
1829 // instead to have any char, which will be later deleted using the
1831 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
1832 range
.document
.createText( '\ufeff' ).insertBefore( startNode
);
1836 // Remove the markers (reset the position, because of the changes in the DOM tree).
1837 range
.setStartBefore( startNode
);
1841 if ( isStartMarkerAlone
) {
1842 // Move the selection start to include the temporary \ufeff.
1843 ieRange
.moveStart( 'character', -1 );
1847 // Remove our temporary stuff.
1848 range
.document
.$.selection
.clear();
1853 range
.moveToPosition( dummySpan
, CKEDITOR
.POSITION_BEFORE_START
);
1856 range
.setEndBefore( endNode
);
1861 var sel
= this.getNative();
1863 // getNative() returns null if iframe is "display:none" in FF. (#6577)
1867 this.removeAllRanges();
1869 for ( var i
= 0; i
< ranges
.length
; i
++ ) {
1870 // Joining sequential ranges introduced by
1871 // readonly elements protection.
1872 if ( i
< ranges
.length
- 1 ) {
1873 var left
= ranges
[ i
],
1874 right
= ranges
[ i
+ 1 ],
1875 between
= left
.clone();
1876 between
.setStart( left
.endContainer
, left
.endOffset
);
1877 between
.setEnd( right
.startContainer
, right
.startOffset
);
1879 // Don't confused by Firefox adjancent multi-ranges
1880 // introduced by table cells selection.
1881 if ( !between
.collapsed
) {
1882 between
.shrink( CKEDITOR
.NODE_ELEMENT
, true );
1883 var ancestor
= between
.getCommonAncestor(),
1884 enclosed
= between
.getEnclosedNode();
1886 // The following cases has to be considered:
1887 // 1. <span contenteditable="false">[placeholder]</span>
1888 // 2. <input contenteditable="false" type="radio"/> (#6621)
1889 if ( ancestor
.isReadOnly() || enclosed
&& enclosed
.isReadOnly() ) {
1890 right
.setStart( left
.startContainer
, left
.startOffset
);
1891 ranges
.splice( i
--, 1 );
1897 range
= ranges
[ i
];
1899 var nativeRange
= this.document
.$.createRange();
1901 if ( range
.collapsed
&& CKEDITOR
.env
.webkit
&& rangeRequiresFix( range
) ) {
1902 // Append a zero-width space so WebKit will not try to
1903 // move the selection by itself (#1272).
1904 var fillingChar
= createFillingCharSequenceNode( this.root
);
1905 range
.insertNode( fillingChar
);
1907 next
= fillingChar
.getNext();
1909 // If the filling char is followed by a <br>, whithout
1910 // having something before it, it'll not blink.
1911 // Let's remove it in this case.
1912 if ( next
&& !fillingChar
.getPrevious() && next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.getName() == 'br' ) {
1913 removeFillingCharSequenceNode( this.root
);
1914 range
.moveToPosition( next
, CKEDITOR
.POSITION_BEFORE_START
);
1916 range
.moveToPosition( fillingChar
, CKEDITOR
.POSITION_AFTER_END
);
1920 nativeRange
.setStart( range
.startContainer
.$, range
.startOffset
);
1923 nativeRange
.setEnd( range
.endContainer
.$, range
.endOffset
);
1925 // There is a bug in Firefox implementation (it would be too easy
1926 // otherwise). The new start can't be after the end (W3C says it can).
1927 // So, let's create a new range and collapse it to the desired point.
1928 if ( e
.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) {
1929 range
.collapse( 1 );
1930 nativeRange
.setEnd( range
.endContainer
.$, range
.endOffset
);
1936 // Select the range.
1937 sel
.addRange( nativeRange
);
1943 // Fakes the IE DOM event "selectionchange" on editable.
1944 this.root
.fire( 'selectionchange' );
1948 * Makes a "fake selection" of an element.
1950 * A fake selection does not render UI artifacts over the selected
1951 * element. Additionally, the browser native selection system is not
1952 * aware of the fake selection. In practice, the native selection is
1953 * moved to a hidden place where no native selection UI artifacts are
1954 * displayed to the user.
1956 * @param {CKEDITOR.dom.element} element The element to be "selected".
1958 fake: function( element
) {
1959 var editor
= this.root
.editor
;
1961 // Cleanup after previous selection - e.g. remove hidden sel container.
1964 hideSelection( editor
);
1966 // Set this value after executing hiseSelection, because it may
1967 // cause reset() which overwrites cache.
1968 var cache
= this._
.cache
;
1970 // Caches a range than holds the element.
1971 var range
= new CKEDITOR
.dom
.range( this.root
);
1972 range
.setStartBefore( element
);
1973 range
.setEndAfter( element
);
1974 cache
.ranges
= new CKEDITOR
.dom
.rangeList( range
);
1976 // Put this element in the cache.
1977 cache
.selectedElement
= cache
.startElement
= element
;
1978 cache
.type
= CKEDITOR
.SELECTION_ELEMENT
;
1980 // Properties that will not be available when isFake.
1981 cache
.selectedText
= cache
.nativeSel
= null;
1984 this.rev
= nextRev
++;
1986 // Save this selection, so it can be returned by editor.getSelection().
1987 editor
._
.fakeSelection
= this;
1989 // Fire selectionchange, just like a normal selection.
1990 this.root
.fire( 'selectionchange' );
1994 * Checks whether selection is placed in hidden element.
1996 * This method is to be used to verify whether fake selection
1997 * (see {@link #fake}) is still hidden.
1999 * **Note:** this method should be executed on real selection - e.g.:
2001 * editor.getSelection( true ).isHidden();
2003 * @returns {Boolean}
2005 isHidden: function() {
2006 var el
= this.getCommonAncestor();
2008 if ( el
&& el
.type
== CKEDITOR
.NODE_TEXT
)
2009 el
= el
.getParent();
2011 return !!( el
&& el
.data( 'cke-hidden-sel' ) );
2015 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2016 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2017 * with extra care taken to avoid interference among those ranges. The arguments
2018 * received are the same as with the underlying range method.
2020 * var bookmarks = editor.getSelection().createBookmarks();
2022 * @returns {Array} Array of bookmarks for each range.
2024 createBookmarks: function( serializable
) {
2025 var bookmark
= this.getRanges().createBookmarks( serializable
);
2026 this.isFake
&& ( bookmark
.isFake
= 1 );
2031 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2032 * by calling the {@link CKEDITOR.dom.range#createBookmark2} method,
2033 * with extra care taken to avoid interference among those ranges. The arguments
2034 * received are the same as with the underlying range method.
2036 * var bookmarks = editor.getSelection().createBookmarks2();
2038 * @returns {Array} Array of bookmarks for each range.
2040 createBookmarks2: function( normalized
) {
2041 var bookmark
= this.getRanges().createBookmarks2( normalized
);
2042 this.isFake
&& ( bookmark
.isFake
= 1 );
2047 * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}.
2049 * var bookmarks = editor.getSelection().createBookmarks();
2050 * editor.getSelection().selectBookmarks( bookmarks );
2052 * @param {Array} bookmarks The bookmarks representing ranges to be selected.
2053 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected.
2055 selectBookmarks: function( bookmarks
) {
2059 for ( var i
= 0; i
< bookmarks
.length
; i
++ ) {
2060 var range
= new CKEDITOR
.dom
.range( this.root
);
2061 range
.moveToBookmark( bookmarks
[ i
] );
2062 ranges
.push( range
);
2065 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2066 if ( bookmarks
.isFake
) {
2067 node
= ranges
[ 0 ].getEnclosedNode();
2068 if ( !node
|| node
.type
!= CKEDITOR
.NODE_ELEMENT
) {
2069 CKEDITOR
.warn( 'selection-not-fake' );
2070 bookmarks
.isFake
= 0;
2074 if ( bookmarks
.isFake
)
2077 this.selectRanges( ranges
);
2083 * Retrieves the common ancestor node of the first range and the last range.
2085 * var ancestor = editor.getSelection().getCommonAncestor();
2087 * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty.
2089 getCommonAncestor: function() {
2090 var ranges
= this.getRanges();
2091 if ( !ranges
.length
)
2094 var startNode
= ranges
[ 0 ].startContainer
,
2095 endNode
= ranges
[ ranges
.length
- 1 ].endContainer
;
2096 return startNode
.getCommonAncestor( endNode
);
2100 * Moves the scrollbar to the starting position of the current selection.
2102 * editor.getSelection().scrollIntoView();
2104 scrollIntoView: function() {
2105 // Scrolls the first range into view.
2106 if ( this.type
!= CKEDITOR
.SELECTION_NONE
)
2107 this.getRanges()[ 0 ].scrollIntoView();
2111 * Remove all the selection ranges from the document.
2113 removeAllRanges: function() {
2114 // Don't clear selection outside this selection's root (#11500).
2115 if ( this.getType() == CKEDITOR
.SELECTION_NONE
)
2118 var nativ
= this.getNative();
2121 nativ
&& nativ
[ isMSSelection
? 'empty' : 'removeAllRanges' ]();
2132 * Fired when selection inside editor has been changed. Note that this event
2133 * is fired only when selection's start element (container of a selecion start)
2134 * changes, not on every possible selection change. Thanks to that `selectionChange`
2135 * is fired less frequently, but on every context
2136 * (the {@link CKEDITOR.editor#elementPath elements path} holding selection's start) change.
2138 * @event selectionChange
2139 * @member CKEDITOR.editor
2140 * @param {CKEDITOR.editor} editor This editor instance.
2142 * @param {CKEDITOR.dom.selection} data.selection
2143 * @param {CKEDITOR.dom.elementPath} data.path
2147 * Selection's revision. This value is incremented every time new
2148 * selection is created or existing one is modified.
2152 * @property {Number} rev
2156 * Document in which selection is anchored.
2159 * @property {CKEDITOR.dom.document} document
2163 * Selection's root element.
2166 * @property {CKEDITOR.dom.element} root
2170 * Whether selection is locked (cannot be modified).
2172 * See {@link #lock} and {@link #unlock} methods.
2175 * @property {Boolean} isLocked
2179 * Whether selection is a fake selection.
2181 * See {@link #fake} method.
2184 * @property {Boolean} isFake