8
0
Просмотр исходного кода

First batch of changes – CommandCollection and new Command class.

Piotrek Koszuliński 8 лет назад
Родитель
Сommit
f9df5d8515

+ 117 - 0
packages/ckeditor5-core/src/command.js

@@ -0,0 +1,117 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @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}.
+ * The shortest way to execute a command is through {@link module:core/editor/editor~Editor#execute}.
+ *
+ * @implements CommandInterface
+ * @mixes module:utils/observablemixin~ObservaleMixin
+ */
+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;
+
+		this.set( 'value', null );
+		this.set( 'isEnabled', false );
+
+		this.decorate( 'execute' );
+
+		// By default every command is refreshed when changes are applied to the model.
+		this.listenTo( this.editor.document, 'changesDone', () => {
+			this.refresh();
+		} );
+	}
+
+	/**
+	 * Destroys the command.
+	 */
+	destroy() {
+		this.stopListening();
+	}
+}
+
+mix( Command, ObservableMixin );
+
+/**
+ * The command interface. Usually implemented by inheriting from the {@link Command base `Command` class}.
+ *
+ * @interface CommandInterface
+ */
+
+/**
+ * Flag indicating whether a command is enabled or disabled.
+ * A disabled command should do nothing when executed.
+ *
+ * @observable
+ * @readonly
+ * @member {Boolean} #isEnabled
+ */
+
+/**
+ * The value of a command. Concrete command class should define what it represents.
+ *
+ * For example, the `bold` command's value is whether the selection starts in a bolded text.
+ * And the value of the `link` command may be an object with links details.
+ *
+ * It's possible for a command to have no value (e.g. for stateless actions such as `uploadImage`).
+ *
+ * @observable
+ * @readonly
+ * @member #value
+ */
+
+/**
+ * Executes the command.
+ *
+ * A command may accept parameters. They will be passed from {@link module:core/editor/editor~Editor#execute}
+ * to the command.
+ *
+ * The `execute()` method should abort when the command is disabled ({@link #isEnabled} is `false`).
+ *
+ * @method #execute
+ */
+
+/**
+ * Refreshes the command. The command should update its {@link #isEnabled} and {@link #value} property
+ * in this method.
+ *
+ * @method #refresh
+ */
+
+/**
+ * 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.
+ *
+ * @event execute
+ */

+ 0 - 146
packages/ckeditor5-core/src/command/command.js

