Skip to content

Commit 27f7e49

Browse files
feat(ui/tree): add universal tree component and migrate folders (#694) (#694)
1 parent c27a293 commit 27f7e49

File tree

10 files changed

+1110
-741
lines changed

10 files changed

+1110
-741
lines changed

src/renderer/components/sidebar/folders/Tree.vue

Lines changed: 255 additions & 136 deletions
Large diffs are not rendered by default.

src/renderer/components/sidebar/folders/TreeNode.vue

Lines changed: 0 additions & 537 deletions
This file was deleted.

src/renderer/components/sidebar/folders/composables/index.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/renderer/components/sidebar/folders/keys.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<script setup lang="ts">
2+
import type { DropPosition, TreeNode } from './types'
3+
import { treeInjectionKey } from './keys'
4+
import TreeNodeComponent from './TreeNode.vue'
5+
6+
interface Props {
7+
modelValue: TreeNode[]
8+
selectedIds?: (string | number)[]
9+
editableId?: string | number | null
10+
focusedId?: string | number | undefined
11+
highlightedIds?: Set<string | number>
12+
indent?: number
13+
}
14+
15+
interface Emits {
16+
(e: 'update:modelValue', value: TreeNode[]): void
17+
(e: 'update:selectedIds', value: (string | number)[]): void
18+
(e: 'update:editableId', value: string | number | null): void
19+
(e: 'update:focusedId', value: string | number | undefined): void
20+
(e: 'update:highlightedIds', value: Set<string | number>): void
21+
(e: 'clickNode', value: { node: TreeNode, event?: MouseEvent }): void
22+
(e: 'dblclickNode', value: TreeNode): void
23+
(e: 'toggleNode', value: TreeNode): void
24+
(
25+
e: 'dragNode',
26+
value: { nodes: TreeNode[], target: TreeNode, position: DropPosition },
27+
): void
28+
(
29+
e: 'externalDrop',
30+
value: { data: DataTransfer, target: TreeNode, position: DropPosition },
31+
): void
32+
(e: 'updateLabel', value: { node: TreeNode, value: string }): void
33+
(e: 'cancelEdit', value: TreeNode): void
34+
(
35+
e: 'contextMenu',
36+
value: { node: TreeNode, selectedNodes: TreeNode[] },
37+
): void
38+
}
39+
40+
const props = withDefaults(defineProps<Props>(), {
41+
selectedIds: () => [],
42+
editableId: null,
43+
focusedId: undefined,
44+
highlightedIds: () => new Set(),
45+
indent: 10,
46+
})
47+
48+
const emit = defineEmits<Emits>()
49+
50+
const hoveredNodeId = ref('')
51+
const isHoveredByIdDisabled = ref(false)
52+
53+
const internalEditableId = computed({
54+
get: () => props.editableId,
55+
set: val => emit('update:editableId', val),
56+
})
57+
58+
const internalSelectedIds = computed({
59+
get: () => props.selectedIds,
60+
set: val => emit('update:selectedIds', val),
61+
})
62+
63+
const internalFocusedId = computed({
64+
get: () => props.focusedId,
65+
set: val => emit('update:focusedId', val),
66+
})
67+
68+
const internalHighlightedIds = computed({
69+
get: () => props.highlightedIds,
70+
set: val => emit('update:highlightedIds', val),
71+
})
72+
73+
function findNodeById(id: string | number): TreeNode | undefined {
74+
const walk = (nodes: TreeNode[]): TreeNode | undefined => {
75+
for (const node of nodes) {
76+
if (node.id === id)
77+
return node
78+
if (node.children?.length) {
79+
const found = walk(node.children)
80+
if (found)
81+
return found
82+
}
83+
}
84+
}
85+
return walk(props.modelValue)
86+
}
87+
88+
function clickNode(id: string | number, event?: MouseEvent) {
89+
const node = findNodeById(id)
90+
if (!node)
91+
return
92+
93+
emit('clickNode', { node, event })
94+
}
95+
96+
function dblclickNode(node: TreeNode) {
97+
emit('dblclickNode', node)
98+
}
99+
100+
function dragNodeHandler(
101+
nodes: TreeNode[],
102+
target: TreeNode,
103+
position: DropPosition,
104+
) {
105+
emit('dragNode', { nodes, target, position })
106+
}
107+
108+
function externalDropHandler(
109+
data: DataTransfer,
110+
target: TreeNode,
111+
position: DropPosition,
112+
) {
113+
emit('externalDrop', { data, target, position })
114+
}
115+
116+
function toggleNode(node: TreeNode) {
117+
emit('toggleNode', node)
118+
}
119+
120+
function contextMenu(node: TreeNode) {
121+
const selectedNodes = internalSelectedIds.value
122+
.map(id => findNodeById(id))
123+
.filter((n): n is TreeNode => Boolean(n))
124+
125+
emit('contextMenu', { node, selectedNodes })
126+
}
127+
128+
function updateLabelHandler(node: TreeNode, value: string) {
129+
emit('updateLabel', { node, value })
130+
}
131+
132+
function cancelEditHandler(node: TreeNode) {
133+
emit('cancelEdit', node)
134+
}
135+
136+
provide(treeInjectionKey, {
137+
clickNode,
138+
dblclickNode,
139+
dragNode: dragNodeHandler,
140+
externalDrop: externalDropHandler,
141+
toggleNode,
142+
contextMenu,
143+
updateLabel: updateLabelHandler,
144+
cancelEdit: cancelEditHandler,
145+
isHoveredByIdDisabled,
146+
editableId: internalEditableId,
147+
selectedIds: internalSelectedIds,
148+
focusedId: internalFocusedId,
149+
highlightedIds: internalHighlightedIds,
150+
})
151+
</script>
152+
153+
<template>
154+
<div
155+
v-if="modelValue.length"
156+
class="h-full min-h-0"
157+
>
158+
<div class="scrollbar h-full min-h-0 overflow-x-hidden overflow-y-auto">
159+
<div data-tree>
160+
<TreeNodeComponent
161+
v-for="(node, index) in modelValue"
162+
:key="node.id"
163+
:node="node"
164+
:nodes="modelValue"
165+
:index="index"
166+
:indent="indent"
167+
:hovered-node-id="hoveredNodeId"
168+
>
169+
<template
170+
v-if="$slots.icon"
171+
#icon="iconProps"
172+
>
173+
<slot
174+
name="icon"
175+
v-bind="iconProps"
176+
/>
177+
</template>
178+
</TreeNodeComponent>
179+
</div>
180+
</div>
181+
</div>
182+
</template>

0 commit comments

Comments
 (0)