|
| 1 | +class CallbackRegistry { |
| 2 | + /** @type {Record<string, Function[]>} */ |
| 3 | + #callbacks = {}; |
| 4 | + /** |
| 5 | + * @param {string} name |
| 6 | + * @param {Function} callback |
| 7 | + */ |
| 8 | + add(name, callback) { |
| 9 | + if (!this.#callbacks[name]) { |
| 10 | + this.#callbacks[name] = []; |
| 11 | + } |
| 12 | + this.#callbacks[name].push(callback); |
| 13 | + } |
| 14 | + /** |
| 15 | + * @param {string} name |
| 16 | + * @param {Function} [callback] - a specific callback to remove |
| 17 | + * @returns {Function[]} - the removed callbacks |
| 18 | + */ |
| 19 | + remove(name, callback) { |
| 20 | + if (!this.#callbacks[name]) { |
| 21 | + return []; |
| 22 | + } |
| 23 | + const callbacks = this.#callbacks[name]; |
| 24 | + if (callback) { |
| 25 | + // Remove a specific callback |
| 26 | + return this.#callbacks[name] = callbacks.filter((cb) => cb !== callback); |
| 27 | + } |
| 28 | + // Remove all callbacks |
| 29 | + this.#callbacks[name] = []; |
| 30 | + return callbacks; |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +/** |
| 35 | + * An R-backed implementation of the @anywidget/types AnyModel interface. |
| 36 | + * |
| 37 | + * @see {@link https://github.com/manzt/anywidget/tree/main/packages/types} |
| 38 | + */ |
1 | 39 | class AnyModel {
|
| 40 | + /** @type {Record<string, any>} */ |
| 41 | + #state; |
| 42 | + /** @type {string} */ |
| 43 | + #ns_id; |
| 44 | + /** @type {WebSocket | undefined} */ |
| 45 | + #ws = undefined; |
| 46 | + /** @type {EventTarget} */ |
| 47 | + #target = new EventTarget(); |
| 48 | + /** @type {CallbackRegistry} */ |
| 49 | + #callbacks = new CallbackRegistry(); |
| 50 | + /** @type {Set<string>} */ |
| 51 | + #unsavedKeys = new Set(); |
| 52 | + |
| 53 | + /** |
| 54 | + * @param {Record<string, any>} state - initial model state |
| 55 | + * @param {string} ns_id - the Shiny namespace ID |
| 56 | + * @param {WebSocket} [ws] - a WebSocket connection |
| 57 | + */ |
2 | 58 | constructor(state, ns_id, ws) {
|
3 |
| - this.ns_id = ns_id; |
4 |
| - this.state = state; |
5 |
| - this.target = new EventTarget(); |
6 |
| - this.ws = ws; |
7 |
| - this.unsavedKeys = new Set(); |
| 59 | + this.#ns_id = ns_id; |
| 60 | + this.#state = state; |
| 61 | + this.#ws = ws; |
8 | 62 | }
|
| 63 | + /** @param {string} name */ |
9 | 64 | get(name) {
|
10 |
| - return this.state[name]; |
| 65 | + return this.#state[name]; |
11 | 66 | }
|
| 67 | + /** |
| 68 | + * @param {string} key |
| 69 | + * @param {any} value |
| 70 | + */ |
12 | 71 | set(key, value) {
|
13 |
| - this.state[key] = value; |
14 |
| - this.unsavedKeys.add(key); |
15 |
| - this.target.dispatchEvent( |
16 |
| - new CustomEvent(`change:${key}`, { detail: value }), |
17 |
| - ); |
| 72 | + this.#state[key] = value; |
| 73 | + this.#unsavedKeys.add(key); |
| 74 | + this.#target.dispatchEvent( |
| 75 | + new CustomEvent(`change:${key}`, { detail: value }), |
| 76 | + ); |
| 77 | + this.#target.dispatchEvent( |
| 78 | + new CustomEvent("change", { detail: value }), |
| 79 | + ); |
18 | 80 | }
|
| 81 | + /** |
| 82 | + * @param {string} name |
| 83 | + * @param {Function} callback |
| 84 | + */ |
19 | 85 | on(name, callback) {
|
20 |
| - this.target.addEventListener(name, callback); |
| 86 | + this.#target.addEventListener(name, callback); |
| 87 | + this.#callbacks.add(name, callback); |
| 88 | + } |
| 89 | + /** |
| 90 | + * @param {string} name |
| 91 | + * @param {Function} [callback] |
| 92 | + */ |
| 93 | + off(name, callback) { |
| 94 | + for (const cb of this.#callbacks.remove(name, callback)) { |
| 95 | + this.#target.removeEventListener(name, cb); |
| 96 | + } |
21 | 97 | }
|
22 |
| - off(name) { |
23 |
| - // Not yet implemented |
| 98 | + /** |
| 99 | + * @param {any} msg |
| 100 | + * @param {unknown} [callbacks] |
| 101 | + * @param {ArrayBuffer[]} [buffers] |
| 102 | + */ |
| 103 | + send(msg, callbacks, buffers) { |
| 104 | + // TODO: impeThrow? |
| 105 | + console.error(`model.send is not yet implemented for anyhtmlwidget`); |
24 | 106 | }
|
25 | 107 | save_changes() {
|
26 | 108 | const unsavedState = Object.fromEntries(
|
27 |
| - Array.from(this.unsavedKeys.values()) |
28 |
| - .map(key => ([key, this.state[key]])) |
| 109 | + Array.from(this.#unsavedKeys.values()) |
| 110 | + .map((key) => [key, this.#state[key]]), |
29 | 111 | );
|
30 |
| - this.unsavedKeys = new Set(); |
31 |
| - if(window && window.Shiny && window.Shiny.setInputValue) { |
32 |
| - const eventPrefix = this.ns_id ? `${this.ns_id}-` : ''; |
33 |
| - Shiny.setInputValue(`${eventPrefix}anyhtmlwidget_on_save_changes`, unsavedState); |
34 |
| - } else if(this.ws) { |
35 |
| - this.ws.send(JSON.stringify({ |
| 112 | + this.#unsavedKeys = new Set(); |
| 113 | + if (window && window.Shiny && window.Shiny.setInputValue) { |
| 114 | + const eventPrefix = this.#ns_id ? `${this.#ns_id}-` : ""; |
| 115 | + Shiny.setInputValue( |
| 116 | + `${eventPrefix}anyhtmlwidget_on_save_changes`, |
| 117 | + unsavedState, |
| 118 | + ); |
| 119 | + } else if (this.#ws) { |
| 120 | + this.#ws.send(JSON.stringify({ |
36 | 121 | type: "on_save_changes",
|
37 | 122 | payload: unsavedState,
|
38 | 123 | }));
|
|
0 commit comments