Skip to content

Commit 2a43dca

Browse files
committed
feat: plugin
Signed-off-by: Stefan Pfaffel <s.pfaffel@gmail.com>
1 parent 99d94e6 commit 2a43dca

File tree

6 files changed

+267
-0
lines changed

6 files changed

+267
-0
lines changed

src/components/observer.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useRouter } from '@vuepress/client'
2+
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
3+
4+
export default (selector) => {
5+
const router = useRouter()
6+
const visible = ref(false)
7+
8+
const observerOptions = {
9+
root: null,
10+
rootMargin: '0px',
11+
threshold: [0.0, 0.75],
12+
}
13+
14+
function intersectionCallback(entries) {
15+
entries.forEach((entry) => {
16+
visible.value = entry.isIntersecting
17+
})
18+
}
19+
20+
const observer = new IntersectionObserver(intersectionCallback, observerOptions)
21+
22+
function observe() {
23+
const element = window.document.querySelector(selector)
24+
observer.observe(element)
25+
}
26+
function disconnect() {
27+
observer.disconnect()
28+
}
29+
30+
router.afterEach((() => {
31+
disconnect()
32+
nextTick(observe)
33+
}))
34+
35+
36+
onMounted(() => {
37+
nextTick(observe)
38+
})
39+
onUnmounted(disconnect)
40+
41+
return visible
42+
}

src/components/outline.vue

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<template>
2+
<Teleport to=".vp-theme-container">
3+
<Transition name="outline">
4+
<div v-if="visible"
5+
class="anchor-right-content">
6+
<ul>
7+
<li class="start">
8+
On this page
9+
</li>
10+
<template v-for="(item, index) in headers"
11+
:key="index">
12+
<template v-if="item.children.length > 0">
13+
<sub-menu :key="item.key"
14+
:menu-info="item" />
15+
</template>
16+
<template v-else>
17+
<li :class="['level', 'level-' + item.level, { active: (!route.hash && index == 0) || route.hash === `#${item.slug}` }]"
18+
@click="headerClick(item)">
19+
{{ item.title }}
20+
</li>
21+
</template>
22+
</template>
23+
</ul>
24+
</div>
25+
</Transition>
26+
</Teleport>
27+
</template>
28+
29+
<script setup>
30+
import { usePageData, useRoute, useRouter } from '@vuepress/client';
31+
import { computed, onMounted, ref } from 'vue';
32+
import observe from 'vuepress-plugin-anchor-right/src/components/observer';
33+
import SubMenu from 'vuepress-plugin-anchor-right/src/components/sub-menu.vue';
34+
35+
const headers = ref([]);
36+
const router = useRouter();
37+
const route = useRoute();
38+
const page = usePageData()
39+
const isFooterVisible = observe('footer')
40+
41+
const isRootPage = computed(() => {
42+
const { path } = page.value
43+
return !path || path === '/' || path === '/index' || path === '/index.html'
44+
})
45+
46+
const visible = computed(() => {
47+
if (isRootPage.value) {
48+
return false
49+
}
50+
return headers.value.length > 0 && isFooterVisible.value === false
51+
})
52+
53+
router.afterEach((_to, _from, failure) => {
54+
if (!failure) {
55+
refresh()
56+
}
57+
})
58+
59+
const refresh = () => {
60+
const page = usePageData();
61+
headers.value = page.value.headers;
62+
}
63+
64+
const headerClick = (item) => {
65+
router.push(`#${item.slug}`);
66+
};
67+
68+
onMounted(refresh);
69+
</script>
70+
71+
<style lang="scss">
72+
@media (max-width: 1000px) {
73+
.anchor-right {
74+
@apply hidden;
75+
}
76+
77+
.sidebar-items {
78+
.sidebar-item-children {
79+
.sidebar-item-children {
80+
@apply block;
81+
}
82+
}
83+
}
84+
85+
.vp-page {
86+
@apply pr-0;
87+
}
88+
}
89+
90+
@media (min-width: 1000px) {
91+
.anchor-right {
92+
@apply block;
93+
}
94+
95+
.sidebar-items {
96+
.sidebar-item-children {
97+
.sidebar-item-children {
98+
@apply hidden;
99+
}
100+
}
101+
}
102+
103+
.vp-page {
104+
@apply pr-64;
105+
}
106+
}
107+
108+
.anchor-right-content {
109+
@apply text-lg right-0 fixed overflow-auto w-56;
110+
111+
top: calc(var(--navbar-height) + 2rem);
112+
max-height: 84vh;
113+
// border-left: #eaecef solid 1px;
114+
115+
ul {
116+
@apply space-y-2;
117+
118+
li {
119+
@apply block;
120+
padding-top: 1px !important;
121+
padding-bottom: 1px !important;
122+
padding-right: 0 !important;
123+
124+
&.start {
125+
@apply mb-8 text-lg font-medium text-gray-400
126+
}
127+
}
128+
}
129+
130+
.level {
131+
@apply block cursor-pointer no-underline;
132+
color: #999;
133+
padding: 4px 12px 4px 0;
134+
margin-left: -1px;
135+
}
136+
137+
.active {
138+
color: var(--c-text-accent);
139+
@apply font-medium;
140+
141+
.menu {
142+
@apply font-normal;
143+
}
144+
}
145+
}
146+
147+
.outline-enter-active,
148+
.outline-leave-active {
149+
transition: opacity 0.25s ease;
150+
}
151+
152+
.outline-enter-from,
153+
.outline-leave-to {
154+
opacity: 0;
155+
}
156+
</style>

src/components/sub-menu.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup>
2+
import { useRoute, useRouter } from '@vuepress/client';
3+
4+
const router = useRouter();
5+
const route = useRoute();
6+
7+
const props = defineProps({
8+
menuInfo: {
9+
type: Object,
10+
},
11+
isFirst: {
12+
type: Boolean,
13+
default: false,
14+
}
15+
});
16+
17+
const headerClick = (item) => {
18+
router.push(`#${item.slug}`);
19+
};
20+
21+
</script>
22+
<template>
23+
<li :class="[
24+
'level',
25+
'level-' + props.menuInfo.level,
26+
{ active: (isFirst && !route.hash) || route.hash === `#${props.menuInfo.slug}` },
27+
]"
28+
@click.prevent="headerClick(props.menuInfo)">
29+
<div>
30+
{{ props.menuInfo.title }}
31+
</div>
32+
<ul class="menu">
33+
<template v-for="(item, index) in props.menuInfo.children"
34+
:key="index">
35+
<sub-menu v-if="item.children.length > 0 && item.level !== 3"
36+
:key="item.key"
37+
:menu-info="item"
38+
:route="route"
39+
:router="router" />
40+
41+
<li v-else
42+
:class="['level', 'level-' + item.level]"
43+
@click.stop="headerClick(item)">
44+
{{ item.title }}
45+
</li>
46+
</template>
47+
</ul>
48+
</li>
49+
</template>

src/config/client.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineClientConfig } from 'vuepress/client'
2+
import Outline from '../components/outline.vue'
3+
4+
export default defineClientConfig({
5+
rootComponents: [Outline]
6+
})

src/config/node.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getDirname, path } from '@vuepress/utils'
2+
3+
const __dirname = import.meta.dirname || getDirname(import.meta.url)
4+
5+
export default () => {
6+
return () => {
7+
return {
8+
name: '@discue/vuepress-plugin-outline',
9+
clientConfigFile: path.resolve(__dirname, './client.js')
10+
}
11+
}
12+
}

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import plugin from './config/node.js'
2+
export default plugin

0 commit comments

Comments
 (0)