Skip to content

Commit 0c5888c

Browse files
committed
adding collapsible table of content
1 parent c6ae9b5 commit 0c5888c

File tree

3 files changed

+124
-30
lines changed

3 files changed

+124
-30
lines changed

src/components/detail/ContentSidebar.vue

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@
22
<div class="blog-content-sidebar">
33
<div class="toc">
44
<div class="title sharp-border">Table of content</div>
5-
<div v-for="(item, index) in toc"
6-
:key="index"
7-
class="toc--item"
8-
@click="scrollToHeading(item.id)"
9-
>
10-
<mdi-menu-right class="mdi-circle" />
11-
{{ item.text }}
12-
</div>
5+
<TOC :toc='toc' :hidden="false"/>
136
</div>
147
<br>
158
<div class="title sharp-border">Tags</div>
@@ -28,6 +21,7 @@
2821
</template>
2922
<script setup>
3023
import { defineProps } from "vue"
24+
import TOC from "./TOC.vue"
3125
3226
defineProps({
3327
toc: {
@@ -42,25 +36,6 @@ defineProps({
4236
}
4337
})
4438
45-
const scrollToHeading = (headingId) => {
46-
const xpath = `//*[@id="${headingId}"]`
47-
const heading = document.evaluate(
48-
xpath,
49-
document,
50-
null,
51-
XPathResult.FIRST_ORDERED_NODE_TYPE,
52-
null
53-
).singleNodeValue
54-
if (heading) {
55-
const position = heading.getBoundingClientRect()
56-
// scrolls to 100px above element
57-
window.scrollTo({
58-
left: position.left,
59-
top: position.top + window.scrollY - 100,
60-
behavior: "smooth"
61-
})
62-
}
63-
}
6439
</script>
6540
<style lang="scss">
6641
.blog-content-sidebar {

src/components/detail/TOC.vue

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<template>
2+
<div>
3+
<div
4+
v-for="item in toc"
5+
:key="item.id"
6+
:style="{
7+
'padding-left': item.val * 0.2 + 'em',
8+
display: hidden ? 'none' : 'block',
9+
}"
10+
@click="scrollToHeading(item.id, $event)"
11+
>
12+
<mdi-menu-right class="mdi-circle" @click="hideNested($event)" />
13+
{{ item.title }}
14+
<TOC v-if="isArray(item.node)" :toc="item.node" :hidden="hidden" />
15+
</div>
16+
</div>
17+
</template>
18+
19+
<script setup>
20+
import { defineProps } from "vue"
21+
22+
// TODO: hide with prop insted of -> hideNested
23+
24+
defineProps({
25+
toc: {
26+
type: Array
27+
},
28+
hidden: {
29+
type: Boolean
30+
}
31+
})
32+
33+
const isArray = (c) => {
34+
return Array.isArray(c)
35+
}
36+
37+
const scrollToHeading = (headingId, e) => {
38+
e.stopPropagation()
39+
const xpath = `//*[@id="${headingId}"]`
40+
const heading = document.evaluate(
41+
xpath,
42+
document,
43+
null,
44+
XPathResult.FIRST_ORDERED_NODE_TYPE,
45+
null
46+
).singleNodeValue
47+
if (heading) {
48+
const position = heading.getBoundingClientRect()
49+
window.scrollTo({
50+
left: position.left,
51+
top: position.top + window.scrollY - 100,
52+
behavior: "smooth"
53+
})
54+
}
55+
}
56+
57+
const hideNested = (e) => {
58+
e.preventDefault()
59+
e.stopPropagation()
60+
e = e.target
61+
while (e.tagName !== "DIV") {
62+
e = e.parentElement
63+
}
64+
e.childNodes.forEach((element) => {
65+
if (element.tagName === "DIV") {
66+
if (element.classList.length === 0) {
67+
element.style.display = "none"
68+
element.classList.add("hide")
69+
} else {
70+
element.style.display = "block"
71+
element.classList.remove("hide")
72+
}
73+
}
74+
})
75+
}
76+
</script>
77+
<style scoped>
78+
.hide {
79+
display: none;
80+
}
81+
</style>

src/helpers/markdown.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,50 @@ export const getTableOfContent = (source) => {
8484
const tokens = marked.lexer(source)
8585
// remove the first 2 tokens to avoid meta information
8686
// and then return every tokens that is of header type
87-
const headers = tokens.slice(2).filter(token => token.type === "heading" && token.depth <= 4)
88-
headers.forEach(header => {
87+
const hg = []
88+
const headers = tokens
89+
.slice(2)
90+
.filter((token) => token.type === "heading")// && token.depth <= 4)
91+
headers.forEach((header) => {
8992
const html = marked.parser([header])
9093
const doc = new DOMParser().parseFromString(html, "text/xml")
9194
header.id = doc.firstChild.id
9295
header.text = doc.firstChild.textContent
96+
hg.push(new Graph(header.depth, header.text, header.id, null))
9397
})
94-
return headers
98+
const stack = [hg[0]]
99+
const ans = []
100+
hg.forEach((h) => {
101+
if (h.val === stack[0].val) {
102+
ans.push(h)
103+
stack.push(h)
104+
} else if (h.val > stack[stack.length - 1].val) {
105+
stack[stack.length - 1].addNode(h)
106+
stack.push(h)
107+
} else if (h.val < stack[stack.length - 1].val) {
108+
while (!(h.val > stack[stack.length - 1].val)) {
109+
stack.splice(stack.length - 1, 1)
110+
}
111+
stack[stack.length - 1].addNode(h)
112+
stack.push(h)
113+
} else {
114+
stack.splice(stack.length - 1, 1)
115+
stack[stack.length - 1].addNode(h)
116+
stack.push(h)
117+
}
118+
})
119+
return ans
120+
}
121+
122+
class Graph {
123+
constructor (val, title, id) {
124+
this.val = val
125+
this.title = title
126+
this.id = id
127+
this.node = []
128+
}
129+
130+
addNode (node) {
131+
this.node.push(node)
132+
}
95133
}

0 commit comments

Comments
 (0)