diff options
Diffstat (limited to 'core/selection.js')
| -rw-r--r-- | core/selection.js | 1701 |
1 files changed, 1701 insertions, 0 deletions
diff --git a/core/selection.js b/core/selection.js new file mode 100644 index 0000000..09634a6 --- /dev/null +++ b/core/selection.js @@ -0,0 +1,1701 @@ +/** + * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.html or http://ckeditor.com/license + */ + +(function() { + // #### 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() { + // Editor may have no selection at all. + var sel = this.getSelection( 1 ); + if ( sel.getType() == CKEDITOR.SELECTION_NONE ) + return; + + this.fire( 'selectionCheck', sel ); + + var currentPath = this.elementPath(); + if ( !currentPath.compare( this._.selectionPreviousPath ) ) { + 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 ); + function rangeRequiresFix( range ) { + function isTextCt( node, isAtEnd ) { + 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; + + var previous = range.getPreviousNode( isVisible, null, ct ), + next = range.getNextNode( isVisible, null, ct ); + + // Any adjacent text container may absorb the cursor, e.g. + // <p><strong>text</strong>^foo</p> + // <p>foo^<strong>text</strong></p> + // <div>^<p>foo</p></div> + if ( isTextCt( previous ) || isTextCt( next, 1 ) ) + return true; + + // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222) + if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) ) + return true; + + return false; + } + + function createFillingChar( element ) { + removeFillingChar( element, false ); + + var fillingChar = element.getDocument().createText( '\u200B' ); + element.setCustomData( 'cke-fillingChar', fillingChar ); + + return fillingChar; + } + + function getFillingChar( element ) { + return element.getCustomData( 'cke-fillingChar' ); + } + + // Checks if a filling char has been used, eventualy removing it (#1272). + function checkFillingChar( element ) { + var fillingChar = getFillingChar( element ); + if ( fillingChar ) { + // Use this flag to avoid removing the filling char right after + // creating it. + if ( fillingChar.getCustomData( 'ready' ) ) + removeFillingChar( element ); + else + fillingChar.setCustomData( 'ready', 1 ); + } + } + + function removeFillingChar( element, keepSelection ) { + var fillingChar = element && element.removeCustomData( 'cke-fillingChar' ); + if ( fillingChar ) { + + // Text selection position might get mangled by + // subsequent dom modification, save it now for restoring. (#8617) + if ( keepSelection !== false ) + { + var bm, + doc = element.getDocument(), + sel = doc.getSelection().getNative(), + // Be error proof. + range = sel && sel.type != 'None' && sel.getRangeAt( 0 ); + + if ( fillingChar.getLength() > 1 && range && range.intersectsNode( fillingChar.$ ) ) { + bm = [ sel.anchorOffset, sel.focusOffset ]; + + // Anticipate the offset change brought by the removed char. + var startAffected = sel.anchorNode == fillingChar.$ && sel.anchorOffset > 0, + endAffected = sel.focusNode == fillingChar.$ && sel.focusOffset > 0; + startAffected && bm[ 0 ]--; + endAffected && bm[ 1 ]--; + + // Revert the bookmark order on reverse selection. + isReversedSelection( sel ) && bm.unshift( bm.pop() ); + } + } + + // 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. + fillingChar.setText( replaceFillingChar( fillingChar.getText() ) ); + + // Restore the bookmark. + if ( bm ) { + var rng = sel.getRangeAt( 0 ); + rng.setStart( rng.startContainer, bm[ 0 ] ); + rng.setEnd( rng.startContainer, bm[ 1 ] ); + sel.removeAllRanges(); + sel.addRange( rng ); + } + } + } + + function replaceFillingChar( html ) { + return html.replace( /\u200B( )?/g, function( match ) { + // #10291 if filling char is followed by a space replace it with nbsp. + return match[ 1 ] ? '\xa0' : ''; + } ); + } + + function isReversedSelection( sel ) { + if ( !sel.isCollapsed ) { + var range = sel.getRangeAt( 0 ); + // Potentially alter an reversed selection range. + range.setStart( sel.anchorNode, sel.anchorOffset ); + range.setEnd( sel.focusNode, sel.focusOffset ); + return range.collapsed; + } + } + + // Setup all editor instances for the necessary selection hooks. + CKEDITOR.on( 'instanceCreated', function( ev ) { + var editor = ev.editor; + + /** + * @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 + */ + editor.define( 'selectionChange', { errorProof:1 } ); + + 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; + + // Give the editable an initial selection on first focus, + // put selection at a consistent position at the start + // of the contents. (#9507) + 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 + // place (at the very start of the editable). #10119 + if ( 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, 'focus', function() { + editor.unlockSelection( restoreSel ); + restoreSel = 0; + }, null, null, -1 ); + + // Disable selection restoring when clicking in. + editable.attachListener( editable, 'mousedown', function() { + restoreSel = 0; + }); + + // 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. + if ( CKEDITOR.env.ie || CKEDITOR.env.opera || isInline ) { + var lastSel; + // Save a fresh copy of the selection. + function saveSel() { + lastSel = editor.getSelection( 1 ); + lastSel.lock(); + } + + // 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 ); + + editable.attachListener( editable, 'blur', function() { + editor.lockSelection( lastSel ); + restoreSel = 1; + }, null, null, -1 ); + } + + // The following selection related fixes applies to only framed 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 + // position and revert it before context menu opens. (#5778) + if ( evt.data.$.button == 2 ) { + var sel = editor.document.$.selection; + if ( sel.type == '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 + // the normal behavior on old IEs. (#1659, #7932) + if ( doc.$.compatMode != 'BackCompat' ) { + if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) { + function moveRangeToPoint( range, x, y ) { + // Error prune in IE7. (#9034, #9110) + try { range.moveToPoint( x, y ); } catch ( e ) {} + } + + 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(); + + moveRangeToPoint( rngEnd, evt.x, evt.y ); + + // 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(); + + // Make it in effect on mouse up. (#9022) + textRng.select(); + } + + + // We're sure that the click happens at the region + // below body, but not on scrollbar. + if ( evt.getTarget().is( 'html' ) && + evt.$.y < html.$.clientHeight && + evt.$.x < html.$.clientWidth ) { + // Start to build the text range. + var textRng = body.$.createTextRange(); + moveRangeToPoint( textRng, evt.$.x, evt.$.y ); + + // Records the dragging start of the above text range. + var startRng = textRng.duplicate(); + + 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. + if ( CKEDITOR.env.version > 7 ) { + html.on( 'mousedown', function( evt ) { + if ( evt.data.getTarget().is( 'html' ) ) { + // Limit the text selection mouse move inside of editable. (#9715) + outerDoc.on( 'mouseup', onSelectEnd ); + html.on( 'mouseup', onSelectEnd ); + } + + }); + + 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. + if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ ) + range.select(); + } + } + } + } + + // 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 ); + // Always fire the selection change on focus gain. + editable.attachListener( editable, 'focus', function() { + editor.forceNextSelectionCheck(); + editor.selectionChange( 1 ); + }); + + // #9699: On Webkit&Gecko in inline editor and on Opera in framed editor we have to check selection + // when it was changed by dragging and releasing mouse button outside editable. Dragging (mousedown) + // has to be initialized in editable, but for mouseup we listen on document element. + // On Opera, listening on document element, helps even if mouse button is released outside iframe. + if ( isInline ? ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) : CKEDITOR.env.opera ) { + var mouseDown; + editable.attachListener( editable, 'mousedown', function() { + mouseDown = 1; + }); + editable.attachListener( doc.getDocumentElement(), 'mouseup', function() { + if ( mouseDown ) + checkSelectionChangeTimeout.call( editor ); + mouseDown = 0; + }); + } + // In all other cases listen on simple mouseup over editable, as we did before #9699. + // + // 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 + // of the editor (#9851). + else + editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor ); + + 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 + removeFillingChar( editable ); + } + + }, null, null, -1 ); + } + }); + + // Clear the cached range path before unload. (#7174) + editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor ); + // Check selection change on data reload. + editor.on( 'dataReady', function() { + editor.selectionChange( 1 ); + }); + + function clearSelection() { + var sel = editor.getSelection(); + sel && sel.removeAllRanges(); + } + + // Clear dom selection before editable destroying to fix some browser + // craziness. + + // IE9 might cease to work if there's an object selection inside the iframe (#7639). + CKEDITOR.env.ie9Compat && editor.on( 'beforeDestroy', clearSelection, null, null, 9 ); + // Webkit's selection will mess up after the data loading. + CKEDITOR.env.webkit && editor.on( 'setData', clearSelection ); + + // Invalidate locked selection when unloading DOM (e.g. after setData). (#9521) + editor.on( 'contentDomUnload', function() { + editor.unlockSelection(); + }); + + }); + + CKEDITOR.on( 'instanceReady', function( evt ) { + var editor = evt.editor; + + // On WebKit only, we need a special "filling" char on some situations + // (#1272). Here we set the events that should invalidate that char. + if ( CKEDITOR.env.webkit ) { + editor.on( 'selectionChange', function() { + checkFillingChar( editor.editable() ); + }, null, null, -1 ); + editor.on( 'beforeSetMode', function() { + removeFillingChar( editor.editable() ); + }, null, null, -1 ); + + var fillingCharBefore, resetSelection; + + function beforeData() { + var editable = editor.editable(); + if ( !editable ) + return; + + var fillingChar = getFillingChar( editable ); + + if ( fillingChar ) { + // If cursor is right blinking by side of the filler node, save it for restoring, + // as the following text substitution will blind it. (#7437) + var sel = editor.document.$.defaultView.getSelection(); + if ( sel.type == 'Caret' && sel.anchorNode == fillingChar.$ ) + resetSelection = 1; + + fillingCharBefore = fillingChar.getText(); + fillingChar.setText( replaceFillingChar( fillingCharBefore ) ); + } + } + + function afterData() { + var editable = editor.editable(); + if ( !editable ) + return; + + var fillingChar = getFillingChar( editable ); + + if ( fillingChar ) { + fillingChar.setText( fillingCharBefore ); + + if ( resetSelection ) { + editor.document.$.defaultView.getSelection().setPosition( fillingChar.$, fillingChar.getLength() ); + resetSelection = 0; + } + } + } + + editor.on( 'beforeUndoImage', beforeData ); + editor.on( 'afterUndoImage', afterData ); + editor.on( 'beforeGetData', beforeData, null, null, 0 ); + editor.on( 'getData', afterData ); + } + }); + + /** + * 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 + * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment. + * @todo param + */ + CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) { + // Check if there exists a locked selection. + if ( this._.savedSelection && !forceRealSelection ) + return this._.savedSelection; + + // Editable element might be absent. + var editable = this.editable(); + return editable ? 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 + * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection + * 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; + + var isMSSelection = typeof window.getSelection != 'function'; + + /** + * Manipulates the selection within a DOM element, if the current browser selection + * spans outside of the element, an empty selection object is returned. + * + * var sel = new CKEDITOR.dom.selection( CKEDITOR.document ); + * + * @class + * @constructor Creates a selection class instance. + * @param {CKEDITOR.dom.document} target The DOM document/element that the DOM selection + * is restrained to, only selection spans within the target element is considered as valid. + */ + CKEDITOR.dom.selection = function( target ) { + var isElement = target instanceof CKEDITOR.dom.element; + this.document = target instanceof CKEDITOR.dom.document ? target : target.getDocument(); + this.root = isElement ? target : this.document.getBody(); + this.isLocked = 0; + this._ = { + cache: {} + }; + + // On WebKit, it may happen that we've already have focus + // on the editable element while still having no selection + // available. We normalize it here by replicating the + // behavior of other browsers. + if ( CKEDITOR.env.webkit ) { + var sel = this.document.getWindow().$.getSelection(); + if ( sel.type == 'None' && this.document.getActive().equals( this.root ) || sel.type == 'Caret' && sel.anchorNode.nodeType == CKEDITOR.NODE_DOCUMENT ) { + var range = new CKEDITOR.dom.range( this.root ); + range.moveToPosition( this.root, CKEDITOR.POSITION_AFTER_START ); + var nativeRange = this.document.$.createRange(); + nativeRange.setStart( range.startContainer.$, range.startOffset ); + nativeRange.collapse( 1 ); + + // It may happen that setting proper selection will + // cause focus to be fired. Cancel it because focus + // shouldn't be fired when retriving selection. (#10115) + var listener = this.root.on( 'focus', function( evt ) { + evt.cancel(); + }, null, null, -100 ); + sel.addRange( nativeRange ); + listener.removeListener(); + } + } + + // Check whether browser focus is really inside of the editable element. + + var nativeSel = this.getNative(), + rangeParent; + + 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. + if ( !( rangeParent && ( 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 }; + + CKEDITOR.dom.selection.prototype = { + /** + * Gets the native selection object from the browser. + * + * var selection = editor.getSelection().getNative(); + * + * @returns {Object} The native browser selection object. + */ + 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; + + if ( startContainer == range.endContainer && startContainer.nodeType == 1 && ( range.endOffset - range.startOffset ) == 1 && styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) { + type = CKEDITOR.SELECTION_ELEMENT; + } + } + + 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 + * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only. + * @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 ) { + // 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(), + doc = parent.ownerDocument; + + // 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 childs 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 { + // IE9 report wrong measurement with compareEndPoints when range anchors between two BRs. + // e.g. <p>text<br />^<br /></p> (#7433) + if ( CKEDITOR.env.ie9Compat && child.tagName == 'BR' ) { + // "Fall back" to w3c selection. + var sel = doc.defaultView.getSelection(); + return { + container: sel[ start ? 'anchorNode' : 'focusNode' ], + offset: sel[ start ? 'anchorOffset' : 'focusOffset' ] }; + } else + return { container: parent, offset: getNodeIndex( child ) }; + } + } + + // All childs are text nodes, + // or to the right hand of test range are all text nodes. (#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 ); + + // IE report line break as CRLF with range.text but + // only LF with textnode.nodeValue, normalize them to avoid + // breaking character counting logic below. (#3949) + distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; + + siblings = parent.childNodes; + + // Actual range anchor right beside test range at the boundary of text node. + if ( !distance ) { + child = siblings[ siblings.length - 1 ]; + + 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; + } + } + + 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. (#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' ]; + if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) { + distance -= sibling.nodeValue.length; + container = sibling; + } + child = sibling; + } + // Measurement in IE could be somtimes wrong because of <select> element. (#4611) + catch ( e ) { + return { container: parent, offset: getNodeIndex( child ) }; + } + } + + return { container: container, offset: position > 0 ? -distance : container.nodeValue.length + distance }; + } + }; + + 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 ); + + // Correct an invalid IE range case on empty list item. (#5850) + 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++ ) { + /*jsl:pass*/ + } + + range.setStart( new CKEDITOR.dom.node( parentElement ), j ); + range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 ); + retval.push( range ); + } + + return retval; + } + + return []; + }; + })() : function() { + + // On browsers implementing the W3C range, we simply + // tranform the native ranges in CKEDITOR.dom.range + // instances. + + var ranges = [], + range, + sel = this.getNative(); + + if ( !sel ) + return ranges; + + for ( var i = 0; i < sel.rangeCount; i++ ) { + var nativeRange = sel.getRangeAt( i ); + + range = new CKEDITOR.dom.range( this.root ); + + 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; + }; + + return function( onlyEditables ) { + var cache = this._.cache; + if ( cache.ranges && !onlyEditables ) + return cache.ranges; + else if ( !cache.ranges ) + cache.ranges = new CKEDITOR.dom.rangeList( func.call( this ) ); + + // Split range into multiple by read-only nodes. + if ( onlyEditables ) { + var ranges = cache.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 cache.ranges; + }; + })(), + + /** + * 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 + // visual impact. ( #3231 ) + while ( 1 ) { + var startContainer = range.startContainer, + startOffset = range.startOffset; + // Limit the fix only to non-block elements.(#3950) + 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() ); + * + * @returns {CKEDITOR.dom.element} The selected element. Null if no + * 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 ], + 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.$; + }); + + 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(), + ranges = !selectedElement && this.getRanges(); + } + + 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; + + if ( selectedElement ) + this.selectElement( selectedElement ); + else + this.selectRanges( ranges ); + } + }, + + /** + * Clears the selection cache. + * + * editor.getSelection().reset(); + */ + reset: function() { + this._.cache = {}; + }, + + /** + * 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 ) { + 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. + !focused.equals( this.root ) && focused.focus(); + return; + } + + 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(); + if ( selected && selected.type == CKEDITOR.NODE_ELEMENT && selected.getName() in styleObjectElements && !( selected.is( 'a' ) && selected.getText() ) ) { + 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>... + if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells || range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) { + range.shrink( CKEDITOR.NODE_ELEMENT, true ); + } + + 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' ); + isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) // already a filler there? + && ( inPre || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) ); + + // Append a temporary <span></span> before the selection. + // This is needed to avoid IE destroying selections inside empty + // inline elements, like <b></b> (#253). + // 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' ); + dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. + 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. + // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) + 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(); + } else + ieRange.select(); + + range.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START ); + dummySpan.remove(); + } else { + range.setEndBefore( endNode ); + endNode.remove(); + ieRange.select(); + } + } else { + var sel = this.getNative(); + + // getNative() returns null if iframe is "display:none" in FF. (#6577) + if ( !sel ) + return; + + // Opera: The above hack work around a *visually wrong* text selection that + // happens in certain situation. (#6874, #9447) + if ( CKEDITOR.env.opera ) { + var nativeRng = this.document.$.createRange(); + nativeRng.selectNodeContents( this.root.$ ); + sel.addRange( nativeRng ); + } + + 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 ); + + // Don't confused by Firefox adjancent multi-ranges + // 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> + // 2. <input contenteditable="false" type="radio"/> (#6621) + 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(); + var startContainer = range.startContainer; + + // In Opera, we have some cases when a collapsed text selection cursor will be moved out of the + // anchor node: + // 1. Inside of any empty inline. (#4657) + // 2. In adjacent to any inline element. + if ( CKEDITOR.env.opera && range.collapsed && startContainer.type == CKEDITOR.NODE_ELEMENT ) { + + var leftSib = startContainer.getChild( range.startOffset - 1 ), + rightSib = startContainer.getChild( range.startOffset ); + + if ( !leftSib && !rightSib && startContainer.is( CKEDITOR.dtd.$removeEmpty ) || + leftSib && leftSib.type == CKEDITOR.NODE_ELEMENT || + rightSib && rightSib.type == CKEDITOR.NODE_ELEMENT ) { + range.insertNode( this.document.createText( '' ) ); + range.collapse( 1 ); + } + } + + if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) { + // Append a zero-width space so WebKit will not try to + // move the selection by itself (#1272). + var fillingChar = createFillingChar( this.root ); + 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' ) { + removeFillingChar( this.root ); + range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START ); + } else + range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END ); + } + + 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 ); + } else + throw e; + } + + // Select the range. + sel.addRange( nativeRange ); + } + } + + this.reset(); + + // Fakes the IE DOM event "selectionchange" on editable. + this.root.fire( 'selectionchange' ); + }, + + /** + * 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 ) { + return this.getRanges().createBookmarks( serializable ); + }, + + /** + * 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 ) { + return this.getRanges().createBookmarks2( normalized ); + }, + + /** + * 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 ) { + var ranges = []; + for ( var i = 0; i < bookmarks.length; i++ ) { + var range = new CKEDITOR.dom.range( this.root ); + range.moveToBookmark( bookmarks[ i ] ); + ranges.push( range ); + } + this.selectRanges( ranges ); + 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. + */ + getCommonAncestor: function() { + var ranges = this.getRanges(), + 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() { + var nativ = this.getNative(); + + try { nativ && nativ[ isMSSelection ? 'empty' : 'removeAllRanges' ](); } + catch(er){} + + this.reset(); + } + }; + +})(); |
