@aarongustafson/form-dropzone 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +159 -2
- package/custom-elements.json +107 -0
- package/define.js +18 -0
- package/form-dropzone.d.ts +42 -0
- package/form-dropzone.js +591 -0
- package/index.js +1 -0
- package/package.json +81 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aaron Gustafson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,160 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `form-dropzone` Web Component
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@aarongustafson/form-dropzone) [](https://github.com/aarongustafson/form-dropzone/actions)
|
|
4
|
+
|
|
5
|
+
A progressively enhanced file-field wrapper that adds a responsive drag-and-drop surface, accessible status announcements, and optional image previews.
|
|
6
|
+
|
|
7
|
+
## Demo
|
|
8
|
+
|
|
9
|
+
- [Live demo](https://aarongustafson.github.io/form-dropzone/demo/) ([source](./demo/index.html))
|
|
10
|
+
- [esm.sh demo](https://aarongustafson.github.io/form-dropzone/demo/esm.html) ([source](./demo/esm.html))
|
|
11
|
+
- [unpkg demo](https://aarongustafson.github.io/form-dropzone/demo/unpkg.html) ([source](./demo/unpkg.html))
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @aarongustafson/form-dropzone
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Import the class and register it:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { FormDropzoneElement } from '@aarongustafson/form-dropzone';
|
|
23
|
+
|
|
24
|
+
customElements.define('form-dropzone', FormDropzoneElement);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or use the guarded definition helper:
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import {
|
|
31
|
+
defineFormDropzone,
|
|
32
|
+
} from '@aarongustafson/form-dropzone/define.js';
|
|
33
|
+
|
|
34
|
+
defineFormDropzone();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Importing `define.js` also attempts to register `<form-dropzone>` automatically in browser environments.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Supply one `input[type="file"]` and its associated `label`. Both remain in Light DOM and provide a native fallback before the component upgrades or when drag-and-drop is unavailable.
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<form-dropzone>
|
|
45
|
+
<label for="attachments">Choose files</label>
|
|
46
|
+
<input
|
|
47
|
+
id="attachments"
|
|
48
|
+
name="attachments"
|
|
49
|
+
type="file"
|
|
50
|
+
accept=".pdf,image/*"
|
|
51
|
+
multiple
|
|
52
|
+
>
|
|
53
|
+
</form-dropzone>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Clicking the non-interactive drop-zone surface opens the native picker. Dropped files are filtered using the input's comma-separated `accept` value, including file extensions, exact MIME types, and wildcard MIME groups. If `multiple` is absent, the first acceptable file is selected and later acceptable files are rejected. Mixed drops keep acceptable files and announce the rejected files.
|
|
57
|
+
|
|
58
|
+
A successful drop replaces the input's existing `FileList`, then dispatches bubbling `input` and `change` events.
|
|
59
|
+
|
|
60
|
+
## Attributes
|
|
61
|
+
|
|
62
|
+
| Attribute | Type | Default | Description |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `drop-label` | string | `Drop file(s) here` | Prompt inserted before the author label. |
|
|
65
|
+
| `separator-label` | string | `or` | Text inserted between the prompt and label. |
|
|
66
|
+
| `received-message` | string | `Received: {files}.` | Accepted-file announcement template. |
|
|
67
|
+
| `rejected-type-message` | string | `Rejected because the file type is not accepted: {files}.` | `accept` rejection template. |
|
|
68
|
+
| `rejected-multiple-message` | string | `Rejected because only one file is allowed: {files}.` | Single-file limit rejection template. |
|
|
69
|
+
| `preview-images` | boolean | absent | Adds image previews for picker and drop selections. |
|
|
70
|
+
|
|
71
|
+
Message templates support `{files}` (a comma-separated filename list) and `{count}`.
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<form-dropzone
|
|
75
|
+
drop-label="Déposez les fichiers ici"
|
|
76
|
+
separator-label="ou"
|
|
77
|
+
received-message="{count} fichier(s) reçu(s) : {files}."
|
|
78
|
+
rejected-type-message="Type de fichier refusé : {files}."
|
|
79
|
+
rejected-multiple-message="Un seul fichier est autorisé. Refusé : {files}."
|
|
80
|
+
>
|
|
81
|
+
<label for="document">Choisir un fichier</label>
|
|
82
|
+
<input id="document" name="document" type="file" accept=".pdf">
|
|
83
|
+
</form-dropzone>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each attribute has a camel-cased JavaScript property: `dropLabel`, `separatorLabel`, `receivedMessage`, `rejectedTypeMessage`, `rejectedMultipleMessage`, and `previewImages`.
|
|
87
|
+
|
|
88
|
+
## Image previews
|
|
89
|
+
|
|
90
|
+
Add `preview-images` to preview selected image MIME types. Accepted non-image files remain selected but are not rendered.
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<form-dropzone preview-images>
|
|
94
|
+
<label for="photos">Choose photos</label>
|
|
95
|
+
<input id="photos" name="photos" type="file" accept="image/*" multiple>
|
|
96
|
+
</form-dropzone>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Object URLs are revoked whenever previews are replaced, disabled, or disconnected. Previews are visual only and do not change form submission.
|
|
100
|
+
|
|
101
|
+
## Styling
|
|
102
|
+
|
|
103
|
+
The component injects low-specificity default styles once. Ordinary author selectors can override the centered layout:
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
form-dropzone {
|
|
107
|
+
align-items: start;
|
|
108
|
+
min-block-size: 10rem;
|
|
109
|
+
text-align: start;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
form-dropzone .form-dropzone__previews {
|
|
113
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Stable Light DOM classes:
|
|
118
|
+
|
|
119
|
+
- `.form-dropzone--enhanced`
|
|
120
|
+
- `.form-dropzone--drag-active`
|
|
121
|
+
- `.form-dropzone__prompt`
|
|
122
|
+
- `.form-dropzone__separator`
|
|
123
|
+
- `.form-dropzone__status`
|
|
124
|
+
- `.form-dropzone__previews`
|
|
125
|
+
- `.form-dropzone__preview`
|
|
126
|
+
- `.form-dropzone__preview-image`
|
|
127
|
+
- `.form-dropzone__preview-name`
|
|
128
|
+
|
|
129
|
+
Preview items also expose the filename through `data-file-name`.
|
|
130
|
+
|
|
131
|
+
## Accessibility and progressive enhancement
|
|
132
|
+
|
|
133
|
+
- The author-provided label and file input remain the keyboard interaction.
|
|
134
|
+
- The wrapper does not add a duplicate button role or tab stop.
|
|
135
|
+
- Drop outcomes are announced through a generated polite, atomic status region.
|
|
136
|
+
- Accepted, invalid-type, and excess-file outcomes can be localized independently.
|
|
137
|
+
- Without the required drag-and-drop and assignable `FileList` APIs, the component does not mutate its children or add behavior.
|
|
138
|
+
- Invalid markup is left unchanged and reported with a console warning.
|
|
139
|
+
|
|
140
|
+
## Browser support
|
|
141
|
+
|
|
142
|
+
The component requires Custom Elements, ES modules, HTML drag-and-drop file APIs, `DataTransfer`, and an assignable file input `files` property. Unsupported environments retain the original label and input.
|
|
143
|
+
|
|
144
|
+
## Inspiration
|
|
145
|
+
|
|
146
|
+
The drop-zone layout and interaction were inspired by Nikita Hlopov's article, [“Custom styled input type file”](https://nikitahl.com/custom-styled-input-type-file). Many thanks to Nikita for sharing their approach!
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install
|
|
152
|
+
npm run test:run
|
|
153
|
+
npm run test:coverage
|
|
154
|
+
npm run lint
|
|
155
|
+
npm run format
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT © [Aaron Gustafson](https://www.aaron-gustafson.com/)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"readme": "README.md",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "javascript-module",
|
|
7
|
+
"path": "form-dropzone.js",
|
|
8
|
+
"declarations": [
|
|
9
|
+
{
|
|
10
|
+
"kind": "class",
|
|
11
|
+
"description": "Progressively enhances an associated label and file input with a Light DOM drag-and-drop surface, accessible announcements, and optional image previews.",
|
|
12
|
+
"name": "FormDropzoneElement",
|
|
13
|
+
"members": [
|
|
14
|
+
{
|
|
15
|
+
"kind": "field",
|
|
16
|
+
"name": "dropLabel",
|
|
17
|
+
"type": { "text": "string" },
|
|
18
|
+
"attribute": "drop-label"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"kind": "field",
|
|
22
|
+
"name": "separatorLabel",
|
|
23
|
+
"type": { "text": "string" },
|
|
24
|
+
"attribute": "separator-label"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "field",
|
|
28
|
+
"name": "receivedMessage",
|
|
29
|
+
"type": { "text": "string" },
|
|
30
|
+
"attribute": "received-message"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"kind": "field",
|
|
34
|
+
"name": "rejectedTypeMessage",
|
|
35
|
+
"type": { "text": "string" },
|
|
36
|
+
"attribute": "rejected-type-message"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"kind": "field",
|
|
40
|
+
"name": "rejectedMultipleMessage",
|
|
41
|
+
"type": { "text": "string" },
|
|
42
|
+
"attribute": "rejected-multiple-message"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"kind": "field",
|
|
46
|
+
"name": "previewImages",
|
|
47
|
+
"type": { "text": "boolean" },
|
|
48
|
+
"attribute": "preview-images"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"events": [],
|
|
52
|
+
"attributes": [
|
|
53
|
+
{
|
|
54
|
+
"name": "drop-label",
|
|
55
|
+
"type": { "text": "string" },
|
|
56
|
+
"default": "\"Drop file(s) here\"",
|
|
57
|
+
"description": "Drop instruction inserted before the author-provided label."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "separator-label",
|
|
61
|
+
"type": { "text": "string" },
|
|
62
|
+
"default": "\"or\"",
|
|
63
|
+
"description": "Text inserted between the drop instruction and label."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "received-message",
|
|
67
|
+
"type": { "text": "string" },
|
|
68
|
+
"default": "\"Received: {files}.\"",
|
|
69
|
+
"description": "Live-region template for accepted files. Supports {files} and {count}."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "rejected-type-message",
|
|
73
|
+
"type": { "text": "string" },
|
|
74
|
+
"default": "\"Rejected because the file type is not accepted: {files}.\"",
|
|
75
|
+
"description": "Live-region template for files rejected by accept. Supports {files} and {count}."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "rejected-multiple-message",
|
|
79
|
+
"type": { "text": "string" },
|
|
80
|
+
"default": "\"Rejected because only one file is allowed: {files}.\"",
|
|
81
|
+
"description": "Live-region template for excess files on a single-file input. Supports {files} and {count}."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "preview-images",
|
|
85
|
+
"type": { "text": "boolean" },
|
|
86
|
+
"default": "false",
|
|
87
|
+
"description": "Renders accepted image selections in a Light DOM preview grid."
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"superclass": { "name": "HTMLElement" },
|
|
91
|
+
"tagName": "form-dropzone",
|
|
92
|
+
"customElement": true
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"exports": [
|
|
96
|
+
{
|
|
97
|
+
"kind": "js",
|
|
98
|
+
"name": "FormDropzoneElement",
|
|
99
|
+
"declaration": {
|
|
100
|
+
"name": "FormDropzoneElement",
|
|
101
|
+
"module": "form-dropzone.js"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
package/define.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FormDropzoneElement } from './form-dropzone.js';
|
|
2
|
+
|
|
3
|
+
export function defineFormDropzone(tagName = 'form-dropzone') {
|
|
4
|
+
const hasWindow = typeof window !== 'undefined';
|
|
5
|
+
const registry = hasWindow ? window.customElements : undefined;
|
|
6
|
+
|
|
7
|
+
if (!registry || typeof registry.define !== 'function') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!registry.get(tagName)) {
|
|
12
|
+
registry.define(tagName, FormDropzoneElement);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
defineFormDropzone();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressively enhances an associated label and file input with a Light DOM
|
|
3
|
+
* drag-and-drop surface, accessible announcements, and optional image previews.
|
|
4
|
+
*
|
|
5
|
+
* @element form-dropzone
|
|
6
|
+
*/
|
|
7
|
+
export class FormDropzoneElement extends HTMLElement {
|
|
8
|
+
static readonly observedAttributes: string[];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns whether the browser supports the drag-and-drop file APIs required
|
|
12
|
+
* by the component.
|
|
13
|
+
*/
|
|
14
|
+
static isSupported(): boolean;
|
|
15
|
+
|
|
16
|
+
constructor();
|
|
17
|
+
connectedCallback(): void;
|
|
18
|
+
disconnectedCallback(): void;
|
|
19
|
+
attributeChangedCallback(
|
|
20
|
+
name: string,
|
|
21
|
+
oldValue: string | null,
|
|
22
|
+
newValue: string | null,
|
|
23
|
+
): void;
|
|
24
|
+
|
|
25
|
+
get dropLabel(): string;
|
|
26
|
+
set dropLabel(value: string | null | undefined);
|
|
27
|
+
|
|
28
|
+
get separatorLabel(): string;
|
|
29
|
+
set separatorLabel(value: string | null | undefined);
|
|
30
|
+
|
|
31
|
+
get receivedMessage(): string;
|
|
32
|
+
set receivedMessage(value: string | null | undefined);
|
|
33
|
+
|
|
34
|
+
get rejectedTypeMessage(): string;
|
|
35
|
+
set rejectedTypeMessage(value: string | null | undefined);
|
|
36
|
+
|
|
37
|
+
get rejectedMultipleMessage(): string;
|
|
38
|
+
set rejectedMultipleMessage(value: string | null | undefined);
|
|
39
|
+
|
|
40
|
+
get previewImages(): boolean;
|
|
41
|
+
set previewImages(value: boolean);
|
|
42
|
+
}
|
package/form-dropzone.js
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
const GENERATED_ATTRIBUTE = 'data-form-dropzone-generated';
|
|
2
|
+
const ENHANCED_CLASS = 'form-dropzone--enhanced';
|
|
3
|
+
const STYLE_ID = 'form-dropzone-styles';
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
dropLabel: 'Drop file(s) here',
|
|
7
|
+
separatorLabel: 'or',
|
|
8
|
+
receivedMessage: 'Received: {files}.',
|
|
9
|
+
rejectedTypeMessage:
|
|
10
|
+
'Rejected because the file type is not accepted: {files}.',
|
|
11
|
+
rejectedMultipleMessage:
|
|
12
|
+
'Rejected because only one file is allowed: {files}.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* FormDropzoneElement - A file field wrapper that builds a customizable drop zone for users to drag files into a form.
|
|
17
|
+
*
|
|
18
|
+
* @element form-dropzone
|
|
19
|
+
*
|
|
20
|
+
* @attr {string} drop-label - Drop instruction shown before the file label
|
|
21
|
+
* @attr {string} separator-label - Text shown between the drop instruction and file label
|
|
22
|
+
* @attr {string} received-message - Announcement template for accepted files
|
|
23
|
+
* @attr {string} rejected-type-message - Announcement template for files rejected by accept
|
|
24
|
+
* @attr {string} rejected-multiple-message - Announcement template for excess files
|
|
25
|
+
* @attr {boolean} preview-images - Whether accepted images are previewed
|
|
26
|
+
*/
|
|
27
|
+
export class FormDropzoneElement extends HTMLElement {
|
|
28
|
+
static get observedAttributes() {
|
|
29
|
+
return [
|
|
30
|
+
'drop-label',
|
|
31
|
+
'separator-label',
|
|
32
|
+
'received-message',
|
|
33
|
+
'rejected-type-message',
|
|
34
|
+
'rejected-multiple-message',
|
|
35
|
+
'preview-images',
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static isSupported() {
|
|
40
|
+
if (
|
|
41
|
+
typeof DataTransfer !== 'function' ||
|
|
42
|
+
typeof DragEvent !== 'function'
|
|
43
|
+
) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const input = document.createElement('input');
|
|
49
|
+
input.type = 'file';
|
|
50
|
+
const transfer = new DataTransfer();
|
|
51
|
+
input.files = transfer.files;
|
|
52
|
+
return (
|
|
53
|
+
input.files !== null &&
|
|
54
|
+
typeof input.files.length === 'number' &&
|
|
55
|
+
typeof input.files[Symbol.iterator] === 'function'
|
|
56
|
+
);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static injectStyles() {
|
|
63
|
+
if (document.getElementById(STYLE_ID)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const style = document.createElement('style');
|
|
68
|
+
style.id = STYLE_ID;
|
|
69
|
+
style.textContent = `
|
|
70
|
+
:where(form-dropzone.form-dropzone--enhanced) {
|
|
71
|
+
align-items: center;
|
|
72
|
+
border: 2px dashed currentColor;
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 0.75rem;
|
|
78
|
+
inline-size: 100%;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
max-inline-size: 100%;
|
|
81
|
+
min-block-size: min(12rem, 50vh);
|
|
82
|
+
padding: clamp(1rem, 4vw, 2rem);
|
|
83
|
+
text-align: center;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:where(form-dropzone.form-dropzone--enhanced[hidden]) {
|
|
87
|
+
display: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:where(.form-dropzone__prompt) {
|
|
91
|
+
font-size: 1.25em;
|
|
92
|
+
font-weight: bold;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
:where(form-dropzone.form-dropzone--drag-active) {
|
|
96
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
97
|
+
border-style: solid;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:where(.form-dropzone__status) {
|
|
101
|
+
block-size: 1px;
|
|
102
|
+
clip: rect(0 0 0 0);
|
|
103
|
+
clip-path: inset(50%);
|
|
104
|
+
inline-size: 1px;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
position: absolute;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
:where(.form-dropzone__previews) {
|
|
111
|
+
display: grid;
|
|
112
|
+
gap: 1rem;
|
|
113
|
+
grid-template-columns: repeat(auto-fit, minmax(min(8rem, 100%), 1fr));
|
|
114
|
+
inline-size: 100%;
|
|
115
|
+
list-style: none;
|
|
116
|
+
margin: 0;
|
|
117
|
+
padding: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:where(.form-dropzone__preview) {
|
|
121
|
+
display: grid;
|
|
122
|
+
gap: 0.5rem;
|
|
123
|
+
min-inline-size: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:where(.form-dropzone__preview-image) {
|
|
127
|
+
aspect-ratio: 1;
|
|
128
|
+
block-size: auto;
|
|
129
|
+
inline-size: 100%;
|
|
130
|
+
object-fit: cover;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
:where(.form-dropzone__preview-name) {
|
|
134
|
+
overflow-wrap: anywhere;
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
document.head.appendChild(style);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
constructor() {
|
|
141
|
+
super();
|
|
142
|
+
this._input = null;
|
|
143
|
+
this._label = null;
|
|
144
|
+
this._prompt = null;
|
|
145
|
+
this._separator = null;
|
|
146
|
+
this._status = null;
|
|
147
|
+
this._previews = null;
|
|
148
|
+
this._previewUrls = [];
|
|
149
|
+
this._enhanced = false;
|
|
150
|
+
this._dragDepth = 0;
|
|
151
|
+
this._announcementToken = 0;
|
|
152
|
+
this._handleClick = this._handleClick.bind(this);
|
|
153
|
+
this._handleDragEnter = this._handleDragEnter.bind(this);
|
|
154
|
+
this._handleDragLeave = this._handleDragLeave.bind(this);
|
|
155
|
+
this._handleDragOver = FormDropzoneElement._handleDragOver;
|
|
156
|
+
this._handleDrop = this._handleDrop.bind(this);
|
|
157
|
+
this._handleInputChange = this._handleInputChange.bind(this);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
connectedCallback() {
|
|
161
|
+
for (const property of [
|
|
162
|
+
'dropLabel',
|
|
163
|
+
'separatorLabel',
|
|
164
|
+
'receivedMessage',
|
|
165
|
+
'rejectedTypeMessage',
|
|
166
|
+
'rejectedMultipleMessage',
|
|
167
|
+
'previewImages',
|
|
168
|
+
]) {
|
|
169
|
+
this._upgradeProperty(property);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!FormDropzoneElement.isSupported()) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const inputs = this.querySelectorAll('input[type="file"]');
|
|
177
|
+
if (inputs.length !== 1) {
|
|
178
|
+
this._reportInvalidMarkup(
|
|
179
|
+
'Expected exactly one input[type="file"].',
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const input = inputs[0];
|
|
185
|
+
const label = Array.from(this.querySelectorAll('label')).find(
|
|
186
|
+
(candidate) =>
|
|
187
|
+
candidate.contains(input) ||
|
|
188
|
+
(Boolean(input.id) && candidate.htmlFor === input.id),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!label) {
|
|
192
|
+
this._reportInvalidMarkup(
|
|
193
|
+
'Expected a label associated with the file input.',
|
|
194
|
+
);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this._input = input;
|
|
199
|
+
this._label = label;
|
|
200
|
+
this._enhance();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
disconnectedCallback() {
|
|
204
|
+
this._teardown();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
208
|
+
if (oldValue === newValue || !this._enhanced) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (name === 'drop-label' && this._prompt) {
|
|
213
|
+
this._prompt.textContent = this.dropLabel;
|
|
214
|
+
}
|
|
215
|
+
if (name === 'separator-label' && this._separator) {
|
|
216
|
+
this._separator.textContent = this.separatorLabel;
|
|
217
|
+
}
|
|
218
|
+
if (name === 'preview-images') {
|
|
219
|
+
this._syncPreviews();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get dropLabel() {
|
|
224
|
+
return this.getAttribute('drop-label') || DEFAULTS.dropLabel;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
set dropLabel(value) {
|
|
228
|
+
this._setStringAttribute('drop-label', value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get separatorLabel() {
|
|
232
|
+
return this.getAttribute('separator-label') || DEFAULTS.separatorLabel;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
set separatorLabel(value) {
|
|
236
|
+
this._setStringAttribute('separator-label', value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get receivedMessage() {
|
|
240
|
+
return (
|
|
241
|
+
this.getAttribute('received-message') || DEFAULTS.receivedMessage
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
set receivedMessage(value) {
|
|
246
|
+
this._setStringAttribute('received-message', value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
get rejectedTypeMessage() {
|
|
250
|
+
return (
|
|
251
|
+
this.getAttribute('rejected-type-message') ||
|
|
252
|
+
DEFAULTS.rejectedTypeMessage
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
set rejectedTypeMessage(value) {
|
|
257
|
+
this._setStringAttribute('rejected-type-message', value);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get rejectedMultipleMessage() {
|
|
261
|
+
return (
|
|
262
|
+
this.getAttribute('rejected-multiple-message') ||
|
|
263
|
+
DEFAULTS.rejectedMultipleMessage
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
set rejectedMultipleMessage(value) {
|
|
268
|
+
this._setStringAttribute('rejected-multiple-message', value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get previewImages() {
|
|
272
|
+
return this.hasAttribute('preview-images');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
set previewImages(value) {
|
|
276
|
+
this.toggleAttribute('preview-images', Boolean(value));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
_enhance() {
|
|
280
|
+
if (this._enhanced) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
FormDropzoneElement.injectStyles();
|
|
285
|
+
|
|
286
|
+
this._prompt = FormDropzoneElement._createGeneratedElement(
|
|
287
|
+
'span',
|
|
288
|
+
'form-dropzone__prompt',
|
|
289
|
+
);
|
|
290
|
+
this._prompt.textContent = this.dropLabel;
|
|
291
|
+
this._separator = FormDropzoneElement._createGeneratedElement(
|
|
292
|
+
'span',
|
|
293
|
+
'form-dropzone__separator',
|
|
294
|
+
);
|
|
295
|
+
this._separator.textContent = this.separatorLabel;
|
|
296
|
+
this._status = FormDropzoneElement._createGeneratedElement(
|
|
297
|
+
'span',
|
|
298
|
+
'form-dropzone__status',
|
|
299
|
+
);
|
|
300
|
+
this._status.setAttribute('role', 'status');
|
|
301
|
+
this._status.setAttribute('aria-live', 'polite');
|
|
302
|
+
this._status.setAttribute('aria-atomic', 'true');
|
|
303
|
+
|
|
304
|
+
this._label.before(this._prompt, this._separator);
|
|
305
|
+
this.appendChild(this._status);
|
|
306
|
+
this.classList.add(ENHANCED_CLASS);
|
|
307
|
+
this.addEventListener('click', this._handleClick);
|
|
308
|
+
this.addEventListener('dragenter', this._handleDragEnter);
|
|
309
|
+
this.addEventListener('dragleave', this._handleDragLeave);
|
|
310
|
+
this.addEventListener('dragover', this._handleDragOver);
|
|
311
|
+
this.addEventListener('drop', this._handleDrop);
|
|
312
|
+
this._input.addEventListener('change', this._handleInputChange);
|
|
313
|
+
this._enhanced = true;
|
|
314
|
+
this._syncPreviews();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_teardown() {
|
|
318
|
+
this.removeEventListener('click', this._handleClick);
|
|
319
|
+
this.removeEventListener('dragenter', this._handleDragEnter);
|
|
320
|
+
this.removeEventListener('dragleave', this._handleDragLeave);
|
|
321
|
+
this.removeEventListener('dragover', this._handleDragOver);
|
|
322
|
+
this.removeEventListener('drop', this._handleDrop);
|
|
323
|
+
this._input?.removeEventListener('change', this._handleInputChange);
|
|
324
|
+
this._removePreviews();
|
|
325
|
+
for (const element of this.querySelectorAll(
|
|
326
|
+
`[${GENERATED_ATTRIBUTE}]`,
|
|
327
|
+
)) {
|
|
328
|
+
element.remove();
|
|
329
|
+
}
|
|
330
|
+
this.classList.remove(ENHANCED_CLASS, 'form-dropzone--drag-active');
|
|
331
|
+
this._input = null;
|
|
332
|
+
this._label = null;
|
|
333
|
+
this._prompt = null;
|
|
334
|
+
this._separator = null;
|
|
335
|
+
this._status = null;
|
|
336
|
+
this._previews = null;
|
|
337
|
+
this._enhanced = false;
|
|
338
|
+
this._dragDepth = 0;
|
|
339
|
+
this._announcementToken += 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_handleInputChange() {
|
|
343
|
+
this._syncPreviews();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
_handleClick(event) {
|
|
347
|
+
if (
|
|
348
|
+
event.defaultPrevented ||
|
|
349
|
+
!(event.target instanceof Element) ||
|
|
350
|
+
event.target.closest(
|
|
351
|
+
'input, label, button, a, select, textarea, [contenteditable]',
|
|
352
|
+
)
|
|
353
|
+
) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this._input.click();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_handleDragEnter(event) {
|
|
361
|
+
if (!FormDropzoneElement._hasFiles(event.dataTransfer)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
this._dragDepth += 1;
|
|
367
|
+
this.classList.add('form-dropzone--drag-active');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_handleDragLeave(event) {
|
|
371
|
+
if (!FormDropzoneElement._hasFiles(event.dataTransfer)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this._dragDepth = Math.max(0, this._dragDepth - 1);
|
|
376
|
+
if (this._dragDepth === 0) {
|
|
377
|
+
this.classList.remove('form-dropzone--drag-active');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
static _handleDragOver(event) {
|
|
382
|
+
if (FormDropzoneElement._hasFiles(event.dataTransfer)) {
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
_handleDrop(event) {
|
|
388
|
+
if (!FormDropzoneElement._hasFiles(event.dataTransfer)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
this._dragDepth = 0;
|
|
394
|
+
this.classList.remove('form-dropzone--drag-active');
|
|
395
|
+
|
|
396
|
+
const accepted = [];
|
|
397
|
+
const rejectedType = [];
|
|
398
|
+
const rejectedMultiple = [];
|
|
399
|
+
|
|
400
|
+
for (const file of event.dataTransfer.files) {
|
|
401
|
+
if (!this._matchesAccept(file)) {
|
|
402
|
+
rejectedType.push(file);
|
|
403
|
+
} else if (!this._input.multiple && accepted.length > 0) {
|
|
404
|
+
rejectedMultiple.push(file);
|
|
405
|
+
} else {
|
|
406
|
+
accepted.push(file);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (accepted.length === 0) {
|
|
411
|
+
this._announceDrop({
|
|
412
|
+
accepted,
|
|
413
|
+
rejectedType,
|
|
414
|
+
rejectedMultiple,
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const transfer = new DataTransfer();
|
|
420
|
+
for (const file of accepted) {
|
|
421
|
+
transfer.items.add(file);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
this._input.files = transfer.files;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error(
|
|
428
|
+
'form-dropzone: Unable to assign dropped files to the input.',
|
|
429
|
+
error,
|
|
430
|
+
);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this._input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
435
|
+
this._input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
436
|
+
this._announceDrop({ accepted, rejectedType, rejectedMultiple });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static _hasFiles(dataTransfer) {
|
|
440
|
+
if (!dataTransfer) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
Array.from(dataTransfer.types || []).includes('Files') ||
|
|
446
|
+
dataTransfer.files.length > 0
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
_matchesAccept(file) {
|
|
451
|
+
const accept = this._input.accept.trim();
|
|
452
|
+
if (!accept) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const fileName = file.name.toLowerCase();
|
|
457
|
+
const fileType = file.type.toLowerCase();
|
|
458
|
+
return accept
|
|
459
|
+
.split(',')
|
|
460
|
+
.map((token) => token.trim().toLowerCase())
|
|
461
|
+
.filter(Boolean)
|
|
462
|
+
.some((token) => {
|
|
463
|
+
if (token.startsWith('.')) {
|
|
464
|
+
return fileName.endsWith(token);
|
|
465
|
+
}
|
|
466
|
+
if (token.endsWith('/*')) {
|
|
467
|
+
return fileType.startsWith(token.slice(0, -1));
|
|
468
|
+
}
|
|
469
|
+
return fileType === token;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_announceDrop({ accepted, rejectedType, rejectedMultiple }) {
|
|
474
|
+
const messages = [];
|
|
475
|
+
for (const [files, template] of [
|
|
476
|
+
[accepted, this.receivedMessage],
|
|
477
|
+
[rejectedType, this.rejectedTypeMessage],
|
|
478
|
+
[rejectedMultiple, this.rejectedMultipleMessage],
|
|
479
|
+
]) {
|
|
480
|
+
if (files.length > 0) {
|
|
481
|
+
messages.push(
|
|
482
|
+
FormDropzoneElement._formatMessage(template, files),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const status = this._status;
|
|
488
|
+
const announcementToken = ++this._announcementToken;
|
|
489
|
+
status.textContent = '';
|
|
490
|
+
queueMicrotask(() => {
|
|
491
|
+
if (
|
|
492
|
+
this._enhanced &&
|
|
493
|
+
this._status === status &&
|
|
494
|
+
this._announcementToken === announcementToken
|
|
495
|
+
) {
|
|
496
|
+
status.textContent = messages.join(' ');
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
static _formatMessage(template, files) {
|
|
502
|
+
return template
|
|
503
|
+
.replaceAll('{count}', String(files.length))
|
|
504
|
+
.replaceAll('{files}', files.map((file) => file.name).join(', '));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_syncPreviews() {
|
|
508
|
+
if (!this.previewImages) {
|
|
509
|
+
this._removePreviews();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!this._previews) {
|
|
514
|
+
this._previews = FormDropzoneElement._createGeneratedElement(
|
|
515
|
+
'ul',
|
|
516
|
+
'form-dropzone__previews',
|
|
517
|
+
);
|
|
518
|
+
this._status.before(this._previews);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this._revokePreviewUrls();
|
|
522
|
+
this._previews.replaceChildren();
|
|
523
|
+
|
|
524
|
+
for (const file of this._input.files) {
|
|
525
|
+
if (!file.type.toLowerCase().startsWith('image/')) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const url = URL.createObjectURL(file);
|
|
530
|
+
this._previewUrls.push(url);
|
|
531
|
+
|
|
532
|
+
const item = document.createElement('li');
|
|
533
|
+
item.className = 'form-dropzone__preview';
|
|
534
|
+
item.dataset.fileName = file.name;
|
|
535
|
+
|
|
536
|
+
const image = document.createElement('img');
|
|
537
|
+
image.className = 'form-dropzone__preview-image';
|
|
538
|
+
image.src = url;
|
|
539
|
+
image.alt = '';
|
|
540
|
+
|
|
541
|
+
const name = document.createElement('span');
|
|
542
|
+
name.className = 'form-dropzone__preview-name';
|
|
543
|
+
name.textContent = file.name;
|
|
544
|
+
|
|
545
|
+
item.append(image, name);
|
|
546
|
+
this._previews.appendChild(item);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
_removePreviews() {
|
|
551
|
+
this._revokePreviewUrls();
|
|
552
|
+
this._previews?.remove();
|
|
553
|
+
this._previews = null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_revokePreviewUrls() {
|
|
557
|
+
for (const url of this._previewUrls) {
|
|
558
|
+
URL.revokeObjectURL(url);
|
|
559
|
+
}
|
|
560
|
+
this._previewUrls = [];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
static _createGeneratedElement(tagName, className) {
|
|
564
|
+
const element = document.createElement(tagName);
|
|
565
|
+
element.className = className;
|
|
566
|
+
element.setAttribute(GENERATED_ATTRIBUTE, '');
|
|
567
|
+
return element;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_setStringAttribute(name, value) {
|
|
571
|
+
if (value === null || value === undefined) {
|
|
572
|
+
this.removeAttribute(name);
|
|
573
|
+
} else {
|
|
574
|
+
this.setAttribute(name, String(value));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_upgradeProperty(property) {
|
|
579
|
+
if (!Object.prototype.hasOwnProperty.call(this, property)) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const value = this[property];
|
|
584
|
+
delete this[property];
|
|
585
|
+
this[property] = value;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_reportInvalidMarkup(message) {
|
|
589
|
+
console.warn(`form-dropzone: ${message}`, this);
|
|
590
|
+
}
|
|
591
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormDropzoneElement } from './form-dropzone.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@aarongustafson/form-dropzone",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A file field wrapper that builds a customizable drop zone for users to drag files into a form.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"html",
|
|
7
|
+
"custom element",
|
|
8
|
+
"web component"
|
|
9
|
+
],
|
|
10
|
+
"author": "Aaron Gustafson <aaron@easy-designs.net> (https://www.aaron-gustafson.com/)",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/aarongustafson/form-dropzone#readme",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/aarongustafson/form-dropzone.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/aarongustafson/form-dropzone/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"module": "form-dropzone.js",
|
|
23
|
+
"customElements": "custom-elements.json",
|
|
24
|
+
"types": "form-dropzone.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./index.js",
|
|
28
|
+
"types": "./form-dropzone.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./form-dropzone.js": {
|
|
31
|
+
"types": "./form-dropzone.d.ts",
|
|
32
|
+
"import": "./form-dropzone.js"
|
|
33
|
+
},
|
|
34
|
+
"./define.js": {
|
|
35
|
+
"import": "./define.js"
|
|
36
|
+
},
|
|
37
|
+
"./custom-elements.json": "./custom-elements.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"form-dropzone.js",
|
|
41
|
+
"form-dropzone.d.ts",
|
|
42
|
+
"define.js",
|
|
43
|
+
"index.js",
|
|
44
|
+
"custom-elements.json",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22.0.0"
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "npm@11.15.0",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"test": "vitest",
|
|
54
|
+
"test:ui": "vitest --ui",
|
|
55
|
+
"test:run": "vitest run",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
57
|
+
"lint": "npm run lint:eslint && npm run lint:prettier",
|
|
58
|
+
"format": "npm run format:eslint && npm run format:prettier",
|
|
59
|
+
"lint:eslint": "eslint .",
|
|
60
|
+
"format:eslint": "eslint . --fix",
|
|
61
|
+
"lint:prettier": "prettier \"**/*.js\" --check --ignore-path .gitignore",
|
|
62
|
+
"format:prettier": "prettier \"**/*.js\" --write --ignore-path .gitignore",
|
|
63
|
+
"version": "node scripts/update-demo-versions.js && git add demo/esm.html",
|
|
64
|
+
"postversion": "git push --follow-tags"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@open-wc/eslint-config": "^13.0.0",
|
|
68
|
+
"@testing-library/dom": "^10.4.1",
|
|
69
|
+
"@testing-library/user-event": "^14.6.1",
|
|
70
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
71
|
+
"@vitest/ui": "^4.1.7",
|
|
72
|
+
"eslint": "^9.39.4",
|
|
73
|
+
"eslint-config-prettier": "^10.1.8",
|
|
74
|
+
"happy-dom": "^20.9.0",
|
|
75
|
+
"marked": "16.4.1",
|
|
76
|
+
"prettier": "^3.8.3",
|
|
77
|
+
"vitest": "^4.1.7"
|
|
78
|
+
},
|
|
79
|
+
"publishConfig": {
|
|
80
|
+
"access": "public"
|
|
81
|
+
}
|
|
82
|
+
}
|