├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── app.html
├── global.d.ts
├── lib
│ ├── actions
│ │ ├── filedrop.ts
│ │ └── index.ts
│ ├── attr-accept.ts
│ ├── components
│ │ ├── FileDrop
│ │ │ ├── FileDrop.svelte
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── errors.ts
│ ├── event.ts
│ ├── file.ts
│ ├── index.ts
│ ├── options.ts
│ └── util.ts
└── routes
│ ├── index.svelte
│ └── test
│ ├── action.svelte
│ └── component.svelte
├── static
└── favicon.png
├── svelte.config.js
├── tests
├── components
│ └── FileDrop.spec.ts
└── data
│ └── pexels-dominika-roseclay-977876.jpg
└── tsconfig.json
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5 | plugins: ['svelte3', '@typescript-eslint'],
6 | ignorePatterns: ['*.cjs'],
7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8 | settings: {
9 | 'svelte3/typescript': () => require('typescript')
10 | },
11 | parserOptions: {
12 | sourceType: 'module',
13 | ecmaVersion: 2019
14 | },
15 | env: {
16 | browser: true,
17 | es2017: true,
18 | node: true
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /.svelte-kit
4 | /package
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .svelte-kit/**
2 | static/**
3 | build/**
4 | node_modules/**
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 4,
4 | "singleQuote": false,
5 | "trailingComma": "all",
6 | "printWidth": 110
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Chance Dinkins
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FileDrop
2 |
3 | A file dropzone action & component for [Svelte](https://svelte.dev/).
4 |
5 | ## Install
6 |
7 | ```bash
8 | npm i filedrop-svelte -D
9 |
10 | # yarn add filedrop-svelte -dev
11 | ```
12 | If using Typescript, see the [Typescript section](#typescript).
13 | ## Usage
14 |
15 | filedrop-svelte comes with both a component and an action. The component is basically a wrapper around the action with some some default styling.
16 |
17 | ### Component
18 |
19 | See [this REPL for minmimal usage](https://svelte.dev/repl/511ad04931514bcf98f7408edb08d075?version=3.41.0).
20 |
21 | ```html
22 |
28 |
29 | { files = e.detail.files }}>
30 | Upload files
31 |
32 |
33 | {#if files}
34 |
Accepted files
35 |
36 | {#each files.accepted as file}
37 | - {file.name} - {fileSize(file.size)}
38 | {/each}
39 |
40 | Rejected files
41 |
42 | {#each files.rejected as rejected}
43 | - {rejected.file.name} - {rejected.error.message}
44 | {/each}
45 |
46 | {/if}
47 | ```
48 |
49 | ### Action
50 |
51 | See this [REPL for minimal usage](https://svelte.dev/repl/645841f327b8484093f94b84de8a7e64?version=3.41.0).
52 |
53 | ```html
54 |
60 |
61 | {files = e.detail.files}}>
62 |
64 | Drag & drop files
65 | {files}
66 |
67 | ```
68 |
69 | ## Reference
70 |
71 | ### Options
72 |
73 | | parameter | purpose | type | default |
74 | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------- |
75 | | `accept` | specify file types to accept. See [HTML attribute: accept on MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) for more information. | `string` `string[]` | `undefined` |
76 | | `maxSize` | the maximum size a file can be in bytes. | `number` | `undefined` |
77 | | `minSize` | the minimum size a file can be in bytes. | `number` | `undefined` |
78 | | `fileLimit` | total number of files allowed in a transaction. A value of 0 disables the action/component, 1 turns multiple off, and any other value enables multiple. Any attempt to upload more files than allowed will result in the files being placed in rejections | `numer` | `undefined` |
79 | | `multiple` | sets the file input to `multiple`. See [HTML attribute: multiple on MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/multiple) for more information. | `boolean` | `true` |
80 | | `disabled` | disables the action/component, removing all event listeners | `boolean` | `false` |
81 | | `windowDrop` | determines whether or not files can be dropped anywhere in the window. A value of `false` would require that the files be droppped within the `` component or the element with `use:filedrop`. | `boolean` | `true` |
82 | | `clickToUpload` | causes the containing element to be treated as the input. If hideInput is true or undefined, disabling this does not change the `tabindex` of the container or remove the `keydown` eventListener | `boolean` | `true` |
83 | | `tabIndex` | tab index of the container. if `disabled` is `true` then this is set to `-1`. If `clickToUpload` is `true` or `undefined`, this defaults to 0. | `number` | `0` |
84 | | `hideInput` | if true or undefined, input[type='file'] will be set to display:none | `boolean` | `true` |
85 | | `input` | allows you to explicitly pass a reference to the file `HTMLInputElement` as a parameter. If `undefined`, the action will search for `input[type="file"]`. If one is not found, it will be appeneded to the element with `use:filedrop` | `HTMLInputElement` | `undefined` |
86 |
87 | ### Events
88 |
89 | | event | description | `event.detail` |
90 | | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------- |
91 | | `filedrop` | one or more files has been selected in the file dialog or drag-and-dropped | `FileDropSelectEvent` |
92 | | `filedragenter` | a dragenter event has occurred on the container element containnig one or more files | `FileDropDragEvent` |
93 | | `filedragleave` | a dragleave event has occurred on the container element containing one or more files | `FileDropDragEvent` |
94 | | `filedragover` | a dragover event has occurred on the container element containing one or more files | `FileDropDragEvent` |
95 | | `filedialogcancel` | the file dialog has been canceled without selecting files | `FileDropEvent` |
96 | | `filedialogclose` | the file dialog has been closed with files selected | `FileDropEvent` |
97 | | `filedialogopen` | the file dialog has been opened | `FileDropEvent` |
98 | | `windowfiledragenter` | a dragenter event has occurred on the document (event is named windowfiledragenter so not to confuse document with file) | `FileDropDragEvent` |
99 | | `windowfiledragleave` | a dragleave event has occurred on the document (event is named windowfiledragleave so not to confuse document with file) | `FileDropDragEvent` |
100 | | `windowfiledragover` | a dragover event has occurred on the document (event is named windowfiledragover so not to confuse document with file) | `FileDropDragEvent` |
101 |
102 | ### Errors
103 |
104 | | class | reason | code |
105 | | ------------------------------ | ------------------------------------------------------------- | --------------------------------- |
106 | | `InvalidFileTypeError` | file type does not satisfy `accept` | `InvalidFileType` (**0**) |
107 | | `FileCountExceededError` | total number of files selected or dropped exceeds `fileLimit` | `FileCountExceeded` (**1**) |
108 | | `FileSizeMinimumNotMetError` | file does not satisify `minSize` | `FileSizeMinimumNotMet` (**2**) |
109 | | `FileSizeMaximumExceededError` | file does not satisify `maxSize` | `FileSizeMaximumExceeded` (**3**) |
110 |
111 | ### Typescript
112 |
113 | In order for typings to work properly, you'll need to add the following to
114 | `global.d.ts` [until this issue is
115 | resolved](https://github.com/sveltejs/language-tools/issues/431):
116 |
117 | ```typescript
118 | declare type FileDropEvent = import("filedrop-svelte/event").FileDropEvent;
119 | declare type FileDropSelectEvent = import("filedrop-svelte/event").FileDropSelectEvent;
120 | declare type FileDropDragEvent = import("filedrop-svelte/event").FileDropDragEvent;
121 | declare namespace svelte.JSX {
122 | interface HTMLAttributes {
123 | onfiledrop?: (event: CustomEvent & { target: EventTarget & T }) => void;
124 | onfiledragenter?: (event: CustomEvent & { target: EventTarget & T }) => void;
125 | onfiledragleave?: (event: CustomEvent & { target: EventTarget & T }) => void;
126 | onfiledragover?: (event: CustomEvent & { target: EventTarget & T }) => void;
127 | onfiledialogcancel?: (event: CustomEvent & { target: EventTarget & T }) => void;
128 | onfiledialogclose?: (event: CustomEvent & { target: EventTarget & T }) => void;
129 | onfiledialogopen?: (event: CustomEvent & { target: EventTarget & T }) => void;
130 | onwindowfiledragenter?: (event: CustomEvent & { target: EventTarget & T }) => void;
131 | onwindowfiledragleave?: (event: CustomEvent & { target: EventTarget & T }) => void;
132 | onwindowfiledragover?: (event: CustomEvent & { target: EventTarget & T }) => void;
133 | }
134 | }
135 | ```
136 |
137 | You may need to edit `tsconfig.json` to include `global.d.ts` if it isn't already.
138 |
139 | ### Alternatives
140 |
141 | - [svelte-file-dropzone](https://github.com/thecodejack/svelte-file-dropzone)
142 |
143 | ### Previous art
144 |
145 | - [react-dropzone](https://github.com/react-dropzone/react-dropzone)
146 | - [svelte-file-dropzone](https://github.com/thecodejack/svelte-file-dropzone)
147 |
148 | ### Dependencies
149 |
150 | - [file-selector](https://github.com/react-dropzone/file-selector)
151 |
152 | ## Todo
153 |
154 | - tests
155 | - better documentation
156 | - demo website
157 |
158 | ## License
159 |
160 | MIT
161 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "filedrop-svelte",
3 | "description": "svelte component and action to create drag-and-drop file dropzones.",
4 | "version": "0.1.2",
5 | "license": "MIT",
6 | "author": {
7 | "name": "chance dinkins",
8 | "email": "chanceusc@gmail.com"
9 | },
10 | "contributors": [
11 | "not_existing"
12 | ],
13 | "repository": {
14 | "url": "https://github.com/chanced/filedrop-svelte",
15 | "type": "git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/chanced/filedrop-svelte/issues"
19 | },
20 | "keywords": [
21 | "svelte",
22 | "sveltekit",
23 | "dropzone",
24 | "drag and drop",
25 | "file upload",
26 | "drag-and-drop"
27 | ],
28 | "type": "module",
29 | "scripts": {
30 | "package": "svelte-kit package",
31 | "dev": "svelte-kit dev",
32 | "build": "svelte-kit build",
33 | "preview": "svelte-kit preview",
34 | "check": "svelte-check --tsconfig ./tsconfig.json",
35 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
36 | "lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
37 | "format": "prettier --write --plugin-search-dir=. .",
38 | "test": "playwright test"
39 | },
40 | "dependencies": {
41 | "file-selector": "^0.2.4",
42 | "pretty-bytes": "^6.0.0"
43 | },
44 | "devDependencies": {
45 | "@sveltejs/adapter-static": "^1.0.0-next.29",
46 | "@playwright/test": "^1.17.1",
47 | "@sveltejs/kit": "next",
48 | "@typescript-eslint/eslint-plugin": "^5.5.0",
49 | "@typescript-eslint/parser": "^5.5.0",
50 | "eslint": "^8.4.0",
51 | "eslint-config-prettier": "^8.3.0",
52 | "eslint-plugin-svelte3": "^3.2.1",
53 | "prettier": "~2.5.1",
54 | "prettier-plugin-svelte": "^2.5.0",
55 | "svelte": "^3.44.2",
56 | "svelte-check": "^2.2.10",
57 | "svelte-preprocess": "^4.9.8",
58 | "svelte2tsx": "^0.4.10",
59 | "ts-node": "^10.4.0",
60 | "tslib": "^2.3.1",
61 | "typescript": "^4.5.2"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %svelte.head%
8 |
9 |
10 | %svelte.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare type FileDropEvent = import("./lib/event").FileDropEvent;
3 | declare type FileDropSelectEvent = import("./lib/event").FileDropSelectEvent;
4 | declare type FileDropDragEvent = import("./lib/event").FileDropDragEvent;
5 | declare namespace svelte.JSX {
6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
7 | interface HTMLAttributes {
8 | onfiledrop?: (event: CustomEvent & { target: EventTarget & T }) => void;
9 | onfiledrop?: (event: CustomEvent & { target: EventTarget & T }) => void;
10 | onfiledragenter?: (event: CustomEvent & { target: EventTarget & T }) => void;
11 | onfiledragleave?: (event: CustomEvent & { target: EventTarget & T }) => void;
12 | onfiledragover?: (event: CustomEvent & { target: EventTarget & T }) => void;
13 | onfiledialogcancel?: (event: CustomEvent & { target: EventTarget & T }) => void;
14 | onfiledialogclose?: (event: CustomEvent & { target: EventTarget & T }) => void;
15 | onfiledialogopen?: (event: CustomEvent & { target: EventTarget & T }) => void;
16 | onwindowfiledragenter?: (event: CustomEvent & { target: EventTarget & T }) => void;
17 | onwindowfiledragleave?: (event: CustomEvent & { target: EventTarget & T }) => void;
18 | onwindowfiledragover?: (event: CustomEvent & { target: EventTarget & T }) => void;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/actions/filedrop.ts:
--------------------------------------------------------------------------------
1 | import type { Events, FileDropOptions } from "..";
2 | import { extractFilesFromEvent, getFilesFromEvent, isEventWithFiles, isNode } from "../event";
3 | import { isBrowser } from "../util";
4 |
5 | type Action = {
6 | destroy(): void;
7 | update(options?: FileDropOptions);
8 | };
9 |
10 | function getMultiple(opts: FileDropOptions): boolean {
11 | if (opts.fileLimit && opts.fileLimit > 1) {
12 | return true;
13 | }
14 | if (opts.fileLimit !== undefined && opts.fileLimit === 1) {
15 | return false;
16 | }
17 | if (opts.multiple !== undefined) {
18 | return opts.multiple;
19 | }
20 | return true;
21 | }
22 |
23 | function getTabIndex(node: HTMLElement, options: FileDropOptions): number {
24 | if (options.disabled) {
25 | return -1;
26 | }
27 | if (node.tabIndex > -1) {
28 | return node.tabIndex;
29 | }
30 | if (options.tabIndex !== undefined) {
31 | return options.tabIndex;
32 | }
33 | if (options.input.tabIndex > -1) {
34 | return options.input.tabIndex;
35 | }
36 | return options.hideInput ? 0 : -1;
37 | }
38 |
39 | function defaultToTrue(value: boolean | undefined): boolean {
40 | return value || value === undefined;
41 | }
42 |
43 | function getDisabled(options: FileDropOptions): boolean {
44 | if (options.fileLimit === 0) {
45 | return true;
46 | }
47 | if (options.disabled !== undefined) {
48 | return options.disabled;
49 | }
50 | return false;
51 | }
52 | function getAccept(options: FileDropOptions): string | string[] | undefined {
53 | if (options.accept !== undefined && options.accept.length) {
54 | if (Array.isArray(options.accept)) {
55 | return [...options.accept];
56 | } else {
57 | return options.accept;
58 | }
59 | }
60 | if (options.input?.accept.length) {
61 | return options.input?.accept;
62 | }
63 | return undefined;
64 | }
65 | function configOptions(node: HTMLElement, opts: FileDropOptions): FileDropOptions {
66 | const options: FileDropOptions = opts ? { ...opts } : {};
67 | options.disabled = getDisabled(options);
68 | options.id = getId(node, options);
69 | options.hideInput = defaultToTrue(options.hideInput);
70 | options.input = getInputElement(node, options);
71 | options.multiple = getMultiple(options);
72 | options.hideInput = defaultToTrue(options.hideInput);
73 | options.windowDrop = isBrowser() && defaultToTrue(options.windowDrop);
74 | options.tabIndex = getTabIndex(node, options);
75 | options.clickToUpload = defaultToTrue(options.clickToUpload);
76 | options.accept = getAccept(options);
77 | return options;
78 | }
79 | function getId(node: HTMLElement, opts: FileDropOptions): string | undefined {
80 | if (opts.id?.length) {
81 | return opts.id;
82 | }
83 | return node.id.length ? node.id : undefined;
84 | }
85 |
86 | function getInputElement(node: HTMLElement, { input }: FileDropOptions): HTMLInputElement {
87 | if (input != undefined) {
88 | if (isFileInput(input)) {
89 | return input;
90 | }
91 | throw new Error("input must be an HTMLInputElement with type file");
92 | }
93 | if (node.tagName === "INPUT") {
94 | throw new Error("FileDrop: action must be used on a containing element, not the input.");
95 | }
96 | const inputs = node.querySelectorAll("input[type='file']");
97 | if (!inputs.length) {
98 | const input = document.createElement("input");
99 | input.setAttribute("type", "file");
100 | input.style.display = "none";
101 | input.tabIndex = -1;
102 | return node.appendChild(input);
103 | }
104 | if (inputs.length > 1) {
105 | throw new Error(
106 | "FileDrop: container node may only contain a single file input unless input is specified in the options",
107 | );
108 | }
109 | return inputs.item(0) as HTMLInputElement;
110 | }
111 |
112 | export const filedrop = function (node: HTMLElement, opts?: FileDropOptions): Action {
113 | function dispatch(typ: K, detail: T): void {
114 | node.dispatchEvent(new CustomEvent(typ, { detail }));
115 | }
116 |
117 | let input: HTMLInputElement;
118 | let options: FileDropOptions;
119 | let isFileDialogOpen = false;
120 | let isDraggingFiles = false;
121 | let triggerEvent: Event;
122 |
123 | init(opts);
124 |
125 | async function handleChange(ev: Event) {
126 | ev.preventDefault();
127 | const files = await getFilesFromEvent(ev, options);
128 | dispatch("filedrop", {
129 | method: "input",
130 | files,
131 | isFileDialogOpen,
132 | isDraggingFiles,
133 | event: triggerEvent,
134 | id: options.id,
135 | options,
136 | });
137 | }
138 |
139 | async function handleDrop(ev: DragEvent) {
140 | isDraggingFiles = isEventWithFiles(ev);
141 | if (!isDraggingFiles) {
142 | return;
143 | }
144 | ev.preventDefault();
145 | triggerEvent = ev;
146 | const files = await getFilesFromEvent(ev, options);
147 | dispatch("filedrop", {
148 | method: "drop",
149 | files,
150 | options,
151 | id: options.id,
152 | isFileDialogOpen,
153 | isDraggingFiles,
154 | event: triggerEvent,
155 | });
156 | }
157 |
158 | function openDialog() {
159 | setTimeout(() => {
160 | input.click();
161 | }, 0);
162 | }
163 |
164 | function handleKeyDown(ev: KeyboardEvent) {
165 | triggerEvent = ev;
166 | if (ev.key === " " || ev.key === "Enter") {
167 | ev.preventDefault();
168 | openDialog();
169 | }
170 | }
171 |
172 | function handleInputClick() {
173 | isFileDialogOpen = true;
174 | dispatch("filedialogopen", {
175 | isDraggingFiles,
176 | isFileDialogOpen,
177 | id: options.id,
178 | options,
179 | });
180 | }
181 |
182 | function handleClick(ev: Event) {
183 | if (isNode(ev.target) && input.isEqualNode(ev.target)) {
184 | return;
185 | }
186 | ev.preventDefault();
187 | triggerEvent = ev;
188 | openDialog();
189 | }
190 | function handleDocumentDragEnter(ev: DragEvent) {
191 | isDraggingFiles = isEventWithFiles(ev);
192 | }
193 | async function handleDocumentDragLeave(ev: DragEvent) {
194 | isDraggingFiles = isEventWithFiles(ev);
195 | if (!isDraggingFiles) {
196 | return;
197 | }
198 | isDraggingFiles = false;
199 | const files = await extractFilesFromEvent(ev);
200 | dispatch("windowfiledragleave", {
201 | event: ev,
202 | files,
203 | isDraggingFiles,
204 | isFileDialogOpen,
205 | id: options.id,
206 | options,
207 | });
208 | }
209 |
210 | async function handleDocumentDragOver(ev: DragEvent) {
211 | ev.preventDefault();
212 | isDraggingFiles = isEventWithFiles(ev);
213 | if (!isDraggingFiles) {
214 | return;
215 | }
216 | const files = await extractFilesFromEvent(ev);
217 | dispatch("windowfiledragover", {
218 | event: ev,
219 | files,
220 | isDraggingFiles,
221 | isFileDialogOpen,
222 | id: options.id,
223 | options,
224 | });
225 | }
226 |
227 | async function handleDragEnter(ev: DragEvent) {
228 | isDraggingFiles = isEventWithFiles(ev);
229 | if (!isDraggingFiles) {
230 | return;
231 | }
232 | isDraggingFiles = true;
233 | const files = await extractFilesFromEvent(ev);
234 | dispatch("filedragenter", {
235 | files,
236 | event: ev,
237 | isDraggingFiles,
238 | isFileDialogOpen,
239 | id: options.id,
240 | options,
241 | });
242 | }
243 |
244 | async function handleDragLeave(ev: DragEvent) {
245 | isDraggingFiles = isEventWithFiles(ev);
246 | if (!isDraggingFiles) {
247 | return;
248 | }
249 | const files = await extractFilesFromEvent(ev);
250 | dispatch("filedragleave", {
251 | event: ev,
252 | files,
253 | isDraggingFiles,
254 | isFileDialogOpen,
255 | id: options.id,
256 | options,
257 | });
258 | }
259 | async function handleDragOver(ev: DragEvent) {
260 | isDraggingFiles = isEventWithFiles(ev);
261 | if (!isDraggingFiles) {
262 | return;
263 | }
264 | const files = await extractFilesFromEvent(ev);
265 | dispatch("filedragover", {
266 | event: ev,
267 | files,
268 | isDraggingFiles,
269 | isFileDialogOpen,
270 | id: options.id,
271 | options,
272 | });
273 | }
274 |
275 | async function handleDocumentDrop(ev: DragEvent) {
276 | ev.preventDefault();
277 | isDraggingFiles = isEventWithFiles(ev);
278 | if (!isDraggingFiles) {
279 | return;
280 | }
281 | if (isNode(ev.target) && (node.isEqualNode(ev.target) || node.contains(ev.target))) {
282 | // let it bubble
283 | return;
284 | }
285 | if (!options.windowDrop) {
286 | return;
287 | }
288 | const files = await getFilesFromEvent(ev, options);
289 | dispatch("filedrop", {
290 | method: "drop",
291 | event: ev,
292 | files,
293 | isDraggingFiles,
294 | isFileDialogOpen,
295 | id: options.id,
296 | options,
297 | });
298 | }
299 |
300 | function handleWindowFocus() {
301 | if (input && isFileDialogOpen) {
302 | isFileDialogOpen = false;
303 | const tick = (t: number) => {
304 | return () => {
305 | if (!input?.files.length && t < 21) {
306 | setTimeout(tick(t + 1), 35);
307 | return;
308 | }
309 | if (!input.files.length) {
310 | dispatch("filedialogcancel", {
311 | isDraggingFiles,
312 | isFileDialogOpen,
313 | id: options.id,
314 | options,
315 | });
316 | return;
317 | }
318 | dispatch("filedialogclose", {
319 | isDraggingFiles,
320 | isFileDialogOpen,
321 | id: options.id,
322 | options,
323 | });
324 | };
325 | };
326 | setTimeout(tick(0), 35);
327 | }
328 | }
329 |
330 | function init(opts: FileDropOptions) {
331 | options = configOptions(node, opts);
332 | input = options.input;
333 | node.tabIndex = options.tabIndex;
334 |
335 | if (!options.disabled) {
336 | node.classList.remove("disabled");
337 | input.multiple = options.multiple;
338 | if (options.accept?.length) {
339 | if (Array.isArray(options.accept)) {
340 | input.accept = options.accept.join(",");
341 | } else {
342 | input.accept = options.accept;
343 | }
344 | } else {
345 | input.removeAttribute("accept");
346 | }
347 |
348 | input.autocomplete = "off";
349 | if (options.hideInput) {
350 | input.style.display = "none";
351 | }
352 |
353 | if (isBrowser) {
354 | node.addEventListener("dragenter", handleDragEnter);
355 | node.addEventListener("dragleave", handleDragLeave);
356 | node.addEventListener("dragover", handleDragOver);
357 | node.addEventListener("drop", handleDrop);
358 |
359 | input.addEventListener("change", handleChange);
360 | input.addEventListener("click", handleInputClick);
361 |
362 | if (options.clickToUpload) {
363 | node.addEventListener("click", handleClick);
364 | } else {
365 | node.removeEventListener("click", handleClick);
366 | }
367 |
368 | if (options.hideInput) {
369 | node.addEventListener("keydown", handleKeyDown);
370 | }
371 |
372 | if (!options.hideInput && !options.clickToUpload) {
373 | node.removeEventListener("keydown", handleKeyDown);
374 | }
375 |
376 | window.addEventListener("focus", handleWindowFocus);
377 | document.addEventListener("dragenter", handleDocumentDragEnter);
378 | document.addEventListener("dragleave", handleDocumentDragLeave);
379 | document.addEventListener("dragover", handleDocumentDragOver);
380 | document.addEventListener("drop", handleDocumentDrop);
381 | }
382 | } else {
383 | node.classList.add("disabled");
384 | teardown();
385 | }
386 | }
387 | function teardown() {
388 | node.removeEventListener("keydown", handleKeyDown);
389 | node.removeEventListener("dragenter", handleDragEnter);
390 | node.removeEventListener("dragleave", handleDragLeave);
391 | node.removeEventListener("dragover", handleDragOver);
392 | node.removeEventListener("drop", handleDrop);
393 | node.removeEventListener("click", handleClick);
394 |
395 | input.removeEventListener("change", handleChange);
396 | input.removeEventListener("click", handleInputClick);
397 |
398 | input.files = null;
399 | if (isBrowser) {
400 | document.removeEventListener("dragover", handleDocumentDragOver);
401 | document.removeEventListener("dragenter", handleDocumentDragEnter);
402 | document.removeEventListener("dragleave", handleDocumentDragLeave);
403 | document.removeEventListener("drop", handleDocumentDrop);
404 | window.removeEventListener("focus", handleWindowFocus);
405 | }
406 | }
407 |
408 | return {
409 | update(opts?: FileDropOptions) {
410 | init(opts || {});
411 | },
412 | destroy() {
413 | teardown();
414 | },
415 | };
416 | };
417 |
418 | function isFileInput(node: HTMLElement): node is HTMLInputElement {
419 | return node.tagName === "INPUT" && node.getAttribute("type")?.toLowerCase() === "file";
420 | }
421 |
422 | export default filedrop;
423 |
--------------------------------------------------------------------------------
/src/lib/actions/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./filedrop";
2 | export { default as filedrop } from "./filedrop";
3 |
--------------------------------------------------------------------------------
/src/lib/attr-accept.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if the provided file type should be accepted by the input with accept attribute.
3 | * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#attr-accept
4 | *
5 | * Inspired by https://github.com/enyo/dropzone
6 | *
7 | * Original source: https://github.com/react-dropzone/attr-accept/blob/master/src/index.js
8 | * @param file {File} https://developer.mozilla.org/en-US/docs/Web/API/File
9 | * @param acceptedFiles {string}
10 | * @returns {boolean}
11 | */
12 |
13 | export default function (file: File, acceptedFiles: string | string[]): boolean {
14 | if (!file || !acceptedFiles) {
15 | return true;
16 | }
17 | const acceptedFilesArray = Array.isArray(acceptedFiles) ? acceptedFiles : acceptedFiles.split(",");
18 | const fileName = file.name || "";
19 | const mimeType = (file.type || "").toLowerCase();
20 | const baseMimeType = mimeType.replace(/\/.*$/, "");
21 |
22 | return acceptedFilesArray.some((type) => {
23 | const validType = type.trim().toLowerCase();
24 | if (validType.charAt(0) === ".") {
25 | return fileName.toLowerCase().endsWith(validType);
26 | } else if (validType.endsWith("/*")) {
27 | // This is something like a image/* mime type
28 | return baseMimeType === validType.replace(/\/.*$/, "");
29 | }
30 | return mimeType === validType;
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/components/FileDrop/FileDrop.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 |
64 |
65 |
101 |
--------------------------------------------------------------------------------
/src/lib/components/FileDrop/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./FileDrop.svelte";
2 | export * from "./FileDrop.svelte";
3 |
--------------------------------------------------------------------------------
/src/lib/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./FileDrop";
2 | export * from "./FileDrop";
3 |
--------------------------------------------------------------------------------
/src/lib/errors.ts:
--------------------------------------------------------------------------------
1 | export enum ErrorCode {
2 | InvalidFileType,
3 | FileSizeMaximumExceeded,
4 | FileSizeMinimumNotMet,
5 | FileCountExceeded,
6 | }
7 |
8 | export const errorCodeNames = {
9 | [ErrorCode.InvalidFileType]: "invalid file type",
10 | [ErrorCode.FileCountExceeded]: "file count exceeded",
11 | [ErrorCode.FileSizeMinimumNotMet]: "min file size not met",
12 | [ErrorCode.FileSizeMaximumExceeded]: "max file size exceeded",
13 | };
14 |
15 | import type { FileWithPath } from "file-selector";
16 | import prettyBytes from "pretty-bytes";
17 | export class FileDropError extends Error {
18 | code: ErrorCode;
19 | message: string;
20 | file: FileWithPath;
21 | constructor(code: ErrorCode, file: FileWithPath, message: string) {
22 | super(message);
23 | this.code = code;
24 | this.message = message;
25 | this.file = file;
26 | this.name = errorCodeNames[code];
27 | }
28 | }
29 | export class InvalidFileTypeError extends FileDropError {
30 | allowed: string[];
31 | constructor(file: FileWithPath, allowed: string | string[], message?: string) {
32 | if (!message) {
33 | message = `${file.name} is not an accepted file type (${file.type}`;
34 | }
35 | super(ErrorCode.InvalidFileType, file, message);
36 | if (typeof allowed === "string") {
37 | allowed = allowed.split(",");
38 | }
39 | this.allowed = allowed;
40 | }
41 | }
42 | export class FileSizeMinimumNotMetError extends FileDropError {
43 | minimum: number;
44 | readableMinimum: string;
45 | readableSize: string;
46 | constructor(file: FileWithPath, minimum: number, message?: string) {
47 | message =
48 | message ??
49 | `$file size (${prettyBytes(file.size)}) does not meet the minimum of ${prettyBytes(minimum)}.`;
50 | super(ErrorCode.FileSizeMinimumNotMet, file, message);
51 |
52 | this.minimum = minimum;
53 | }
54 | }
55 | export class FileSizeLimitExceededError extends FileDropError {
56 | limit: number;
57 | readableLimit: string;
58 | readableSize: string;
59 | constructor(file: File, limit: number, message?: string) {
60 | message =
61 | message ?? `file size (${prettyBytes(file.size)}) exceeds maximum of ${prettyBytes(limit)}.`;
62 | super(ErrorCode.FileSizeMaximumExceeded, file, message);
63 | }
64 | }
65 | export class FileCountExceededError extends FileDropError {
66 | limit: number;
67 | constructor(file: FileWithPath, limit: number, message?: string) {
68 | message = message ?? `file count limit of ${limit} exceeded`;
69 | super(ErrorCode.FileCountExceeded, file, message);
70 | this.limit = limit;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/lib/event.ts:
--------------------------------------------------------------------------------
1 | import type { FileDropOptions } from "./options";
2 | import { FileWithPath, fromEvent } from "file-selector";
3 | import { processFiles, Files, isFileWithPath } from "./file";
4 |
5 | export interface FileDropEvent {
6 | readonly id?: string;
7 | readonly options: FileDropOptions;
8 | readonly isFileDialogOpen: boolean;
9 | readonly isDraggingFiles: boolean;
10 | }
11 |
12 | export interface FileDropDragEvent extends FileDropEvent {
13 | files: File[];
14 | event: DragEvent;
15 | }
16 |
17 | export interface FileDropSelectEvent extends FileDropEvent {
18 | files: Files;
19 | event: Event;
20 | method: "drop" | "input";
21 | }
22 |
23 | export interface Events {
24 | /**
25 | * one or more files has been selected in the file dialog or drag-and-dropped
26 | */
27 | filedrop: FileDropSelectEvent;
28 | /**
29 | * a dragenter event has occurred on the container element containnig one or more files
30 | */
31 | filedragenter: FileDropDragEvent;
32 | /**
33 | * a dragleave event has occurred on the container element containing one or more files
34 | */
35 | filedragleave: FileDropDragEvent;
36 | /**
37 | * a dragover event has occurred on the container element containing one or more files
38 | */
39 | filedragover: FileDropDragEvent;
40 | /**
41 | * the file dialog has been canceled without selecting files
42 | */
43 | filedialogcancel: FileDropEvent;
44 | /**
45 | * the file dialog has been closed with files selected
46 | */
47 | filedialogclose: FileDropEvent;
48 | /**
49 | * the file dialog has been opened
50 | */
51 | filedialogopen: FileDropEvent;
52 | /**
53 | * a dragenter event has occurred on the `document`
54 | *
55 | * Note: event is named windowfiledragenter so not to confuse `document` with file
56 | */
57 | windowfiledragenter: FileDropDragEvent;
58 | /**
59 | * a dragleave event has occurred on the `document`
60 | *
61 | * Note: event is named windowfiledragleave so not to confuse document with file
62 | */
63 | windowfiledragleave: FileDropDragEvent;
64 | /**
65 | * a dragover event has occurred on the `document`
66 | *
67 | * Note: event is named windowfiledragover so not to confuse document with file
68 | */
69 | windowfiledragover: FileDropDragEvent;
70 | }
71 |
72 | export function isDragEvent(event: Event | DragEvent): event is DragEvent {
73 | return "dataTransfer" in event;
74 | }
75 |
76 | type EventWithFiles = Event & { target: { files: File[] } };
77 |
78 | export function doesEventTargetHaveFiles(event: Event | EventWithFiles): event is EventWithFiles {
79 | return "files" in event.target;
80 | }
81 |
82 | function isTargetHTMLElement(target: EventTarget): target is HTMLElement {
83 | return "tagName" in target;
84 | }
85 | function doesTargetHaveFiles(target: EventTarget): target is HTMLInputElement {
86 | return "files" in target;
87 | }
88 |
89 | function isEventTargetHTMLElementInput(ev: InputEvent | Event): ev is Event & { target: HTMLInputElement } {
90 | return isTargetHTMLElement(ev.target) && doesTargetHaveFiles(ev.target);
91 | }
92 |
93 | export function isEventWithFiles(ev: Event): boolean {
94 | if (isEventTargetHTMLElementInput(ev)) {
95 | return !!ev.target.files.length;
96 | }
97 | return (
98 | isDragEvent(ev) && ev.dataTransfer.types.some((t) => t === "Files" || t === "application/x-moz-file")
99 | );
100 | }
101 |
102 | export async function extractFilesFromEvent(ev: Event): Promise {
103 | const res = await fromEvent(ev);
104 | const files = res.map((f) => (isFileWithPath(f) ? f : f.getAsFile()));
105 | return files as FileWithPath[];
106 | }
107 |
108 | export async function getFilesFromEvent(ev: Event, opts: FileDropOptions): Promise {
109 | const files = await extractFilesFromEvent(ev);
110 | return processFiles(files, opts);
111 | }
112 |
113 | export function isNode(target: EventTarget | Node): target is Node {
114 | return "childNodes" in target;
115 | }
116 |
--------------------------------------------------------------------------------
/src/lib/file.ts:
--------------------------------------------------------------------------------
1 | import type { FileWithPath } from "file-selector";
2 | import doesAccept from "./attr-accept";
3 | import {
4 | FileCountExceededError,
5 | FileDropError,
6 | FileSizeLimitExceededError,
7 | FileSizeMinimumNotMetError,
8 | InvalidFileTypeError,
9 | } from "./errors";
10 | import type { FileDropOptions } from "./options";
11 |
12 | declare module "file-selector" {
13 | interface FileWithPath extends Blob {
14 | readonly path?: string;
15 | readonly webkitRelativePath: string;
16 | }
17 | }
18 |
19 | export interface RejectedFile {
20 | file: FileWithPath;
21 | error: FileDropError;
22 | }
23 |
24 | export interface Files {
25 | accepted: FileWithPath[];
26 | rejected: RejectedFile[];
27 | }
28 |
29 | type CheckParams = {
30 | accept?: string | string[];
31 | maxSize?: number | undefined | null;
32 | minSize?: number | undefined | null;
33 | };
34 |
35 | export function processFiles(files: FileWithPath[], options: FileDropOptions): Files {
36 | let { fileLimit } = options;
37 |
38 | if (options.multiple != undefined && !options.multiple) {
39 | fileLimit = 1;
40 | }
41 | let count = 0;
42 | return files.reduce(
43 | (accumulator, file) => {
44 | let error = checkFile(file, options);
45 | if (error != undefined) {
46 | accumulator.rejected.push({ file, error });
47 | return accumulator;
48 | } else if (fileLimit > 0 && count >= fileLimit) {
49 | error = new FileCountExceededError(file, fileLimit);
50 | accumulator.rejected.push({ file, error });
51 | return accumulator;
52 | }
53 | accumulator.accepted.push(file);
54 | count = count + 1;
55 | return accumulator;
56 | },
57 | { accepted: [], rejected: [] } as Files,
58 | );
59 | }
60 |
61 | export function checkFile(file: FileWithPath, params: CheckParams): FileDropError | undefined {
62 | const { accept, minSize: min, maxSize: max } = params;
63 | return checkFileType(file, accept) || checkFileSize(file, { min, max });
64 | }
65 |
66 | export function checkFileType(
67 | file: File,
68 | accept: string | string[] | undefined,
69 | ): InvalidFileTypeError | undefined {
70 | // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with
71 | // that MIME type will always be accepted
72 | if (accept != undefined) {
73 | if (typeof accept === "string") {
74 | accept = accept.split(",");
75 | }
76 | accept.push("application/x-moz-file");
77 | if (!doesAccept(file, accept)) {
78 | return new InvalidFileTypeError(file, accept);
79 | }
80 | }
81 | return undefined;
82 | }
83 |
84 | export function checkMinFileSize(file: File, min?: number | null): FileSizeMinimumNotMetError | undefined {
85 | if (min && file.size < min) {
86 | return new FileSizeMinimumNotMetError(file, min);
87 | }
88 | }
89 |
90 | export function checkMaxFileSize(file: File, max?: number | null): FileSizeLimitExceededError | undefined {
91 | if (max && file.size > max) {
92 | return new FileSizeLimitExceededError(file, max);
93 | }
94 | }
95 |
96 | type FileSizeLimits = {
97 | min?: number | null | undefined;
98 | max?: number | null | undefined;
99 | };
100 | export function checkFileSize(file: File, { min, max }: FileSizeLimits): FileDropError | undefined {
101 | return checkMaxFileSize(file, max) || checkMinFileSize(file, min);
102 | }
103 |
104 | export function isFileWithPath(f: FileWithPath | DataTransferItem): f is FileWithPath {
105 | return "arrayBuffer" in f && "size" in f;
106 | }
107 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./components";
2 | export * from "./options";
3 | export * from "./components";
4 | export * from "./errors";
5 | export * from "./file";
6 | export * from "./event";
7 | export * from "./util";
8 | export * from "./actions";
9 |
--------------------------------------------------------------------------------
/src/lib/options.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Options for FileDrop component & action
3 | */
4 | export interface FileDropOptions {
5 | /**
6 | * specify file types to accept.
7 | *
8 | * See [HTML attribute: accept on MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) for more information.
9 | */
10 | accept?: string | string[];
11 | /**
12 | * the maximum size a file can be in bytes.
13 | */
14 | maxSize?: number;
15 | /**
16 | * the minimum size a file can be in bytes.
17 | */
18 | minSize?: number;
19 | /**
20 | * total number of files allowed in a transaction.
21 | *
22 | * A value of 0 disables the action/component, 1 turns multiple off, and any other value enables multiple.
23 | *
24 | * Any attempt to upload more files than allowed will result in the files being placed in rejections
25 | */
26 | fileLimit?: number;
27 | /**
28 | * sets the file input to `multiple`.
29 | *
30 | * See [HTML attribute: multiple on MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/multiple) for more information.
31 | */
32 | multiple?: boolean;
33 | /**
34 | * disables the action/component, removing all event listeners
35 | */
36 | disabled?: boolean;
37 | /**
38 | * determines whether or not files can be dropped anywhere in the window.
39 | *
40 | * A value of `false` would require that the files be droppped within the `` component or the element with `use:filedrop`.
41 | */
42 | windowDrop?: boolean;
43 | /**
44 | * causes the containing element to be treated as the input.
45 | *
46 | * If hideInput is `true` or `undefined`, disabling this does not change the `tabindex` of the container or remove the `keydown` eventListener
47 | */
48 | clickToUpload?: boolean;
49 | /**
50 | * tab index of the container.
51 | *
52 | * If `disabled` is `true` then this is set to `-1`.
53 | *
54 | * If `clickToUpload` is `true` or `undefined`, this defaults to 0.
55 | */
56 | tabIndex?: number;
57 | /**
58 | * if true or undefined, input[type='file'] will be set to display:none
59 | */
60 | hideInput?: boolean;
61 | /**
62 | * style applied to the node
63 | */
64 | style?: string;
65 | /**
66 | * id of the node
67 | */
68 | id?: string;
69 | /**
70 | * allows you to explicitly pass the file `HTMLInputElement` as a parameter.
71 | *
72 | * If this `undefined`, the action will search for `input[type="file"]`.
73 | *
74 | * If an `input[type="file"]` is not found, it will be appeneded to the element with `use:filedrop`
75 | */
76 | input?: HTMLInputElement;
77 | }
78 |
--------------------------------------------------------------------------------
/src/lib/util.ts:
--------------------------------------------------------------------------------
1 | import type { FileWithPath } from "file-selector";
2 |
3 | export function isDataTransferItem(f: FileWithPath | DataTransferItem): f is DataTransferItem {
4 | return f && "getAsFile" in f;
5 | }
6 |
7 | export function isArrayOfStrings(value: unknown): value is string[] {
8 | return Array.isArray(value) && value.every((v) => typeof v === "string");
9 | }
10 |
11 | export function isString(value: unknown): value is string {
12 | return typeof value === "string";
13 | }
14 | export function isBrowser(): boolean {
15 | return typeof window !== "undefined";
16 | }
17 |
--------------------------------------------------------------------------------
/src/routes/index.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | FileDrop Examples
15 | Component
16 |
17 |
{
19 | compFiles = ev.detail.files;
20 | }}
21 | {fileLimit}
22 | {maxSize}
23 | {minSize}
24 | />
25 | {#if compFiles}
26 | Accepted Files
27 |
28 | {#each compFiles?.accepted as file}
29 | - {file.name} - {file.size}
30 | {/each}
31 |
32 | Rejected files
33 |
34 | {#each compFiles?.rejected as rejected}
35 | -
36 | {rejected.file.name} - {rejected.error.message}
37 |
38 | {/each}
39 |
40 | {/if}
41 |
42 |
43 |
44 |
{
47 | actionFiles = e.detail.files;
48 | }}
49 | class="filedrop"
50 | >
51 |
56 |
Upload content (action)
57 |
58 | {#if actionFiles}
59 |
Accepted Files
60 |
61 | {#each actionFiles?.accepted as file}
62 | - {file.name} - {file.size}
63 | {/each}
64 |
65 |
Rejected files
66 |
67 | {#each actionFiles?.rejected as rejected}
68 | -
69 | {rejected.file.name} - {rejected.error.message}
70 |
71 | {/each}
72 |
73 | {/if}
74 |
75 |
76 |
77 |
78 |
92 |
93 |
98 |
--------------------------------------------------------------------------------
/src/routes/test/action.svelte:
--------------------------------------------------------------------------------
1 |
50 |
--------------------------------------------------------------------------------
/src/routes/test/component.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 |
58 |
59 | (files = e.detail.files)} />
60 |
61 | {#if files}
62 | Accepted files
63 |
64 | {#each files.accepted as file}
65 | - {file.name} ({file.size})
66 | {/each}
67 |
68 | Rejected files
69 |
70 | {#each files.rejected as rejected}
71 | - {rejected.file.name} - {rejected.error.message}
72 | {/each}
73 |
74 | {/if}
75 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chanced/filedrop-svelte/9a6070765043567576bf2e90f4f1012f81996587/static/favicon.png
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from "@sveltejs/adapter-static";
2 | import preprocess from "svelte-preprocess";
3 | /** @type {import('@sveltejs/kit').Config} */
4 | const config = {
5 | // Consult https://github.com/sveltejs/svelte-preprocess
6 | // for more information about preprocessors
7 | preprocess: preprocess(),
8 | kit: {
9 | adapter: adapter(),
10 | },
11 | };
12 |
13 | export default config;
14 |
--------------------------------------------------------------------------------
/tests/components/FileDrop.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test";
2 |
3 | test("basic test", async ({ page }) => {
4 | page.on("console", (msg) => console.log(msg.text()));
5 |
6 | await page.goto("http://localhost:3000/test/component?disabled=true");
7 | });
8 |
--------------------------------------------------------------------------------
/tests/data/pexels-dominika-roseclay-977876.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chanced/filedrop-svelte/9a6070765043567576bf2e90f4f1012f81996587/tests/data/pexels-dominika-roseclay-977876.jpg
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "module": "es2020",
5 | "lib": [
6 | "es2020",
7 | "DOM"
8 | ],
9 | "target": "es2019",
10 | /**
11 | svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
12 | to enforce using \`import type\` instead of \`import\` for Types.
13 | */
14 | "importsNotUsedAsValues": "error",
15 | "isolatedModules": true,
16 | "resolveJsonModule": true,
17 | /**
18 | To have warnings/errors of the Svelte compiler at the correct position,
19 | enable source maps by default.
20 | */
21 | "sourceMap": true,
22 | "esModuleInterop": true,
23 | "skipLibCheck": true,
24 | "forceConsistentCasingInFileNames": true,
25 | "baseUrl": ".",
26 | "allowJs": true,
27 | "checkJs": true,
28 | "paths": {
29 | "$lib": [
30 | "src/lib"
31 | ],
32 | "$lib/*": [
33 | "src/lib/*"
34 | ],
35 | }
36 | },
37 | "include": [
38 | "src/**/*.d.ts",
39 | "src/**/*.js",
40 | "src/**/*.ts",
41 | "src/**/*.svelte"
42 | ]
43 | }
--------------------------------------------------------------------------------