Skip to content

Commit 85af27f

Browse files
committed
feat: upload files by dragging
1 parent ab709b0 commit 85af27f

File tree

5 files changed

+103
-33
lines changed

5 files changed

+103
-33
lines changed

sites/preview/src/lib/components/tree/ContextMenu.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
}
124124
125125
onFilesSelected = (files) => {
126-
if (files === null || files.length === 0) {
126+
if (files === null) {
127127
return;
128128
}
129129

sites/preview/src/lib/components/tree/NameFormDialog.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
throw new Error("Dialog is closed");
4040
}
4141
42-
event.preventDefault();
4342
showArgs.onSubmit(name);
43+
event.preventDefault();
4444
};
4545
</script>
4646

sites/preview/src/lib/components/tree/Tree.svelte

+47-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { FileNode, FolderNode, type FileTree, type FileTreeNode } from "$lib/tree.svelte";
3-
import { formatSize } from "$lib/utils.js";
3+
import { composeEventHandlers, formatSize } from "$lib/utils.js";
44
import {
55
Tree,
66
type CircularReferenceErrorArgs,
@@ -11,6 +11,7 @@
1111
type TreeProps,
1212
} from "svelte-file-tree";
1313
import { toast } from "svelte-sonner";
14+
import type { EventHandler } from "svelte/elements";
1415
import { SvelteSet } from "svelte/reactivity";
1516
import ContextMenu from "./ContextMenu.svelte";
1617
import NameConflictDialog from "./NameConflictDialog.svelte";
@@ -50,6 +51,9 @@
5051
clipboardIds = new SvelteSet(defaultClipboardIds),
5152
pasteOperation = $bindable(),
5253
ref = $bindable(null),
54+
onfocusout,
55+
ondragover,
56+
ondrop,
5357
...rest
5458
}: Props = $props();
5559
@@ -210,6 +214,39 @@
210214
},
211215
});
212216
}
217+
218+
function handleExpand(target: TreeItemState<FileTreeNode>): void {
219+
expandedIds.add(target.node.id);
220+
}
221+
222+
function handleCollapse(target: TreeItemState<FileTreeNode>): void {
223+
expandedIds.delete(target.node.id);
224+
}
225+
226+
function handleFocusOut(): void {
227+
focusedItem = undefined;
228+
}
229+
230+
function handleFocusInItem(target: TreeItemState<FileTreeNode>): void {
231+
focusedItem = target;
232+
}
233+
234+
const handleDragOver: EventHandler<DragEvent, HTMLDivElement> = (event) => {
235+
event.preventDefault();
236+
};
237+
238+
const handleDrop: EventHandler<DragEvent, HTMLDivElement> = (event) => {
239+
if (event.defaultPrevented) {
240+
return;
241+
}
242+
243+
if (event.dataTransfer === null) {
244+
return;
245+
}
246+
247+
handleUploadFiles(tree, event.dataTransfer.files);
248+
event.preventDefault();
249+
};
213250
</script>
214251

