| 1 | <script lang="ts" context="module"> |
| 2 | export function computeTreeLeafDepth(node) { |
| 3 | let depth = 0; |
| 4 | |
| 5 | if (node == null) return depth; |
| 6 | |
| 7 | let parentNode = node.parentNode; |
| 8 | |
| 9 | while (parentNode != null && parentNode.getAttribute("role") !== "tree") { |
| 10 | parentNode = parentNode.parentNode; |
| 11 | if (parentNode.tagName === "LI") depth++; |
| 12 | } |
| 13 | |
| 14 | return depth; |
| 15 | } |
| 16 | </script> |
| 17 | <script lang="ts"> |
| 18 | import { createEventDispatcher } from "svelte"; |
| 19 | import File from "../Icons/File.svelte"; |
| 20 | import ContextMenu from "../utility/ContextMenu.svelte"; |
| 21 | import { moveToTrash, openInExplorer, renameFile } from "../File"; |
| 22 | import { addEditorTab } from "../EditorTabList.svelte"; |
| 23 | import { clipboard } from "@tauri-apps/api"; |
| 24 | import { commands } from "../../config/commands"; |
| 25 | |
| 26 | export let id; |
| 27 | export let name; |
| 28 | export let path; |
| 29 | export let contextMenuEnabled; |
| 30 | export let iconsEnabled; |
| 31 | export let canDrag = false; |
| 32 | |
| 33 | let selected = false; |
| 34 | |
| 35 | let ref = null; |
| 36 | let refLabel = null; |
| 37 | let icon = File; |
| 38 | |
| 39 | let contextmenu = false; |
| 40 | let contextmenuitems = [ |
| 41 | {name: "Open in File Explorer", shortcut: commands.openInExplorer.keybind, action: async () => {commands.openInExplorer.command(path)}}, |
| 42 | {name: "Copy", shortcut: "Ctrl + C", action: async () => {await clipboard.writeText(path)}}, |
| 43 | {name: "Cut", shortcut: "Ctrl + X", action: () => {console.warn("Feature not implemented yet.")}}, |
| 44 | {name: "Copy Filename", shortcut: "", action: async () => {await clipboard.writeText(name)}}, |
| 45 | {name: "Edit", shortcut: "", action: () => {addEditorTab(path, name)}}, |
| 46 | {name: "Rename...", shortcut: commands.renameFile.keybind, action: () => {commands.renameFile.command(name, path)}}, |
| 47 | {name: "Delete", shortcut: "Delete", action: async () => {await moveToTrash(path)}} |
| 48 | ] |
| 49 | |
| 50 | function dragstart(e) { |
| 51 | let data = {element: `filetree-node-${id}`, id:id, type: "file"}; |
| 52 | e.dataTransfer.setData("text/plain", JSON.stringify(data)); |
| 53 | } |
| 54 | |
| 55 | const offset = () => computeTreeLeafDepth(refLabel) + (icon ? 2 : 1); |
| 56 | |
| 57 | const dispatch = createEventDispatcher(); |
| 58 | function handleSelect(node, event) { |
| 59 | for (let nodes of document.getElementsByClassName("tree-label")) { |
| 60 | nodes.classList.remove("selected") |
| 61 | } |
| 62 | if (event.target === refLabel) { |
| 63 | event.target.classList.add("selected") |
| 64 | } |
| 65 | if (!contextmenu) { |
| 66 | dispatch("nodeselect", {node}); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | function handleDoubleSelect(node, event) { |
| 71 | for (let nodes of document.getElementsByClassName("tree-label")) { |
| 72 | nodes.classList.remove("selected") |
| 73 | } |
| 74 | if (event.target === refLabel) { |
| 75 | event.target.classList.add("selected") |
| 76 | } |
| 77 | if (!contextmenu) { |
| 78 | dispatch("dblnodeselect", {node}); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | $: if (refLabel) { |
| 83 | refLabel.style.marginLeft = `-${offset()}rem`; |
| 84 | refLabel.style.paddingLeft = `${offset()}rem`; |
| 85 | } |
| 86 | |
| 87 | $:treenode = {id, name, path} |
| 88 | </script> |
| 89 | |
| 90 | <svelte:window on:click={(e) => { |
| 91 | if (e.target !== refLabel) { |
| 92 | refLabel.classList.remove("selected") |
| 93 | contextmenu = false; |
| 94 | } |
| 95 | }}></svelte:window> |
| 96 | |
| 97 | <li id={`filetree-node-${id}`} bind:this={ref} class="treenode" title={path}> |
| 98 | <!-- svelte-ignore a11y-click-events-have-key-events --> |
| 99 | <div bind:this={refLabel} class="tree-label" class:selected |
| 100 | on:click={(e) => {handleSelect(treenode, e)}} |
| 101 | on:dblclick={(e) => {handleDoubleSelect(treenode, e)}} |
| 102 | draggable={canDrag} on:dragstart={dragstart} on:mouseup={(e) => { |
| 103 | if (e.button === 2 && contextMenuEnabled) { |
| 104 | contextmenu = true; |
| 105 | handleSelect(treenode, e); |
| 106 | } |
| 107 | }}> |
| 108 | <span class="no-arrow"></span> |
| 109 | {#if icon && iconsEnabled} |
| 110 | <svelte:component this={icon} /> |
| 111 | {/if} |
| 112 | {name} |
| 113 | </div> |
| 114 | </li> |
| 115 | |
| 116 | {#if contextmenu} |
| 117 | <ContextMenu target={refLabel} items={contextmenuitems}></ContextMenu> |
| 118 | {/if} |
| 119 | |
| 120 | <style lang="scss"> |
| 121 | .treenode { |
| 122 | display: flex; |
| 123 | align-items: center; |
| 124 | cursor: pointer; |
| 125 | padding-left: 1rem; |
| 126 | } |
| 127 | .tree-label { |
| 128 | display: flex; |
| 129 | min-height: 1.5rem; |
| 130 | font-size: 14px; |
| 131 | align-items: center; |
| 132 | flex: 1; |
| 133 | white-space: nowrap; |
| 134 | :global(svg){ |
| 135 | min-width: 18px; |
| 136 | min-height: 18px; |
| 137 | padding: 0 5px 0 0; |
| 138 | } |
| 139 | } |
| 140 | .no-arrow { |
| 141 | display: flex; |
| 142 | margin-right: 15px; |
| 143 | } |
| 144 | </style> |