ripple 0.2.113 → 0.2.115
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/package.json +1 -6
- package/src/compiler/phases/1-parse/index.js +1 -0
- package/src/compiler/phases/2-analyze/index.js +17 -0
- package/src/compiler/phases/3-transform/client/index.js +48 -6
- package/src/compiler/phases/3-transform/segments.js +164 -10
- package/src/runtime/index-client.js +185 -5
- package/src/runtime/internal/client/runtime.js +4 -4
- package/src/utils/builders.js +274 -262
- package/tests/client/input-value.test.ripple +66 -6
- package/types/index.d.ts +33 -2
- package/src/bindings/index.d.ts +0 -13
- package/src/bindings/index.js +0 -79
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.115",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -38,11 +38,6 @@
|
|
|
38
38
|
"require": "./src/compiler/index.js",
|
|
39
39
|
"default": "./src/compiler/index.js"
|
|
40
40
|
},
|
|
41
|
-
"./bindings": {
|
|
42
|
-
"types": "./src/bindings/index.d.ts",
|
|
43
|
-
"require": "./src/bindings/index.js",
|
|
44
|
-
"default": "./src/bindings/index.js"
|
|
45
|
-
},
|
|
46
41
|
"./validator": {
|
|
47
42
|
"types": "./types/index.d.ts",
|
|
48
43
|
"require": "./validator/index.js",
|
|
@@ -567,6 +567,23 @@ const visitors = {
|
|
|
567
567
|
|
|
568
568
|
mark_control_flow_has_template(path);
|
|
569
569
|
|
|
570
|
+
// Store capitalized name for dynamic components/elements
|
|
571
|
+
if (node.id.tracked) {
|
|
572
|
+
const original_name = node.id.name;
|
|
573
|
+
const capitalized_name = original_name.charAt(0).toUpperCase() + original_name.slice(1);
|
|
574
|
+
node.metadata.ts_name = capitalized_name;
|
|
575
|
+
node.metadata.original_name = original_name;
|
|
576
|
+
|
|
577
|
+
// Mark the binding as a dynamic component so we can capitalize it everywhere
|
|
578
|
+
const binding = context.state.scope.get(original_name);
|
|
579
|
+
if (binding) {
|
|
580
|
+
if (!binding.metadata) {
|
|
581
|
+
binding.metadata = {};
|
|
582
|
+
}
|
|
583
|
+
binding.metadata.is_dynamic_component = true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
570
587
|
if (is_dom_element) {
|
|
571
588
|
if (node.id.name === 'head') {
|
|
572
589
|
// head validation
|
|
@@ -161,6 +161,15 @@ const visitors = {
|
|
|
161
161
|
if (is_reference(node, parent)) {
|
|
162
162
|
if (context.state.to_ts) {
|
|
163
163
|
if (node.tracked) {
|
|
164
|
+
// Check if this identifier is used as a dynamic component/element
|
|
165
|
+
// by checking if it has a capitalized name in metadata
|
|
166
|
+
const binding = context.state.scope.get(node.name);
|
|
167
|
+
if (binding?.metadata?.is_dynamic_component) {
|
|
168
|
+
// Capitalize the identifier for TypeScript
|
|
169
|
+
const capitalizedName = node.name.charAt(0).toUpperCase() + node.name.slice(1);
|
|
170
|
+
const capitalizedNode = { ...node, name: capitalizedName };
|
|
171
|
+
return b.member(capitalizedNode, b.literal('#v'), true);
|
|
172
|
+
}
|
|
164
173
|
return b.member(node, b.literal('#v'), true);
|
|
165
174
|
}
|
|
166
175
|
} else {
|
|
@@ -204,7 +213,7 @@ const visitors = {
|
|
|
204
213
|
return {
|
|
205
214
|
...node,
|
|
206
215
|
specifiers: node.specifiers
|
|
207
|
-
.filter((spec) => spec.importKind !== 'type')
|
|
216
|
+
.filter((spec) => context.state.to_ts || spec.importKind !== 'type')
|
|
208
217
|
.map((spec) => context.visit(spec)),
|
|
209
218
|
};
|
|
210
219
|
},
|
|
@@ -441,6 +450,22 @@ const visitors = {
|
|
|
441
450
|
return context.next();
|
|
442
451
|
},
|
|
443
452
|
|
|
453
|
+
VariableDeclarator(node, context) {
|
|
454
|
+
// In TypeScript mode, capitalize identifiers that are used as dynamic components
|
|
455
|
+
if (context.state.to_ts && node.id.type === 'Identifier') {
|
|
456
|
+
const binding = context.state.scope.get(node.id.name);
|
|
457
|
+
if (binding?.metadata?.is_dynamic_component) {
|
|
458
|
+
const capitalizedName = node.id.name.charAt(0).toUpperCase() + node.id.name.slice(1);
|
|
459
|
+
return {
|
|
460
|
+
...node,
|
|
461
|
+
id: { ...node.id, name: capitalizedName },
|
|
462
|
+
init: node.init ? context.visit(node.init) : null
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return context.next();
|
|
467
|
+
},
|
|
468
|
+
|
|
444
469
|
FunctionDeclaration(node, context) {
|
|
445
470
|
return visit_function(node, context);
|
|
446
471
|
},
|
|
@@ -1397,7 +1422,8 @@ function transform_ts_child(node, context) {
|
|
|
1397
1422
|
// Do we need to do something special here?
|
|
1398
1423
|
state.init.push(b.stmt(visit(node.expression, { ...state })));
|
|
1399
1424
|
} else if (node.type === 'Element') {
|
|
1400
|
-
|
|
1425
|
+
// Use capitalized name for dynamic components/elements in TypeScript output
|
|
1426
|
+
const type = node.metadata?.ts_name || node.id.name;
|
|
1401
1427
|
const children = [];
|
|
1402
1428
|
let has_children_props = false;
|
|
1403
1429
|
|
|
@@ -1463,7 +1489,17 @@ function transform_ts_child(node, context) {
|
|
|
1463
1489
|
}
|
|
1464
1490
|
|
|
1465
1491
|
const opening_type = b.jsx_id(type);
|
|
1466
|
-
|
|
1492
|
+
// Use node.id.loc if available, otherwise create a loc based on the element's position
|
|
1493
|
+
opening_type.loc = node.id.loc || {
|
|
1494
|
+
start: {
|
|
1495
|
+
line: node.loc.start.line,
|
|
1496
|
+
column: node.loc.start.column + 2, // After "<@"
|
|
1497
|
+
},
|
|
1498
|
+
end: {
|
|
1499
|
+
line: node.loc.start.line,
|
|
1500
|
+
column: node.loc.start.column + 2 + type.length,
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1467
1503
|
|
|
1468
1504
|
let closing_type = undefined;
|
|
1469
1505
|
|
|
@@ -1481,9 +1517,15 @@ function transform_ts_child(node, context) {
|
|
|
1481
1517
|
};
|
|
1482
1518
|
}
|
|
1483
1519
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
)
|
|
1520
|
+
const jsxElement = b.jsx_element(opening_type, attributes, children, node.selfClosing, closing_type);
|
|
1521
|
+
// Preserve metadata from Element node for mapping purposes
|
|
1522
|
+
if (node.metadata && (node.metadata.ts_name || node.metadata.original_name)) {
|
|
1523
|
+
jsxElement.metadata = {
|
|
1524
|
+
ts_name: node.metadata.ts_name,
|
|
1525
|
+
original_name: node.metadata.original_name
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
state.init.push(b.stmt(jsxElement));
|
|
1487
1529
|
} else if (node.type === 'IfStatement') {
|
|
1488
1530
|
const consequent_scope = context.state.scopes.get(node.consequent);
|
|
1489
1531
|
const consequent = b.block(
|
|
@@ -5,6 +5,14 @@ export const mapping_data = {
|
|
|
5
5
|
completion: true,
|
|
6
6
|
semantic: true,
|
|
7
7
|
navigation: true,
|
|
8
|
+
rename: true,
|
|
9
|
+
codeActions: false, // set to false to disable auto import when importing yourself
|
|
10
|
+
formatting: false, // not doing formatting through Volar, using Prettier.
|
|
11
|
+
// these 3 below will be true by default
|
|
12
|
+
// leaving for reference
|
|
13
|
+
// hover: true,
|
|
14
|
+
// definition: true,
|
|
15
|
+
// references: true,
|
|
8
16
|
};
|
|
9
17
|
|
|
10
18
|
/**
|
|
@@ -22,6 +30,26 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
22
30
|
let sourceIndex = 0;
|
|
23
31
|
let generatedIndex = 0;
|
|
24
32
|
|
|
33
|
+
// Map to track capitalized names: original name -> capitalized name
|
|
34
|
+
/** @type {Map<string, string>} */
|
|
35
|
+
const capitalizedNames = new Map();
|
|
36
|
+
// Reverse map: capitalized name -> original name
|
|
37
|
+
/** @type {Map<string, string>} */
|
|
38
|
+
const reverseCapitalizedNames = new Map();
|
|
39
|
+
|
|
40
|
+
// Pre-walk to collect capitalized names from JSXElement nodes (transformed AST)
|
|
41
|
+
// These are identifiers that are used as dynamic components/elements
|
|
42
|
+
walk(ast, null, {
|
|
43
|
+
_(node, { next }) {
|
|
44
|
+
// Check JSXElement nodes with metadata (preserved from Element nodes)
|
|
45
|
+
if (node.type === 'JSXElement' && node.metadata?.ts_name && node.metadata?.original_name) {
|
|
46
|
+
capitalizedNames.set(node.metadata.original_name, node.metadata.ts_name);
|
|
47
|
+
reverseCapitalizedNames.set(node.metadata.ts_name, node.metadata.original_name);
|
|
48
|
+
}
|
|
49
|
+
next();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
25
53
|
/**
|
|
26
54
|
* Check if character is a word boundary (not alphanumeric or underscore)
|
|
27
55
|
* @param {string} char
|
|
@@ -31,6 +59,29 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
31
59
|
return char === undefined || !/[a-zA-Z0-9_$]/.test(char);
|
|
32
60
|
};
|
|
33
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Check if a position is inside a comment
|
|
64
|
+
* @param {number} pos - Position to check
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
const isInComment = (pos) => {
|
|
68
|
+
// Check for single-line comment: find start of line and check if there's // before this position
|
|
69
|
+
let lineStart = source.lastIndexOf('\n', pos - 1) + 1;
|
|
70
|
+
const lineBeforePos = source.substring(lineStart, pos);
|
|
71
|
+
if (lineBeforePos.includes('//')) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// Check for multi-line comment: look backwards for /* and forwards for */
|
|
75
|
+
const lastCommentStart = source.lastIndexOf('/*', pos);
|
|
76
|
+
if (lastCommentStart !== -1) {
|
|
77
|
+
const commentEnd = source.indexOf('*/', lastCommentStart);
|
|
78
|
+
if (commentEnd === -1 || commentEnd > pos) {
|
|
79
|
+
return true; // We're inside an unclosed or open comment
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
|
|
34
85
|
/**
|
|
35
86
|
* Find text in source string, searching character by character from sourceIndex
|
|
36
87
|
* @param {string} text - Text to find
|
|
@@ -46,6 +97,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
46
97
|
}
|
|
47
98
|
}
|
|
48
99
|
if (match) {
|
|
100
|
+
// Skip if this match is inside a comment
|
|
101
|
+
if (isInComment(i)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
49
105
|
// Check word boundaries for identifier-like tokens
|
|
50
106
|
const isIdentifierLike = /^[a-zA-Z_$]/.test(text);
|
|
51
107
|
if (isIdentifierLike) {
|
|
@@ -96,30 +152,73 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
96
152
|
};
|
|
97
153
|
|
|
98
154
|
// Collect text tokens from AST nodes
|
|
99
|
-
|
|
155
|
+
// Tokens can be either strings or objects with source/generated properties
|
|
156
|
+
/** @type {Array<string | {source: string, generated: string}>} */
|
|
100
157
|
const tokens = [];
|
|
101
158
|
|
|
159
|
+
// Collect import declarations for full-statement mappings
|
|
160
|
+
/** @type {Array<{start: number, end: number}>} */
|
|
161
|
+
const importDeclarations = [];
|
|
162
|
+
|
|
102
163
|
// We have to visit everything in generated order to maintain correct indices
|
|
103
164
|
walk(ast, null, {
|
|
104
165
|
_(node, { visit }) {
|
|
105
166
|
// Collect key node types: Identifiers, Literals, and JSX Elements
|
|
167
|
+
// Only collect tokens from nodes with .loc (skip synthesized nodes like children attribute)
|
|
106
168
|
if (node.type === 'Identifier' && node.name) {
|
|
107
|
-
|
|
169
|
+
if (node.loc) {
|
|
170
|
+
// Check if this identifier was capitalized (reverse lookup)
|
|
171
|
+
const originalName = reverseCapitalizedNames.get(node.name);
|
|
172
|
+
if (originalName) {
|
|
173
|
+
// This is a capitalized name in generated code, map to lowercase in source
|
|
174
|
+
tokens.push({ source: originalName, generated: node.name });
|
|
175
|
+
} else {
|
|
176
|
+
// Check if this identifier should be capitalized (forward lookup)
|
|
177
|
+
const capitalizedName = capitalizedNames.get(node.name);
|
|
178
|
+
if (capitalizedName) {
|
|
179
|
+
tokens.push({ source: node.name, generated: capitalizedName });
|
|
180
|
+
} else {
|
|
181
|
+
tokens.push(node.name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
108
185
|
return; // Leaf node, don't traverse further
|
|
109
186
|
} else if (node.type === 'JSXIdentifier' && node.name) {
|
|
110
|
-
|
|
187
|
+
if (node.loc) {
|
|
188
|
+
// Check if this was capitalized (reverse lookup)
|
|
189
|
+
const originalName = reverseCapitalizedNames.get(node.name);
|
|
190
|
+
if (originalName) {
|
|
191
|
+
tokens.push({ source: originalName, generated: node.name });
|
|
192
|
+
} else {
|
|
193
|
+
// Check if this should be capitalized (forward lookup)
|
|
194
|
+
const capitalizedName = capitalizedNames.get(node.name);
|
|
195
|
+
if (capitalizedName) {
|
|
196
|
+
tokens.push({ source: node.name, generated: capitalizedName });
|
|
197
|
+
} else {
|
|
198
|
+
tokens.push(node.name);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
111
202
|
return; // Leaf node, don't traverse further
|
|
112
203
|
} else if (node.type === 'Literal' && node.raw) {
|
|
113
|
-
|
|
204
|
+
if (node.loc) {
|
|
205
|
+
tokens.push(node.raw);
|
|
206
|
+
}
|
|
114
207
|
return; // Leaf node, don't traverse further
|
|
115
208
|
} else if (node.type === 'ImportDeclaration') {
|
|
209
|
+
// Collect import declaration range for full-statement mapping
|
|
210
|
+
// TypeScript reports unused imports with diagnostics covering the entire statement
|
|
211
|
+
if (node.start !== undefined && node.end !== undefined) {
|
|
212
|
+
importDeclarations.push({ start: node.start, end: node.end });
|
|
213
|
+
}
|
|
214
|
+
|
|
116
215
|
// Visit specifiers in source order
|
|
117
216
|
if (node.specifiers) {
|
|
118
217
|
for (const specifier of node.specifiers) {
|
|
119
218
|
visit(specifier);
|
|
120
219
|
}
|
|
121
220
|
}
|
|
122
|
-
|
|
221
|
+
visit(node.source);
|
|
123
222
|
return;
|
|
124
223
|
} else if (node.type === 'ImportSpecifier') {
|
|
125
224
|
// If local and imported are the same, only visit local to avoid duplicates
|
|
@@ -209,7 +308,20 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
209
308
|
|
|
210
309
|
// 3. Push closing tag name (not visited by AST walker)
|
|
211
310
|
if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
|
|
212
|
-
|
|
311
|
+
const closingName = node.closingElement.name.name;
|
|
312
|
+
// Check if this was capitalized (reverse lookup)
|
|
313
|
+
const originalName = reverseCapitalizedNames.get(closingName);
|
|
314
|
+
if (originalName) {
|
|
315
|
+
tokens.push({ source: originalName, generated: closingName });
|
|
316
|
+
} else {
|
|
317
|
+
// Check if this should be capitalized (forward lookup)
|
|
318
|
+
const capitalizedName = capitalizedNames.get(closingName);
|
|
319
|
+
if (capitalizedName) {
|
|
320
|
+
tokens.push({ source: closingName, generated: capitalizedName });
|
|
321
|
+
} else {
|
|
322
|
+
tokens.push(closingName);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
213
325
|
}
|
|
214
326
|
|
|
215
327
|
return;
|
|
@@ -988,23 +1100,65 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
988
1100
|
});
|
|
989
1101
|
|
|
990
1102
|
// Process each token in order
|
|
991
|
-
for (const
|
|
992
|
-
|
|
993
|
-
|
|
1103
|
+
for (const token of tokens) {
|
|
1104
|
+
let sourceText, generatedText;
|
|
1105
|
+
|
|
1106
|
+
if (typeof token === 'string') {
|
|
1107
|
+
sourceText = token;
|
|
1108
|
+
generatedText = token;
|
|
1109
|
+
} else {
|
|
1110
|
+
// Token with different source and generated names
|
|
1111
|
+
sourceText = token.source;
|
|
1112
|
+
generatedText = token.generated;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const sourcePos = findInSource(sourceText);
|
|
1116
|
+
const genPos = findInGenerated(generatedText);
|
|
994
1117
|
|
|
995
1118
|
if (sourcePos !== null && genPos !== null) {
|
|
996
1119
|
mappings.push({
|
|
997
1120
|
sourceOffsets: [sourcePos],
|
|
998
1121
|
generatedOffsets: [genPos],
|
|
999
|
-
lengths: [
|
|
1122
|
+
lengths: [sourceText.length],
|
|
1000
1123
|
data: mapping_data,
|
|
1001
1124
|
});
|
|
1002
1125
|
}
|
|
1003
1126
|
}
|
|
1004
1127
|
|
|
1128
|
+
// Add full-statement mappings for import declarations
|
|
1129
|
+
// TypeScript reports unused import diagnostics covering the entire import statement
|
|
1130
|
+
// Use verification-only mapping to avoid duplicate hover/completion
|
|
1131
|
+
for (const importDecl of importDeclarations) {
|
|
1132
|
+
const length = importDecl.end - importDecl.start;
|
|
1133
|
+
mappings.push({
|
|
1134
|
+
sourceOffsets: [importDecl.start],
|
|
1135
|
+
generatedOffsets: [importDecl.start], // Same position in generated code
|
|
1136
|
+
lengths: [length],
|
|
1137
|
+
data: {
|
|
1138
|
+
// only verification (diagnostics) to avoid duplicate hover/completion
|
|
1139
|
+
verification: true
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1005
1144
|
// Sort mappings by source offset
|
|
1006
1145
|
mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
|
|
1007
1146
|
|
|
1147
|
+
// Add a mapping for the very beginning of the file to handle import additions
|
|
1148
|
+
// This ensures that code actions adding imports at the top work correctly
|
|
1149
|
+
if (mappings.length > 0 && mappings[0].sourceOffsets[0] > 0) {
|
|
1150
|
+
mappings.unshift({
|
|
1151
|
+
sourceOffsets: [0],
|
|
1152
|
+
generatedOffsets: [0],
|
|
1153
|
+
lengths: [1],
|
|
1154
|
+
data: {
|
|
1155
|
+
...mapping_data,
|
|
1156
|
+
codeActions: true, // auto-import
|
|
1157
|
+
rename: false, // avoid rename for a “dummy” mapping
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1008
1162
|
return {
|
|
1009
1163
|
code: generated_code,
|
|
1010
1164
|
mappings,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/** @import { Block,
|
|
1
|
+
/** @import { Block, Tracked } from '#client' */
|
|
2
2
|
|
|
3
|
-
import { destroy_block, root } from './internal/client/blocks.js';
|
|
4
|
-
import { handle_root_events } from './internal/client/events.js';
|
|
3
|
+
import { destroy_block, effect, render, root } from './internal/client/blocks.js';
|
|
4
|
+
import { handle_root_events, on } from './internal/client/events.js';
|
|
5
5
|
import { init_operations } from './internal/client/operations.js';
|
|
6
|
-
import { active_block,
|
|
7
|
-
import { create_anchor } from './internal/client/utils.js';
|
|
6
|
+
import { active_block, get, set, tick } from './internal/client/runtime.js';
|
|
7
|
+
import { create_anchor, is_array, is_tracked_object } from './internal/client/utils.js';
|
|
8
8
|
import { remove_ssr_css } from './internal/client/css.js';
|
|
9
9
|
|
|
10
10
|
// Re-export JSX runtime functions for jsxImportSource: "ripple"
|
|
@@ -77,3 +77,183 @@ export { Portal } from './internal/client/portal.js';
|
|
|
77
77
|
export { ref_prop as createRefKey } from './internal/client/runtime.js';
|
|
78
78
|
|
|
79
79
|
export { on } from './internal/client/events.js';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} value
|
|
83
|
+
*/
|
|
84
|
+
function to_number(value) {
|
|
85
|
+
return value === '' ? null : +value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {HTMLInputElement} input
|
|
90
|
+
*/
|
|
91
|
+
function is_numberlike_input(input) {
|
|
92
|
+
var type = input.type;
|
|
93
|
+
return type === 'number' || type === 'range';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {HTMLOptionElement} option */
|
|
97
|
+
function get_option_value(option) {
|
|
98
|
+
return option.value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Selects the correct option(s) (depending on whether this is a multiple select)
|
|
103
|
+
* @template V
|
|
104
|
+
* @param {HTMLSelectElement} select
|
|
105
|
+
* @param {V} value
|
|
106
|
+
* @param {boolean} mounting
|
|
107
|
+
*/
|
|
108
|
+
function select_option(select, value, mounting = false) {
|
|
109
|
+
if (select.multiple) {
|
|
110
|
+
// If value is null or undefined, keep the selection as is
|
|
111
|
+
if (value == undefined) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If not an array, warn and keep the selection as is
|
|
116
|
+
if (!is_array(value)) {
|
|
117
|
+
// TODO
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Otherwise, update the selection
|
|
121
|
+
for (var option of select.options) {
|
|
122
|
+
option.selected = /** @type {string[]} */ (value).includes(get_option_value(option));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (option of select.options) {
|
|
129
|
+
var option_value = get_option_value(option);
|
|
130
|
+
if (option_value === value) {
|
|
131
|
+
option.selected = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!mounting || value !== undefined) {
|
|
137
|
+
select.selectedIndex = -1; // no option should be selected
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {unknown} maybe_tracked
|
|
143
|
+
* @returns {(node: HTMLInputElement | HTMLSelectElement) => void}
|
|
144
|
+
*/
|
|
145
|
+
export function bindValue(maybe_tracked) {
|
|
146
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
147
|
+
throw new TypeError('bindValue() argument is not a tracked object');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var block = /** @type {Block} */ (active_block);
|
|
151
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
152
|
+
|
|
153
|
+
return (node) => {
|
|
154
|
+
var clear_event;
|
|
155
|
+
|
|
156
|
+
if (node.tagName === 'SELECT') {
|
|
157
|
+
var select = /** @type {HTMLSelectElement} */ (node);
|
|
158
|
+
var mounting = true;
|
|
159
|
+
|
|
160
|
+
clear_event = on(select, 'change', async () => {
|
|
161
|
+
var query = ':checked';
|
|
162
|
+
/** @type {unknown} */
|
|
163
|
+
var value;
|
|
164
|
+
|
|
165
|
+
if (select.multiple) {
|
|
166
|
+
value = [].map.call(select.querySelectorAll(query), get_option_value);
|
|
167
|
+
} else {
|
|
168
|
+
/** @type {HTMLOptionElement | null} */
|
|
169
|
+
var selected_option =
|
|
170
|
+
select.querySelector(query) ??
|
|
171
|
+
// will fall back to first non-disabled option if no option is selected
|
|
172
|
+
select.querySelector('option:not([disabled])');
|
|
173
|
+
value = selected_option && get_option_value(selected_option);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
set(tracked, value, block);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
effect(() => {
|
|
180
|
+
var value = get(tracked);
|
|
181
|
+
select_option(select, value, mounting);
|
|
182
|
+
|
|
183
|
+
// Mounting and value undefined -> take selection from dom
|
|
184
|
+
if (mounting && value === undefined) {
|
|
185
|
+
/** @type {HTMLOptionElement | null} */
|
|
186
|
+
var selected_option = select.querySelector(':checked');
|
|
187
|
+
if (selected_option !== null) {
|
|
188
|
+
value = get_option_value(selected_option);
|
|
189
|
+
set(tracked, value, block);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
mounting = false;
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
var input = /** @type {HTMLInputElement} */ (node);
|
|
197
|
+
|
|
198
|
+
clear_event = on(input, 'input', async () => {
|
|
199
|
+
/** @type {any} */
|
|
200
|
+
var value = input.value;
|
|
201
|
+
value = is_numberlike_input(input) ? to_number(value) : value;
|
|
202
|
+
set(tracked, value, block);
|
|
203
|
+
|
|
204
|
+
await tick();
|
|
205
|
+
|
|
206
|
+
if (value !== (value = get(tracked))) {
|
|
207
|
+
var start = input.selectionStart;
|
|
208
|
+
var end = input.selectionEnd;
|
|
209
|
+
input.value = value ?? '';
|
|
210
|
+
|
|
211
|
+
// Restore selection
|
|
212
|
+
if (end !== null) {
|
|
213
|
+
input.selectionStart = start;
|
|
214
|
+
input.selectionEnd = Math.min(end, input.value.length);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
render(() => {
|
|
220
|
+
var value = get(tracked);
|
|
221
|
+
|
|
222
|
+
if (is_numberlike_input(input) && value === to_number(input.value)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (input.type === 'date' && !value && !input.value) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (value !== input.value) {
|
|
231
|
+
input.value = value ?? '';
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return clear_event;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {unknown} maybe_tracked
|
|
242
|
+
* @returns {(node: HTMLInputElement) => void}
|
|
243
|
+
*/
|
|
244
|
+
export function bindChecked(maybe_tracked) {
|
|
245
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
246
|
+
throw new TypeError('bindChecked() argument is not a tracked object');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const block = /** @type {any} */ (active_block);
|
|
250
|
+
const tracked = /** @type {Tracked<any>} */ (maybe_tracked);
|
|
251
|
+
|
|
252
|
+
return (input) => {
|
|
253
|
+
const clear_event = on(input, 'change', () => {
|
|
254
|
+
set(tracked, input.checked, block);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return clear_event;
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -269,16 +269,16 @@ var empty_get_set = { get: undefined, set: undefined };
|
|
|
269
269
|
/**
|
|
270
270
|
*
|
|
271
271
|
* @param {any} v
|
|
272
|
-
* @param {Block}
|
|
272
|
+
* @param {Block} block
|
|
273
273
|
* @param {(value: any) => any} [get]
|
|
274
274
|
* @param {(next: any, prev: any) => any} [set]
|
|
275
275
|
* @returns {Tracked}
|
|
276
276
|
*/
|
|
277
|
-
export function tracked(v,
|
|
277
|
+
export function tracked(v, block, get, set) {
|
|
278
278
|
// TODO: now we expose tracked, we should likely block access in DEV somehow
|
|
279
279
|
return {
|
|
280
280
|
a: get || set ? { get, set } : empty_get_set,
|
|
281
|
-
b,
|
|
281
|
+
b: block || active_block,
|
|
282
282
|
c: 0,
|
|
283
283
|
f: TRACKED,
|
|
284
284
|
v,
|
|
@@ -295,7 +295,7 @@ export function tracked(v, b, get, set) {
|
|
|
295
295
|
export function derived(fn, block, get, set) {
|
|
296
296
|
return {
|
|
297
297
|
a: get || set ? { get, set } : empty_get_set,
|
|
298
|
-
b: block,
|
|
298
|
+
b: block || active_block,
|
|
299
299
|
blocks: null,
|
|
300
300
|
c: 0,
|
|
301
301
|
co: active_component,
|