@@ -1,146 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module core/command/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 main way to manipulate editor contents and state. They are mostly used by UI elements (or by other
- * commands) to make changes in Tree Model. Commands are available in every part of code that has access to
- * {@link module:core/editor/editor~Editor} instance, since they are registered in it and executed through
- * {@link module:core/editor/editor~Editor#execute}.
- * Commands instances are available through {@link module:core/editor/editor~Editor#commands}.
- *
- * This is an abstract base class for all commands.
- *
- * @mixes module:utils/observablemixin~ObservaleMixin
- */
-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 ) {
-		/**
-		 * Editor on which this command will be used.
-		 *
-		 * @readonly
-		 * @member {module:core/editor/editor~Editor} #editor
-		 */
-		this.editor = editor;
-
-		/**
-		 * Flag indicating whether a command is enabled or disabled.
-		 * A disabled command should do nothing upon it's execution.
-		 *
-		 * @observable
-		 * @member {Boolean} #isEnabled
-		 */
-		this.set( 'isEnabled', true );
-
-		// If schema checking function is specified, add it to the `refreshState` listeners.
-		// Feature will be disabled if it does not apply to schema requirements.
-		if ( this._checkEnabled ) {
-			this.on( 'refreshState', ( evt, data ) => {
-				data.isEnabled = this._checkEnabled();
-			} );
-		}
-	}
-
-	destroy() {
-		this.stopListening();
-	}
-
-	/**
-	 * Fires `refreshState` event and checks it's resolve value to decide whether command should be enabled or not.
-	 * Other parts of code might listen to `refreshState` event on this command and add their callbacks. This
-	 * way the responsibility of deciding whether a command should be enabled is shared.
-	 *
-	 * @fires refreshState
-	 */
-	refreshState() {
-		const data = { isEnabled: true };
-		this.fire( 'refreshState', data );
-
-		this.isEnabled = data.isEnabled;
-	}
-
-	/**
-	 * Executes the command if it is enabled.
-	 *
-	 * @protected
-	 * @param {*} param Parameter passed to {@link module:core/editor/editor~Editor#execute execute} method of this command.
-	 */
-	_execute( param ) {
-		if ( this.isEnabled ) {
-			this._doExecute( param );
-		}
-	}
-
-	/**
-	 * Disables the command. This should be used only by the command itself. Other parts of code should add
-	 * listeners to `refreshState` event.
-	 *
-	 * @protected
-	 */
-	_disable() {
-		this.on( 'refreshState', disableCallback );
-		this.refreshState();
-	}
-
-	/**
-	 * Enables the command (internally). This should be used only by the command itself. Command will be enabled if
-	 * other listeners does not return false on `refreshState` event callbacks. Firing {@link #_enable}
-	 * does not guarantee that {@link #isEnabled} will be set to true, as it depends on other listeners.
-	 *
-	 * @protected
-	 */
-	_enable() {
-		this.off( 'refreshState', disableCallback );
-		this.refreshState();
-	}
-
-	/**
-	 * Executes command.
-	 * This is an abstract method that should be overwritten in child classes.
-	 *
-	 * @protected
-	 */
-	_doExecute() {}
-
-	/**
-	 * Checks if a command should be enabled according to its own rules. Mostly it will check schema to see if the command
-	 * is allowed to be executed in given position. This method can be defined in child class (but is not obligatory).
-	 * If it is defined, it will be added as a callback to `refreshState` event.
-	 *
-	 * @protected
-	 * @method #_checkEnabled
-	 * @returns {Boolean} `true` if command should be enabled according to
-	 * {@link module:engine/model/document~Document#schema}. `false` otherwise.
-	 */
-}
-
-function disableCallback( evt, data ) {
-	data.isEnabled = false;
-}
-
-mix( Command, ObservableMixin );
-
-/**
- * Fired whenever command has to have its {@link #isEnabled} property refreshed. Every feature,
- * command or other class which needs to disable command (set `isEnabled` to `false`) should listen to this
- * event.
- *
- * @event refreshState
- * @param {Object} data
- * @param {Boolean} [data.isEnabled=true]
- */

+ 112 - 0
packages/ckeditor5-core/src/commandcollection.js

