├── .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 | 40 |

Rejected files

41 | 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 |
ev.preventDefault()} class="settings"> 79 | 83 | 87 | 91 |
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 | } --------------------------------------------------------------------------------