|
1 | 1 | <script lang="ts">
|
2 |
| - import { fromEvent } from "file-selector"; |
| 2 | + import { fromEvent, type FileWithPath } from "file-selector"; |
3 | 3 | import {
|
4 | 4 | fileAccepted,
|
5 | 5 | fileMatchSize,
|
6 | 6 | isEvtWithFiles,
|
7 | 7 | isIeOrEdge,
|
8 | 8 | isPropagationStopped,
|
9 |
| - TOO_MANY_FILES_REJECTION |
| 9 | + TOO_MANY_FILES_REJECTION, |
| 10 | + type GenericFileItem, |
| 11 | + type ErrorDescription |
10 | 12 | } from "../utils/index";
|
11 | 13 | import { onDestroy, createEventDispatcher } from "svelte";
|
12 | 14 |
|
|
17 | 19 | */
|
18 | 20 | export let accept: string | string[];
|
19 | 21 | export let disabled = false;
|
20 |
| - export let getFilesFromEvent = fromEvent; |
| 22 | + export let getFilesFromEvent = fromEvent as (evt: Event) => Promise<(GenericFileItem)[]>; |
21 | 23 | export let maxSize = Infinity;
|
22 | 24 | export let minSize = 0;
|
23 | 25 | export let multiple = true;
|
|
30 | 32 | export let containerStyles = "";
|
31 | 33 | export let disableDefaultStyles = false;
|
32 | 34 | export let name = "";
|
33 |
| - const dispatch = createEventDispatcher(); |
| 35 | +
|
| 36 | + export type { GenericFileItem }; |
| 37 | +
|
| 38 | + interface GenericEventDetails { |
| 39 | + event: Event; |
| 40 | + } |
| 41 | +
|
| 42 | + interface DragEventDetails { |
| 43 | + dragEvent: DragEvent |
| 44 | + }; |
| 45 | +
|
| 46 | + const dispatch = createEventDispatcher<{ |
| 47 | + dragenter: DragEventDetails; |
| 48 | + dragover: DragEventDetails; |
| 49 | + dragleave: DragEventDetails; |
| 50 | + filedropped: GenericEventDetails; |
| 51 | + drop: { |
| 52 | + acceptedFiles: File[]; |
| 53 | + fileRejections: { file: File, errors: ErrorDescription[] }[]; |
| 54 | + } & GenericEventDetails; |
| 55 | + droprejected: { |
| 56 | + fileRejections: { file: File, errors: ErrorDescription[] }[]; |
| 57 | + } & GenericEventDetails; |
| 58 | + dropaccepted: { |
| 59 | + acceptedFiles: File[]; |
| 60 | + } & GenericEventDetails; |
| 61 | + filedialogcancel: undefined; |
| 62 | + }>(); |
34 | 63 |
|
35 | 64 | //state
|
36 | 65 |
|
|
40 | 69 | isDragActive: false,
|
41 | 70 | isDragAccept: false,
|
42 | 71 | isDragReject: false,
|
43 |
| - draggedFiles: [], |
44 |
| - acceptedFiles: [], |
45 |
| - fileRejections: [] |
| 72 | + draggedFiles: [] as (FileWithPath | DataTransferItem)[], |
| 73 | + acceptedFiles: [] as File[], |
| 74 | + fileRejections: [] as { file: File, errors: ErrorDescription[] }[] |
46 | 75 | };
|
47 | 76 |
|
48 |
| - let rootRef; |
49 |
| - let inputRef; |
| 77 | + let rootRef: HTMLDivElement; |
| 78 | + let inputRef: HTMLInputElement | null; |
50 | 79 |
|
51 | 80 | function resetState() {
|
52 | 81 | state.isFileDialogActive = false;
|
|
59 | 88 | // Fn for opening the file dialog programmatically
|
60 | 89 | function openFileDialog() {
|
61 | 90 | if (inputRef) {
|
62 |
| - inputRef.value = null; // TODO check if null needs to be set |
| 91 | + inputRef.value = ""; // TODO check if empty needs to be set |
63 | 92 | state.isFileDialogActive = true;
|
64 | 93 | inputRef.click();
|
65 | 94 | }
|
66 | 95 | }
|
67 | 96 |
|
68 | 97 | // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
69 |
| - function onKeyDownCb(event) { |
| 98 | + function onKeyDownCb(event: KeyboardEvent) { |
70 | 99 | // Ignore keyboard events bubbling up the DOM tree
|
71 |
| - if (!rootRef || !rootRef.isEqualNode(event.target)) { |
| 100 | + // TODO should we just check `rootRef === event.target`? |
| 101 | + if (!rootRef || !(event.target instanceof Node) || !rootRef.isEqualNode(event.target)) { |
72 | 102 | return;
|
73 | 103 | }
|
74 | 104 |
|
| 105 | + // @ts-ignore: can't fix this deprecation, we need this version for legacy support |
75 | 106 | if (event.keyCode === 32 || event.keyCode === 13) {
|
76 | 107 | event.preventDefault();
|
77 | 108 | openFileDialog();
|
|
102 | 133 | }
|
103 | 134 | }
|
104 | 135 |
|
105 |
| - function onDragEnterCb(event) { |
| 136 | + function onDragEnterCb(event: DragEvent) { |
106 | 137 | event.preventDefault();
|
107 | 138 | stopPropagation(event);
|
108 | 139 |
|
109 |
| - dragTargetsRef = [...dragTargetsRef, event.target]; |
| 140 | + if (event.target != null) { |
| 141 | + dragTargetsRef = [...dragTargetsRef, event.target]; |
| 142 | + } |
110 | 143 |
|
111 | 144 | if (isEvtWithFiles(event)) {
|
112 | 145 | Promise.resolve(getFilesFromEvent(event)).then(draggedFiles => {
|
|
124 | 157 | }
|
125 | 158 | }
|
126 | 159 |
|
127 |
| - function onDragOverCb(event) { |
| 160 | + function onDragOverCb(event: DragEvent) { |
128 | 161 | event.preventDefault();
|
129 | 162 | stopPropagation(event);
|
130 | 163 |
|
|
143 | 176 | return false;
|
144 | 177 | }
|
145 | 178 |
|
146 |
| - function onDragLeaveCb(event) { |
| 179 | + function onDragLeaveCb(event: DragEvent) { |
147 | 180 | event.preventDefault();
|
148 | 181 | stopPropagation(event);
|
149 | 182 |
|
150 | 183 | // Only deactivate once the dropzone and all children have been left
|
151 | 184 | const targets = dragTargetsRef.filter(
|
152 |
| - target => rootRef && rootRef.contains(target) |
| 185 | + target => rootRef && target instanceof Node && rootRef.contains(target) |
153 | 186 | );
|
154 | 187 | // Make sure to remove a target present multiple times only once
|
155 | 188 | // (Firefox may fire dragenter/dragleave multiple times on the same element)
|
156 |
| - const targetIdx = targets.indexOf(event.target); |
| 189 | + const targetIdx = (targets as (EventTarget | null)[]).indexOf(event.target); |
157 | 190 | if (targetIdx !== -1) {
|
158 | 191 | targets.splice(targetIdx, 1);
|
159 | 192 | }
|
|
172 | 205 | }
|
173 | 206 | }
|
174 | 207 |
|
175 |
| - function onDropCb(event) { |
| 208 | + function onDropCb(event: Event) { |
176 | 209 | event.preventDefault();
|
177 | 210 | stopPropagation(event);
|
178 | 211 |
|
|
187 | 220 | return;
|
188 | 221 | }
|
189 | 222 |
|
190 |
| - const acceptedFiles = []; |
191 |
| - const fileRejections = []; |
| 223 | + const acceptedFiles: File[] = []; |
| 224 | + const fileRejections: { file: File, errors: ErrorDescription[] }[] = []; |
| 225 | +
|
| 226 | + files.forEach(fileGeneric => { |
| 227 | + const file = fileGeneric instanceof DataTransferItem ? (fileGeneric.getAsFile()!) : fileGeneric; |
192 | 228 |
|
193 |
| - files.forEach(file => { |
194 |
| - const [accepted, acceptError] = fileAccepted(file, accept); |
195 |
| - const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize); |
| 229 | + const { accepted, acceptError } = fileAccepted(file, accept); |
| 230 | + const { sizeMatch, sizeError } = fileMatchSize(file, minSize, maxSize); |
196 | 231 | if (accepted && sizeMatch) {
|
197 | 232 | acceptedFiles.push(file);
|
198 | 233 | } else {
|
199 |
| - const errors = [acceptError, sizeError].filter(e => e); |
| 234 | + const errors = [acceptError, sizeError].filter(e => e) as ErrorDescription[]; |
200 | 235 | fileRejections.push({ file, errors });
|
201 | 236 | }
|
202 | 237 | });
|
|
236 | 271 | resetState();
|
237 | 272 | }
|
238 | 273 |
|
239 |
| - function composeHandler(fn) { |
| 274 | + function composeHandler<Fn>(fn: Fn) { |
240 | 275 | return disabled ? null : fn;
|
241 | 276 | }
|
242 | 277 |
|
243 |
| - function composeKeyboardHandler(fn) { |
| 278 | + function composeKeyboardHandler<Fn>(fn: Fn) { |
244 | 279 | return noKeyboard ? null : composeHandler(fn);
|
245 | 280 | }
|
246 | 281 |
|
247 |
| - function composeDragHandler(fn) { |
| 282 | + function composeDragHandler<Fn>(fn: Fn) { |
248 | 283 | return noDrag ? null : composeHandler(fn);
|
249 | 284 | }
|
250 | 285 |
|
251 |
| - function stopPropagation(event) { |
| 286 | + function stopPropagation(event: Event) { |
252 | 287 | if (noDragEventsBubbling) {
|
253 | 288 | event.stopPropagation();
|
254 | 289 | }
|
255 | 290 | }
|
256 | 291 |
|
257 | 292 | // allow the entire document to be a drag target
|
258 |
| - function onDocumentDragOver(event) { |
| 293 | + function onDocumentDragOver(event: DragEvent) { |
259 | 294 | if (preventDropOnDocument) {
|
260 | 295 | event.preventDefault();
|
261 | 296 | }
|
262 | 297 | }
|
263 | 298 |
|
264 |
| - let dragTargetsRef = []; |
265 |
| - function onDocumentDrop(event) { |
| 299 | + let dragTargetsRef: EventTarget[] = []; |
| 300 | + function onDocumentDrop(event: DragEvent) { |
266 | 301 | if (!preventDropOnDocument) {
|
267 | 302 | return;
|
268 | 303 | }
|
269 |
| - if (rootRef && rootRef.contains(event.target)) { |
| 304 | + if (rootRef && event.target instanceof Node && rootRef.contains(event.target)) { |
270 | 305 | // If we intercepted an event for our instance, let it propagate down to the instance's onDrop handler
|
271 | 306 | return;
|
272 | 307 | }
|
|
282 | 317 | if (inputRef) {
|
283 | 318 | const { files } = inputRef;
|
284 | 319 |
|
285 |
| - if (!files.length) { |
| 320 | + if (!files?.length) { |
286 | 321 | state.isFileDialogActive = false;
|
287 | 322 | dispatch("filedialogcancel");
|
288 | 323 | }
|
|
296 | 331 | inputRef = null;
|
297 | 332 | });
|
298 | 333 |
|
299 |
| - function onInputElementClick(event) { |
| 334 | + function onInputElementClick(event: MouseEvent) { |
300 | 335 | event.stopPropagation();
|
301 | 336 | }
|
302 | 337 | </script>
|
|
339 | 374 | on:dragleave={composeDragHandler(onDragLeaveCb)}
|
340 | 375 | on:drop={composeDragHandler(onDropCb)}>
|
341 | 376 | <input
|
342 |
| - {accept} |
| 377 | + accept={Array.isArray(accept) ? accept.join(',') : accept} |
343 | 378 | {multiple}
|
344 | 379 | type="file"
|
345 | 380 | name={name}
|
|
0 commit comments