command.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module core/command
  7. */
  8. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  9. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  10. /**
  11. * The base class for CKEditor commands.
  12. *
  13. * Commands are the main way to manipulate editor contents and state. They are mostly used by UI elements (or by other
  14. * commands) to make changes in the model. Commands are available in every part of code that has access to
  15. * the {@link module:core/editor/editor~Editor editor} instance.
  16. *
  17. * Instances of registered commands can be retrieved from {@link module:core/editor/editor~Editor#commands `editor.commands`}.
  18. * The easiest way to execute a command is through {@link module:core/editor/editor~Editor#execute `editor.execute()`}.
  19. *
  20. * By default commands are disabled when the editor is in {@link module:core/editor/editor~Editor#isReadOnly read-only} mode.
  21. *
  22. * @mixes module:utils/observablemixin~ObservableMixin
  23. */
  24. export default class Command {
  25. /**
  26. * Creates a new `Command` instance.
  27. *
  28. * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
  29. */
  30. constructor( editor ) {
  31. /**
  32. * The editor on which this command will be used.
  33. *
  34. * @readonly
  35. * @member {module:core/editor/editor~Editor}
  36. */
  37. this.editor = editor;
  38. /**
  39. * The value of the command. A concrete command class should define what it represents for it.
  40. *
  41. * For example, the `'bold'` command's value indicates whether the selection starts in a bolded text.
  42. * And the value of the `'link'` command may be an object with links details.
  43. *
  44. * It is possible for a command to have no value (e.g. for stateless actions such as `'imageUpload'`).
  45. *
  46. * A concrete command class should control this value by overriding the {@link #refresh `refresh()`} method.
  47. *
  48. * @observable
  49. * @readonly
  50. * @member #value
  51. */
  52. this.set( 'value', undefined );
  53. /**
  54. * Flag indicating whether a command is enabled or disabled.
  55. * A disabled command will do nothing when executed.
  56. *
  57. * A concrete command class should control this value by overriding the {@link #refresh `refresh()`} method.
  58. *
  59. * It is possible to disable a command from "outside". For instance, in your integration you may want to disable
  60. * a certain set of commands for the time being. To do that, you can use the fact that `isEnabled` is observable
  61. * and it fires the `set:isEnabled` event every time anyone tries to modify its value:
  62. *
  63. * function disableCommand( cmd ) {
  64. * cmd.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
  65. *
  66. * cmd.isEnabled = false;
  67. *
  68. * // Make it possible to enable the command again.
  69. * return () => {
  70. * cmd.off( 'set:isEnabled', forceDisable );
  71. * cmd.refresh();
  72. * };
  73. *
  74. * function forceDisable( evt ) {
  75. * evt.return = false;
  76. * evt.stop();
  77. * }
  78. * }
  79. *
  80. * // Usage:
  81. *
  82. * // Disabling the command.
  83. * const enableBold = disableCommand( editor.commands.get( 'bold' ) );
  84. *
  85. * // Enabling the command again.
  86. * enableBold();
  87. *
  88. * @observable
  89. * @readonly
  90. * @member {Boolean} #isEnabled
  91. */
  92. this.set( 'isEnabled', false );
  93. /**
  94. * Holds identifiers for {@link #disable}/{@link #enable} mechanism.
  95. *
  96. * @type {Set.<String>}
  97. * @private
  98. */
  99. this._disableStack = new Set();
  100. this.decorate( 'execute' );
  101. // By default every command is refreshed when changes are applied to the model.
  102. this.listenTo( this.editor.model.document, 'change', () => {
  103. this.refresh();
  104. } );
  105. this.on( 'execute', evt => {
  106. if ( !this.isEnabled ) {
  107. evt.stop();
  108. }
  109. }, { priority: 'high' } );
  110. // By default commands are disabled when the editor is in read-only mode.
  111. this.listenTo( editor, 'change:isReadOnly', ( evt, name, value ) => {
  112. if ( value ) {
  113. this.forceDisabled( 'Command:readOnlyMode' );
  114. } else {
  115. this.clearForceDisabled( 'Command:readOnlyMode' );
  116. }
  117. } );
  118. }
  119. /**
  120. * Refreshes the command. The command should update its {@link #isEnabled} and {@link #value} properties
  121. * in this method.
  122. *
  123. * This method is automatically called when
  124. * {@link module:engine/model/document~Document#event:change any changes are applied to the document}.
  125. */
  126. refresh() {
  127. this.isEnabled = true;
  128. }
  129. /**
  130. * Disables the command.
  131. *
  132. * Command may become disabled by multiple features or algorithms (at once). When disabling a command, unique id should be passed
  133. * (e.g. feature name). The same identifier should be used when {@link #clearForceDisabled enabling back} the command.
  134. * The command becomes enabled only after all features {@link #clearForceDisabled enabled it back}.
  135. *
  136. * Disabling and enabling a command:
  137. *
  138. * command.isEnabled; // -> true
  139. * command.forceDisabled( 'MyFeature' );
  140. * command.isEnabled; // -> false
  141. * command.clearForceDisabled( 'MyFeature' );
  142. * command.isEnabled; // -> true
  143. *
  144. * Command disabled by multiple features:
  145. *
  146. * command.forceDisabled( 'MyFeature' );
  147. * command.forceDisabled( 'OtherFeature' );
  148. * command.clearForceDisabled( 'MyFeature' );
  149. * command.isEnabled; // -> false
  150. * command.clearForceDisabled( 'OtherFeature' );
  151. * command.isEnabled; // -> true
  152. *
  153. * Multiple disabling with the same identifier is redundant:
  154. *
  155. * command.forceDisabled( 'MyFeature' );
  156. * command.forceDisabled( 'MyFeature' );
  157. * command.clearForceDisabled( 'MyFeature' );
  158. * command.isEnabled; // -> true
  159. *
  160. * **Note:** some commands or algorithms may have more complex logic when it comes to enabling or disabling certain commands,
  161. * so the command might be still disabled after {@link #clearForceDisabled} was used.
  162. *
  163. * @param {String} id Unique identifier for disabling. Use the same id when {@link #clearForceDisabled enabling back} the command.
  164. */
  165. forceDisabled( id ) {
  166. this._disableStack.add( id );
  167. if ( this._disableStack.size == 1 ) {
  168. this.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
  169. this.isEnabled = false;
  170. }
  171. }
  172. /**
  173. * Clears forced disable previously set through {@link #clearForceDisabled}. See {@link #clearForceDisabled}.
  174. *
  175. * @param {String} id "Disable stack" identifier.
  176. */
  177. clearForceDisabled( id ) {
  178. this._disableStack.delete( id );
  179. if ( this._disableStack.size == 0 ) {
  180. this.off( 'set:isEnabled', forceDisable );
  181. this.refresh();
  182. }
  183. }
  184. /**
  185. * Executes the command.
  186. *
  187. * A command may accept parameters. They will be passed from {@link module:core/editor/editor~Editor#execute `editor.execute()`}
  188. * to the command.
  189. *
  190. * The `execute()` method will automatically abort when the command is disabled ({@link #isEnabled} is `false`).
  191. * This behavior is implemented by a high priority listener to the {@link #event:execute} event.
  192. *
  193. * In order to see how to disable a command from "outside" see the {@link #isEnabled} documentation.
  194. *
  195. * @fires execute
  196. */
  197. execute() {}
  198. /**
  199. * Destroys the command.
  200. */
  201. destroy() {
  202. this.stopListening();
  203. }
  204. /**
  205. * Event fired by the {@link #execute} method. The command action is a listener to this event so it's
  206. * possible to change/cancel the behavior of the command by listening to this event.
  207. *
  208. * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
  209. *
  210. * **Note:** This event is fired even if command is disabled. However, it is automatically blocked
  211. * by a high priority listener in order to prevent command execution.
  212. *
  213. * @event execute
  214. */
  215. }
  216. mix( Command, ObservableMixin );
  217. // Helper function that forces command to be disabled.
  218. function forceDisable( evt ) {
  219. evt.return = false;
  220. evt.stop();
  221. }