feat(theme): add sidebar collapse button, close #687 (#839)

This commit is contained in:
pengzhanbo 2026-02-12 01:02:57 +08:00 committed by GitHub
parent d2b4654ae3
commit 3e68b44771
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 333 additions and 215 deletions

View File

@ -9,23 +9,22 @@ import VPNavBarTitle from '@theme/Nav/VPNavBarTitle.vue'
import VPNavBarTranslations from '@theme/Nav/VPNavBarTranslations.vue'
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useData, useSidebar } from '../../composables/index.js'
import { useLayout, useSidebarControl } from '../../composables/index.js'
const { isScreenOpen } = defineProps<{
isScreenOpen: boolean
}>()
defineEmits<(e: 'toggleScreen') => void>()
const { frontmatter } = useData()
const { y } = useWindowScroll()
const { hasSidebar } = useSidebar()
const { hasSidebar, isHome } = useLayout()
const { isSidebarCollapsed } = useSidebarControl()
const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
'home': frontmatter.value.pageLayout === 'home',
'has-sidebar': hasSidebar.value && !isSidebarCollapsed.value,
'home': isHome.value,
'top': y.value === 0,
'screen-open': isScreenOpen,
}

View File

@ -2,15 +2,16 @@
import VPImage from '@theme/VPImage.vue'
import VPLink from '@theme/VPLink.vue'
import { useRouteLocale } from 'vuepress/client'
import { useData, useSidebar } from '../../composables/index.js'
import { useData, useLayout, useSidebarControl } from '../../composables/index.js'
const { theme, site } = useData()
const { hasSidebar } = useSidebar()
const { hasSidebar } = useLayout()
const routeLocale = useRouteLocale()
const { isSidebarCollapsed } = useSidebarControl()
</script>
<template>
<div class="vp-navbar-title" :class="{ 'has-sidebar': hasSidebar }">
<div class="vp-navbar-title" :class="{ 'has-sidebar': hasSidebar && !isSidebarCollapsed }">
<VPLink class="title" :href="theme.home ?? routeLocale" no-icon>
<slot name="nav-bar-title-before" />

View File

