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: + * + * @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 (#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; + + // 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.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) ) && + startContainer.type == CKEDITOR.NODE_ELEMENT && + !startContainer.getChildCount() ) + { + startContainer.appendText( '' ); + } + + 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 ); + } + + this.reset(); + } + }, + + /** + * Create bookmark for every single of this selection range (from #getRanges) + * by calling the {@link CKEDITOR.dom.range.prototype.createBookmark} method, + * with extra cares to avoid interferon among those ranges. Same arguments are + * received as with the underlay range method. + */ + createBookmarks : function( serializable ) + { + return this.getRanges().createBookmarks( serializable ); + }, + + /** + * Create bookmark for every single of this selection range (from #getRanges) + * by calling the {@link CKEDITOR.dom.range.prototype.createBookmark2} method, + * with extra cares to avoid interferon among those ranges. Same arguments are + * received as with the underlay range method. + */ + createBookmarks2 : function( normalized ) + { + return this.getRanges().createBookmarks2( normalized ); + }, + + /** + * Select the virtual ranges denote by the bookmarks by calling #selectRanges. + * @param bookmarks + */ + selectBookmarks : function( bookmarks ) + { + var ranges = []; + for ( var i = 0 ; i < bookmarks.length ; i++ ) + { + var range = new CKEDITOR.dom.range( this.document ); + range.moveToBookmark( bookmarks[i] ); + ranges.push( range ); + } + this.selectRanges( ranges ); + return this; + }, + + /** + * Retrieve the common ancestor node of the first range and the last range. + */ + getCommonAncestor : function() + { + var ranges = this.getRanges(), + startNode = ranges[ 0 ].startContainer, + endNode = ranges[ ranges.length - 1 ].endContainer; + return startNode.getCommonAncestor( endNode ); + }, + + /** + * Moving scroll bar to the current selection's start position. + */ + 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(); + } + }; +})(); + +( function() +{ + var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), + fillerTextRegex = /\ufeff|\u00a0/, + nonCells = { table:1,tbody:1,tr:1 }; + + CKEDITOR.dom.range.prototype.select = + CKEDITOR.env.ie ? + // V2 + function( forceExpand ) + { + var collapsed = this.collapsed, + isStartMarkerAlone, dummySpan, ieRange; + + // 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 ) {} + } + + // 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 ) + { + this.shrink( CKEDITOR.NODE_ELEMENT, true ); + } + + var bookmark = this.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 = this.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 = this.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 ); + 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 ); + } + } + + // Remove the markers (reset the position, because of the changes in the DOM tree). + this.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. + this.document.$.selection.clear(); + } + else + ieRange.select(); + + this.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START ); + dummySpan.remove(); + } + else + { + this.setEndBefore( endNode ); + endNode.remove(); + ieRange.select(); + } + + this.document.fire( 'selectionchange' ); + } + : + function() + { + this.document.getSelection().selectRanges( [ this ] ); + }; +} )();
[cell