Skip to content

Commit fdd498b

Browse files
authored
fix: fix next and prev links not working (#130)
1 parent f56e745 commit fdd498b

File tree

7 files changed

+110
-170
lines changed

7 files changed

+110
-170
lines changed

src/client/theme-default/components/EditLink.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ export default defineComponent({
4949
margin-left: 4px;
5050
width: 1rem;
5151
height: 1rem;
52+
transform: translateY(-1px);
5253
}
5354
</style>

src/client/theme-default/components/NextAndPrevLinks.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

src/client/theme-default/components/NextAndPrevLinks.vue

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
<template>
22
<div v-if="hasLinks" class="next-and-prev-link">
3-
<div class="prev">
4-
<a v-if="prev" class="link" :href="prev.link">
5-
<ArrowLeft class="icon icon-prev" />
6-
<span class="text">{{ prev.text }}</span>
7-
</a>
8-
</div>
9-
<div class="next">
10-
<a v-if="next" class="link" :href="next.link">
11-
<span class="text">{{ next.text }}</span>
12-
<ArrowRight class="icon icon-next" />
13-
</a>
3+
<div class="container">
4+
<div class="prev">
5+
<a v-if="prev" class="link" :href="prev.link">
6+
<ArrowLeft class="icon icon-prev" />
7+
<span class="text">{{ prev.text }}</span>
8+
</a>
9+
</div>
10+
<div class="next">
11+
<a v-if="next" class="link" :href="next.link">
12+
<span class="text">{{ next.text }}</span>
13+
<ArrowRight class="icon icon-next" />
14+
</a>
15+
</div>
1416
</div>
1517
</div>
1618
</template>
@@ -41,6 +43,10 @@ export default defineComponent({
4143

4244
<style scoped>
4345
.next-and-prev-link {
46+
padding-top: 1rem;
47+
}
48+
49+
.container {
4450
display: flex;
4551
justify-content: space-between;
4652
border-top: 1px solid var(--border-color);
@@ -82,8 +88,8 @@ export default defineComponent({
8288
.icon {
8389
display: block;
8490
flex-shrink: 0;
85-
width: 1rem;
86-
height: 1rem;
91+
width: 16px;
92+
height: 16px;
8793
fill: var(--text-color);
8894
}
8995
Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,102 @@
11
import { computed } from 'vue'
2-
import { useRoute, useSiteData } from 'vitepress'
2+
import { useSiteDataByRoute, usePageData } from 'vitepress'
3+
import { isArray, getPathDirName, ensureStartingSlash } from '../utils'
34
import { DefaultTheme } from '../config'
45

56
export function useNextAndPrevLinks() {
6-
const route = useRoute()
7-
// TODO: could this be useSiteData<DefaultTheme.Config> or is the siteData
8-
// resolved and has a different structure?
9-
const siteData = useSiteData()
10-
11-
const resolveLink = (targetLink: string) => {
12-
let target: DefaultTheme.SideBarLink | undefined
13-
Object.keys(siteData.value.themeConfig.sidebar).some((k) => {
14-
return siteData.value.themeConfig.sidebar[k].some(
15-
(v: { children: any }) => {
16-
if (Array.isArray(v.children)) {
17-
target = v.children.find((value: any) => {
18-
return value.link === targetLink
19-
})
20-
}
21-
return !!target
22-
}
23-
)
7+
const site = useSiteDataByRoute()
8+
const page = usePageData()
9+
10+
const candidates = computed(() => {
11+
const path = ensureStartingSlash(page.value.relativePath)
12+
const sidebar = site.value.themeConfig.sidebar
13+
14+
return getFlatSidebarLinks(path, sidebar)
15+
})
16+
17+
const currentPath = computed(() => {
18+
const path = ensureStartingSlash(page.value.relativePath)
19+
20+
return path.replace(/(index)?\.(md|html)$/, '')
21+
})
22+
23+
const currentIndex = computed(() => {
24+
return candidates.value.findIndex((item) => {
25+
return item.link === currentPath.value
2426
})
25-
return target
26-
}
27+
})
2728

2829
const next = computed(() => {
29-
const pageData = route.data
30-
if (pageData.frontmatter.next === false) {
31-
return undefined
32-
}
33-
if (typeof pageData.frontmatter.next === 'string') {
34-
return resolveLink(pageData.frontmatter.next)
30+
if (
31+
site.value.themeConfig.nextLinks !== false &&
32+
currentIndex.value > -1 &&
33+
currentIndex.value < candidates.value.length - 1
34+
) {
35+
return candidates.value[currentIndex.value + 1]
3536
}
36-
return pageData.next
3737
})
3838

3939
const prev = computed(() => {
40-
const pageData = route.data
41-
if (pageData.frontmatter.prev === false) {
42-
return undefined
40+
if (site.value.themeConfig.prevLinks !== false && currentIndex.value > 0) {
41+
return candidates.value[currentIndex.value - 1]
4342
}
44-
if (typeof pageData.frontmatter.prev === 'string') {
45-
return resolveLink(pageData.frontmatter.prev)
46-
}
47-
return pageData.prev
4843
})
4944

50-
const hasLinks = computed(() => {
51-
return !!next.value || !!prev.value
52-
})
45+
const hasLinks = computed(() => !!next.value || !!prev.value)
5346

5447
return {
5548
next,
5649
prev,
5750
hasLinks
5851
}
5952
}
53+
54+
function getFlatSidebarLinks(
55+
path: string,
56+
sidebar?: DefaultTheme.SideBarConfig
57+
): DefaultTheme.SideBarLink[] {
58+
if (!sidebar || sidebar === 'auto') {
59+
return []
60+
}
61+
62+
return isArray(sidebar)
63+
? getFlatSidebarLinksFromArray(path, sidebar)
64+
: getFlatSidebarLinksFromObject(path, sidebar)
65+
}
66+
67+
function getFlatSidebarLinksFromArray(
68+
path: string,
69+
sidebar: DefaultTheme.SideBarItem[]
70+
): DefaultTheme.SideBarLink[] {
71+
return sidebar.reduce<DefaultTheme.SideBarLink[]>((links, item) => {
72+
if (item.link) {
73+
links.push({ text: item.text, link: item.link })
74+
}
75+
76+
if (isSideBarGroup(item)) {
77+
links = [...links, ...getFlatSidebarLinks(path, item.children)]
78+
}
79+
80+
return links
81+
}, [])
82+
}
83+
84+
function getFlatSidebarLinksFromObject(
85+
path: string,
86+
sidebar: DefaultTheme.MultiSideBarConfig
87+
): DefaultTheme.SideBarLink[] {
88+
const paths = [path, Object.keys(sidebar)[0]]
89+
const item = paths.map((p) => sidebar[getPathDirName(p)]).find(Boolean)
90+
91+
if (isArray(item)) {
92+
return getFlatSidebarLinksFromArray(path, item)
93+
}
94+
95+
return []
96+
}
97+
98+
function isSideBarGroup(
99+
item: DefaultTheme.SideBarItem
100+
): item is DefaultTheme.SideBarGroup {
101+
return (item as DefaultTheme.SideBarGroup).children !== undefined
102+
}

src/client/theme-default/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export function isNullish(value: any): value is null | undefined {
99
return value === null || value === undefined
1010
}
1111

12+
export function isArray(value: any): value is any[] {
13+
return Array.isArray(value)
14+
}
15+
1216
export function withBase(path: string) {
1317
return (useSiteData().value.base + path).replace(/\/+/g, '/')
1418
}
@@ -62,6 +66,10 @@ export function getPathDirName(path: string): string {
6266
return ensureEndingSlash(segments.join('/'))
6367
}
6468

69+
export function ensureStartingSlash(path: string): string {
70+
return /^\//.test(path) ? path : `/${path}`
71+
}
72+
6573
export function ensureEndingSlash(path: string): string {
6674
return /(\.html|\/)$/.test(path) ? path : `${path}/`
6775
}

src/node/server.ts

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,13 @@ function createVitePressPlugin({
105105
ctx.body = vueSrc
106106
debug(ctx.url, ctx.status)
107107

108-
const pageDataWithLinks = {
109-
...pageData,
110-
// TODO: this doesn't work with locales
111-
...getNextAndPrev(siteData.themeConfig, ctx.path)
112-
}
113108
await next()
114109

115110
// make sure this is the main <script> block
116111
if (!ctx.query.type) {
117112
// inject pageData to generated script
118113
ctx.body += `\nexport const __pageData = ${JSON.stringify(
119-
JSON.stringify(pageDataWithLinks)
114+
JSON.stringify(pageData)
120115
)}`
121116
}
122117
return
@@ -133,56 +128,6 @@ function createVitePressPlugin({
133128
}
134129
}
135130

136-
// TODO: share types from SideBarLink, SideBarGroup, etc. We are also assuming
137-
// all themes follow this structure, in which case, we should expose the type
138-
// instead of having any for themeConfig or not nest `sidebar` inside
139-
// `themeConfig`, specially given it must be specified inside `locales` if there
140-
// are any
141-
interface SideBarLink {
142-
text: string
143-
link: string
144-
}
145-
146-
function getNextAndPrev(themeConfig: any, pagePath: string) {
147-
if (!themeConfig.sidebar) {
148-
return
149-
}
150-
const sidebar = themeConfig.sidebar
151-
let candidates: SideBarLink[] = []
152-
Object.keys(sidebar).forEach((k) => {
153-
if (!pagePath.startsWith(k)) {
154-
return
155-
}
156-
sidebar[k].forEach((sidebarItem: { children?: SideBarLink[] }) => {
157-
if (!sidebarItem.children) {
158-
return
159-
}
160-
sidebarItem.children.forEach((candidate) => {
161-
candidates.push(candidate)
162-
})
163-
})
164-
})
165-
166-
const path = pagePath.replace(/\.(md|html)$/, '')
167-
const currentLinkIndex = candidates.findIndex((v) => v.link === path)
168-
169-
const nextAndPrev: { prev?: SideBarLink; next?: SideBarLink } = {}
170-
171-
if (
172-
themeConfig.nextLinks !== false &&
173-
currentLinkIndex > -1 &&
174-
currentLinkIndex < candidates.length - 1
175-
) {
176-
nextAndPrev.next = candidates[currentLinkIndex + 1]
177-
}
178-
179-
if (themeConfig.prevLinks !== false && currentLinkIndex > 0) {
180-
nextAndPrev.next = candidates[currentLinkIndex - 1]
181-
}
182-
183-
return nextAndPrev
184-
}
185-
186131
export async function createServer(options: ServerConfig = {}) {
187132
const config = await resolveConfig(options.root)
188133

types/shared.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export interface PageData {
2929
headers: Header[]
3030
relativePath: string
3131
lastUpdated: number
32-
next?: { text: string; link: string }
33-
prev?: { text: string; link: string }
3432
}
3533

3634
export interface Header {

0 commit comments

Comments
 (0)