/* Copyright (c) 2003-2010, 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 var selectAllCmd = { modes : { wysiwyg : 1, source : 1 }, exec : function( editor ) { switch ( editor.mode ) { case 'wysiwyg' : editor.document.$.execCommand( 'SelectAll', false, null ); 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 }; CKEDITOR.plugins.add( 'selection', { init : function( editor ) { 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) {} } savedRange = null; } }); body.on( 'focus', function() { // Enable selections to be saved. saveEnabled = true; 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 = false; 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 ) { editor.document && editor.document.$.selection.empty(); }); } // 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(); }); } // IE fires the "selectionchange" event when clicking // inside a selection. We don't want to capture that. body.on( 'mousedown', function () { disableSave(); }); body.on( 'mouseup', function() { saveEnabled = true; setTimeout( function() { saveSelection( true ); }, 0 ); }); body.on( 'keydown', disableSave ); body.on( 'keyup', function() { saveEnabled = true; saveSelection(); }); // IE is the only to provide the "selectionchange" // event. doc.on( 'selectionchange', saveSelection ); function disableSave() { saveEnabled = false; } 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 ); } }); editor.addCommand( 'selectAll', selectAllCmd ); editor.ui.addButton( 'SelectAll', { label : editor.lang.selectAll, command : 'selectAll' }); editor.selectionChange = checkSelectionChangeTimeout; } }); /** * 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 = false; 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, th: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() { // 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(); var siblings = parent.childNodes; var testRange; for ( var i = 0 ; i < siblings.length ; i++ ) { var child = siblings[ i ]; if ( child.nodeType == 1 ) { 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; } } if ( !testRange ) { testRange = range.duplicate(); testRange.moveToElementText( parent ); testRange.collapse( false ); } 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; try { while ( distance > 0 ) distance -= siblings[ --i ].nodeValue.length; } // Measurement in IE could be somtimes wrong because of