@@ -58,16 +58,20 @@ export function RegularElement(node, context) {
58
58
return ;
59
59
}
60
60
61
+ const is_custom_element = is_custom_element_node ( node ) ;
62
+
63
+ if ( is_custom_element ) {
64
+ // cloneNode is faster, but it does not instantiate the underlying class of the
65
+ // custom element until the template is connected to the dom, which would
66
+ // cause problems when setting properties on the custom element.
67
+ // Therefore we need to use importNode instead, which doesn't have this caveat.
68
+ context . state . metadata . context . template_needs_import_node = true ;
69
+ }
70
+
61
71
if ( node . name === 'script' ) {
62
72
context . state . metadata . context . template_contains_script_tag = true ;
63
73
}
64
74
65
- const metadata = context . state . metadata ;
66
- const child_metadata = {
67
- ...context . state . metadata ,
68
- namespace : determine_namespace_for_children ( node , context . state . metadata . namespace )
69
- } ;
70
-
71
75
context . state . template . push ( `<${ node . name } ` ) ;
72
76
73
77
/** @type {Array<AST.Attribute | AST.SpreadAttribute> } */
@@ -79,151 +83,138 @@ export function RegularElement(node, context) {
79
83
/** @type {AST.StyleDirective[] } */
80
84
const style_directives = [ ] ;
81
85
86
+ /** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective> } */
87
+ const other_directives = [ ] ;
88
+
82
89
/** @type {ExpressionStatement[] } */
83
90
const lets = [ ] ;
84
91
85
- const is_custom_element = is_custom_element_node ( node ) ;
86
- let needs_input_reset = false ;
87
- let needs_content_reset = false ;
88
-
89
- /** @type {AST.BindDirective | null } */
90
- let value_binding = null ;
92
+ /** @type {Map<string, AST.Attribute> } */
93
+ const lookup = new Map ( ) ;
91
94
92
- /** If true, needs `__value` for inputs */
93
- let needs_special_value_handling = node . name === 'option' || node . name === 'select' ;
94
- let is_content_editable = false ;
95
- let has_content_editable_binding = false ;
96
- let img_might_be_lazy = false ;
97
- let might_need_event_replaying = false ;
98
- let has_direction_attribute = false ;
99
- let has_style_attribute = false ;
95
+ /** @type {Map<string, AST.BindDirective> } */
96
+ const bindings = new Map ( ) ;
100
97
101
- if ( is_custom_element ) {
102
- // cloneNode is faster, but it does not instantiate the underlying class of the
103
- // custom element until the template is connected to the dom, which would
104
- // cause problems when setting properties on the custom element.
105
- // Therefore we need to use importNode instead, which doesn't have this caveat.
106
- metadata . context . template_needs_import_node = true ;
107
- }
98
+ let has_spread = false ;
99
+ let has_use = false ;
108
100
109
- // visit let directives first, to set state
110
101
for ( const attribute of node . attributes ) {
111
- if ( attribute . type === 'LetDirective' ) {
112
- lets . push ( /** @type {ExpressionStatement } */ ( context . visit ( attribute ) ) ) ;
102
+ switch ( attribute . type ) {
103
+ case 'AnimateDirective' :
104
+ other_directives . push ( attribute ) ;
105
+ break ;
106
+
107
+ case 'Attribute' :
108
+ attributes . push ( attribute ) ;
109
+ lookup . set ( attribute . name , attribute ) ;
110
+ break ;
111
+
112
+ case 'BindDirective' :
113
+ bindings . set ( attribute . name , attribute ) ;
114
+ other_directives . push ( attribute ) ;
115
+ break ;
116
+
117
+ case 'ClassDirective' :
118
+ class_directives . push ( attribute ) ;
119
+ break ;
120
+
121
+ case 'LetDirective' :
122
+ // visit let directives before everything else, to set state
123
+ lets . push ( /** @type {ExpressionStatement } */ ( context . visit ( attribute ) ) ) ;
124
+ break ;
125
+
126
+ case 'OnDirective' :
127
+ other_directives . push ( attribute ) ;
128
+ break ;
129
+
130
+ case 'SpreadAttribute' :
131
+ attributes . push ( attribute ) ;
132
+ has_spread = true ;
133
+ break ;
134
+
135
+ case 'StyleDirective' :
136
+ style_directives . push ( attribute ) ;
137
+ break ;
138
+
139
+ case 'TransitionDirective' :
140
+ other_directives . push ( attribute ) ;
141
+ break ;
142
+
143
+ case 'UseDirective' :
144
+ has_use = true ;
145
+ other_directives . push ( attribute ) ;
146
+ break ;
113
147
}
114
148
}
115
149
116
- for ( const attribute of node . attributes ) {
117
- if ( attribute . type === 'Attribute' ) {
118
- attributes . push ( attribute ) ;
119
- if ( node . name === 'img' && attribute . name === 'loading' ) {
120
- img_might_be_lazy = true ;
121
- }
122
- if ( attribute . name === 'dir' ) {
123
- has_direction_attribute = true ;
124
- }
125
- if ( attribute . name === 'style' ) {
126
- has_style_attribute = true ;
127
- }
128
- if (
129
- ( attribute . name === 'value' || attribute . name === 'checked' ) &&
130
- ! is_text_attribute ( attribute )
131
- ) {
132
- needs_input_reset = true ;
133
- needs_content_reset = true ;
134
- } else if (
135
- attribute . name === 'contenteditable' &&
136
- ( attribute . value === true ||
137
- ( is_text_attribute ( attribute ) && attribute . value [ 0 ] . data === 'true' ) )
138
- ) {
139
- is_content_editable = true ;
140
- }
141
- } else if ( attribute . type === 'SpreadAttribute' ) {
142
- attributes . push ( attribute ) ;
143
- needs_input_reset = true ;
144
- needs_content_reset = true ;
145
- if ( is_load_error_element ( node . name ) ) {
146
- might_need_event_replaying = true ;
147
- }
148
- } else if ( attribute . type === 'ClassDirective' ) {
149
- class_directives . push ( attribute ) ;
150
- } else if ( attribute . type === 'StyleDirective' ) {
151
- style_directives . push ( attribute ) ;
152
- } else if ( attribute . type === 'OnDirective' ) {
150
+ for ( const attribute of other_directives ) {
151
+ if ( attribute . type === 'OnDirective' ) {
153
152
const handler = /** @type {Expression } */ ( context . visit ( attribute ) ) ;
154
- const has_action_directive = node . attributes . find ( ( a ) => a . type === 'UseDirective' ) ;
155
153
156
154
context . state . after_update . push (
157
- b . stmt ( has_action_directive ? b . call ( '$.effect' , b . thunk ( handler ) ) : handler )
155
+ b . stmt ( has_use ? b . call ( '$.effect' , b . thunk ( handler ) ) : handler )
158
156
) ;
159
- } else if ( attribute . type !== 'LetDirective' ) {
160
- if ( attribute . type === 'BindDirective' ) {
161
- if ( attribute . name === 'group' || attribute . name === 'checked' ) {
162
- needs_special_value_handling = true ;
163
- needs_input_reset = true ;
164
- } else if ( attribute . name === 'value' ) {
165
- value_binding = attribute ;
166
- needs_content_reset = true ;
167
- needs_input_reset = true ;
168
- } else if (
169
- attribute . name === 'innerHTML' ||
170
- attribute . name === 'innerText' ||
171
- attribute . name === 'textContent'
172
- ) {
173
- has_content_editable_binding = true ;
174
- }
175
- } else if ( attribute . type === 'UseDirective' && is_load_error_element ( node . name ) ) {
176
- might_need_event_replaying = true ;
177
- }
157
+ } else {
178
158
context . visit ( attribute ) ;
179
159
}
180
160
}
181
161
182
- if ( is_content_editable && has_content_editable_binding ) {
183
- child_metadata . bound_contenteditable = true ;
184
- }
185
-
186
- if ( needs_input_reset && node . name === 'input' ) {
162
+ if (
163
+ node . name === 'input' &&
164
+ ( has_spread ||
165
+ bindings . has ( 'value' ) ||
166
+ bindings . has ( 'checked' ) ||
167
+ bindings . has ( 'group' ) ||
168
+ attributes . some (
169
+ ( attribute ) =>
170
+ attribute . type === 'Attribute' &&
171
+ ( attribute . name === 'value' || attribute . name === 'checked' ) &&
172
+ ! is_text_attribute ( attribute )
173
+ ) )
174
+ ) {
187
175
context . state . init . push ( b . stmt ( b . call ( '$.remove_input_defaults' , context . state . node ) ) ) ;
188
176
}
189
177
190
- if ( needs_content_reset && node . name === 'textarea' ) {
191
- context . state . init . push ( b . stmt ( b . call ( '$.remove_textarea_child' , context . state . node ) ) ) ;
192
- }
178
+ if ( node . name === 'textarea' ) {
179
+ const attribute = lookup . get ( 'value' ) ?? lookup . get ( 'checked' ) ;
180
+ const needs_content_reset = attribute && ! is_text_attribute ( attribute ) ;
193
181
194
- if ( value_binding !== null && node . name === 'select' ) {
195
- setup_select_synchronization ( value_binding , context ) ;
182
+ if ( has_spread || bindings . has ( 'value' ) || needs_content_reset ) {
183
+ context . state . init . push ( b . stmt ( b . call ( '$.remove_textarea_child' , context . state . node ) ) ) ;
184
+ }
196
185
}
197
186
198
- const node_id = context . state . node ;
187
+ if ( node . name === 'select' && bindings . has ( 'value' ) ) {
188
+ setup_select_synchronization ( /** @type {AST.BindDirective } */ ( bindings . get ( 'value' ) ) , context ) ;
189
+ }
199
190
200
191
// Let bindings first, they can be used on attributes
201
192
context . state . init . push ( ...lets ) ;
202
193
194
+ const node_id = context . state . node ;
195
+
203
196
// Then do attributes
204
197
let is_attributes_reactive = false ;
205
198
if ( node . metadata . has_spread ) {
206
- if ( node . name === 'img' ) {
207
- img_might_be_lazy = true ;
208
- }
209
199
build_element_spread_attributes (
210
200
attributes ,
211
201
context ,
212
202
node ,
213
203
node_id ,
214
204
// If value binding exists, that one takes care of calling $.init_select
215
- value_binding === null && node . name === 'select'
205
+ node . name === 'select' && ! bindings . has ( 'value' )
216
206
) ;
217
207
is_attributes_reactive = true ;
218
208
} else {
209
+ /** If true, needs `__value` for inputs */
210
+ const needs_special_value_handling =
211
+ node . name === 'option' ||
212
+ node . name === 'select' ||
213
+ bindings . has ( 'group' ) ||
214
+ bindings . has ( 'checked' ) ;
215
+
219
216
for ( const attribute of /** @type {AST.Attribute[] } */ ( attributes ) ) {
220
217
if ( is_event_attribute ( attribute ) ) {
221
- if (
222
- ( attribute . name === 'onload' || attribute . name === 'onerror' ) &&
223
- is_load_error_element ( node . name )
224
- ) {
225
- might_need_event_replaying = true ;
226
- }
227
218
visit_event_attribute ( attribute , context ) ;
228
219
continue ;
229
220
}
@@ -260,35 +251,52 @@ export function RegularElement(node, context) {
260
251
}
261
252
}
262
253
263
- // Apply the src and loading attributes for <img> elements after the element is appended to the document
264
- if ( img_might_be_lazy ) {
265
- context . state . after_update . push ( b . stmt ( b . call ( '$.handle_lazy_img' , node_id ) ) ) ;
266
- }
267
-
268
254
// class/style directives must be applied last since they could override class/style attributes
269
255
build_class_directives ( class_directives , node_id , context , is_attributes_reactive ) ;
270
256
build_style_directives (
271
257
style_directives ,
272
258
node_id ,
273
259
context ,
274
260
is_attributes_reactive ,
275
- has_style_attribute || node . metadata . has_spread
261
+ lookup . has ( 'style' ) || node . metadata . has_spread
276
262
) ;
277
263
278
- if ( might_need_event_replaying ) {
264
+ // Apply the src and loading attributes for <img> elements after the element is appended to the document
265
+ if ( node . name === 'img' && ( has_spread || lookup . has ( 'loading' ) ) ) {
266
+ context . state . after_update . push ( b . stmt ( b . call ( '$.handle_lazy_img' , node_id ) ) ) ;
267
+ }
268
+
269
+ if (
270
+ is_load_error_element ( node . name ) &&
271
+ ( has_spread || has_use || lookup . has ( 'onload' ) || lookup . has ( 'onerror' ) )
272
+ ) {
279
273
context . state . after_update . push ( b . stmt ( b . call ( '$.replay_events' , node_id ) ) ) ;
280
274
}
281
275
282
276
context . state . template . push ( '>' ) ;
283
277
284
- /** @type {SourceLocation[] } */
285
- const child_locations = [ ] ;
278
+ const metadata = {
279
+ ...context . state . metadata ,
280
+ namespace : determine_namespace_for_children ( node , context . state . metadata . namespace )
281
+ } ;
282
+
283
+ if ( bindings . has ( 'innerHTML' ) || bindings . has ( 'innerText' ) || bindings . has ( 'textContent' ) ) {
284
+ const contenteditable = lookup . get ( 'contenteditable' ) ;
285
+
286
+ if (
287
+ contenteditable &&
288
+ ( contenteditable . value === true ||
289
+ ( is_text_attribute ( contenteditable ) && contenteditable . value [ 0 ] . data === 'true' ) )
290
+ ) {
291
+ metadata . bound_contenteditable = true ;
292
+ }
293
+ }
286
294
287
295
/** @type {ComponentClientTransformState } */
288
296
const state = {
289
297
...context . state ,
290
- metadata : child_metadata ,
291
- locations : child_locations ,
298
+ metadata,
299
+ locations : [ ] ,
292
300
scope : /** @type {Scope } */ ( context . state . scopes . get ( node . fragment ) ) ,
293
301
preserve_whitespace :
294
302
context . state . preserve_whitespace || node . name === 'pre' || node . name === 'textarea'
@@ -298,15 +306,12 @@ export function RegularElement(node, context) {
298
306
node ,
299
307
node . fragment . nodes ,
300
308
context . path ,
301
- child_metadata . namespace ,
309
+ state . metadata . namespace ,
302
310
state ,
303
311
node . name === 'script' || state . preserve_whitespace ,
304
312
state . options . preserveComments
305
313
) ;
306
314
307
- /** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
308
- const has_declaration = node . fragment . nodes . some ( ( node ) => node . type === 'SnippetBlock' ) ;
309
-
310
315
/** @type {typeof state } */
311
316
const child_state = { ...state , init : [ ] , update : [ ] , after_update : [ ] } ;
312
317
@@ -357,7 +362,8 @@ export function RegularElement(node, context) {
357
362
}
358
363
}
359
364
360
- if ( has_declaration ) {
365
+ if ( node . fragment . nodes . some ( ( node ) => node . type === 'SnippetBlock' ) ) {
366
+ // Wrap children in `{...}` to avoid declaration conflicts
361
367
context . state . init . push (
362
368
b . block ( [
363
369
...child_state . init ,
@@ -371,16 +377,16 @@ export function RegularElement(node, context) {
371
377
context . state . after_update . push ( ...child_state . after_update ) ;
372
378
}
373
379
374
- if ( has_direction_attribute ) {
380
+ if ( lookup . has ( 'dir' ) ) {
375
381
// This fixes an issue with Chromium where updates to text content within an element
376
382
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
377
383
const dir = b . member ( node_id , 'dir' ) ;
378
384
context . state . update . push ( b . stmt ( b . assignment ( '=' , dir , dir ) ) ) ;
379
385
}
380
386
381
- if ( child_locations . length > 0 ) {
387
+ if ( state . locations . length > 0 ) {
382
388
// @ts -expect-error
383
- location . push ( child_locations ) ;
389
+ location . push ( state . locations ) ;
384
390
}
385
391
386
392
if ( ! is_void ( node . name ) ) {
0 commit comments