@setoelkahfi / svara / commits / 43a914f

customized treeview code from carbon components

mellbacon committed Aug 9, 2022 at 18:07 UTC 43a914f1e8fd6fc806c99e920f70cee4f419894c
3 files changed +439
src/components/FileTree/TreeView.svelte new
+163
@@ -0,0 +1,163 @@
1 +<script>
2 + /**
3 + * @typedef {string | number} TreeNodeId
4 + * @typedef {{ id: TreeNodeId; name: string; icon?: typeof import("svelte").SvelteComponent; disabled?: boolean; expanded?: boolean; path?: string; }} TreeNode
5 + * @event {TreeNode & { expanded: boolean; leaf: boolean; }} select
6 + * @event {TreeNode & { expanded: boolean; leaf: boolean; }} toggle
7 + * @event {TreeNode & { expanded: boolean; leaf: boolean; }} focus
8 + */
9 +
10 + /**
11 + * Provide an array of children nodes to render
12 + * @type {Array<TreeNode & { children?: TreeNode[] }>}
13 + */
14 + export let children = [];
15 +
16 + /**
17 + * Set the current active node id
18 + * Only one node can be active
19 + * @type {TreeNodeId}
20 + */
21 + export let activeId = "";
22 +
23 + /**
24 + * Set the node ids to be selected
25 + * @type {ReadonlyArray<TreeNodeId>}
26 + */
27 + export let selectedIds = [];
28 +
29 + /**
30 + * Set the node ids to be expanded
31 + * @type {ReadonlyArray<TreeNodeId>}
32 + */
33 + export let expandedIds = [];
34 +
35 + /**
36 + * Specify the TreeView size
37 + * @type {"default" | "compact"}
38 + */
39 + export let size = "default";
40 +
41 + /** Specify the label text */
42 + export let labelText = "";
43 +
44 + /** Set to `true` to visually hide the label text */
45 + export let hideLabel = false;
46 +
47 + /**
48 + * Programmatically expand all nodes
49 + * @type {() => void}
50 + */
51 + export function expandAll() {
52 + expandedIds = [...nodeIds];
53 + }
54 +
55 + /**
56 + * Programmatically collapse all nodes
57 + * @type {() => void}
58 + */
59 + export function collapseAll() {
60 + expandedIds = [];
61 + }
62 +
63 + import { createEventDispatcher, setContext, onMount } from "svelte";
64 + import { writable } from "svelte/store";
65 + import TreeViewNodeList from "./TreeViewNodeList.svelte";
66 +
67 + const dispatch = createEventDispatcher();
68 + const labelId = `label-${Math.random().toString(36)}`;
69 + const activeNodeId = writable(activeId);
70 + const selectedNodeIds = writable(selectedIds);
71 + const expandedNodeIds = writable(expandedIds);
72 +
73 + let ref = null;
74 + let treeWalker = null;
75 +
76 + setContext("TreeView", {
77 + activeNodeId,
78 + selectedNodeIds,
79 + expandedNodeIds,
80 + clickNode: (node) => {
81 + activeId = node.id;
82 + selectedIds = [node.id];
83 + dispatch("select", node);
84 + },
85 + selectNode: (node) => {
86 + selectedIds = [node.id];
87 + },
88 + expandNode: (node, expanded) => {
89 + if (expanded) {
90 + expandedIds = [...expandedIds, node.id];
91 + } else {
92 + expandedIds = expandedIds.filter((_id) => _id !== node.id);
93 + }
94 + },
95 + focusNode: (node) => dispatch("focus", node),
96 + toggleNode: (node) => dispatch("toggle", node),
97 + });
98 +
99 + onMount(() => {
100 + const firstFocusableNode = ref.querySelector(
101 + "li.bx--tree-node:not(.bx--tree-node--disabled)"
102 + );
103 +
104 + if (firstFocusableNode != null) {
105 + firstFocusableNode.tabIndex = "0";
106 + }
107 + });
108 +
109 + /**
110 + * @param {Array<TreeNode & { children?: TreeNode[] }>} children
111 + */
112 + function traverse(children) {
113 + let nodes = [];
114 +
115 + children.forEach((node) => {
116 + nodes.push(node);
117 +
118 + if (Array.isArray(node.children)) {
119 + nodes = [...nodes, ...traverse(node.children)];
120 + }
121 + });
122 +
123 + return nodes;
124 + }
125 +
126 + $: nodes = traverse(children);
127 + $: nodeIds = nodes.map((node) => node.id);
128 + $: activeNodeId.set(activeId);
129 + $: selectedNodeIds.set(selectedIds);
130 + $: expandedNodeIds.set(expandedIds);
131 + $: if (ref) {
132 + treeWalker = document.createTreeWalker(ref, NodeFilter.SHOW_ELEMENT, {
133 + acceptNode: (node) => {
134 + if (node.classList.contains("bx--tree-node--disabled"))
135 + return NodeFilter.FILTER_REJECT;
136 + if (node.matches("li.bx--tree-node")) return NodeFilter.FILTER_ACCEPT;
137 + return NodeFilter.FILTER_SKIP;
138 + },
139 + });
140 + }
141 +</script>
142 +
143 +{#if !hideLabel}
144 + <!-- svelte-ignore a11y-label-has-associated-control -->
145 + <label id={labelId} class:bx--label={true}>
146 + <slot name="labelText">{labelText}</slot>
147 + </label>
148 +{/if}
149 +
150 +<ul
151 + {...$$restProps}
152 + role="tree"
153 + bind:this={ref}
154 + class:bx--tree={true}
155 + class:bx--tree--default={size === "default"}
156 + class:bx--tree--compact={size === "compact"}
157 + aria-label={hideLabel ? labelText : undefined}
158 + aria-labelledby={!hideLabel ? labelId : undefined}
159 + aria-multiselectable={selectedIds.length > 1 || undefined}
160 + on:keydown
161 +>
162 + <TreeViewNodeList root {children} />
163 +</ul>
src/components/FileTree/TreeViewNode.svelte new
+125
@@ -0,0 +1,125 @@
1 +<script 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 +
17 + function findParentTreeNode(node) {
18 + if (node.classList.contains("bx--tree-parent-node")) return node;
19 + if (node.classList.contains("bx--tree")) return null;
20 + return findParentTreeNode(node.parentNode);
21 + }
22 +</script>
23 +
24 +<script>
25 + /**
26 + * @typedef {string | number} TreeNodeId
27 + */
28 +
29 + export let leaf = false;
30 +
31 + /** @type {TreeNodeId} */
32 + export let id = "";
33 + export let name = "";
34 + export let disabled = false;
35 + export let path = "";
36 +
37 + /**
38 + * Specify the icon to render
39 + * @type {typeof import("svelte").SvelteComponent}
40 + */
41 + export let icon = undefined;
42 +
43 + import { afterUpdate, getContext } from "svelte";
44 + import RenameModel from "../Modal/RenameModel.svelte";
45 + import LeafNodeMenu from "./LeafNodeMenu.svelte";
46 +
47 + let ref = null;
48 + let refLabel = null;
49 + let prevActiveId = undefined;
50 +
51 + const { activeNodeId, selectedNodeIds, clickNode, selectNode, focusNode } =
52 + getContext("TreeView");
53 + const offset = () =>
54 + computeTreeLeafDepth(refLabel) + (leaf && icon ? 2 : 2.5);
55 +
56 + afterUpdate(() => {
57 + if (id === $activeNodeId && prevActiveId !== $activeNodeId) {
58 + if (!$selectedNodeIds.includes(id)) selectNode(node);
59 + }
60 +
61 + prevActiveId = $activeNodeId;
62 + });
63 +
64 + $: node = { id, name: name, expanded: false, leaf, path };
65 + $: if (refLabel) {
66 + refLabel.style.marginLeft = `-${offset()}rem`;
67 + refLabel.style.paddingLeft = `${offset()}rem`;
68 + }
69 +
70 + let open = false;
71 +</script>
72 +
73 +<li
74 + bind:this={ref}
75 + role="treeitem"
76 + {id}
77 + tabindex={disabled ? undefined : -1}
78 + aria-current={id === $activeNodeId || undefined}
79 + aria-selected={disabled ? undefined : $selectedNodeIds.includes(id)}
80 + aria-disabled={disabled}
81 + class:bx--tree-node={true}
82 + class:bx--tree-leaf-node={true}
83 + class:bx--tree-node--active={id === $activeNodeId}
84 + class:bx--tree-node--selected={$selectedNodeIds.includes(id)}
85 + class:bx--tree-node--disabled={disabled}
86 + class:bx--tree-node--with-icon={icon}
87 + on:click|stopPropagation={() => {
88 + if (disabled) return;
89 + clickNode(node);
90 + }}
91 + on:keydown={(e) => {
92 + if (
93 + e.key === "ArrowLeft" ||
94 + e.key === "ArrowRight" ||
95 + e.key === "F2"
96 + ) {
97 + e.stopPropagation();
98 + }
99 + if (e.key === "ArrowLeft") {
100 + const parentNode = findParentTreeNode(ref.parentNode);
101 + if (parentNode) parentNode.focus();
102 + }
103 + if (e.key === "F2") {
104 + e.preventDefault();
105 + open = true;
106 + }
107 + }}
108 + on:focus={() => {
109 + focusNode(node);
110 + }}
111 +>
112 + <div bind:this={refLabel} class:bx--tree-node__label={true}>
113 + <svelte:component this={icon} class="bx--tree-node__icon" />
114 + {name}
115 + </div>
116 +</li>
117 +<LeafNodeMenu target={ref} filename={name} path={path}></LeafNodeMenu>
118 +<RenameModel bind:open bind:filename={name} bind:path={path} />
119 +
120 +<style>
121 + :global(.bx--tree-node__label) {
122 + white-space: nowrap;
123 + cursor: pointer;
124 + }
125 +</style>
\ No newline at end of file
src/components/FileTree/TreeViewNodeList.svelte new
+151
@@ -0,0 +1,151 @@
1 +<script>
2 + /**
3 + * @typedef {string | number} TreeNodeId
4 + * @typedef {{ id: TreeNodeId; name: string; disabled?: boolean; expanded?: boolean; path?: string; }} TreeNode
5 + */
6 +
7 + /** @type {Array<TreeNode & { children?: TreeNode[] }>} */
8 + export let children = [];
9 + export let expanded = false;
10 + export let root = false;
11 +
12 + /** @type {string | number} */
13 + export let id = "";
14 + export let name = "";
15 + export let disabled = false;
16 + export let path = "";
17 +
18 + /**
19 + * Specify the icon to render
20 + * @type {typeof import("svelte").SvelteComponent}
21 + */
22 + export let icon = undefined;
23 +
24 + import { afterUpdate, getContext } from "svelte";
25 + import CaretDown from "carbon-icons-svelte/lib/CaretDown.svelte";
26 + import TreeViewNode, { computeTreeLeafDepth } from "./TreeViewNode.svelte";
27 + import ParentNodeMenu from "./ParentNodeMenu.svelte";
28 +
29 + let ref = null;
30 + let refLabel = null;
31 + let prevActiveId = undefined;
32 +
33 + const {
34 + activeNodeId,
35 + selectedNodeIds,
36 + expandedNodeIds,
37 + clickNode,
38 + selectNode,
39 + expandNode,
40 + focusNode,
41 + toggleNode,
42 + } = getContext("TreeView");
43 +
44 + const offset = () => {
45 + const depth = computeTreeLeafDepth(refLabel);
46 +
47 + if (parent) return depth + 1;
48 + if (icon) return depth + 2;
49 + return depth + 2.5;
50 + };
51 +
52 + afterUpdate(() => {
53 + if (id === $activeNodeId && prevActiveId !== $activeNodeId) {
54 + if (!$selectedNodeIds.includes(id)) selectNode(node);
55 + }
56 +
57 + prevActiveId = $activeNodeId;
58 + });
59 +
60 + $: parent = Array.isArray(children);
61 + $: node = { id, name, expanded, leaf: !parent, path };
62 + $: if (refLabel) {
63 + refLabel.style.marginLeft = `-${offset()}rem`;
64 + refLabel.style.paddingLeft = `${offset()}rem`;
65 + }
66 + $: expanded = $expandedNodeIds.includes(id);
67 +</script>
68 +
69 +{#if root}
70 + {#each children as child (child.id)}
71 + {#if Array.isArray(child.children)}
72 + <svelte:self {...child} />
73 + {:else}
74 + <TreeViewNode leaf {...child} />
75 + {/if}
76 + {/each}
77 +{:else}
78 + <li
79 + bind:this={ref}
80 + role="treeitem"
81 + {id}
82 + tabindex={disabled ? undefined : -1}
83 + aria-current={id === $activeNodeId || undefined}
84 + aria-selected={disabled ? undefined : $selectedNodeIds.includes(id)}
85 + aria-disabled={disabled}
86 + class:bx--tree-node={true}
87 + class:bx--tree-parent-node={true}
88 + class:bx--tree-node--active={id === $activeNodeId}
89 + class:bx--tree-node--selected={$selectedNodeIds.includes(id)}
90 + class:bx--tree-node--disabled={disabled}
91 + class:bx--tree-node--with-icon={icon}
92 + aria-expanded={expanded}
93 + on:keydown={(e) => {
94 + if (
95 + e.key === "ArrowLeft" ||
96 + e.key === "ArrowRight"
97 + ) {
98 + e.stopPropagation();
99 + }
100 + if (parent && e.key === "ArrowLeft") {
101 + expanded = false;
102 + expandNode(node, false);
103 + toggleNode(node);
104 + }
105 + if (parent && e.key === "ArrowRight") {
106 + if (expanded) {
107 + ref.lastChild.firstElementChild?.focus();
108 + } else {
109 + expanded = true;
110 + expandNode(node, true);
111 + toggleNode(node);
112 + }
113 + }
114 + }}
115 + on:focus={() => {
116 + focusNode(node);
117 + }}
118 + >
119 + <div class:bx--tree-node__label={true} bind:this={refLabel}
120 + on:click={() => {
121 + if (disabled) return;
122 + expanded = !expanded;
123 + expandNode(node, expanded);
124 + toggleNode(node);
125 + }}>
126 + <span class:bx--tree-parent-node__toggle={true} {disabled}>
127 + <CaretDown
128 + class="bx--tree-parent-node__toggle-icon {expanded &&
129 + 'bx--tree-parent-node__toggle-icon--expanded'}"
130 + />
131 + </span>
132 + <span class:bx--tree-node__label__details={true}>
133 + <svelte:component this={icon} class="bx--tree-node__icon" />
134 + {name}
135 + </span>
136 + </div>
137 + {#if expanded}
138 + <ul role="group" class:bx--tree-node__children={true}>
139 + {#each children as child (child.id)}
140 + {#if Array.isArray(child.children)}
141 + <svelte:self {...child} />
142 + {:else}
143 + <TreeViewNode leaf {...child} />
144 + {/if}
145 + {/each}
146 + </ul>
147 + {/if}
148 + </li>
149 +{/if}
150 +
151 +<ParentNodeMenu target={[refLabel]} filename={name} path={path}></ParentNodeMenu>
\ No newline at end of file