master
svelte 236 lines 7.96 KB
Raw
1 <script context="module">
2 const _expansionState = {
3 /* treeNodeId: expanded <boolean> */
4 };
5 </script>
6
7 <script lang="ts">
8
9 import FileTreeNode, { computeTreeLeafDepth } from "./FileTreeNode.svelte";
10 import Directory from "../Icons/Directory.svelte";
11 import {filetree} from "../FileTree.svelte";
12 import ContextMenu from "../utility/ContextMenu.svelte";
13 import { moveFile, moveToTrash, pasteFile } from "../File";
14 import { clipboard } from "@tauri-apps/api";
15 import { onMount } from "svelte";
16 import { commands } from "../../config/commands";
17
18 export let root = false;
19 export let isroot = false;
20
21 export let id = "";
22 export let name = "";
23 export let path = "";
24 export let children = [];
25 export let contextMenuEnabled;
26 export let iconsEnabled;
27 export let isExpanded;
28 export let canDrag = false;
29
30 let expanded = _expansionState[name] || false;
31 let ref = null;
32 let refLabel = null;
33 let refChildren = null;
34 let selected = false;
35
36 let icon = Directory;
37
38 let contextmenu = false;
39 let contextmenuitems = [
40 {name: "Open in File Explorer", shortcut: commands.openInExplorer.keybind, action: async () => commands.openInExplorer.command(path)},
41 {name: "New Folder...", shortcut: "", action: () => commands.createFolder.command(path)},
42 {name: "New File...", shortcut: "", action: () => commands.createFile.command(path)},
43 {name: "Copy", shortcut: "Ctrl + C", action: async () => {await clipboard.writeText(path)}},
44 {name: "Cut", disabled: isroot, shortcut: "Ctrl + X", action: () => {console.warn("Feature not implemented yet.")}},
45 {name: "Paste", shortcut: "Ctrl + V", action: async () => {await pasteFile(path)}},
46 {name: "Copy Filename", shortcut: "", action: async () => {await clipboard.writeText(name)}},
47 {name: "Rename...", disabled: isroot, shortcut: commands.renameFile.keybind, action: () => {commands.renameFile.command(name, path)}},
48 {name: "Delete", disabled: isroot, shortcut: "Delete", action: async () => {await moveToTrash(path)}}
49 ]
50
51 onMount(() => {
52 if (isExpanded) {
53 expanded = _expansionState[name] = !expanded;
54 }
55 })
56 function findNode(nodeid, tree = null) {
57 if (!tree) {
58 tree = $filetree;
59 if (nodeid === tree[0].id) {
60 return tree[0];
61 }
62 }
63 for (let node of tree) {
64 if (nodeid === node.id) {
65 return node;
66 }
67 else if (node.children) {
68 let n = findNode(nodeid, node.children);
69 if (n) {
70 return n;
71 }
72 continue;
73 }
74 }
75 }
76
77 function dragstart(e) {
78 let data = {element: `filetree-node-${id}`, id:id, type: "directory"};
79 e.dataTransfer.setData("text/plain", JSON.stringify(data));
80 }
81
82 async function drop(e) {
83 let data = JSON.parse(e.dataTransfer.getData("text/plain"))
84 let el = document.getElementById(data.element);
85 let from = el.parentElement;
86 let to = dropTarget.parentElement.parentElement;
87 if (dropTarget.hasAttribute("data-nodetype")) {
88 if (dropTarget.nextElementSibling) {
89 to = dropTarget.nextElementSibling;
90 }
91 else {
92 to = dropTarget;
93 }
94 }
95
96 const fromid = from.getAttribute("data-id");
97 const toid = to.getAttribute("data-id");
98 const element = findNode(parseInt(data.id));
99 const fromlist = findNode(parseInt(fromid));
100 const tolist = findNode(parseInt(toid));
101
102 if (fromlist.path === tolist.path) return;
103
104 await moveFile(fromlist.path, tolist.path, element.path);
105
106 e.target.classList.remove("hover");
107 }
108
109 let dropTarget = null;
110 function dragenter(e) {
111 switch (e.target.nodeName) {
112 case "svg": case "SPAN":
113 dropTarget = e.target.parentElement;
114 break;
115 case "path":
116 dropTarget = e.target.parentElement.parentElement;
117 break;
118 default:
119 dropTarget = e.target;
120 }
121 e.target.classList.add("hover");
122 }
123 function dragleave(e) {
124 e.target.classList.remove("hover");
125 }
126
127 const toggleExpansion = (event) => {
128 for (let nodes of document.getElementsByClassName("tree-label")) {
129 nodes.classList.remove("selected")
130 }
131 if (event.target === refLabel) {
132 event.target.classList.add("selected")
133 }
134
135 if (!contextmenu) {
136 expanded = _expansionState[name] = !expanded;
137 }
138 };
139
140 function offset () {
141 const depth = computeTreeLeafDepth(refLabel);
142
143 if (parent) return depth + 1;
144 if (icon) return depth + 2;
145 return depth + 2.5;
146 };
147
148 $: arrowDown = expanded;
149 $: parent = Array.isArray(children);
150 $: if (refLabel) {
151 refLabel.style.marginLeft = `-${offset()}rem`;
152 refLabel.style.paddingLeft = `${offset()}rem`;
153 }
154 </script>
155
156 <svelte:window on:click={(e) => {
157 if (e.target !== refLabel) {
158 contextmenu = false;
159 }
160 }}></svelte:window>
161
162 {#if root}
163 {#each children as child}
164 {#if Array.isArray(child.children)}
165 <svelte:self {canDrag} on:nodeselect on:dblnodeselect isroot={root} {...child} {contextMenuEnabled} {iconsEnabled} {isExpanded}></svelte:self>
166 {:else}
167 <FileTreeNode {canDrag} on:nodeselect on:dblnodeselect {...child} {contextMenuEnabled} {iconsEnabled}/>
168 {/if}
169 {/each}
170 {:else}
171 <li id={`filetree-node-${id}`} bind:this={ref} class="treenode" class:root={isroot} on:dragenter|stopPropagation={dragenter} on:dragleave|stopPropagation={dragleave} on:dragend|stopPropagation={dragleave} title={path}>
172 <!-- svelte-ignore a11y-click-events-have-key-events -->
173 <div bind:this={refLabel} data-id={id} class:selected class="tree-label" data-nodetype="directory" on:click={toggleExpansion} on:dragover|preventDefault on:drop|stopPropagation={drop} draggable={isroot === false && canDrag} on:dragstart={dragstart} on:mouseup={(e) => {
174 if (e.button === 2 && contextMenuEnabled) {
175 contextmenu = true;
176 }
177 }}>
178 <span class="arrow" class:arrowDown>&#x25b6</span>
179 {#if icon && iconsEnabled}
180 <svelte:component this={icon} />
181 {/if}
182 {name}
183 </div>
184 {#if expanded}
185 <ul role="group" bind:this={refChildren} data-id={id} class="tree-children" on:dragover|preventDefault on:drop|stopPropagation={drop}>
186 {#each children as child (child.id) }
187 {#if Array.isArray(child.children)}
188 <svelte:self {canDrag} on:nodeselect on:dblnodeselect {...child} {contextMenuEnabled} {iconsEnabled} {isExpanded}></svelte:self>
189 {:else}
190 <FileTreeNode {canDrag} on:nodeselect on:dblnodeselect {...child} {contextMenuEnabled} {iconsEnabled} />
191 {/if}
192 {/each}
193 </ul>
194 {/if}
195 </li>
196 {/if}
197
198 {#if contextmenu}
199 <ContextMenu target={refLabel} items={contextmenuitems}></ContextMenu>
200 {/if}
201
202
203 <style lang="scss">
204 .arrow {
205 display: flex;
206 align-items: center;
207 transition: transform 200ms;
208 font-size: 12px;
209 margin-right: 5px;
210 }
211 .arrowDown {
212 transform: rotate(90deg);
213 }
214 .treenode {
215 display: block;
216 align-items: center;
217 cursor: pointer;
218 padding-left: 1rem;
219 }
220 .tree-label {
221 display: flex;
222 min-height: 1.5rem;
223 font-size: 14px;
224 align-items: center;
225 flex: 1;
226 white-space: nowrap;
227 :global(svg) {
228 min-width: 18px;
229 min-height: 18px;
230 padding: 0 5px 0 0;
231 }
232 }
233 .root > .tree-label {
234 font-weight: bold;
235 }
236 </style>