215252
<div class="root flex h-full flex-col">
@@ -240,24 +277,23 @@
240277
bind:this={treeComponent}
241278
bind:pasteOperation
242279
bind:ref
243-
class="px-(--tree-inline-padding) py-2"
280+
class="h-full px-(--tree-inline-padding) py-2"
244281
copyNode={(node) => node.copy()}
245282
onResolveNameConflict={handleResolveNameConflict}
246283
onCircularReferenceError={handleCircularReferenceError}
247-
onfocusout={() => {
248-
focusedItem = undefined;
249-
}}
284+
onfocusout={composeEventHandlers(onfocusout, handleFocusOut)}
285+
ondragover={composeEventHandlers(ondragover, handleDragOver)}
286+
ondrop={composeEventHandlers(ondrop, handleDrop)}
250287
>
251288
{#snippet item({ item })}
252289
<TreeItem
253290
{item}
254291
{contextMenu}
255-
onExpand={() => expandedIds.add(item.node.id)}
256-
onCollapse={() => expandedIds.delete(item.node.id)}
257-
onRename={() => handleRename(item)}
258-
onfocusin={() => {
259-
focusedItem = item;
260-
}}
292+
onExpand={handleExpand}
293+
onCollapse={handleCollapse}
294+
onRename={handleRename}
295+
onUploadFiles={handleUploadFiles}
296+
onfocusin={() => handleFocusInItem(item)}
261297
/>
262298
{/snippet}
263299
</Tree>

sites/preview/src/lib/components/tree/TreeItem.svelte

+35-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
2-
import type { FileTreeNode } from "$lib/tree.svelte";
3-
import { formatSize } from "$lib/utils.js";
2+
import type { FileTreeNode, FolderNode } from "$lib/tree.svelte";
3+
import { composeEventHandlers, formatSize } from "$lib/utils.js";
44
import { ChevronDownIcon, FileIcon, FolderIcon, FolderOpenIcon } from "@lucide/svelte";
55
import { TreeItem, type TreeItemProps, type TreeItemState } from "svelte-file-tree";
66
import type { EventHandler } from "svelte/elements";
@@ -9,9 +9,10 @@
99
interface Props extends Omit<TreeItemProps, "children"> {
1010
item: TreeItemState<FileTreeNode>;
1111
contextMenu: ContextMenu | null;
12-
onExpand: () => void;
13-
onCollapse: () => void;
14-
onRename: () => void;
12+
onExpand: (target: TreeItemState<FileTreeNode>) => void;
13+
onCollapse: (target: TreeItemState<FileTreeNode>) => void;
14+
onRename: (target: TreeItemState<FileTreeNode>) => void;
15+
onUploadFiles: (target: FolderNode, files: FileList) => void;
1516
}
1617
1718
let {
@@ -20,55 +21,67 @@
2021
onExpand,
2122
onCollapse,
2223
onRename,
24+
onUploadFiles,
2325
ref = $bindable(null),
26+
onkeydown,
27+
oncontextmenu,
28+
ondrop,
2429
...rest
2530
}: Props = $props();
2631
2732
const handleKeyDown: EventHandler<KeyboardEvent, HTMLDivElement> = (event) => {
33+
if (item.disabled) {
34+
return;
35+
}
36+
2837
if (event.key === "F2") {
29-
onRename();
38+
onRename(item);
3039
event.preventDefault();
3140
}
3241
};
3342
3443
const handleContextMenu: EventHandler<MouseEvent, HTMLDivElement> = (event) => {
35-
if (contextMenu === null) {
36-
throw new Error("Context menu is not mounted");
37-
}
38-
39-
if (event.defaultPrevented) {
40-
return;
41-
}
42-
4344
if (item.disabled) {
4445
event.preventDefault();
4546
return;
4647
}
4748
49+
if (contextMenu === null) {
50+
throw new Error("Context menu is not mounted");
51+
}
52+
4853
contextMenu.show({
4954
type: "item",
5055
item: () => item,
5156
});
5257
};
5358
59+
const handleDrop: EventHandler<DragEvent, HTMLDivElement> = (event) => {
60+
if (event.dataTransfer === null || item.disabled || item.node.type === "file") {
61+
return;
62+
}
63+
64+
onUploadFiles(item.node, event.dataTransfer.files);
65+
event.preventDefault();
66+
};
67+
5468
const handleToggleClick: EventHandler<MouseEvent, HTMLButtonElement> = (event) => {
5569
if (ref === null) {
5670
throw new Error("Tree item is not mounted");
5771
}
5872
5973
if (item.disabled) {
60-
event.preventDefault();
6174
return;
6275
}
6376
6477
if (item.expanded) {
65-
onCollapse();
78+
onCollapse(item);
6679
} else {
67-
onExpand();
80+
onExpand(item);
6881
}
6982
70-
event.stopPropagation();
7183
ref.focus();
84+
event.stopPropagation();
7285
};
7386
7487
$effect(() => {
@@ -94,14 +107,16 @@
94107
dropPosition === "after" && "before:border-neutral-300 before:border-b-red-500",
95108
dropPosition === "inside" && "before:border-red-500",
96109
]}
97-
onkeydown={handleKeyDown}
98-
oncontextmenu={handleContextMenu}
110+
onkeydown={composeEventHandlers(onkeydown, handleKeyDown)}
111+
oncontextmenu={composeEventHandlers(oncontextmenu, handleContextMenu)}
112+
ondrop={composeEventHandlers(ondrop, handleDrop)}
99113
>
100114
<div
101115
class="flex items-center"
102116
style="padding-inline-start: calc(var(--spacing) * {item.depth * 6})"
103117
>
104118
<button
119+
type="button"
105120
aria-expanded={item.expanded}
106121
tabindex={-1}
107122
class={[

sites/preview/src/lib/utils.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { EventHandler } from "svelte/elements";
2+
13
const sizeFormatter = new Intl.NumberFormat(undefined, {
24
style: "decimal",
35
maximumFractionDigits: 2,
@@ -26,3 +28,20 @@ export function formatSize(size: number): string {
2628
size /= 1000;
2729
return sizeFormatter.format(size) + " TB";
2830
}
31+
32+
export function composeEventHandlers<TEvent extends Event, TTarget extends EventTarget>(
33+
a: EventHandler<TEvent, TTarget> | null | undefined,
34+
b: EventHandler<TEvent, TTarget>,
35+
): EventHandler<TEvent, TTarget> {
36+
return (event) => {
37+
if (a != null) {
38+
a(event);
39+
40+
if (event.defaultPrevented) {
41+
return;
42+
}
43+
}
44+
45+
b(event);
46+
};
47+
}

0 commit comments

Comments
 (0)