feat(theme): page outline for mobile

pull/2104/head
Evan You 1 year ago
parent 15a2dd2eb9
commit 7182c4231f

@ -6,20 +6,6 @@ outline: deep
Site config is where you can define the global settings of the site. App config options define settings that apply to every VitePress site, regardless of what theme it is using. For example, the base directory or the title of the site.
<div class="site-config-toc">
[[toc]]
</div>
<style>
@media (min-width: 1280px) {
.site-config-toc {
display: none;
}
}
</style>
## Overview
### Config Resolution

@ -4,6 +4,7 @@ import { computed } from 'vue'
import { useSidebar } from '../composables/sidebar.js'
import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue'
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
const route = useRoute()
const { hasSidebar, hasAside } = useSidebar()
@ -38,6 +39,7 @@ const pageName = computed(() =>
<div class="content">
<div class="content-container">
<slot name="doc-before" />
<VPDocOutlineDropdown />
<main class="main">
<Content class="vp-doc" :class="pageName" />
</main>
@ -56,6 +58,16 @@ const pageName = computed(() =>
width: 100%;
}
.VPDoc .VPDocOutlineDropdown {
display: none;
}
@media (min-width: 768px) and (max-width: 1280px) {
.VPDoc .VPDocOutlineDropdown {
display: block;
}
}
@media (min-width: 768px) {
.VPDoc {
padding: 48px 32px 128px;

@ -3,10 +3,11 @@ import { ref, shallowRef } from 'vue'
import { useData } from '../composables/data.js'
import {
getHeaders,
resolveTitle,
useActiveAnchor,
type MenuItem
} from '../composables/outline.js'
import VPDocAsideOutlineItem from './VPDocAsideOutlineItem.vue'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
const { frontmatter, theme } = useData()
@ -23,14 +24,6 @@ const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
function handleClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector<HTMLAnchorElement>(
decodeURIComponent(id)
)
heading?.focus()
}
</script>
<template>
@ -38,21 +31,13 @@ function handleClick({ target: el }: Event) {
<div class="content">
<div class="outline-marker" ref="marker" />
<div class="outline-title">
{{
(typeof theme.outline === 'object' &&
!Array.isArray(theme.outline) &&
theme.outline.label) ||
theme.outlineTitle ||
'On this page'
}}
</div>
<div class="outline-title">{{ resolveTitle(theme) }}</div>
<nav aria-labelledby="doc-outline-aria-label">
<span class="visually-hidden" id="doc-outline-aria-label">
Table of Contents for current page
</span>
<VPDocAsideOutlineItem :headers="headers" :root="true" :onClick="handleClick" />
<VPDocOutlineItem :headers="headers" :root="true" />
</nav>
</div>
</div>

@ -0,0 +1,76 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useData } from '../composables/data.js'
import { getHeaders, resolveTitle } from '../composables/outline.js'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const { frontmatter, theme } = useData()
const open = ref(false)
onContentUpdated(() => {
open.value = false
})
</script>
<template>
<div class="VPDocOutlineDropdown">
<button @click="open = !open" :class="{ open }">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" />
</button>
<div class="items" v-if="open">
<VPDocOutlineItem :headers="getHeaders(frontmatter.outline ?? theme.outline)" />
</div>
</div>
</template>
<style scoped>
.VPDocOutlineDropdown {
margin-bottom: 42px;
}
.VPDocOutlineDropdown button {
display: block;
font-size: 14px;
font-weight: 500;
line-height: 24px;
color: var(--vp-c-text-2);
transition: color 0.5s;
border: 1px solid var(--vp-c-border);
padding: 4px 12px;
border-radius: 8px;
}
.VPDocOutlineDropdown button:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPDocOutlineDropdown button.open {
color: var(--vp-c-text-1);
}
.icon {
display: inline-block;
vertical-align: middle;
margin-left: 2px;
width: 14px;
height: 14px;
fill: currentColor;
}
:deep(.outline-link) {
font-size: 13px;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
margin-top: 10px;
border-left: 1px solid var(--vp-c-divider);
}
</style>

@ -3,9 +3,16 @@ import type { MenuItem } from '../composables/outline.js'
defineProps<{
headers: MenuItem[]
onClick: (e: MouseEvent) => void
root?: boolean
}>()
function onClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector<HTMLAnchorElement>(
decodeURIComponent(id)
)
heading?.focus()
}
</script>
<template>
@ -13,7 +20,7 @@ defineProps<{
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick">{{ title }}</a>
<template v-if="children?.length">
<VPDocAsideOutlineItem :headers="children" :onClick="onClick" />
<VPDocOutlineItem :headers="children" />
</template>
</li>
</ul>
@ -37,6 +44,7 @@ defineProps<{
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
font-weight: 500;
}
.outline-link:hover,

@ -2,6 +2,7 @@
import { useData } from '../composables/data.js'
import { useSidebar } from '../composables/sidebar.js'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
defineProps<{
open: boolean
@ -13,10 +14,6 @@ defineEmits<{
const { theme } = useData()
const { hasSidebar } = useSidebar()
function scrollToTop() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
}
</script>
<template>
@ -33,9 +30,7 @@ function scrollToTop() {
</span>
</button>
<a class="top-link" href="#" @click="scrollToTop">
{{ theme.returnToTopLabel || 'Return to top' }}
</a>
<VPLocalNavOutlineDropdown />
</div>
</template>
@ -91,23 +86,12 @@ function scrollToTop() {
fill: currentColor;
}
.top-link {
display: block;
.VPOutlineDropdown {
padding: 12px 24px 11px;
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.top-link:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
@media (min-width: 768px) {
.top-link {
.VPOutlineDropdown {
padding: 12px 32px 11px;
}
}

@ -0,0 +1,113 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useData } from '../composables/data.js'
import { getHeaders, resolveTitle } from '../composables/outline.js'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const { frontmatter, theme } = useData()
const open = ref(false)
const vh = ref(0)
onContentUpdated(() => {
open.value = false
})
function toggle() {
open.value = !open.value
vh.value = window.innerHeight + Math.min(window.scrollY - 64, 0)
}
function onItemClick(e: Event) {
if ((e.target as HTMLElement).classList.contains('outline-link')) {
open.value = false
}
}
function scrollToTop() {
open.value = false
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
}
</script>
<template>
<div class="VPLocalNavOutlineDropdown" :style="{ '--vp-vh': vh + 'px' }">
<button @click="toggle" :class="{ open }">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" />
</button>
<div class="items" v-if="open" @click="onItemClick">
<a class="top-link" href="#" @click="scrollToTop">
{{ theme.returnToTopLabel || 'Return to top' }}
</a>
<VPDocOutlineItem :headers="getHeaders(frontmatter.outline ?? theme.outline)" />
</div>
</div>
</template>
<style scoped>
.VPLocalNavOutlineDropdown {
padding: 12px 20px 11px;
}
.VPLocalNavOutlineDropdown button {
display: block;
font-size: 12px;
font-weight: 500;
line-height: 24px;
color: var(--vp-c-text-2);
transition: color 0.5s;
position: relative;
}
.VPLocalNavOutlineDropdown button:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPLocalNavOutlineDropdown button.open {
color: var(--vp-c-text-1);
}
.icon {
display: inline-block;
vertical-align: middle;
margin-left: 2px;
width: 14px;
height: 14px;
fill: currentColor;
}
:deep(.outline-link) {
font-size: 14px;
padding: 2px 0;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
position: absolute;
left: 20px;
right: 20px;
top: 64px;
background-color: var(--vp-local-nav-bg-color);
padding: 4px 10px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
max-height: calc(var(--vp-vh, 100vh) - 86px);
overflow: scroll;
box-shadow: var(--vp-shadow-3);
}
.top-link {
display: block;
color: var(--vp-c-brand);
font-size: 13px;
font-weight: 500;
padding: 6px 0;
margin: 0 13px 10px;
border-bottom: 1px solid var(--vp-c-divider);
}
</style>

@ -11,6 +11,16 @@ export type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[]
}
export function resolveTitle(theme: DefaultTheme.Config) {
return (
(typeof theme.outline === 'object' &&
!Array.isArray(theme.outline) &&
theme.outline.label) ||
theme.outlineTitle ||
'On this page'
)
}
export function getHeaders(range: DefaultTheme.Config['outline']) {
const headers = [...document.querySelectorAll('.VPDoc h2,h3,h4,h5,h6')]
.filter((el) => el.id && el.hasChildNodes())

Loading…
Cancel
Save