commandlinker.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import { JSONExt, ReadonlyPartialJSONObject } from '@lumino/coreutils';
  6. import { IDisposable } from '@lumino/disposable';
  7. import { CommandRegistry } from '@lumino/commands';
  8. import { ElementDataset } from '@lumino/virtualdom';
  9. /**
  10. * The command data attribute added to nodes that are connected.
  11. */
  12. const COMMAND_ATTR = 'commandlinker-command';
  13. /**
  14. * The args data attribute added to nodes that are connected.
  15. */
  16. const ARGS_ATTR = 'commandlinker-args';
  17. /**
  18. * A static class that provides helper methods to generate clickable nodes that
  19. * execute registered commands with pre-populated arguments.
  20. */
  21. export class CommandLinker implements IDisposable {
  22. /**
  23. * Instantiate a new command linker.
  24. */
  25. constructor(options: CommandLinker.IOptions) {
  26. this._commands = options.commands;
  27. document.body.addEventListener('click', this);
  28. }
  29. /**
  30. * Test whether the linker is disposed.
  31. */
  32. get isDisposed(): boolean {
  33. return this._isDisposed;
  34. }
  35. /**
  36. * Dispose of the resources held by the linker.
  37. */
  38. dispose(): void {
  39. if (this.isDisposed) {
  40. return;
  41. }
  42. this._isDisposed = true;
  43. document.body.removeEventListener('click', this);
  44. }
  45. /**
  46. * Connect a command/argument pair to a given node so that when it is clicked,
  47. * the command will execute.
  48. *
  49. * @param node - The node being connected.
  50. *
  51. * @param command - The command ID to execute upon click.
  52. *
  53. * @param args - The arguments with which to invoke the command.
  54. *
  55. * @returns The same node that was passed in, after it has been connected.
  56. *
  57. * #### Notes
  58. * Only `click` events will execute the command on a connected node. So, there
  59. * are two considerations that are relevant:
  60. * 1. If a node is connected, the default click action will be prevented.
  61. * 2. The `HTMLElement` passed in should be clickable.
  62. */
  63. connectNode(
  64. node: HTMLElement,
  65. command: string,
  66. args?: ReadonlyPartialJSONObject
  67. ): HTMLElement {
  68. node.setAttribute(`data-${COMMAND_ATTR}`, command);
  69. if (args !== void 0) {
  70. node.setAttribute(`data-${ARGS_ATTR}`, JSON.stringify(args));
  71. }
  72. return node;
  73. }
  74. /**
  75. * Disconnect a node that has been connected to execute a command on click.
  76. *
  77. * @param node - The node being disconnected.
  78. *
  79. * @returns The same node that was passed in, after it has been disconnected.
  80. *
  81. * #### Notes
  82. * This method is safe to call multiple times and is safe to call on nodes
  83. * that were never connected.
  84. *
  85. * This method can be called on rendered virtual DOM nodes that were populated
  86. * using the `populateVNodeDataset` method in order to disconnect them from
  87. * executing their command/argument pair.
  88. */
  89. disconnectNode(node: HTMLElement): HTMLElement {
  90. node.removeAttribute(`data-${COMMAND_ATTR}`);
  91. node.removeAttribute(`data-${ARGS_ATTR}`);
  92. return node;
  93. }
  94. /**
  95. * Handle the DOM events for the command linker helper class.
  96. *
  97. * @param event - The DOM event sent to the class.
  98. *
  99. * #### Notes
  100. * This method implements the DOM `EventListener` interface and is
  101. * called in response to events on the panel's DOM node. It should
  102. * not be called directly by user code.
  103. */
  104. handleEvent(event: Event): void {
  105. switch (event.type) {
  106. case 'click':
  107. this._evtClick(event as MouseEvent);
  108. break;
  109. default:
  110. return;
  111. }
  112. }
  113. /**
  114. * Populate the `dataset` attribute within the collection of attributes used
  115. * to instantiate a virtual DOM node with the values necessary for its
  116. * rendered DOM node to respond to clicks by executing a command/argument
  117. * pair.
  118. *
  119. * @param command - The command ID to execute upon click.
  120. *
  121. * @param args - The arguments with which to invoke the command.
  122. *
  123. * @returns A `dataset` collection for use within virtual node attributes.
  124. *
  125. * #### Notes
  126. * The return value can be used on its own as the value for the `dataset`
  127. * attribute of a virtual element, or it can be added to an existing `dataset`
  128. * as in the example below.
  129. *
  130. * #### Example
  131. * ```typescript
  132. * let command = 'some:command-id';
  133. * let args = { alpha: 'beta' };
  134. * let anchor = h.a({
  135. * className: 'some-class',
  136. * dataset: {
  137. * foo: '1',
  138. * bar: '2',
  139. * ../...linker.populateVNodeDataset(command, args)
  140. * }
  141. * }, 'some text');
  142. * ```
  143. */
  144. populateVNodeDataset(
  145. command: string,
  146. args?: ReadonlyPartialJSONObject
  147. ): ElementDataset {
  148. let dataset: ElementDataset;
  149. if (args !== void 0) {
  150. dataset = { [ARGS_ATTR]: JSON.stringify(args), [COMMAND_ATTR]: command };
  151. } else {
  152. dataset = { [COMMAND_ATTR]: command };
  153. }
  154. return dataset;
  155. }
  156. /**
  157. * The global click handler that deploys commands/argument pairs that are
  158. * attached to the node being clicked.
  159. */
  160. private _evtClick(event: MouseEvent): void {
  161. let target = event.target as HTMLElement;
  162. while (target && target.parentElement) {
  163. if (target.hasAttribute(`data-${COMMAND_ATTR}`)) {
  164. event.preventDefault();
  165. const command = target.getAttribute(`data-${COMMAND_ATTR}`);
  166. if (!command) {
  167. return;
  168. }
  169. const argsValue = target.getAttribute(`data-${ARGS_ATTR}`);
  170. let args = JSONExt.emptyObject;
  171. if (argsValue) {
  172. args = JSON.parse(argsValue);
  173. }
  174. void this._commands.execute(command, args);
  175. return;
  176. }
  177. target = target.parentElement;
  178. }
  179. }
  180. private _commands: CommandRegistry;
  181. private _isDisposed = false;
  182. }
  183. /**
  184. * A namespace for command linker statics.
  185. */
  186. export namespace CommandLinker {
  187. /**
  188. * The instantiation options for a command linker.
  189. */
  190. export interface IOptions {
  191. /**
  192. * The command registry instance that all linked commands will use.
  193. */
  194. commands: CommandRegistry;
  195. }
  196. }