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>▶</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>