svelte 5.46.4 → 5.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiler/index.js +1 -1
- package/package.json +1 -1
- package/src/compiler/phases/2-analyze/css/css-prune.js +60 -0
- package/src/compiler/phases/2-analyze/visitors/RegularElement.js +14 -1
- package/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +3 -0
- package/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +4 -0
- package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +76 -4
- package/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +2 -3
- package/src/compiler/phases/3-transform/server/visitors/RegularElement.js +36 -4
- package/src/compiler/phases/nodes.js +95 -0
- package/src/html-tree-validation.js +2 -4
- package/src/internal/client/dom/elements/customizable-select.js +98 -0
- package/src/internal/client/index.js +1 -0
- package/src/internal/server/renderer.js +6 -4
- package/src/version.js +1 -1
package/package.json
CHANGED
|
@@ -770,7 +770,39 @@ function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
|
|
|
770
770
|
}
|
|
771
771
|
|
|
772
772
|
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
|
|
773
|
+
// Special handling for <option> inside <select>: elements inside <option> should
|
|
774
|
+
// also be considered descendants of <selectedcontent>, which clones the selected option's content
|
|
775
|
+
if (parent.type === 'RegularElement' && parent.name === 'option') {
|
|
776
|
+
const is_direct_child = ancestors.length === 0;
|
|
777
|
+
|
|
778
|
+
const select_element = path.findLast(
|
|
779
|
+
(element, j) => element.type === 'RegularElement' && element.name === 'select' && j < i
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
if (select_element && (!adjacent_only || is_direct_child)) {
|
|
783
|
+
/** @type {Compiler.AST.RegularElement | null} */
|
|
784
|
+
let selectedcontent_element = null;
|
|
785
|
+
walk(select_element, null, {
|
|
786
|
+
RegularElement(child, context) {
|
|
787
|
+
if (child.name === 'selectedcontent') {
|
|
788
|
+
selectedcontent_element = child;
|
|
789
|
+
context.stop();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
context.next();
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
if (adjacent_only && is_direct_child && selectedcontent_element) {
|
|
797
|
+
return [selectedcontent_element, parent];
|
|
798
|
+
} else if (selectedcontent_element) {
|
|
799
|
+
ancestors.push(selectedcontent_element);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
773
804
|
ancestors.push(parent);
|
|
805
|
+
|
|
774
806
|
if (adjacent_only) {
|
|
775
807
|
break;
|
|
776
808
|
}
|
|
@@ -817,6 +849,34 @@ function get_descendant_elements(node, adjacent_only, seen = new Set()) {
|
|
|
817
849
|
|
|
818
850
|
walk_children(node.type === 'RenderTag' ? node : node.fragment);
|
|
819
851
|
|
|
852
|
+
// Special handling for <selectedcontent>: it clones the content of the selected <option>,
|
|
853
|
+
// so descendants of <option> elements in the same <select> should also be considered descendants
|
|
854
|
+
if (node.type === 'RegularElement' && node.name === 'selectedcontent') {
|
|
855
|
+
const path = node.metadata.path;
|
|
856
|
+
const select_element = path.findLast(
|
|
857
|
+
(/** @type {Compiler.AST.SvelteNode} */ element) =>
|
|
858
|
+
element.type === 'RegularElement' && element.name === 'select'
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
if (select_element) {
|
|
862
|
+
walk(
|
|
863
|
+
select_element,
|
|
864
|
+
{ inside_option: false },
|
|
865
|
+
{
|
|
866
|
+
_(child, context) {
|
|
867
|
+
if (child.type === 'RegularElement' && child.name === 'option') {
|
|
868
|
+
context.next({ inside_option: true });
|
|
869
|
+
} else if (context.state.inside_option) {
|
|
870
|
+
walk_children(child);
|
|
871
|
+
} else {
|
|
872
|
+
context.next();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
820
880
|
return descendants;
|
|
821
881
|
}
|
|
822
882
|
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
} from '../../../../html-tree-validation.js';
|
|
8
8
|
import * as e from '../../../errors.js';
|
|
9
9
|
import * as w from '../../../warnings.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
create_attribute,
|
|
12
|
+
is_custom_element_node,
|
|
13
|
+
is_customizable_select_element
|
|
14
|
+
} from '../../nodes.js';
|
|
11
15
|
import { regex_starts_with_newline } from '../../patterns.js';
|
|
12
16
|
import { check_element } from './shared/a11y/index.js';
|
|
13
17
|
import { validate_element } from './shared/element.js';
|
|
@@ -74,6 +78,15 @@ export function RegularElement(node, context) {
|
|
|
74
78
|
node.metadata.synthetic_value_node = child;
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
// Special case: <select>, <option> or <optgroup> with rich content needs special hydration handling
|
|
82
|
+
// We mark the subtree as dynamic so parent elements properly include the child init code
|
|
83
|
+
if (is_customizable_select_element(node) || node.name === 'selectedcontent') {
|
|
84
|
+
// Mark the element's own fragment as dynamic so it's not treated as static
|
|
85
|
+
node.fragment.metadata.dynamic = true;
|
|
86
|
+
// Also mark ancestor fragments so parents properly include the child init code
|
|
87
|
+
mark_subtree_dynamic(context.path);
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
const binding = context.state.scope.get(node.name);
|
|
78
91
|
if (
|
|
79
92
|
binding !== null &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @import { AST } from '#compiler' */
|
|
2
2
|
/** @import { Context } from '../types' */
|
|
3
3
|
import * as e from '../../../errors.js';
|
|
4
|
+
import { mark_subtree_dynamic } from './shared/fragment.js';
|
|
4
5
|
|
|
5
6
|
const valid = ['onerror', 'failed', 'pending'];
|
|
6
7
|
|
|
@@ -23,5 +24,7 @@ export function SvelteBoundary(node, context) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
mark_subtree_dynamic(context.path);
|
|
28
|
+
|
|
26
29
|
context.next();
|
|
27
30
|
}
|
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
import { is_ignored } from '../../../../state.js';
|
|
12
12
|
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
|
|
13
13
|
import * as b from '#compiler/builders';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
create_attribute,
|
|
16
|
+
ExpressionMetadata,
|
|
17
|
+
is_custom_element_node,
|
|
18
|
+
is_customizable_select_element
|
|
19
|
+
} from '../../../nodes.js';
|
|
15
20
|
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
|
|
16
21
|
import { build_getter } from '../utils.js';
|
|
17
22
|
import {
|
|
@@ -21,9 +26,12 @@ import {
|
|
|
21
26
|
build_set_class,
|
|
22
27
|
build_set_style
|
|
23
28
|
} from './shared/element.js';
|
|
24
|
-
import { process_children } from './shared/fragment.js';
|
|
29
|
+
import { process_children, is_static_element } from './shared/fragment.js';
|
|
25
30
|
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
|
|
26
31
|
import { visit_event_attribute } from './shared/events.js';
|
|
32
|
+
import { Template } from '../transform-template/template.js';
|
|
33
|
+
import { transform_template } from '../transform-template/index.js';
|
|
34
|
+
import { TEMPLATE_FRAGMENT } from '../../../../../constants.js';
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
37
|
* @param {AST.RegularElement} node
|
|
@@ -351,13 +359,65 @@ export function RegularElement(node, context) {
|
|
|
351
359
|
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
|
|
352
360
|
);
|
|
353
361
|
}
|
|
362
|
+
} else if (is_customizable_select_element(node)) {
|
|
363
|
+
// For <option>, <optgroup>, or <select> elements with rich content, we need to branch based on browser support.
|
|
364
|
+
// Modern browsers preserve rich HTML in options, older browsers strip it to text only.
|
|
365
|
+
// We create a separate template for the rich content and append it to the element.
|
|
366
|
+
|
|
367
|
+
const element_node = context.state.node;
|
|
368
|
+
|
|
369
|
+
// Add a hydration marker inside the option element so $.child() has an anchor to find
|
|
370
|
+
context.state.template.push_comment();
|
|
371
|
+
|
|
372
|
+
// Create a separate template for the rich content
|
|
373
|
+
const template_name = context.state.scope.root.unique(`${node.name}_content`);
|
|
374
|
+
const fragment_id = b.id(context.state.scope.generate('fragment'));
|
|
375
|
+
const anchor_id = b.id(context.state.scope.generate('anchor'));
|
|
376
|
+
|
|
377
|
+
// Create state with a new template for the rich content
|
|
378
|
+
/** @type {typeof state} */
|
|
379
|
+
const select_state = {
|
|
380
|
+
...state,
|
|
381
|
+
init: [],
|
|
382
|
+
update: [],
|
|
383
|
+
after_update: [],
|
|
384
|
+
template: new Template()
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
process_children(
|
|
388
|
+
trimmed,
|
|
389
|
+
(is_text) => b.call('$.first_child', fragment_id, is_text && b.true),
|
|
390
|
+
false,
|
|
391
|
+
{
|
|
392
|
+
...context,
|
|
393
|
+
state: select_state
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Transform the template to $.from_html(...) and hoist it
|
|
398
|
+
const template = transform_template(select_state, metadata.namespace, TEMPLATE_FRAGMENT);
|
|
399
|
+
context.state.hoisted.push(b.var(template_name, template));
|
|
400
|
+
|
|
401
|
+
// Build the rich content function body
|
|
402
|
+
// The anchor is the child of the element (a hydration marker during hydration)
|
|
403
|
+
const body = b.block([
|
|
404
|
+
b.var(anchor_id, b.call('$.child', element_node)),
|
|
405
|
+
b.var(fragment_id, b.call(template_name)),
|
|
406
|
+
...select_state.init,
|
|
407
|
+
...(select_state.update.length > 0 ? [build_render_statement(select_state)] : []),
|
|
408
|
+
...select_state.after_update,
|
|
409
|
+
b.stmt(b.call('$.append', anchor_id, fragment_id))
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
child_state.init.push(b.stmt(b.call('$.customizable_select', element_node, b.arrow([], body))));
|
|
354
413
|
} else {
|
|
355
414
|
/** @type {Expression} */
|
|
356
415
|
let arg = context.state.node;
|
|
357
416
|
|
|
358
417
|
// If `hydrate_node` is set inside the element, we need to reset it
|
|
359
|
-
// after the element has been hydrated
|
|
360
|
-
|
|
418
|
+
// after the element has been hydrated. We need to check if any child
|
|
419
|
+
// would actually advance the hydrate_node cursor - static elements don't.
|
|
420
|
+
let needs_reset = trimmed.some((node) => node.type !== 'Text' && !is_static_element(node));
|
|
361
421
|
|
|
362
422
|
// The same applies if it's a `<template>` element, since we need to
|
|
363
423
|
// set the value of `hydrate_node` to `node.content`
|
|
@@ -397,6 +457,18 @@ export function RegularElement(node, context) {
|
|
|
397
457
|
context.state.after_update.push(...element_state.after_update);
|
|
398
458
|
}
|
|
399
459
|
|
|
460
|
+
if (node.name === 'selectedcontent') {
|
|
461
|
+
context.state.init.push(
|
|
462
|
+
b.stmt(
|
|
463
|
+
b.call(
|
|
464
|
+
'$.selectedcontent',
|
|
465
|
+
context.state.node,
|
|
466
|
+
b.arrow([b.id('$$element')], b.assignment('=', context.state.node, b.id('$$element')))
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
400
472
|
if (lookup.has('dir')) {
|
|
401
473
|
// This fixes an issue with Chromium where updates to text content within an element
|
|
402
474
|
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
|
|
@@ -98,7 +98,7 @@ export function process_children(nodes, initial, is_element, context) {
|
|
|
98
98
|
|
|
99
99
|
let child_state = context.state;
|
|
100
100
|
|
|
101
|
-
if (is_static_element(node
|
|
101
|
+
if (is_static_element(node)) {
|
|
102
102
|
skipped += 1;
|
|
103
103
|
} else if (
|
|
104
104
|
node.type === 'EachBlock' &&
|
|
@@ -137,9 +137,8 @@ export function process_children(nodes, initial, is_element, context) {
|
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
139
|
* @param {AST.SvelteNode} node
|
|
140
|
-
* @param {ComponentContext["state"]} state
|
|
141
140
|
*/
|
|
142
|
-
function is_static_element(node
|
|
141
|
+
export function is_static_element(node) {
|
|
143
142
|
if (node.type !== 'RegularElement') return false;
|
|
144
143
|
if (node.fragment.metadata.dynamic) return false;
|
|
145
144
|
if (is_custom_element_node(node)) return false; // we're setting all attributes on custom elements through properties
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
PromiseOptimiser,
|
|
16
16
|
create_async_block
|
|
17
17
|
} from './shared/utils.js';
|
|
18
|
+
import { is_customizable_select_element } from '../../../nodes.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* @param {AST.RegularElement} node
|
|
@@ -124,6 +125,10 @@ export function RegularElement(node, context) {
|
|
|
124
125
|
|
|
125
126
|
const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
|
|
126
127
|
|
|
128
|
+
if (is_customizable_select_element(node)) {
|
|
129
|
+
rest.push(b.true);
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest));
|
|
128
133
|
|
|
129
134
|
if (optimiser.expressions.length > 0) {
|
|
@@ -149,14 +154,34 @@ export function RegularElement(node, context) {
|
|
|
149
154
|
const inner_state = { ...state, template: [], init: [] };
|
|
150
155
|
process_children(trimmed, { ...context, state: inner_state });
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
157
|
+
/** @type {import('estree').Statement[]} */
|
|
158
|
+
const body_statements = [...state.init, ...build_template(inner_state.template)];
|
|
159
|
+
|
|
160
|
+
if (dev) {
|
|
161
|
+
const location = locator(node.start);
|
|
162
|
+
body_statements.unshift(
|
|
163
|
+
b.stmt(
|
|
164
|
+
b.call(
|
|
165
|
+
'$.push_element',
|
|
166
|
+
b.id('$$renderer'),
|
|
167
|
+
b.literal(node.name),
|
|
168
|
+
b.literal(location.line),
|
|
169
|
+
b.literal(location.column)
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
body_statements.push(b.stmt(b.call('$.pop_element')));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
body = b.arrow([b.id('$$renderer')], b.block(body_statements));
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
|
|
159
180
|
|
|
181
|
+
if (is_customizable_select_element(node)) {
|
|
182
|
+
rest.push(b.true);
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
const statement = b.stmt(b.call('$$renderer.option', attributes, body, ...rest));
|
|
161
186
|
|
|
162
187
|
if (optimiser.expressions.length > 0) {
|
|
@@ -192,7 +217,14 @@ export function RegularElement(node, context) {
|
|
|
192
217
|
)
|
|
193
218
|
);
|
|
194
219
|
} else {
|
|
220
|
+
// For optgroup or select with rich content, add hydration marker at the start
|
|
195
221
|
process_children(trimmed, { ...context, state });
|
|
222
|
+
if (
|
|
223
|
+
(node.name === 'optgroup' || node.name === 'select') &&
|
|
224
|
+
is_customizable_select_element(node)
|
|
225
|
+
) {
|
|
226
|
+
state.template.push(b.literal('<!>'));
|
|
227
|
+
}
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
if (!node_is_void) {
|
|
@@ -148,3 +148,98 @@ export function get_name(node) {
|
|
|
148
148
|
|
|
149
149
|
return null;
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks if an <option>, <optgroup>, or <select> element has rich content that requires special hydration handling.
|
|
154
|
+
* Rich content is anything beyond simple text, expressions, and comments for <option>,
|
|
155
|
+
* anything beyond <option> children for <optgroup>,
|
|
156
|
+
* or anything beyond <option>, <optgroup>, and empty text for <select>.
|
|
157
|
+
* Control flow blocks are recursively checked - they only count as rich content if they contain rich content.
|
|
158
|
+
* @param {AST.RegularElement} node
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
export function is_customizable_select_element(node) {
|
|
162
|
+
if (node.name === 'select' || node.name === 'optgroup' || node.name === 'option') {
|
|
163
|
+
for (const child of find_descendants(node.fragment)) {
|
|
164
|
+
if (child.type === 'RegularElement') {
|
|
165
|
+
if (node.name === 'select' && child.name !== 'option' && child.name !== 'optgroup') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (node.name === 'optgroup' && child.name !== 'option') {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (node.name === 'option') {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Text nodes directly in <select> or <optgroup> are rich content
|
|
179
|
+
else if (child.type === 'Text') {
|
|
180
|
+
if (node.name === 'select' || node.name === 'optgroup') {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Any non-RegularElement, non-Text node is rich content
|
|
186
|
+
else {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {AST.Fragment | null} fragment
|
|
197
|
+
* @returns {Iterable<AST.SvelteNode>}
|
|
198
|
+
*/
|
|
199
|
+
function* find_descendants(fragment) {
|
|
200
|
+
if (fragment === null) return;
|
|
201
|
+
|
|
202
|
+
for (const node of fragment.nodes) {
|
|
203
|
+
switch (node.type) {
|
|
204
|
+
case 'SnippetBlock':
|
|
205
|
+
case 'DebugTag':
|
|
206
|
+
case 'ConstTag':
|
|
207
|
+
case 'Comment':
|
|
208
|
+
case 'ExpressionTag':
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
case 'Text':
|
|
212
|
+
if (node.data.trim() !== '') {
|
|
213
|
+
yield node;
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case 'IfBlock':
|
|
218
|
+
yield* find_descendants(node.consequent);
|
|
219
|
+
yield* find_descendants(node.alternate);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case 'EachBlock':
|
|
223
|
+
yield* find_descendants(node.body);
|
|
224
|
+
yield* find_descendants(node.fallback ?? null);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'KeyBlock':
|
|
228
|
+
yield* find_descendants(node.fragment);
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case 'AwaitBlock':
|
|
232
|
+
yield* find_descendants(node.pending);
|
|
233
|
+
yield* find_descendants(node.then);
|
|
234
|
+
yield* find_descendants(node.catch);
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case 'SvelteBoundary':
|
|
238
|
+
yield* find_descendants(node.fragment);
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
default:
|
|
242
|
+
yield node;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -80,9 +80,9 @@ export function closing_tag_omitted(current, next) {
|
|
|
80
80
|
*/
|
|
81
81
|
const disallowed_children = {
|
|
82
82
|
...autoclosing_children,
|
|
83
|
-
optgroup: { only: ['option', '#text'] },
|
|
84
83
|
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>, but we assume it here
|
|
85
|
-
option
|
|
84
|
+
// option or optgroup does not have an `only` restriction because newer browsers support rich HTML content
|
|
85
|
+
// inside option elements. For older browsers, hydration will handle the mismatch.
|
|
86
86
|
form: { descendant: ['form'] },
|
|
87
87
|
a: { descendant: ['a'] },
|
|
88
88
|
button: { descendant: ['button'] },
|
|
@@ -92,8 +92,6 @@ const disallowed_children = {
|
|
|
92
92
|
h4: { descendant: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] },
|
|
93
93
|
h5: { descendant: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] },
|
|
94
94
|
h6: { descendant: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] },
|
|
95
|
-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
|
|
96
|
-
select: { only: ['option', 'optgroup', '#text', 'hr', 'script', 'template'] },
|
|
97
95
|
|
|
98
96
|
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
|
|
99
97
|
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { hydrating, reset, set_hydrate_node, set_hydrating } from '../hydration.js';
|
|
2
|
+
import { create_comment } from '../operations.js';
|
|
3
|
+
import { attach } from './attachments.js';
|
|
4
|
+
|
|
5
|
+
/** @type {boolean | null} */
|
|
6
|
+
let supported = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the browser supports rich HTML content inside `<option>` elements.
|
|
10
|
+
* Modern browsers preserve HTML elements inside options, while older browsers
|
|
11
|
+
* strip them during parsing, leaving only text content.
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function is_supported() {
|
|
15
|
+
if (supported === null) {
|
|
16
|
+
var select = document.createElement('select');
|
|
17
|
+
select.innerHTML = '<option><span>t</span></option>';
|
|
18
|
+
supported = /** @type {Element} */ (select.firstChild)?.firstChild?.nodeType === 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return supported;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
* @param {HTMLElement} element
|
|
27
|
+
* @param {(new_element: HTMLElement) => void} update_element
|
|
28
|
+
*/
|
|
29
|
+
export function selectedcontent(element, update_element) {
|
|
30
|
+
// if it's not supported no need for special logic
|
|
31
|
+
if (!is_supported()) return;
|
|
32
|
+
|
|
33
|
+
// we use the attach function directly just to make sure is executed when is mounted to the dom
|
|
34
|
+
attach(element, () => () => {
|
|
35
|
+
const select = element.closest('select');
|
|
36
|
+
if (!select) return;
|
|
37
|
+
|
|
38
|
+
const observer = new MutationObserver((entries) => {
|
|
39
|
+
var selected = false;
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.target === element) {
|
|
43
|
+
// the `<selectedcontent>` already changed, no need to replace it
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// if the changes doesn't include the selected `<option>` we don't need to do anything
|
|
48
|
+
selected ||= !!entry.target.parentElement?.closest('option')?.selected;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (selected) {
|
|
52
|
+
// replace the `<selectedcontent>` with a clone
|
|
53
|
+
element.replaceWith((element = /** @type {HTMLElement} */ (element.cloneNode(true))));
|
|
54
|
+
update_element(element);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
observer.observe(select, {
|
|
59
|
+
childList: true,
|
|
60
|
+
characterData: true,
|
|
61
|
+
subtree: true
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
observer.disconnect();
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handles rich HTML content inside `<option>`, `<optgroup>`, or `<select>` elements with browser-specific branching.
|
|
72
|
+
* Modern browsers preserve HTML inside options, while older browsers strip it to text only.
|
|
73
|
+
*
|
|
74
|
+
* @param {HTMLOptionElement | HTMLOptGroupElement | HTMLSelectElement} element The element to process
|
|
75
|
+
* @param {() => void} rich_fn Function to process rich HTML content (modern browsers)
|
|
76
|
+
*/
|
|
77
|
+
export function customizable_select(element, rich_fn) {
|
|
78
|
+
var was_hydrating = hydrating;
|
|
79
|
+
|
|
80
|
+
if (!is_supported()) {
|
|
81
|
+
set_hydrating(false);
|
|
82
|
+
element.textContent = '';
|
|
83
|
+
element.append(create_comment(''));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
rich_fn();
|
|
88
|
+
} finally {
|
|
89
|
+
if (was_hydrating) {
|
|
90
|
+
if (hydrating) {
|
|
91
|
+
reset(element);
|
|
92
|
+
} else {
|
|
93
|
+
set_hydrating(true);
|
|
94
|
+
set_hydrate_node(element);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -42,6 +42,7 @@ export {
|
|
|
42
42
|
export { set_class } from './dom/elements/class.js';
|
|
43
43
|
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
|
|
44
44
|
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
|
|
45
|
+
export { customizable_select, selectedcontent } from './dom/elements/customizable-select.js';
|
|
45
46
|
export { set_style } from './dom/elements/style.js';
|
|
46
47
|
export { animation, transition } from './dom/elements/transitions.js';
|
|
47
48
|
export { bind_active_element } from './dom/elements/bindings/document.js';
|
|
@@ -220,9 +220,10 @@ export class Renderer {
|
|
|
220
220
|
* @param {Record<string, boolean> | undefined} [classes]
|
|
221
221
|
* @param {Record<string, string> | undefined} [styles]
|
|
222
222
|
* @param {number | undefined} [flags]
|
|
223
|
+
* @param {boolean | undefined} [is_rich]
|
|
223
224
|
* @returns {void}
|
|
224
225
|
*/
|
|
225
|
-
select(attrs, fn, css_hash, classes, styles, flags) {
|
|
226
|
+
select(attrs, fn, css_hash, classes, styles, flags, is_rich) {
|
|
226
227
|
const { value, ...select_attrs } = attrs;
|
|
227
228
|
|
|
228
229
|
this.push(`<select${attributes(select_attrs, css_hash, classes, styles, flags)}>`);
|
|
@@ -230,7 +231,7 @@ export class Renderer {
|
|
|
230
231
|
renderer.local.select_value = value;
|
|
231
232
|
fn(renderer);
|
|
232
233
|
});
|
|
233
|
-
this.push('</select
|
|
234
|
+
this.push(`${is_rich ? '<!>' : ''}</select>`);
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
/**
|
|
@@ -240,8 +241,9 @@ export class Renderer {
|
|
|
240
241
|
* @param {Record<string, boolean> | undefined} [classes]
|
|
241
242
|
* @param {Record<string, string> | undefined} [styles]
|
|
242
243
|
* @param {number | undefined} [flags]
|
|
244
|
+
* @param {boolean | undefined} [is_rich]
|
|
243
245
|
*/
|
|
244
|
-
option(attrs, body, css_hash, classes, styles, flags) {
|
|
246
|
+
option(attrs, body, css_hash, classes, styles, flags, is_rich) {
|
|
245
247
|
this.#out.push(`<option${attributes(attrs, css_hash, classes, styles, flags)}`);
|
|
246
248
|
|
|
247
249
|
/**
|
|
@@ -258,7 +260,7 @@ export class Renderer {
|
|
|
258
260
|
renderer.#out.push(' selected');
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
renderer.#out.push(`>${body}</option>`);
|
|
263
|
+
renderer.#out.push(`>${body}${is_rich ? '<!>' : ''}</option>`);
|
|
262
264
|
|
|
263
265
|
// super edge case, but may as well handle it
|
|
264
266
|
if (head) {
|
package/src/version.js
CHANGED