X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;ds=sidebyside;f=_source%2Fplugins%2Fselection%2Fplugin.js;h=3a0a181d2954b6a2d96b8c64f326da8480b884fd;hb=6e682412d5cc0dfaedb376482e585bf2989c6863;hp=f0d2c4456ffa10d94a892dc55aea9f01df9d69ac;hpb=c9fdde67e6384bd5a66adc2b3bba5c4ce9db56c7;p=ckeditor.git diff --git a/_source/plugins/selection/plugin.js b/_source/plugins/selection/plugin.js index f0d2c44..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 */ @@ -70,38 +70,202 @@ 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, @@ -129,21 +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 ) { - if ( restoreEnabled ) - { - // Well not break because of this. - try - { - savedRange.select(); - } - catch (e) - {} - } - + // Well not break because of this. + try { savedRange.select(); } catch (e) {} savedRange = null; } }); @@ -151,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(); }); @@ -164,23 +329,29 @@ 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() { - editor.on( 'blur', function( evt ) - { - editor.document && 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 () + html.on( 'mousedown', function() { // Lock restore selection now, as we have // a followed 'click' event which introduce @@ -188,25 +359,11 @@ For licensing, see LICENSE.html or http://ckeditor.com/license restoreEnabled = 0; }); - html.on( 'mouseup', function () + 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. @@ -235,7 +392,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license } scroll = null; - saveEnabled = true; + saveEnabled = 1; setTimeout( function() { saveSelection( true ); @@ -247,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. @@ -258,7 +496,7 @@ For licensing, see LICENSE.html or http://ckeditor.com/license function disableSave() { - saveEnabled = false; + saveEnabled = 0; } function saveSelection( testIt ) @@ -315,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', { @@ -325,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() @@ -351,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() @@ -370,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; @@ -390,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 ) { @@ -400,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 : {} @@ -408,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; } @@ -425,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 ? @@ -455,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): A text or a collapsed
+ * selection is selected.{@link CKEDITOR.SELECTION_ELEMENT}
(3): An element is
+ * selected.{@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 ?
@@ -535,19 +828,23 @@ For licensing, see LICENSE.html or http://ckeditor.com/license
},
/**
- * 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}
+ * 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);
+ * var ranges = selection.getRanges();
+ * alert( ranges.length );
*/
- getRanges : (function ()
+ 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 )
@@ -557,77 +854,131 @@ 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
^