/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ /** * @fileOverview Increase and Decrease Indent commands. */ ( function() { 'use strict'; var TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED, TRISTATE_OFF = CKEDITOR.TRISTATE_OFF; CKEDITOR.plugins.add( 'indent', { // jscs:disable maximumLineLength lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE% // jscs:enable maximumLineLength icons: 'indent,indent-rtl,outdent,outdent-rtl', // %REMOVE_LINE_CORE% hidpi: true, // %REMOVE_LINE_CORE% init: function( editor ) { var genericDefinition = CKEDITOR.plugins.indent.genericDefinition; // Register generic commands. setupGenericListeners( editor, editor.addCommand( 'indent', new genericDefinition( true ) ) ); setupGenericListeners( editor, editor.addCommand( 'outdent', new genericDefinition() ) ); // Create and register toolbar button if possible. if ( editor.ui.addButton ) { editor.ui.addButton( 'Indent', { label: editor.lang.indent.indent, command: 'indent', directional: true, toolbar: 'indent,20' } ); editor.ui.addButton( 'Outdent', { label: editor.lang.indent.outdent, command: 'outdent', directional: true, toolbar: 'indent,10' } ); } // Register dirChanged listener. editor.on( 'dirChanged', function( evt ) { var range = editor.createRange(), dataNode = evt.data.node; range.setStartBefore( dataNode ); range.setEndAfter( dataNode ); var walker = new CKEDITOR.dom.walker( range ), node; while ( ( node = walker.next() ) ) { if ( node.type == CKEDITOR.NODE_ELEMENT ) { // A child with the defined dir is to be ignored. if ( !node.equals( dataNode ) && node.getDirection() ) { range.setStartAfter( node ); walker = new CKEDITOR.dom.walker( range ); continue; } // Switch alignment classes. var classes = editor.config.indentClasses; if ( classes ) { var suffix = ( evt.data.dir == 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ]; for ( var i = 0; i < classes.length; i++ ) { if ( node.hasClass( classes[ i ] + suffix[ 0 ] ) ) { node.removeClass( classes[ i ] + suffix[ 0 ] ); node.addClass( classes[ i ] + suffix[ 1 ] ); } } } // Switch the margins. var marginLeft = node.getStyle( 'margin-right' ), marginRight = node.getStyle( 'margin-left' ); marginLeft ? node.setStyle( 'margin-left', marginLeft ) : node.removeStyle( 'margin-left' ); marginRight ? node.setStyle( 'margin-right', marginRight ) : node.removeStyle( 'margin-right' ); } } } ); } } ); /** * Global command class definitions and global helpers. * * @class * @singleton */ CKEDITOR.plugins.indent = { /** * A base class for a generic command definition, responsible mainly for creating * Increase Indent and Decrease Indent toolbar buttons as well as for refreshing * UI states. * * Commands of this class do not perform any indentation by themselves. They * delegate this job to content-specific indentation commands (i.e. indentlist). * * @class CKEDITOR.plugins.indent.genericDefinition * @extends CKEDITOR.commandDefinition * @param {CKEDITOR.editor} editor The editor instance this command will be * applied to. * @param {String} name The name of the command. * @param {Boolean} [isIndent] Defines the command as indenting or outdenting. */ genericDefinition: function( isIndent ) { /** * Determines whether the command belongs to the indentation family. * Otherwise it is assumed to be an outdenting command. * * @readonly * @property {Boolean} [=false] */ this.isIndent = !!isIndent; // Mimic naive startDisabled behavior for outdent. this.startDisabled = !this.isIndent; }, /** * A base class for specific indentation command definitions responsible for * handling a pre-defined set of elements i.e. indentlist for lists or * indentblock for text block elements. * * Commands of this class perform indentation operations and modify the DOM structure. * They listen for events fired by {@link CKEDITOR.plugins.indent.genericDefinition} * and execute defined actions. * * **NOTE**: This is not an {@link CKEDITOR.command editor command}. * Context-specific commands are internal, for indentation system only. * * @class CKEDITOR.plugins.indent.specificDefinition * @param {CKEDITOR.editor} editor The editor instance this command will be * applied to. * @param {String} name The name of the command. * @param {Boolean} [isIndent] Defines the command as indenting or outdenting. */ specificDefinition: function( editor, name, isIndent ) { this.name = name; this.editor = editor; /** * An object of jobs handled by the command. Each job consists * of two functions: `refresh` and `exec` as well as the execution priority. * * * The `refresh` function determines whether a job is doable for * a particular context. These functions are executed in the * order of priorities, one by one, for all plugins that registered * jobs. As jobs are related to generic commands, refreshing * occurs when the global command is firing the `refresh` event. * * **Note**: This function must return either {@link CKEDITOR#TRISTATE_DISABLED} * or {@link CKEDITOR#TRISTATE_OFF}. * * * The `exec` function modifies the DOM if possible. Just like * `refresh`, `exec` functions are executed in the order of priorities * while the generic command is executed. This function is not executed * if `refresh` for this job returned {@link CKEDITOR#TRISTATE_DISABLED}. * * **Note**: This function must return a Boolean value, indicating whether it * was successful. If a job was successful, then no other jobs are being executed. * * Sample definition: * * command.jobs = { * // Priority = 20. * '20': { * refresh( editor, path ) { * if ( condition ) * return CKEDITOR.TRISTATE_OFF; * else * return CKEDITOR.TRISTATE_DISABLED; * }, * exec( editor ) { * // DOM modified! This was OK. * return true; * } * }, * // Priority = 60. This job is done later. * '60': { * // Another job. * } * }; * * For additional information, please check comments for * the `setupGenericListeners` function. * * @readonly * @property {Object} [={}] */ this.jobs = {}; /** * Determines whether the editor that the command belongs to has * {@link CKEDITOR.config#enterMode config.enterMode} set to {@link CKEDITOR#ENTER_BR}. * * @readonly * @see CKEDITOR.config#enterMode * @property {Boolean} [=false] */ this.enterBr = editor.config.enterMode == CKEDITOR.ENTER_BR; /** * Determines whether the command belongs to the indentation family. * Otherwise it is assumed to be an outdenting command. * * @readonly * @property {Boolean} [=false] */ this.isIndent = !!isIndent; /** * The name of the global command related to this one. * * @readonly */ this.relatedGlobal = isIndent ? 'indent' : 'outdent'; /** * A keystroke associated with this command (*Tab* or *Shift+Tab*). * * @readonly */ this.indentKey = isIndent ? 9 : CKEDITOR.SHIFT + 9; /** * Stores created markers for the command so they can eventually be * purged after the `exec` function is run. */ this.database = {}; }, /** * Registers content-specific commands as a part of the indentation system * directed by generic commands. Once a command is registered, * it listens for events of a related generic command. * * CKEDITOR.plugins.indent.registerCommands( editor, { * 'indentlist': new indentListCommand( editor, 'indentlist' ), * 'outdentlist': new indentListCommand( editor, 'outdentlist' ) * } ); * * Content-specific commands listen for the generic command's `exec` and * try to execute their own jobs, one after another. If some execution is * successful, `evt.data.done` is set so no more jobs (commands) are involved. * * Content-specific commands also listen for the generic command's `refresh` * and fill the `evt.data.states` object with states of jobs. A generic command * uses this data to determine its own state and to update the UI. * * @member CKEDITOR.plugins.indent * @param {CKEDITOR.editor} editor The editor instance this command is * applied to. * @param {Object} commands An object of {@link CKEDITOR.command}. */ registerCommands: function( editor, commands ) { editor.on( 'pluginsLoaded', function() { for ( var name in commands ) { ( function( editor, command ) { var relatedGlobal = editor.getCommand( command.relatedGlobal ); for ( var priority in command.jobs ) { // Observe generic exec event and execute command when necessary. // If the command was successfully handled by the command and // DOM has been modified, stop event propagation so no other plugin // will bother. Job is done. relatedGlobal.on( 'exec', function( evt ) { if ( evt.data.done ) return; // Make sure that anything this command will do is invisible // for undoManager. What undoManager only can see and // remember is the execution of the global command (relatedGlobal). editor.fire( 'lockSnapshot' ); if ( command.execJob( editor, priority ) ) evt.data.done = true; editor.fire( 'unlockSnapshot' ); // Clean up the markers. CKEDITOR.dom.element.clearAllMarkers( command.database ); }, this, null, priority ); // Observe generic refresh event and force command refresh. // Once refreshed, save command state in event data // so generic command plugin can update its own state and UI. relatedGlobal.on( 'refresh', function( evt ) { if ( !evt.data.states ) evt.data.states = {}; evt.data.states[ command.name + '@' + priority ] = command.refreshJob( editor, priority, evt.data.path ); }, this, null, priority ); } // Since specific indent commands have no UI elements, // they need to be manually registered as a editor feature. editor.addFeature( command ); } )( this, commands[ name ] ); } } ); } }; CKEDITOR.plugins.indent.genericDefinition.prototype = { context: 'p', exec: function() {} }; CKEDITOR.plugins.indent.specificDefinition.prototype = { /** * Executes the content-specific procedure if the context is correct. * It calls the `exec` function of a job of the given `priority` * that modifies the DOM. * * @param {CKEDITOR.editor} editor The editor instance this command * will be applied to. * @param {Number} priority The priority of the job to be executed. * @returns {Boolean} Indicates whether the job was successful. */ execJob: function( editor, priority ) { var job = this.jobs[ priority ]; if ( job.state != TRISTATE_DISABLED ) return job.exec.call( this, editor ); }, /** * Calls the `refresh` function of a job of the given `priority`. * The function returns the state of the job which can be either * {@link CKEDITOR#TRISTATE_DISABLED} or {@link CKEDITOR#TRISTATE_OFF}. * * @param {CKEDITOR.editor} editor The editor instance this command * will be applied to. * @param {Number} priority The priority of the job to be executed. * @returns {Number} The state of the job. */ refreshJob: function( editor, priority, path ) { var job = this.jobs[ priority ]; if ( !editor.activeFilter.checkFeature( this ) ) job.state = TRISTATE_DISABLED; else job.state = job.refresh.call( this, editor, path ); return job.state; }, /** * Checks if the element path contains the element handled * by this indentation command. * * @param {CKEDITOR.dom.elementPath} node A path to be checked. * @returns {CKEDITOR.dom.element} */ getContext: function( path ) { return path.contains( this.context ); } }; /** * Attaches event listeners for this generic command. Since the indentation * system is event-oriented, generic commands communicate with * content-specific commands using the `exec` and `refresh` events. * * Listener priorities are crucial. Different indentation phases * are executed with different priorities. * * For the `exec` event: * * * 0: Selection and bookmarks are saved by the generic command. * * 1-99: Content-specific commands try to indent the code by executing * their own jobs ({@link CKEDITOR.plugins.indent.specificDefinition#jobs}). * * 100: Bookmarks are re-selected by the generic command. * * The visual interpretation looks as follows: * * +------------------+ * | Exec event fired | * +------ + ---------+ * | * 0 -<----------+ Selection and bookmarks saved. * | * | * 25 -<---+ Exec 1st job of plugin#1 (return false, continuing...). * | * | * 50 -<---+ Exec 1st job of plugin#2 (return false, continuing...). * | * | * 75 -<---+ Exec 2nd job of plugin#1 (only if plugin#2 failed). * | * | * 100 -<-----------+ Re-select bookmarks, clean-up. * | * +-------- v ----------+ * | Exec event finished | * +---------------------+ * * For the `refresh` event: * * * <100: Content-specific commands refresh their job states according * to the given path. Jobs save their states in the `evt.data.states` object * passed along with the event. This can be either {@link CKEDITOR#TRISTATE_DISABLED} * or {@link CKEDITOR#TRISTATE_OFF}. * * 100: Command state is determined according to what states * have been returned by content-specific jobs (`evt.data.states`). * UI elements are updated at this stage. * * **Note**: If there is at least one job with the {@link CKEDITOR#TRISTATE_OFF} state, * then the generic command state is also {@link CKEDITOR#TRISTATE_OFF}. Otherwise, * the command state is {@link CKEDITOR#TRISTATE_DISABLED}. * * @param {CKEDITOR.command} command The command to be set up. * @private */ function setupGenericListeners( editor, command ) { var selection, bookmarks; // Set the command state according to content-specific // command states. command.on( 'refresh', function( evt ) { // If no state comes with event data, disable command. var states = [ TRISTATE_DISABLED ]; for ( var s in evt.data.states ) states.push( evt.data.states[ s ] ); this.setState( CKEDITOR.tools.search( states, TRISTATE_OFF ) ? TRISTATE_OFF : TRISTATE_DISABLED ); }, command, null, 100 ); // Initialization. Save bookmarks and mark event as not handled // by any plugin (command) yet. command.on( 'exec', function( evt ) { selection = editor.getSelection(); bookmarks = selection.createBookmarks( 1 ); // Mark execution as not handled yet. if ( !evt.data ) evt.data = {}; evt.data.done = false; }, command, null, 0 ); // Housekeeping. Make sure selectionChange will be called. // Also re-select previously saved bookmarks. command.on( 'exec', function() { editor.forceNextSelectionCheck(); selection.selectBookmarks( bookmarks ); }, command, null, 100 ); } } )();