提交 5253cc24 编写于 作者: D DCloud_LXH

feat: add toc

上级 da91ce87
......@@ -36,6 +36,7 @@ const config = {
docsDir: 'docs',
editLinks: true,
editLinkText: '帮助我们改善此页面!',
lastUpdated: '上次更新',
// smoothScroll: true,
algolia: {
apiKey: '2fdcc4e76c8e260671ad70065e60b2e7',
......
......@@ -45,15 +45,13 @@ function createMarkdownArray(contents = [], childrenName = 'children') {
}
}
function removeParent(childs = []) {
// 移除最后一项 parent 节点,防止循环引用报错
(function removeParent(childs = []) {
childs.forEach(child => {
if (child.parent) delete child.parent
if (child[childrenName]) removeParent(child[childrenName])
})
}
// 移除最后一项 parent 节点,防止循环引用报错
removeParent(markdownArray[markdownArray.length - 1][childrenName])
})(markdownArray[markdownArray.length - 1][childrenName])
return markdownArray
}
......
......@@ -121,6 +121,7 @@ export default {
this.mainNavBar = document.querySelector('.main-navbar')
this.subNavBar = document.querySelector('.sub-navbar')
this.pageContainer = document.querySelector('.page')
this.vuepressToc = document.querySelector('.vuepress-toc')
this.navbarHeight = this.navbar.clientHeight
this.subNavBarHeight = this.subNavBar.clientHeight
this.mainNavBarHeight = this.mainNavBar.clientHeight
......@@ -134,6 +135,7 @@ export default {
window.removeEventListener('scroll', this.onWindowScroll)
this.fixedNavbar = false
this.sideBar && this.sideBar.removeAttribute('style')
this.vuepressToc && this.vuepressToc.removeAttribute('style')
this.navbar && this.navbar.removeAttribute('style')
this.pageContainer && this.pageContainer.removeAttribute('style')
},
......@@ -144,6 +146,7 @@ export default {
let sideTop = this.navbarHeight - scrollTop
sideTop <= this.subNavBarHeight && (sideTop = this.subNavBarHeight)
this.sideBar && (this.sideBar.style.top = `${sideTop + 1}px`)
this.vuepressToc && (this.vuepressToc.style.top = `${sideTop + 1}px`)
}
if (scrollTop >= this.mainNavBarHeight) {
......@@ -182,6 +185,7 @@ export default {
this.navbarHeight = this.navbar.clientHeight
this.subNavBarHeight = this.subNavBar.clientHeight
this.sideBar.style.top = `${this.navbarHeight + 1}px`
this.vuepressToc.style.top = `${this.navbarHeight + 1}px`
})
}
}
......
<template>
<component
:is="tag || 'div'"
class="sticker"
:class="needFloat ? ['stick-float'] : undefined"
:style="needFloat ? { bottom: `${stickBottom}px` } : undefined"
>
<slot></slot>
</component>
</template>
<script>
import { findContainerInVm } from '../util';
export default {
props: ['stick', 'tag'],
data() {
return {
needFloat: false,
stickBottom: 0,
};
},
watch: {
stick() {
this.unStick();
this.stickHandle();
},
},
mounted() {
this.stickHandle();
},
beforeDestroy() {
this.unStick();
},
methods: {
stickHandle() {
if (!this.stick) return;
const stickElement = findContainerInVm(this.stick, this);
if (!stickElement) return;
this._stickerScroll = () => {
const rect = this.$el.getBoundingClientRect();
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
this.needFloat =
document.body.offsetHeight - scrollTop - rect.height < stickElement.offsetHeight;
this.stickBottom = stickElement.offsetHeight;
};
this._stickerScroll();
window.addEventListener('scroll', this._stickerScroll);
},
unStick() {
this.needFloat = false;
this.stickBottom = 0;
window.removeEventListener('scroll', this._stickerScroll);
},
},
};
</script>
<style lang="stylus">
.sticker
position fixed
&.stick-float
top auto
position absolute
</style>
<template>
<Sticker v-if="visible" class="vuepress-toc" v-bind="$attrs" :style="{ top: initVisibleTop }">
<h5>ON THIS PAGE</h5>
<div
v-for="(item, index) in $page.headers"
ref="chairTocItem"
:key="index"
class="vuepress-toc-item"
:class="[`vuepress-toc-h${item.level}`, { active: activeIndex === index }]"
>
<a :href="`#${item.slug}`" :title="item.title">{{ item.title }}</a>
</div>
</Sticker>
</template>
<script>
import Sticker from './Sticker.vue';
let initTop;
// get offset top
function getAbsoluteTop(dom) {
return dom && dom.getBoundingClientRect
? dom.getBoundingClientRect().top +
document.body.scrollTop +
document.documentElement.scrollTop
: 0;
}
export default {
components: {
Sticker,
},
data() {
return {
activeIndex: 0,
initVisibleTop: '0px',
};
},
computed: {
visible() {
return (
this.$frontmatter &&
this.$frontmatter.toc !== false
// && !!(this.$page && this.$page.headers && this.$page.headers.length)
);
},
},
watch: {
activeIndex() {
const items = this.$refs.chairTocItem || [];
const dom = items[this.activeIndex];
if (!dom) return;
const rect = dom.getBoundingClientRect();
const wrapperRect = this.$el.getBoundingClientRect();
const top = rect.top - wrapperRect.top;
if (top < 20) {
this.$el.scrollTop = this.$el.scrollTop + top - 20;
} else if (top + rect.height > wrapperRect.height) {
this.$el.scrollTop += rect.top - (wrapperRect.height - rect.height);
}
},
$route() {},
},
mounted() {
// sync visible to parent component
const syncVisible = () => {
this.$emit('visible-change', this.visible);
};
syncVisible();
this.$watch('visible', syncVisible);
// binding event
setTimeout(() => this.triggerEvt(), 1000);
this._onScroll = () => this.onScroll();
this._onHashChange = () => {
const hash = decodeURIComponent(location.hash.substring(1));
const index = (this.$page.headers || []).findIndex(h => h.slug === hash);
if (index >= 0) this.activeIndex = index;
const dom = hash && document.getElementById(hash);
if (dom) window.scrollTo(0, getAbsoluteTop(dom) - 20);
};
window.addEventListener('scroll', this._onScroll);
// window.addEventListener('hashchange', this._onHashChange);
},
beforeDestroy() {
window.removeEventListener('scroll', this._onScroll);
window.removeEventListener('hashchange', this._onHashChange);
},
methods: {
onScroll() {
if (initTop === undefined) {
initTop = getAbsoluteTop(this.$el);
}
// update position
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
const headings = this.$page.headers || [];
// change active toc with scrolling
let i = 0;
const addLink = index => {
this.activeIndex = index;
};
for (; i < headings.length; i++) {
const dom = document.getElementById(headings[i].slug);
const top = getAbsoluteTop(dom);
if (top - 50 < scrollTop) {
addLink(i);
} else {
if (!i) addLink(i);
break;
}
}
},
triggerEvt() {
this._onScroll();
this._onHashChange();
},
},
};
</script>
<style lang="stylus">
.table-of-contents
display none !important
.vuepress-toc
position fixed
display none
max-height 89vh
width $vuepress-toc-width
overflow-y auto
// margin-top $navbarHeight
top 0
right 0
box-sizing border-box
background-color #fff
/* background: #fff; */
z-index 0
.vuepress-toc-item
position relative
padding 0.1rem 0.6rem 0.1rem 1.5rem
line-height 1.5rem
border-left 2px solid rgba(0, 0, 0, 0.08)
overflow hidden
a
display block
color $textColor
width 100%
box-sizing border-box
font-size 14px
font-weight 400
text-decoration none
transition color 0.3s
overflow hidden
text-overflow ellipsis
white-space nowrap
&.active
border-left-color $accentColor
a
color $accentColor
&:hover
a
color $accentColor
for i in range(3, 6)
.vuepress-toc-h{i} a
padding-left 1rem * (i - 2)
// for vuepress-toc
@media (min-width: 1300px)
.vuepress-toc
display block
</style>
......@@ -43,6 +43,8 @@
<Footer />
</template>
</Page>
<Toc />
</div>
</template>
......@@ -53,6 +55,7 @@ import Page from '@theme/components/Page.vue'
import Sidebar from '@theme/components/Sidebar.vue'
import Footer from '@theme/components/Footer.vue';
import SiderBarBottom from '../components/SiderBarBottom.vue';
import Toc from '../components/Toc';
import { resolveSidebarItems, forbidScroll, BaiduStat } from '../util'
import navProvider from '../mixin/navProvider';
......@@ -65,7 +68,8 @@ export default {
Sidebar,
Navbar,
Footer,
SiderBarBottom
SiderBarBottom,
Toc
},
data () {
return {
......
......@@ -81,6 +81,11 @@ body.forbid_scroll
main.page
padding-bottom 0px
padding-right 0px
@media (min-width: 1300px)
&
padding-right $vuepress-toc-width
{$contentClass}:not(.custom)
> *:first-child
......
......@@ -4,4 +4,5 @@ $contentWidth = 1200px
$navbarHeight = 9rem
$navbar-sub-navbar-height = 5rem
$navbar-background-color = #f7f7f7
$search-container-color = #f5f6f7
\ No newline at end of file
$search-container-color = #f5f6f7
$vuepress-toc-width = 220px
\ No newline at end of file
......@@ -300,4 +300,25 @@ export function debounce(fn, delay) {
clearTimeout(timeout)
}
return newFn
}
/*
* find parent vm by ref
* @param {String} ref
* @param {Vue} vm
* @param {any} def default value
* @returns {Element}
*/
export function findContainerInVm(ref, vm, def) {
if (!ref) return def
let container
let parent = vm
while ((parent = parent.$parent) && !container) {
container = parent.$refs[ref]
}
// Ensure it's html element (ref could be component)
if (container && container.$el) {
container = container.$el
}
return container || def
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册