95 lines
1.9 KiB
Vue
95 lines
1.9 KiB
Vue
<template>
|
|
<ul class="tree-list">
|
|
<li v-for="node in flatNodes" :key="node.id" :style="{ paddingLeft: `${node.depth * 12}px` }">
|
|
<button
|
|
v-if="node.hasChildren"
|
|
class="card"
|
|
type="button"
|
|
style="margin-right: 6px;"
|
|
@click="toggle(node.id)"
|
|
>
|
|
{{ isCollapsed(node.id) ? '+' : '-' }}
|
|
</button>
|
|
<span>{{ node.nom }}</span>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
type TreeItem = {
|
|
id: string
|
|
nom: string
|
|
parent_id?: string | null
|
|
}
|
|
|
|
type FlatNode = TreeItem & {
|
|
depth: number
|
|
hasChildren: boolean
|
|
}
|
|
|
|
const props = defineProps<{ items: TreeItem[] }>()
|
|
|
|
const collapsed = ref<Set<string>>(new Set())
|
|
|
|
const childrenMap = computed(() => {
|
|
const map = new Map<string | null, TreeItem[]>()
|
|
props.items.forEach((item) => {
|
|
const key = item.parent_id || null
|
|
const list = map.get(key) || []
|
|
list.push(item)
|
|
map.set(key, list)
|
|
})
|
|
return map
|
|
})
|
|
|
|
const hasChildren = (id: string) => {
|
|
const list = childrenMap.value.get(id)
|
|
return !!(list && list.length)
|
|
}
|
|
|
|
const buildFlat = (parentId: string | null, depth: number, acc: FlatNode[]) => {
|
|
const children = childrenMap.value.get(parentId) || []
|
|
children
|
|
.slice()
|
|
.sort((a, b) => a.nom.localeCompare(b.nom))
|
|
.forEach((child) => {
|
|
acc.push({
|
|
...child,
|
|
depth,
|
|
hasChildren: hasChildren(child.id)
|
|
})
|
|
if (!collapsed.value.has(child.id)) {
|
|
buildFlat(child.id, depth + 1, acc)
|
|
}
|
|
})
|
|
}
|
|
|
|
const flatNodes = computed(() => {
|
|
const acc: FlatNode[] = []
|
|
buildFlat(null, 0, acc)
|
|
return acc
|
|
})
|
|
|
|
const toggle = (id: string) => {
|
|
const next = new Set(collapsed.value)
|
|
if (next.has(id)) {
|
|
next.delete(id)
|
|
} else {
|
|
next.add(id)
|
|
}
|
|
collapsed.value = next
|
|
}
|
|
|
|
const isCollapsed = (id: string) => collapsed.value.has(id)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tree-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
</style>
|