Skip to content
This repository was archived by the owner on Mar 7, 2025. It is now read-only.

Commit 4ad0d8d

Browse files
ittusegoist
authored andcommitted
feat: toggle sidebar children (#223)
* Create SidebarItem component * Add collapsable attribute * Add arrow icon * Update document * Hide collapsable by default * Automatically open collapsed item by path * Handle open state in parent * Update index.js * Update SidebarItem.vue
1 parent fb36e7c commit 4ad0d8d

File tree

6 files changed

+239
-124
lines changed

6 files changed

+239
-124
lines changed

src/components/Sidebar.vue

+38-124
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<template>
22
<div class="Sidebar" :class="{isShown: $store.state.showSidebar}">
33
<InjectedComponents position="sidebar:start" />
4-
54
<InjectedComponents position="mobile-sidebar:start" />
65

76
<HeaderNav
@@ -11,56 +10,13 @@
1110
/>
1211

1312
<div class="SidebarItems">
14-
<div
13+
<sidebar-item
1514
v-for="(item, index) in $store.getters.sidebar"
16-
:class="['SidebarItem', item.title && 'hasTitle']"
1715
:key="index"
18-
>
19-
<div class="ItemTitle" v-if="item.title">{{ item.title }}</div>
20-
<template v-for="(link, index) of getChildren(item)">
21-
<a
22-
v-if="isExternalLink(link.link)"
23-
:key="index"
24-
:href="link.link"
25-
class="ItemLink"
26-
target="_blank"
27-
>
28-
{{ link.title }}
29-
<external-link-icon />
30-
</a>
31-
<router-link
32-
v-else
33-
:key="index"
34-
:to="link.link"
35-
:prefetch-files="getPrefetchFiles(link.link)"
36-
class="ItemLink"
37-
:class="{active: $route.path === link.link}"
38-
>
39-
{{ link.title }}
40-
</router-link>
41-
<div
42-
class="LinkToc"
43-
v-if="
44-
!$store.state.fetchingFile &&
45-
link.toc !== false &&
46-
link.link === $route.path &&
47-
$store.state.page.headings &&
48-
$store.state.page.headings.length > 0
49-
"
50-
:key="`toc-${index}`"
51-
>
52-
<router-link
53-
class="TocHeading"
54-
:to="{hash: heading.slug}"
55-
:data-level="heading.level"
56-
v-for="heading in $store.state.page.headings"
57-
:key="heading.slug"
58-
v-html="heading.text"
59-
>
60-
</router-link>
61-
</div>
62-
</template>
63-
</div>
16+
:item="item"
17+
:open="currentOpenIndex === index"
18+
@toggle="openSidebar(index)"
19+
/>
6420
</div>
6521

6622
<InjectedComponents position="sidebar:end" />
@@ -69,29 +25,48 @@
6925
</template>
7026

7127
<script>
72-
import {isExternalLink, getFileUrl, getFilenameByPath} from '../utils'
7328
import HeaderNav from './HeaderNav.vue'
29+
import SidebarItem from './SidebarItem.vue'
7430
7531
export default {
7632
components: {
77-
HeaderNav
33+
HeaderNav,
34+
SidebarItem
35+
},
36+
data() {
37+
return {
38+
currentOpenIndex: 0
39+
}
40+
},
41+
watch: {
42+
'$route.path': {
43+
handler() {
44+
this.currentOpenIndex = this.getCurrentIndex(
45+
this.$route.path,
46+
this.$store.getters.sidebar
47+
)
48+
},
49+
immediate: true
50+
}
7851
},
79-
8052
methods: {
81-
isExternalLink,
82-
getPrefetchFiles(path) {
83-
const {sourcePath, routes} = this.$store.getters.config
84-
if (routes && routes[path]) {
85-
const {file} = routes[path]
86-
return file ? [file] : []
53+
openSidebar(index) {
54+
this.currentOpenIndex = this.currentOpenIndex === index ? -1 : index
55+
},
56+
getCurrentIndex(currentPath, sidebarItems) {
57+
for (let idx = 0; idx < sidebarItems.length; idx++) {
58+
if (
59+
this.getChildren(sidebarItems[idx]).some(
60+
child => child.link === currentPath
61+
)
62+
) {
63+
return idx
64+
}
8765
}
88-
const filename = getFilenameByPath(path)
89-
const fileUrl = getFileUrl(sourcePath, filename)
90-
return fileUrl ? [fileUrl] : []
66+
return -1
9167
},
9268
getChildren(item) {
93-
// backward compabillity
94-
return item.children || item.links
69+
return item.children || item.links || []
9570
}
9671
}
9772
}
@@ -130,65 +105,4 @@ export default {
130105
display: none;
131106
}
132107
}
133-
134-
.SidebarItem {
135-
&:not(:last-child) {
136-
padding-bottom: 1.2rem;
137-
margin-bottom: 1.2rem;
138-
}
139-
140-
&.hasTitle {
141-
& .ItemLink {
142-
font-size: 0.9rem;
143-
}
144-
}
145-
146-
&.hasTitle >>> .TocHeading {
147-
font-size: 0.9rem;
148-
}
149-
}
150-
151-
.ItemTitle {
152-
font-size: 1rem;
153-
padding: 0 20px;
154-
margin-bottom: 10px;
155-
position: relative;
156-
color: var(--sidebar-section-title-color);
157-
text-transform: uppercase;
158-
}
159-
160-
.ItemLink {
161-
padding: 2px 20px;
162-
display: flex;
163-
font-size: 1.1rem;
164-
position: relative;
165-
166-
&.active {
167-
font-weight: bold;
168-
}
169-
}
170-
171-
.TocHeading {
172-
display: flex;
173-
line-height: 1.4;
174-
margin: 5px 0;
175-
position: relative;
176-
177-
&[data-level='2'] {
178-
padding: 0 20px;
179-
&:before {
180-
content: '-';
181-
margin-right: 5px;
182-
color: #979797;
183-
}
184-
}
185-
186-
&[data-level='3'] {
187-
padding: 0 20px 0 40px;
188-
}
189-
190-
&.router-link-exact-active {
191-
font-weight: bold;
192-
}
193-
}
194108
</style>

src/components/SidebarItem.vue

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<template>
2+
<div :class="['SidebarItem', item.title && 'hasTitle']">
3+
<div
4+
class="ItemTitle"
5+
v-if="item.title"
6+
:collapsable="item.collapsable"
7+
:class="{collapsable: item.collapsable}"
8+
@click="$emit('toggle')"
9+
>
10+
{{ item.title }}
11+
<span
12+
v-if="item.collapsable"
13+
class="arrow"
14+
:class="open ? 'down' : 'right'"
15+
></span>
16+
</div>
17+
<template v-if="!item.collapsable || open">
18+
<template v-for="(link, index) of children">
19+
<a
20+
v-if="isExternalLink(link.link)"
21+
:key="index"
22+
:href="link.link"
23+
class="ItemLink"
24+
target="_blank"
25+
>
26+
{{ link.title }}
27+
<external-link-icon />
28+
</a>
29+
<router-link
30+
v-else
31+
:key="index"
32+
:to="link.link"
33+
:prefetch-files="getPrefetchFiles(link.link)"
34+
class="ItemLink"
35+
:class="{active: $route.path === link.link}"
36+
>
37+
{{ link.title }}
38+
</router-link>
39+
<div
40+
class="LinkToc"
41+
v-if="
42+
!$store.state.fetchingFile &&
43+
link.toc !== false &&
44+
link.link === $route.path &&
45+
$store.state.page.headings &&
46+
$store.state.page.headings.length > 0
47+
"
48+
:key="`toc-${index}`"
49+
>
50+
<router-link
51+
class="TocHeading"
52+
:to="{hash: heading.slug}"
53+
:data-level="heading.level"
54+
v-for="heading in $store.state.page.headings"
55+
:key="heading.slug"
56+
v-html="heading.text"
57+
>
58+
</router-link>
59+
</div>
60+
</template>
61+
</template>
62+
</div>
63+
</template>
64+
65+
<script>
66+
import {isExternalLink, getFileUrl, getFilenameByPath} from '../utils'
67+
68+
export default {
69+
props: {
70+
item: {
71+
type: Object,
72+
required: true,
73+
default() {
74+
return {}
75+
}
76+
},
77+
open: {
78+
type: Boolean,
79+
required: false,
80+
default() {
81+
return true
82+
}
83+
}
84+
},
85+
computed: {
86+
children() {
87+
return this.item.children || this.item.links || []
88+
}
89+
},
90+
methods: {
91+
isExternalLink,
92+
getPrefetchFiles(path) {
93+
const {sourcePath, routes} = this.$store.getters.config
94+
if (routes && routes[path]) {
95+
const {file} = routes[path]
96+
return file ? [file] : []
97+
}
98+
const filename = getFilenameByPath(path)
99+
const fileUrl = getFileUrl(sourcePath, filename)
100+
return fileUrl ? [fileUrl] : []
101+
}
102+
}
103+
}
104+
</script>
105+
106+
<style scoped>
107+
.SidebarItem {
108+
&:not(:last-child) {
109+
margin-bottom: 1.2rem;
110+
}
111+
112+
&.hasTitle {
113+
& .ItemLink {
114+
font-size: 0.9rem;
115+
}
116+
}
117+
118+
&.hasTitle >>> .TocHeading {
119+
font-size: 0.9rem;
120+
}
121+
}
122+
123+
.ItemTitle {
124+
font-size: 1rem;
125+
padding: 0 20px;
126+
margin-bottom: 10px;
127+
position: relative;
128+
color: var(--sidebar-section-title-color);
129+
text-transform: uppercase;
130+
131+
&.collapsable {
132+
&:hover {
133+
cursor: pointer;
134+
}
135+
}
136+
}
137+
138+
.ItemLink {
139+
padding: 2px 20px;
140+
display: flex;
141+
font-size: 1.1rem;
142+
position: relative;
143+
144+
&.active {
145+
font-weight: bold;
146+
}
147+
}
148+
149+
.TocHeading {
150+
display: flex;
151+
line-height: 1.4;
152+
margin: 5px 0;
153+
position: relative;
154+
155+
&[data-level='2'] {
156+
padding: 0 20px;
157+
&:before {
158+
content: '-';
159+
margin-right: 5px;
160+
color: #979797;
161+
}
162+
}
163+
164+
&[data-level='3'] {
165+
padding: 0 20px 0 40px;
166+
}
167+
168+
&.router-link-exact-active {
169+
font-weight: bold;
170+
}
171+
}
172+
173+
a {
174+
text-decoration: none;
175+
color: var(--text-color);
176+
}
177+
178+
.arrow {
179+
display: inline-block;
180+
position: relative;
181+
top: -0.1em;
182+
left: 0.5em;
183+
&.right {
184+
border-left: 6px solid #ccc;
185+
border-top: 4px solid transparent;
186+
border-bottom: 4px solid transparent;
187+
}
188+
189+
&.down {
190+
border-top: 6px solid #ccc;
191+
border-left: 4px solid transparent;
192+
border-right: 4px solid transparent;
193+
}
194+
}
195+
</style>

0 commit comments

Comments
 (0)