@octane-xplat/files 0.11.0 → 0.11.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.
@@ -0,0 +1,434 @@
1
+ import { files } from "./files.js";
2
+ import { cx, formatFileSize, normalizePicked, validateFiles } from "./file-input-utils.js";
3
+ import { defineUniversalComponent, hookSlots, universalChildren, universalComponent, universalPlan, universalProps, universalValue, useId, useImperativeHandle, useRef, useState, withSlot } from "@nativescript-community/octane";
4
+ import { Field, Icon, Pressable, useFieldControl } from "@octane-xplat/ui";
5
+ //#region src/FileInput.tsrx
6
+ var _hs$ = /* @__PURE__ */ hookSlots(8);
7
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
8
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
9
+ var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
10
+ var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
11
+ var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
12
+ var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
13
+ var _h$6 = /* @__PURE__ */ Symbol(_hs$ + 6);
14
+ var _h$7 = /* @__PURE__ */ Symbol(_hs$ + 7);
15
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
16
+ "kind": "host",
17
+ "type": "label",
18
+ "props": { "className": "vx-fileinput-itemsize" },
19
+ "bindings": [["text", 0]]
20
+ });
21
+ var __octaneUniversalPlan1 = universalPlan("nativescript", {
22
+ "kind": "host",
23
+ "type": "label",
24
+ "props": { "text": "×" }
25
+ });
26
+ var __octaneUniversalPlan2 = universalPlan("nativescript", {
27
+ "kind": "slot",
28
+ "slot": 0
29
+ });
30
+ var __octaneUniversalPlan3 = universalPlan("nativescript", {
31
+ "kind": "host",
32
+ "type": "flexboxlayout",
33
+ "propsSlot": 0,
34
+ "children": [
35
+ {
36
+ "kind": "slot",
37
+ "slot": 1
38
+ },
39
+ {
40
+ "kind": "host",
41
+ "type": "label",
42
+ "props": { "className": "vx-fileinput-itemname" },
43
+ "bindings": [["text", 2]]
44
+ },
45
+ {
46
+ "kind": "slot",
47
+ "slot": 3
48
+ },
49
+ {
50
+ "kind": "slot",
51
+ "slot": 4
52
+ }
53
+ ]
54
+ });
55
+ var __octaneUniversalPlan4 = universalPlan("nativescript", {
56
+ "kind": "host",
57
+ "type": "flexboxlayout",
58
+ "props": {
59
+ "flexDirection": "column",
60
+ "className": "vx-fileinput-list"
61
+ },
62
+ "children": [{
63
+ "kind": "slot",
64
+ "slot": 0
65
+ }]
66
+ });
67
+ var __octaneUniversalPlan5 = universalPlan("nativescript", {
68
+ "kind": "host",
69
+ "type": "flexboxlayout",
70
+ "props": {
71
+ "flexDirection": "row",
72
+ "alignItems": "center"
73
+ },
74
+ "children": [{
75
+ "kind": "slot",
76
+ "slot": 0
77
+ }, {
78
+ "kind": "host",
79
+ "type": "label",
80
+ "bindings": [["text", 1]]
81
+ }]
82
+ });
83
+ var __octaneUniversalPlan6 = universalPlan("nativescript", {
84
+ "kind": "host",
85
+ "type": "label",
86
+ "props": { "text": "×" }
87
+ });
88
+ var __octaneUniversalPlan7 = universalPlan("nativescript", {
89
+ "kind": "slot",
90
+ "slot": 0
91
+ });
92
+ var __octaneUniversalPlan8 = universalPlan("nativescript", {
93
+ "kind": "host",
94
+ "type": "label",
95
+ "bindings": [
96
+ ["className", 0],
97
+ ["text", 1],
98
+ ["accessibilityLabel", 2]
99
+ ]
100
+ });
101
+ var __octaneUniversalPlan9 = universalPlan("nativescript", {
102
+ "kind": "host",
103
+ "type": "label",
104
+ "bindings": [["className", 0], ["text", 1]]
105
+ });
106
+ var __octaneUniversalPlan10 = universalPlan("nativescript", {
107
+ "kind": "host",
108
+ "type": "flexboxlayout",
109
+ "props": { "flexDirection": "column" },
110
+ "bindings": [["className", 0], ["style", 1]],
111
+ "children": [
112
+ {
113
+ "kind": "slot",
114
+ "slot": 2
115
+ },
116
+ {
117
+ "kind": "slot",
118
+ "slot": 3
119
+ },
120
+ {
121
+ "kind": "slot",
122
+ "slot": 4
123
+ },
124
+ {
125
+ "kind": "slot",
126
+ "slot": 5
127
+ },
128
+ {
129
+ "kind": "slot",
130
+ "slot": 6
131
+ }
132
+ ]
133
+ });
134
+ var __octaneUniversalPlan11 = universalPlan("nativescript", {
135
+ "kind": "slot",
136
+ "slot": 0
137
+ });
138
+ var __octaneUniversalPlan12 = universalPlan("nativescript", {
139
+ "kind": "slot",
140
+ "slot": 0
141
+ });
142
+ var __octaneUniversalPlan13 = universalPlan("nativescript", {
143
+ "kind": "range",
144
+ "children": [{
145
+ "kind": "slot",
146
+ "slot": 0
147
+ }]
148
+ });
149
+ var STATUS_TEXT = {
150
+ error: "⚠",
151
+ warning: "⚠",
152
+ success: "✓"
153
+ };
154
+ var FileInputControl = defineUniversalComponent("nativescript", function FileInputControl(inputProps) {
155
+ const hostRef = useRef(null, _h$0);
156
+ const { props } = withSlot(_h$1, useFieldControl, inputProps);
157
+ const generatedId = useId(_h$2);
158
+ const inputId = props.id ?? generatedId;
159
+ const [busy, setBusy] = useState(false, _h$3);
160
+ const [validationErrors, setValidationErrors] = useState([], _h$4);
161
+ const files$1 = props.value == null ? [] : Array.isArray(props.value) ? props.value : [props.value];
162
+ const isDisabled = !!props.isDisabled || !!props.isReadOnly;
163
+ const isBusy = !!props.isLoading || busy;
164
+ const statusType = validationErrors.length ? "error" : props.status?.type;
165
+ const statusMessage = validationErrors.length ? validationErrors.join(" ") : props.status?.message;
166
+ const emitValue = (next) => props.isMultiple ? next : next[0] ?? null;
167
+ const fireChange = (next) => {
168
+ const emitted = emitValue(next);
169
+ props.onChange?.(emitted);
170
+ if (props.changeAction) {
171
+ setBusy(true);
172
+ Promise.resolve(props.changeAction(emitted)).finally(() => setBusy(false));
173
+ }
174
+ };
175
+ const acceptRefs = (picked) => {
176
+ if (isDisabled || isBusy || !picked.length) return;
177
+ const merged = props.isMultiple ? [...files$1, ...picked] : picked;
178
+ const { valid, errors } = validateFiles(merged, {
179
+ accept: props.accept,
180
+ maxSize: props.maxSize,
181
+ maxFiles: props.maxFiles
182
+ });
183
+ setValidationErrors(errors);
184
+ const next = !props.isMultiple && errors.length ? files$1 : valid;
185
+ if (next.length !== files$1.length || next.some((file, index) => file.uri !== files$1[index]?.uri)) fireChange(next);
186
+ };
187
+ const removeAt = (index) => {
188
+ if (isDisabled || isBusy) return;
189
+ setValidationErrors([]);
190
+ fireChange(files$1.filter((_, i) => i !== index));
191
+ };
192
+ const pick = () => {
193
+ if (isDisabled || isBusy) return;
194
+ const options = {
195
+ accept: props.accept,
196
+ multiple: props.isMultiple
197
+ };
198
+ const picked = props.pick ? props.pick(options) : props.isMultiple ? files.pickMultiple(props.accept) : files.pick(props.accept);
199
+ Promise.resolve(picked).then((result) => acceptRefs(normalizePicked(result))).catch((error) => setValidationErrors([String(error?.message ?? error)]));
200
+ };
201
+ const dropzone = props.mode === "dropzone";
202
+ const statusText = statusType ? STATUS_TEXT[statusType] : null;
203
+ useImperativeHandle(props.ref, () => ({
204
+ open: pick,
205
+ native: hostRef.current
206
+ }), void 0, _h$5);
207
+ return universalValue(__octaneUniversalPlan10, [
208
+ cx("vx-fileinput", dropzone && "vx-fileinput--dropzone", isDisabled && "vx-fileinput--disabled", isBusy && "vx-fileinput--loading", statusType && `vx-fileinput--status-${statusType}`, props.size && `vx-fileinput--size-${props.size}`, props.className),
209
+ props.style,
210
+ files$1.length ? universalValue(__octaneUniversalPlan4, [files$1.map((f, i) => universalValue(__octaneUniversalPlan3, [
211
+ universalProps([
212
+ [
213
+ "set",
214
+ "key",
215
+ `${f.uri}-${i}`
216
+ ],
217
+ [
218
+ "set",
219
+ "flexDirection",
220
+ "row"
221
+ ],
222
+ [
223
+ "set",
224
+ "alignItems",
225
+ "center"
226
+ ],
227
+ [
228
+ "set",
229
+ "className",
230
+ "vx-fileinput-item"
231
+ ]
232
+ ]),
233
+ universalComponent("nativescript", Icon, universalProps([[
234
+ "set",
235
+ "name",
236
+ "xplat-upload"
237
+ ], [
238
+ "set",
239
+ "size",
240
+ 14
241
+ ]])),
242
+ f.name,
243
+ f.size != null ? universalValue(__octaneUniversalPlan0, [formatFileSize(f.size)]) : null,
244
+ !isDisabled && !isBusy ? universalValue(__octaneUniversalPlan2, [universalComponent("nativescript", Pressable, universalProps([
245
+ [
246
+ "set",
247
+ "className",
248
+ "vx-fileinput-itemremove"
249
+ ],
250
+ [
251
+ "set",
252
+ "accessibilityLabel",
253
+ `Remove ${f.name}`
254
+ ],
255
+ [
256
+ "set",
257
+ "onPress",
258
+ () => removeAt(i)
259
+ ]
260
+ ], universalChildren("nativescript", () => {
261
+ return universalValue(__octaneUniversalPlan1, []);
262
+ })))]) : null
263
+ ]))]) : null,
264
+ universalComponent("nativescript", Pressable, universalProps([
265
+ [
266
+ "set",
267
+ "ref",
268
+ (v) => {
269
+ hostRef.current = v;
270
+ }
271
+ ],
272
+ [
273
+ "set",
274
+ "id",
275
+ inputId
276
+ ],
277
+ [
278
+ "set",
279
+ "className",
280
+ "vx-fileinput-trigger"
281
+ ],
282
+ [
283
+ "set",
284
+ "disabled",
285
+ isDisabled || isBusy
286
+ ],
287
+ [
288
+ "set",
289
+ "accessibilityLabel",
290
+ props.accessibilityLabel ?? props.ariaLabel ?? props.fieldLabel ?? props.label
291
+ ],
292
+ [
293
+ "set",
294
+ "accessibilityHint",
295
+ props.accessibilityHint
296
+ ],
297
+ [
298
+ "set",
299
+ "accessibilityState",
300
+ {
301
+ disabled: isDisabled,
302
+ busy: isBusy
303
+ }
304
+ ],
305
+ [
306
+ "set",
307
+ "onPress",
308
+ pick
309
+ ]
310
+ ], universalChildren("nativescript", () => {
311
+ return universalValue(__octaneUniversalPlan5, [universalComponent("nativescript", Icon, universalProps([[
312
+ "set",
313
+ "name",
314
+ "xplat-upload"
315
+ ], [
316
+ "set",
317
+ "size",
318
+ 14
319
+ ]])), files$1.length ? "Choose more files" : "Choose files"]);
320
+ }))),
321
+ files$1.length > 0 && !isDisabled && !isBusy ? universalValue(__octaneUniversalPlan7, [universalComponent("nativescript", Pressable, universalProps([
322
+ [
323
+ "set",
324
+ "className",
325
+ "vx-fileinput-clear"
326
+ ],
327
+ [
328
+ "set",
329
+ "accessibilityLabel",
330
+ `Clear ${props.fieldLabel ?? props.label ?? "files"}`
331
+ ],
332
+ [
333
+ "set",
334
+ "onPress",
335
+ () => {
336
+ setValidationErrors([]);
337
+ fireChange([]);
338
+ }
339
+ ]
340
+ ], universalChildren("nativescript", () => {
341
+ return universalValue(__octaneUniversalPlan6, []);
342
+ })))]) : null,
343
+ statusText != null ? universalValue(__octaneUniversalPlan8, [
344
+ cx("vx-field-icon", `vx-field-icon--${statusType}`),
345
+ statusText,
346
+ props.statusVariant === "tooltip" ? statusMessage : void 0
347
+ ]) : null,
348
+ statusMessage && props.statusVariant !== "tooltip" ? universalValue(__octaneUniversalPlan9, [cx("vx-field-status", `vx-field-status--${statusType}`), statusMessage]) : null
349
+ ]);
350
+ }, { "module": "@nativescript-community/octane" });
351
+ var FileInput = defineUniversalComponent("nativescript", function FileInput(props) {
352
+ const { inField } = withSlot(_h$6, useFieldControl, props);
353
+ const cid = useId(_h$7);
354
+ const control = universalValue(__octaneUniversalPlan11, [universalComponent("nativescript", FileInputControl, universalProps([["spread", props]]))]);
355
+ return universalValue(__octaneUniversalPlan13, [inField ? control : universalValue(__octaneUniversalPlan12, [universalComponent("nativescript", Field, universalProps([
356
+ [
357
+ "set",
358
+ "inputID",
359
+ props.id ?? cid
360
+ ],
361
+ [
362
+ "set",
363
+ "label",
364
+ props.label
365
+ ],
366
+ [
367
+ "set",
368
+ "isLabelHidden",
369
+ props.isLabelHidden
370
+ ],
371
+ [
372
+ "set",
373
+ "description",
374
+ props.description
375
+ ],
376
+ [
377
+ "set",
378
+ "isDisabled",
379
+ props.isDisabled
380
+ ],
381
+ [
382
+ "set",
383
+ "disabledMessage",
384
+ props.disabledMessage
385
+ ],
386
+ [
387
+ "set",
388
+ "isReadOnly",
389
+ props.isReadOnly
390
+ ],
391
+ [
392
+ "set",
393
+ "isLoading",
394
+ props.isLoading
395
+ ],
396
+ [
397
+ "set",
398
+ "isRequired",
399
+ props.isRequired
400
+ ],
401
+ [
402
+ "set",
403
+ "isOptional",
404
+ props.isOptional
405
+ ],
406
+ [
407
+ "set",
408
+ "size",
409
+ props.size
410
+ ],
411
+ [
412
+ "set",
413
+ "status",
414
+ props.status
415
+ ],
416
+ [
417
+ "set",
418
+ "statusVariant",
419
+ props.statusVariant
420
+ ],
421
+ [
422
+ "set",
423
+ "labelTooltip",
424
+ props.labelTooltip
425
+ ],
426
+ [
427
+ "set",
428
+ "width",
429
+ props.width
430
+ ]
431
+ ], control))])]);
432
+ }, { "module": "@nativescript-community/octane" });
433
+ //#endregion
434
+ export { FileInput };
@@ -0,0 +1,45 @@
1
+ //#region src/file-bytes.ts
2
+ /**
3
+ * Shared helpers for the binary file primitives (readBytes/writeBytes/export).
4
+ * DOM-free so the native leaf can use it too.
5
+ */
6
+ /** File names used for sandbox writes and export temp files must be bare
7
+ * names, not paths — a `/`, `\`, or `..` segment escapes the sandbox. */
8
+ function assertSafeFileName(name) {
9
+ if (typeof name !== "string" || name.length === 0 || name === "." || name === ".." || name.includes("/") || name.includes("\\") || name.includes("\0")) throw new Error(`Unsafe file name: ${JSON.stringify(name)}`);
10
+ }
11
+ function fileTooLarge(name, maxBytes) {
12
+ return /* @__PURE__ */ new Error(`${name}: file exceeds the ${maxBytes}-byte limit`);
13
+ }
14
+ /** Named rejection for a second `files.export` while one is in flight. */
15
+ function exportBusy() {
16
+ const error = /* @__PURE__ */ new Error("files.export: an export is already in progress");
17
+ error.name = "ExportBusyError";
18
+ return error;
19
+ }
20
+ function throwIfAborted(signal) {
21
+ if (!signal?.aborted) return;
22
+ const thrower = signal.throwIfAborted;
23
+ if (typeof thrower === "function") thrower.call(signal);
24
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
25
+ error.name = "AbortError";
26
+ throw error;
27
+ }
28
+ /** Concatenate Uint8Array chunks into one exact-length array. */
29
+ function concatBytes(chunks, total) {
30
+ const out = new Uint8Array(total);
31
+ let offset = 0;
32
+ for (const chunk of chunks) {
33
+ out.set(chunk, offset);
34
+ offset += chunk.byteLength;
35
+ }
36
+ return out;
37
+ }
38
+ /** Return an ArrayBuffer covering exactly this view's byte range — native
39
+ * marshallers treat the argument as the whole buffer. */
40
+ function exactBuffer(bytes) {
41
+ if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) return bytes.buffer;
42
+ return bytes.slice().buffer;
43
+ }
44
+ //#endregion
45
+ export { assertSafeFileName, concatBytes, exactBuffer, exportBusy, fileTooLarge, throwIfAborted };
@@ -0,0 +1,52 @@
1
+ //#region src/file-input-utils.ts
2
+ function cx(...values) {
3
+ return values.filter(Boolean).join(" ");
4
+ }
5
+ function normalizePicked(result) {
6
+ if (result == null) return [];
7
+ return Array.isArray(result) ? result : [result];
8
+ }
9
+ function fileAccepts(file, accept) {
10
+ if (!accept) return true;
11
+ const tokens = accept.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
12
+ if (!tokens.length) return true;
13
+ const name = file.name.toLowerCase();
14
+ const mime = (file.mimeType ?? "").toLowerCase();
15
+ return tokens.some((token) => {
16
+ if (token === "*/*") return true;
17
+ if (token.startsWith(".")) return name.endsWith(token);
18
+ if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
19
+ return mime === token;
20
+ });
21
+ }
22
+ function formatFileSize(bytes) {
23
+ if (bytes < 1024) return `${bytes} B`;
24
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
25
+ if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
26
+ return `${(bytes / 1073741824).toFixed(1)} GB`;
27
+ }
28
+ function validateFiles(files, opts) {
29
+ const errors = [];
30
+ const valid = [];
31
+ for (const file of files) {
32
+ if (!fileAccepts(file, opts.accept)) {
33
+ errors.push(`${file.name}: file type not accepted`);
34
+ continue;
35
+ }
36
+ if (opts.maxSize != null && file.size != null && file.size > opts.maxSize) {
37
+ errors.push(`${file.name}: file exceeds ${formatFileSize(opts.maxSize)}`);
38
+ continue;
39
+ }
40
+ valid.push(file);
41
+ }
42
+ if (opts.maxFiles != null && valid.length > opts.maxFiles) {
43
+ const extra = valid.splice(opts.maxFiles);
44
+ for (const file of extra) errors.push(`${file.name}: exceeds the ${opts.maxFiles}-file limit`);
45
+ }
46
+ return {
47
+ valid,
48
+ errors
49
+ };
50
+ }
51
+ //#endregion
52
+ export { cx, fileAccepts, formatFileSize, normalizePicked, validateFiles };