ripple 0.2.172 → 0.2.174
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 +4 -3
- package/src/compiler/index.js +8 -6
- package/src/compiler/phases/3-transform/segments.js +161 -42
- package/src/runtime/index-client.js +1 -0
- package/src/runtime/internal/client/bindings.js +48 -26
- package/src/runtime/internal/client/blocks.js +4 -2
- package/src/runtime/internal/client/render.js +1 -1
- package/tests/client/basic/basic.components.test.ripple +25 -0
- package/tests/client/composite/composite.props.test.ripple +2 -2
- package/tests/client/input-value.test.ripple +297 -0
- package/types/index.d.ts +2 -0
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.174",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -78,9 +78,10 @@
|
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/estree": "^1.0.8",
|
|
80
80
|
"@types/node": "^24.3.0",
|
|
81
|
-
"typescript": "^5.9.2"
|
|
81
|
+
"typescript": "^5.9.2",
|
|
82
|
+
"@volar/language-core": "~2.4.23"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
|
-
"ripple": "0.2.
|
|
85
|
+
"ripple": "0.2.174"
|
|
85
86
|
}
|
|
86
87
|
}
|
package/src/compiler/index.js
CHANGED
|
@@ -25,20 +25,22 @@ export function parse(source) {
|
|
|
25
25
|
export function compile(source, filename, options = {}) {
|
|
26
26
|
const ast = parse_module(source);
|
|
27
27
|
const analysis = analyze(ast, filename, options);
|
|
28
|
-
const result =
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const result =
|
|
29
|
+
options.mode === 'server'
|
|
30
|
+
? transform_server(filename, source, analysis)
|
|
31
|
+
: transform_client(filename, source, analysis, false);
|
|
31
32
|
|
|
32
33
|
return result;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
|
|
37
|
+
/** @import { MappingsResult } from './phases/3-transform/segments.js' */
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Compile Ripple component to Volar virtual code with TypeScript mappings
|
|
39
41
|
* @param {string} source
|
|
40
42
|
* @param {string} filename
|
|
41
|
-
* @returns {
|
|
43
|
+
* @returns {MappingsResult} Volar mappings object
|
|
42
44
|
*/
|
|
43
45
|
export function compile_to_volar_mappings(source, filename) {
|
|
44
46
|
const ast = parse_module(source);
|
|
@@ -51,7 +53,7 @@ export function compile_to_volar_mappings(source, filename) {
|
|
|
51
53
|
source,
|
|
52
54
|
transformed.js.code,
|
|
53
55
|
transformed.js.map,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
/** @type {PostProcessingChanges} */ (transformed.js.post_processing_changes),
|
|
57
|
+
/** @type {LineOffsets} */ (transformed.js.line_offsets),
|
|
56
58
|
);
|
|
57
59
|
}
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
+
/** @typedef {import('@volar/language-core').CodeMapping} VolarCodeMapping */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} CustomMappingData
|
|
5
|
+
* @property {[number]} generatedLengths
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {VolarCodeMapping & {
|
|
10
|
+
* data: VolarCodeMapping['data'] & {
|
|
11
|
+
* customData: CustomMappingData
|
|
12
|
+
* }
|
|
13
|
+
* }} CodeMapping
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @typedef {{code: string, mappings: CodeMapping[]}} MappingsResult */
|
|
17
|
+
|
|
1
18
|
import { walk } from 'zimmerframe';
|
|
2
19
|
import { build_source_to_generated_map, get_generated_position } from '../../source-map-utils.js';
|
|
3
20
|
|
|
21
|
+
/** @type {VolarCodeMapping['data']} */
|
|
4
22
|
export const mapping_data = {
|
|
5
23
|
verification: true,
|
|
6
24
|
completion: true,
|
|
7
25
|
semantic: true,
|
|
8
26
|
navigation: true,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
formatting: false, // not doing formatting through Volar, using Prettier.
|
|
12
|
-
// these 3 below will be true by default
|
|
13
|
-
// leaving for reference
|
|
14
|
-
// hover: true,
|
|
15
|
-
// definition: true,
|
|
16
|
-
// references: true,
|
|
27
|
+
structure: true,
|
|
28
|
+
format: false,
|
|
17
29
|
};
|
|
18
30
|
|
|
19
31
|
/**
|
|
@@ -28,14 +40,24 @@ export const mapping_data = {
|
|
|
28
40
|
* @param {object} esrap_source_map - Esrap source map for accurate position lookup
|
|
29
41
|
* @param {PostProcessingChanges } post_processing_changes - Optional post-processing changes
|
|
30
42
|
* @param {number[]} line_offsets - Pre-computed line offsets array for generated code
|
|
31
|
-
* @returns {
|
|
43
|
+
* @returns {MappingsResult}
|
|
32
44
|
*/
|
|
33
|
-
export function convert_source_map_to_mappings(
|
|
34
|
-
|
|
45
|
+
export function convert_source_map_to_mappings(
|
|
46
|
+
ast,
|
|
47
|
+
source,
|
|
48
|
+
generated_code,
|
|
49
|
+
esrap_source_map,
|
|
50
|
+
post_processing_changes,
|
|
51
|
+
line_offsets,
|
|
52
|
+
) {
|
|
53
|
+
/** @type {CodeMapping[]} */
|
|
35
54
|
const mappings = [];
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Converts line/column positions to byte offsets
|
|
58
|
+
* @param {string} text
|
|
59
|
+
* @returns {number[]}
|
|
60
|
+
*/
|
|
39
61
|
const build_line_offsets = (text) => {
|
|
40
62
|
const offsets = [0]; // Line 1 starts at offset 0
|
|
41
63
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -73,7 +95,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
73
95
|
const adjusted_source_map = build_source_to_generated_map(
|
|
74
96
|
esrap_source_map,
|
|
75
97
|
post_processing_changes,
|
|
76
|
-
line_offsets
|
|
98
|
+
line_offsets,
|
|
77
99
|
);
|
|
78
100
|
|
|
79
101
|
// Collect text tokens from AST nodes
|
|
@@ -96,11 +118,19 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
96
118
|
if (node.name && node.loc) {
|
|
97
119
|
// Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
|
|
98
120
|
if (node.metadata?.tracked_shorthand) {
|
|
99
|
-
tokens.push({
|
|
121
|
+
tokens.push({
|
|
122
|
+
source: node.metadata.tracked_shorthand,
|
|
123
|
+
generated: node.name,
|
|
124
|
+
loc: node.loc,
|
|
125
|
+
});
|
|
100
126
|
} else if (node.metadata?.is_capitalized) {
|
|
101
127
|
// This identifier was capitalized during transformation
|
|
102
128
|
// Map the original lowercase name to the capitalized generated name
|
|
103
|
-
tokens.push({
|
|
129
|
+
tokens.push({
|
|
130
|
+
source: node.metadata.original_name,
|
|
131
|
+
generated: node.name,
|
|
132
|
+
loc: node.loc,
|
|
133
|
+
});
|
|
104
134
|
} else {
|
|
105
135
|
// No transformation - source and generated names are the same
|
|
106
136
|
tokens.push({ source: node.name, generated: node.name, loc: node.loc });
|
|
@@ -111,7 +141,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
111
141
|
// JSXIdentifiers can also be capitalized (for dynamic components)
|
|
112
142
|
if (node.loc && node.name) {
|
|
113
143
|
if (node.metadata?.is_capitalized) {
|
|
114
|
-
tokens.push({
|
|
144
|
+
tokens.push({
|
|
145
|
+
source: node.metadata.original_name,
|
|
146
|
+
generated: node.name,
|
|
147
|
+
loc: node.loc,
|
|
148
|
+
});
|
|
115
149
|
} else {
|
|
116
150
|
tokens.push({ source: node.name, generated: node.name, loc: node.loc });
|
|
117
151
|
}
|
|
@@ -130,7 +164,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
130
164
|
source: '',
|
|
131
165
|
generated: '',
|
|
132
166
|
loc: node.loc,
|
|
133
|
-
is_import_statement: true
|
|
167
|
+
is_import_statement: true,
|
|
134
168
|
});
|
|
135
169
|
}
|
|
136
170
|
|
|
@@ -152,7 +186,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
152
186
|
visit(node.local);
|
|
153
187
|
}
|
|
154
188
|
return;
|
|
155
|
-
} else if (
|
|
189
|
+
} else if (
|
|
190
|
+
node.type === 'ImportDefaultSpecifier' ||
|
|
191
|
+
node.type === 'ImportNamespaceSpecifier'
|
|
192
|
+
) {
|
|
156
193
|
// Just visit local
|
|
157
194
|
if (node.local) {
|
|
158
195
|
visit(node.local);
|
|
@@ -246,17 +283,32 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
// 3. Push closing tag name (not visited by AST walker)
|
|
249
|
-
if (
|
|
286
|
+
if (
|
|
287
|
+
!node.openingElement?.selfClosing &&
|
|
288
|
+
node.closingElement?.name?.type === 'JSXIdentifier'
|
|
289
|
+
) {
|
|
250
290
|
const closingNameNode = node.closingElement.name;
|
|
251
291
|
if (closingNameNode.metadata?.is_capitalized) {
|
|
252
|
-
tokens.push({
|
|
292
|
+
tokens.push({
|
|
293
|
+
source: closingNameNode.metadata.original_name,
|
|
294
|
+
generated: closingNameNode.name,
|
|
295
|
+
loc: closingNameNode.loc,
|
|
296
|
+
});
|
|
253
297
|
} else {
|
|
254
|
-
tokens.push({
|
|
298
|
+
tokens.push({
|
|
299
|
+
source: closingNameNode.name,
|
|
300
|
+
generated: closingNameNode.name,
|
|
301
|
+
loc: closingNameNode.loc,
|
|
302
|
+
});
|
|
255
303
|
}
|
|
256
304
|
}
|
|
257
305
|
|
|
258
306
|
return;
|
|
259
|
-
} else if (
|
|
307
|
+
} else if (
|
|
308
|
+
node.type === 'FunctionDeclaration' ||
|
|
309
|
+
node.type === 'FunctionExpression' ||
|
|
310
|
+
node.type === 'ArrowFunctionExpression'
|
|
311
|
+
) {
|
|
260
312
|
// Add function/component keyword token
|
|
261
313
|
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
|
|
262
314
|
const source_keyword = node.metadata?.was_component ? 'component' : 'function';
|
|
@@ -266,8 +318,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
266
318
|
generated: 'function',
|
|
267
319
|
loc: {
|
|
268
320
|
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
269
|
-
end: {
|
|
270
|
-
|
|
321
|
+
end: {
|
|
322
|
+
line: node.loc.start.line,
|
|
323
|
+
column: node.loc.start.column + source_keyword.length,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
271
326
|
});
|
|
272
327
|
}
|
|
273
328
|
|
|
@@ -654,7 +709,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
654
709
|
}
|
|
655
710
|
}
|
|
656
711
|
return;
|
|
657
|
-
} else if (
|
|
712
|
+
} else if (
|
|
713
|
+
node.type === 'JSXClosingElement' ||
|
|
714
|
+
node.type === 'JSXClosingFragment' ||
|
|
715
|
+
node.type === 'JSXOpeningFragment'
|
|
716
|
+
) {
|
|
658
717
|
// These are handled by their parent nodes
|
|
659
718
|
return;
|
|
660
719
|
} else if (node.type === 'JSXMemberExpression') {
|
|
@@ -736,7 +795,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
736
795
|
}
|
|
737
796
|
// Skip typeAnnotation
|
|
738
797
|
return;
|
|
739
|
-
} else if (
|
|
798
|
+
} else if (
|
|
799
|
+
node.type === 'TSTypeParameterInstantiation' ||
|
|
800
|
+
node.type === 'TSTypeParameterDeclaration'
|
|
801
|
+
) {
|
|
740
802
|
// Generic type parameters - visit to collect type variable names
|
|
741
803
|
if (node.params) {
|
|
742
804
|
for (const param of node.params) {
|
|
@@ -881,7 +943,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
881
943
|
visit(node.typeAnnotation);
|
|
882
944
|
}
|
|
883
945
|
return;
|
|
884
|
-
} else if (
|
|
946
|
+
} else if (
|
|
947
|
+
node.type === 'TSCallSignatureDeclaration' ||
|
|
948
|
+
node.type === 'TSConstructSignatureDeclaration'
|
|
949
|
+
) {
|
|
885
950
|
// Call or construct signature
|
|
886
951
|
if (node.typeParameters) {
|
|
887
952
|
visit(node.typeParameters);
|
|
@@ -1078,7 +1143,22 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
1078
1143
|
visit(node.typeAnnotation);
|
|
1079
1144
|
}
|
|
1080
1145
|
return;
|
|
1081
|
-
} else if (
|
|
1146
|
+
} else if (
|
|
1147
|
+
node.type === 'TSAnyKeyword' ||
|
|
1148
|
+
node.type === 'TSUnknownKeyword' ||
|
|
1149
|
+
node.type === 'TSNumberKeyword' ||
|
|
1150
|
+
node.type === 'TSObjectKeyword' ||
|
|
1151
|
+
node.type === 'TSBooleanKeyword' ||
|
|
1152
|
+
node.type === 'TSBigIntKeyword' ||
|
|
1153
|
+
node.type === 'TSStringKeyword' ||
|
|
1154
|
+
node.type === 'TSSymbolKeyword' ||
|
|
1155
|
+
node.type === 'TSVoidKeyword' ||
|
|
1156
|
+
node.type === 'TSUndefinedKeyword' ||
|
|
1157
|
+
node.type === 'TSNullKeyword' ||
|
|
1158
|
+
node.type === 'TSNeverKeyword' ||
|
|
1159
|
+
node.type === 'TSThisType' ||
|
|
1160
|
+
node.type === 'TSIntrinsicKeyword'
|
|
1161
|
+
) {
|
|
1082
1162
|
// Primitive type keywords - leaf nodes, no children
|
|
1083
1163
|
return;
|
|
1084
1164
|
} else if (node.type === 'TSDeclareFunction') {
|
|
@@ -1120,23 +1200,40 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
1120
1200
|
}
|
|
1121
1201
|
|
|
1122
1202
|
throw new Error(`Unhandled AST node type in mapping walker: ${node.type}`);
|
|
1123
|
-
}
|
|
1203
|
+
},
|
|
1124
1204
|
});
|
|
1125
1205
|
|
|
1126
1206
|
// Process each token in order
|
|
1127
1207
|
// All tokens now have .loc property - no need for fallback logic
|
|
1128
1208
|
for (const token of tokens) {
|
|
1129
1209
|
const source_text = token.source;
|
|
1210
|
+
const gen_text = token.generated;
|
|
1130
1211
|
|
|
1131
1212
|
// Handle import statement full-statement mapping
|
|
1132
1213
|
if (token.is_import_statement) {
|
|
1133
1214
|
// Get source position from start
|
|
1134
|
-
const source_start = loc_to_offset(
|
|
1135
|
-
|
|
1215
|
+
const source_start = loc_to_offset(
|
|
1216
|
+
token.loc.start.line,
|
|
1217
|
+
token.loc.start.column,
|
|
1218
|
+
source_line_offsets,
|
|
1219
|
+
);
|
|
1220
|
+
const source_end = loc_to_offset(
|
|
1221
|
+
token.loc.end.line,
|
|
1222
|
+
token.loc.end.column,
|
|
1223
|
+
source_line_offsets,
|
|
1224
|
+
);
|
|
1136
1225
|
|
|
1137
1226
|
// Get generated positions using source map
|
|
1138
|
-
const gen_start_pos = get_generated_position(
|
|
1139
|
-
|
|
1227
|
+
const gen_start_pos = get_generated_position(
|
|
1228
|
+
token.loc.start.line,
|
|
1229
|
+
token.loc.start.column,
|
|
1230
|
+
adjusted_source_map,
|
|
1231
|
+
);
|
|
1232
|
+
const gen_end_pos = get_generated_position(
|
|
1233
|
+
token.loc.end.line,
|
|
1234
|
+
token.loc.end.column,
|
|
1235
|
+
adjusted_source_map,
|
|
1236
|
+
);
|
|
1140
1237
|
|
|
1141
1238
|
if (source_start !== null && source_end !== null && gen_start_pos && gen_end_pos) {
|
|
1142
1239
|
// Convert generated line:col to byte offsets
|
|
@@ -1152,7 +1249,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
1152
1249
|
lengths: [Math.min(source_length, gen_length)],
|
|
1153
1250
|
data: {
|
|
1154
1251
|
// only verification (diagnostics) to avoid duplicate hover/completion
|
|
1155
|
-
verification: true
|
|
1252
|
+
verification: true,
|
|
1253
|
+
|
|
1254
|
+
customData: {
|
|
1255
|
+
generatedLengths: [gen_text.length],
|
|
1256
|
+
},
|
|
1156
1257
|
},
|
|
1157
1258
|
});
|
|
1158
1259
|
}
|
|
@@ -1160,25 +1261,42 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
1160
1261
|
}
|
|
1161
1262
|
|
|
1162
1263
|
// Use .loc to get the exact source position
|
|
1163
|
-
const source_pos = loc_to_offset(
|
|
1264
|
+
const source_pos = loc_to_offset(
|
|
1265
|
+
token.loc.start.line,
|
|
1266
|
+
token.loc.start.column,
|
|
1267
|
+
source_line_offsets,
|
|
1268
|
+
);
|
|
1164
1269
|
|
|
1165
1270
|
// Get generated position using source map
|
|
1166
|
-
const gen_line_col = get_generated_position(
|
|
1271
|
+
const gen_line_col = get_generated_position(
|
|
1272
|
+
token.loc.start.line,
|
|
1273
|
+
token.loc.start.column,
|
|
1274
|
+
adjusted_source_map,
|
|
1275
|
+
);
|
|
1167
1276
|
let gen_pos = null;
|
|
1168
1277
|
if (gen_line_col) {
|
|
1169
1278
|
// Convert generated line:col to byte offset
|
|
1170
1279
|
gen_pos = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
|
|
1171
1280
|
} else {
|
|
1172
1281
|
// No mapping found in source map - this shouldn't happen since all tokens should have mappings
|
|
1173
|
-
console.warn(
|
|
1282
|
+
console.warn(
|
|
1283
|
+
`[segments.js] No source map entry for token "${source_text}" at ${token.loc.start.line}:${token.loc.start.column}`,
|
|
1284
|
+
);
|
|
1174
1285
|
}
|
|
1175
1286
|
|
|
1176
1287
|
if (source_pos !== null && gen_pos !== null) {
|
|
1288
|
+
// !IMPORTANT: don't set generatedLengths, otherwise Volar will use that vs our source
|
|
1289
|
+
// We're adding it to our custom metadata instead as we need it for patching positions
|
|
1177
1290
|
mappings.push({
|
|
1178
1291
|
sourceOffsets: [source_pos],
|
|
1179
1292
|
generatedOffsets: [gen_pos],
|
|
1180
1293
|
lengths: [source_text.length],
|
|
1181
|
-
data:
|
|
1294
|
+
data: {
|
|
1295
|
+
...mapping_data,
|
|
1296
|
+
customData: {
|
|
1297
|
+
generatedLengths: [gen_text.length],
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1182
1300
|
});
|
|
1183
1301
|
}
|
|
1184
1302
|
}
|
|
@@ -1195,9 +1313,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
|
|
|
1195
1313
|
lengths: [1],
|
|
1196
1314
|
data: {
|
|
1197
1315
|
...mapping_data,
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1316
|
+
customData: {
|
|
1317
|
+
generatedLengths: [1],
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1201
1320
|
});
|
|
1202
1321
|
}
|
|
1203
1322
|
|
|
@@ -256,10 +256,10 @@ export function bindChecked(maybe_tracked) {
|
|
|
256
256
|
throw not_tracked_type_error('bindChecked()');
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
260
260
|
|
|
261
261
|
return (input) => {
|
|
262
|
-
|
|
262
|
+
var clear_event = on(input, 'change', () => {
|
|
263
263
|
set(tracked, input.checked);
|
|
264
264
|
});
|
|
265
265
|
|
|
@@ -281,10 +281,10 @@ export function bindIndeterminate(maybe_tracked) {
|
|
|
281
281
|
throw not_tracked_type_error('bindIndeterminate()');
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
285
285
|
|
|
286
286
|
return (input) => {
|
|
287
|
-
|
|
287
|
+
var clear_event = on(input, 'change', () => {
|
|
288
288
|
set(tracked, input.indeterminate);
|
|
289
289
|
});
|
|
290
290
|
|
|
@@ -311,32 +311,28 @@ export function bindGroup(maybe_tracked) {
|
|
|
311
311
|
return (input) => {
|
|
312
312
|
var is_checkbox = input.getAttribute('type') === 'checkbox';
|
|
313
313
|
|
|
314
|
-
// Store the input's value
|
|
315
|
-
// @ts-ignore
|
|
316
|
-
input.__value = input.value;
|
|
317
|
-
|
|
318
314
|
var clear_event = on(input, 'change', () => {
|
|
319
|
-
|
|
320
|
-
var
|
|
315
|
+
var value = input.value;
|
|
316
|
+
var result;
|
|
321
317
|
|
|
322
318
|
if (is_checkbox) {
|
|
323
319
|
/** @type {Array<any>} */
|
|
324
|
-
var
|
|
320
|
+
var list = get(tracked) || [];
|
|
325
321
|
|
|
326
322
|
if (input.checked) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
value = [...current_value, value];
|
|
323
|
+
if (!list.includes(value)) {
|
|
324
|
+
result = [...list, value];
|
|
330
325
|
} else {
|
|
331
|
-
|
|
326
|
+
result = list;
|
|
332
327
|
}
|
|
333
328
|
} else {
|
|
334
|
-
|
|
335
|
-
value = current_value.filter((v) => v !== value);
|
|
329
|
+
result = list.filter((v) => v !== value);
|
|
336
330
|
}
|
|
331
|
+
} else {
|
|
332
|
+
result = input.value;
|
|
337
333
|
}
|
|
338
334
|
|
|
339
|
-
set(tracked,
|
|
335
|
+
set(tracked, result);
|
|
340
336
|
});
|
|
341
337
|
|
|
342
338
|
effect(() => {
|
|
@@ -344,11 +340,9 @@ export function bindGroup(maybe_tracked) {
|
|
|
344
340
|
|
|
345
341
|
if (is_checkbox) {
|
|
346
342
|
value = value || [];
|
|
347
|
-
|
|
348
|
-
input.checked = value.includes(input.__value);
|
|
343
|
+
input.checked = value.includes(input.value);
|
|
349
344
|
} else {
|
|
350
|
-
|
|
351
|
-
input.checked = value === input.__value;
|
|
345
|
+
input.checked = value === input.value;
|
|
352
346
|
}
|
|
353
347
|
});
|
|
354
348
|
|
|
@@ -373,7 +367,7 @@ function bind_element_size(maybe_tracked, type) {
|
|
|
373
367
|
);
|
|
374
368
|
|
|
375
369
|
effect(() => {
|
|
376
|
-
|
|
370
|
+
set(tracked, element[type]);
|
|
377
371
|
return unsubscribe;
|
|
378
372
|
});
|
|
379
373
|
};
|
|
@@ -480,10 +474,10 @@ export function bind_content_editable(maybe_tracked, property) {
|
|
|
480
474
|
throw not_tracked_type_error(`bind${property.charAt(0).toUpperCase() + property.slice(1)}()`);
|
|
481
475
|
}
|
|
482
476
|
|
|
483
|
-
|
|
477
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
484
478
|
|
|
485
479
|
return (element) => {
|
|
486
|
-
|
|
480
|
+
var clear_event = on(element, 'input', () => {
|
|
487
481
|
set(tracked, element[property]);
|
|
488
482
|
});
|
|
489
483
|
|
|
@@ -530,6 +524,34 @@ export function bindTextContent(maybe_tracked) {
|
|
|
530
524
|
return bind_content_editable(maybe_tracked, 'textContent');
|
|
531
525
|
}
|
|
532
526
|
|
|
527
|
+
/**
|
|
528
|
+
* @param {unknown} maybe_tracked
|
|
529
|
+
* @returns {(node: HTMLInputElement) => void}
|
|
530
|
+
*/
|
|
531
|
+
export function bindFiles(maybe_tracked) {
|
|
532
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
533
|
+
throw not_tracked_type_error('bindFiles()');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
537
|
+
|
|
538
|
+
return (input) => {
|
|
539
|
+
var clear_event = on(input, 'change', () => {
|
|
540
|
+
set(tracked, input.files);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
effect(() => {
|
|
544
|
+
var value = get(tracked);
|
|
545
|
+
|
|
546
|
+
if (value !== input.files && value instanceof FileList) {
|
|
547
|
+
input.files = value;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return clear_event;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
533
555
|
/**
|
|
534
556
|
* Syntactic sugar for binding a HTMLElement with {ref fn}
|
|
535
557
|
* @param {unknown} maybe_tracked
|
|
@@ -540,7 +562,7 @@ export function bindNode(maybe_tracked) {
|
|
|
540
562
|
throw not_tracked_type_error('bindNode()');
|
|
541
563
|
}
|
|
542
564
|
|
|
543
|
-
|
|
565
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
544
566
|
|
|
545
567
|
/** @param {HTMLElement} node */
|
|
546
568
|
return (node) => {
|
|
@@ -369,8 +369,10 @@ export function destroy_block(block, remove_dom = true) {
|
|
|
369
369
|
|
|
370
370
|
if ((remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK)) !== 0) || (f & HEAD_BLOCK) !== 0) {
|
|
371
371
|
var s = block.s;
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
if (s !== null) {
|
|
373
|
+
remove_block_dom(s.start, s.end);
|
|
374
|
+
removed = true;
|
|
375
|
+
}
|
|
374
376
|
}
|
|
375
377
|
|
|
376
378
|
destroy_block_children(block, remove_dom && !removed);
|
|
@@ -129,7 +129,7 @@ function apply_styles(element, new_styles, prev) {
|
|
|
129
129
|
* @param {string} key
|
|
130
130
|
* @param {any} value
|
|
131
131
|
* @param {Record<string, (() => void) | undefined>} remove_listeners
|
|
132
|
-
* @param {Record<string, any>} prev
|
|
132
|
+
* @param {Record<string | symbol, any>} prev
|
|
133
133
|
*/
|
|
134
134
|
function set_attribute_helper(element, key, value, remove_listeners, prev) {
|
|
135
135
|
if (key === 'class') {
|
|
@@ -284,4 +284,29 @@ describe('basic client > components & composition', () => {
|
|
|
284
284
|
render(App);
|
|
285
285
|
}).not.toThrow();
|
|
286
286
|
});
|
|
287
|
+
|
|
288
|
+
it('handles component without any output', () => {
|
|
289
|
+
component Noop() {
|
|
290
|
+
// No output
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
component Op() {
|
|
294
|
+
<div>{'Some HTML content'}</div>
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
component App() {
|
|
298
|
+
let Content = track(() => Noop);
|
|
299
|
+
<@Content />
|
|
300
|
+
|
|
301
|
+
<button onClick={() => @Content = Op}>{'Show Op'}</button>
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
render(App);
|
|
305
|
+
|
|
306
|
+
const button = container.querySelector('button');
|
|
307
|
+
|
|
308
|
+
expect(() => {
|
|
309
|
+
button.click();
|
|
310
|
+
}).not.toThrow();
|
|
311
|
+
});
|
|
287
312
|
});
|
|
@@ -81,7 +81,7 @@ describe('composite > props', () => {
|
|
|
81
81
|
logs.push(@count);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
<button onClick={() => @count = @count + 1}>{'+'}</button>
|
|
84
|
+
<button onClick={() => (@count = @count + 1)}>{'+'}</button>
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
component App() {
|
|
@@ -122,7 +122,7 @@ describe('composite > props', () => {
|
|
|
122
122
|
|
|
123
123
|
component Toggle(props) {
|
|
124
124
|
const [pressed, rest] = trackSplit(props, ['pressed']);
|
|
125
|
-
const onClick = () => @pressed = !@pressed;
|
|
125
|
+
const onClick = () => (@pressed = !@pressed);
|
|
126
126
|
<Button {...@rest} class={@pressed ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
|
|
127
127
|
<Button class={@pressed ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
|
|
128
128
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
bindInnerText,
|
|
19
19
|
bindTextContent,
|
|
20
20
|
bindNode,
|
|
21
|
+
bindFiles,
|
|
21
22
|
} from 'ripple';
|
|
22
23
|
|
|
23
24
|
// Mock ResizeObserver for testing
|
|
@@ -57,9 +58,37 @@ function triggerResize(element: Element, entry: Partial<ResizeObserverEntry>) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Mock DataTransfer for testing file inputs
|
|
62
|
+
class MockDataTransfer {
|
|
63
|
+
items: MockDataTransferItemList;
|
|
64
|
+
files: FileList;
|
|
65
|
+
|
|
66
|
+
constructor() {
|
|
67
|
+
this.items = new MockDataTransferItemList();
|
|
68
|
+
this.files = this.items.files;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class MockDataTransferItemList {
|
|
73
|
+
_files: File[] = [];
|
|
74
|
+
|
|
75
|
+
get files(): FileList {
|
|
76
|
+
return this._files as any as FileList;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
add(file: File): void {
|
|
80
|
+
this._files.push(file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get length(): number {
|
|
84
|
+
return this._files.length;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
// Setup ResizeObserver mock
|
|
61
89
|
beforeAll(() => {
|
|
62
90
|
(global as any).ResizeObserver = createMockResizeObserver;
|
|
91
|
+
(global as any).DataTransfer = MockDataTransfer;
|
|
63
92
|
});
|
|
64
93
|
|
|
65
94
|
afterAll(() => {
|
|
@@ -1227,6 +1256,265 @@ describe('bindNode', () => {
|
|
|
1227
1256
|
});
|
|
1228
1257
|
});
|
|
1229
1258
|
|
|
1259
|
+
describe('bindFiles', () => {
|
|
1260
|
+
it('should bind files from file input', () => {
|
|
1261
|
+
const logs: FileList[] = [];
|
|
1262
|
+
|
|
1263
|
+
component App() {
|
|
1264
|
+
const files = track(null);
|
|
1265
|
+
|
|
1266
|
+
effect(() => {
|
|
1267
|
+
@files;
|
|
1268
|
+
if (@files) logs.push(@files);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
<input type="file" multiple {ref bindFiles(files)} />
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
render(App);
|
|
1275
|
+
flushSync();
|
|
1276
|
+
|
|
1277
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1278
|
+
|
|
1279
|
+
// Create mock FileList using DataTransfer
|
|
1280
|
+
const dt = new DataTransfer();
|
|
1281
|
+
const file1 = new File(['content1'], 'file1.txt', { type: 'text/plain' });
|
|
1282
|
+
const file2 = new File(['content2'], 'file2.txt', { type: 'text/plain' });
|
|
1283
|
+
dt.items.add(file1);
|
|
1284
|
+
dt.items.add(file2);
|
|
1285
|
+
|
|
1286
|
+
// Simulate file selection
|
|
1287
|
+
Object.defineProperty(input, 'files', {
|
|
1288
|
+
value: dt.files,
|
|
1289
|
+
writable: true,
|
|
1290
|
+
});
|
|
1291
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1292
|
+
flushSync();
|
|
1293
|
+
|
|
1294
|
+
expect(logs.length).toBeGreaterThan(0);
|
|
1295
|
+
const lastFiles = logs[logs.length - 1];
|
|
1296
|
+
expect(lastFiles.length).toBe(2);
|
|
1297
|
+
expect(lastFiles[0].name).toBe('file1.txt');
|
|
1298
|
+
expect(lastFiles[1].name).toBe('file2.txt');
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it('should update tracked value when files are selected', () => {
|
|
1302
|
+
let capturedFiles: FileList | null = null;
|
|
1303
|
+
|
|
1304
|
+
component App() {
|
|
1305
|
+
const files = track(null);
|
|
1306
|
+
|
|
1307
|
+
effect(() => {
|
|
1308
|
+
capturedFiles = @files;
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
<input type="file" {ref bindFiles(files)} />
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
render(App);
|
|
1315
|
+
flushSync();
|
|
1316
|
+
|
|
1317
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1318
|
+
|
|
1319
|
+
// Create mock file
|
|
1320
|
+
const dt = new DataTransfer();
|
|
1321
|
+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
|
1322
|
+
dt.items.add(file);
|
|
1323
|
+
|
|
1324
|
+
Object.defineProperty(input, 'files', {
|
|
1325
|
+
value: dt.files,
|
|
1326
|
+
writable: true,
|
|
1327
|
+
});
|
|
1328
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1329
|
+
flushSync();
|
|
1330
|
+
|
|
1331
|
+
expect(capturedFiles).not.toBeNull();
|
|
1332
|
+
expect(capturedFiles?.length).toBe(1);
|
|
1333
|
+
expect(capturedFiles?.[0].name).toBe('test.txt');
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('should allow clearing files via input.files', () => {
|
|
1337
|
+
let capturedFiles: FileList | null = null;
|
|
1338
|
+
|
|
1339
|
+
component App() {
|
|
1340
|
+
const files = track(null);
|
|
1341
|
+
const input = track(null);
|
|
1342
|
+
|
|
1343
|
+
effect(() => {
|
|
1344
|
+
capturedFiles = @files;
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
<div>
|
|
1348
|
+
<input type="file" {ref bindFiles(files)} {ref bindNode(input)} />
|
|
1349
|
+
<button
|
|
1350
|
+
onClick={() => {
|
|
1351
|
+
if (@input) {
|
|
1352
|
+
@input.files = new DataTransfer().files;
|
|
1353
|
+
@input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1354
|
+
}
|
|
1355
|
+
}}
|
|
1356
|
+
>
|
|
1357
|
+
{'Clear'}
|
|
1358
|
+
</button>
|
|
1359
|
+
</div>
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
render(App);
|
|
1363
|
+
flushSync();
|
|
1364
|
+
|
|
1365
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1366
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
1367
|
+
|
|
1368
|
+
// Add a file first
|
|
1369
|
+
const dt = new DataTransfer();
|
|
1370
|
+
const file = new File(['content'], 'file.txt', { type: 'text/plain' });
|
|
1371
|
+
dt.items.add(file);
|
|
1372
|
+
|
|
1373
|
+
Object.defineProperty(input, 'files', {
|
|
1374
|
+
value: dt.files,
|
|
1375
|
+
writable: true,
|
|
1376
|
+
});
|
|
1377
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1378
|
+
flushSync();
|
|
1379
|
+
|
|
1380
|
+
expect(capturedFiles?.length).toBe(1);
|
|
1381
|
+
|
|
1382
|
+
// Clear via button
|
|
1383
|
+
button.click();
|
|
1384
|
+
flushSync();
|
|
1385
|
+
|
|
1386
|
+
expect(capturedFiles?.length).toBe(0);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('should handle multiple file selections', () => {
|
|
1390
|
+
const fileCounts: number[] = [];
|
|
1391
|
+
|
|
1392
|
+
component App() {
|
|
1393
|
+
const files = track(null);
|
|
1394
|
+
|
|
1395
|
+
effect(() => {
|
|
1396
|
+
@files;
|
|
1397
|
+
if (@files) {
|
|
1398
|
+
fileCounts.push(@files.length);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
<input type="file" multiple {ref bindFiles(files)} />
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
render(App);
|
|
1406
|
+
flushSync();
|
|
1407
|
+
|
|
1408
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1409
|
+
|
|
1410
|
+
// First selection: 2 files
|
|
1411
|
+
const dt1 = new DataTransfer();
|
|
1412
|
+
dt1.items.add(new File(['a'], 'a.txt'));
|
|
1413
|
+
dt1.items.add(new File(['b'], 'b.txt'));
|
|
1414
|
+
|
|
1415
|
+
Object.defineProperty(input, 'files', {
|
|
1416
|
+
value: dt1.files,
|
|
1417
|
+
writable: true,
|
|
1418
|
+
});
|
|
1419
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1420
|
+
flushSync();
|
|
1421
|
+
|
|
1422
|
+
// Second selection: 3 files
|
|
1423
|
+
const dt2 = new DataTransfer();
|
|
1424
|
+
dt2.items.add(new File(['x'], 'x.txt'));
|
|
1425
|
+
dt2.items.add(new File(['y'], 'y.txt'));
|
|
1426
|
+
dt2.items.add(new File(['z'], 'z.txt'));
|
|
1427
|
+
|
|
1428
|
+
Object.defineProperty(input, 'files', {
|
|
1429
|
+
value: dt2.files,
|
|
1430
|
+
writable: true,
|
|
1431
|
+
});
|
|
1432
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1433
|
+
flushSync();
|
|
1434
|
+
|
|
1435
|
+
expect(fileCounts).toEqual([2, 3]);
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
it('should handle file input without multiple attribute', () => {
|
|
1439
|
+
let capturedFile: File | null = null;
|
|
1440
|
+
|
|
1441
|
+
component App() {
|
|
1442
|
+
const files = track(null);
|
|
1443
|
+
|
|
1444
|
+
effect(() => {
|
|
1445
|
+
@files;
|
|
1446
|
+
if (@files && @files.length > 0) {
|
|
1447
|
+
capturedFile = @files[0];
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
<input type="file" {ref bindFiles(files)} />
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
render(App);
|
|
1455
|
+
flushSync();
|
|
1456
|
+
|
|
1457
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1458
|
+
|
|
1459
|
+
const dt = new DataTransfer();
|
|
1460
|
+
const file = new File(['single file content'], 'single.pdf', { type: 'application/pdf' });
|
|
1461
|
+
dt.items.add(file);
|
|
1462
|
+
|
|
1463
|
+
Object.defineProperty(input, 'files', {
|
|
1464
|
+
value: dt.files,
|
|
1465
|
+
writable: true,
|
|
1466
|
+
});
|
|
1467
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1468
|
+
flushSync();
|
|
1469
|
+
|
|
1470
|
+
expect(capturedFile).not.toBeNull();
|
|
1471
|
+
expect(capturedFile?.name).toBe('single.pdf');
|
|
1472
|
+
expect(capturedFile?.type).toBe('application/pdf');
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it('should handle empty file selection', () => {
|
|
1476
|
+
const logs: (FileList | null)[] = [];
|
|
1477
|
+
|
|
1478
|
+
component App() {
|
|
1479
|
+
const files = track(null);
|
|
1480
|
+
|
|
1481
|
+
effect(() => {
|
|
1482
|
+
logs.push(@files);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
<input type="file" {ref bindFiles(files)} />
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
render(App);
|
|
1489
|
+
flushSync();
|
|
1490
|
+
|
|
1491
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1492
|
+
|
|
1493
|
+
// Select a file
|
|
1494
|
+
const dt = new DataTransfer();
|
|
1495
|
+
dt.items.add(new File(['test'], 'test.txt'));
|
|
1496
|
+
|
|
1497
|
+
Object.defineProperty(input, 'files', {
|
|
1498
|
+
value: dt.files,
|
|
1499
|
+
writable: true,
|
|
1500
|
+
});
|
|
1501
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1502
|
+
flushSync();
|
|
1503
|
+
|
|
1504
|
+
// Clear selection
|
|
1505
|
+
Object.defineProperty(input, 'files', {
|
|
1506
|
+
value: new DataTransfer().files,
|
|
1507
|
+
writable: true,
|
|
1508
|
+
});
|
|
1509
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1510
|
+
flushSync();
|
|
1511
|
+
|
|
1512
|
+
expect(logs.length).toBeGreaterThan(1);
|
|
1513
|
+
const lastFiles = logs[logs.length - 1];
|
|
1514
|
+
expect(lastFiles?.length).toBe(0);
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1230
1518
|
describe('error handling', () => {
|
|
1231
1519
|
it('should throw error when bindValue receives non-tracked object', () => {
|
|
1232
1520
|
expect(() => {
|
|
@@ -1371,4 +1659,13 @@ describe('error handling', () => {
|
|
|
1371
1659
|
render(App);
|
|
1372
1660
|
}).toThrow('bindNode() argument is not a tracked object');
|
|
1373
1661
|
});
|
|
1662
|
+
|
|
1663
|
+
it('should throw error when bindFiles receives non-tracked object', () => {
|
|
1664
|
+
expect(() => {
|
|
1665
|
+
component App() {
|
|
1666
|
+
<input type="file" {ref bindFiles({ value: null })} />
|
|
1667
|
+
}
|
|
1668
|
+
render(App);
|
|
1669
|
+
}).toThrow('bindFiles() argument is not a tracked object');
|
|
1670
|
+
});
|
|
1374
1671
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -307,3 +307,5 @@ export declare function bindOffsetHeight<V>(tracked: Tracked<V>): (node: HTMLEle
|
|
|
307
307
|
export declare function bindOffsetWidth<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
308
308
|
|
|
309
309
|
export declare function bindIndeterminate<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
310
|
+
|
|
311
|
+
export declare function bindFiles<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|