X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=_source%2Fplugins%2Fselection%2Fplugin.js;h=e192e5e1cf9cf1a16defdf4f8ebd528be43d7cba;hb=3fe9cac293e090ea459a3ee10d78cbe9e1dd0e03;hp=e7c24b7587b81ea5c6c9862a1827a3c75eaa4af2;hpb=ea7e3453c7b0f023b050aca6d9f83ab372860d91;p=ckeditor.git diff --git a/_source/plugins/selection/plugin.js b/_source/plugins/selection/plugin.js index e7c24b7..e192e5e 100644 --- a/_source/plugins/selection/plugin.js +++ b/_source/plugins/selection/plugin.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. +Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.html or http://ckeditor.com/license */ @@ -17,7 +17,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license // In IE, the "selectionchange" event may still get thrown when // releasing the WYSIWYG mode, so we need to check it first. var sel = this.getSelection(); - if ( !sel ) + if ( !sel || !sel.document.getWindow().$ ) return; var firstElement = sel.getStartElement(); @@ -70,29 +70,211 @@ For licensing, see LICENSE.html or http://ckeditor.com/license // #### checkSelectionChange : END + 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 ); + } + + 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. + //

text^foo

+ //

foo^text

+ //
^

foo

+ if ( isTextCt( previous ) || isTextCt( next, 1 ) ) + return true; + + // Empty block/inline element is also affected. ^,

^

