ckeditor/ckeditor5-core

View on GitHub
src/command.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module core/command
 */

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

/**
 * The base class for CKEditor commands.
 *
 * Commands are the main way to manipulate editor contents and state. They are mostly used by UI elements (or by other
 * commands) to make changes in the model. Commands are available in every part of code that has access to
 * the {@link module:core/editor/editor~Editor editor} instance.
 *
 * Instances of registered commands can be retrieved from {@link module:core/editor/editor~Editor#commands `editor.commands`}.
 * The easiest way to execute a command is through {@link module:core/editor/editor~Editor#execute `editor.execute()`}.
 *
 * By default commands are disabled when the editor is in {@link module:core/editor/editor~Editor#isReadOnly read-only} mode.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
export default class Command {
    /**
     * Creates a new `Command` instance.
     *
     * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
     */
    constructor( editor ) {
        /**
         * The editor on which this command will be used.
         *
         * @readonly
         * @member {module:core/editor/editor~Editor}
         */
        this.editor = editor;

        /**
         * The value of the command. A concrete command class should define what it represents for it.
         *
         * For example, the `'bold'` command's value indicates whether the selection starts in a bolded text.
         * And the value of the `'link'` command may be an object with links details.
         *
         * It is possible for a command to have no value (e.g. for stateless actions such as `'imageUpload'`).
         *
         * A concrete command class should control this value by overriding the {@link #refresh `refresh()`} method.
         *
         * @observable
         * @readonly
         * @member #value
         */
        this.set( 'value', undefined );

        /**
         * Flag indicating whether a command is enabled or disabled.
         * A disabled command will do nothing when executed.
         *
         * A concrete command class should control this value by overriding the {@link #refresh `refresh()`} method.
         *
         * It is possible to disable a command from "outside". For instance, in your integration you may want to disable
         * a certain set of commands for the time being. To do that, you can use the fact that `isEnabled` is observable
         * and it fires the `set:isEnabled` event every time anyone tries to modify its value:
         *
         *        function disableCommand( cmd ) {
         *            cmd.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
         *
         *            cmd.isEnabled = false;
         *
         *            // Make it possible to enable the command again.
         *            return () => {
         *                cmd.off( 'set:isEnabled', forceDisable );
         *                cmd.refresh();
         *            };
         *
         *            function forceDisable( evt ) {
         *                evt.return = false;
         *                evt.stop();
         *            }
         *        }
         *
         *        // Usage:
         *
         *        // Disabling the command.
         *        const enableBold = disableCommand( editor.commands.get( 'bold' ) );
         *
         *        // Enabling the command again.
         *        enableBold();
         *
         * @observable
         * @readonly
         * @member {Boolean} #isEnabled
         */
        this.set( 'isEnabled', false );

        /**
         * Holds identifiers for {@link #forceDisabled} mechanism.
         *
         * @type {Set.<String>}
         * @private
         */
        this._disableStack = new Set();

        this.decorate( 'execute' );

        // By default every command is refreshed when changes are applied to the model.
        this.listenTo( this.editor.model.document, 'change', () => {
            this.refresh();
        } );

        this.on( 'execute', evt => {
            if ( !this.isEnabled ) {
                evt.stop();
            }
        }, { priority: 'high' } );

        // By default commands are disabled when the editor is in read-only mode.
        this.listenTo( editor, 'change:isReadOnly', ( evt, name, value ) => {
            if ( value ) {
                this.forceDisabled( 'readOnlyMode' );
            } else {
                this.clearForceDisabled( 'readOnlyMode' );
            }
        } );
    }

    /**
     * Refreshes the command. The command should update its {@link #isEnabled} and {@link #value} properties
     * in this method.
     *
     * This method is automatically called when
     * {@link module:engine/model/document~Document#event:change any changes are applied to the document}.
     */
    refresh() {
        this.isEnabled = true;
    }

    /**
     * Disables the command.
     *
     * Command may be disabled by multiple features or algorithms (at once). When disabling a command, unique id should be passed
     * (e.g. feature name). The same identifier should be used when {@link #clearForceDisabled enabling back} the command.
     * The command becomes enabled only after all features {@link #clearForceDisabled enabled it back}.
     *
     * Disabling and enabling a command:
     *
     *        command.isEnabled; // -> true
     *        command.forceDisabled( 'MyFeature' );
     *        command.isEnabled; // -> false
     *        command.clearForceDisabled( 'MyFeature' );
     *        command.isEnabled; // -> true
     *
     * Command disabled by multiple features:
     *
     *        command.forceDisabled( 'MyFeature' );
     *        command.forceDisabled( 'OtherFeature' );
     *        command.clearForceDisabled( 'MyFeature' );
     *        command.isEnabled; // -> false
     *        command.clearForceDisabled( 'OtherFeature' );
     *        command.isEnabled; // -> true
     *
     * Multiple disabling with the same identifier is redundant:
     *
     *        command.forceDisabled( 'MyFeature' );
     *        command.forceDisabled( 'MyFeature' );
     *        command.clearForceDisabled( 'MyFeature' );
     *        command.isEnabled; // -> true
     *
     * **Note:** some commands or algorithms may have more complex logic when it comes to enabling or disabling certain commands,
     * so the command might be still disabled after {@link #clearForceDisabled} was used.
     *
     * @param {String} id Unique identifier for disabling. Use the same id when {@link #clearForceDisabled enabling back} the command.
     */
    forceDisabled( id ) {
        this._disableStack.add( id );

        if ( this._disableStack.size == 1 ) {
            this.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
            this.isEnabled = false;
        }
    }

    /**
     * Clears forced disable previously set through {@link #forceDisabled}. See {@link #forceDisabled}.
     *
     * @param {String} id Unique identifier, equal to the one passed in {@link #forceDisabled} call.
     */
    clearForceDisabled( id ) {
        this._disableStack.delete( id );

        if ( this._disableStack.size == 0 ) {
            this.off( 'set:isEnabled', forceDisable );
            this.refresh();
        }
    }

    /**
     * Executes the command.
     *
     * A command may accept parameters. They will be passed from {@link module:core/editor/editor~Editor#execute `editor.execute()`}
     * to the command.
     *
     * The `execute()` method will automatically abort when the command is disabled ({@link #isEnabled} is `false`).
     * This behavior is implemented by a high priority listener to the {@link #event:execute} event.
     *
     * In order to see how to disable a command from "outside" see the {@link #isEnabled} documentation.
     *
     * @fires execute
     */
    execute() {}

    /**
     * Destroys the command.
     */
    destroy() {
        this.stopListening();
    }

    /**
     * Event fired by the {@link #execute} method. The command action is a listener to this event so it's
     * possible to change/cancel the behavior of the command by listening to this event.
     *
     * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
     *
     * **Note:** This event is fired even if command is disabled. However, it is automatically blocked
     * by a high priority listener in order to prevent command execution.
     *
     * @event execute
     */
}

mix( Command, ObservableMixin );

// Helper function that forces command to be disabled.
function forceDisable( evt ) {
    evt.return = false;
    evt.stop();
}