2018-06-17 16:07:19 +00:00
/ * *
* @ license Copyright ( c ) 2003 - 2018 , CKSource - Frederico Knabben . All rights reserved .
* For licensing , see LICENSE . md or https : //ckeditor.com/legal/ckeditor-oss-license
2014-04-11 20:05:45 +00:00
* /
( function ( ) {
2018-06-17 16:07:19 +00:00
var isMSSelection = typeof window . getSelection != 'function' ,
nextRev = 1 ,
// https://dev.ckeditor.com/ticket/13816
fillingCharSequence = CKEDITOR . tools . repeat ( '\u200b' , 7 ) ,
fillingCharSequenceRegExp = new RegExp ( fillingCharSequence + '( )?' , 'g' ) ,
isSelectingTable ;
function isWidget ( element ) {
return CKEDITOR . plugins . widget && CKEDITOR . plugins . widget . isDomWidget ( element ) ;
}
// #### table selection : START
// @param {CKEDITOR.dom.range[]} ranges
// @param {Boolean} allowPartially Whether a collapsed selection within table is recognized to be a valid selection.
// This happens for WebKits on MacOS, when you right click inside the table.
function isTableSelection ( ranges , allowPartially ) {
if ( ranges . length === 0 ) {
return false ;
}
// It's not table selection when selected node is a widget (#1027).
if ( isWidget ( ranges [ 0 ] . getEnclosedNode ( ) ) ) {
return false ;
}
var node ,
i ;
function isPartiallySelected ( range ) {
var startCell = range . startContainer . getAscendant ( { td : 1 , th : 1 } , true ) ,
endCell = range . endContainer . getAscendant ( { td : 1 , th : 1 } , true ) ,
trim = CKEDITOR . tools . trim ,
selected ;
// Check if the selection is inside one cell and we don't have any nested table contents selected.
if ( ! startCell || ! startCell . equals ( endCell ) || startCell . findOne ( 'td, th, tr, tbody, table' ) ) {
return false ;
}
selected = range . cloneContents ( ) ;
// Empty selection is still partially selected.
if ( ! selected . getFirst ( ) ) {
return true ;
}
return trim ( selected . getFirst ( ) . getText ( ) ) !== trim ( startCell . getText ( ) ) ;
}
// Edge case: partially selected text node inside one table cell or cursor inside cell.
if ( ! allowPartially && ranges . length === 1 &&
( ranges [ 0 ] . collapsed || isPartiallySelected ( ranges [ 0 ] ) ) ) {
return false ;
}
for ( i = 0 ; i < ranges . length ; i ++ ) {
node = ranges [ i ] . _getTableElement ( ) ;
if ( ! node ) {
return false ;
}
}
return true ;
}
// After performing fake table selection, the real selection is limited
// to the first selected cell. Therefore to check if the real selection
// matches the fake selection, we check if the table cell from fake selection's
// first range and real selection's range are the same.
// Also if the selection is collapsed, we should check if it's placed inside the table
// in which the fake selection is or inside nested table. Such selection occurs after right mouse click.
function isRealTableSelection ( selection , fakeSelection ) {
var ranges = selection . getRanges ( ) ,
fakeRanges = fakeSelection . getRanges ( ) ,
table = ranges . length && ranges [ 0 ] . _getTableElement ( ) &&
ranges [ 0 ] . _getTableElement ( ) . getAscendant ( 'table' , true ) ,
fakeTable = fakeRanges . length && fakeRanges [ 0 ] . _getTableElement ( ) &&
fakeRanges [ 0 ] . _getTableElement ( ) . getAscendant ( 'table' , true ) ,
isTableRange = ranges . length === 1 && ranges [ 0 ] . _getTableElement ( ) &&
ranges [ 0 ] . _getTableElement ( ) . is ( 'table' ) ,
isFakeTableRange = fakeRanges . length === 1 && fakeRanges [ 0 ] . _getTableElement ( ) &&
fakeRanges [ 0 ] . _getTableElement ( ) . is ( 'table' ) ;
function isValidTableSelection ( table , fakeTable , ranges , fakeRanges ) {
var isMenuOpen = ranges . length === 1 && ranges [ 0 ] . collapsed ,
// In case of WebKit on MacOS, when checking real selection, we must allow selection to be partial.
// Otherwise the check will fail for table selection with opened context menu.
isInTable = isTableSelection ( ranges , ! ! CKEDITOR . env . webkit ) && isTableSelection ( fakeRanges ) ;
return isSameTable ( table , fakeTable ) && ( isMenuOpen || isInTable ) ;
}
function isSameTable ( table , fakeTable ) {
if ( ! table || ! fakeTable ) {
return false ;
}
return table . equals ( fakeTable ) || fakeTable . contains ( table ) ;
}
// When widget is selected, then definitely it's not a table (#1027).
if ( isWidget ( fakeSelection . getSelectedElement ( ) ) ) {
return false ;
}
if ( isValidTableSelection ( table , fakeTable , ranges , fakeRanges ) ) {
// Edge case: when editor contains only table and that table is selected using selectAll command,
// then the selection is not properly refreshed and it must be done manually.
if ( isTableRange && ! isFakeTableRange ) {
fakeSelection . selectRanges ( ranges ) ;
}
return true ;
}
return false ;
}
function getSelectedCells ( ranges ) {
var cells = [ ] ,
node ,
i ;
function getCellsFromElement ( element ) {
var cells = element . find ( 'td, th' ) ,
cellsArray = [ ] ,
i ;
for ( i = 0 ; i < cells . count ( ) ; i ++ ) {
cellsArray . push ( cells . getItem ( i ) ) ;
}
return cellsArray ;
}
for ( i = 0 ; i < ranges . length ; i ++ ) {
node = ranges [ i ] . _getTableElement ( ) ;
if ( node . is && node . is ( { td : 1 , th : 1 } ) ) {
cells . push ( node ) ;
} else {
cells = cells . concat ( getCellsFromElement ( node ) ) ;
}
}
return cells ;
}
// Cells in the same row are separated by tab and the rows are separated by new line, e.g.
// Cell 1.1 Cell 1.2
// Cell 2.1 Cell 2.2
function getTextFromSelectedCells ( ranges ) {
var cells = getSelectedCells ( ranges ) ,
txt = '' ,
currentRow = [ ] ,
lastRow ,
i ;
for ( i = 0 ; i < cells . length ; i ++ ) {
if ( lastRow && ! lastRow . equals ( cells [ i ] . getAscendant ( 'tr' ) ) ) {
txt += currentRow . join ( '\t' ) + '\n' ;
lastRow = cells [ i ] . getAscendant ( 'tr' ) ;
currentRow = [ ] ;
} else if ( i === 0 ) {
lastRow = cells [ i ] . getAscendant ( 'tr' ) ;
}
currentRow . push ( cells [ i ] . getText ( ) ) ;
}
txt += currentRow . join ( '\t' ) ;
return txt ;
}
function performFakeTableSelection ( ranges ) {
var editor = this . root . editor ,
realSelection = editor . getSelection ( 1 ) ,
cache ;
// Cleanup after previous selection - e.g. remove hidden sel container.
this . reset ( ) ;
// Indicate that the table is being fake-selected to prevent infinite loop
// inside `selectRanges`.
isSelectingTable = true ;
// Cancel selectionchange for the real selection.
realSelection . root . once ( 'selectionchange' , function ( evt ) {
evt . cancel ( ) ;
} , null , null , 0 ) ;
// Move real selection to the first selected range.
realSelection . selectRanges ( [ ranges [ 0 ] ] ) ;
cache = this . _ . cache ;
// Caches given ranges.
cache . ranges = new CKEDITOR . dom . rangeList ( ranges ) ;
cache . type = CKEDITOR . SELECTION _TEXT ;
cache . selectedElement = ranges [ 0 ] . _getTableElement ( ) ;
// `selectedText` should contain text from all selected data ("plain text table")
// to be compatible with Firefox's implementation.
cache . selectedText = getTextFromSelectedCells ( ranges ) ;
// Properties that will not be available when isFake.
cache . nativeSel = null ;
this . isFake = 1 ;
this . rev = nextRev ++ ;
// Save this selection, so it can be returned by editor.getSelection().
editor . _ . fakeSelection = this ;
isSelectingTable = false ;
// Fire selectionchange, just like a normal selection.
this . root . fire ( 'selectionchange' ) ;
}
// #### table selection : END
2014-04-11 20:05:45 +00:00
// #### checkSelectionChange : START
// The selection change check basically saves the element parent tree of
// the current node and check it on successive requests. If there is any
// change on the tree, then the selectionChange event gets fired.
function checkSelectionChange ( ) {
// A possibly available fake-selection.
var sel = this . _ . fakeSelection ,
realSel ;
if ( sel ) {
realSel = this . getSelection ( 1 ) ;
2018-06-17 16:07:19 +00:00
// If real (not locked/stored) selection was moved from hidden container
// or is not a table one, then the fake-selection must be invalidated.
if ( ! realSel || ( ! realSel . isHidden ( ) && ! isRealTableSelection ( realSel , sel ) ) ) {
2014-04-11 20:05:45 +00:00
// Remove the cache from fake-selection references in use elsewhere.
sel . reset ( ) ;
// Have the code using the native selection.
sel = 0 ;
}
}
// If not fake-selection is available then get the native selection.
if ( ! sel ) {
sel = realSel || this . getSelection ( 1 ) ;
// Editor may have no selection at all.
if ( ! sel || sel . getType ( ) == CKEDITOR . SELECTION _NONE )
return ;
}
this . fire ( 'selectionCheck' , sel ) ;
var currentPath = this . elementPath ( ) ;
if ( ! currentPath . compare ( this . _ . selectionPreviousPath ) ) {
2018-06-17 16:07:19 +00:00
// Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (https://dev.ckeditor.com/ticket/13362)
var sameBlockParent = this . _ . selectionPreviousPath && this . _ . selectionPreviousPath . blockLimit . equals ( currentPath . blockLimit ) ;
2014-04-11 20:05:45 +00:00
// Cache the active element, which we'll eventually lose on Webkit.
2018-06-17 16:07:19 +00:00
if ( CKEDITOR . env . webkit && ! sameBlockParent )
2014-04-11 20:05:45 +00:00
this . _ . previousActive = this . document . getActive ( ) ;
this . _ . selectionPreviousPath = currentPath ;
this . fire ( 'selectionChange' , { selection : sel , path : currentPath } ) ;
}
}
var checkSelectionChangeTimer , checkSelectionChangeTimeoutPending ;
function checkSelectionChangeTimeout ( ) {
// Firing the "OnSelectionChange" event on every key press started to
// be too slow. This function guarantees that there will be at least
// 200ms delay between selection checks.
checkSelectionChangeTimeoutPending = true ;
if ( checkSelectionChangeTimer )
return ;
checkSelectionChangeTimeoutExec . call ( this ) ;
checkSelectionChangeTimer = CKEDITOR . tools . setTimeout ( checkSelectionChangeTimeoutExec , 200 , this ) ;
}
function checkSelectionChangeTimeoutExec ( ) {
checkSelectionChangeTimer = null ;
if ( checkSelectionChangeTimeoutPending ) {
// Call this with a timeout so the browser properly moves the
// selection after the mouseup. It happened that the selection was
// being moved after the mouseup when clicking inside selected text
// with Firefox.
CKEDITOR . tools . setTimeout ( checkSelectionChange , 0 , this ) ;
checkSelectionChangeTimeoutPending = false ;
}
}
// #### checkSelectionChange : END
var isVisible = CKEDITOR . dom . walker . invisible ( 1 ) ;
2018-06-17 16:07:19 +00:00
// May absorb the caret if:
// * is a visible node,
// * is a non-empty element (this rule will accept elements like <strong></strong> because they
// they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
// See https://dev.ckeditor.com/ticket/12621.
function mayAbsorbCaret ( node ) {
if ( isVisible ( node ) )
return true ;
if ( node . type == CKEDITOR . NODE _ELEMENT && ! node . is ( CKEDITOR . dtd . $empty ) )
return true ;
return false ;
}
2014-04-11 20:05:45 +00:00
function rangeRequiresFix ( range ) {
2018-06-17 16:07:19 +00:00
// Whether we must prevent from absorbing caret by this context node.
// Also checks whether there's an editable position next to that node.
function ctxRequiresFix ( node , isAtEnd ) {
// It's ok for us if a text node absorbs the caret, because
// the caret container element isn't changed then.
2014-04-11 20:05:45 +00:00
if ( ! node || node . type == CKEDITOR . NODE _TEXT )
return false ;
var testRng = range . clone ( ) ;
return testRng [ 'moveToElementEdit' + ( isAtEnd ? 'End' : 'Start' ) ] ( node ) ;
}
// Range root must be the editable element, it's to avoid creating filler char
// on any temporary internal selection.
if ( ! ( range . root instanceof CKEDITOR . editable ) )
return false ;
var ct = range . startContainer ;
2018-06-17 16:07:19 +00:00
var previous = range . getPreviousNode ( mayAbsorbCaret , null , ct ) ,
next = range . getNextNode ( mayAbsorbCaret , null , ct ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// Any adjacent text container may absorb the caret, e.g.
2014-04-11 20:05:45 +00:00
// <p><strong>text</strong>^foo</p>
// <p>foo^<strong>text</strong></p>
// <div>^<p>foo</p></div>
2018-06-17 16:07:19 +00:00
if ( ctxRequiresFix ( previous ) || ctxRequiresFix ( next , 1 ) )
2014-04-11 20:05:45 +00:00
return true ;
2018-06-17 16:07:19 +00:00
// Empty block/inline element is also affected. <span>^</span>, <p>^</p> (https://dev.ckeditor.com/ticket/7222)
// If you found this line confusing check https://dev.ckeditor.com/ticket/12655.
2014-04-11 20:05:45 +00:00
if ( ! ( previous || next ) && ! ( ct . type == CKEDITOR . NODE _ELEMENT && ct . isBlockBoundary ( ) && ct . getBogus ( ) ) )
return true ;
return false ;
}
2018-06-17 16:07:19 +00:00
function createFillingCharSequenceNode ( editable ) {
removeFillingCharSequenceNode ( editable , false ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
var fillingChar = editable . getDocument ( ) . createText ( fillingCharSequence ) ;
editable . setCustomData ( 'cke-fillingChar' , fillingChar ) ;
2014-04-11 20:05:45 +00:00
return fillingChar ;
}
2018-06-17 16:07:19 +00:00
// Checks if a filling char has been used, eventually removing it (https://dev.ckeditor.com/ticket/1272).
function checkFillingCharSequenceNodeReady ( editable ) {
var fillingChar = editable . getCustomData ( 'cke-fillingChar' ) ;
2014-04-11 20:05:45 +00:00
if ( fillingChar ) {
// Use this flag to avoid removing the filling char right after
// creating it.
2018-06-17 16:07:19 +00:00
if ( fillingChar . getCustomData ( 'ready' ) ) {
removeFillingCharSequenceNode ( editable ) ;
editable . editor . fire ( 'selectionCheck' ) ;
} else {
2014-04-11 20:05:45 +00:00
fillingChar . setCustomData ( 'ready' , 1 ) ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
}
}
2018-06-17 16:07:19 +00:00
function removeFillingCharSequenceNode ( editable , keepSelection ) {
var fillingChar = editable && editable . removeCustomData ( 'cke-fillingChar' ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
if ( fillingChar ) {
2014-04-11 20:05:45 +00:00
// Text selection position might get mangled by
2018-06-17 16:07:19 +00:00
// subsequent dom modification, save it now for restoring. (https://dev.ckeditor.com/ticket/8617)
if ( keepSelection !== false ) {
var sel = editable . getDocument ( ) . getSelection ( ) . getNative ( ) ,
2014-04-11 20:05:45 +00:00
// Be error proof.
2018-06-17 16:07:19 +00:00
range = sel && sel . type != 'None' && sel . getRangeAt ( 0 ) ,
fillingCharSeqLength = fillingCharSequence . length ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// If there's some text other than the sequence in the FC text node and the range
// intersects with that node...
if ( fillingChar . getLength ( ) > fillingCharSeqLength && range && range . intersectsNode ( fillingChar . $ ) ) {
var bm = createNativeSelectionBookmark ( sel ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// Correct start offset anticipating the removal of FC.
if ( sel . anchorNode == fillingChar . $ && sel . anchorOffset > fillingCharSeqLength ) {
bm [ 0 ] . offset -= fillingCharSeqLength ;
}
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// Correct end offset anticipating the removal of FC.
if ( sel . focusNode == fillingChar . $ && sel . focusOffset > fillingCharSeqLength ) {
bm [ 1 ] . offset -= fillingCharSeqLength ;
}
2014-04-11 20:05:45 +00:00
}
}
// We can't simply remove the filling node because the user
// will actually enlarge it when typing, so we just remove the
// invisible char from it.
2018-06-17 16:07:19 +00:00
fillingChar . setText ( removeFillingCharSequenceString ( fillingChar . getText ( ) , 1 ) ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// Restore the bookmark preserving selection's direction.
2014-04-11 20:05:45 +00:00
if ( bm ) {
2018-06-17 16:07:19 +00:00
moveNativeSelectionToBookmark ( editable . getDocument ( ) . $ , bm ) ;
2014-04-11 20:05:45 +00:00
}
}
}
2018-06-17 16:07:19 +00:00
// https://dev.ckeditor.com/ticket/13816
function removeFillingCharSequenceString ( str , nbspAware ) {
if ( nbspAware ) {
return str . replace ( fillingCharSequenceRegExp , function ( m , p ) {
// https://dev.ckeditor.com/ticket/10291 if filling char is followed by a space replace it with NBSP.
return p ? '\xa0' : '' ;
} ) ;
} else {
return str . replace ( fillingCharSequence , '' ) ;
2014-04-11 20:05:45 +00:00
}
}
2018-06-17 16:07:19 +00:00
function createNativeSelectionBookmark ( sel ) {
return [
{ node : sel . anchorNode , offset : sel . anchorOffset } ,
{ node : sel . focusNode , offset : sel . focusOffset }
] ;
}
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
function moveNativeSelectionToBookmark ( document , bm ) {
var sel = document . getSelection ( ) ,
range = document . createRange ( ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
range . setStart ( bm [ 0 ] . node , bm [ 0 ] . offset ) ;
range . collapse ( true ) ;
sel . removeAllRanges ( ) ;
sel . addRange ( range ) ;
sel . extend ( bm [ 1 ] . node , bm [ 1 ] . offset ) ;
2014-04-11 20:05:45 +00:00
}
// Creates cke_hidden_sel container and puts real selection there.
2018-06-17 16:07:19 +00:00
function hideSelection ( editor , ariaLabel ) {
var content = ariaLabel && CKEDITOR . tools . htmlEncode ( ariaLabel ) || ' ' ,
style = CKEDITOR . env . ie && CKEDITOR . env . version < 14 ? 'display:none' : 'position:fixed;top:0;left:-1000px' ,
2014-04-11 20:05:45 +00:00
hiddenEl = CKEDITOR . dom . element . createFromHtml (
2018-06-17 16:07:19 +00:00
'<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">' + content + '</div>' ,
2014-04-11 20:05:45 +00:00
editor . document ) ;
editor . fire ( 'lockSnapshot' ) ;
editor . editable ( ) . append ( hiddenEl ) ;
2018-06-17 16:07:19 +00:00
// Always use real selection to avoid overriding locked one (https://dev.ckeditor.com/ticket/11104#comment:13).
2014-04-11 20:05:45 +00:00
var sel = editor . getSelection ( 1 ) ,
range = editor . createRange ( ) ,
// Cancel selectionchange fired by selectRanges - prevent from firing selectionChange.
listener = sel . root . on ( 'selectionchange' , function ( evt ) {
evt . cancel ( ) ;
} , null , null , 0 ) ;
range . setStartAt ( hiddenEl , CKEDITOR . POSITION _AFTER _START ) ;
range . setEndAt ( hiddenEl , CKEDITOR . POSITION _BEFORE _END ) ;
sel . selectRanges ( [ range ] ) ;
listener . removeListener ( ) ;
editor . fire ( 'unlockSnapshot' ) ;
// Set this value at the end, so reset() executed by selectRanges()
// will clean up old hidden selection container.
editor . _ . hiddenSelectionContainer = hiddenEl ;
}
function removeHiddenSelectionContainer ( editor ) {
var hiddenEl = editor . _ . hiddenSelectionContainer ;
if ( hiddenEl ) {
2018-06-17 16:07:19 +00:00
var isDirty = editor . checkDirty ( ) ;
2014-04-11 20:05:45 +00:00
editor . fire ( 'lockSnapshot' ) ;
hiddenEl . remove ( ) ;
editor . fire ( 'unlockSnapshot' ) ;
2018-06-17 16:07:19 +00:00
! isDirty && editor . resetDirty ( ) ;
2014-04-11 20:05:45 +00:00
}
delete editor . _ . hiddenSelectionContainer ;
}
// Object containing keystroke handlers for fake selection.
var fakeSelectionDefaultKeystrokeHandlers = ( function ( ) {
function leave ( right ) {
return function ( evt ) {
var range = evt . editor . createRange ( ) ;
// Move selection only if there's a editable place for it.
// It no, then do nothing (keystroke will be blocked, widget selection kept).
if ( range . moveToClosestEditablePosition ( evt . selected , right ) )
evt . editor . getSelection ( ) . selectRanges ( [ range ] ) ;
// Prevent default.
return false ;
} ;
}
function del ( right ) {
return function ( evt ) {
var editor = evt . editor ,
range = editor . createRange ( ) ,
found ;
2018-06-17 16:07:19 +00:00
// We have to skip deletion for read only editor (#1516).
if ( editor . readOnly ) {
return ;
}
2014-04-11 20:05:45 +00:00
// If haven't found place for caret on the default side,
// try to find it on the other side.
if ( ! ( found = range . moveToClosestEditablePosition ( evt . selected , right ) ) )
found = range . moveToClosestEditablePosition ( evt . selected , ! right ) ;
if ( found )
editor . getSelection ( ) . selectRanges ( [ range ] ) ;
// Save the state before removing selected element.
editor . fire ( 'saveSnapshot' ) ;
evt . selected . remove ( ) ;
// Haven't found any editable space before removing element,
// try to place the caret anywhere (most likely, in empty editable).
if ( ! found ) {
range . moveToElementEditablePosition ( editor . editable ( ) ) ;
editor . getSelection ( ) . selectRanges ( [ range ] ) ;
}
editor . fire ( 'saveSnapshot' ) ;
// Prevent default.
return false ;
} ;
}
var leaveLeft = leave ( ) ,
leaveRight = leave ( 1 ) ;
return {
37 : leaveLeft , // LEFT
38 : leaveLeft , // UP
39 : leaveRight , // RIGHT
40 : leaveRight , // DOWN
8 : del ( ) , // BACKSPACE
46 : del ( 1 ) // DELETE
} ;
} ) ( ) ;
// Handle left, right, delete and backspace keystrokes next to non-editable elements
// by faking selection on them.
function getOnKeyDownListener ( editor ) {
var keystrokes = { 37 : 1 , 39 : 1 , 8 : 1 , 46 : 1 } ;
return function ( evt ) {
var keystroke = evt . data . getKeystroke ( ) ;
// Handle only left/right/del/bspace keys.
if ( ! keystrokes [ keystroke ] )
return ;
var sel = editor . getSelection ( ) ,
ranges = sel . getRanges ( ) ,
range = ranges [ 0 ] ;
// Handle only single range and it has to be collapsed.
if ( ranges . length != 1 || ! range . collapsed )
return ;
var next = range [ keystroke < 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ] ( ) ;
if ( next && next . type == CKEDITOR . NODE _ELEMENT && next . getAttribute ( 'contenteditable' ) == 'false' ) {
editor . getSelection ( ) . fake ( next ) ;
evt . data . preventDefault ( ) ;
evt . cancel ( ) ;
}
} ;
}
// If fake selection should be applied this function will return instance of
// CKEDITOR.dom.element which should gain fake selection.
function getNonEditableFakeSelectionReceiver ( ranges ) {
var enclosedNode , shrinkedNode , clone , range ;
if ( ranges . length == 1 && ! ( range = ranges [ 0 ] ) . collapsed &&
( enclosedNode = range . getEnclosedNode ( ) ) && enclosedNode . type == CKEDITOR . NODE _ELEMENT ) {
// So far we can't say that enclosed element is non-editable. Before checking,
// we'll shrink range (clone). Shrinking will stop on non-editable range, or
2018-06-17 16:07:19 +00:00
// innermost element (https://dev.ckeditor.com/ticket/11114).
2014-04-11 20:05:45 +00:00
clone = range . clone ( ) ;
clone . shrink ( CKEDITOR . SHRINK _ELEMENT , true ) ;
// If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements).
if ( ( shrinkedNode = clone . getEnclosedNode ( ) ) && shrinkedNode . type == CKEDITOR . NODE _ELEMENT )
enclosedNode = shrinkedNode ;
if ( enclosedNode . getAttribute ( 'contenteditable' ) == 'false' )
return enclosedNode ;
}
}
// Fix ranges which may end after hidden selection container.
// Note: this function may only be used if hidden selection container
// is not in DOM any more.
function fixRangesAfterHiddenSelectionContainer ( ranges , root ) {
var range ;
for ( var i = 0 ; i < ranges . length ; ++ i ) {
range = ranges [ i ] ;
if ( range . endContainer . equals ( root ) ) {
// We can use getChildCount() because hidden selection container is not in DOM.
range . endOffset = Math . min ( range . endOffset , root . getChildCount ( ) ) ;
}
}
}
// Extract only editable part or ranges.
// Note: this function modifies ranges list!
// @param {CKEDITOR.dom.rangeList} ranges
function extractEditableRanges ( ranges ) {
for ( var i = 0 ; i < ranges . length ; i ++ ) {
var range = ranges [ i ] ;
// Drop range spans inside one ready-only node.
var parent = range . getCommonAncestor ( ) ;
if ( parent . isReadOnly ( ) )
ranges . splice ( i , 1 ) ;
if ( range . collapsed )
continue ;
// Range may start inside a non-editable element,
// replace the range start after it.
if ( range . startContainer . isReadOnly ( ) ) {
var current = range . startContainer ,
isElement ;
while ( current ) {
isElement = current . type == CKEDITOR . NODE _ELEMENT ;
if ( ( isElement && current . is ( 'body' ) ) || ! current . isReadOnly ( ) )
break ;
if ( isElement && current . getAttribute ( 'contentEditable' ) == 'false' )
range . setStartAfter ( current ) ;
current = current . getParent ( ) ;
}
}
var startContainer = range . startContainer ,
endContainer = range . endContainer ,
startOffset = range . startOffset ,
endOffset = range . endOffset ,
walkerRange = range . clone ( ) ;
// Enlarge range start/end with text node to avoid walker
// being DOM destructive, it doesn't interfere our checking
// of elements below as well.
if ( startContainer && startContainer . type == CKEDITOR . NODE _TEXT ) {
if ( startOffset >= startContainer . getLength ( ) )
walkerRange . setStartAfter ( startContainer ) ;
else
walkerRange . setStartBefore ( startContainer ) ;
}
if ( endContainer && endContainer . type == CKEDITOR . NODE _TEXT ) {
if ( ! endOffset )
walkerRange . setEndBefore ( endContainer ) ;
else
walkerRange . setEndAfter ( endContainer ) ;
}
// Looking for non-editable element inside the range.
var walker = new CKEDITOR . dom . walker ( walkerRange ) ;
walker . evaluator = function ( node ) {
if ( node . type == CKEDITOR . NODE _ELEMENT && node . isReadOnly ( ) ) {
var newRange = range . clone ( ) ;
range . setEndBefore ( node ) ;
// Drop collapsed range around read-only elements,
// it make sure the range list empty when selecting
// only non-editable elements.
if ( range . collapsed )
ranges . splice ( i -- , 1 ) ;
// Avoid creating invalid range.
if ( ! ( node . getPosition ( walkerRange . endContainer ) & CKEDITOR . POSITION _CONTAINS ) ) {
newRange . setStartAfter ( node ) ;
if ( ! newRange . collapsed )
ranges . splice ( i + 1 , 0 , newRange ) ;
}
return true ;
}
return false ;
} ;
walker . next ( ) ;
}
return ranges ;
}
// Setup all editor instances for the necessary selection hooks.
CKEDITOR . on ( 'instanceCreated' , function ( ev ) {
var editor = ev . editor ;
editor . on ( 'contentDom' , function ( ) {
var doc = editor . document ,
outerDoc = CKEDITOR . document ,
editable = editor . editable ( ) ,
body = doc . getBody ( ) ,
html = doc . getDocumentElement ( ) ;
var isInline = editable . isInline ( ) ;
var restoreSel ,
lastSel ;
// Give the editable an initial selection on first focus,
// put selection at a consistent position at the start
2018-06-17 16:07:19 +00:00
// of the contents. (https://dev.ckeditor.com/ticket/9507)
2014-04-11 20:05:45 +00:00
if ( CKEDITOR . env . gecko ) {
editable . attachListener ( editable , 'focus' , function ( evt ) {
evt . removeListener ( ) ;
if ( restoreSel !== 0 ) {
var nativ = editor . getSelection ( ) . getNative ( ) ;
// Do it only if the native selection is at an unwanted
2018-06-17 16:07:19 +00:00
// place (at the very start of the editable). https://dev.ckeditor.com/ticket/10119
2014-04-11 20:05:45 +00:00
if ( nativ && nativ . isCollapsed && nativ . anchorNode == editable . $ ) {
var rng = editor . createRange ( ) ;
rng . moveToElementEditStart ( editable ) ;
rng . select ( ) ;
}
}
} , null , null , - 2 ) ;
}
// Plays the magic here to restore/save dom selection on editable focus/blur.
editable . attachListener ( editable , CKEDITOR . env . webkit ? 'DOMFocusIn' : 'focus' , function ( ) {
// On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
// to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
// for the same active element, what will e.g. mean restoring after displaying dialog.
2018-06-17 16:07:19 +00:00
if ( restoreSel && CKEDITOR . env . webkit ) {
2014-04-11 20:05:45 +00:00
restoreSel = editor . _ . previousActive && editor . _ . previousActive . equals ( doc . getActive ( ) ) ;
2018-06-17 16:07:19 +00:00
// On Webkit when editor uses divarea, native focus causes editable viewport to scroll
// to the top (when there is no active selection inside while focusing) so the scroll
// position should be restored after focusing back editable area. (https://dev.ckeditor.com/ticket/14659)
if ( restoreSel && editor . _ . previousScrollTop != null && editor . _ . previousScrollTop != editable . $ . scrollTop ) {
editable . $ . scrollTop = editor . _ . previousScrollTop ;
}
}
2014-04-11 20:05:45 +00:00
editor . unlockSelection ( restoreSel ) ;
restoreSel = 0 ;
} , null , null , - 1 ) ;
// Disable selection restoring when clicking in.
editable . attachListener ( editable , 'mousedown' , function ( ) {
restoreSel = 0 ;
} ) ;
2018-06-17 16:07:19 +00:00
// Save a cloned version of current selection.
function saveSel ( ) {
lastSel = new CKEDITOR . dom . selection ( editor . getSelection ( ) ) ;
lastSel . lock ( ) ;
}
2014-04-11 20:05:45 +00:00
// Browsers could loose the selection once the editable lost focus,
// in such case we need to reproduce it by saving a locked selection
// and restoring it upon focus gain.
2018-06-17 16:07:19 +00:00
if ( CKEDITOR . env . ie || isInline ) {
2014-04-11 20:05:45 +00:00
// For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event.
// For the rest, a more frequent check is required for each selection change made.
if ( isMSSelection )
editable . attachListener ( editable , 'beforedeactivate' , saveSel , null , null , - 1 ) ;
else
editable . attachListener ( editor , 'selectionCheck' , saveSel , null , null , - 1 ) ;
// Lock the selection and mark it to be restored.
// On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be
// fired when nested editable is blurred.
editable . attachListener ( editable , CKEDITOR . env . webkit ? 'DOMFocusOut' : 'blur' , function ( ) {
editor . lockSelection ( lastSel ) ;
restoreSel = 1 ;
} , null , null , - 1 ) ;
// Disable selection restoring when clicking in.
editable . attachListener ( editable , 'mousedown' , function ( ) {
restoreSel = 0 ;
} ) ;
}
// The following selection-related fixes only apply to classic (`iframe`-based) editable.
if ( CKEDITOR . env . ie && ! isInline ) {
var scroll ;
editable . attachListener ( editable , 'mousedown' , function ( evt ) {
// IE scrolls document to top on right mousedown
// when editor has no focus, remember this scroll
2018-06-17 16:07:19 +00:00
// position and revert it before context menu opens. (https://dev.ckeditor.com/ticket/5778)
2014-04-11 20:05:45 +00:00
if ( evt . data . $ . button == 2 ) {
var sel = editor . document . getSelection ( ) ;
if ( ! sel || sel . getType ( ) == CKEDITOR . SELECTION _NONE )
scroll = editor . window . getScrollPosition ( ) ;
}
} ) ;
editable . attachListener ( editable , 'mouseup' , function ( evt ) {
// Restore recorded scroll position when needed on right mouseup.
if ( evt . data . $ . button == 2 && scroll ) {
editor . document . $ . documentElement . scrollLeft = scroll . x ;
editor . document . $ . documentElement . scrollTop = scroll . y ;
}
scroll = null ;
} ) ;
// When content doc is in standards mode, IE doesn't focus the editor when
// clicking at the region below body (on html element) content, we emulate
2018-06-17 16:07:19 +00:00
// the normal behavior on old IEs. (https://dev.ckeditor.com/ticket/1659, https://dev.ckeditor.com/ticket/7932)
2014-04-11 20:05:45 +00:00
if ( doc . $ . compatMode != 'BackCompat' ) {
if ( CKEDITOR . env . ie7Compat || CKEDITOR . env . ie6Compat ) {
2018-06-17 16:07:19 +00:00
var textRng ,
startRng ;
2014-04-11 20:05:45 +00:00
html . on ( 'mousedown' , function ( evt ) {
evt = evt . data ;
// Expand the text range along with mouse move.
function onHover ( evt ) {
evt = evt . data . $ ;
if ( textRng ) {
// Read the current cursor.
var rngEnd = body . $ . createTextRange ( ) ;
2018-06-17 16:07:19 +00:00
moveRangeToPoint ( rngEnd , evt . clientX , evt . clientY ) ;
2014-04-11 20:05:45 +00:00
// Handle drag directions.
textRng . setEndPoint (
startRng . compareEndPoints ( 'StartToStart' , rngEnd ) < 0 ?
'EndToEnd' : 'StartToStart' , rngEnd ) ;
// Update selection with new range.
textRng . select ( ) ;
}
}
function removeListeners ( ) {
outerDoc . removeListener ( 'mouseup' , onSelectEnd ) ;
html . removeListener ( 'mouseup' , onSelectEnd ) ;
}
function onSelectEnd ( ) {
html . removeListener ( 'mousemove' , onHover ) ;
removeListeners ( ) ;
2018-06-17 16:07:19 +00:00
// Make it in effect on mouse up. (https://dev.ckeditor.com/ticket/9022)
2014-04-11 20:05:45 +00:00
textRng . select ( ) ;
}
// We're sure that the click happens at the region
// below body, but not on scrollbar.
if ( evt . getTarget ( ) . is ( 'html' ) &&
2018-06-17 16:07:19 +00:00
evt . $ . y < html . $ . clientHeight &&
evt . $ . x < html . $ . clientWidth ) {
2014-04-11 20:05:45 +00:00
// Start to build the text range.
2018-06-17 16:07:19 +00:00
textRng = body . $ . createTextRange ( ) ;
moveRangeToPoint ( textRng , evt . $ . clientX , evt . $ . clientY ) ;
2014-04-11 20:05:45 +00:00
// Records the dragging start of the above text range.
2018-06-17 16:07:19 +00:00
startRng = textRng . duplicate ( ) ;
2014-04-11 20:05:45 +00:00
html . on ( 'mousemove' , onHover ) ;
outerDoc . on ( 'mouseup' , onSelectEnd ) ;
html . on ( 'mouseup' , onSelectEnd ) ;
}
} ) ;
}
// It's much simpler for IE8+, we just need to reselect the reported range.
// This hack does not work on IE>=11 because there's no old selection&range APIs.
if ( CKEDITOR . env . version > 7 && CKEDITOR . env . version < 11 ) {
html . on ( 'mousedown' , function ( evt ) {
if ( evt . data . getTarget ( ) . is ( 'html' ) ) {
2018-06-17 16:07:19 +00:00
// Limit the text selection mouse move inside of editable. (https://dev.ckeditor.com/ticket/9715)
2014-04-11 20:05:45 +00:00
outerDoc . on ( 'mouseup' , onSelectEnd ) ;
html . on ( 'mouseup' , onSelectEnd ) ;
}
} ) ;
}
}
}
// We check the selection change:
// 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code)
// 2. After the accomplish of keyboard and mouse events.
editable . attachListener ( editable , 'selectionchange' , checkSelectionChange , editor ) ;
editable . attachListener ( editable , 'keyup' , checkSelectionChangeTimeout , editor ) ;
2018-06-17 16:07:19 +00:00
// https://dev.ckeditor.com/ticket/14407 - Don't even let anything happen if the selection is in a non-editable element.
editable . attachListener ( editable , 'keydown' , function ( evt ) {
var sel = this . getSelection ( 1 ) ;
if ( nonEditableAscendant ( sel ) ) {
sel . selectElement ( nonEditableAscendant ( sel ) ) ;
evt . data . preventDefault ( ) ;
}
} , editor ) ;
2014-04-11 20:05:45 +00:00
// Always fire the selection change on focus gain.
// On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
// we need synchronization between those listeners to not lost cached editor._.previousActive property
// (which is updated on selectionCheck).
editable . attachListener ( editable , CKEDITOR . env . webkit ? 'DOMFocusIn' : 'focus' , function ( ) {
editor . forceNextSelectionCheck ( ) ;
editor . selectionChange ( 1 ) ;
} ) ;
2018-06-17 16:07:19 +00:00
// https://dev.ckeditor.com/ticket/9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
// by dragging and releasing mouse button outside editable. Dragging (mousedown)
2014-04-11 20:05:45 +00:00
// has to be initialized in editable, but for mouseup we listen on document element.
2018-06-17 16:07:19 +00:00
if ( isInline && ( CKEDITOR . env . webkit || CKEDITOR . env . gecko ) ) {
2014-04-11 20:05:45 +00:00
var mouseDown ;
editable . attachListener ( editable , 'mousedown' , function ( ) {
mouseDown = 1 ;
} ) ;
editable . attachListener ( doc . getDocumentElement ( ) , 'mouseup' , function ( ) {
if ( mouseDown )
checkSelectionChangeTimeout . call ( editor ) ;
mouseDown = 0 ;
} ) ;
}
2018-06-17 16:07:19 +00:00
// In all other cases listen on simple mouseup over editable, as we did before https://dev.ckeditor.com/ticket/9699.
2014-04-11 20:05:45 +00:00
//
// Use document instead of editable in non-IEs for observing mouseup
// since editable won't fire the event if selection process started within iframe and ended out
2018-06-17 16:07:19 +00:00
// of the editor (https://dev.ckeditor.com/ticket/9851).
else {
2014-04-11 20:05:45 +00:00
editable . attachListener ( CKEDITOR . env . ie ? editable : doc . getDocumentElement ( ) , 'mouseup' , checkSelectionChangeTimeout , editor ) ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
if ( CKEDITOR . env . webkit ) {
// Before keystroke is handled by editor, check to remove the filling char.
editable . attachListener ( doc , 'keydown' , function ( evt ) {
var key = evt . data . getKey ( ) ;
// Remove the filling char before some keys get
// executed, so they'll not get blocked by it.
switch ( key ) {
case 13 : // ENTER
case 33 : // PAGEUP
case 34 : // PAGEDOWN
case 35 : // HOME
case 36 : // END
case 37 : // LEFT-ARROW
case 39 : // RIGHT-ARROW
case 8 : // BACKSPACE
case 45 : // INS
case 46 : // DEl
2018-06-17 16:07:19 +00:00
if ( editable . hasFocus ) {
removeFillingCharSequenceNode ( editable ) ;
}
2014-04-11 20:05:45 +00:00
}
} , null , null , - 1 ) ;
}
editable . attachListener ( editable , 'keydown' , getOnKeyDownListener ( editor ) , null , null , - 1 ) ;
2018-06-17 16:07:19 +00:00
function moveRangeToPoint ( range , x , y ) {
// Error prune in IE7. (https://dev.ckeditor.com/ticket/9034, https://dev.ckeditor.com/ticket/9110)
try {
range . moveToPoint ( x , y ) ;
} catch ( e ) { }
}
function removeListeners ( ) {
outerDoc . removeListener ( 'mouseup' , onSelectEnd ) ;
html . removeListener ( 'mouseup' , onSelectEnd ) ;
}
function onSelectEnd ( ) {
removeListeners ( ) ;
// The event is not fired when clicking on the scrollbars,
// so we can safely check the following to understand
// whether the empty space following <body> has been clicked.
var sel = CKEDITOR . document . $ . selection ,
range = sel . createRange ( ) ;
// The selection range is reported on host, but actually it should applies to the content doc.
// The parentElement may be null for read only mode in IE10 and below (https://dev.ckeditor.com/ticket/9780).
if ( sel . type != 'None' && range . parentElement ( ) && range . parentElement ( ) . ownerDocument == doc . $ )
range . select ( ) ;
}
function nonEditableAscendant ( sel ) {
if ( CKEDITOR . env . ie ) {
var range = sel . getRanges ( ) [ 0 ] ,
ascendant = range ? range . startContainer . getAscendant ( function ( parent ) {
return parent . type == CKEDITOR . NODE _ELEMENT &&
( parent . getAttribute ( 'contenteditable' ) == 'false' || parent . getAttribute ( 'contenteditable' ) == 'true' ) ;
} , true ) : null ;
return range && ascendant . getAttribute ( 'contenteditable' ) == 'false' && ascendant ;
}
}
} ) ;
editor . on ( 'setData' , function ( ) {
// Invalidate locked selection when unloading DOM.
// (https://dev.ckeditor.com/ticket/9521, https://dev.ckeditor.com/ticket/5217#comment:32 and https://dev.ckeditor.com/ticket/11500#comment:11)
editor . unlockSelection ( ) ;
// Webkit's selection will mess up after the data loading.
if ( CKEDITOR . env . webkit )
clearSelection ( ) ;
} ) ;
// Catch all the cases which above setData listener couldn't catch.
// For example: switching to source mode and destroying editor.
editor . on ( 'contentDomUnload' , function ( ) {
editor . unlockSelection ( ) ;
2014-04-11 20:05:45 +00:00
} ) ;
2018-06-17 16:07:19 +00:00
// IE9 might cease to work if there's an object selection inside the iframe (https://dev.ckeditor.com/ticket/7639).
if ( CKEDITOR . env . ie9Compat )
editor . on ( 'beforeDestroy' , clearSelection , null , null , 9 ) ;
2014-04-11 20:05:45 +00:00
// Check selection change on data reload.
editor . on ( 'dataReady' , function ( ) {
// Clean up fake selection after setting data.
delete editor . _ . fakeSelection ;
delete editor . _ . hiddenSelectionContainer ;
editor . selectionChange ( 1 ) ;
} ) ;
2018-06-17 16:07:19 +00:00
2014-04-11 20:05:45 +00:00
// When loaded data are ready check whether hidden selection container was not loaded.
editor . on ( 'loadSnapshot' , function ( ) {
2018-06-17 16:07:19 +00:00
var isElement = CKEDITOR . dom . walker . nodeType ( CKEDITOR . NODE _ELEMENT ) ,
// TODO replace with el.find() which will be introduced in https://dev.ckeditor.com/ticket/9764,
// because it may happen that hidden sel container won't be the last element.
last = editor . editable ( ) . getLast ( isElement ) ;
if ( last && last . hasAttribute ( 'data-cke-hidden-sel' ) ) {
last . remove ( ) ;
// Firefox does a very unfortunate thing. When a non-editable element is the only
// element in the editable, when we remove the hidden selection container, Firefox
// will insert a bogus <br> at the beginning of the editable...
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201
//
// This behavior is never desired because this <br> pushes the content lower, but in
// this case it is especially dangerous, because it happens when a bookmark is being restored.
// Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2
// what results in errors.
//
// So... let's revert what Firefox broke.
if ( CKEDITOR . env . gecko ) {
var first = editor . editable ( ) . getFirst ( isElement ) ;
if ( first && first . is ( 'br' ) && first . getAttribute ( '_moz_editor_bogus_node' ) ) {
first . remove ( ) ;
}
}
}
2014-04-11 20:05:45 +00:00
} , null , null , 100 ) ;
editor . on ( 'key' , function ( evt ) {
2018-06-17 16:07:19 +00:00
if ( editor . mode != 'wysiwyg' ) {
2014-04-11 20:05:45 +00:00
return ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
var sel = editor . getSelection ( ) ;
if ( ! sel . isFake )
return ;
var handler = fakeSelectionDefaultKeystrokeHandlers [ evt . data . keyCode ] ;
if ( handler )
return handler ( { editor : editor , selected : sel . getSelectedElement ( ) , selection : sel , keyEvent : evt } ) ;
} ) ;
2018-06-17 16:07:19 +00:00
function clearSelection ( ) {
var sel = editor . getSelection ( ) ;
sel && sel . removeAllRanges ( ) ;
}
2014-04-11 20:05:45 +00:00
} ) ;
2018-06-17 16:07:19 +00:00
// On WebKit only, we need a special "filling" char on some situations
// (https://dev.ckeditor.com/ticket/1272). Here we set the events that should invalidate that char.
if ( CKEDITOR . env . webkit ) {
CKEDITOR . on ( 'instanceReady' , function ( evt ) {
var editor = evt . editor ;
2014-04-11 20:05:45 +00:00
editor . on ( 'selectionChange' , function ( ) {
2018-06-17 16:07:19 +00:00
checkFillingCharSequenceNodeReady ( editor . editable ( ) ) ;
2014-04-11 20:05:45 +00:00
} , null , null , - 1 ) ;
2018-06-17 16:07:19 +00:00
2014-04-11 20:05:45 +00:00
editor . on ( 'beforeSetMode' , function ( ) {
2018-06-17 16:07:19 +00:00
removeFillingCharSequenceNode ( editor . editable ( ) ) ;
2014-04-11 20:05:45 +00:00
} , null , null , - 1 ) ;
2018-06-17 16:07:19 +00:00
// Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
// Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
// bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (https://dev.ckeditor.com/ticket/13816).
editor . on ( 'getSnapshot' , function ( evt ) {
if ( evt . data ) {
evt . data = removeFillingCharSequenceString ( evt . data ) ;
2014-04-11 20:05:45 +00:00
}
2018-06-17 16:07:19 +00:00
} , editor , null , 20 ) ;
// Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
// instead of #getData because once removed, FCSeq may leave an empty element,
// which should be pruned by the dataProcessor (https://dev.ckeditor.com/ticket/13816).
// Note: Used low priority to filter when dataProcessor works on strings,
// not pseudo– DOM.
editor . on ( 'toDataFormat' , function ( evt ) {
evt . data . dataValue = removeFillingCharSequenceString ( evt . data . dataValue ) ;
} , null , null , 0 ) ;
} ) ;
}
2014-04-11 20:05:45 +00:00
/ * *
* Check the selection change in editor and potentially fires
* the { @ link CKEDITOR . editor # event - selectionChange } event .
*
* @ method
* @ member CKEDITOR . editor
* @ param { Boolean } [ checkNow = false ] Force the check to happen immediately
* instead of coming with a timeout delay ( default ) .
* /
CKEDITOR . editor . prototype . selectionChange = function ( checkNow ) {
( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ) . call ( this ) ;
} ;
/ * *
* Retrieve the editor selection in scope of editable element .
*
* * * Note : * * Since the native browser selection provides only one single
* selection at a time per document , so if editor ' s editable element has lost focus ,
* this method will return a null value unless the { @ link CKEDITOR . editor # lockSelection }
* has been called beforehand so the saved selection is retrieved .
*
* var selection = CKEDITOR . instances . editor1 . getSelection ( ) ;
* alert ( selection . getType ( ) ) ;
*
* @ method
* @ member CKEDITOR . editor
* @ param { Boolean } forceRealSelection Return real selection , instead of saved or fake one .
* @ returns { CKEDITOR . dom . selection } A selection object or null if not available for the moment .
* /
CKEDITOR . editor . prototype . getSelection = function ( forceRealSelection ) {
// Check if there exists a locked or fake selection.
if ( ( this . _ . savedSelection || this . _ . fakeSelection ) && ! forceRealSelection )
return this . _ . savedSelection || this . _ . fakeSelection ;
// Editable element might be absent or editor might not be in a wysiwyg mode.
var editable = this . editable ( ) ;
return editable && this . mode == 'wysiwyg' ? new CKEDITOR . dom . selection ( editable ) : null ;
} ;
/ * *
* Locks the selection made in the editor in order to make it possible to
* manipulate it without browser interference . A locked selection is
* cached and remains unchanged until it is released with the
* { @ link CKEDITOR . editor # unlockSelection } method .
*
* @ method
* @ member CKEDITOR . editor
* @ param { CKEDITOR . dom . selection } [ sel ] Specify the selection to be locked .
* @ returns { Boolean } ` true ` if selection was locked .
* /
CKEDITOR . editor . prototype . lockSelection = function ( sel ) {
sel = sel || this . getSelection ( 1 ) ;
if ( sel . getType ( ) != CKEDITOR . SELECTION _NONE ) {
! sel . isLocked && sel . lock ( ) ;
this . _ . savedSelection = sel ;
return true ;
}
return false ;
} ;
/ * *
* Unlocks the selection made in the editor and locked with the
2018-06-17 16:07:19 +00:00
* { @ link CKEDITOR . editor # lockSelection } method . An unlocked selection
2014-04-11 20:05:45 +00:00
* is no longer cached and can be changed .
*
* @ method
* @ member CKEDITOR . editor
* @ param { Boolean } [ restore ] If set to ` true ` , the selection is
* restored back to the selection saved earlier by using the
* { @ link CKEDITOR . dom . selection # lock } method .
* /
CKEDITOR . editor . prototype . unlockSelection = function ( restore ) {
var sel = this . _ . savedSelection ;
if ( sel ) {
sel . unlock ( restore ) ;
delete this . _ . savedSelection ;
return true ;
}
return false ;
} ;
/ * *
* @ method
* @ member CKEDITOR . editor
* @ todo
* /
CKEDITOR . editor . prototype . forceNextSelectionCheck = function ( ) {
delete this . _ . selectionPreviousPath ;
} ;
/ * *
* Gets the current selection in context of the document ' s body element .
*
* var selection = CKEDITOR . instances . editor1 . document . getSelection ( ) ;
* alert ( selection . getType ( ) ) ;
*
* @ method
* @ member CKEDITOR . dom . document
* @ returns { CKEDITOR . dom . selection } A selection object .
* /
CKEDITOR . dom . document . prototype . getSelection = function ( ) {
return new CKEDITOR . dom . selection ( this ) ;
} ;
/ * *
* Select this range as the only one with { @ link CKEDITOR . dom . selection # selectRanges } .
*
* @ method
* @ returns { CKEDITOR . dom . selection }
* @ member CKEDITOR . dom . range
* /
CKEDITOR . dom . range . prototype . select = function ( ) {
var sel = this . root instanceof CKEDITOR . editable ? this . root . editor . getSelection ( ) : new CKEDITOR . dom . selection ( this . root ) ;
sel . selectRanges ( [ this ] ) ;
return sel ;
} ;
/ * *
* No selection .
*
* if ( editor . getSelection ( ) . getType ( ) == CKEDITOR . SELECTION _NONE )
* alert ( 'Nothing is selected' ) ;
*
* @ readonly
* @ property { Number } [ = 1 ]
* @ member CKEDITOR
* /
CKEDITOR . SELECTION _NONE = 1 ;
/ * *
* A text or a collapsed selection .
*
* if ( editor . getSelection ( ) . getType ( ) == CKEDITOR . SELECTION _TEXT )
* alert ( 'A text is selected' ) ;
*
* @ readonly
* @ property { Number } [ = 2 ]
* @ member CKEDITOR
* /
CKEDITOR . SELECTION _TEXT = 2 ;
/ * *
* Element selection .
*
* if ( editor . getSelection ( ) . getType ( ) == CKEDITOR . SELECTION _ELEMENT )
* alert ( 'An element is selected' ) ;
*
* @ readonly
* @ property { Number } [ = 3 ]
* @ member CKEDITOR
* /
CKEDITOR . SELECTION _ELEMENT = 3 ;
/ * *
* Manipulates the selection within a DOM element . If the current browser selection
* spans outside of the element , an empty selection object is returned .
*
* Despite the fact that selection ' s constructor allows to create selection instances ,
* usually it ' s better to get selection from the editor instance :
*
* var sel = editor . getSelection ( ) ;
*
* See { @ link CKEDITOR . editor # getSelection } .
*
* @ class
* @ constructor Creates a selection class instance .
*
* // Selection scoped in document.
* var sel = new CKEDITOR . dom . selection ( CKEDITOR . document ) ;
*
* // Selection scoped in element with 'editable' id.
* var sel = new CKEDITOR . dom . selection ( CKEDITOR . document . getById ( 'editable' ) ) ;
*
* // Cloning selection.
* var clone = new CKEDITOR . dom . selection ( sel ) ;
*
* @ param { CKEDITOR . dom . document / CKEDITOR . dom . element / CKEDITOR . dom . selection } target
* The DOM document / element that the DOM selection is restrained to . Only selection which spans
* within the target element is considered as valid .
*
* If { @ link CKEDITOR . dom . selection } is passed , then its clone will be created .
* /
CKEDITOR . dom . selection = function ( target ) {
// Target is a selection - clone it.
if ( target instanceof CKEDITOR . dom . selection ) {
var selection = target ;
target = target . root ;
}
var isElement = target instanceof CKEDITOR . dom . element ,
root ;
this . rev = selection ? selection . rev : nextRev ++ ;
this . document = target instanceof CKEDITOR . dom . document ? target : target . getDocument ( ) ;
this . root = root = isElement ? target : this . document . getBody ( ) ;
this . isLocked = 0 ;
this . _ = {
cache : { }
} ;
// Clone selection.
if ( selection ) {
CKEDITOR . tools . extend ( this . _ . cache , selection . _ . cache ) ;
this . isFake = selection . isFake ;
this . isLocked = selection . isLocked ;
return this ;
}
// Check whether browser focus is really inside of the editable element.
var nativeSel = this . getNative ( ) ,
rangeParent ,
range ;
if ( nativeSel ) {
if ( nativeSel . getRangeAt ) {
range = nativeSel . rangeCount && nativeSel . getRangeAt ( 0 ) ;
rangeParent = range && new CKEDITOR . dom . node ( range . commonAncestorContainer ) ;
}
// For old IEs.
else {
// Sometimes, mostly when selection is close to the table or hr,
// IE throws "Unspecified error".
try {
range = nativeSel . createRange ( ) ;
} catch ( err ) { }
rangeParent = range && CKEDITOR . dom . element . get ( range . item && range . item ( 0 ) || range . parentElement ( ) ) ;
}
}
// Selection out of concerned range, empty the selection.
// TODO check whether this condition cannot be reverted to its old
2018-06-17 16:07:19 +00:00
// form (commented out) after we closed https://dev.ckeditor.com/ticket/10438.
2014-04-11 20:05:45 +00:00
//if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
if ( ! (
rangeParent &&
( rangeParent . type == CKEDITOR . NODE _ELEMENT || rangeParent . type == CKEDITOR . NODE _TEXT ) &&
( this . root . equals ( rangeParent ) || this . root . contains ( rangeParent ) )
) ) {
this . _ . cache . type = CKEDITOR . SELECTION _NONE ;
this . _ . cache . startElement = null ;
this . _ . cache . selectedElement = null ;
this . _ . cache . selectedText = '' ;
this . _ . cache . ranges = new CKEDITOR . dom . rangeList ( ) ;
}
return this ;
} ;
var styleObjectElements = { img : 1 , hr : 1 , li : 1 , table : 1 , tr : 1 , td : 1 , th : 1 , embed : 1 , object : 1 , ol : 1 , ul : 1 ,
a : 1 , input : 1 , form : 1 , select : 1 , textarea : 1 , button : 1 , fieldset : 1 , thead : 1 , tfoot : 1 } ;
2018-06-17 16:07:19 +00:00
CKEDITOR . tools . extend ( CKEDITOR . dom . selection , {
_removeFillingCharSequenceString : removeFillingCharSequenceString ,
_createFillingCharSequenceNode : createFillingCharSequenceNode ,
/ * *
* The sequence used in a WebKit - based browser to create a Filling Character . By default it is
* a string of 7 zero - width space characters ( U + 200 B ) .
*
* @ since 4.5 . 7
* @ readonly
* @ property { String }
* /
FILLING _CHAR _SEQUENCE : fillingCharSequence
} ) ;
2014-04-11 20:05:45 +00:00
CKEDITOR . dom . selection . prototype = {
/ * *
* Gets the native selection object from the browser .
*
* var selection = editor . getSelection ( ) . getNative ( ) ;
*
2018-06-17 16:07:19 +00:00
* @ returns { Object } The native browser selection object or null if this is a fake selection .
2014-04-11 20:05:45 +00:00
* /
getNative : function ( ) {
if ( this . _ . cache . nativeSel !== undefined )
return this . _ . cache . nativeSel ;
return ( this . _ . cache . nativeSel = isMSSelection ? this . document . $ . selection : this . document . getWindow ( ) . $ . getSelection ( ) ) ;
} ,
/ * *
* Gets the type of the current selection . The following values are
* available :
*
* * { @ link CKEDITOR # SELECTION _NONE } ( 1 ) : No selection .
* * { @ link CKEDITOR # SELECTION _TEXT } ( 2 ) : A text or a collapsed selection is selected .
* * { @ link CKEDITOR # SELECTION _ELEMENT } ( 3 ) : An element is selected .
*
* Example :
*
* if ( editor . getSelection ( ) . getType ( ) == CKEDITOR . SELECTION _TEXT )
* alert ( 'A text is selected' ) ;
*
* @ method
* @ returns { Number } One of the following constant values : { @ link CKEDITOR # SELECTION _NONE } ,
* { @ link CKEDITOR # SELECTION _TEXT } or { @ link CKEDITOR # SELECTION _ELEMENT } .
* /
getType : isMSSelection ?
function ( ) {
var cache = this . _ . cache ;
if ( cache . type )
return cache . type ;
var type = CKEDITOR . SELECTION _NONE ;
try {
var sel = this . getNative ( ) ,
ieType = sel . type ;
if ( ieType == 'Text' )
type = CKEDITOR . SELECTION _TEXT ;
if ( ieType == 'Control' )
type = CKEDITOR . SELECTION _ELEMENT ;
// It is possible that we can still get a text range
// object even when type == 'None' is returned by IE.
// So we'd better check the object returned by
// createRange() rather than by looking at the type.
if ( sel . createRange ( ) . parentElement ( ) )
type = CKEDITOR . SELECTION _TEXT ;
} catch ( e ) { }
return ( cache . type = type ) ;
} : function ( ) {
var cache = this . _ . cache ;
if ( cache . type )
return cache . type ;
var type = CKEDITOR . SELECTION _TEXT ;
var sel = this . getNative ( ) ;
if ( ! ( sel && sel . rangeCount ) )
type = CKEDITOR . SELECTION _NONE ;
else if ( sel . rangeCount == 1 ) {
// Check if the actual selection is a control (IMG,
// TABLE, HR, etc...).
var range = sel . getRangeAt ( 0 ) ,
startContainer = range . startContainer ;
2018-06-17 16:07:19 +00:00
if ( startContainer == range . endContainer && startContainer . nodeType == 1 &&
( range . endOffset - range . startOffset ) == 1 &&
styleObjectElements [ startContainer . childNodes [ range . startOffset ] . nodeName . toLowerCase ( ) ] ) {
2014-04-11 20:05:45 +00:00
type = CKEDITOR . SELECTION _ELEMENT ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
}
return ( cache . type = type ) ;
} ,
/ * *
* Retrieves the { @ link CKEDITOR . dom . range } instances that represent the current selection .
*
* Note : Some browsers return multiple ranges even for a continuous selection . Firefox , for example , returns
* one range for each table cell when one or more table rows are selected .
*
* var ranges = selection . getRanges ( ) ;
* alert ( ranges . length ) ;
*
* @ method
2018-06-17 16:07:19 +00:00
* @ param { Boolean } [ onlyEditables ] If set to ` true ` , this function retrieves editable ranges only .
2014-04-11 20:05:45 +00:00
* @ returns { Array } Range instances that represent the current selection .
* /
getRanges : ( function ( ) {
var func = isMSSelection ? ( function ( ) {
function getNodeIndex ( node ) {
return new CKEDITOR . dom . node ( node ) . getIndex ( ) ;
}
// Finds the container and offset for a specific boundary
// of an IE range.
var getBoundaryInformation = function ( range , start ) {
2018-06-17 16:07:19 +00:00
// Creates a collapsed range at the requested boundary.
range = range . duplicate ( ) ;
range . collapse ( start ) ;
// Gets the element that encloses the range entirely.
var parent = range . parentElement ( ) ;
// Empty parent element, e.g. <i>^</i>
if ( ! parent . hasChildNodes ( ) )
return { container : parent , offset : 0 } ;
var siblings = parent . children ,
child , sibling ,
testRange = range . duplicate ( ) ,
startIndex = 0 ,
endIndex = siblings . length - 1 ,
index = - 1 ,
position , distance , container ;
// Binary search over all element children to test the range to see whether
// range is right on the boundary of one element.
while ( startIndex <= endIndex ) {
index = Math . floor ( ( startIndex + endIndex ) / 2 ) ;
child = siblings [ index ] ;
testRange . moveToElementText ( child ) ;
position = testRange . compareEndPoints ( 'StartToStart' , range ) ;
if ( position > 0 )
endIndex = index - 1 ;
else if ( position < 0 )
startIndex = index + 1 ;
else
return { container : parent , offset : getNodeIndex ( child ) } ;
}
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// All children are text nodes,
// or to the right hand of test range are all text nodes. (https://dev.ckeditor.com/ticket/6992)
if ( index == - 1 || index == siblings . length - 1 && position < 0 ) {
// Adapt test range to embrace the entire parent contents.
testRange . moveToElementText ( parent ) ;
testRange . setEndPoint ( 'StartToStart' , range ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// IE report line break as CRLF with range.text but
// only LF with textnode.nodeValue, normalize them to avoid
// breaking character counting logic below. (https://dev.ckeditor.com/ticket/3949)
distance = testRange . text . replace ( /(\r\n|\r)/g , '\n' ) . length ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
siblings = parent . childNodes ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
// Actual range anchor right beside test range at the boundary of text node.
if ( ! distance ) {
child = siblings [ siblings . length - 1 ] ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
if ( child . nodeType != CKEDITOR . NODE _TEXT )
return { container : parent , offset : siblings . length } ;
else
return { container : child , offset : child . nodeValue . length } ;
}
// Start the measuring until distance overflows, meanwhile count the text nodes.
var i = siblings . length ;
while ( distance > 0 && i > 0 ) {
sibling = siblings [ -- i ] ;
if ( sibling . nodeType == CKEDITOR . NODE _TEXT ) {
container = sibling ;
distance -= sibling . nodeValue . length ;
2014-04-11 20:05:45 +00:00
}
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
return { container : container , offset : - distance } ;
}
// Test range was one offset beyond OR behind the anchored text node.
else {
// Adapt one side of test range to the actual range
// for measuring the offset between them.
testRange . collapse ( position > 0 ? true : false ) ;
testRange . setEndPoint ( position > 0 ? 'StartToStart' : 'EndToStart' , range ) ;
// IE report line break as CRLF with range.text but
// only LF with textnode.nodeValue, normalize them to avoid
// breaking character counting logic below. (https://dev.ckeditor.com/ticket/3949)
distance = testRange . text . replace ( /(\r\n|\r)/g , '\n' ) . length ;
// Actual range anchor right beside test range at the inner boundary of text node.
if ( ! distance )
return { container : parent , offset : getNodeIndex ( child ) + ( position > 0 ? 0 : 1 ) } ;
// Start the measuring until distance overflows, meanwhile count the text nodes.
while ( distance > 0 ) {
try {
sibling = child [ position > 0 ? 'previousSibling' : 'nextSibling' ] ;
2014-04-11 20:05:45 +00:00
if ( sibling . nodeType == CKEDITOR . NODE _TEXT ) {
distance -= sibling . nodeValue . length ;
2018-06-17 16:07:19 +00:00
container = sibling ;
2014-04-11 20:05:45 +00:00
}
2018-06-17 16:07:19 +00:00
child = sibling ;
2014-04-11 20:05:45 +00:00
}
2018-06-17 16:07:19 +00:00
// Measurement in IE could be sometimes wrong because of <select> element. (https://dev.ckeditor.com/ticket/4611)
catch ( e ) {
return { container : parent , offset : getNodeIndex ( child ) } ;
2014-04-11 20:05:45 +00:00
}
}
2018-06-17 16:07:19 +00:00
return { container : container , offset : position > 0 ? - distance : container . nodeValue . length + distance } ;
}
} ;
2014-04-11 20:05:45 +00:00
return function ( ) {
// IE doesn't have range support (in the W3C way), so we
// need to do some magic to transform selections into
// CKEDITOR.dom.range instances.
var sel = this . getNative ( ) ,
nativeRange = sel && sel . createRange ( ) ,
type = this . getType ( ) ,
range ;
if ( ! sel )
return [ ] ;
if ( type == CKEDITOR . SELECTION _TEXT ) {
range = new CKEDITOR . dom . range ( this . root ) ;
var boundaryInfo = getBoundaryInformation ( nativeRange , true ) ;
range . setStart ( new CKEDITOR . dom . node ( boundaryInfo . container ) , boundaryInfo . offset ) ;
boundaryInfo = getBoundaryInformation ( nativeRange ) ;
range . setEnd ( new CKEDITOR . dom . node ( boundaryInfo . container ) , boundaryInfo . offset ) ;
2018-06-17 16:07:19 +00:00
// Correct an invalid IE range case on empty list item. (https://dev.ckeditor.com/ticket/5850)
2014-04-11 20:05:45 +00:00
if ( range . endContainer . getPosition ( range . startContainer ) & CKEDITOR . POSITION _PRECEDING && range . endOffset <= range . startContainer . getIndex ( ) )
range . collapse ( ) ;
return [ range ] ;
} else if ( type == CKEDITOR . SELECTION _ELEMENT ) {
var retval = [ ] ;
for ( var i = 0 ; i < nativeRange . length ; i ++ ) {
var element = nativeRange . item ( i ) ,
parentElement = element . parentNode ,
j = 0 ;
range = new CKEDITOR . dom . range ( this . root ) ;
for ( ; j < parentElement . childNodes . length && parentElement . childNodes [ j ] != element ; j ++ ) {
2018-06-17 16:07:19 +00:00
2014-04-11 20:05:45 +00:00
}
range . setStart ( new CKEDITOR . dom . node ( parentElement ) , j ) ;
range . setEnd ( new CKEDITOR . dom . node ( parentElement ) , j + 1 ) ;
retval . push ( range ) ;
}
return retval ;
}
return [ ] ;
} ;
2018-06-17 16:07:19 +00:00
} ) ( ) :
function ( ) {
// On browsers implementing the W3C range, we simply
// transform the native ranges in CKEDITOR.dom.range
// instances.
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
var ranges = [ ] ,
range ,
sel = this . getNative ( ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
if ( ! sel )
return ranges ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
for ( var i = 0 ; i < sel . rangeCount ; i ++ ) {
var nativeRange = sel . getRangeAt ( i ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
range = new CKEDITOR . dom . range ( this . root ) ;
2014-04-11 20:05:45 +00:00
2018-06-17 16:07:19 +00:00
range . setStart ( new CKEDITOR . dom . node ( nativeRange . startContainer ) , nativeRange . startOffset ) ;
range . setEnd ( new CKEDITOR . dom . node ( nativeRange . endContainer ) , nativeRange . endOffset ) ;
ranges . push ( range ) ;
}
return ranges ;
} ;
2014-04-11 20:05:45 +00:00
return function ( onlyEditables ) {
var cache = this . _ . cache ,
ranges = cache . ranges ;
if ( ! ranges )
cache . ranges = ranges = new CKEDITOR . dom . rangeList ( func . call ( this ) ) ;
if ( ! onlyEditables )
return ranges ;
// Split range into multiple by read-only nodes.
2018-06-17 16:07:19 +00:00
// Clone ranges array to avoid changing cached ranges (https://dev.ckeditor.com/ticket/11493).
2014-04-11 20:05:45 +00:00
return extractEditableRanges ( new CKEDITOR . dom . rangeList ( ranges . slice ( ) ) ) ;
} ;
} ) ( ) ,
/ * *
* Gets the DOM element in which the selection starts .
*
* var element = editor . getSelection ( ) . getStartElement ( ) ;
* alert ( element . getName ( ) ) ;
*
* @ returns { CKEDITOR . dom . element } The element at the beginning of the selection .
* /
getStartElement : function ( ) {
var cache = this . _ . cache ;
if ( cache . startElement !== undefined )
return cache . startElement ;
var node ;
switch ( this . getType ( ) ) {
case CKEDITOR . SELECTION _ELEMENT :
return this . getSelectedElement ( ) ;
case CKEDITOR . SELECTION _TEXT :
var range = this . getRanges ( ) [ 0 ] ;
if ( range ) {
if ( ! range . collapsed ) {
range . optimize ( ) ;
// Decrease the range content to exclude particial
// selected node on the start which doesn't have
2018-06-17 16:07:19 +00:00
// visual impact. ( https://dev.ckeditor.com/ticket/3231 )
2014-04-11 20:05:45 +00:00
while ( 1 ) {
var startContainer = range . startContainer ,
startOffset = range . startOffset ;
2018-06-17 16:07:19 +00:00
// Limit the fix only to non-block elements.(https://dev.ckeditor.com/ticket/3950)
2014-04-11 20:05:45 +00:00
if ( startOffset == ( startContainer . getChildCount ? startContainer . getChildCount ( ) : startContainer . getLength ( ) ) && ! startContainer . isBlockBoundary ( ) )
range . setStartAfter ( startContainer ) ;
else
break ;
}
node = range . startContainer ;
if ( node . type != CKEDITOR . NODE _ELEMENT )
return node . getParent ( ) ;
node = node . getChild ( range . startOffset ) ;
if ( ! node || node . type != CKEDITOR . NODE _ELEMENT )
node = range . startContainer ;
else {
var child = node . getFirst ( ) ;
while ( child && child . type == CKEDITOR . NODE _ELEMENT ) {
node = child ;
child = child . getFirst ( ) ;
}
}
} else {
node = range . startContainer ;
if ( node . type != CKEDITOR . NODE _ELEMENT )
node = node . getParent ( ) ;
}
node = node . $ ;
}
}
return cache . startElement = ( node ? new CKEDITOR . dom . element ( node ) : null ) ;
} ,
/ * *
* Gets the currently selected element .
*
* var element = editor . getSelection ( ) . getSelectedElement ( ) ;
* alert ( element . getName ( ) ) ;
*
2018-06-17 16:07:19 +00:00
* @ returns { CKEDITOR . dom . element / null } The selected element . ` null ` if no
2014-04-11 20:05:45 +00:00
* selection is available or the selection type is not { @ link CKEDITOR # SELECTION _ELEMENT } .
* /
getSelectedElement : function ( ) {
var cache = this . _ . cache ;
if ( cache . selectedElement !== undefined )
return cache . selectedElement ;
var self = this ;
var node = CKEDITOR . tools . tryThese (
// Is it native IE control type selection?
function ( ) {
return self . getNative ( ) . createRange ( ) . item ( 0 ) ;
} ,
// Figure it out by checking if there's a single enclosed
// node of the range.
function ( ) {
var range = self . getRanges ( ) [ 0 ] . clone ( ) ,
enclosed , selected ;
// Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul>
for ( var i = 2 ; i && ! ( ( enclosed = range . getEnclosedNode ( ) ) && ( enclosed . type == CKEDITOR . NODE _ELEMENT ) && styleObjectElements [ enclosed . getName ( ) ] && ( selected = enclosed ) ) ; i -- ) {
// Then check any deep wrapped element, e.g. [<b><i><img /></i></b>]
range . shrink ( CKEDITOR . SHRINK _ELEMENT ) ;
}
return selected && selected . $ ;
}
) ;
return cache . selectedElement = ( node ? new CKEDITOR . dom . element ( node ) : null ) ;
} ,
/ * *
* Retrieves the text contained within the range . An empty string is returned for non - text selection .
*
* var text = editor . getSelection ( ) . getSelectedText ( ) ;
* alert ( text ) ;
*
* @ since 3.6 . 1
* @ returns { String } A string of text within the current selection .
* /
getSelectedText : function ( ) {
var cache = this . _ . cache ;
if ( cache . selectedText !== undefined )
return cache . selectedText ;
var nativeSel = this . getNative ( ) ,
text = isMSSelection ? nativeSel . type == 'Control' ? '' : nativeSel . createRange ( ) . text : nativeSel . toString ( ) ;
return ( cache . selectedText = text ) ;
} ,
/ * *
* Locks the selection made in the editor in order to make it possible to
* manipulate it without browser interference . A locked selection is
* cached and remains unchanged until it is released with the { @ link # unlock } method .
*
* editor . getSelection ( ) . lock ( ) ;
* /
lock : function ( ) {
// Call all cacheable function.
this . getRanges ( ) ;
this . getStartElement ( ) ;
this . getSelectedElement ( ) ;
this . getSelectedText ( ) ;
// The native selection is not available when locked.
this . _ . cache . nativeSel = null ;
this . isLocked = 1 ;
} ,
/ * *
* @ todo
* /
unlock : function ( restore ) {
if ( ! this . isLocked )
return ;
if ( restore ) {
var selectedElement = this . getSelectedElement ( ) ,
2018-06-17 16:07:19 +00:00
ranges = this . getRanges ( ) ,
2014-04-11 20:05:45 +00:00
faked = this . isFake ;
}
this . isLocked = 0 ;
this . reset ( ) ;
if ( restore ) {
// Saved selection may be outdated (e.g. anchored in offline nodes).
// Avoid getting broken by such.
var common = selectedElement || ranges [ 0 ] && ranges [ 0 ] . getCommonAncestor ( ) ;
if ( ! ( common && common . getAscendant ( 'body' , 1 ) ) )
return ;
2018-06-17 16:07:19 +00:00
if ( isTableSelection ( ranges ) ) {
// Tables have it's own selection method.
performFakeTableSelection . call ( this , ranges ) ;
} else if ( faked )
2014-04-11 20:05:45 +00:00
this . fake ( selectedElement ) ;
else if ( selectedElement )
this . selectElement ( selectedElement ) ;
else
this . selectRanges ( ranges ) ;
}
} ,
/ * *
* Clears the selection cache .
*
* editor . getSelection ( ) . reset ( ) ;
* /
reset : function ( ) {
this . _ . cache = { } ;
this . isFake = 0 ;
2018-06-17 16:07:19 +00:00
var editor = this . root . editor ;
2014-04-11 20:05:45 +00:00
// Invalidate any fake selection available in the editor.
if ( editor && editor . _ . fakeSelection ) {
// Test whether this selection is the one that was
// faked or its clone.
if ( this . rev == editor . _ . fakeSelection . rev ) {
delete editor . _ . fakeSelection ;
removeHiddenSelectionContainer ( editor ) ;
}
2018-06-17 16:07:19 +00:00
else {
CKEDITOR . warn ( 'selection-fake-reset' ) ;
}
2014-04-11 20:05:45 +00:00
}
this . rev = nextRev ++ ;
} ,
/ * *
* Makes the current selection of type { @ link CKEDITOR # SELECTION _ELEMENT } by enclosing the specified element .
*
* var element = editor . document . getById ( 'sampleElement' ) ;
* editor . getSelection ( ) . selectElement ( element ) ;
*
* @ param { CKEDITOR . dom . element } element The element to enclose in the selection .
* /
selectElement : function ( element ) {
var range = new CKEDITOR . dom . range ( this . root ) ;
range . setStartBefore ( element ) ;
range . setEndAfter ( element ) ;
this . selectRanges ( [ range ] ) ;
} ,
/ * *
* Clears the original selection and adds the specified ranges to the document selection .
*
* // Move selection to the end of the editable element.
* var range = editor . createRange ( ) ;
* range . moveToPosition ( range . root , CKEDITOR . POSITION _BEFORE _END ) ;
* editor . getSelection ( ) . selectRanges ( [ ranges ] ) ;
*
* @ param { Array } ranges An array of { @ link CKEDITOR . dom . range } instances
* representing ranges to be added to the document .
* /
selectRanges : function ( ranges ) {
var editor = this . root . editor ,
hadHiddenSelectionContainer = editor && editor . _ . hiddenSelectionContainer ;
this . reset ( ) ;
// Check if there's a hiddenSelectionContainer in editable at some index.
// Some ranges may be anchored after the hiddenSelectionContainer and,
// once the container is removed while resetting the selection, they
2018-06-17 16:07:19 +00:00
// may need new endOffset (one element less within the range) (https://dev.ckeditor.com/ticket/11021 https://dev.ckeditor.com/ticket/11393).
2014-04-11 20:05:45 +00:00
if ( hadHiddenSelectionContainer )
fixRangesAfterHiddenSelectionContainer ( ranges , this . root ) ;
if ( ! ranges . length )
return ;
// Refresh the locked selection.
if ( this . isLocked ) {
// making a new DOM selection will force the focus on editable in certain situation,
// we have to save the currently focused element for later recovery.
var focused = CKEDITOR . document . getActive ( ) ;
this . unlock ( ) ;
this . selectRanges ( ranges ) ;
this . lock ( ) ;
// Return to the previously focused element.
2018-06-17 16:07:19 +00:00
focused && ! focused . equals ( this . root ) && focused . focus ( ) ;
2014-04-11 20:05:45 +00:00
return ;
}
// Handle special case - automatic fake selection on non-editable elements.
var receiver = getNonEditableFakeSelectionReceiver ( ranges ) ;
if ( receiver ) {
this . fake ( receiver ) ;
return ;
}
2018-06-17 16:07:19 +00:00
// Handle special case - fake selection of table cells.
if ( editor && editor . plugins . tableselection &&
CKEDITOR . plugins . tableselection . isSupportedEnvironment &&
isTableSelection ( ranges ) && ! isSelectingTable
) {
performFakeTableSelection . call ( this , ranges ) ;
return ;
}
2014-04-11 20:05:45 +00:00
if ( isMSSelection ) {
var notWhitespaces = CKEDITOR . dom . walker . whitespaces ( true ) ,
fillerTextRegex = /\ufeff|\u00a0/ ,
nonCells = { table : 1 , tbody : 1 , tr : 1 } ;
if ( ranges . length > 1 ) {
// IE doesn't accept multiple ranges selection, so we join all into one.
var last = ranges [ ranges . length - 1 ] ;
ranges [ 0 ] . setEnd ( last . endContainer , last . endOffset ) ;
}
var range = ranges [ 0 ] ;
var collapsed = range . collapsed ,
isStartMarkerAlone , dummySpan , ieRange ;
// Try to make a object selection, be careful with selecting phase element in IE
// will breaks the selection in non-framed environment.
var selected = range . getEnclosedNode ( ) ;
2018-06-17 16:07:19 +00:00
if ( selected && selected . type == CKEDITOR . NODE _ELEMENT && selected . getName ( ) in styleObjectElements &&
! ( selected . is ( 'a' ) && selected . getText ( ) ) ) {
2014-04-11 20:05:45 +00:00
try {
ieRange = selected . $ . createControlRange ( ) ;
ieRange . addElement ( selected . $ ) ;
ieRange . select ( ) ;
return ;
} catch ( er ) { }
}
// IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
// <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>...
2018-06-17 16:07:19 +00:00
if ( range . startContainer . type == CKEDITOR . NODE _ELEMENT && range . startContainer . getName ( ) in nonCells ||
range . endContainer . type == CKEDITOR . NODE _ELEMENT && range . endContainer . getName ( ) in nonCells ) {
2014-04-11 20:05:45 +00:00
range . shrink ( CKEDITOR . NODE _ELEMENT , true ) ;
2018-06-17 16:07:19 +00:00
// The range might get collapsed (https://dev.ckeditor.com/ticket/7975). Update cached variable.
collapsed = range . collapsed ;
}
2014-04-11 20:05:45 +00:00
var bookmark = range . createBookmark ( ) ;
// Create marker tags for the start and end boundaries.
var startNode = bookmark . startNode ;
var endNode ;
if ( ! collapsed )
endNode = bookmark . endNode ;
// Create the main range which will be used for the selection.
ieRange = range . document . $ . body . createTextRange ( ) ;
// Position the range at the start boundary.
ieRange . moveToElementText ( startNode . $ ) ;
ieRange . moveStart ( 'character' , 1 ) ;
if ( endNode ) {
// Create a tool range for the end.
var ieRangeEnd = range . document . $ . body . createTextRange ( ) ;
// Position the tool range at the end.
ieRangeEnd . moveToElementText ( endNode . $ ) ;
// Move the end boundary of the main range to match the tool range.
ieRange . setEndPoint ( 'EndToEnd' , ieRangeEnd ) ;
ieRange . moveEnd ( 'character' , - 1 ) ;
} else {
// The isStartMarkerAlone logic comes from V2. It guarantees that the lines
// will expand and that the cursor will be blinking on the right place.
// Actually, we are using this flag just to avoid using this hack in all
// situations, but just on those needed.
var next = startNode . getNext ( notWhitespaces ) ;
var inPre = startNode . hasAscendant ( 'pre' ) ;
2018-06-17 16:07:19 +00:00
isStartMarkerAlone = ( ! ( next && next . getText && next . getText ( ) . match ( fillerTextRegex ) ) && // already a filler there?
( inPre || ! startNode . hasPrevious ( ) || ( startNode . getPrevious ( ) . is && startNode . getPrevious ( ) . is ( 'br' ) ) ) ) ;
2014-04-11 20:05:45 +00:00
// Append a temporary <span></span> before the selection.
// This is needed to avoid IE destroying selections inside empty
2018-06-17 16:07:19 +00:00
// inline elements, like <b></b> (https://dev.ckeditor.com/ticket/253).
2014-04-11 20:05:45 +00:00
// It is also needed when placing the selection right after an inline
// element to avoid the selection moving inside of it.
dummySpan = range . document . createElement ( 'span' ) ;
2018-06-17 16:07:19 +00:00
dummySpan . setHtml ( '' ) ; // Zero Width No-Break Space (U+FEFF). See https://dev.ckeditor.com/ticket/1359.
2014-04-11 20:05:45 +00:00
dummySpan . insertBefore ( startNode ) ;
if ( isStartMarkerAlone ) {
// To expand empty blocks or line spaces after <br>, we need
// instead to have any char, which will be later deleted using the
// selection.
2018-06-17 16:07:19 +00:00
// \ufeff = Zero Width No-Break Space (U+FEFF). (https://dev.ckeditor.com/ticket/1359)
2014-04-11 20:05:45 +00:00
range . document . createText ( '\ufeff' ) . insertBefore ( startNode ) ;
}
}
// Remove the markers (reset the position, because of the changes in the DOM tree).
range . setStartBefore ( startNode ) ;
startNode . remove ( ) ;
if ( collapsed ) {
if ( isStartMarkerAlone ) {
// Move the selection start to include the temporary \ufeff.
ieRange . moveStart ( 'character' , - 1 ) ;
ieRange . select ( ) ;
// Remove our temporary stuff.
range . document . $ . selection . clear ( ) ;
2018-06-17 16:07:19 +00:00
} else {
2014-04-11 20:05:45 +00:00
ieRange . select ( ) ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
range . moveToPosition ( dummySpan , CKEDITOR . POSITION _BEFORE _START ) ;
dummySpan . remove ( ) ;
} else {
range . setEndBefore ( endNode ) ;
endNode . remove ( ) ;
ieRange . select ( ) ;
}
} else {
var sel = this . getNative ( ) ;
2018-06-17 16:07:19 +00:00
// getNative() returns null if iframe is "display:none" in FF. (https://dev.ckeditor.com/ticket/6577)
2014-04-11 20:05:45 +00:00
if ( ! sel )
return ;
this . removeAllRanges ( ) ;
for ( var i = 0 ; i < ranges . length ; i ++ ) {
// Joining sequential ranges introduced by
// readonly elements protection.
if ( i < ranges . length - 1 ) {
var left = ranges [ i ] ,
right = ranges [ i + 1 ] ,
between = left . clone ( ) ;
between . setStart ( left . endContainer , left . endOffset ) ;
between . setEnd ( right . startContainer , right . startOffset ) ;
2018-06-17 16:07:19 +00:00
// Don't confused by Firefox adjacent multi-ranges
2014-04-11 20:05:45 +00:00
// introduced by table cells selection.
if ( ! between . collapsed ) {
between . shrink ( CKEDITOR . NODE _ELEMENT , true ) ;
var ancestor = between . getCommonAncestor ( ) ,
enclosed = between . getEnclosedNode ( ) ;
// The following cases has to be considered:
// 1. <span contenteditable="false">[placeholder]</span>
2018-06-17 16:07:19 +00:00
// 2. <input contenteditable="false" type="radio"/> (https://dev.ckeditor.com/ticket/6621)
2014-04-11 20:05:45 +00:00
if ( ancestor . isReadOnly ( ) || enclosed && enclosed . isReadOnly ( ) ) {
right . setStart ( left . startContainer , left . startOffset ) ;
ranges . splice ( i -- , 1 ) ;
continue ;
}
}
}
range = ranges [ i ] ;
var nativeRange = this . document . $ . createRange ( ) ;
if ( range . collapsed && CKEDITOR . env . webkit && rangeRequiresFix ( range ) ) {
// Append a zero-width space so WebKit will not try to
2018-06-17 16:07:19 +00:00
// move the selection by itself (https://dev.ckeditor.com/ticket/1272).
var fillingChar = createFillingCharSequenceNode ( this . root ) ;
2014-04-11 20:05:45 +00:00
range . insertNode ( fillingChar ) ;
next = fillingChar . getNext ( ) ;
// If the filling char is followed by a <br>, whithout
// having something before it, it'll not blink.
// Let's remove it in this case.
if ( next && ! fillingChar . getPrevious ( ) && next . type == CKEDITOR . NODE _ELEMENT && next . getName ( ) == 'br' ) {
2018-06-17 16:07:19 +00:00
removeFillingCharSequenceNode ( this . root ) ;
2014-04-11 20:05:45 +00:00
range . moveToPosition ( next , CKEDITOR . POSITION _BEFORE _START ) ;
2018-06-17 16:07:19 +00:00
} else {
2014-04-11 20:05:45 +00:00
range . moveToPosition ( fillingChar , CKEDITOR . POSITION _AFTER _END ) ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
}
nativeRange . setStart ( range . startContainer . $ , range . startOffset ) ;
try {
nativeRange . setEnd ( range . endContainer . $ , range . endOffset ) ;
} catch ( e ) {
// There is a bug in Firefox implementation (it would be too easy
// otherwise). The new start can't be after the end (W3C says it can).
// So, let's create a new range and collapse it to the desired point.
if ( e . toString ( ) . indexOf ( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) {
range . collapse ( 1 ) ;
nativeRange . setEnd ( range . endContainer . $ , range . endOffset ) ;
2018-06-17 16:07:19 +00:00
} else {
2014-04-11 20:05:45 +00:00
throw e ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
}
// Select the range.
sel . addRange ( nativeRange ) ;
}
}
this . reset ( ) ;
// Fakes the IE DOM event "selectionchange" on editable.
this . root . fire ( 'selectionchange' ) ;
} ,
/ * *
* Makes a "fake selection" of an element .
*
* A fake selection does not render UI artifacts over the selected
* element . Additionally , the browser native selection system is not
* aware of the fake selection . In practice , the native selection is
* moved to a hidden place where no native selection UI artifacts are
* displayed to the user .
*
* @ param { CKEDITOR . dom . element } element The element to be "selected" .
2018-06-17 16:07:19 +00:00
* @ param { String } [ ariaLabel ] A string to be used by the screen reader to describe the selection .
2014-04-11 20:05:45 +00:00
* /
2018-06-17 16:07:19 +00:00
fake : function ( element , ariaLabel ) {
2014-04-11 20:05:45 +00:00
var editor = this . root . editor ;
2018-06-17 16:07:19 +00:00
// Attempt to retrieve aria-label if possible (https://dev.ckeditor.com/ticket/14539).
if ( ariaLabel === undefined && element . hasAttribute ( 'aria-label' ) ) {
ariaLabel = element . getAttribute ( 'aria-label' ) ;
}
2014-04-11 20:05:45 +00:00
// Cleanup after previous selection - e.g. remove hidden sel container.
this . reset ( ) ;
2018-06-17 16:07:19 +00:00
hideSelection ( editor , ariaLabel ) ;
2014-04-11 20:05:45 +00:00
// Set this value after executing hiseSelection, because it may
// cause reset() which overwrites cache.
var cache = this . _ . cache ;
// Caches a range than holds the element.
var range = new CKEDITOR . dom . range ( this . root ) ;
range . setStartBefore ( element ) ;
range . setEndAfter ( element ) ;
cache . ranges = new CKEDITOR . dom . rangeList ( range ) ;
// Put this element in the cache.
cache . selectedElement = cache . startElement = element ;
cache . type = CKEDITOR . SELECTION _ELEMENT ;
// Properties that will not be available when isFake.
cache . selectedText = cache . nativeSel = null ;
this . isFake = 1 ;
this . rev = nextRev ++ ;
// Save this selection, so it can be returned by editor.getSelection().
editor . _ . fakeSelection = this ;
// Fire selectionchange, just like a normal selection.
this . root . fire ( 'selectionchange' ) ;
} ,
/ * *
* Checks whether selection is placed in hidden element .
*
* This method is to be used to verify whether fake selection
* ( see { @ link # fake } ) is still hidden .
*
* * * Note : * * this method should be executed on real selection - e . g . :
*
* editor . getSelection ( true ) . isHidden ( ) ;
*
* @ returns { Boolean }
* /
isHidden : function ( ) {
var el = this . getCommonAncestor ( ) ;
if ( el && el . type == CKEDITOR . NODE _TEXT )
el = el . getParent ( ) ;
return ! ! ( el && el . data ( 'cke-hidden-sel' ) ) ;
} ,
2018-06-17 16:07:19 +00:00
/ * *
* Checks if the selection contains an HTML element inside a table .
* Returns ` false ` for text selection inside a table ( e . g . it will return ` false `
* for text selected in one cell ) .
*
* editor . getSelection ( ) . isInTable ( ) ;
*
* @ since 4.7 . 0
* @ param { Boolean } [ allowPartialSelection = false ] Whether a partial cell selection should be included .
* Added in 4.7 . 2.
* @ returns { Boolean }
* /
isInTable : function ( allowPartialSelection ) {
return isTableSelection ( this . getRanges ( ) , allowPartialSelection ) ;
} ,
/ * *
* Checks if the selection contains only one range which is collapsed .
*
* if ( editor . getSelection ( ) . isCollapsed ( ) ) {
* // Do something when the selection is collapsed.
* }
*
* @ since 4.7 . 3
* @ returns { Boolean }
* /
isCollapsed : function ( ) {
var ranges = this . getRanges ( ) ;
return ranges . length === 1 && ranges [ 0 ] . collapsed ;
} ,
2014-04-11 20:05:45 +00:00
/ * *
* Creates a bookmark for each range of this selection ( from { @ link # getRanges } )
* by calling the { @ link CKEDITOR . dom . range # createBookmark } method ,
* with extra care taken to avoid interference among those ranges . The arguments
* received are the same as with the underlying range method .
*
* var bookmarks = editor . getSelection ( ) . createBookmarks ( ) ;
*
* @ returns { Array } Array of bookmarks for each range .
* /
createBookmarks : function ( serializable ) {
var bookmark = this . getRanges ( ) . createBookmarks ( serializable ) ;
this . isFake && ( bookmark . isFake = 1 ) ;
return bookmark ;
} ,
/ * *
* Creates a bookmark for each range of this selection ( from { @ link # getRanges } )
* by calling the { @ link CKEDITOR . dom . range # createBookmark2 } method ,
* with extra care taken to avoid interference among those ranges . The arguments
* received are the same as with the underlying range method .
*
* var bookmarks = editor . getSelection ( ) . createBookmarks2 ( ) ;
*
* @ returns { Array } Array of bookmarks for each range .
* /
createBookmarks2 : function ( normalized ) {
var bookmark = this . getRanges ( ) . createBookmarks2 ( normalized ) ;
this . isFake && ( bookmark . isFake = 1 ) ;
return bookmark ;
} ,
/ * *
* Selects the virtual ranges denoted by the bookmarks by calling { @ link # selectRanges } .
*
* var bookmarks = editor . getSelection ( ) . createBookmarks ( ) ;
* editor . getSelection ( ) . selectBookmarks ( bookmarks ) ;
*
* @ param { Array } bookmarks The bookmarks representing ranges to be selected .
* @ returns { CKEDITOR . dom . selection } This selection object , after the ranges were selected .
* /
selectBookmarks : function ( bookmarks ) {
2018-06-17 16:07:19 +00:00
var ranges = [ ] ,
node ;
2014-04-11 20:05:45 +00:00
for ( var i = 0 ; i < bookmarks . length ; i ++ ) {
var range = new CKEDITOR . dom . range ( this . root ) ;
range . moveToBookmark ( bookmarks [ i ] ) ;
ranges . push ( range ) ;
}
2018-06-17 16:07:19 +00:00
// It may happen that the content change during loading, before selection is set so bookmark leads to text node.
if ( bookmarks . isFake ) {
node = isTableSelection ( ranges ) ? ranges [ 0 ] . _getTableElement ( ) : ranges [ 0 ] . getEnclosedNode ( ) ;
if ( ! node || node . type != CKEDITOR . NODE _ELEMENT ) {
CKEDITOR . warn ( 'selection-not-fake' ) ;
bookmarks . isFake = 0 ;
}
}
if ( bookmarks . isFake && ! isTableSelection ( ranges ) ) {
this . fake ( node ) ;
} else {
2014-04-11 20:05:45 +00:00
this . selectRanges ( ranges ) ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:05:45 +00:00
return this ;
} ,
/ * *
* Retrieves the common ancestor node of the first range and the last range .
*
* var ancestor = editor . getSelection ( ) . getCommonAncestor ( ) ;
*
* @ returns { CKEDITOR . dom . element } The common ancestor of the selection or ` null ` if selection is empty .
* /
getCommonAncestor : function ( ) {
var ranges = this . getRanges ( ) ;
if ( ! ranges . length )
return null ;
var startNode = ranges [ 0 ] . startContainer ,
endNode = ranges [ ranges . length - 1 ] . endContainer ;
return startNode . getCommonAncestor ( endNode ) ;
} ,
/ * *
* Moves the scrollbar to the starting position of the current selection .
*
* editor . getSelection ( ) . scrollIntoView ( ) ;
* /
scrollIntoView : function ( ) {
// Scrolls the first range into view.
if ( this . type != CKEDITOR . SELECTION _NONE )
this . getRanges ( ) [ 0 ] . scrollIntoView ( ) ;
} ,
/ * *
* Remove all the selection ranges from the document .
* /
removeAllRanges : function ( ) {
2018-06-17 16:07:19 +00:00
// Don't clear selection outside this selection's root (https://dev.ckeditor.com/ticket/11500).
2014-04-11 20:05:45 +00:00
if ( this . getType ( ) == CKEDITOR . SELECTION _NONE )
return ;
var nativ = this . getNative ( ) ;
2018-06-17 16:07:19 +00:00
try {
nativ && nativ [ isMSSelection ? 'empty' : 'removeAllRanges' ] ( ) ;
} catch ( er ) { }
2014-04-11 20:05:45 +00:00
this . reset ( ) ;
}
} ;
} ) ( ) ;
/ * *
* Fired when selection inside editor has been changed . Note that this event
* is fired only when selection ' s start element ( container of a selecion start )
* changes , not on every possible selection change . Thanks to that ` selectionChange `
* is fired less frequently , but on every context
* ( the { @ link CKEDITOR . editor # elementPath elements path } holding selection ' s start ) change .
*
* @ event selectionChange
* @ member CKEDITOR . editor
* @ param { CKEDITOR . editor } editor This editor instance .
* @ param data
* @ param { CKEDITOR . dom . selection } data . selection
* @ param { CKEDITOR . dom . elementPath } data . path
* /
/ * *
* Selection ' s revision . This value is incremented every time new
* selection is created or existing one is modified .
*
* @ since 4.3
* @ readonly
* @ property { Number } rev
* /
/ * *
* Document in which selection is anchored .
*
* @ readonly
* @ property { CKEDITOR . dom . document } document
* /
/ * *
* Selection ' s root element .
*
* @ readonly
* @ property { CKEDITOR . dom . element } root
* /
/ * *
* Whether selection is locked ( cannot be modified ) .
*
* See { @ link # lock } and { @ link # unlock } methods .
*
* @ readonly
* @ property { Boolean } isLocked
* /
/ * *
* Whether selection is a fake selection .
*
* See { @ link # fake } method .
*
* @ readonly
* @ property { Boolean } isFake
* /