@ -6,14 +6,15 @@ import VPFriends from '@theme/VPFriends.vue'
import VPPage from '@theme/VPPage.vue'
import { nextTick, watch } from 'vue'
import { useRoute } from 'vuepress/client'
import { useData, usePostsPageData, useSidebar } from '../composables/index.js'
import { useData, useLayout, usePostsPageData, useSidebarControl } from '../composables/index.js'
import { inBrowser } from '../utils/index.js'
const { isNotFound } = defineProps<{
isNotFound?: boolean
}>()
const { hasSidebar } = useSidebar()
const { hasSidebar, isHome } = useLayout()
const { isSidebarCollapsed } = useSidebarControl()
const { frontmatter, collection } = useData()
const { isPostsLayout } = usePostsPageData()
const route = useRoute()
@ -43,8 +44,8 @@ watch(
<template>
<div
id="VPContent" vp-content class="vp-content" :class="{
'has-sidebar': hasSidebar && !isNotFound,
'is-home': frontmatter.pageLayout === 'home',
'has-sidebar': hasSidebar && !isSidebarCollapsed && !isNotFound,
'is-home': isHome,
}"
>
<VPPosts
@ -220,6 +221,8 @@ watch(
@media (min-width: 960px) {
.vp-content {
padding-top: var(--vp-nav-height);
padding-left: 0;
transition: padding-left var(--vp-t-color);
}
.vp-content.has-sidebar {

View File

@ -14,14 +14,16 @@ import {
useData,
useEncrypt,
useHeaders,
useLayout,
usePostsPageData,
useSidebar,
useSidebarControl,
} from '../composables/index.js'
const { page, theme, frontmatter } = useData()
const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useSidebar()
const { hasSidebar, hasAside, leftAside } = useLayout()
const { isSidebarCollapsed } = useSidebarControl()
const { isPosts } = usePostsPageData()
const headers = useHeaders()
const { isPageDecrypted } = useEncrypt()
@ -76,7 +78,7 @@ watch(
<template>
<div
class="vp-doc-container" :class="{
'has-sidebar': hasSidebar,
'has-sidebar': hasSidebar && !isSidebarCollapsed,
'has-aside': enableAside,
'is-posts': isPosts,
'with-encrypt': !isPageDecrypted,
@ -263,7 +265,7 @@ watch(
padding: 48px 32px 0;
}
.vp-doc-container:not(.has-sidebar) .container {
.vp-doc-container:not(.has-sidebar) .container, {
display: flex;
justify-content: center;
max-width: 992px;

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import { useCssVar } from '@vueuse/core'
import { onMounted, ref } from 'vue'
import { useData, useSidebar } from '../composables/index.js'
import { useData, useLayout, useSidebarControl } from '../composables/index.js'
import { inBrowser } from '../utils/index.js'
const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar()
const { hasSidebar } = useLayout()
const { isSidebarCollapsed } = useSidebarControl()
const footerHeight = useCssVar('--vp-footer-height', inBrowser ? document.body : null)
const footer = ref<HTMLElement | null>(null)
@ -21,7 +22,7 @@ onMounted(() => {
v-if="theme.footer && frontmatter.footer !== false"
ref="footer"
class="vp-footer"
:class="{ 'has-sidebar': hasSidebar }"
:class="{ 'has-sidebar': hasSidebar && !isSidebarCollapsed }"
vp-footer
>
<slot name="footer-content">

View File

@ -2,7 +2,7 @@
import VPLocalNavOutlineDropdown from '@theme/VPLocalNavOutlineDropdown.vue'
import { useWindowScroll } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { useData, useHeaders, usePostsPageData, useSidebar } from '../composables/index.js'
import { useData, useHeaders, useLayout, usePostsPageData, useSidebarControl } from '../composables/index.js'
const { open, showOutline } = defineProps<{
open: boolean
@ -14,7 +14,8 @@ defineEmits<(e: 'openMenu') => void>()
const { theme } = useData()
const { isPosts, isPostsLayout } = usePostsPageData()
const { hasSidebar } = useSidebar()
const { hasSidebar, hasLocalNav } = useLayout()
const { isSidebarCollapsed } = useSidebarControl()
const { y } = useWindowScroll()
const navHeight = ref(0)
@ -22,7 +23,7 @@ const navHeight = ref(0)
const headers = useHeaders()
const empty = computed(() => {
return headers.value.length === 0 && !hasSidebar.value
return !hasLocalNav.value && !hasSidebar.value
})
onMounted(() => {
@ -40,6 +41,7 @@ const classes = computed(() => {
'reached-top': y.value >= navHeight.value,
'is-posts': isPosts.value && !isPostsLayout.value,
'with-outline': !showOutline,
'has-sidebar': hasSidebar.value && !isSidebarCollapsed.value,
}
})
@ -98,9 +100,12 @@ const showLocalNav = computed(() => {
@media (min-width: 960px) {
.vp-local-nav {
top: var(--vp-nav-height);
border-top: none;
}
.vp-local-nav.has-sidebar {
width: calc(100% - var(--vp-sidebar-width));
margin-left: var(--vp-sidebar-width);
border-top: none;
}
.vp-local-nav.is-posts {

View File

@ -4,7 +4,7 @@ import VPTransitionFadeSlideY from '@theme/VPTransitionFadeSlideY.vue'
import { useScrollLock } from '@vueuse/core'
import { nextTick, onMounted, ref, watch } from 'vue'
import { useRoutePath } from 'vuepress/client'
import { useData, useSidebar } from '../composables/index.js'
import { useData, useLayout, useSidebar, useSidebarControl } from '../composables/index.js'
import { inBrowser } from '../utils/index.js'
const { open } = defineProps<{
@ -12,7 +12,9 @@ const { open } = defineProps<{
}>()
const { theme } = useData()
const { sidebarGroups, hasSidebar, sidebarKey } = useSidebar()
const { hasSidebar } = useLayout()
const { sidebarGroups, sidebarKey } = useSidebar()
const { isSidebarCollapsed, toggleSidebarCollapse } = useSidebarControl()
const routePath = useRoutePath()
// a11y: focus Nav element when menu has opened
@ -65,7 +67,11 @@ onMounted(() => {
v-if="hasSidebar"
ref="navEl"
class="vp-sidebar"
:class="{ open, 'hide-scrollbar': !(theme.sidebarScrollbar ?? true) }"
:class="{
open,
'hide-scrollbar': !(theme.sidebarScrollbar ?? true),
'collapsed': isSidebarCollapsed,
}"
vp-sidebar
@click.stop
>
@ -92,6 +98,15 @@ onMounted(() => {
</VPTransitionFadeSlideY>
</aside>
</Transition>
<div v-if="hasSidebar" class="vp-sidebar-control" :class="{ collapsed: isSidebarCollapsed }">
<button
type="button" class="toggle-sidebar-btn"
aria-label="Toggle sidebar"
@click="toggleSidebarCollapse()"
>
<span :class="`vpi-sidebar-${isSidebarCollapsed ? 'open' : 'close'}`" />
</button>
</div>
</template>
<style scoped>
@ -152,6 +167,11 @@ onMounted(() => {
opacity: 1;
transform: translateX(0);
}
.vp-sidebar:not(.open).collapsed {
opacity: 0;
transform: translateX(-100%);
}
}
@media (min-width: 1440px) {
@ -187,4 +207,68 @@ onMounted(() => {
.nav {
outline: 0;
}
.vp-sidebar-control {
position: fixed;
bottom: 0;
left: 0;
z-index: calc(var(--vp-z-index-sidebar) + 1);
display: none;
width: calc(100vw - 64px);
max-width: 320px;
transition: transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
transform: translateX(0);
}
.vp-sidebar-control .toggle-sidebar-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin-bottom: 8px;
background-color: transparent;
border: 1px solid transparent;
border-radius: 50%;
box-shadow: 0 0 0 transparent;
transition: background-color var(--vp-t-color), box-shadow var(--vp-t-color), border-color var(--vp-t-color);
}
.vp-sidebar-control [class^="vpi-sidebar-"] {
font-size: 20px;
color: var(--vp-c-text-3);
transition: color var(--vp-t-color);
}
@media (min-width: 960px) {
.vp-sidebar-control {
display: flex;
justify-content: flex-end;
width: var(--vp-sidebar-width);
max-width: 100%;
padding-right: 7px;
}
}
@media (min-width: 1440px) {
.vp-sidebar-control {
width:
calc(
(100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) -
32px
);
}
}
.vp-sidebar-control.collapsed {
transform: translateX(calc(-100% + 54px));
}
.vp-sidebar-control.collapsed .toggle-sidebar-btn {
width: 36px;
height: 36px;
background-color: var(--vp-c-bg-safe);
border-color: var(--vp-c-divider);
box-shadow: var(--vp-shadow-2);
}
</style>

View File

@ -5,7 +5,7 @@ import VPIcon from '@theme/VPIcon.vue'
import VPLink from '@theme/VPLink.vue'
import { FadeInExpandTransition } from '@vuepress/helper/client'
import { computed } from 'vue'
import { useSidebarControl } from '../composables/index.js'
import { useSidebarItemControl } from '../composables/index.js'
import '@vuepress/helper/transition/fade-in-height-expand.css'
@ -22,7 +22,7 @@ const {
hasActiveLink,
hasChildren,
toggle,
} = useSidebarControl(computed(() => item))
} = useSidebarItemControl(computed(() => item))
const sectionTag = computed(() => (hasChildren.value ? 'section' : `div`))

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { ThemeHomeConfig } from 'theme/src/shared/index.js'
import { useElementSize, useMediaQuery, useWindowSize } from '@vueuse/core'
import { useElementSize, useWindowSize } from '@vueuse/core'
import { computed, onMounted, shallowRef } from 'vue'
import { useData } from '../composables/index.js'
import { useData, useLayout } from '../composables/index.js'
const body = shallowRef<HTMLElement | null>()
const { height: bodyHeight } = useElementSize(body)
@ -31,7 +31,8 @@ const show = computed(() => {
return true
})
const is960 = useMediaQuery('(min-width: 960px)')
const { is960 } = useLayout()
function onClick() {
document.documentElement.scrollTo({
top: document.documentElement.clientHeight - (is960.value ? 64 : 0),

View File

@ -1,23 +0,0 @@
import type { ComputedRef } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import { computed } from 'vue'
import { useSidebar } from './sidebar.js'
export function useAside(): {
isAsideEnabled: ComputedRef<boolean>
} {
const { hasSidebar } = useSidebar()
const is960 = useMediaQuery('(min-width: 960px)')
const is1280 = useMediaQuery('(min-width: 1280px)')
const isAsideEnabled = computed(() => {
if (!is1280.value && !is960.value)
return false
return hasSidebar.value ? is1280.value : is960.value
})
return {
isAsideEnabled,
}
}

View File

@ -1,5 +1,10 @@
import type { Ref } from 'vue'
import type { ThemeBaseCollection, ThemeCollectionItem, ThemeDocCollection, ThemePostCollection } from '../../shared/index.js'
import type {
ThemeBaseCollection,
ThemeCollectionItem,
ThemeDocCollection,
ThemePostCollection,
} from '../../shared/index.js'
import { collections as collectionsRaw } from '@internal/collectionsData'
import { ref, watchEffect } from 'vue'
import { useRouteLocale } from 'vuepress/client'

View File

@ -1,4 +1,3 @@
export * from './aside.js'
export * from './bulletin.js'
export * from './collections.js'
export * from './contributors.js'
@ -14,6 +13,7 @@ export * from './icons.js'
export * from './internal-link.js'
export * from './langs.js'
export * from './latest-updated.js'
export * from './layout.js'
export * from './link.js'
export * from './nav.js'
export * from './outline.js'

View File

@ -0,0 +1,96 @@
import { computed, shallowRef, watch } from 'vue'
import { useRoute } from 'vuepress/client'
import { inBrowser } from '../utils/index.js'
import { useData } from './data.js'
import { useEncrypt } from './encrypt.js'
import { useHeaders } from './outline.js'
import { useSidebarData } from './sidebar-data.js'
import { useCloseSidebarOnEscape, useSidebarControl } from './sidebar.js'
const is960 = shallowRef(false)
const is1280 = shallowRef(false)
export function useLayout() {
const { frontmatter, theme } = useData()
const { isPageDecrypted } = useEncrypt()
const sidebar = useSidebarData()
const headers = useHeaders()
const isHome = computed(() => frontmatter.value.home ?? frontmatter.value.pageLayout === 'home')
const hasSidebar = computed(() => {
return (
frontmatter.value.sidebar !== false
&& sidebar.value.length > 0
&& frontmatter.value.pageLayout !== 'home'
)
})
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)
const hasAside = computed(() => {
if (frontmatter.value.pageLayout === 'home' || frontmatter.value.home)
return false
if (frontmatter.value.pageLayout === 'friends' || frontmatter.value.friends)
return false
if (!isPageDecrypted.value)
return false
if (frontmatter.value.aside != null)
return !!frontmatter.value.aside
return theme.value.aside !== false
})
const leftAside = computed(() => {
if (hasAside.value) {
return frontmatter.value.aside == null
? theme.value.aside === 'left'
: frontmatter.value.aside === 'left'
}
return false
})
const hasLocalNav = computed(() => headers.value.length > 0)
const isAsideEnabled = computed(() => {
if (!is1280.value && !is960.value)
return false
return hasSidebar.value ? is1280.value : is960.value
})
return {
isHome,
hasAside,
hasSidebar,
leftAside,
hasLocalNav,
isSidebarEnabled,
isAsideEnabled,
is960,
is1280,
}
}
export function registerWatchers() {
if (inBrowser) {
is960.value = window.innerWidth >= 960
is1280.value = window.innerWidth >= 1280
window.addEventListener('resize', () => {
is960.value = window.innerWidth >= 960
is1280.value = window.innerWidth >= 1280
}, { passive: true })
}
const route = useRoute()
const { disableSidebar, toggleSidebarCollapse } = useSidebarControl()
watch(() => route.path, () => {
disableSidebar()
toggleSidebarCollapse(false)
})
useCloseSidebarOnEscape()
}

View File

@ -1,11 +1,11 @@
import type { InjectionKey, Ref } from 'vue'
import type { Ref } from 'vue'
import type { Router } from 'vuepress/client'
import type { ThemeOutline } from '../../shared/index.js'
import { useThrottleFn, watchDebounced } from '@vueuse/core'
import { inject, onMounted, onUnmounted, onUpdated, provide, ref } from 'vue'
import { onMounted, onUnmounted, onUpdated, ref } from 'vue'
import { onContentUpdated, useRouter } from 'vuepress/client'
import { useAside } from './aside.js'
import { useData } from './data.js'
import { useLayout } from './layout.js'
export interface Header {
/**
@ -45,28 +45,19 @@ export type MenuItem = Omit<Header, 'slug' | 'children'> & {
lowLevel?: number
}
export const headersSymbol: InjectionKey<Ref<MenuItem[]>> = Symbol(
__VUEPRESS_DEV__ ? 'headers' : '',
)
const headers = ref<MenuItem[]>([])
export function setupHeaders(): Ref<MenuItem[]> {
const { frontmatter, theme } = useData()
const headers = ref<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
provide(headersSymbol, headers)
return headers
}
export function useHeaders(): Ref<MenuItem[]> {
const headers = inject(headersSymbol)
if (!headers) {
throw new Error('useHeaders() is called without provider.')
}
return headers
}
@ -213,7 +204,7 @@ function resolveSubRangeHeader(headers: MenuItem[], low: number): MenuItem[] {
}
export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<HTMLElement | null>): void {
const { isAsideEnabled } = useAside()
const { isAsideEnabled } = useLayout()
const router = useRouter()
const routeHash = ref<string>(router.currentRoute.value.hash)

View File

@ -1,8 +1,8 @@
import type { ComputedRef, Ref } from 'vue'
import type { ThemePostsItem } from '../../shared/index.js'
import { useMediaQuery } from '@vueuse/core'
import { computed } from 'vue'
import { useData } from './data.js'
import { useLayout } from './layout.js'
import { useLocalePostList } from './posts-data.js'
import { useRouteQuery } from './route-query.js'
@ -26,7 +26,7 @@ export function usePostListControl(homePage: Ref<boolean>): UsePostListControlRe
const { collection } = useData<'page', 'post'>()
const list = useLocalePostList()
const is960 = useMediaQuery('(max-width: 960px)')
const { is960 } = useLayout()
const postCollection = computed(() => {
if (collection.value?.type === 'post')

View File

@ -1,4 +1,4 @@
import type { InjectionKey, Ref } from 'vue'
import type { Ref } from 'vue'
import type { ResolvedSidebarItem, ThemeSidebar, ThemeSidebarItem } from '../../shared/index.js'
import { sidebar as sidebarRaw } from '@internal/sidebar'
import {
@ -7,12 +7,7 @@ import {
isString,
removeLeadingSlash,
} from '@vuepress/helper/client'
import {
computed,
inject,
provide,
ref,
} from 'vue'
import { computed, ref, watch } from 'vue'
import { useRouteLocale } from 'vuepress/client'
import { normalizeLink, normalizePrefix, resolveNavLink } from '../utils/index.js'
import { useData } from './data.js'
@ -41,9 +36,7 @@ if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
}
}
const sidebarSymbol: InjectionKey<Ref<ResolvedSidebarItem[]>> = Symbol(
__VUEPRESS_DEV__ ? 'sidebar' : '',
)
const sidebar: Ref<ResolvedSidebarItem[]> = ref([])
export function setupSidebar(): void {
const { page, frontmatter } = useData()
@ -59,23 +52,22 @@ export function setupSidebar(): void {
)
})
const sidebarData = computed(() => {
return hasSidebar.value
watch([
hasSidebar,
routeLocale,
() => frontmatter.value.sidebar,
() => page.value.path,
], () => {
sidebar.value = hasSidebar.value
? getSidebar(typeof frontmatter.value.sidebar === 'string'
? frontmatter.value.sidebar
: page.value.path, routeLocale.value)
: []
})
provide(sidebarSymbol, sidebarData)
}, { immediate: true })
}
export function useSidebarData(): Ref<ResolvedSidebarItem[]> {
const sidebarData = inject(sidebarSymbol)
if (!sidebarData) {
throw new Error('useSidebarData() is called without provider.')
}
return sidebarData
return sidebar
}
/**

View File

@ -1,12 +1,11 @@
import type { ComputedRef, Ref } from 'vue'
import type { ResolvedSidebarItem } from '../../shared/index.js'
import { ensureLeadingSlash, isArray } from '@vuepress/helper/client'
import { useMediaQuery } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
import { resolveRouteFullPath, useRoute, useRouteLocale } from 'vuepress/client'
import { isActive } from '../utils/index.js'
import { useData } from './data.js'
import { useEncrypt } from './encrypt.js'
import { useLayout } from './layout.js'
import { getSidebarGroups, sidebarData, useSidebarData } from './sidebar-data.js'
/**
@ -27,39 +26,56 @@ export function hasActiveLink(path: string, items: ResolvedSidebarItem | Resolve
: false
}
export interface SidebarControl {
collapsed: Ref<boolean>
collapsible: ComputedRef<boolean>
isLink: ComputedRef<boolean>
isActiveLink: Ref<boolean>
hasActiveLink: ComputedRef<boolean>
hasChildren: ComputedRef<boolean>
toggle: () => void
}
export interface UseSidebarReturn {
isOpen: Ref<boolean>
sidebar: Ref<ResolvedSidebarItem[]>
sidebarKey: Ref<string>
sidebarGroups: Ref<ResolvedSidebarItem[]>
hasSidebar: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
open: () => void
close: () => void
toggle: () => void
}
const containsActiveLink = hasActiveLink
export function useSidebar(): UseSidebarReturn {
const { theme, frontmatter, page } = useData()
const routeLocal = useRouteLocale()
const is960 = useMediaQuery('(min-width: 960px)')
const { isPageDecrypted } = useEncrypt()
const isSidebarEnabled = ref(false)
const isSidebarCollapsed = ref(false)
const isOpen = ref(false)
export function useSidebarControl() {
const enableSidebar = (): void => {
isSidebarEnabled.value = true
}
const disableSidebar = (): void => {
isSidebarEnabled.value = false
}
const toggleSidebarEnabled = (): void => {
if (isSidebarEnabled.value) {
disableSidebar()
}
else {
enableSidebar()
}
}
function toggleSidebarCollapse(collapse?: boolean) {
isSidebarCollapsed.value = collapse ?? !isSidebarCollapsed.value
}
return {
isSidebarEnabled,
enableSidebar,
disableSidebar,
toggleSidebarEnabled,
isSidebarCollapsed,
toggleSidebarCollapse,
}
}
export function useSidebar(): {
sidebar: Ref<ResolvedSidebarItem[]>
sidebarKey: ComputedRef<string>
sidebarGroups: ComputedRef<ResolvedSidebarItem[]>
} {
const { page } = useData()
const routeLocal = useRouteLocale()
const { hasSidebar } = useLayout()
const sidebar = useSidebarData()
const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : []
})
const sidebarKey = computed(() => {
const _sidebar = sidebarData.value[routeLocal.value]
@ -73,87 +89,19 @@ export function useSidebar(): UseSidebarReturn {
}) || ''
})
const sidebar = useSidebarData()
const hasSidebar = computed(() => {
return (
frontmatter.value.sidebar !== false
&& sidebar.value.length > 0
&& frontmatter.value.pageLayout !== 'home'
)
})
const hasAside = computed(() => {
if (frontmatter.value.pageLayout === 'home' || frontmatter.value.home)
return false
if (frontmatter.value.pageLayout === 'friends' || frontmatter.value.friends)
return false
if (!isPageDecrypted.value)
return false
if (frontmatter.value.aside != null)
return !!frontmatter.value.aside
return theme.value.aside !== false
})
const leftAside = computed(() => {
if (hasAside.value) {
return frontmatter.value.aside == null
? theme.value.aside === 'left'
: frontmatter.value.aside === 'left'
}
return false
})
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)
const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : []
})
const open = (): void => {
isOpen.value = true
}
const close = (): void => {
isOpen.value = false
}
const toggle = (): void => {
if (isOpen.value) {
close()
}
else {
open()
}
}
return {
isOpen,
sidebar,
sidebarKey,
sidebarGroups,
hasSidebar,
hasAside,
leftAside,
isSidebarEnabled,
open,
close,
toggle,
}
return { sidebar, sidebarKey, sidebarGroups }
}
/**
* a11y: cache the element that opened the Sidebar (the menu button) then
* focus that button again when Menu is closed with Escape key.
*/
export function useCloseSidebarOnEscape(isOpen: Ref<boolean>, close: () => void): void {
export function useCloseSidebarOnEscape(): void {
const { disableSidebar } = useSidebarControl()
let triggerElement: HTMLButtonElement | undefined
watchEffect(() => {
triggerElement = isOpen.value
triggerElement = isSidebarEnabled.value
? (document.activeElement as HTMLButtonElement)
: undefined
})
@ -167,14 +115,14 @@ export function useCloseSidebarOnEscape(isOpen: Ref<boolean>, close: () => void)
})
function onEscape(e: KeyboardEvent): void {
if (e.key === 'Escape' && isOpen.value) {
close()
if (e.key === 'Escape' && isSidebarEnabled.value) {
disableSidebar()
triggerElement?.focus()
}
}
}
export function useSidebarControl(item: ComputedRef<ResolvedSidebarItem>): SidebarControl {
export function useSidebarItemControl(item: ComputedRef<ResolvedSidebarItem>): SidebarItemControl {
const { page } = useData()
const route = useRoute()
@ -240,3 +188,13 @@ export function useSidebarControl(item: ComputedRef<ResolvedSidebarItem>): Sideb
toggle,
}
}
export interface SidebarItemControl {
collapsed: Ref<boolean>
collapsible: ComputedRef<boolean>
isLink: ComputedRef<boolean>
isActiveLink: Ref<boolean>
hasActiveLink: ComputedRef<boolean>
hasChildren: ComputedRef<boolean>
toggle: () => void
}

View File

@ -10,23 +10,18 @@ import VPLocalNav from '@theme/VPLocalNav.vue'
import VPSidebar from '@theme/VPSidebar.vue'
import VPSignDown from '@theme/VPSignDown.vue'
import VPSkipLink from '@theme/VPSkipLink.vue'
import { watch } from 'vue'
import { useRoute } from 'vuepress/client'
import { useCloseSidebarOnEscape, useData, useEncrypt, useSidebar } from '../composables/index.js'
import { registerWatchers, useData, useEncrypt, useSidebarControl } from '../composables/index.js'
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar,
} = useSidebar()
isSidebarEnabled,
enableSidebar,
disableSidebar,
} = useSidebarControl()
const { frontmatter } = useData()
const { isGlobalDecrypted, isPageDecrypted } = useEncrypt()
const route = useRoute()
watch(() => route.path, closeSidebar)
useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
registerWatchers()
</script>
<template>
@ -42,7 +37,7 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
<VPSkipLink />
<VPBackdrop :show="isSidebarOpen" @click="closeSidebar" />
<VPBackdrop :show="isSidebarEnabled" @click="disableSidebar" />
<VPNav>
<template #nav-bar-title-before>
@ -78,12 +73,12 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
</VPNav>
<VPLocalNav
:open="isSidebarOpen"
:open="isSidebarEnabled"
:show-outline="isPageDecrypted"
@open-menu="openSidebar"
@open-menu="enableSidebar"
/>
<VPSidebar :open="isSidebarOpen">
<VPSidebar :open="isSidebarEnabled">
<template #sidebar-nav-before>
<slot name="sidebar-nav-before" />
</template>

View File

@ -167,3 +167,11 @@
.vpi-close {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z'/%3E%3Cpath fill='%23000' d='m12 14.122l5.303 5.303a1.5 1.5 0 0 0 2.122-2.122L14.12 12l5.304-5.303a1.5 1.5 0 1 0-2.122-2.121L12 9.879L6.697 4.576a1.5 1.5 0 1 0-2.122 2.12L9.88 12l-5.304 5.304a1.5 1.5 0 1 0 2.122 2.12z'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-sidebar-open {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='18' height='18' x='3' y='3' rx='2'/%3E%3Cpath d='M9 3v18m5-12l3 3l-3 3'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-sidebar-close {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='18' height='18' x='3' y='3' rx='2'/%3E%3Cpath d='M9 3v18m7-6l-3-3l3-3'/%3E%3C/g%3E%3C/svg%3E");
}