Files
2026-07-18 22:36:24 +02:00

201 lines
4.5 KiB
Vue
Raw Permalink 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 route = useRoute()
const { collapsed } = useTreeState()
function isActive(path) {
return !!path && route.path === path
}
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>
<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"
class="row"
:class="{ active: isActive(node.path) }"
:style="{ paddingLeft: `calc(${depth} * 1rem + 2rem)` }"
@click.prevent="navigate(node.path)"
>
{{ getTitle(node) }}
</a>
<span
v-else
class="row"
:style="{ paddingLeft: `calc(${depth} * 1rem + 2rem)` }"
>{{ getTitle(node) }}</span>
</template>
<!-- Folder node -->
<template v-else>
<div
class="row folder-row"
:class="{ active: isActive(getSelfTitledChild(node)?.path) }"
:style="{ paddingLeft: `calc(${depth} * 1rem + 2rem)` }"
>
<!-- 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: 0;
margin: 0;
}
li {
margin-bottom: 0;
}
// Shared row styles — applied to both <a> leaf nodes and .folder-row divs
a.row {
border: none;
}
.row {
display: flex;
align-items: center;
width: 100%;
padding-right: 0.4rem;
padding-top: 0.15rem;
padding-bottom: 0.15rem;
box-sizing: border-box;
border-radius: 4px;
text-decoration: none;
color: var(--text-link);
a {
border: none;
}
&.active {
background-color: var(--bg-body);
color: var(--text-heading, currentColor);
}
&:not(.active):hover {
background-color: var(--bg-hover, rgba(0, 0, 0, 0.04));
}
}
// Folder-specific row layout
.folder-row {
gap: 0.2rem;
cursor: default;
}
.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;
&--link {
color: var(--text-link);
cursor: pointer;
text-decoration: none;
}
}
.chevron {
display: inline-block;
font-style: normal;
transition: transform 0.15s ease;
transform: rotate(0deg);
line-height: 1;
&.open {
transform: rotate(90deg);
}
}
</style>