|
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