(#7222) + if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) ) + return true; + + return false; + } + var selectAllCmd = { + modes : { wysiwyg : 1, source : 1 }, + readOnly : CKEDITOR.env.ie || CKEDITOR.env.webkit, exec : function( editor ) { switch ( editor.mode ) { case 'wysiwyg' : editor.document.$.execCommand( 'SelectAll', false, null ); + // Force triggering selectionChange (#7008) + editor.forceNextSelectionCheck(); + editor.selectionChange(); break; case 'source' : - // TODO + // Select the contents of the textarea + var textarea = editor.textarea.$; + if ( CKEDITOR.env.ie ) + textarea.createTextRange().execCommand( 'SelectAll' ); + else + { + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + } + textarea.focus(); } }, canUndo : false }; + function createFillingChar( doc ) + { + removeFillingChar( doc ); + + var fillingChar = doc.createText( '\u200B' ); + doc.setCustomData( 'cke-fillingChar', fillingChar ); + + return fillingChar; + } + + function getFillingChar( doc ) + { + return doc && doc.getCustomData( 'cke-fillingChar' ); + } + + // Checks if a filling char has been used, eventualy removing it (#1272). + function checkFillingChar( doc ) + { + var fillingChar = doc && getFillingChar( doc ); + if ( fillingChar ) + { + // Use this flag to avoid removing the filling char right after + // creating it. + if ( fillingChar.getCustomData( 'ready' ) ) + removeFillingChar( doc ); + else + fillingChar.setCustomData( 'ready', 1 ); + } + } + + function removeFillingChar( doc ) + { + var fillingChar = doc && doc.removeCustomData( 'cke-fillingChar' ); + if ( fillingChar ) + { + var bm, + sel = doc.getSelection().getNative(), + // Be error proof. + range = sel && sel.type != 'None' && sel.getRangeAt( 0 ); + + // Text selection position might get mangled by + // subsequent dom modification, save it now for restoring. (#8617) + 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( fillingChar.getText().replace( /\u200B/g, '' ) ); + + // 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 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; + } + } + CKEDITOR.plugins.add( 'selection', { init : function( 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.document ); } ); + editor.on( 'beforeSetMode', function() { removeFillingChar( editor.document ); } ); + + var fillingCharBefore, + resetSelection; + + function beforeData() + { + var doc = editor.document, + fillingChar = getFillingChar( doc ); + + 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 = doc.$.defaultView.getSelection(); + if ( sel.type == 'Caret' && sel.anchorNode == fillingChar.$ ) + resetSelection = 1; + + fillingCharBefore = fillingChar.getText(); + fillingChar.setText( fillingCharBefore.replace( /\u200B/g, '' ) ); + } + } + function afterData() + { + var doc = editor.document, + fillingChar = getFillingChar( doc ); + + if ( fillingChar ) + { + fillingChar.setText( fillingCharBefore ); + + if ( resetSelection ) + { + doc.$.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 ); + } + editor.on( 'contentDom', function() { - var doc = editor.document; + var doc = editor.document, + outerDoc = CKEDITOR.document, + body = doc.getBody(), + html = doc.getDocumentElement(); if ( CKEDITOR.env.ie ) { @@ -102,59 +284,119 @@ For licensing, see LICENSE.html or http://ckeditor.com/license // than firing the selection change event. var savedRange, - saveEnabled; + saveEnabled, + restoreEnabled = 1; // "onfocusin" is fired before "onfocus". It makes it // possible to restore the selection before click // events get executed. - doc.on( 'focusin', function() + body.on( 'focusin', function( evt ) { - // If we have saved a range, restore it at this + // If there are elements with layout they fire this event but + // it must be ignored to allow edit its contents #4682 + if ( evt.data.$.srcElement.nodeName != 'BODY' ) + return; + + // Give the priority to locked selection since it probably + // reflects the actual situation, besides locked selection + // could be interfered because of text nodes normalizing. + // (#6083, #6987) + var lockedSelection = doc.getCustomData( 'cke_locked_selection' ); + if ( lockedSelection ) + { + lockedSelection.unlock( 1 ); + lockedSelection.lock(); + } + // Then check ff we have saved a range, restore it at this // point. - if ( savedRange ) + else if ( savedRange && restoreEnabled ) { // Well not break because of this. - try - { - savedRange.select(); - } - catch (e) - {} - + try { savedRange.select(); } catch (e) {} savedRange = null; } }); - editor.window.on( 'focus', function() + body.on( 'focus', function() { // Enable selections to be saved. - saveEnabled = true; + saveEnabled = 1; saveSelection(); }); - // Check document selection before 'blur' fired, this - // will prevent us from breaking text selection somewhere - // else on the host page.(#3909) - editor.document.on( 'beforedeactivate', function() + body.on( 'beforedeactivate', function( evt ) { - // Disable selections from being saved. - saveEnabled = false; + // Ignore this event if it's caused by focus switch between + // internal editable control type elements, e.g. layouted paragraph. (#4682) + if ( evt.data.$.toElement ) + return; - // IE may leave the selection still inside the - // document. Let's force it to be removed. - // TODO: The following has effect for - // collapsed selections. - editor.document.$.execCommand( 'Unselect' ); + // Disable selections from being saved. + saveEnabled = 0; + restoreEnabled = 1; }); + // [IE] Iframe will still keep the selection when blurred, if + // focus is moved onto a non-editing host, e.g. link or button, but + // it becomes a problem for the object type selection, since the resizer + // handler attached on it will mark other part of the UI, especially + // for the dialog. (#8157) + // [IE<8] Even worse For old IEs, the cursor will not vanish even if + // the selection has been moved to another text input in some cases. (#4716) + // + // Now the range restore is disabled, so we simply force IE to clean + // up the selection before blur. + CKEDITOR.env.ie && editor.on( 'blur', function() + { + // Error proof when the editor is not visible. (#6375) + try{ doc.$.selection.empty(); } catch ( er){} + }); + + // Listening on document element ensures that + // scrollbar is included. (#5280) + html.on( 'mousedown', function() + { + // Lock restore selection now, as we have + // a followed 'click' event which introduce + // new selection. (#5735) + restoreEnabled = 0; + }); + + html.on( 'mouseup', function() + { + restoreEnabled = 1; + }); + + var scroll; // IE fires the "selectionchange" event when clicking // inside a selection. We don't want to capture that. - doc.on( 'mousedown', disableSave ); - doc.on( 'mouseup', - function() + body.on( '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 ) { - saveEnabled = true; + var sel = editor.document.$.selection; + if ( sel.type == 'None' ) + scroll = editor.window.getScrollPosition(); + } + disableSave(); + }); + + body.on( '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; + + saveEnabled = 1; setTimeout( function() { saveSelection( true ); @@ -162,22 +404,132 @@ For licensing, see LICENSE.html or http://ckeditor.com/license 0 ); }); - doc.on( 'keydown', disableSave ); - doc.on( 'keyup', + body.on( 'keydown', disableSave ); + body.on( 'keyup', function() { - saveEnabled = true; + saveEnabled = 1; saveSelection(); }); + // When content doc is in standards mode, IE doesn't produce text selection + // when click on the region outside of body, we emulate + // the correct behavior here. (#1659, #7932, # 9097) + 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 ) + { + // 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(); + } + + evt = evt.data; + + // We're sure that the click happens at the region + // outside body, but not on scrollbar. + if ( evt.getTarget().is( 'html' ) && + evt.$.x < html.$.clientWidth && + evt.$.y < html.$.clientHeight ) + { + // 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 IE > 8, we just need to reselect the reported range. + if ( CKEDITOR.env.ie8 ) + { + 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 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(); + } + } + + } // IE is the only to provide the "selectionchange" // event. doc.on( 'selectionchange', saveSelection ); function disableSave() { - saveEnabled = false; + saveEnabled = 0; } function saveSelection( testIt ) @@ -185,7 +537,8 @@ For licensing, see LICENSE.html or http://ckeditor.com/license if ( saveEnabled ) { var doc = editor.document, - sel = doc && doc.$.selection; + sel = editor.getSelection(), + nativeSel = sel && sel.getNative(); // There is a very specific case, when clicking // inside a text selection. In that case, the @@ -195,7 +548,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license // range at the very start of the document. In // such situation we have to test the range, to // be sure it's valid. - if ( testIt && sel && sel.type == 'None' ) + if ( testIt && nativeSel && nativeSel.type == 'None' ) { // The "InsertImage" command can be used to // test whether the selection is good or not. @@ -208,7 +561,19 @@ For licensing, see LICENSE.html or http://ckeditor.com/license } } - savedRange = sel && sel.createRange(); + // Avoid saving selection from within text input. (#5747) + var parentTag; + if ( nativeSel && nativeSel.type && nativeSel.type != 'Control' + && ( parentTag = nativeSel.createRange() ) + && ( parentTag = parentTag.parentElement() ) + && ( parentTag = parentTag.nodeName ) + && parentTag.toLowerCase() in { input: 1, textarea : 1 } ) + { + return; + } + + // Not break because of this. (#9132) + try{ savedRange = nativeSel && sel.getRanges()[ 0 ]; } catch( er ) {} checkSelectionChangeTimeout.call( editor ); } @@ -222,9 +587,39 @@ For licensing, see LICENSE.html or http://ckeditor.com/license doc.on( 'mouseup', checkSelectionChangeTimeout, editor ); doc.on( 'keyup', checkSelectionChangeTimeout, editor ); + doc.on( 'selectionchange', checkSelectionChangeTimeout, editor ); + } + + if ( CKEDITOR.env.webkit ) + { + // Before keystroke is handled by editor, check to remove the filling char. + doc.on( '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( editor.document ); + } + + }, null, null, -1 ); } }); + // Clear the cached range path before unload. (#7174) + editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor ); + editor.addCommand( 'selectAll', selectAllCmd ); editor.ui.addButton( 'SelectAll', { @@ -232,16 +627,32 @@ For licensing, see LICENSE.html or http://ckeditor.com/license command : 'selectAll' }); - editor.selectionChange = checkSelectionChangeTimeout; + /** + * Check if to fire the {@link CKEDITOR.editor#selectionChange} event + * for the current editor instance. + * + * @param {Boolean} checkNow Check immediately without any delay. + */ + editor.selectionChange = function( checkNow ) + { + ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this ); + }; + + // IE9 might cease to work if there's an object selection inside the iframe (#7639). + CKEDITOR.env.ie9Compat && editor.on( 'destroy', function() + { + var sel = editor.getSelection(); + sel && sel.getNative().clear(); + }, null, null, 9 ); } }); /** * Gets the current selection from the editing area when in WYSIWYG mode. - * @returns {CKEDITOR.dom.selection} A selection object or null if not on + * @returns {CKEDITOR.dom.selection} A selection object or null if not in * WYSIWYG mode or no selection is available. * @example - * var selection = CKEDITOR.instances.editor1.getSelection(); + * var selection = CKEDITOR.instances.editor1.getSelection(); * alert( selection.getType() ); */ CKEDITOR.editor.prototype.getSelection = function() @@ -258,7 +669,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license * Gets the current selection from the document. * @returns {CKEDITOR.dom.selection} A selection object. * @example - * var selection = CKEDITOR.instances.editor1.document.getSelection(); + * var selection = CKEDITOR.instances.editor1.document.getSelection(); * alert( selection.getType() ); */ CKEDITOR.dom.document.prototype.getSelection = function() @@ -277,11 +688,11 @@ For licensing, see LICENSE.html or http://ckeditor.com/license CKEDITOR.SELECTION_NONE = 1; /** - * Text or collapsed selection. + * A text or a collapsed selection. * @constant * @example * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) - * alert( 'Text is selected' ); + * alert( 'A text is selected' ); */ CKEDITOR.SELECTION_TEXT = 2; @@ -297,7 +708,9 @@ For licensing, see LICENSE.html or http://ckeditor.com/license /** * Manipulates the selection in a DOM document. * @constructor + * @param {CKEDITOR.dom.document} document The DOM document that contains the selection. * @example + * var sel = new CKEDITOR.dom.selection( CKEDITOR.document ); */ CKEDITOR.dom.selection = function( document ) { @@ -307,7 +720,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license return lockedSelection; this.document = document; - this.isLocked = false; + this.isLocked = 0; this._ = { cache : {} @@ -315,14 +728,22 @@ For licensing, see LICENSE.html or http://ckeditor.com/license /** * IE BUG: The selection's document may be a different document than the - * editor document. Return null if that's the case. + * editor document. Return null if that is the case. */ if ( CKEDITOR.env.ie ) { - var range = this.getNative().createRange(); - if ( !range - || ( range.item && range.item(0).ownerDocument != this.document.$ ) - || ( range.parentElement && range.parentElement().ownerDocument != this.document.$ ) ) + // Avoid breaking because of it. (#8836) + try + { + var range = this.getNative().createRange(); + if ( !range || + ( range.item && range.item( 0 ).ownerDocument != this.document.$ ) || + ( range.parentElement && range.parentElement().ownerDocument != this.document.$ ) ) + { + throw 0; + } + } + catch ( e ) { this.isInvalid = true; } @@ -332,19 +753,19 @@ For licensing, see LICENSE.html or http://ckeditor.com/license }; var styleObjectElements = - { - img:1,hr:1,li:1,table:1,tr:1,td:1,embed:1,object:1,ol:1,ul:1, - a:1, input:1, form:1, select:1, textarea:1, button:1, fieldset:1, th:1, thead:1, tfoot:1 - }; + { + 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. * @function - * @returns {Object} The native selection object. + * @returns {Object} The native browser selection object. * @example - * var selection = editor.getSelection().getNative(); + * var selection = editor.getSelection().getNative(); */ getNative : CKEDITOR.env.ie ? @@ -362,19 +783,19 @@ For licensing, see LICENSE.html or http://ckeditor.com/license * Gets the type of the current selection. The following values are * available: * * @function * @returns {Number} One of the following constant values: - * {@link CKEDITOR.SELECTION_NONE}, {@link CKEDITOR.SELECTION_TEXT} or - * {@link CKEDITOR.SELECTION_ELEMENT}. + * {@link CKEDITOR.SELECTION_NONE}, {@link CKEDITOR.SELECTION_TEXT}, or + * {@link CKEDITOR.SELECTION_ELEMENT}. * @example - * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) - * alert( 'Text is selected' ); + * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) + * alert( 'A text is selected' ); */ getType : CKEDITOR.env.ie ? @@ -441,10 +862,24 @@ For licensing, see LICENSE.html or http://ckeditor.com/license return ( cache.type = type ); }, - getRanges : - CKEDITOR.env.ie ? + /** + * 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. + * @function + * @param {Boolean} [onlyEditables] If set to true, this function retrives editable ranges only. + * @return {Array} Range instances that represent the current selection. + * @example + * var ranges = selection.getRanges(); + * alert( ranges.length ); + */ + getRanges : (function() + { + var func = CKEDITOR.env.ie ? ( 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 ) @@ -454,73 +889,136 @@ For licensing, see LICENSE.html or http://ckeditor.com/license range.collapse( start ); // Gets the element that encloses the range entirely. - var parent = range.parentElement(); - var siblings = parent.childNodes; - - var testRange; - - for ( var i = 0 ; i < siblings.length ; i++ ) + var parent = range.parentElement(), + doc = parent.ownerDocument; + + // Empty parent element, e.g. ^ + 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 ) { - var child = siblings[ i ]; - if ( child.nodeType == 1 ) + 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 { - testRange = range.duplicate(); - - testRange.moveToElementText( child ); - testRange.collapse(); - - var comparison = testRange.compareEndPoints( 'StartToStart', range ); - - if ( comparison > 0 ) - break; - else if ( comparison === 0 ) - return { - container : parent, - offset : i - }; - - testRange = null; + // IE9 report wrong measurement with compareEndPoints when range anchors between two BRs. + // e.g.

text
^

(#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 ) }; } } - if ( !testRange ) + // 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 ) { - testRange = range.duplicate(); + // Adapt test range to embrace the entire parent contents. testRange.moveToElementText( parent ); - testRange.collapse( false ); - } + testRange.setEndPoint( 'StartToStart', range ); - 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) - var distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; + // 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; - while ( distance > 0 ) - distance -= siblings[ --i ].nodeValue.length; + siblings = parent.childNodes; - if ( distance === 0 ) - { - return { - container : parent, - offset : i - }; + // 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 { - return { - container : siblings[ i ], - offset : -distance - }; + // 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 (#6621) + if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) + { + right.setStart( left.startContainer, left.startOffset ); + ranges.splice( i--, 1 ); + continue; + } + } + } + var range = ranges[ i ]; var nativeRange = this.document.$.createRange(); var startContainer = range.startContainer; @@ -866,68 +1631,105 @@ For licensing, see LICENSE.html or http://ckeditor.com/license // In FF2, if we have a collapsed range, inside an empty // element, we must add something to it otherwise the caret // will not be visible. + // In Opera instead, the selection will be moved out of the + // element. (#4657) if ( range.collapsed && - ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) && + ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) ) && startContainer.type == CKEDITOR.NODE_ELEMENT && !startContainer.getChildCount() ) { startContainer.appendText( '' ); } - nativeRange.setStart( startContainer.$, range.startOffset ); - nativeRange.setEnd( range.endContainer.$, range.endOffset ); + 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.document ); + range.insertNode( fillingChar ) ; + + var next = fillingChar.getNext(); + + // If the filling char is followed by a
, 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.document ); + 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 ); } + // Don't miss selection change event for non-IEs. + this.document.fire( 'selectionchange' ); this.reset(); } }, + /** + * Creates a bookmark for each range of this selection (from #getRanges) + * by calling the {@link CKEDITOR.dom.range.prototype.createBookmark} method, + * with extra care taken to avoid interference among those ranges. The arguments + * received are the same as with the underlying range method. + * @returns {Array} Array of bookmarks for each range. + * @example + * var bookmarks = editor.getSelection().createBookmarks(); + */ createBookmarks : function( serializable ) { - var retval = [], - ranges = this.getRanges(), - length = ranges.length, - bookmark; - for ( var i = 0; i < length ; i++ ) - { - retval.push( bookmark = ranges[ i ].createBookmark( serializable, true ) ); - - serializable = bookmark.serializable; - - var bookmarkStart = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode, - bookmarkEnd = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode; - - // Updating the offset values for rest of ranges which have been mangled(#3256). - for ( var j = i + 1 ; j < length ; j++ ) - { - var dirtyRange = ranges[ j ], - rangeStart = dirtyRange.startContainer, - rangeEnd = dirtyRange.endContainer; - - rangeStart.equals( bookmarkStart.getParent() ) && dirtyRange.startOffset++; - rangeStart.equals( bookmarkEnd.getParent() ) && dirtyRange.startOffset++; - rangeEnd.equals( bookmarkStart.getParent() ) && dirtyRange.endOffset++; - rangeEnd.equals( bookmarkEnd.getParent() ) && dirtyRange.endOffset++; - } - } - - return retval; + return this.getRanges().createBookmarks( serializable ); }, + /** + * Creates a bookmark for each range of this selection (from #getRanges) + * by calling the {@link CKEDITOR.dom.range.prototype.createBookmark2} method, + * with extra care taken to avoid interference among those ranges. The arguments + * received are the same as with the underlying range method. + * @returns {Array} Array of bookmarks for each range. + * @example + * var bookmarks = editor.getSelection().createBookmarks2(); + */ createBookmarks2 : function( normalized ) { - var bookmarks = [], - ranges = this.getRanges(); - - for ( var i = 0 ; i < ranges.length ; i++ ) - bookmarks.push( ranges[i].createBookmark2( normalized ) ); - - return bookmarks; + return this.getRanges().createBookmarks2( normalized ); }, + /** + * Selects the virtual ranges denoted by the bookmarks by calling #selectRanges. + * @param {Array} bookmarks The bookmarks representing ranges to be selected. + * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected. + * @example + * var bookmarks = editor.getSelection().createBookmarks(); + * editor.getSelection().selectBookmarks( bookmarks ); + */ selectBookmarks : function( bookmarks ) { var ranges = []; @@ -939,134 +1741,162 @@ For licensing, see LICENSE.html or http://ckeditor.com/license } this.selectRanges( ranges ); return this; - } - }; -})(); + }, -CKEDITOR.dom.range.prototype.select = - CKEDITOR.env.ie ? - // V2 - function( forceExpand ) + /** + * Retrieves the common ancestor node of the first range and the last range. + * @returns {CKEDITOR.dom.element} The common ancestor of the selection. + * @example + * var ancestor = editor.getSelection().getCommonAncestor(); + */ + getCommonAncestor : function() { - var collapsed = this.collapsed; - var isStartMarkerAlone; - var dummySpan; - - var bookmark = this.createBookmark(); - - // Create marker tags for the start and end boundaries. - var startNode = bookmark.startNode; - - var endNode; - if ( !collapsed ) - endNode = bookmark.endNode; + var ranges = this.getRanges(), + startNode = ranges[ 0 ].startContainer, + endNode = ranges[ ranges.length - 1 ].endContainer; + return startNode.getCommonAncestor( endNode ); + }, - // Create the main range which will be used for the selection. - var ieRange = this.document.$.body.createTextRange(); + /** + * Moves the scrollbar to the starting position of the current selection. + * @example + * editor.getSelection().scrollIntoView(); + */ + scrollIntoView : function() + { + // If we have split the block, adds a temporary span at the + // range position and scroll relatively to it. + var start = this.getStartElement(); + start.scrollIntoView(); + } + }; - // Position the range at the start boundary. - ieRange.moveToElementText( startNode.$ ); - ieRange.moveStart( 'character', 1 ); + var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), + isVisible = CKEDITOR.dom.walker.invisible( 1 ), + fillerTextRegex = /\ufeff|\u00a0/, + nonCells = { table:1,tbody:1,tr:1 }; - if ( endNode ) + CKEDITOR.dom.range.prototype.select = + CKEDITOR.env.ie ? + // V2 + function( forceExpand ) { - // Create a tool range for the end. - var ieRangeEnd = this.document.$.body.createTextRange(); + var collapsed = this.collapsed, + isStartMarkerAlone, dummySpan, ieRange; - // Position the tool range at the end. - ieRangeEnd.moveToElementText( endNode.$ ); + // Try to make a object selection. + var selected = this.getEnclosedNode(); + if ( selected ) + { + try + { + ieRange = this.document.$.body.createControlRange(); + ieRange.addElement( selected.$ ); + ieRange.select(); + return; + } + catch( er ) {} + } - // 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. - isStartMarkerAlone = forceExpand || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ); - - // Append a temporary  before the selection. - // This is needed to avoid IE destroying selections inside empty - // inline elements, like (#253). - // It is also needed when placing the selection right after an inline - // element to avoid the selection moving inside of it. - dummySpan = this.document.createElement( 'span' ); - dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. - dummySpan.insertBefore( startNode ); - - if ( isStartMarkerAlone ) + // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g. + // [... =>
cell
... + if ( this.startContainer.type == CKEDITOR.NODE_ELEMENT && this.startContainer.getName() in nonCells + || this.endContainer.type == CKEDITOR.NODE_ELEMENT && this.endContainer.getName() in nonCells ) { - // To expand empty blocks or line spaces after
, we need - // instead to have any char, which will be later deleted using the - // selection. - // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) - this.document.createText( '\ufeff' ).insertBefore( startNode ); + this.shrink( CKEDITOR.NODE_ELEMENT, true ); } - } - // Remove the markers (reset the position, because of the changes in the DOM tree). - this.setStartBefore( startNode ); - startNode.remove(); + var bookmark = this.createBookmark(); - if ( collapsed ) - { - if ( isStartMarkerAlone ) + // 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 = this.document.$.body.createTextRange(); + + // Position the range at the start boundary. + ieRange.moveToElementText( startNode.$ ); + ieRange.moveStart( 'character', 1 ); + + if ( endNode ) { - // Move the selection start to include the temporary \ufeff. - ieRange.moveStart( 'character', -1 ); + // Create a tool range for the end. + var ieRangeEnd = this.document.$.body.createTextRange(); - ieRange.select(); + // Position the tool range at the end. + ieRangeEnd.moveToElementText( endNode.$ ); - // Remove our temporary stuff. - this.document.$.selection.clear(); + // Move the end boundary of the main range to match the tool range. + ieRange.setEndPoint( 'EndToEnd', ieRangeEnd ); + ieRange.moveEnd( 'character', -1 ); } else - ieRange.select(); + { + // 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 ); + isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) // already a filler there? + && ( forceExpand || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) ); + + // Append a temporary  before the selection. + // This is needed to avoid IE destroying selections inside empty + // inline elements, like (#253). + // It is also needed when placing the selection right after an inline + // element to avoid the selection moving inside of it. + dummySpan = this.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
, we need + // instead to have any char, which will be later deleted using the + // selection. + // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) + this.document.createText( '\ufeff' ).insertBefore( startNode ); + } + } - dummySpan.remove(); - } - else - { - this.setEndBefore( endNode ); - endNode.remove(); - ieRange.select(); - } - } - : - function() - { - var startContainer = this.startContainer; + // Remove the markers (reset the position, because of the changes in the DOM tree). + this.setStartBefore( startNode ); + startNode.remove(); - // If we have a collapsed range, inside an empty element, we must add - // something to it, otherwise the caret will not be visible. - if ( this.collapsed && startContainer.type == CKEDITOR.NODE_ELEMENT && !startContainer.getChildCount() ) - startContainer.append( new CKEDITOR.dom.text( '' ) ); + if ( collapsed ) + { + if ( isStartMarkerAlone ) + { + // Move the selection start to include the temporary \ufeff. + ieRange.moveStart( 'character', -1 ); - var nativeRange = this.document.$.createRange(); - nativeRange.setStart( startContainer.$, this.startOffset ); + ieRange.select(); - try - { - nativeRange.setEnd( this.endContainer.$, this.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 ) - { - this.collapse( true ); - nativeRange.setEnd( this.endContainer.$, this.endOffset ); + // Remove our temporary stuff. + this.document.$.selection.clear(); + } + else + ieRange.select(); + + this.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START ); + dummySpan.remove(); } else - throw( e ); - } + { + this.setEndBefore( endNode ); + endNode.remove(); + ieRange.select(); + } - var selection = this.document.getSelection().getNative(); - selection.removeAllRanges(); - selection.addRange( nativeRange ); - }; + this.document.fire( 'selectionchange' ); + } + : + function() + { + this.document.getSelection().selectRanges( [ this ] ); + }; +} )();
[cell