@georapbox/files-dropzone-element 2.0.0 → 2.1.0

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 CHANGED
@@ -1,51 +1,30 @@
1
1
  {
2
2
  "name": "@georapbox/files-dropzone-element",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A custom element that creates a drag and drop zone for files",
5
5
  "main": "dist/files-dropzone.js",
6
6
  "module": "dist/files-dropzone.js",
7
7
  "unpkg": "dist/files-dropzone.js",
8
8
  "type": "module",
9
9
  "types": "dist/files-dropzone.d.ts",
10
- "targets": {
11
- "files-dropzone": {
12
- "context": "browser",
13
- "outputFormat": "esmodule",
14
- "source": "src/files-dropzone.js",
15
- "optimize": true,
16
- "isLibrary": true,
17
- "distDir": "dist",
18
- "engines": {
19
- "browsers": "> 0.5%, last 2 versions, not dead"
20
- }
21
- },
22
- "files-dropzone-defined": {
23
- "context": "browser",
24
- "outputFormat": "esmodule",
25
- "source": "src/files-dropzone-defined.js",
26
- "optimize": true,
27
- "isLibrary": true,
28
- "distDir": "dist",
29
- "engines": {
30
- "browsers": "> 0.5%, last 2 versions, not dead"
31
- }
32
- }
33
- },
34
10
  "files": [
35
11
  "/src",
36
12
  "/dist"
37
13
  ],
38
14
  "scripts": {
39
- "lint": "eslint . --ext .js",
15
+ "lint": "eslint",
16
+ "format": "prettier --ignore-unknown --write .",
40
17
  "test": "web-test-runner",
41
18
  "test:watch": "npm run test -- --watch",
42
19
  "test:coverage": "npm run test -- --coverage",
43
20
  "types": "tsc --project tsconfig.json",
44
- "dev:parcel": "parcel watch",
45
- "dev": "npm-run-all clean dev:parcel",
46
- "build:parcel": "parcel build",
47
- "build": "npm-run-all clean build:parcel",
48
- "clean": "rimraf dist .parcel-cache",
21
+ "dev:esbuild": "node ./scripts/dev.mjs",
22
+ "dev": "npm-run-all clean dev:esbuild",
23
+ "serve:dev": "web-dev-server --node-resolve --open /docs/ --watch",
24
+ "start": "npm-run-all --parallel \"serve:dev -- {1}\" dev",
25
+ "build:esbuild": "node ./scripts/build.mjs",
26
+ "build": "npm-run-all clean build:esbuild",
27
+ "clean": "rimraf dist",
49
28
  "prepare": "npm-run-all clean lint test build types"
50
29
  },
51
30
  "keywords": [
@@ -67,17 +46,21 @@
67
46
  },
68
47
  "homepage": "https://github.com/georapbox/files-dropzone-element#readme",
69
48
  "devDependencies": {
70
- "@esm-bundle/chai": "~4.3.4-fix.0",
49
+ "@eslint/js": "~9.34.0",
50
+ "@esm-bundle/chai": "~4.3.4",
71
51
  "@open-wc/testing": "~4.0.0",
72
- "@web/test-runner": "~0.18.0",
52
+ "@web/dev-server": "~0.4.6",
53
+ "@web/test-runner": "~0.20.2",
73
54
  "@web/test-runner-playwright": "~0.11.0",
74
- "eslint": "~8.56.0",
75
- "eslint-plugin-wc": "~2.0.4",
55
+ "esbuild": "~0.25.9",
56
+ "eslint": "~9.34.0",
57
+ "eslint-plugin-wc": "~3.0.1",
58
+ "globals": "~16.3.0",
76
59
  "npm-run-all": "~4.1.5",
77
- "parcel": "~2.9.3",
78
- "rimraf": "~5.0.5",
79
- "sinon": "~17.0.1",
80
- "typescript": "~5.3.3"
60
+ "prettier": "~3.6.2",
61
+ "rimraf": "~6.0.1",
62
+ "sinon": "~21.0.0",
63
+ "typescript": "~5.9.2"
81
64
  },
82
65
  "publishConfig": {
83
66
  "access": "public"
@@ -7,17 +7,35 @@
7
7
  * @typedef {T | null} Nullable
8
8
  */
9
9
 
10
+ /**
11
+ * Stable identifier for error codes used in the `files-dropzone-error` event.
12
+ *
13
+ * @typedef {typeof FilesDropzone.ERROR_CODES[keyof typeof FilesDropzone.ERROR_CODES]} FilesDropzoneErrorCode
14
+ */
15
+
16
+ /**
17
+ * The detail payload for the `files-dropzone-error` event.
18
+ *
19
+ * @typedef {Object} FilesDropzoneErrorDetail
20
+ * @property {FilesDropzoneErrorCode} code - Stable identifier for the type of error that occurred.
21
+ * @property {unknown} error - The underlying error object or value that triggered the event.
22
+ */
23
+
24
+ /**
25
+ * Stable identifier for rejection codes used in the various drop-related events.
26
+ *
27
+ * @typedef {typeof FilesDropzone.REJECTION_CODES[keyof typeof FilesDropzone.REJECTION_CODES]} FilesDropzoneRejectionCode
28
+ */
29
+
10
30
  import { isValidFile } from './utils/is-valid-file.js';
11
31
  import { getFilesFromEvent } from './utils/files-selector.js';
12
32
 
33
+ const css = String.raw;
34
+ const html = String.raw;
13
35
  const COMPONENT_NAME = 'files-dropzone';
14
- const TOO_MANY_FILES = 'TOO_MANY_FILES';
15
- const FILE_TOO_LARGE = 'FILE_TOO_LARGE';
16
- const FILE_TOO_SMALL = 'FILE_TOO_SMALL';
17
- const INVALID_MIME_TYPE = 'INVALID_MIME_TYPE';
18
36
  const template = document.createElement('template');
19
37
 
20
- const styles = /* css */`
38
+ const styles = css`
21
39
  *,
22
40
  *::before,
23
41
  *::after {
@@ -42,7 +60,7 @@ const styles = /* css */`
42
60
  --dropzone-body-color: #3f3f46;
43
61
  --dropzone-body-color-dragover: var(--dropzone-body-color);
44
62
  --dropzone-body-color-hover: var(--dropzone-body-color-dragover);
45
- --dropzone-focus-shadow-rgb: 49,132,253;
63
+ --dropzone-focus-shadow-rgb: 49, 132, 253;
46
64
  --dropzone-focus-box-shadow: 0 0 0 0.25rem rgba(var(--dropzone-focus-shadow-rgb), 0.5);
47
65
  --transition-duration: 0.2s; /* for backwards compatibility */
48
66
  --dropzone-transition-duration: var(--transition-duration);
@@ -59,7 +77,11 @@ const styles = /* css */`
59
77
  color: var(--dropzone-body-color);
60
78
  text-align: center;
61
79
  cursor: pointer;
62
- transition: border var(--dropzone-transition-duration) ease-in-out, background-color var(--dropzone-transition-duration) ease-in-out, color var(--dropzone-transition-duration) ease-in-out, box-shadow var(--dropzone-transition-duration) ease-in-out;
80
+ transition:
81
+ border var(--dropzone-transition-duration) ease-in-out,
82
+ background-color var(--dropzone-transition-duration) ease-in-out,
83
+ color var(--dropzone-transition-duration) ease-in-out,
84
+ box-shadow var(--dropzone-transition-duration) ease-in-out;
63
85
  }
64
86
 
65
87
  :host(:not([no-style])[disabled]) .dropzone {
@@ -88,15 +110,15 @@ const styles = /* css */`
88
110
  }
89
111
  `;
90
112
 
91
- template.innerHTML = /* html */`
113
+ template.innerHTML = html`
92
114
  <style>
93
115
  ${styles}
94
116
  </style>
95
117
 
96
- <input type="file" id="file-input" hidden>
118
+ <input type="file" id="file-input" hidden />
97
119
 
98
120
  <div part="dropzone" class="dropzone" id="dropzone" tabindex="0" role="button" aria-disabled="false">
99
- <slot>Drag 'n' drop files here, or click to select files</slot>
121
+ <slot><span>Drag 'n' drop files here, or click to select files</span></slot>
100
122
  </div>
101
123
  `;
102
124
 
@@ -157,6 +179,28 @@ template.innerHTML = /* html */`
157
179
  * @method openFileDialog - Instance method. Opens the file dialog programmatically.
158
180
  */
159
181
  class FilesDropzone extends HTMLElement {
182
+ /**
183
+ * Central list of error codes used in drop error event.
184
+ * @readonly
185
+ */
186
+ static ERROR_CODES = /** @type {const} */ ({
187
+ FILE_DIALOG_OPEN_FAILED: 'FILE_DIALOG_OPEN_FAILED',
188
+ FILE_INPUT_CHANGE_FAILED: 'FILE_INPUT_CHANGE_FAILED',
189
+ DROP_EVENT_PROCESSING_FAILED: 'DROP_EVENT_PROCESSING_FAILED',
190
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR'
191
+ });
192
+
193
+ /**
194
+ * Central list of rejection codes for files that are rejected during the drop process.
195
+ * @readonly
196
+ */
197
+ static REJECTION_CODES = /** @type {const} */ ({
198
+ TOO_MANY_FILES: 'TOO_MANY_FILES',
199
+ FILE_TOO_LARGE: 'FILE_TOO_LARGE',
200
+ FILE_TOO_SMALL: 'FILE_TOO_SMALL',
201
+ INVALID_MIME_TYPE: 'INVALID_MIME_TYPE'
202
+ });
203
+
160
204
  /** @type {Nullable<HTMLInputElement>} */
161
205
  #fileInput = null;
162
206
 
@@ -172,7 +216,7 @@ class FilesDropzone extends HTMLElement {
172
216
  }
173
217
 
174
218
  if (this.shadowRoot) {
175
- this.#fileInput = /** @type {Nullable<HTMLInputElement>} */(this.shadowRoot.getElementById('file-input'));
219
+ this.#fileInput = /** @type {Nullable<HTMLInputElement>} */ (this.shadowRoot.getElementById('file-input'));
176
220
  this.#dropzoneEl = this.shadowRoot.getElementById('dropzone');
177
221
  }
178
222
  }
@@ -372,6 +416,40 @@ class FilesDropzone extends HTMLElement {
372
416
  this.toggleAttribute('no-style', !!value);
373
417
  }
374
418
 
419
+ /**
420
+ * Emit a custom event with the given name and detail.
421
+ *
422
+ * @template D
423
+ * @param {string} name - The name of the event.
424
+ * @param {D} [detail] - The detail payload of the event.
425
+ * @param {CustomEventInit<D>} [init] - Override the default event initialization options.
426
+ * @returns {boolean} - Returns false if at least one event listener called `preventDefault()`, otherwise true.
427
+ */
428
+ #emitEvent(name, detail, init) {
429
+ const options = {
430
+ bubbles: true,
431
+ composed: true,
432
+ cancelable: false,
433
+ ...init,
434
+ detail
435
+ };
436
+ const evt = new CustomEvent(`${COMPONENT_NAME}-${name}`, options);
437
+ return this.dispatchEvent(evt);
438
+ }
439
+
440
+ /**
441
+ * Emit a standardized error event with typed detail.
442
+ *
443
+ * @param {FilesDropzoneErrorCode} code - Stable identifier for the type of error that occurred.
444
+ * @param {unknown} error - The underlying error that triggered the event.
445
+ */
446
+ #emitErrorEvent(code, error) {
447
+ /** @type {FilesDropzoneErrorDetail} */
448
+ const detail = { code, error };
449
+
450
+ this.#emitEvent('error', detail);
451
+ }
452
+
375
453
  /**
376
454
  * Handles the change event of the file input.
377
455
  *
@@ -380,12 +458,8 @@ class FilesDropzone extends HTMLElement {
380
458
  #handleFileInputChange = async evt => {
381
459
  try {
382
460
  this.#handleFilesSelect(await getFilesFromEvent(evt));
383
- } catch (error) {
384
- this.dispatchEvent(new CustomEvent(`${COMPONENT_NAME}-error`, {
385
- bubbles: true,
386
- composed: true,
387
- detail: { error }
388
- }));
461
+ } catch (err) {
462
+ this.#emitErrorEvent(FilesDropzone.ERROR_CODES.FILE_INPUT_CHANGE_FAILED, err);
389
463
  }
390
464
  };
391
465
 
@@ -397,10 +471,7 @@ class FilesDropzone extends HTMLElement {
397
471
  return;
398
472
  }
399
473
 
400
- this.dispatchEvent(new Event(`${COMPONENT_NAME}-dragenter`, {
401
- bubbles: true,
402
- composed: true
403
- }));
474
+ this.#emitEvent('dragenter');
404
475
  };
405
476
 
406
477
  /**
@@ -423,10 +494,7 @@ class FilesDropzone extends HTMLElement {
423
494
  this.#dropzoneEl.part.add('dropzone--dragover');
424
495
  }
425
496
 
426
- this.dispatchEvent(new Event(`${COMPONENT_NAME}-dragover`, {
427
- bubbles: true,
428
- composed: true
429
- }));
497
+ this.#emitEvent('dragover');
430
498
  };
431
499
 
432
500
  /**
@@ -442,10 +510,7 @@ class FilesDropzone extends HTMLElement {
442
510
  this.#dropzoneEl.part.remove('dropzone--dragover');
443
511
  }
444
512
 
445
- this.dispatchEvent(new Event(`${COMPONENT_NAME}-dragleave`, {
446
- bubbles: true,
447
- composed: true
448
- }));
513
+ this.#emitEvent('dragleave');
449
514
  };
450
515
 
451
516
  /**
@@ -467,12 +532,8 @@ class FilesDropzone extends HTMLElement {
467
532
 
468
533
  try {
469
534
  this.#handleFilesSelect(await getFilesFromEvent(evt));
470
- } catch (error) {
471
- this.dispatchEvent(new CustomEvent(`${COMPONENT_NAME}-error`, {
472
- bubbles: true,
473
- composed: true,
474
- detail: { error }
475
- }));
535
+ } catch (err) {
536
+ this.#emitErrorEvent(FilesDropzone.ERROR_CODES.DROP_EVENT_PROCESSING_FAILED, err);
476
537
  }
477
538
  };
478
539
 
@@ -484,7 +545,7 @@ class FilesDropzone extends HTMLElement {
484
545
  return;
485
546
  }
486
547
 
487
- this.#fileInput?.click();
548
+ this.openFileDialog();
488
549
  };
489
550
 
490
551
  /**
@@ -498,7 +559,7 @@ class FilesDropzone extends HTMLElement {
498
559
  }
499
560
 
500
561
  if (evt.key === ' ' || evt.key === 'Enter') {
501
- this.#fileInput?.click();
562
+ this.openFileDialog();
502
563
  }
503
564
  };
504
565
 
@@ -521,10 +582,12 @@ class FilesDropzone extends HTMLElement {
521
582
  for (const file of files) {
522
583
  rejectedFiles.push({
523
584
  file,
524
- errors: [{
525
- code: TOO_MANY_FILES,
526
- message: `Too many files selected. Only 1 file is allowed.`
527
- }]
585
+ errors: [
586
+ {
587
+ code: FilesDropzone.REJECTION_CODES.TOO_MANY_FILES,
588
+ message: `Too many files selected. Only 1 file is allowed.`
589
+ }
590
+ ]
528
591
  });
529
592
  }
530
593
  } else if (this.multiple && filesLength > this.maxFiles) {
@@ -533,10 +596,12 @@ class FilesDropzone extends HTMLElement {
533
596
  for (const file of files) {
534
597
  rejectedFiles.push({
535
598
  file,
536
- errors: [{
537
- code: TOO_MANY_FILES,
538
- message: `Too many files selected. Only ${this.maxFiles} ${this.maxFiles > 1 ? 'files are' : 'file is'} allowed.`
539
- }]
599
+ errors: [
600
+ {
601
+ code: FilesDropzone.REJECTION_CODES.TOO_MANY_FILES,
602
+ message: `Too many files selected. Only ${this.maxFiles} ${this.maxFiles > 1 ? 'files are' : 'file is'} allowed.`
603
+ }
604
+ ]
540
605
  });
541
606
  }
542
607
  } else {
@@ -554,21 +619,21 @@ class FilesDropzone extends HTMLElement {
554
619
 
555
620
  if (!fileHasValidType) {
556
621
  errors.push({
557
- code: INVALID_MIME_TYPE,
622
+ code: FilesDropzone.REJECTION_CODES.INVALID_MIME_TYPE,
558
623
  message: `File type "${file.type}" is not accepted.`
559
624
  });
560
625
  }
561
626
 
562
627
  if (fileExceedsMaxSize) {
563
628
  errors.push({
564
- code: FILE_TOO_LARGE,
629
+ code: FilesDropzone.REJECTION_CODES.FILE_TOO_LARGE,
565
630
  message: `File size ${file.size} exceeds the maximum size of ${this.maxSize}.`
566
631
  });
567
632
  }
568
633
 
569
634
  if (fileIsSmallerThanMinSize) {
570
635
  errors.push({
571
- code: FILE_TOO_SMALL,
636
+ code: FilesDropzone.REJECTION_CODES.FILE_TOO_SMALL,
572
637
  message: `File size ${file.size} is smaller than the minimum size of ${this.minSize}.`
573
638
  });
574
639
  }
@@ -578,33 +643,14 @@ class FilesDropzone extends HTMLElement {
578
643
  }
579
644
  }
580
645
 
581
- this.dispatchEvent(new CustomEvent(`${COMPONENT_NAME}-drop`, {
582
- bubbles: true,
583
- composed: true,
584
- detail: {
585
- acceptedFiles,
586
- rejectedFiles
587
- }
588
- }));
646
+ this.#emitEvent('drop', { acceptedFiles, rejectedFiles });
589
647
 
590
648
  if (acceptedFiles.length > 0) {
591
- this.dispatchEvent(new CustomEvent(`${COMPONENT_NAME}-drop-accepted`, {
592
- bubbles: true,
593
- composed: true,
594
- detail: {
595
- acceptedFiles
596
- }
597
- }));
649
+ this.#emitEvent('drop-accepted', { acceptedFiles });
598
650
  }
599
651
 
600
652
  if (rejectedFiles.length > 0) {
601
- this.dispatchEvent(new CustomEvent(`${COMPONENT_NAME}-drop-rejected`, {
602
- bubbles: true,
603
- composed: true,
604
- detail: {
605
- rejectedFiles
606
- }
607
- }));
653
+ this.#emitEvent('drop-rejected', { rejectedFiles });
608
654
  }
609
655
 
610
656
  if (this.#fileInput) {
@@ -616,11 +662,15 @@ class FilesDropzone extends HTMLElement {
616
662
  * Opens the file dialog programmatically.
617
663
  */
618
664
  openFileDialog() {
619
- if (this.disabled) {
665
+ if (this.disabled || !this.#fileInput) {
620
666
  return;
621
667
  }
622
668
 
623
- this.#fileInput?.click();
669
+ try {
670
+ this.#fileInput.showPicker();
671
+ } catch (err) {
672
+ this.#emitErrorEvent(FilesDropzone.ERROR_CODES.FILE_DIALOG_OPEN_FAILED, err);
673
+ }
624
674
  }
625
675
 
626
676
  /**
@@ -239,7 +239,7 @@ const getFilesFromDataTransferItemList = async dataTransferItemList => {
239
239
  files.push(file);
240
240
  }
241
241
  } else if (entry.isDirectory) {
242
- queue.push(...await readAllDirectoryEntries(entry.createReader()));
242
+ queue.push(...(await readAllDirectoryEntries(entry.createReader())));
243
243
  }
244
244
  }
245
245
 
@@ -13,7 +13,12 @@ export function isValidFile(file, acceptedTypeSpecifiers = '') {
13
13
  }
14
14
 
15
15
  const acceptedMimeTypesList = [
16
- ...new Set(acceptedTypeSpecifiers.split(',').map(v => v.trim()).filter(Boolean))
16
+ ...new Set(
17
+ acceptedTypeSpecifiers
18
+ .split(',')
19
+ .map(v => v.trim())
20
+ .filter(Boolean)
21
+ )
17
22
  ];
18
23
 
19
24
  const fileMimeType = file.type;