@@ -0,0 +1,112 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module core/commandcollection
+ */
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Collection of commands. Its instance is available in {@module:core/editor/editor~Editor#commands}.
+ */
+export default class CommandCollection {
+	/**
+	 * Creates collection instance.
+	 */
+	constructor() {
+		/**
+		 * Command map.
+		 *
+		 * @private
+		 * @member {Map}
+		 */
+		this._commands = new Map();
+	}
+
+	/**
+	 * Registers a new command.
+	 *
+	 * @param {String} commandName The name of the command.
+	 * @param {module:core/command~CommandInterface} command
+	 */
+	add( commandName, command ) {
+		this._commands.set( commandName, command );
+	}
+
+	/**
+	 * Retrieves a command from the collection.
+	 *
+	 * @param {String} commandName The name of the command.
+	 * @returns {module:core/command~CommandInterface}
+	 */
+	get( commandName ) {
+		this._commands.get( commandName );
+	}
+
+	/**
+	 * Executes a command.
+	 *
+	 * @param {String} commandName The name of the command.
+	 */
+	execute( commandName, ...args ) {
+		const command = this.get( commandName );
+
+		if ( !command ) {
+			/**
+			 * Command does not exist.
+			 *
+			 * @error commandcollection-command-not-found
+			 * @param {String} commandName Name of the command.
+			 */
+			throw new CKEditorError( 'commandcollection-command-not-found: Command does not exist.', { commandName } );
+		}
+
+		command.execute( ...args );
+	}
+
+	/**
+	 * Returns iterator of command names.
+	 *
+	 * @returns {Iterator.<String>}
+	 */
+	* names() {
+		yield* this._commands.keys();
+	}
+
+	/**
+	 * Returns iterator of command instances.
+	 *
+	 * @returns {Iterator.<module:core/command~CommandInterface>}
+	 */
+	* commands() {
+		yield* this._commands.values();
+	}
+
+	/**
+	 * Collection iterator.
+	 */
+	[ Symbol.iterator ]() {
+		return this._commands[ Symbol.iterator ]();
+	}
+
+	/**
+	 * Refreshes all commands.
+	 */
+	refreshAll() {
+		for ( const command of this.commands() ) {
+			command.refresh();
+		}
+	}
+
+	/**
+	 * Destroys the collection and all its commands.
+	 */
+	destroy() {
+		for ( const command of this.commands() ) {
+			command.destroy();
+		}
+	}
+}

+ 16 - 20
packages/ckeditor5-core/src/editor/editor.js

@@ -7,14 +7,14 @@
  * @module core/editor/editor
  */
 
-import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import Config from '@ckeditor/ckeditor5-utils/src/config';
 import PluginCollection from '../plugincollection';
+import CommandCollection from './commandcollection';
 import Locale from '@ckeditor/ckeditor5-utils/src/locale';
 import DataController from '@ckeditor/ckeditor5-engine/src/controller/datacontroller';
 import Document from '@ckeditor/ckeditor5-engine/src/model/document';
 
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
@@ -55,9 +55,9 @@ export default class Editor {
 		 * Commands registered to the editor.
 		 *
 		 * @readonly
-		 * @member {Map.<module:core/command/command~Command>}
+		 * @member {module:core/command/commandcollection~CommandCollection}
 		 */
-		this.commands = new Map();
+		this.commands = new CommandCollection();
 
 		/**
 		 * @readonly
@@ -140,13 +140,16 @@ export default class Editor {
 	/**
 	 * Destroys the editor instance, releasing all resources used by it.
 	 *
-	 * @fires module:core/editor/editor~Editor#destroy
+	 * @fires destroy
 	 * @returns {Promise} A promise that resolves once the editor instance is fully destroyed.
 	 */
 	destroy() {
 		this.fire( 'destroy' );
+
 		this.stopListening();
 
+		this.commands.destroy();
+
 		return Promise.resolve()
 			.then( () => {
 				this.document.destroy();
@@ -155,24 +158,17 @@ export default class Editor {
 	}
 
 	/**
-	 * Executes specified command with given parameter.
+	 * Executes specified command with given parameters.
+	 *
+	 * Shorthand for:
+	 *
+	 *		editor.commands.get( commandName ).execute( ... )
 	 *
 	 * @param {String} commandName Name of command to execute.
-	 * @param {*} [commandParam] If set, command will be executed with this parameter.
+	 * @param {*} [...commandParams] Command parameters.
 	 */
-	execute( commandName, commandParam ) {
-		const command = this.commands.get( commandName );
-
-		if ( !command ) {
-			/**
-			 * Specified command has not been added to the editor.
-			 *
-			 * @error editor-command-not-found
-			 */
-			throw new CKEditorError( 'editor-command-not-found: Specified command has not been added to the editor.' );
-		}
-
-		command._execute( commandParam );
+	execute( ...args ) {
+		this.commands.execute( ...args );
 	}
 
 	/**

+ 0 - 162
packages/ckeditor5-core/tests/command/command.js

@@ -1,162 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Editor from '../../src/editor/editor';
-import Command from '../../src/command/command';
-
-describe( 'Command', () => {
-	let editor, command;
-
-	class CommandWithSchema extends Command {
-		constructor( editor, schemaValid ) {
-			super( editor );
-
-			this.schemaValid = schemaValid;
-		}
-
-		_checkEnabled() {
-			return this.schemaValid;
-		}
-	}
-
-	beforeEach( () => {
-		editor = new Editor();
-		command = new Command( editor );
-	} );
-
-	afterEach( () => {
-		// Might be redundant if editor destroys the commands.
-		command.destroy();
-		editor.destroy();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create a new command instance, that is enabled and bound to given editor', () => {
-			expect( command ).to.have.property( 'editor' ).equal( editor );
-			expect( command.isEnabled ).to.be.true;
-		} );
-
-		it( 'Command should have _doExecute method', () => {
-			expect( () => {
-				command._doExecute();
-			} ).not.to.throw;
-		} );
-
-		it( 'should add listener to its refreshState event if checkSchema method is present', () => {
-			expect( command._checkEnabled ).to.be.undefined;
-
-			command._checkEnabled = sinon.spy();
-			command.refreshState();
-
-			expect( command._checkEnabled.called ).to.be.false;
-
-			const newCommand = new CommandWithSchema( editor, true );
-			sinon.spy( newCommand, '_checkEnabled' );
-
-			newCommand.refreshState();
-
-			expect( newCommand._checkEnabled.calledOnce ).to.be.true;
-		} );
-	} );
-
-	describe( 'destroy', () => {
-		it( 'should stop listening', () => {
-			sinon.spy( command, 'stopListening' );
-
-			command.destroy();
-
-			expect( command.stopListening.calledOnce ).to.be.true;
-		} );
-	} );
-
-	describe( 'refreshState', () => {
-		it( 'should fire refreshState event', () => {
-			const spy = sinon.spy();
-
-			command.on( 'refreshState', spy );
-			command.refreshState();
-
-			expect( spy.called ).to.be.true;
-		} );
-
-		it( 'should set isEnabled property to the value passed by object-reference', () => {
-			command.on( 'refreshState', ( evt, data ) => {
-				data.isEnabled = true;
-			} );
-
-			expect( command.isEnabled ).to.be.true;
-		} );
-
-		it( 'should set isEnabled to false if _checkEnabled returns false', () => {
-			const disabledCommand = new CommandWithSchema( editor, false );
-
-			disabledCommand.refreshState();
-
-			expect( disabledCommand.isEnabled ).to.be.false;
-		} );
-	} );
-
-	describe( 'disable', () => {
-		it( 'should make command disabled', () => {
-			command._disable();
-
-			expect( command.isEnabled ).to.be.false;
-		} );
-
-		it( 'should not make command disabled if there is a high-priority listener forcing command to be enabled', () => {
-			command.on( 'refreshState', evt => {
-				evt.stop();
-
-				return true;
-			}, command, 1 );
-
-			command._disable();
-
-			expect( command.isEnabled ).to.be.true;
-		} );
-	} );
-
-	describe( 'enable', () => {
-		it( 'should make command enabled if it was previously disabled by disable()', () => {
-			command._disable();
-			command._enable();
-
-			expect( command.isEnabled ).to.be.true;
-		} );
-
-		it( 'should not make command enabled if there are other listeners disabling it', () => {
-			command._disable();
-
-			command.on( 'refreshState', ( evt, data ) => {
-				data.isEnabled = false;
-			} );
-
-			command.refreshState();
-			command._enable();
-
-			expect( command.isEnabled ).to.be.false;
-		} );
-	} );
-
-	describe( '_execute', () => {
-		it( 'should not execute command if it is disabled', () => {
-			command._disable();
-
-			sinon.spy( command, '_doExecute' );
-
-			command._execute();
-
-			expect( command._doExecute.called ).to.be.false;
-		} );
-
-		it( 'should execute command if it is enabled', () => {
-			sinon.spy( command, '_doExecute' );
-
-			command._execute();
-
-			expect( command._doExecute.called ).to.be.true;
-		} );
-	} );
-} );