@setoelkahfi / svara / commits / edf8a9c

added file tree

mellbacon committed May 5, 2023 at 15:34 UTC edf8a9cc86809c8e95c6af367d2555b85fa0d0a2
5 files changed +277 -2
src/global.css
+4 -1
@@ -66,6 +66,9 @@ button {
66 background: none;
67 border: none;
68 }
69 +ul {
70 + padding: 0
71 +}
72
73 .editor-panes .splitpanes__pane {
74 background-color: transparent;
@@ -85,7 +88,7 @@ button {
88 transition: opacity 0.2s;
89 background-color: #2d6fd2;
90 opacity: 0;
88 - z-index: 2;
91 + z-index: 3;
92 }
93 .editor-panes .splitpanes__splitter:hover::before {
94 opacity: 1;
src/lib/FileTree.svelte
+24 -1
@@ -1,5 +1,26 @@
1 <script lang="ts">
2 - export let tree = [];
2 + import FileTreeView from "./FileTree/FileTreeView.svelte";
3 +
4 + let data = [{
5 + id: 0, label: "FakeDir", path:"FakeDir/", children: [
6 + {id: 1, label: "src", path:"FakeDir/src", children: [
7 + {id: 2, label: "dist", path:"FakeDir/src/dist/", children: [
8 + {id: 3, label: "build.asm", path:"FakeDir/src/dist/buid.asm",},
9 + {id: 4, label: "model.py", path:"FakeDir/src/dist/model.py",},
10 + {id: 5, label: "something", path:"FakeDir/src/dist/something",},
11 + ]},
12 + {id: 6, label: "file1", path:"FakeDir/src/file1",},
13 + {id: 7, label: "file2", path:"FakeDir/src/file2",},
14 + ]},
15 + {id: 8, label: "Misc", path:"FakeDir/Misc/", children: [
16 + {id: 9, label: "misc1.txt", path:"FakeDir/Misc/misc1.txt",},
17 + {id: 10, label: "misc2.png", path:"FakeDir/Misc/misc2.png",},
18 + {id: 11, label: "misc3.nucleus", path:"FakeDir/Misc/misc3.nucleus",},
19 + ]},
20 + ],
21 + }]
22 + export let tree = data;
23 +
24 </script>
25
26 {#if tree.length === 0}
@@ -7,6 +28,8 @@
28 <span>No folder/workspace open</span>
29 <button class="toolbar-button">Open Folder</button>
30 </div>
31 +{:else}
32 +<FileTreeView tree={tree} on:nodeselect={(e) => {console.log(e.detail.node)}}></FileTreeView>
33 {/if}
34
35 <style lang="scss">
src/lib/FileTree/FileTreeList.svelte new
+125
@@ -0,0 +1,125 @@
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 "../../util/icons/Directory.svelte";
11 +
12 + export let root = false;
13 + export let isroot = false;
14 +
15 + export let id = "";
16 + export let label = "";
17 + export let path = "";
18 + export let children = [];
19 +
20 + let expanded = _expansionState[label] || false;
21 + let ref = null;
22 + let refLabel = null;
23 + let selected = false;
24 +
25 + let icon = Directory;
26 +
27 + const toggleExpansion = (event) => {
28 + for (let nodes of document.getElementsByClassName("tree-label")) {
29 + nodes.classList.remove("selected")
30 + }
31 + if (event.target === refLabel) {
32 + event.target.classList.add("selected")
33 + }
34 +
35 + expanded = _expansionState[label] = !expanded;
36 + };
37 +
38 + function offset () {
39 + const depth = computeTreeLeafDepth(refLabel);
40 +
41 + if (parent) return depth + 1;
42 + if (icon) return depth + 2;
43 + return depth + 2.5;
44 + };
45 +
46 + $: arrowDown = expanded;
47 + $: parent = Array.isArray(children);
48 + $: if (refLabel) {
49 + refLabel.style.marginLeft = `-${offset()}rem`;
50 + refLabel.style.paddingLeft = `${offset()}rem`;
51 + }
52 +</script>
53 +
54 +{#if root}
55 + {#each children as child}
56 + {#if Array.isArray(child.children)}
57 + <svelte:self on:nodeselect isroot={root} {...child}></svelte:self>
58 + {:else}
59 + <FileTreeNode on:nodeselect {...child} />
60 + {/if}
61 + {/each}
62 +{:else}
63 + <li id={id} bind:this={ref} class="treenode" class:root={isroot}>
64 + <!-- svelte-ignore a11y-click-events-have-key-events -->
65 + <div bind:this={refLabel} class:selected class="tree-label" on:click={toggleExpansion}>
66 + <span class="arrow" class:arrowDown>&#x25b6</span>
67 + {#if icon}
68 + <svelte:component this={icon} />
69 + {/if}
70 + <div class="item-label">{label}</div>
71 + </div>
72 + {#if expanded}
73 + <ul role="group" class="tree-children">
74 + {#each children as child (child.id) }
75 + {#if Array.isArray(child.children)}
76 + <svelte:self on:nodeselect {...child}></svelte:self>
77 + {:else}
78 + <FileTreeNode on:nodeselect {...child} />
79 + {/if}
80 + {/each}
81 + </ul>
82 + {/if}
83 + </li>
84 +{/if}
85 +
86 +<style lang="scss">
87 + .arrow {
88 + display: flex;
89 + align-items: center;
90 + transition: transform 200ms;
91 + font-size: 12px;
92 + margin-right: 5px;
93 + }
94 + .arrowDown {
95 + transform: rotate(90deg);
96 + }
97 + .treenode {
98 + display: block;
99 + align-items: center;
100 + cursor: pointer;
101 + padding-left: 1rem;
102 + }
103 + .tree-label:hover {
104 + color: white;
105 + background-color: #353535;
106 + }
107 + .tree-label {
108 + display: flex;
109 + min-height: 1.5rem;
110 + align-items: center;
111 + white-space: nowrap;
112 + :global(svg) {
113 + width: 20px;
114 + height: 20px;
115 + color: white;
116 + padding: 0 5px 0 0;
117 + }
118 + }
119 + .root > .tree-label .item-label {
120 + font-weight: bold;
121 + }
122 + .selected {
123 + color: #34b0f8;
124 + }
125 +</style>
\ No newline at end of file
src/lib/FileTree/FileTreeNode.svelte new
+96
@@ -0,0 +1,96 @@
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 "../../util/icons/File.svelte";
20 +
21 +
22 + export let id;
23 + export let label;
24 + export let path;
25 +
26 + let selected = false;
27 +
28 + let ref = null;
29 + let refLabel = null;
30 + let icon = File;
31 +
32 + const offset = () => computeTreeLeafDepth(refLabel) + (icon ? 2 : 1);
33 +
34 + const dispatch = createEventDispatcher();
35 + function handleSelect(node, event) {
36 + for (let nodes of document.getElementsByClassName("tree-label")) {
37 + nodes.classList.remove("selected")
38 + }
39 + if (event.target === refLabel) {
40 + event.target.classList.add("selected")
41 + }
42 + dispatch("nodeselect", {node});
43 + }
44 +
45 + $: if (refLabel) {
46 + refLabel.style.marginLeft = `-${offset()}rem`;
47 + refLabel.style.paddingLeft = `${offset()}rem`;
48 + }
49 +
50 + $:treenode = {id, label, path}
51 +</script>
52 +
53 +<svelte:window on:click={(e) => {
54 + if (e.target !== refLabel) {
55 + refLabel.classList.remove("selected")
56 + }
57 +}}></svelte:window>
58 +
59 +<li id={id} bind:this={ref} class="treenode">
60 + <!-- svelte-ignore a11y-click-events-have-key-events -->
61 + <div bind:this={refLabel} class="tree-label" class:selected on:click={(e) => {handleSelect(treenode, e)}}>
62 + {#if icon}
63 + <svelte:component this={icon} />
64 + {/if}
65 + {label}
66 + </div>
67 +</li>
68 +
69 +<style lang="scss">
70 + .treenode {
71 + display: flex;
72 + align-items: center;
73 + cursor: pointer;
74 + padding-left: 1rem;
75 + }
76 + .tree-label:hover {
77 + color: white;
78 + background-color: #353535;
79 + }
80 + .tree-label {
81 + display: flex;
82 + min-height: 1.5rem;
83 + align-items: center;
84 + flex: 1;
85 + white-space: nowrap;
86 + :global(svg){
87 + width: 20px;
88 + height: 20px;
89 + color: white;
90 + padding: 0 5px 0 0;
91 + }
92 + }
93 + .selected {
94 + color: #34b0f8;
95 + }
96 +</style>
\ No newline at end of file
src/lib/FileTree/FileTreeView.svelte new
+28
@@ -0,0 +1,28 @@
1 +<script context="module">
2 + const _expansionState = {
3 + /* treeNodeId: expanded <boolean> */
4 + };
5 +</script>
6 +
7 +<script lang="ts">
8 + import FileTreeList from "./FileTreeList.svelte";
9 +
10 + export let tree;
11 +</script>
12 +
13 +<ul class="tree" role="tree">
14 + <FileTreeList on:nodeselect children={tree} root />
15 +</ul>
16 +
17 +<style>
18 + ul {
19 + margin: 0;
20 + list-style: none;
21 + user-select: none;
22 + }
23 + .tree {
24 + width: 100%;
25 + color: gray;
26 + padding: 10px 0;
27 + }
28 +</style>