@destyler/file-upload 0.0.7-beta.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/dist/index.mjs ADDED
@@ -0,0 +1,572 @@
1
+ import { createAnatomy } from '@zag-js/anatomy';
2
+ import { createScope, getEventTarget, dataAttr, isSelfTarget, contains, visuallyHiddenStyle, addDomEvent, raf } from '@zag-js/dom-query';
3
+ import { formatBytes } from '@zag-js/i18n-utils';
4
+ import { isValidFileType, isValidFileSize, getAcceptAttrString, isFileEqual } from '@zag-js/file-utils';
5
+ import { createMachine, ref } from '@zag-js/core';
6
+ import { compact, callAll, createSplitProps } from '@zag-js/utils';
7
+ import { createProps } from '@zag-js/types';
8
+
9
+ const anatomy = createAnatomy("file-upload").parts(
10
+ "root",
11
+ "dropzone",
12
+ "item",
13
+ "itemDeleteTrigger",
14
+ "itemGroup",
15
+ "itemName",
16
+ "itemPreview",
17
+ "itemPreviewImage",
18
+ "itemSizeText",
19
+ "label",
20
+ "trigger",
21
+ "clearTrigger"
22
+ );
23
+ const parts = anatomy.build();
24
+
25
+ const dom = createScope({
26
+ getRootId: (ctx) => ctx.ids?.root ?? `file-upload:${ctx.id}`,
27
+ getDropzoneId: (ctx) => ctx.ids?.dropzone ?? `file-upload:${ctx.id}:dropzone`,
28
+ getHiddenInputId: (ctx) => ctx.ids?.hiddenInput ?? `file-upload:${ctx.id}:input`,
29
+ getTriggerId: (ctx) => ctx.ids?.trigger ?? `file-upload:${ctx.id}:trigger`,
30
+ getLabelId: (ctx) => ctx.ids?.label ?? `file-upload:${ctx.id}:label`,
31
+ getItemId: (ctx, id) => ctx.ids?.item?.(id) ?? `file-upload:${ctx.id}:item:${id}`,
32
+ getItemNameId: (ctx, id) => ctx.ids?.itemName?.(id) ?? `file-upload:${ctx.id}:item-name:${id}`,
33
+ getItemSizeTextId: (ctx, id) => ctx.ids?.itemSizeText?.(id) ?? `file-upload:${ctx.id}:item-size:${id}`,
34
+ getItemPreviewId: (ctx, id) => ctx.ids?.itemPreview?.(id) ?? `file-upload:${ctx.id}:item-preview:${id}`,
35
+ getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
36
+ getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
37
+ getDropzoneEl: (ctx) => dom.getById(ctx, dom.getDropzoneId(ctx))
38
+ });
39
+
40
+ function isEventWithFiles(event) {
41
+ const target = getEventTarget(event);
42
+ if (!event.dataTransfer)
43
+ return !!target && "files" in target;
44
+ return event.dataTransfer.types.some((type) => {
45
+ return type === "Files" || type === "application/x-moz-file";
46
+ });
47
+ }
48
+ function isFilesWithinRange(ctx, incomingCount) {
49
+ if (!ctx.multiple && incomingCount > 1)
50
+ return false;
51
+ if (!ctx.multiple && incomingCount + ctx.acceptedFiles.length === 2)
52
+ return true;
53
+ if (incomingCount + ctx.acceptedFiles.length > ctx.maxFiles)
54
+ return false;
55
+ return true;
56
+ }
57
+ function getFilesFromEvent(ctx, files) {
58
+ const acceptedFiles = [];
59
+ const rejectedFiles = [];
60
+ files.forEach((file) => {
61
+ const [accepted, acceptError] = isValidFileType(file, ctx.acceptAttr);
62
+ const [sizeMatch, sizeError] = isValidFileSize(file, ctx.minFileSize, ctx.maxFileSize);
63
+ const validateErrors = ctx.validate?.(file, {
64
+ acceptedFiles: ctx.acceptedFiles,
65
+ rejectedFiles: ctx.rejectedFiles
66
+ });
67
+ const valid = validateErrors ? validateErrors.length === 0 : true;
68
+ if (accepted && sizeMatch && valid) {
69
+ acceptedFiles.push(file);
70
+ } else {
71
+ const errors = [acceptError, sizeError];
72
+ if (!valid)
73
+ errors.push(...validateErrors ?? []);
74
+ rejectedFiles.push({ file, errors: errors.filter(Boolean) });
75
+ }
76
+ });
77
+ if (!isFilesWithinRange(ctx, acceptedFiles.length)) {
78
+ acceptedFiles.forEach((file) => {
79
+ rejectedFiles.push({ file, errors: ["TOO_MANY_FILES"] });
80
+ });
81
+ acceptedFiles.splice(0);
82
+ }
83
+ return {
84
+ acceptedFiles,
85
+ rejectedFiles
86
+ };
87
+ }
88
+
89
+ function connect(state, send, normalize) {
90
+ const disabled = state.context.disabled;
91
+ const allowDrop = state.context.allowDrop;
92
+ const translations = state.context.translations;
93
+ const dragging = state.matches("dragging");
94
+ const focused = state.matches("focused") && !disabled;
95
+ return {
96
+ dragging,
97
+ focused,
98
+ openFilePicker() {
99
+ send("OPEN");
100
+ },
101
+ deleteFile(file) {
102
+ send({ type: "FILE.DELETE", file });
103
+ },
104
+ acceptedFiles: state.context.acceptedFiles,
105
+ rejectedFiles: state.context.rejectedFiles,
106
+ setFiles(files) {
107
+ const count = files.length;
108
+ send({ type: "FILES.SET", files, count });
109
+ },
110
+ clearRejectedFiles() {
111
+ send({ type: "REJECTED_FILES.CLEAR" });
112
+ },
113
+ clearFiles() {
114
+ send({ type: "FILES.CLEAR" });
115
+ },
116
+ getFileSize(file) {
117
+ return formatBytes(file.size, state.context.locale);
118
+ },
119
+ createFileUrl(file, cb) {
120
+ const win = dom.getWin(state.context);
121
+ const url = win.URL.createObjectURL(file);
122
+ cb(url);
123
+ return () => win.URL.revokeObjectURL(url);
124
+ },
125
+ setClipboardFiles(dt) {
126
+ const items = Array.from(dt?.items ?? []);
127
+ const files = items.reduce((acc, item) => {
128
+ if (item.kind !== "file")
129
+ return acc;
130
+ const file = item.getAsFile();
131
+ if (!file)
132
+ return acc;
133
+ return [...acc, file];
134
+ }, []);
135
+ if (!files.length)
136
+ return false;
137
+ send({ type: "FILES.SET", files });
138
+ return true;
139
+ },
140
+ getRootProps() {
141
+ return normalize.element({
142
+ ...parts.root.attrs,
143
+ "dir": state.context.dir,
144
+ "id": dom.getRootId(state.context),
145
+ "data-disabled": dataAttr(disabled),
146
+ "data-dragging": dataAttr(dragging)
147
+ });
148
+ },
149
+ getDropzoneProps() {
150
+ return normalize.element({
151
+ ...parts.dropzone.attrs,
152
+ "dir": state.context.dir,
153
+ "id": dom.getDropzoneId(state.context),
154
+ "tabIndex": disabled ? void 0 : 0,
155
+ "role": "button",
156
+ "aria-label": translations.dropzone,
157
+ "aria-disabled": disabled,
158
+ "data-invalid": dataAttr(state.context.invalid),
159
+ "data-disabled": dataAttr(disabled),
160
+ "data-dragging": dataAttr(dragging),
161
+ onKeyDown(event) {
162
+ if (event.defaultPrevented)
163
+ return;
164
+ if (!isSelfTarget(event))
165
+ return;
166
+ if (event.key !== "Enter" && event.key !== " ")
167
+ return;
168
+ send({ type: "DROPZONE.CLICK", src: "keydown" });
169
+ },
170
+ onClick(event) {
171
+ const isLabel = event.currentTarget.localName === "label";
172
+ if (isLabel)
173
+ event.preventDefault();
174
+ send("DROPZONE.CLICK");
175
+ },
176
+ onDragOver(event) {
177
+ if (!allowDrop)
178
+ return;
179
+ event.preventDefault();
180
+ event.stopPropagation();
181
+ try {
182
+ event.dataTransfer.dropEffect = "copy";
183
+ } catch {
184
+ }
185
+ const hasFiles = isEventWithFiles(event);
186
+ if (!hasFiles)
187
+ return;
188
+ const count = event.dataTransfer.items.length;
189
+ send({ type: "DROPZONE.DRAG_OVER", count });
190
+ },
191
+ onDragLeave(event) {
192
+ if (!allowDrop || disabled)
193
+ return;
194
+ if (contains(event.currentTarget, event.relatedTarget))
195
+ return;
196
+ send({ type: "DROPZONE.DRAG_LEAVE" });
197
+ },
198
+ onDrop(event) {
199
+ if (allowDrop) {
200
+ event.preventDefault();
201
+ event.stopPropagation();
202
+ }
203
+ const hasFiles = isEventWithFiles(event);
204
+ if (disabled || !hasFiles)
205
+ return;
206
+ send({ type: "DROPZONE.DROP", files: Array.from(event.dataTransfer.files) });
207
+ },
208
+ onFocus() {
209
+ send("DROPZONE.FOCUS");
210
+ },
211
+ onBlur() {
212
+ send("DROPZONE.BLUR");
213
+ }
214
+ });
215
+ },
216
+ getTriggerProps() {
217
+ return normalize.button({
218
+ ...parts.trigger.attrs,
219
+ "dir": state.context.dir,
220
+ "id": dom.getTriggerId(state.context),
221
+ disabled,
222
+ "data-disabled": dataAttr(disabled),
223
+ "data-invalid": dataAttr(state.context.invalid),
224
+ "type": "button",
225
+ onClick(event) {
226
+ if (disabled)
227
+ return;
228
+ if (contains(dom.getDropzoneEl(state.context), event.currentTarget)) {
229
+ event.stopPropagation();
230
+ }
231
+ send("OPEN");
232
+ }
233
+ });
234
+ },
235
+ getHiddenInputProps() {
236
+ return normalize.input({
237
+ id: dom.getHiddenInputId(state.context),
238
+ tabIndex: -1,
239
+ disabled,
240
+ type: "file",
241
+ required: state.context.required,
242
+ capture: state.context.capture,
243
+ name: state.context.name,
244
+ accept: state.context.acceptAttr,
245
+ webkitdirectory: state.context.directory ? "" : void 0,
246
+ multiple: state.context.multiple || state.context.maxFiles > 1,
247
+ onClick(event) {
248
+ event.stopPropagation();
249
+ event.currentTarget.value = "";
250
+ },
251
+ onChange(event) {
252
+ if (disabled)
253
+ return;
254
+ const { files } = event.currentTarget;
255
+ send({ type: "FILES.SET", files: files ? Array.from(files) : [] });
256
+ },
257
+ style: visuallyHiddenStyle
258
+ });
259
+ },
260
+ getItemGroupProps() {
261
+ return normalize.element({
262
+ ...parts.itemGroup.attrs,
263
+ "dir": state.context.dir,
264
+ "data-disabled": dataAttr(disabled)
265
+ });
266
+ },
267
+ getItemProps(props) {
268
+ const { file } = props;
269
+ return normalize.element({
270
+ ...parts.item.attrs,
271
+ "dir": state.context.dir,
272
+ "id": dom.getItemId(state.context, file.name),
273
+ "data-disabled": dataAttr(disabled)
274
+ });
275
+ },
276
+ getItemNameProps(props) {
277
+ const { file } = props;
278
+ return normalize.element({
279
+ ...parts.itemName.attrs,
280
+ "dir": state.context.dir,
281
+ "id": dom.getItemNameId(state.context, file.name),
282
+ "data-disabled": dataAttr(disabled)
283
+ });
284
+ },
285
+ getItemSizeTextProps(props) {
286
+ const { file } = props;
287
+ return normalize.element({
288
+ ...parts.itemSizeText.attrs,
289
+ "dir": state.context.dir,
290
+ "id": dom.getItemSizeTextId(state.context, file.name),
291
+ "data-disabled": dataAttr(disabled)
292
+ });
293
+ },
294
+ getItemPreviewProps(props) {
295
+ const { file } = props;
296
+ return normalize.element({
297
+ ...parts.itemPreview.attrs,
298
+ "dir": state.context.dir,
299
+ "id": dom.getItemPreviewId(state.context, file.name),
300
+ "data-disabled": dataAttr(disabled)
301
+ });
302
+ },
303
+ getItemPreviewImageProps(props) {
304
+ const { file, url } = props;
305
+ const isImage = file.type.startsWith("image/");
306
+ if (!isImage) {
307
+ throw new Error("Preview Image is only supported for image files");
308
+ }
309
+ return normalize.img({
310
+ ...parts.itemPreviewImage.attrs,
311
+ "alt": translations.itemPreview?.(file),
312
+ "src": url,
313
+ "data-disabled": dataAttr(disabled)
314
+ });
315
+ },
316
+ getItemDeleteTriggerProps(props) {
317
+ const { file } = props;
318
+ return normalize.button({
319
+ ...parts.itemDeleteTrigger.attrs,
320
+ "dir": state.context.dir,
321
+ "type": "button",
322
+ disabled,
323
+ "data-disabled": dataAttr(disabled),
324
+ "aria-label": translations.deleteFile?.(file),
325
+ onClick() {
326
+ if (disabled)
327
+ return;
328
+ send({ type: "FILE.DELETE", file });
329
+ }
330
+ });
331
+ },
332
+ getLabelProps() {
333
+ return normalize.label({
334
+ ...parts.label.attrs,
335
+ "dir": state.context.dir,
336
+ "id": dom.getLabelId(state.context),
337
+ "htmlFor": dom.getHiddenInputId(state.context),
338
+ "data-disabled": dataAttr(disabled)
339
+ });
340
+ },
341
+ getClearTriggerProps() {
342
+ return normalize.button({
343
+ ...parts.clearTrigger.attrs,
344
+ "dir": state.context.dir,
345
+ "type": "button",
346
+ disabled,
347
+ "hidden": state.context.acceptedFiles.length === 0,
348
+ "data-disabled": dataAttr(disabled),
349
+ onClick(event) {
350
+ if (event.defaultPrevented)
351
+ return;
352
+ if (disabled)
353
+ return;
354
+ send({ type: "FILES.CLEAR" });
355
+ }
356
+ });
357
+ }
358
+ };
359
+ }
360
+
361
+ const invoke = {
362
+ change: (ctx) => {
363
+ ctx.onFileChange?.({
364
+ acceptedFiles: ctx.acceptedFiles,
365
+ rejectedFiles: ctx.rejectedFiles
366
+ });
367
+ },
368
+ accept: (ctx) => {
369
+ ctx.onFileAccept?.({ files: ctx.acceptedFiles });
370
+ },
371
+ reject: (ctx) => {
372
+ ctx.onFileReject?.({ files: ctx.rejectedFiles });
373
+ }
374
+ };
375
+ const set = {
376
+ files: (ctx, acceptedFiles, rejectedFiles) => {
377
+ ctx.acceptedFiles = ref(acceptedFiles);
378
+ invoke.accept(ctx);
379
+ if (rejectedFiles) {
380
+ ctx.rejectedFiles = ref(rejectedFiles);
381
+ invoke.reject(ctx);
382
+ }
383
+ invoke.change(ctx);
384
+ }
385
+ };
386
+ function machine(userContext) {
387
+ const ctx = compact(userContext);
388
+ return createMachine(
389
+ {
390
+ id: "fileupload",
391
+ initial: "idle",
392
+ context: {
393
+ minFileSize: 0,
394
+ maxFileSize: Number.POSITIVE_INFINITY,
395
+ maxFiles: 1,
396
+ allowDrop: true,
397
+ accept: ctx.accept,
398
+ preventDocumentDrop: true,
399
+ ...ctx,
400
+ acceptedFiles: ref([]),
401
+ rejectedFiles: ref([]),
402
+ translations: {
403
+ dropzone: "dropzone",
404
+ itemPreview: (file) => `preview of ${file.name}`,
405
+ deleteFile: (file) => `delete file ${file.name}`,
406
+ ...ctx.translations
407
+ }
408
+ },
409
+ computed: {
410
+ acceptAttr: (ctx2) => getAcceptAttrString(ctx2.accept),
411
+ multiple: (ctx2) => ctx2.maxFiles > 1
412
+ },
413
+ watch: {
414
+ acceptedFiles: ["syncInputElement"]
415
+ },
416
+ on: {
417
+ "FILES.SET": {
418
+ actions: ["setFilesFromEvent"]
419
+ },
420
+ "FILE.DELETE": {
421
+ actions: ["removeFile"]
422
+ },
423
+ "FILES.CLEAR": {
424
+ actions: ["clearFiles"]
425
+ },
426
+ "REJECTED_FILES.CLEAR": {
427
+ actions: ["clearRejectedFiles"]
428
+ }
429
+ },
430
+ activities: ["preventDocumentDrop"],
431
+ states: {
432
+ idle: {
433
+ on: {
434
+ "OPEN": {
435
+ actions: ["openFilePicker"]
436
+ },
437
+ "DROPZONE.CLICK": {
438
+ actions: ["openFilePicker"]
439
+ },
440
+ "DROPZONE.FOCUS": "focused",
441
+ "DROPZONE.DRAG_OVER": "dragging"
442
+ }
443
+ },
444
+ focused: {
445
+ on: {
446
+ "DROPZONE.BLUR": "idle",
447
+ "OPEN": {
448
+ actions: ["openFilePicker"]
449
+ },
450
+ "DROPZONE.CLICK": {
451
+ actions: ["openFilePicker"]
452
+ },
453
+ "DROPZONE.DRAG_OVER": "dragging"
454
+ }
455
+ },
456
+ dragging: {
457
+ on: {
458
+ "DROPZONE.DROP": {
459
+ target: "idle",
460
+ actions: ["setFilesFromEvent"]
461
+ },
462
+ "DROPZONE.DRAG_LEAVE": "idle"
463
+ }
464
+ }
465
+ }
466
+ },
467
+ {
468
+ activities: {
469
+ preventDocumentDrop(ctx2) {
470
+ if (!ctx2.preventDocumentDrop)
471
+ return;
472
+ if (!ctx2.allowDrop)
473
+ return;
474
+ if (ctx2.disabled)
475
+ return;
476
+ const doc = dom.getDoc(ctx2);
477
+ const onDragOver = (event) => {
478
+ event?.preventDefault();
479
+ };
480
+ const onDrop = (event) => {
481
+ if (contains(dom.getRootEl(ctx2), getEventTarget(event)))
482
+ return;
483
+ event.preventDefault();
484
+ };
485
+ return callAll(addDomEvent(doc, "dragover", onDragOver, false), addDomEvent(doc, "drop", onDrop, false));
486
+ }
487
+ },
488
+ actions: {
489
+ syncInputElement(ctx2) {
490
+ queueMicrotask(() => {
491
+ const inputEl = dom.getHiddenInputEl(ctx2);
492
+ if (!inputEl)
493
+ return;
494
+ const win = dom.getWin(ctx2);
495
+ const dataTransfer = new win.DataTransfer();
496
+ ctx2.acceptedFiles.forEach((v) => {
497
+ dataTransfer.items.add(v);
498
+ });
499
+ inputEl.files = dataTransfer.files;
500
+ });
501
+ },
502
+ openFilePicker(ctx2) {
503
+ raf(() => {
504
+ dom.getHiddenInputEl(ctx2)?.click();
505
+ });
506
+ },
507
+ setFilesFromEvent(ctx2, evt) {
508
+ const result = getFilesFromEvent(ctx2, evt.files);
509
+ const { acceptedFiles, rejectedFiles } = result;
510
+ if (ctx2.multiple) {
511
+ const files = ref([...ctx2.acceptedFiles, ...acceptedFiles]);
512
+ set.files(ctx2, files, rejectedFiles);
513
+ return;
514
+ }
515
+ if (acceptedFiles.length) {
516
+ const files = ref([acceptedFiles[0]]);
517
+ set.files(ctx2, files, rejectedFiles);
518
+ } else if (rejectedFiles.length) {
519
+ set.files(ctx2, ctx2.acceptedFiles, rejectedFiles);
520
+ }
521
+ },
522
+ removeFile(ctx2, evt) {
523
+ const nextFiles = ctx2.acceptedFiles.filter((file) => file !== evt.file);
524
+ ctx2.acceptedFiles = ref(nextFiles);
525
+ invoke.change(ctx2);
526
+ },
527
+ clearRejectedFiles(ctx2) {
528
+ ctx2.rejectedFiles = ref([]);
529
+ invoke.change(ctx2);
530
+ },
531
+ clearFiles(ctx2) {
532
+ ctx2.acceptedFiles = ref([]);
533
+ ctx2.rejectedFiles = ref([]);
534
+ invoke.change(ctx2);
535
+ }
536
+ },
537
+ compareFns: {
538
+ acceptedFiles: (a, b) => a.length === b.length && a.every((file, i) => isFileEqual(file, b[i]))
539
+ }
540
+ }
541
+ );
542
+ }
543
+
544
+ const props = createProps()([
545
+ "accept",
546
+ "allowDrop",
547
+ "capture",
548
+ "dir",
549
+ "directory",
550
+ "disabled",
551
+ "getRootNode",
552
+ "id",
553
+ "ids",
554
+ "locale",
555
+ "maxFiles",
556
+ "maxFileSize",
557
+ "minFileSize",
558
+ "name",
559
+ "invalid",
560
+ "onFileAccept",
561
+ "onFileReject",
562
+ "onFileChange",
563
+ "preventDocumentDrop",
564
+ "required",
565
+ "translations",
566
+ "validate"
567
+ ]);
568
+ const splitProps = createSplitProps(props);
569
+ const itemProps = createProps()(["file"]);
570
+ const splitItemProps = createSplitProps(itemProps);
571
+
572
+ export { anatomy, connect, itemProps, machine, props, splitItemProps, splitProps };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@destyler/file-upload",
3
+ "type": "module",
4
+ "version": "0.0.7-beta.1",
5
+ "description": "a component that is used to upload multiple files.",
6
+ "author": "Elone Hoo <elonehoo@gmail.com>",
7
+ "license": "MIT",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "default": "./dist/index.mjs"
12
+ },
13
+ "./*": "./*"
14
+ },
15
+ "main": "dist/index.cjs",
16
+ "module": "dist/index.mjs",
17
+ "types": "dist/index.d.ts",
18
+ "files": [
19
+ "*.d.ts",
20
+ "dist"
21
+ ],
22
+ "dependencies": {
23
+ "@zag-js/anatomy": "^0.80.0",
24
+ "@zag-js/core": "^0.80.0",
25
+ "@zag-js/dom-query": "^0.80.0",
26
+ "@zag-js/file-utils": "^0.80.0",
27
+ "@zag-js/i18n-utils": "^0.80.0",
28
+ "@zag-js/types": "^0.80.0",
29
+ "@zag-js/utils": "^0.80.0"
30
+ },
31
+ "scripts": {
32
+ "clear": "rimraf ./dist",
33
+ "clear:node_modules": "rimraf ./node_modules",
34
+ "build": "pnpm run clear && unbuild",
35
+ "stub": "unbuild --stub"
36
+ }
37
+ }