301 lines
6.9 KiB
TypeScript
301 lines
6.9 KiB
TypeScript
import type { DefaultTheme } from 'vitepress/theme'
|
|
import type { ComputedRef, Ref } from 'vue'
|
|
|
|
import { useEventListener } from '@vueuse/core'
|
|
import { useData, withBase } from 'vitepress'
|
|
import {
|
|
computed,
|
|
|
|
onMounted,
|
|
|
|
ref,
|
|
watch,
|
|
watchEffect,
|
|
watchPostEffect,
|
|
} from 'vue'
|
|
|
|
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 SidebarItem extends DefaultTheme.SidebarItem {}
|
|
|
|
/**
|
|
* 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,
|
|
) {
|
|
let triggerElement: HTMLButtonElement | undefined
|
|
|
|
watchEffect(() => {
|
|
triggerElement = isOpen.value
|
|
? (document.activeElement as HTMLButtonElement)
|
|
: undefined
|
|
})
|
|
|
|
useEventListener('keyup', onEscape)
|
|
|
|
function onEscape(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' && isOpen.value) {
|
|
close()
|
|
triggerElement?.focus()
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useSidebarControl(
|
|
item: ComputedRef<DefaultTheme.SidebarItem>,
|
|
): SidebarControl {
|
|
const { page, hash } = useData()
|
|
|
|
const collapsed = ref(false)
|
|
|
|
const collapsible = computed(() => {
|
|
return item.value.collapsed != null
|
|
})
|
|
|
|
const isLink = computed(() => {
|
|
return !!item.value.link
|
|
})
|
|
|
|
const isActiveLink = ref(false)
|
|
const updateIsActiveLink = () => {
|
|
isActiveLink.value = isActive(withBase(`/${page.value.relativePath}`), item.value.link)
|
|
}
|
|
|
|
watch([page, item, hash], updateIsActiveLink)
|
|
onMounted(updateIsActiveLink)
|
|
|
|
const hasActiveLink = computed(() => {
|
|
if (isActiveLink.value)
|
|
return true
|
|
|
|
return item.value.items
|
|
? containsActiveLink(withBase(`/${page.value.relativePath}`), item.value.items)
|
|
: false
|
|
})
|
|
|
|
const hasChildren = computed(() => {
|
|
return !!(item.value.items && item.value.items.length)
|
|
})
|
|
|
|
watchEffect(() => {
|
|
collapsed.value = !!(collapsible.value && item.value.collapsed)
|
|
})
|
|
|
|
watchPostEffect(() => {
|
|
if (isActiveLink.value || hasActiveLink.value)
|
|
collapsed.value = false
|
|
})
|
|
|
|
function toggle() {
|
|
if (collapsible.value) {
|
|
collapsed.value = !collapsed.value
|
|
}
|
|
}
|
|
|
|
return {
|
|
collapsed,
|
|
collapsible,
|
|
isLink,
|
|
isActiveLink,
|
|
hasActiveLink,
|
|
hasChildren,
|
|
toggle,
|
|
}
|
|
}
|
|
|
|
// From https://github.com/vuejs/vitepress/blob/97f9469b6d4eb7ba9de9a1111986581d1f704ec3/src/shared/shared.ts
|
|
const inBrowser = typeof document !== 'undefined'
|
|
|
|
const HASH_RE = /#.*$/
|
|
const HASH_OR_QUERY_RE = /[?#].*$/
|
|
const INDEX_OR_EXT_RE = /(?:(^|\/)index)?\.(?:md|html)$/
|
|
|
|
export function isActive(
|
|
currentPath: string,
|
|
matchPath?: string,
|
|
asRegex: boolean = false,
|
|
): boolean {
|
|
if (matchPath === undefined) {
|
|
return false
|
|
}
|
|
|
|
if (currentPath.startsWith('/')) {
|
|
currentPath = normalize(`${currentPath}`)
|
|
}
|
|
else {
|
|
currentPath = normalize(`/${currentPath}`)
|
|
}
|
|
|
|
if (asRegex) {
|
|
return new RegExp(matchPath).test(currentPath)
|
|
}
|
|
|
|
if (normalize(matchPath) !== currentPath) {
|
|
return false
|
|
}
|
|
|
|
const hashMatch = matchPath.match(HASH_RE)
|
|
|
|
if (hashMatch) {
|
|
return (inBrowser ? location.hash : '') === hashMatch[0]
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function normalize(path: string): string {
|
|
return decodeURI(path)
|
|
.replace(HASH_OR_QUERY_RE, '')
|
|
.replace(INDEX_OR_EXT_RE, '$1')
|
|
}
|
|
|
|
// From https://github.com/vuejs/vitepress/blob/97f9469b6d4eb7ba9de9a1111986581d1f704ec3/src/client/theme-default/support/sidebar.ts
|
|
function containsActiveLink(
|
|
path: string,
|
|
items: any | any[],
|
|
): boolean {
|
|
if (Array.isArray(items)) {
|
|
return items.some(item => containsActiveLink(path, item))
|
|
}
|
|
|
|
return isActive(path, items.link)
|
|
? true
|
|
: items.items
|
|
? containsActiveLink(path, items.items)
|
|
: false
|
|
}
|
|
|
|
// From https://github.com/vuejs/vitepress/blob/fa81e89643523170047ca2c9a690f4d7adf4ffdc/src/client/theme-default/support/sidebar.ts
|
|
export interface SidebarLink {
|
|
text: string
|
|
link: string
|
|
docFooterText?: string
|
|
}
|
|
|
|
function ensureStartingSlash(path: string): string {
|
|
return path.startsWith('/') ? path : `/${path}`
|
|
}
|
|
|
|
/**
|
|
* Get the `Sidebar` from sidebar option. This method will ensure to get correct
|
|
* sidebar config from `MultiSideBarConfig` with various path combinations such
|
|
* as matching `guide/` and `/guide/`. If no matching config was found, it will
|
|
* return empty array.
|
|
*/
|
|
export function getSidebar(
|
|
sidebar: DefaultTheme.Sidebar | undefined,
|
|
path: string,
|
|
): SidebarItem[] {
|
|
if (Array.isArray(sidebar))
|
|
return addBase(sidebar)
|
|
if (sidebar == null)
|
|
return []
|
|
|
|
path = ensureStartingSlash(path)
|
|
|
|
const dir = Object.keys(sidebar)
|
|
.sort((a, b) => {
|
|
return b.split('/').length - a.split('/').length
|
|
})
|
|
.find((dir) => {
|
|
// make sure the multi sidebar key starts with slash too
|
|
return path.startsWith(ensureStartingSlash(dir))
|
|
})
|
|
|
|
const sidebarDir = dir ? sidebar[dir]! : []
|
|
return Array.isArray(sidebarDir)
|
|
? addBase(sidebarDir)
|
|
: addBase(sidebarDir.items, sidebarDir.base)
|
|
}
|
|
|
|
/**
|
|
* Get or generate sidebar group from the given sidebar items.
|
|
*/
|
|
export function getSidebarGroups(sidebar: SidebarItem[]): SidebarItem[] {
|
|
const groups: SidebarItem[] = []
|
|
|
|
let lastGroupIndex: number = 0
|
|
|
|
for (const index in sidebar) {
|
|
const item = sidebar[index]!
|
|
|
|
if (item.items) {
|
|
lastGroupIndex = groups.push(item)
|
|
continue
|
|
}
|
|
|
|
if (!groups[lastGroupIndex]) {
|
|
groups.push({ items: [] })
|
|
}
|
|
|
|
groups[lastGroupIndex]!.items!.push(item)
|
|
}
|
|
|
|
return groups
|
|
}
|
|
|
|
export function getFlatSideBarLinks(sidebar: SidebarItem[]): SidebarLink[] {
|
|
const links: SidebarLink[] = []
|
|
|
|
function recursivelyExtractLinks(items: SidebarItem[]) {
|
|
for (const item of items) {
|
|
if (item.text && item.link) {
|
|
links.push({
|
|
text: item.text,
|
|
link: item.link,
|
|
docFooterText: item.docFooterText,
|
|
})
|
|
}
|
|
|
|
if (item.items) {
|
|
recursivelyExtractLinks(item.items)
|
|
}
|
|
}
|
|
}
|
|
|
|
recursivelyExtractLinks(sidebar)
|
|
|
|
return links
|
|
}
|
|
|
|
/**
|
|
* Check if the given sidebar item contains any active link.
|
|
*/
|
|
export function hasActiveLink(
|
|
path: string,
|
|
items: SidebarItem | SidebarItem[],
|
|
): boolean {
|
|
if (Array.isArray(items)) {
|
|
return items.some(item => hasActiveLink(path, item))
|
|
}
|
|
|
|
return isActive(path, items.link)
|
|
? true
|
|
: items.items
|
|
? hasActiveLink(path, items.items)
|
|
: false
|
|
}
|
|
|
|
function addBase(items: SidebarItem[], _base?: string): SidebarItem[] {
|
|
return Array.from(items, (_item) => {
|
|
const item = { ..._item }
|
|
const base = item.base || _base
|
|
if (base && item.link)
|
|
item.link = base + item.link
|
|
if (item.items)
|
|
item.items = addBase(item.items, base)
|
|
return item
|
|
})
|
|
}
|