parent
d2b4654ae3
commit
3e68b44771
@ -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,
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`))
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
96
theme/src/client/composables/layout.ts
Normal file
96
theme/src/client/composables/layout.ts
Normal 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()
|
||||
}
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user