@@ -131,77 +131,81 @@ export const executeComponent = (
131
131
} ;
132
132
133
133
/**
134
- * Stores the JSX output of the last execution of the component .
134
+ * Adds `useOn` events to the JSX output .
135
135
*
136
- * Component can execute multiple times because:
137
- *
138
- * - Component can have multiple tasks
139
- * - Tasks can track signals
140
- * - Task A can change signal which causes Task B to rerun.
141
- *
142
- * So when executing a component we only care about its last JSX Output.
136
+ * @param jsx The JSX output to modify.
137
+ * @param useOnEvents The `useOn` events to add.
138
+ * @returns The modified JSX output.
143
139
*/
144
-
145
140
function addUseOnEvents (
146
141
jsx : JSXOutput ,
147
142
useOnEvents : UseOnMap
148
143
) : ValueOrPromise < JSXNodeInternal < string > | null | JSXOutput > {
149
- const jsxElement = findFirstStringJSX ( jsx ) ;
144
+ const jsxElement = findFirstElementNode ( jsx ) ;
150
145
let jsxResult = jsx ;
146
+ const qVisibleEvent = 'onQvisible$' ;
151
147
return maybeThen ( jsxElement , ( jsxElement ) => {
152
- let isInvisibleComponent = false ;
153
- if ( ! jsxElement ) {
154
- /**
155
- * We did not find any jsx node with a string tag. This means that we should append:
156
- *
157
- * ```html
158
- * <script type="placeholder" hidden on-document:qinit="..."></script>
159
- * ```
160
- *
161
- * This is needed because use on events should have a node to attach them to.
162
- */
163
- isInvisibleComponent = true ;
164
- }
148
+ // headless components are components that don't render a real DOM element
149
+ const isHeadless = ! jsxElement ;
150
+ // placeholder element is a <script> element that is used to add events to the document or window
151
+ let placeholderElement : JSXNodeInternal < string > | null = null ;
165
152
for ( const key in useOnEvents ) {
166
153
if ( Object . prototype . hasOwnProperty . call ( useOnEvents , key ) ) {
167
- if ( isInvisibleComponent ) {
168
- if ( key === 'onQvisible$' ) {
169
- const [ jsxElement , jsx ] = addScriptNodeForInvisibleComponents ( jsxResult ) ;
170
- jsxResult = jsx ;
171
- if ( jsxElement ) {
172
- addUseOnEvent ( jsxElement , 'document:onQinit$' , useOnEvents [ key ] ) ;
173
- }
174
- } else if (
154
+ let targetElement = jsxElement ;
155
+ let eventKey = key ;
156
+
157
+ if ( isHeadless ) {
158
+ // if the component is headless, we need to add the event to the placeholder element
159
+ if (
160
+ key === qVisibleEvent ||
175
161
key . startsWith ( EventNameJSXScope . document ) ||
176
162
key . startsWith ( EventNameJSXScope . window )
177
163
) {
178
- const [ jsxElement , jsx ] = addScriptNodeForInvisibleComponents ( jsxResult ) ;
179
- jsxResult = jsx ;
180
- if ( jsxElement ) {
181
- addUseOnEvent ( jsxElement , key , useOnEvents [ key ] ) ;
164
+ if ( ! placeholderElement ) {
165
+ const [ createdElement , newJsx ] = injectPlaceholderElement ( jsxResult ) ;
166
+ jsxResult = newJsx ;
167
+ placeholderElement = createdElement ;
168
+ }
169
+ targetElement = placeholderElement ;
170
+ } else {
171
+ if ( isDev ) {
172
+ logWarn (
173
+ 'You are trying to add an event "' +
174
+ key +
175
+ '" using `useOn` hook, ' +
176
+ 'but a node to which you can add an event is not found. ' +
177
+ 'Please make sure that the component has a valid element node. '
178
+ ) ;
182
179
}
183
- } else if ( isDev ) {
180
+ continue ;
181
+ }
182
+ }
183
+ if ( targetElement ) {
184
+ if ( targetElement . type === 'script' && key === qVisibleEvent ) {
185
+ eventKey = 'document:onQinit$' ;
184
186
logWarn (
185
187
'You are trying to add an event "' +
186
188
key +
187
- '" using `useOn ` hook, ' +
189
+ '" using `useVisibleTask$ ` hook, ' +
188
190
'but a node to which you can add an event is not found. ' +
189
- 'Please make sure that the component has a valid element node. '
191
+ 'Using document:onQinit$ instead. '
190
192
) ;
191
193
}
192
- } else if ( jsxElement ) {
193
- if ( jsxElement . type === 'script' && key === 'onQvisible$' ) {
194
- addUseOnEvent ( jsxElement , 'document:onQinit$' , useOnEvents [ key ] ) ;
195
- } else {
196
- addUseOnEvent ( jsxElement , key , useOnEvents [ key ] ) ;
197
- }
194
+ addUseOnEvent ( targetElement , eventKey , useOnEvents [ key ] ) ;
198
195
}
199
196
}
200
197
}
201
198
return jsxResult ;
202
199
} ) ;
203
200
}
204
201
202
+ /**
203
+ * Adds an event to the JSX element.
204
+ *
205
+ * @param jsxElement The JSX element to add the event to.
206
+ * @param key The event name.
207
+ * @param value The event value.
208
+ */
205
209
function addUseOnEvent (
206
210
jsxElement : JSXNodeInternal ,
207
211
key : string ,
@@ -221,7 +225,13 @@ function addUseOnEvent(
221
225
props [ key ] = propValue ;
222
226
}
223
227
224
- function findFirstStringJSX ( jsx : JSXOutput ) : ValueOrPromise < JSXNodeInternal < string > | null > {
228
+ /**
229
+ * Finds the first element node in the JSX output.
230
+ *
231
+ * @param jsx The JSX output to search.
232
+ * @returns The first element node or null if no element node is found.
233
+ */
234
+ function findFirstElementNode ( jsx : JSXOutput ) : ValueOrPromise < JSXNodeInternal < string > | null > {
225
235
const queue : any [ ] = [ jsx ] ;
226
236
while ( queue . length ) {
227
237
const jsx = queue . shift ( ) ;
@@ -234,45 +244,67 @@ function findFirstStringJSX(jsx: JSXOutput): ValueOrPromise<JSXNodeInternal<stri
234
244
queue . push ( ...jsx ) ;
235
245
} else if ( isPromise ( jsx ) ) {
236
246
return maybeThen < JSXOutput , JSXNodeInternal < string > | null > ( jsx , ( jsx ) =>
237
- findFirstStringJSX ( jsx )
247
+ findFirstElementNode ( jsx )
238
248
) ;
239
249
} else if ( isSignal ( jsx ) ) {
240
- return findFirstStringJSX ( untrack ( ( ) => jsx . value as JSXOutput ) ) ;
250
+ return findFirstElementNode ( untrack ( ( ) => jsx . value as JSXOutput ) ) ;
241
251
}
242
252
}
243
253
return null ;
244
254
}
245
255
246
- function addScriptNodeForInvisibleComponents (
256
+ /**
257
+ * Injects a placeholder <script> element into the JSX output.
258
+ *
259
+ * This is necessary for headless components (components that don't render a real DOM element) to
260
+ * have an anchor point for `useOn` event listeners that target the document or window.
261
+ *
262
+ * @param jsx The JSX output to modify.
263
+ * @returns A tuple containing the created placeholder element and the modified JSX output.
264
+ */
265
+ function injectPlaceholderElement (
247
266
jsx : JSXOutput
248
267
) : [ JSXNodeInternal < string > | null , JSXOutput | null ] {
268
+ // For regular JSX nodes, we can append the placeholder to its children.
249
269
if ( isJSXNode ( jsx ) ) {
250
- const jsxElement = createScriptNode ( ) ;
270
+ const placeholder = createPlaceholderScriptNode ( ) ;
271
+ // For slots, we can't add children, so we wrap them in a fragment.
251
272
if ( jsx . type === Slot ) {
252
- return [ jsxElement , _jsxSorted ( Fragment , null , null , [ jsx , jsxElement ] , 0 , null ) ] ;
273
+ return [ placeholder , _jsxSorted ( Fragment , null , null , [ jsx , placeholder ] , 0 , null ) ] ;
253
274
}
254
275
255
276
if ( jsx . children == null ) {
256
- jsx . children = jsxElement ;
277
+ jsx . children = placeholder ;
257
278
} else if ( isArray ( jsx . children ) ) {
258
- jsx . children . push ( jsxElement ) ;
279
+ jsx . children . push ( placeholder ) ;
259
280
} else {
260
- jsx . children = [ jsx . children , jsxElement ] ;
281
+ jsx . children = [ jsx . children , placeholder ] ;
261
282
}
262
- return [ jsxElement , jsx ] ;
263
- } else if ( isArray ( jsx ) && jsx . length ) {
264
- // get first element
265
- const [ jsxElement , _ ] = addScriptNodeForInvisibleComponents ( jsx [ 0 ] ) ;
266
- return [ jsxElement , jsx ] ;
267
- } else if ( isPrimitive ( jsx ) ) {
268
- const jsxElement = createScriptNode ( ) ;
269
- return [ jsxElement , _jsxSorted ( Fragment , null , null , [ jsx , jsxElement ] , 0 , null ) ] ;
283
+ return [ placeholder , jsx ] ;
270
284
}
271
285
286
+ // For primitives, we can't add children, so we wrap them in a fragment.
287
+ if ( isPrimitive ( jsx ) ) {
288
+ const placeholder = createPlaceholderScriptNode ( ) ;
289
+ return [ placeholder , _jsxSorted ( Fragment , null , null , [ jsx , placeholder ] , 0 , null ) ] ;
290
+ }
291
+
292
+ // For an array of nodes, we inject the placeholder into the first element.
293
+ if ( isArray ( jsx ) && jsx . length > 0 ) {
294
+ const [ createdElement , _ ] = injectPlaceholderElement ( jsx [ 0 ] ) ;
295
+ return [ createdElement , jsx ] ;
296
+ }
297
+
298
+ // For anything else we do nothing.
272
299
return [ null , jsx ] ;
273
300
}
274
301
275
- function createScriptNode ( ) : JSXNodeInternal < string > {
302
+ /**
303
+ * Creates a <script> element with a placeholder type.
304
+ *
305
+ * @returns A <script> element with a placeholder type.
306
+ */
307
+ function createPlaceholderScriptNode ( ) : JSXNodeInternal < string > {
276
308
return new JSXNodeImpl (
277
309
'script' ,
278
310
{ } ,
0 commit comments