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