Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for collapsible multi-level sidebar #1361

Closed
wants to merge 9 commits into from
139 changes: 121 additions & 18 deletions src/client/theme-default/components/VPSidebarLink.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue'
import { computed, inject, ref, watchEffect } from 'vue'
import { useData } from 'vitepress'
import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue'
import VPIconPlusSquare from './icons/VPIconPlusSquare.vue'
import VPIconMinusSquare from './icons/VPIconMinusSquare.vue'

withDefaults(
const props = withDefaults(
defineProps<{ item: DefaultTheme.SidebarItem; depth?: number }>(),
{ depth: 1 }
)
Expand All @@ -15,29 +17,71 @@ const maxDepth = computed<number>(
() => frontmatter.value.sidebarDepth || Infinity
)
const closeSideBar = inject('close-sidebar') as () => void

const collapsible = computed(() => 'items' in props.item ? !!props.item.collapsible : false)
const collapsed = ref(false)

/**
* When this node is both a page and a parent node,
* a split line will be added between the collapse button and the link to distinguish the functional area.
*/
const divider = computed(() => !!props.item.link && collapsible.value)


watchEffect(() => {
if ('items' in props.item)
collapsed.value = !!(collapsible.value && props.item.collapsed)
})

function toggle() {
if (collapsible.value) {
collapsed.value = !collapsed.value
}
}

function clickLink() {
closeSideBar()

// If there are no links to jump to, switch to expand when clicking on the text
if (!props.item.link) {
toggle()
}
}
</script>

<template>
<VPLink
class="link"
:class="{ active: isActive(page.relativePath, item.link) }"
:style="{ paddingLeft: 16 * (depth - 1) + 'px' }"
:href="item.link"
@click="closeSideBar"
>
<span class="link-text" :class="{ light: depth > 1 }">{{ item.text }}</span>
</VPLink>
<template
v-if="'items' in item && depth < maxDepth"
v-for="child in item.items"
:key="child.link"
>
<VPSidebarLink :item="child" :depth="depth + 1" />
</template>
<section class="VPSidebarLink" :class="{ collapsible, collapsed }">
<div class="link-label">
<VPLink
class="link"
:class="{ active: isActive(page.relativePath, item.link), divider }"
:style="{ paddingLeft: 16 * (depth - 1) + 'px' }"
:href="item.link"
@click="clickLink"
>
<span class="link-text" :class="{ light: depth > 1 }">{{ item.text }}</span>
</VPLink>

<button class="action" @click.stop="toggle" type="button">
<VPIconMinusSquare class="icon minus" />
<VPIconPlusSquare class="icon plus" />
</button>
</div>
<div class="items">
<template
v-if="'items' in item && depth < maxDepth"
v-for="child in item.items"
:key="child.link"
>
<VPSidebarLink :item="child" :depth="depth + 1" />
</template>
</div>
</section>
</template>

<style scoped>
.link {
flex: 1;
display: block;
margin: 4px 0;
color: var(--vp-c-text-2);
Expand Down Expand Up @@ -68,4 +112,63 @@ const closeSideBar = inject('close-sidebar') as () => void
font-size: 13px;
font-weight: 400;
}

.link-label {
display: flex;
align-items: center;
}

.link.divider {
border-right: 1px solid var(--vp-c-divider-light);
}

.link:not(.divider):hover + .action {
color: var(--vp-c-text-2);
}

.action {
display: none;
position: relative;
margin-right: -8px;
border-radius: 4px;
width: 32px;
height: 32px;
color: var(--vp-c-text-3);
transition: color 0.25s;
}

.action:hover {
color: var(--vp-c-text-2);
}

.VPSidebarLink.collapsible > .link-label .action {
display: block;
}

.VPSidebarLink.collapsible > .link-label .link {
cursor: pointer;
}

.icon {
position: absolute;
top: 8px;
left: 8px;
width: 16px;
height: 16px;
fill: currentColor;
}

.icon.minus { opacity: 1; }
.icon.plus { opacity: 0; }

.VPSidebarLink.collapsed > .link-label .icon.minus { opacity: 0; }
.VPSidebarLink.collapsed > .link-label .icon.plus { opacity: 1; }

.items {
overflow: hidden;
}

.VPSidebarLink.collapsed .items {
max-height: 0;
}
</style>
20 changes: 19 additions & 1 deletion types/default-theme.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,25 @@ export namespace DefaultTheme {

export type SidebarItem =
| { text: string; link: string }
| { text: string; link?: string; items: SidebarItem[] }
| {
text: string
link?: string
items: SidebarItem[]

/**
* If `true`, toggle button is shown.
*
* @default false
*/
collapsible?: boolean

/**
* If `true`, collapsible group is collapsed by default.
*
* @default false
*/
collapsed?: boolean
brc-dd marked this conversation as resolved.
Show resolved Hide resolved
}

// edit link -----------------------------------------------------------------

Expand Down