X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;ds=sidebyside;f=_source%2Fplugins%2Fselection%2Fplugin.js;h=3a0a181d2954b6a2d96b8c64f326da8480b884fd;hb=6e682412d5cc0dfaedb376482e585bf2989c6863;hp=04be63171d0a106aeacbb2888e7f5b8277a498e7;hpb=8665a7c6c60586526e32e8941fe2896739b6ebfb;p=ckeditor.git
diff --git a/_source/plugins/selection/plugin.js b/_source/plugins/selection/plugin.js
index 04be631..3a0a181 100644
--- a/_source/plugins/selection/plugin.js
+++ b/_source/plugins/selection/plugin.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2012, 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,42 +70,207 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
// #### checkSelectionChange : END
+ function rangeRequiresFix( range )
+ {
+ function isInlineCt( node )
+ {
+ return node && node.type == CKEDITOR.NODE_ELEMENT
+ && node.getName() in CKEDITOR.dtd.$removeEmpty;
+ }
+
+ function singletonBlock( node )
+ {
+ var body = range.document.getBody();
+ return !node.is( 'body' ) && body.getChildCount() == 1;
+ }
+
+ var start = range.startContainer,
+ offset = range.startOffset;
+
+ if ( start.type == CKEDITOR.NODE_TEXT )
+ return false;
+
+ // 1. Empty inline element. ^
+ // 2. Adjoin to inline element.
text^
+ // 3. The only empty block in document.
^
(#7222)
+ return !CKEDITOR.tools.trim( start.getHtml() ) ? isInlineCt( start ) || singletonBlock( start )
+ : isInlineCt( start.getChild( offset - 1 ) ) || isInlineCt( start.getChild( offset ) );
+ }
+
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' :
// Select the contents of the textarea
- var textarea = editor.textarea.$ ;
+ var textarea = editor.textarea.$;
if ( CKEDITOR.env.ie )
- {
- textarea.createTextRange().execCommand( 'SelectAll' ) ;
- }
+ textarea.createTextRange().execCommand( 'SelectAll' );
else
{
- textarea.selectionStart = 0 ;
- textarea.selectionEnd = textarea.value.length ;
+ textarea.selectionStart = 0;
+ textarea.selectionEnd = textarea.value.length;
}
- textarea.focus() ;
+ 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,
- body = doc.getBody();
+ body = doc.getBody(),
+ html = doc.getDocumentElement();
if ( CKEDITOR.env.ie )
{
@@ -115,7 +280,8 @@ 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
@@ -127,18 +293,22 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
if ( evt.data.$.srcElement.nodeName != 'BODY' )
return;
- // If we have saved a range, restore it at this
+ // 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;
}
});
@@ -146,7 +316,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
body.on( 'focus', function()
{
// Enable selections to be saved.
- saveEnabled = true;
+ saveEnabled = 1;
saveSelection();
});
@@ -159,26 +329,70 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
return;
// Disable selections from being saved.
- saveEnabled = false;
+ saveEnabled = 0;
+ restoreEnabled = 1;
});
- // IE before version 8 will leave cursor blinking inside the document after
- // editor blurred unless we clean up the selection. (#4716)
- if ( CKEDITOR.env.ie && CKEDITOR.env.version < 8 )
+ // [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()
{
- doc.getWindow().on( 'blur', function( evt )
- {
- editor.document.$.selection.empty();
- });
- }
+ // 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.
- body.on( 'mousedown', disableSave );
+ 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 )
+ {
+ var sel = editor.document.$.selection;
+ if ( sel.type == 'None' )
+ scroll = editor.window.getScrollPosition();
+ }
+ disableSave();
+ });
+
body.on( 'mouseup',
- function()
+ function( evt )
{
- saveEnabled = true;
+ // 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 );
@@ -190,10 +404,91 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
body.on( 'keyup',
function()
{
- saveEnabled = true;
+ saveEnabled = 1;
saveSelection();
});
+ // 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 ( ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat )
+ && doc.$.compatMode != 'BackCompat' )
+ {
+ 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(
+ textRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ?
+ 'EndToEnd' :
+ 'StartToStart',
+ rngEnd );
+
+ // Update selection with new range.
+ textRng.select();
+ }
+ }
+
+ evt = evt.data.$;
+
+ // We're sure that the click happens at the region
+ // below body, but not on scrollbar.
+ if ( evt.y < html.$.clientHeight
+ && evt.y > body.$.offsetTop + body.$.clientHeight
+ && evt.x < html.$.clientWidth )
+ {
+ // Start to build the text range.
+ var textRng = body.$.createTextRange();
+ moveRangeToPoint( textRng, evt.x, evt.y );
+
+ html.on( 'mousemove', onHover );
+
+ html.on( 'mouseup', function( evt )
+ {
+ html.removeListener( 'mousemove', onHover );
+ evt.removeListener();
+
+ // Make it in effect on mouse up. (#9022)
+ textRng.select();
+ } );
+ }
+ });
+ }
+
+ // It's much simpler for IE8, we just need to reselect the reported range.
+ if ( CKEDITOR.env.ie8 )
+ {
+ html.on( 'mouseup', function( evt )
+ {
+ // 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.
+ if ( evt.data.getTarget().getName() == 'html' )
+ {
+ 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.
@@ -201,7 +496,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
function disableSave()
{
- saveEnabled = false;
+ saveEnabled = 0;
}
function saveSelection( testIt )
@@ -235,9 +530,11 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
// Avoid saving selection from within text input. (#5747)
var parentTag;
- if ( nativeSel.type == 'Text'
- && ( parentTag = nativeSel.createRange().parentElement().nodeName.toLowerCase() )
- && parentTag in { input: 1, textarea : 1 } )
+ if ( nativeSel && nativeSel.type && nativeSel.type != 'Control'
+ && ( parentTag = nativeSel.createRange() )
+ && ( parentTag = parentTag.parentElement() )
+ && ( parentTag = parentTag.nodeName )
+ && parentTag.toLowerCase() in { input: 1, textarea : 1 } )
{
return;
}
@@ -256,9 +553,38 @@ 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 )
+ {
+ 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, 10 );
}
});
+ // Clear the cached range path before unload. (#7174)
+ editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor );
+
editor.addCommand( 'selectAll', selectAllCmd );
editor.ui.addButton( 'SelectAll',
{
@@ -266,16 +592,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()
@@ -292,7 +634,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()
@@ -311,11 +653,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;
@@ -331,7 +673,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 )
{
@@ -341,7 +685,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
return lockedSelection;
this.document = document;
- this.isLocked = false;
+ this.isLocked = 0;
this._ =
{
cache : {}
@@ -349,14 +693,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;
}
@@ -366,19 +718,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,th: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 ?
@@ -396,19 +748,19 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
* Gets the type of the current selection. The following values are
* available:
*
- *
{@link CKEDITOR.SELECTION_NONE} (1): No selection.
- *
{@link CKEDITOR.SELECTION_TEXT} (2): Text is selected or
- * collapsed selection.
- *
{@link CKEDITOR.SELECTION_ELEMENT} (3): A element
- * selection.
+ *
{@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.
*
* @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 ?
@@ -475,10 +827,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 )
@@ -488,86 +854,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 );
-
- var comparisonStart = testRange.compareEndPoints( 'StartToStart', range ),
- comparisonEnd = testRange.compareEndPoints( 'EndToStart', range );
-
- testRange.collapse();
-
- if ( comparisonStart > 0 )
- break;
- // When selection stay at the side of certain self-closing elements, e.g. BR,
- // our comparison will never shows an equality. (#4824)
- else if ( !comparisonStart
- || comparisonEnd == 1 && comparisonStart == -1 )
- return { container : parent, offset : i };
- else if ( !comparisonEnd )
- return { container : parent, offset : i + 1 };
-
- 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;
- try
- {
- while ( distance > 0 )
- distance -= siblings[ --i ].nodeValue.length;
- }
- // Measurement in IE could be somtimes wrong because of