X-Git-Url: https://scm.cri.ensmp.fr/git/ckeditor.git/blobdiff_plain/256592bf803e851aa7fc953e08a6e9e58d970f8c..871bad8291b6dbc29d489d95d185458caab25158:/skins/ckeditor/_source/plugins/selection/plugin.js
diff --git a/skins/ckeditor/_source/plugins/selection/plugin.js b/skins/ckeditor/_source/plugins/selection/plugin.js
new file mode 100644
index 0000000..d8888c1
--- /dev/null
+++ b/skins/ckeditor/_source/plugins/selection/plugin.js
@@ -0,0 +1,1601 @@
+/*
+Copyright (c) 2003-2011, 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()
+ {
+ try
+ {
+ // 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 || !sel.document.getWindow().$ )
+ return;
+
+ var firstElement = sel.getStartElement();
+ var currentPath = new CKEDITOR.dom.elementPath( firstElement );
+
+ if ( !currentPath.compare( this._.selectionPreviousPath ) )
+ {
+ this._.selectionPreviousPath = currentPath;
+ this.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } );
+ }
+ }
+ catch (e)
+ {}
+ }
+
+ 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
+
+ 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.$;
+ 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 )
+ {
+ // 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, '' ) );
+ fillingChar = 0;
+ }
+ }
+
+ 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 ); } );
+ editor.on( 'key', function( e )
+ {
+ // Remove the filling char before some keys get
+ // executed, so they'll not get blocked by it.
+ switch ( e.data.keyCode )
+ {
+ case 13 : // ENTER
+ case CKEDITOR.SHIFT + 13 : // SHIFT-ENTER
+ case 37 : // LEFT-ARROW
+ case 39 : // RIGHT-ARROW
+ case 8 : // BACKSPACE
+ removeFillingChar( editor.document );
+ }
+ }, null, null, 10 );
+
+ 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(),
+ html = doc.getDocumentElement();
+
+ if ( CKEDITOR.env.ie )
+ {
+ // Other browsers don't loose the selection if the
+ // editor document loose the focus. In IE, we don't
+ // have support for it, so we reproduce it here, other
+ // than firing the selection change event.
+
+ var savedRange,
+ saveEnabled,
+ restoreEnabled = 1;
+
+ // "onfocusin" is fired before "onfocus". It makes it
+ // possible to restore the selection before click
+ // events get executed.
+ body.on( 'focusin', function( evt )
+ {
+ // 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;
+
+ // If we have saved a range, restore it at this
+ // point.
+ if ( savedRange )
+ {
+ if ( restoreEnabled )
+ {
+ // Well not break because of this.
+ try
+ {
+ savedRange.select();
+ }
+ catch (e)
+ {}
+
+ // Update locked selection because of the normalized text nodes. (#6083, #6987)
+ var lockedSelection = doc.getCustomData( 'cke_locked_selection' );
+ if ( lockedSelection )
+ {
+ lockedSelection.unlock();
+ lockedSelection.lock();
+ }
+ }
+
+ savedRange = null;
+ }
+ });
+
+ body.on( 'focus', function()
+ {
+ // Enable selections to be saved.
+ saveEnabled = 1;
+
+ saveSelection();
+ });
+
+ body.on( 'beforedeactivate', function( evt )
+ {
+ // 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;
+
+ // Disable selections from being saved.
+ 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 )
+ {
+ editor.on( 'blur', function( evt )
+ {
+ // Try/Catch to avoid errors if the editor is hidden. (#6375)
+ try
+ {
+ editor.document && editor.document.$.selection.empty();
+ }
+ catch (e) {}
+ });
+ }
+
+ // 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;
+ });
+
+ // In IE6/7 the blinking cursor appears, but contents are
+ // not editable. (#5634)
+ if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.version < 8 || CKEDITOR.env.quirks ) )
+ {
+ // The 'click' event is not fired when clicking the
+ // scrollbars, so we can use it to check whether
+ // the empty space following has been clicked.
+ html.on( 'click', function( evt )
+ {
+ if ( evt.data.getTarget().getName() == 'html' )
+ editor.getSelection().getRanges()[ 0 ].select();
+ });
+ }
+
+ var scroll;
+ // IE fires the "selectionchange" event when clicking
+ // inside a selection. We don't want to capture that.
+ 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( 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 );
+ },
+ 0 );
+ });
+
+ body.on( 'keydown', disableSave );
+ body.on( 'keyup',
+ function()
+ {
+ saveEnabled = 1;
+ saveSelection();
+ });
+
+
+ // IE is the only to provide the "selectionchange"
+ // event.
+ doc.on( 'selectionchange', saveSelection );
+
+ function disableSave()
+ {
+ saveEnabled = 0;
+ }
+
+ function saveSelection( testIt )
+ {
+ if ( saveEnabled )
+ {
+ var doc = editor.document,
+ sel = editor.getSelection(),
+ nativeSel = sel && sel.getNative();
+
+ // There is a very specific case, when clicking
+ // inside a text selection. In that case, the
+ // selection collapses at the clicking point,
+ // but the selection object remains in an
+ // unknown state, making createRange return a
+ // 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 && nativeSel && nativeSel.type == 'None' )
+ {
+ // The "InsertImage" command can be used to
+ // test whether the selection is good or not.
+ // If not, it's enough to give some time to
+ // IE to put things in order for us.
+ if ( !doc.$.queryCommandEnabled( 'InsertImage' ) )
+ {
+ CKEDITOR.tools.setTimeout( saveSelection, 50, this, true );
+ return;
+ }
+ }
+
+ // 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;
+ }
+
+ savedRange = nativeSel && sel.getRanges()[ 0 ];
+
+ checkSelectionChangeTimeout.call( editor );
+ }
+ }
+ }
+ else
+ {
+ // In other browsers, we make the selection change
+ // check based on other events, like clicks or keys
+ // press.
+
+ doc.on( 'mouseup', checkSelectionChangeTimeout, editor );
+ doc.on( 'keyup', checkSelectionChangeTimeout, editor );
+ }
+ });
+
+ // Clear the cached range path before unload. (#7174)
+ editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor );
+
+ editor.addCommand( 'selectAll', selectAllCmd );
+ editor.ui.addButton( 'SelectAll',
+ {
+ label : editor.lang.selectAll,
+ command : 'selectAll'
+ });
+
+ editor.selectionChange = checkSelectionChangeTimeout;
+
+ // 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
+ * WYSIWYG mode or no selection is available.
+ * @example
+ * var selection = CKEDITOR.instances.editor1.getSelection();
+ * alert( selection.getType() );
+ */
+ CKEDITOR.editor.prototype.getSelection = function()
+ {
+ return this.document && this.document.getSelection();
+ };
+
+ CKEDITOR.editor.prototype.forceNextSelectionCheck = function()
+ {
+ delete this._.selectionPreviousPath;
+ };
+
+ /**
+ * Gets the current selection from the document.
+ * @returns {CKEDITOR.dom.selection} A selection object.
+ * @example
+ * var selection = CKEDITOR.instances.editor1.document.getSelection();
+ * alert( selection.getType() );
+ */
+ CKEDITOR.dom.document.prototype.getSelection = function()
+ {
+ var sel = new CKEDITOR.dom.selection( this );
+ return ( !sel || sel.isInvalid ) ? null : sel;
+ };
+
+ /**
+ * No selection.
+ * @constant
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
+ * alert( 'Nothing is selected' );
+ */
+ CKEDITOR.SELECTION_NONE = 1;
+
+ /**
+ * Text or collapsed selection.
+ * @constant
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
+ * alert( 'Text is selected' );
+ */
+ CKEDITOR.SELECTION_TEXT = 2;
+
+ /**
+ * Element selection.
+ * @constant
+ * @example
+ * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
+ * alert( 'An element is selected' );
+ */
+ CKEDITOR.SELECTION_ELEMENT = 3;
+
+ /**
+ * Manipulates the selection in a DOM document.
+ * @constructor
+ * @example
+ */
+ CKEDITOR.dom.selection = function( document )
+ {
+ var lockedSelection = document.getCustomData( 'cke_locked_selection' );
+
+ if ( lockedSelection )
+ return lockedSelection;
+
+ this.document = document;
+ this.isLocked = 0;
+ this._ =
+ {
+ cache : {}
+ };
+
+ /**
+ * IE BUG: The selection's document may be a different document than the
+ * editor document. Return null if that's 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.$ ) )
+ {
+ this.isInvalid = true;
+ }
+ }
+
+ 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.
+ * @function
+ * @returns {Object} The native selection object.
+ * @example
+ * var selection = editor.getSelection().getNative();
+ */
+ getNative :
+ CKEDITOR.env.ie ?
+ function()
+ {
+ return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.$.selection );
+ }
+ :
+ function()
+ {
+ return this._.cache.nativeSel || ( this._.cache.nativeSel = 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): Text is selected or
+ * collapsed selection.
+ *
{@link CKEDITOR.SELECTION_ELEMENT} (3): A element
+ * selection.
+ *
+ * @function
+ * @returns {Number} One of the following constant values:
+ * {@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' );
+ */
+ getType :
+ CKEDITOR.env.ie ?
+ 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 )
+ 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 );
+ },
+
+ /**
+ * Retrieve the {@link CKEDITOR.dom.range} instances that represent the current selection.
+ * Note: Some browsers returns multiple ranges even on a sequent selection, e.g. Firefox returns
+ * one range for each table cell when one or more table row is selected.
+ * @return {Array}
+ * @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 )
+ {
+ // 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. ^
+ 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;
+
+ // 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.
text ^
(#7433)
+ if ( CKEDITOR.env.ie9Compat && child.tagName == 'BR' )
+ {
+ var bmId = 'cke_range_marker';
+ range.execCommand( 'CreateBookmark', false, bmId );
+ child = doc.getElementsByName( bmId )[ 0 ];
+ var offset = getNodeIndex( child );
+ parent.removeChild( child );
+ return { container : parent, offset : offset };
+ }
+ 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_ELEMENT )
+ 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 )
+ distance -= siblings[ --i ].nodeValue.length;
+
+ return { container : siblings[ i ], 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' ];
+ distance -= sibling.nodeValue.length;
+ child = sibling;
+ }
+ // Measurement in IE could be somtimes wrong because of