1
+ class CallbackRegistry {
2
+ #callbacks = { } ;
3
+ add ( name , callback ) {
4
+ if ( ! this . #callbacks[ name ] ) {
5
+ this . #callbacks[ name ] = [ ] ;
6
+ }
7
+ this . #callbacks[ name ] . push ( callback ) ;
8
+ }
9
+ remove ( name , callback ) {
10
+ if ( ! this . #callbacks[ name ] ) {
11
+ return [ ] ;
12
+ }
13
+ const callbacks = this . #callbacks[ name ] ;
14
+ if ( callback ) {
15
+ // Remove a specific callback
16
+ return this . #callbacks[ name ] = callbacks . filter ( ( cb ) => cb !== callback ) ;
17
+ }
18
+ // Remove all callbacks
19
+ this . #callbacks[ name ] = [ ] ;
20
+ return callbacks ;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * An R-backed implementation of the @anywidget/types AnyModel interface.
26
+ *
27
+ * @see {@link https://github.com/manzt/anywidget/tree/main/packages/types }
28
+ */
1
29
class AnyModel {
30
+ /** @type {Record<string, any> } */
31
+ #state;
32
+ /** @type {string } */
33
+ #ns_id;
34
+ /** @type {WebSocket | undefined } */
35
+ #ws = undefined ;
36
+ /** @type {EventTarget } */
37
+ #target = new EventTarget ( ) ;
38
+ /** @type {CallbackRegistry } */
39
+ #callbacks = new CallbackRegistry ( ) ;
40
+ /** @type {Set<string> } */
41
+ #unsavedKeys = new Set ( ) ;
42
+
43
+ /**
44
+ * @param {Record<string, any> } state - initial model state
45
+ * @param {string } ns_id - the Shiny namespace ID
46
+ * @param {WebSocket } [ws] - a WebSocket connection
47
+ */
2
48
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 ( ) ;
49
+ this . #ns_id = ns_id ;
50
+ this . #state = state ;
51
+ this . #ws = ws ;
8
52
}
53
+ /** @param {string } name */
9
54
get ( name ) {
10
- return this . state [ name ] ;
55
+ return this . # state[ name ] ;
11
56
}
57
+ /**
58
+ * @param {string } key
59
+ * @param {any } value
60
+ */
12
61
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
- ) ;
62
+ this . #state[ key ] = value ;
63
+ this . #unsavedKeys. add ( key ) ;
64
+ this . #target. dispatchEvent (
65
+ new CustomEvent ( `change:${ key } ` , { detail : value } ) ,
66
+ ) ;
67
+ this . #target. dispatchEvent (
68
+ new CustomEvent ( "change" , { detail : value } ) ,
69
+ ) ;
18
70
}
71
+ /**
72
+ * @param {string } name
73
+ * @param {Function } callback
74
+ */
19
75
on ( name , callback ) {
20
- this . target . addEventListener ( name , callback ) ;
76
+ this . #target. addEventListener ( name , callback ) ;
77
+ this . #callbacks. add ( name , callback ) ;
21
78
}
22
- off ( name ) {
23
- // Not yet implemented
79
+ /**
80
+ * @param {string } name
81
+ * @param {Function } [callback]
82
+ */
83
+ off ( name , callback ) {
84
+ for ( const cb of this . #callbacks. remove ( name , callback ) ) {
85
+ this . #target. removeEventListener ( name , cb ) ;
86
+ }
87
+ }
88
+ /**
89
+ * @param {any } msg
90
+ * @param {unknown } [callbacks]
91
+ * @param {ArrayBuffer[] } [buffers]
92
+ */
93
+ send ( msg , callbacks , buffers ) {
94
+ // TODO: implement
95
+ console . error ( `model.send is not yet implemented for anyhtmlwidget` ) ;
24
96
}
25
97
save_changes ( ) {
26
98
const unsavedState = Object . fromEntries (
27
- Array . from ( this . unsavedKeys . values ( ) )
28
- . map ( key => ( [ key , this . state [ key ] ] ) )
99
+ Array . from ( this . # unsavedKeys. values ( ) )
100
+ . map ( ( key ) => [ key , this . # state[ key ] ] ) ,
29
101
) ;
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 ( {
102
+ this . #unsavedKeys = new Set ( ) ;
103
+ if ( window && window . Shiny && window . Shiny . setInputValue ) {
104
+ const eventPrefix = this . #ns_id ? `${ this . #ns_id} -` : "" ;
105
+ Shiny . setInputValue (
106
+ `${ eventPrefix } anyhtmlwidget_on_save_changes` ,
107
+ unsavedState ,
108
+ ) ;
109
+ } else if ( this . #ws) {
110
+ this . #ws. send ( JSON . stringify ( {
36
111
type : "on_save_changes" ,
37
112
payload : unsavedState ,
38
113
} ) ) ;
@@ -41,82 +116,85 @@ class AnyModel {
41
116
}
42
117
43
118
function emptyElement ( el ) {
44
- while ( el . firstChild ) {
45
- el . removeChild ( el . firstChild ) ;
46
- }
119
+ while ( el . firstChild ) {
120
+ el . removeChild ( el . firstChild ) ;
121
+ }
47
122
}
48
123
49
124
HTMLWidgets . widget ( {
50
- name : 'anyhtmlwidget' ,
51
- type : 'output' ,
52
- factory : function ( el , width , height ) {
53
-
125
+ name : "anyhtmlwidget" ,
126
+ type : "output" ,
127
+ factory : function ( el , width , height ) {
54
128
let widget ;
55
129
let model ;
56
130
let cleanup ;
57
131
let ws ;
58
132
59
133
return {
60
- renderValue : async function ( x ) {
61
- if ( cleanup && typeof cleanup === "function" ) {
134
+ renderValue : async function ( x ) {
135
+ if ( cleanup && typeof cleanup === "function" ) {
62
136
cleanup ( ) ;
63
137
cleanup = undefined ;
64
- if ( ws ) {
138
+ if ( ws ) {
65
139
ws . close ( ) ;
66
140
ws = undefined ;
67
141
}
68
142
}
69
- // The default can either be an object like { render, initialize }
70
- // or a function that returns this object.
71
- if ( ! widget ) {
72
- const esm = x . esm ;
73
- const url = URL . createObjectURL ( new Blob ( [ esm ] , { type : "text/javascript" } ) ) ;
74
- const mod = await import ( /* webpackIgnore: true */ url ) ;
75
- URL . revokeObjectURL ( url ) ;
143
+ // The default can either be an object like { render, initialize }
144
+ // or a function that returns this object.
145
+ if ( ! widget ) {
146
+ const esm = x . esm ;
147
+ const url = URL . createObjectURL (
148
+ new Blob ( [ esm ] , { type : "text/javascript" } ) ,
149
+ ) ;
150
+ const mod = await import ( /* webpackIgnore: true */ url ) ;
151
+ URL . revokeObjectURL ( url ) ;
76
152
77
- widget = typeof mod . default === "function"
78
- ? await mod . default ( )
79
- : mod . default ;
153
+ widget = typeof mod . default === "function"
154
+ ? await mod . default ( )
155
+ : mod . default ;
80
156
81
- // TODO: initialize here
82
- }
157
+ // TODO: initialize here
158
+ }
83
159
84
- if ( x . port && x . host && ! window . Shiny ) {
85
- ws = new WebSocket ( `ws://${ x . host } :${ x . port } ` ) ;
86
- }
160
+ if ( x . port && x . host && ! window . Shiny ) {
161
+ ws = new WebSocket ( `ws://${ x . host } :${ x . port } ` ) ;
162
+ }
87
163
88
- model = new AnyModel ( x . values , x . ns_id , ws ) ;
164
+ model = new AnyModel ( x . values , x . ns_id , ws ) ;
89
165
90
- if ( window && window . Shiny && window . Shiny . addCustomMessageHandler ) {
91
- const eventPrefix = x . ns_id ? `${ x . ns_id } -` : '' ;
92
- Shiny . addCustomMessageHandler ( `${ eventPrefix } anyhtmlwidget_on_change` , ( { key, value } ) => {
93
- model . set ( key , value ) ;
94
- } ) ;
95
- } else if ( x . port && x . host ) {
96
- ws . onmessage = ( event ) => {
97
- const { type, payload } = JSON . parse ( event . data ) ;
98
- if ( type === "on_change" ) {
99
- const { key, value } = payload ;
100
- model . set ( key , value ) ;
101
- }
102
- } ;
103
- }
166
+ if ( window && window . Shiny && window . Shiny . addCustomMessageHandler ) {
167
+ const eventPrefix = x . ns_id ? `${ x . ns_id } -` : "" ;
168
+ Shiny . addCustomMessageHandler (
169
+ `${ eventPrefix } anyhtmlwidget_on_change` ,
170
+ ( { key, value } ) => {
171
+ model . set ( key , value ) ;
172
+ } ,
173
+ ) ;
174
+ } else if ( x . port && x . host ) {
175
+ ws . onmessage = ( event ) => {
176
+ const { type, payload } = JSON . parse ( event . data ) ;
177
+ if ( type === "on_change" ) {
178
+ const { key, value } = payload ;
179
+ model . set ( key , value ) ;
180
+ }
181
+ } ;
182
+ }
104
183
105
- try {
106
- emptyElement ( el ) ;
184
+ try {
185
+ emptyElement ( el ) ;
107
186
// Register cleanup function.
108
- cleanup = await widget . render ( { model, el, width, height } ) ;
109
-
110
- } catch ( e ) {
111
- // TODO: re-throw error
112
- }
187
+ cleanup = await widget . render ( { model, el, width, height } ) ;
188
+ } catch ( e ) {
189
+ // TODO: re-throw error
190
+ }
113
191
} ,
114
- resize : async function ( width , height ) {
192
+ resize : async function ( width , height ) {
115
193
// TODO: emit resize event on window (and let user handle)?
116
- if ( widget ?. resize ) {
194
+ if ( widget ?. resize ) {
117
195
await widget . resize ( { model, el, width, height } ) ;
118
196
}
119
- }
197
+ } ,
120
198
} ;
121
- }
199
+ } ,
122
200
} ) ;
0 commit comments