/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ /** * @fileOverview Defines the {@link CKEDITOR.editor} class that represents an * editor instance. */ ( function() { // Override the basic constructor defined at editor_basic.js. Editor.prototype = CKEDITOR.editor.prototype; CKEDITOR.editor = Editor; /** * Represents an editor instance. This constructor should be rarely * used in favor of the {@link CKEDITOR} editor creation functions. * * @class CKEDITOR.editor * @mixins CKEDITOR.event * @constructor Creates an editor class instance. * @param {Object} [instanceConfig] Configuration values for this specific instance. * @param {CKEDITOR.dom.element} [element] The DOM element upon which this editor * will be created. * @param {Number} [mode] The element creation mode to be used by this editor. */ function Editor( instanceConfig, element, mode ) { // Call the CKEDITOR.event constructor to initialize this instance. CKEDITOR.event.call( this ); // Make a clone of the config object, to avoid having it touched by our code. (http://dev.ckeditor.com/ticket/9636) instanceConfig = instanceConfig && CKEDITOR.tools.clone( instanceConfig ); // if editor is created off one page element. if ( element !== undefined ) { // Asserting element and mode not null. if ( !( element instanceof CKEDITOR.dom.element ) ) throw new Error( 'Expect element of type CKEDITOR.dom.element.' ); else if ( !mode ) throw new Error( 'One of the element modes must be specified.' ); if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && mode == CKEDITOR.ELEMENT_MODE_INLINE ) throw new Error( 'Inline element mode is not supported on IE quirks.' ); if ( !isSupportedElement( element, mode ) ) throw new Error( 'The specified element mode is not supported on element: "' + element.getName() + '".' ); /** * The original host page element upon which the editor is created. It is only * supposed to be provided by the particular editor creator and is not subject to * be modified. * * @readonly * @property {CKEDITOR.dom.element} */ this.element = element; /** * This property indicates the way this instance is associated with the {@link #element}. * See also {@link CKEDITOR#ELEMENT_MODE_INLINE} and {@link CKEDITOR#ELEMENT_MODE_REPLACE}. * * @readonly * @property {Number} */ this.elementMode = mode; this.name = ( this.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO ) && ( element.getId() || element.getNameAtt() ); } else { this.elementMode = CKEDITOR.ELEMENT_MODE_NONE; } // Declare the private namespace. this._ = {}; this.commands = {}; /** * Contains all UI templates created for this editor instance. * * @readonly * @property {Object} */ this.templates = {}; /** * A unique identifier of this editor instance. * * **Note:** It will be originated from the `id` or `name` * attribute of the {@link #element}, otherwise a name pattern of * `'editor{n}'` will be used. * * @readonly * @property {String} */ this.name = this.name || genEditorName(); /** * A unique random string assigned to each editor instance on the page. * * @readonly * @property {String} */ this.id = CKEDITOR.tools.getNextId(); /** * Indicates editor initialization status. The following statuses are available: * * * **unloaded**: The initial state — the editor instance was initialized, * but its components (configuration, plugins, language files) are not loaded yet. * * **loaded**: The editor components were loaded — see the {@link CKEDITOR.editor#loaded} event. * * **ready**: The editor is fully initialized and ready — see the {@link CKEDITOR.editor#instanceReady} event. * * **destroyed**: The editor was destroyed — see the {@link CKEDITOR.editor#method-destroy} method. * * @since 4.1 * @readonly * @property {String} */ this.status = 'unloaded'; /** * The configuration for this editor instance. It inherits all * settings defined in {@link CKEDITOR.config}, combined with settings * loaded from custom configuration files and those defined inline in * the page when creating the editor. * * var editor = CKEDITOR.instances.editor1; * alert( editor.config.skin ); // e.g. 'moono' * * @readonly * @property {CKEDITOR.config} */ this.config = CKEDITOR.tools.prototypedCopy( CKEDITOR.config ); /** * The namespace containing UI features related to this editor instance. * * @readonly * @property {CKEDITOR.ui} */ this.ui = new CKEDITOR.ui( this ); /** * Controls the focus state of this editor instance. This property * is rarely used for normal API operations. It is mainly * targeted at developers adding UI elements to the editor interface. * * @readonly * @property {CKEDITOR.focusManager} */ this.focusManager = new CKEDITOR.focusManager( this ); /** * Controls keystroke typing in this editor instance. * * @readonly * @property {CKEDITOR.keystrokeHandler} */ this.keystrokeHandler = new CKEDITOR.keystrokeHandler( this ); // Make the editor update its command states on mode change. this.on( 'readOnly', updateCommands ); this.on( 'selectionChange', function( evt ) { updateCommandsContext( this, evt.data.path ); } ); this.on( 'activeFilterChange', function() { updateCommandsContext( this, this.elementPath(), true ); } ); this.on( 'mode', updateCommands ); // Handle startup focus. this.on( 'instanceReady', function() { this.config.startupFocus && this.focus(); } ); CKEDITOR.fire( 'instanceCreated', null, this ); // Add this new editor to the CKEDITOR.instances collections. CKEDITOR.add( this ); // Return the editor instance immediately to enable early stage event registrations. CKEDITOR.tools.setTimeout( function() { if ( this.status !== 'destroyed' ) { initConfig( this, instanceConfig ); } else { CKEDITOR.warn( 'editor-incorrect-destroy' ); } }, 0, this ); } var nameCounter = 0; function genEditorName() { do { var name = 'editor' + ( ++nameCounter ); } while ( CKEDITOR.instances[ name ] ); return name; } // Asserting element DTD depending on mode. function isSupportedElement( element, mode ) { if ( mode == CKEDITOR.ELEMENT_MODE_INLINE ) return element.is( CKEDITOR.dtd.$editable ) || element.is( 'textarea' ); else if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE ) return !element.is( CKEDITOR.dtd.$nonBodyContent ); return 1; } function updateCommands() { var commands = this.commands, name; for ( name in commands ) updateCommand( this, commands[ name ] ); } function updateCommand( editor, cmd ) { cmd[ cmd.startDisabled ? 'disable' : editor.readOnly && !cmd.readOnly ? 'disable' : cmd.modes[ editor.mode ] ? 'enable' : 'disable' ](); } function updateCommandsContext( editor, path, forceRefresh ) { // Commands cannot be refreshed without a path. In edge cases // it may happen that there's no selection when this function is executed. // For example when active filter is changed in http://dev.ckeditor.com/ticket/10877. if ( !path ) return; var command, name, commands = editor.commands; for ( name in commands ) { command = commands[ name ]; if ( forceRefresh || command.contextSensitive ) command.refresh( editor, path ); } } // ##### START: Config Privates // These function loads custom configuration files and cache the // CKEDITOR.editorConfig functions defined on them, so there is no need to // download them more than once for several instances. var loadConfigLoaded = {}; function loadConfig( editor ) { var customConfig = editor.config.customConfig; // Check if there is a custom config to load. if ( !customConfig ) return false; customConfig = CKEDITOR.getUrl( customConfig ); var loadedConfig = loadConfigLoaded[ customConfig ] || ( loadConfigLoaded[ customConfig ] = {} ); // If the custom config has already been downloaded, reuse it. if ( loadedConfig.fn ) { // Call the cached CKEDITOR.editorConfig defined in the custom // config file for the editor instance depending on it. loadedConfig.fn.call( editor, editor.config ); // If there is no other customConfig in the chain, fire the // "configLoaded" event. if ( CKEDITOR.getUrl( editor.config.customConfig ) == customConfig || !loadConfig( editor ) ) editor.fireOnce( 'customConfigLoaded' ); } else { // Load the custom configuration file. // To resolve customConfig race conflicts, use scriptLoader#queue // instead of scriptLoader#load (http://dev.ckeditor.com/ticket/6504). CKEDITOR.scriptLoader.queue( customConfig, function() { // If the CKEDITOR.editorConfig function has been properly // defined in the custom configuration file, cache it. if ( CKEDITOR.editorConfig ) loadedConfig.fn = CKEDITOR.editorConfig; else loadedConfig.fn = function() {}; // Call the load config again. This time the custom // config is already cached and so it will get loaded. loadConfig( editor ); } ); } return true; } function initConfig( editor, instanceConfig ) { // Setup the lister for the "customConfigLoaded" event. editor.on( 'customConfigLoaded', function() { if ( instanceConfig ) { // Register the events that may have been set at the instance // configuration object. if ( instanceConfig.on ) { for ( var eventName in instanceConfig.on ) { editor.on( eventName, instanceConfig.on[ eventName ] ); } } // Overwrite the settings from the in-page config. CKEDITOR.tools.extend( editor.config, instanceConfig, true ); delete editor.config.on; } onConfigLoaded( editor ); } ); // The instance config may override the customConfig setting to avoid // loading the default ~/config.js file. if ( instanceConfig && instanceConfig.customConfig != null ) editor.config.customConfig = instanceConfig.customConfig; // Load configs from the custom configuration files. if ( !loadConfig( editor ) ) editor.fireOnce( 'customConfigLoaded' ); } // ##### END: Config Privates // Set config related properties. function onConfigLoaded( editor ) { var config = editor.config; /** * Indicates the read-only state of this editor. This is a read-only property. * See also {@link CKEDITOR.editor#setReadOnly}. * * @since 3.6 * @readonly * @property {Boolean} */ editor.readOnly = isEditorReadOnly(); function isEditorReadOnly() { if ( config.readOnly ) { return true; } if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) { if ( editor.element.is( 'textarea' ) ) { return editor.element.hasAttribute( 'disabled' ) || editor.element.hasAttribute( 'readonly' ); } else { return editor.element.isReadOnly(); } } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { return editor.element.hasAttribute( 'disabled' ) || editor.element.hasAttribute( 'readonly' ); } return false; } /** * Indicates that the editor is running in an environment where * no block elements are accepted inside the content. * * This can be for example inline editor based on an `

` element. * * @readonly * @property {Boolean} */ editor.blockless = editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? !( editor.element.is( 'textarea' ) || CKEDITOR.dtd[ editor.element.getName() ].p ) : false; /** * The [tabbing navigation](http://en.wikipedia.org/wiki/Tabbing_navigation) order determined for this editor instance. * This can be set by the {@link CKEDITOR.config#tabIndex} * setting or taken from the `tabindex` attribute of the * {@link #element} associated with the editor. * * alert( editor.tabIndex ); // e.g. 0 * * @readonly * @property {Number} [=0] */ editor.tabIndex = config.tabIndex || editor.element && editor.element.getAttribute( 'tabindex' ) || 0; editor.activeEnterMode = editor.enterMode = validateEnterMode( editor, config.enterMode ); editor.activeShiftEnterMode = editor.shiftEnterMode = validateEnterMode( editor, config.shiftEnterMode ); // Set CKEDITOR.skinName. Note that it is not possible to have // different skins on the same page, so the last one to set it "wins". if ( config.skin ) CKEDITOR.skinName = config.skin; // Fire the "configLoaded" event. editor.fireOnce( 'configLoaded' ); initComponents( editor ); } // Various other core components that read editor configuration. function initComponents( editor ) { // Documented in dataprocessor.js. editor.dataProcessor = new CKEDITOR.htmlDataProcessor( editor ); // Set activeFilter directly to avoid firing event. editor.filter = editor.activeFilter = new CKEDITOR.filter( editor ); loadSkin( editor ); } function loadSkin( editor ) { CKEDITOR.skin.loadPart( 'editor', function() { loadLang( editor ); } ); } function loadLang( editor ) { CKEDITOR.lang.load( editor.config.language, editor.config.defaultLanguage, function( languageCode, lang ) { var configTitle = editor.config.title; /** * The code for the language resources that have been loaded * for the user interface elements of this editor instance. * * alert( editor.langCode ); // e.g. 'en' * * @readonly * @property {String} */ editor.langCode = languageCode; /** * An object that contains all language strings used by the editor interface. * * alert( editor.lang.basicstyles.bold ); // e.g. 'Negrito' (if the language is set to Portuguese) * * @readonly * @property {Object} lang */ // As we'll be adding plugin specific entries that could come // from different language code files, we need a copy of lang, // not a direct reference to it. editor.lang = CKEDITOR.tools.prototypedCopy( lang ); /** * Indicates the human-readable title of this editor. Although this is a read-only property, * it can be initialized with {@link CKEDITOR.config#title}. * * **Note:** Please do not confuse this property with {@link CKEDITOR.editor#name editor.name} * which identifies the instance in the {@link CKEDITOR#instances} literal. * * @since 4.2 * @readonly * @property {String/Boolean} */ editor.title = typeof configTitle == 'string' || configTitle === false ? configTitle : [ editor.lang.editor, editor.name ].join( ', ' ); if ( !editor.config.contentsLangDirection ) { // Fallback to either the editable element direction or editor UI direction depending on creators. editor.config.contentsLangDirection = editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? editor.element.getDirection( 1 ) : editor.lang.dir; } editor.fire( 'langLoaded' ); preloadStylesSet( editor ); } ); } // Preloads styles set file (config.stylesSet). // If stylesSet was defined directly (by an array) // this function will call loadPlugins fully synchronously. // If stylesSet is a string (path) loadPlugins will // be called asynchronously. // In both cases - styles will be preload before plugins initialization. function preloadStylesSet( editor ) { editor.getStylesSet( function( styles ) { // Wait for editor#loaded, so plugins could add their listeners. // But listen with high priority to fire editor#stylesSet before editor#uiReady and editor#setData. editor.once( 'loaded', function() { // Note: we can't use fireOnce because this event may canceled and fired again. editor.fire( 'stylesSet', { styles: styles } ); }, null, null, 1 ); loadPlugins( editor ); } ); } function loadPlugins( editor ) { var config = editor.config, plugins = config.plugins, extraPlugins = config.extraPlugins, removePlugins = config.removePlugins; if ( extraPlugins ) { // Remove them first to avoid duplications. var extraRegex = new RegExp( '(?:^|,)(?:' + extraPlugins.replace( /\s*,\s*/g, '|' ) + ')(?=,|$)', 'g' ); plugins = plugins.replace( extraRegex, '' ); plugins += ',' + extraPlugins; } if ( removePlugins ) { var removeRegex = new RegExp( '(?:^|,)(?:' + removePlugins.replace( /\s*,\s*/g, '|' ) + ')(?=,|$)', 'g' ); plugins = plugins.replace( removeRegex, '' ); } // Load the Adobe AIR plugin conditionally. CKEDITOR.env.air && ( plugins += ',adobeair' ); // Load all plugins defined in the "plugins" setting. CKEDITOR.plugins.load( plugins.split( ',' ), function( plugins ) { // The list of plugins. var pluginsArray = []; // The language code to get loaded for each plugin. Null // entries will be appended for plugins with no language files. var languageCodes = []; // The list of URLs to language files. var languageFiles = []; /** * An object that contains references to all plugins used by this * editor instance. * * alert( editor.plugins.dialog.path ); // e.g. 'http://example.com/ckeditor/plugins/dialog/' * * // Check if a plugin is available. * if ( editor.plugins.image ) { * ... * } * * @readonly * @property {Object} */ editor.plugins = plugins; // Loop through all plugins, to build the list of language // files to get loaded. // // Check also whether any of loaded plugins doesn't require plugins // defined in config.removePlugins. Throw non-blocking error if this happens. for ( var pluginName in plugins ) { var plugin = plugins[ pluginName ], pluginLangs = plugin.lang, lang = null, requires = plugin.requires, match, name; // Transform it into a string, if it's not one. if ( CKEDITOR.tools.isArray( requires ) ) requires = requires.join( ',' ); if ( requires && ( match = requires.match( removeRegex ) ) ) { while ( ( name = match.pop() ) ) { CKEDITOR.error( 'editor-plugin-required', { plugin: name.replace( ',', '' ), requiredBy: pluginName } ); } } // If the plugin has "lang". if ( pluginLangs && !editor.lang[ pluginName ] ) { // Trasnform the plugin langs into an array, if it's not one. if ( pluginLangs.split ) pluginLangs = pluginLangs.split( ',' ); // Resolve the plugin language. If the current language // is not available, get English or the first one. if ( CKEDITOR.tools.indexOf( pluginLangs, editor.langCode ) >= 0 ) lang = editor.langCode; else { // The language code may have the locale information (zh-cn). // Fall back to locale-less in that case (zh). var langPart = editor.langCode.replace( /-.*/, '' ); if ( langPart != editor.langCode && CKEDITOR.tools.indexOf( pluginLangs, langPart ) >= 0 ) lang = langPart; // Try the only "generic" option we have: English. else if ( CKEDITOR.tools.indexOf( pluginLangs, 'en' ) >= 0 ) lang = 'en'; else lang = pluginLangs[ 0 ]; } if ( !plugin.langEntries || !plugin.langEntries[ lang ] ) { // Put the language file URL into the list of files to // get downloaded. languageFiles.push( CKEDITOR.getUrl( plugin.path + 'lang/' + lang + '.js' ) ); } else { editor.lang[ pluginName ] = plugin.langEntries[ lang ]; lang = null; } } // Save the language code, so we know later which // language has been resolved to this plugin. languageCodes.push( lang ); pluginsArray.push( plugin ); } // Load all plugin specific language files in a row. CKEDITOR.scriptLoader.load( languageFiles, function() { // Initialize all plugins that have the "beforeInit" and "init" methods defined. var methods = [ 'beforeInit', 'init', 'afterInit' ]; for ( var m = 0; m < methods.length; m++ ) { for ( var i = 0; i < pluginsArray.length; i++ ) { var plugin = pluginsArray[ i ]; // Uses the first loop to update the language entries also. if ( m === 0 && languageCodes[ i ] && plugin.lang && plugin.langEntries ) editor.lang[ plugin.name ] = plugin.langEntries[ languageCodes[ i ] ]; // Call the plugin method (beforeInit and init). if ( plugin[ methods[ m ] ] ) plugin[ methods[ m ] ]( editor ); } } editor.fireOnce( 'pluginsLoaded' ); // Setup the configured keystrokes. config.keystrokes && editor.setKeystroke( editor.config.keystrokes ); // Setup the configured blocked keystrokes. for ( i = 0; i < editor.config.blockedKeystrokes.length; i++ ) editor.keystrokeHandler.blockedKeystrokes[ editor.config.blockedKeystrokes[ i ] ] = 1; editor.status = 'loaded'; editor.fireOnce( 'loaded' ); CKEDITOR.fire( 'instanceLoaded', null, editor ); } ); } ); } // Send to data output back to editor's associated element. function updateEditorElement() { var element = this.element; // Some editor creation mode will not have the // associated element. if ( element && this.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO ) { var data = this.getData(); if ( this.config.htmlEncodeOutput ) data = CKEDITOR.tools.htmlEncode( data ); if ( element.is( 'textarea' ) ) element.setValue( data ); else element.setHtml( data ); return true; } return false; } // Always returns ENTER_BR in case of blockless editor. function validateEnterMode( editor, enterMode ) { return editor.blockless ? CKEDITOR.ENTER_BR : enterMode; } // Create DocumentFragment from specified ranges. For now it handles only tables // and returns DocumentFragment from the 1. range for other cases. (http://dev.ckeditor.com/ticket/13884) function createDocumentFragmentFromRanges( ranges, editable ) { var docFragment = new CKEDITOR.dom.documentFragment(), tableClone, currentRow, currentRowClone; // We must handle two cases here: // 1. [Cell] (IE9+, Edge, Chrome, Firefox) // 2. [Cell] (IE8-, Safari) function isSelectedCell( range ) { var start = range.startContainer, end = range.endContainer; if ( start.is && ( start.is( 'tr' ) || ( start.is( 'td' ) && start.equals( end ) && range.endOffset === start.getChildCount() ) ) ) { return true; } return false; } function cloneCell( range ) { var start = range.startContainer; if ( start.is( 'tr' ) ) { return range.cloneContents(); } return start.clone( true ); } for ( var i = 0; i < ranges.length; i++ ) { var range = ranges[ i ], container = range.startContainer.getAscendant( 'tr', true ); if ( isSelectedCell( range ) ) { if ( !tableClone ) { tableClone = container.getAscendant( 'table' ).clone(); tableClone.append( container.getAscendant( { thead: 1, tbody: 1, tfoot: 1 } ).clone() ); docFragment.append( tableClone ); tableClone = tableClone.findOne( 'thead, tbody, tfoot' ); } if ( !( currentRow && currentRow.equals( container ) ) ) { currentRow = container; currentRowClone = container.clone(); tableClone.append( currentRowClone ); } currentRowClone.append( cloneCell( range ) ); } else { // If there was something else copied with table, // append it to DocumentFragment. docFragment.append( range.cloneContents() ); } } if ( !tableClone ) { return editable.getHtmlFromRange( ranges[ 0 ] ); } return docFragment; } CKEDITOR.tools.extend( CKEDITOR.editor.prototype, { /** * Adds a command definition to the editor instance. Commands added with * this function can be executed later with the {@link #execCommand} method. * * editorInstance.addCommand( 'sample', { * exec: function( editor ) { * alert( 'Executing a command for the editor name "' + editor.name + '"!' ); * } * } ); * * @param {String} commandName The indentifier name of the command. * @param {CKEDITOR.commandDefinition} commandDefinition The command definition. */ addCommand: function( commandName, commandDefinition ) { commandDefinition.name = commandName.toLowerCase(); var cmd = new CKEDITOR.command( this, commandDefinition ); // Update command when mode is set. // This guarantees that commands added before first editor#mode // aren't immediately updated, but waits for editor#mode and that // commands added later are immediately refreshed, even when added // before instanceReady. http://dev.ckeditor.com/ticket/10103, http://dev.ckeditor.com/ticket/10249 if ( this.mode ) updateCommand( this, cmd ); return this.commands[ commandName ] = cmd; }, /** * Attaches the editor to a form to call {@link #updateElement} before form submission. * This method is called by both creators ({@link CKEDITOR#replace replace} and * {@link CKEDITOR#inline inline}), so there is no reason to call it manually. * * @private */ _attachToForm: function() { var editor = this, element = editor.element, form = new CKEDITOR.dom.element( element.$.form ); // If are replacing a textarea, we must if ( element.is( 'textarea' ) ) { if ( form ) { form.on( 'submit', onSubmit ); // Check if there is no element/elements input with name == "submit". // If they exists they will overwrite form submit function (form.$.submit). // If form.$.submit is overwritten we can not do anything with it. if ( isFunction( form.$.submit ) ) { // Setup the submit function because it doesn't fire the // "submit" event. form.$.submit = CKEDITOR.tools.override( form.$.submit, function( originalSubmit ) { return function() { onSubmit(); // For IE, the DOM submit function is not a // function, so we need third check. if ( originalSubmit.apply ) originalSubmit.apply( this ); else originalSubmit(); }; } ); } // Remove 'submit' events registered on form element before destroying.(http://dev.ckeditor.com/ticket/3988) editor.on( 'destroy', function() { form.removeListener( 'submit', onSubmit ); } ); } } function onSubmit( evt ) { editor.updateElement(); // http://dev.ckeditor.com/ticket/8031 If textarea had required attribute and editor is empty fire 'required' event and if // it was cancelled, prevent submitting the form. if ( editor._.required && !element.getValue() && editor.fire( 'required' ) === false ) { // When user press save button event (evt) is undefined (see save plugin). // This method works because it throws error so originalSubmit won't be called. // Also this error won't be shown because it will be caught in save plugin. evt.data.preventDefault(); } } function isFunction( f ) { // For IE8 typeof fun == object so we cannot use it. return !!( f && f.call && f.apply ); } }, /** * Destroys the editor instance, releasing all resources used by it. * If the editor replaced an element, the element will be recovered. * * alert( CKEDITOR.instances.editor1 ); // e.g. object * CKEDITOR.instances.editor1.destroy(); * alert( CKEDITOR.instances.editor1 ); // undefined * * @param {Boolean} [noUpdate] If the instance is replacing a DOM * element, this parameter indicates whether or not to update the * element with the instance content. */ destroy: function( noUpdate ) { this.fire( 'beforeDestroy' ); !noUpdate && updateEditorElement.call( this ); this.editable( null ); if ( this.filter ) { this.filter.destroy(); delete this.filter; } delete this.activeFilter; this.status = 'destroyed'; this.fire( 'destroy' ); // Plug off all listeners to prevent any further events firing. this.removeAllListeners(); CKEDITOR.remove( this ); CKEDITOR.fire( 'instanceDestroyed', null, this ); }, /** * Returns an {@link CKEDITOR.dom.elementPath element path} for the selection in the editor. * * @param {CKEDITOR.dom.node} [startNode] From which the path should start, * if not specified defaults to editor selection's * start element yielded by {@link CKEDITOR.dom.selection#getStartElement}. * @returns {CKEDITOR.dom.elementPath} */ elementPath: function( startNode ) { if ( !startNode ) { var sel = this.getSelection(); if ( !sel ) return null; startNode = sel.getStartElement(); } return startNode ? new CKEDITOR.dom.elementPath( startNode, this.editable() ) : null; }, /** * Shortcut to create a {@link CKEDITOR.dom.range} instance from the editable element. * * @returns {CKEDITOR.dom.range} The DOM range created if the editable has presented. * @see CKEDITOR.dom.range */ createRange: function() { var editable = this.editable(); return editable ? new CKEDITOR.dom.range( editable ) : null; }, /** * Executes a command associated with the editor. * * editorInstance.execCommand( 'bold' ); * * @param {String} commandName The identifier name of the command. * @param {Object} [data] The data to be passed to the command. It defaults to * an empty object starting from 4.7.0. * @returns {Boolean} `true` if the command was executed successfully, `false` otherwise. * @see CKEDITOR.editor#addCommand */ execCommand: function( commandName, data ) { var command = this.getCommand( commandName ); var eventData = { name: commandName, commandData: data || {}, command: command }; if ( command && command.state != CKEDITOR.TRISTATE_DISABLED ) { if ( this.fire( 'beforeCommandExec', eventData ) !== false ) { eventData.returnValue = command.exec( eventData.commandData ); // Fire the 'afterCommandExec' immediately if command is synchronous. if ( !command.async && this.fire( 'afterCommandExec', eventData ) !== false ) return eventData.returnValue; } } // throw 'Unknown command name "' + commandName + '"'; return false; }, /** * Gets one of the registered commands. Note that after registering a * command definition with {@link #addCommand}, it is * transformed internally into an instance of * {@link CKEDITOR.command}, which will then be returned by this function. * * @param {String} commandName The name of the command to be returned. * This is the same name that is used to register the command with `addCommand`. * @returns {CKEDITOR.command} The command object identified by the provided name. */ getCommand: function( commandName ) { return this.commands[ commandName ]; }, /** * Gets the editor data. The data will be in "raw" format. It is the same * data that is posted by the editor. * * if ( CKEDITOR.instances.editor1.getData() == '' ) * alert( 'There is no data available.' ); * * @param {Boolean} internal If set to `true`, it will prevent firing the * {@link CKEDITOR.editor#beforeGetData} and {@link CKEDITOR.editor#event-getData} events, so * the real content of the editor will not be read and cached data will be returned. The method will work * much faster, but this may result in the editor returning the data that is not up to date. This parameter * should thus only be set to `true` when you are certain that the cached data is up to date. * * @returns {String} The editor data. */ getData: function( internal ) { !internal && this.fire( 'beforeGetData' ); var eventData = this._.data; if ( typeof eventData != 'string' ) { var element = this.element; if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) eventData = element.is( 'textarea' ) ? element.getValue() : element.getHtml(); else eventData = ''; } eventData = { dataValue: eventData }; // Fire "getData" so data manipulation may happen. !internal && this.fire( 'getData', eventData ); return eventData.dataValue; }, /** * Gets the "raw data" currently available in the editor. This is a * fast method which returns the data as is, without processing, so it is * not recommended to use it on resulting pages. Instead it can be used * combined with the {@link #method-loadSnapshot} method in order * to automatically save the editor data from time to time * while the user is using the editor, to avoid data loss, without risking * performance issues. * * alert( editor.getSnapshot() ); * * See also: * * * {@link CKEDITOR.editor#method-getData}. * * @returns {String} Editor "raw data". */ getSnapshot: function() { var data = this.fire( 'getSnapshot' ); if ( typeof data != 'string' ) { var element = this.element; if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { data = element.is( 'textarea' ) ? element.getValue() : element.getHtml(); } else { // If we don't have a proper element, set data to an empty string, // as this method is expected to return a string. (http://dev.ckeditor.com/ticket/13385) data = ''; } } return data; }, /** * Loads "raw data" into the editor. The data is loaded with processing * straight to the editing area. It should not be used as a way to load * any kind of data, but instead in combination with * {@link #method-getSnapshot}-produced data. * * var data = editor.getSnapshot(); * editor.loadSnapshot( data ); * * @see CKEDITOR.editor#setData */ loadSnapshot: function( snapshot ) { this.fire( 'loadSnapshot', snapshot ); }, /** * Sets the editor data. The data must be provided in the "raw" format (HTML). * * Note that this method is asynchronous. The `callback` parameter must * be used if interaction with the editor is needed after setting the data. * * CKEDITOR.instances.editor1.setData( '

This is the editor data.

' ); * * CKEDITOR.instances.editor1.setData( '

Some other editor data.

', { * callback: function() { * this.checkDirty(); // true * } * } ); * * Note: In **CKEditor 4.4.2** the signature of this method has changed. All arguments * except `data` were wrapped into the `options` object. However, backward compatibility * was preserved and it is still possible to use the `data, callback, internal` arguments. * * * @param {String} data The HTML code to replace current editor content. * @param {Object} [options] * @param {Boolean} [options.internal=false] Whether to suppress any event firing when copying data internally inside the editor. * @param {Function} [options.callback] Function to be called after `setData` is completed (on {@link #dataReady}). * @param {Boolean} [options.noSnapshot=false] If set to `true`, it will prevent recording an undo snapshot. * Introduced in CKEditor 4.4.2. */ setData: function( data, options, internal ) { var fireSnapshot = true, // Backward compatibility. callback = options, eventData; if ( options && typeof options == 'object' ) { internal = options.internal; callback = options.callback; fireSnapshot = !options.noSnapshot; } if ( !internal && fireSnapshot ) this.fire( 'saveSnapshot' ); if ( callback || !internal ) { this.once( 'dataReady', function( evt ) { if ( !internal && fireSnapshot ) this.fire( 'saveSnapshot' ); if ( callback ) callback.call( evt.editor ); } ); } // Fire "setData" so data manipulation may happen. eventData = { dataValue: data }; !internal && this.fire( 'setData', eventData ); this._.data = eventData.dataValue; !internal && this.fire( 'afterSetData', eventData ); }, /** * Puts or restores the editor into the read-only state. When in read-only, * the user is not able to change the editor content, but can still use * some editor features. This function sets the {@link #property-readOnly} * property of the editor, firing the {@link #event-readOnly} event. * * **Note:** The current editing area will be reloaded. * * @since 3.6 * @param {Boolean} [isReadOnly] Indicates that the editor must go * read-only (`true`, default) or be restored and made editable (`false`). */ setReadOnly: function( isReadOnly ) { isReadOnly = ( isReadOnly == null ) || isReadOnly; if ( this.readOnly != isReadOnly ) { this.readOnly = isReadOnly; // Block or release BACKSPACE key according to current read-only // state to prevent browser's history navigation (http://dev.ckeditor.com/ticket/9761). this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly; this.editable().setReadOnly( isReadOnly ); // Fire the readOnly event so the editor features can update // their state accordingly. this.fire( 'readOnly' ); } }, /** * Inserts HTML code into the currently selected position in the editor in WYSIWYG mode. * * Example: * * CKEDITOR.instances.editor1.insertHtml( '

This is a new paragraph.

' ); * * Fires the {@link #event-insertHtml} and {@link #event-afterInsertHtml} events. The HTML is inserted * in the {@link #event-insertHtml} event's listener with a default priority (10) so you can add listeners with * lower or higher priorities in order to execute some code before or after the HTML is inserted. * * @param {String} html HTML code to be inserted into the editor. * @param {String} [mode='html'] The mode in which the HTML code will be inserted. One of the following: * * * `'html'` – The inserted content will completely override the styles at the selected position. * * `'unfiltered_html'` – Like `'html'` but the content is not filtered with {@link CKEDITOR.filter}. * * `'text'` – The inserted content will inherit the styles applied in * the selected position. This mode should be used when inserting "htmlified" plain text * (HTML without inline styles and styling elements like ``, ``, ``). * * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case). * Introduced in CKEditor 4.5. */ insertHtml: function( html, mode, range ) { this.fire( 'insertHtml', { dataValue: html, mode: mode, range: range } ); }, /** * Inserts text content into the currently selected position in the * editor in WYSIWYG mode. The styles of the selected element will be applied to the inserted text. * Spaces around the text will be left untouched. * * CKEDITOR.instances.editor1.insertText( ' line1 \n\n line2' ); * * Fires the {@link #event-insertText} and {@link #event-afterInsertHtml} events. The text is inserted * in the {@link #event-insertText} event's listener with a default priority (10) so you can add listeners with * lower or higher priorities in order to execute some code before or after the text is inserted. * * @since 3.5 * @param {String} text Text to be inserted into the editor. */ insertText: function( text ) { this.fire( 'insertText', text ); }, /** * Inserts an element into the currently selected position in the editor in WYSIWYG mode. * * var element = CKEDITOR.dom.element.createFromHtml( '' ); * CKEDITOR.instances.editor1.insertElement( element ); * * Fires the {@link #event-insertElement} event. The element is inserted in the listener with a default priority (10), * so you can add listeners with lower or higher priorities in order to execute some code before or after * the element is inserted. * * @param {CKEDITOR.dom.element} element The element to be inserted into the editor. */ insertElement: function( element ) { this.fire( 'insertElement', element ); }, /** * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment} * or a string). This method is designed to work as the user would expect the copy functionality to work. * For instance, if the following selection was made: * *

ab{c}de

* * The following HTML will be returned: * * c * * As you can see, the information about the bold formatting was preserved, even though the selection was * anchored inside the `` element. * * See also: * * * the {@link #extractSelectedHtml} method, * * the {@link CKEDITOR.editable#getHtmlFromRange} method. * * @since 4.5 * @param {Boolean} [toString] If `true`, then stringified HTML will be returned. * @returns {CKEDITOR.dom.documentFragment/String} */ getSelectedHtml: function( toString ) { var editable = this.editable(), selection = this.getSelection(), ranges = selection && selection.getRanges(); if ( !editable || !ranges || ranges.length === 0 ) { return null; } var docFragment = createDocumentFragmentFromRanges( ranges, editable ); return toString ? docFragment.getHtml() : docFragment; }, /** * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment} * or a string) and removes the selected part of the DOM. This method is designed to work as the user would * expect the cut and delete functionalities to work. * * See also: * * * the {@link #getSelectedHtml} method, * * the {@link CKEDITOR.editable#extractHtmlFromRange} method. * * @since 4.5 * @param {Boolean} [toString] If `true`, then stringified HTML will be returned. * @param {Boolean} [removeEmptyBlock=false] Default `false` means that the function will keep an empty block (if the * entire content was removed) or it will create it (if a block element was removed) and set the selection in that block. * If `true`, the empty block will be removed or not created. In this case the function will not handle the selection. * @returns {CKEDITOR.dom.documentFragment/String/null} */ extractSelectedHtml: function( toString, removeEmptyBlock ) { var editable = this.editable(), ranges = this.getSelection().getRanges(), docFragment = new CKEDITOR.dom.documentFragment(), i; if ( !editable || ranges.length === 0 ) { return null; } for ( i = 0; i < ranges.length; i++ ) { docFragment.append( editable.extractHtmlFromRange( ranges[ i ], removeEmptyBlock ) ); } if ( !removeEmptyBlock ) { this.getSelection().selectRanges( [ ranges[ 0 ] ] ); } return toString ? docFragment.getHtml() : docFragment; }, /** * Moves the selection focus to the editing area space in the editor. */ focus: function() { this.fire( 'beforeFocus' ); }, /** * Checks whether the current editor content contains changes when * compared to the content loaded into the editor at startup, or to * the content available in the editor when {@link #resetDirty} * was called. * * function beforeUnload( evt ) { * if ( CKEDITOR.instances.editor1.checkDirty() ) * return evt.returnValue = "You will lose the changes made in the editor."; * } * * if ( window.addEventListener ) * window.addEventListener( 'beforeunload', beforeUnload, false ); * else * window.attachEvent( 'onbeforeunload', beforeUnload ); * * @returns {Boolean} `true` if the content contains changes. */ checkDirty: function() { return this.status == 'ready' && this._.previousValue !== this.getSnapshot(); }, /** * Resets the "dirty state" of the editor so subsequent calls to * {@link #checkDirty} will return `false` if the user will not * have made further changes to the content. * * alert( editor.checkDirty() ); // e.g. true * editor.resetDirty(); * alert( editor.checkDirty() ); // false */ resetDirty: function() { this._.previousValue = this.getSnapshot(); }, /** * Updates the `