Files
dnd-vault-template/template/app/components/TreeNode.vue
2026-07-18 19:03:30 +02:00

158 lines
3.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { useTreeState } from '~/composables/useTreeState'
defineProps({ nodes: Array, depth: { type: Number, default: 0 } })
const router = useRouter()
const { collapsed } = useTreeState()
function getTitle(node) {
if (node.title === 'index') return 'Index'
return node.title || node._path?.split('/').pop() || 'Home'
}
function navigate(path) {
if (!path) return
router.push(path)
}
function toggle(key) {
collapsed.value[key] = !collapsed.value[key]
}
/**
* If the folder contains a child whose path/title matches the folder itself
* (e.g. places/ has a child at places/places.md), return that child.
* Otherwise return null.
*/
function getSelfTitledChild(node) {
if (!node.children?.length) return null
const folderSlug = node.path?.split('/').pop() ?? node.title
return node.children.find((child) => {
const childSlug = child.path?.split('/').pop() ?? child.title
return childSlug === folderSlug
}) ?? null
}
/**
* Children to actually render — excludes the self-titled child so it
* doesn't appear twice (once as the clickable folder label, once in the list).
*/
function visibleChildren(node) {
const selfTitled = getSelfTitledChild(node)
if (!selfTitled) return node.children
return node.children.filter((c) => c !== selfTitled)
}
</script>
<template>
<ul :style="{ paddingLeft: depth ? '1rem' : '0' }">
<li v-for="node in nodes" :key="node.path ?? node.title">
<!-- Leaf node (no children) -->
<template v-if="!node.children?.length">
<a v-if="node.path" :href="node.path" @click.prevent="navigate(node.path)">
{{ getTitle(node) }}
</a>
<span v-else>{{ getTitle(node) }}</span>
</template>
<!-- Folder node -->
<template v-else>
<div class="folder-row">
<!-- Chevron toggle (always present) -->
<button
class="folder-toggle"
@click="toggle(node.path ?? node.title)"
:aria-expanded="!collapsed[node.path ?? node.title]"
>
<span class="chevron" :class="{ open: !collapsed[node.path ?? node.title] }"></span>
</button>
<!-- Folder label: clickable link if there's a self-titled child, plain text otherwise -->
<a
v-if="getSelfTitledChild(node)"
class="folder-label folder-label--link"
:href="getSelfTitledChild(node).path"
@click.prevent="navigate(getSelfTitledChild(node).path)"
>
{{ getTitle(node) }}
</a>
<span v-else class="folder-label">{{ getTitle(node) }}</span>
</div>
<TreeNode
v-if="!collapsed[node.path ?? node.title]"
:nodes="visibleChildren(node)"
:depth="depth + 1"
/>
</template>
</li>
</ul>
</template>
<style lang="scss" scoped>
ul {
list-style: none;
padding-left: 0;
}
li {
margin-bottom: 0;
}
a {
color: var(--text-link);
}
.folder-row {
display: flex;
align-items: center;
gap: 0.2rem;
}
.folder-toggle {
display: flex;
align-items: center;
flex-shrink: 0;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: inherit;
font: inherit;
line-height: 1;
&:hover {
color: var(--text-link);
}
}
.folder-label {
flex: 1;
cursor: default;
&--link {
color: var(--text-link);
cursor: pointer;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.chevron {
display: inline-block;
font-style: normal;
transition: transform 0.15s ease;
transform: rotate(0deg);
line-height: 1;
&.open {
transform: rotate(90deg);
}
}
</style>