Skip to content

Commit 8a064a5

Browse files
authored
feat: show toc on mobile screens (cotes2020#1964)
1 parent 740bd84 commit 8a064a5

File tree

13 files changed

+430
-30
lines changed

13 files changed

+430
-30
lines changed

_includes/toc-status.html

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% comment %}
2+
Determine TOC state and return it through variable "enable_toc"
3+
{% endcomment %}
4+
5+
{% assign enable_toc = false %}
6+
{% if site.toc and page.toc %}
7+
{% if page.content contains '<h2' or page.content contains '<h3' %}
8+
{% assign enable_toc = true %}
9+
{% endif %}
10+
{% endif %}

_includes/toc.html

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
{% assign enable_toc = false %}
2-
{% if site.toc and page.toc %}
3-
{% if page.content contains '<h2' or page.content contains '<h3' %}
4-
{% assign enable_toc = true %}
5-
{% endif %}
6-
{% endif %}
1+
{% include toc-status.html %}
72

83
{% if enable_toc %}
9-
<section id="toc-wrapper" class="d-none ps-0 pe-4">
4+
<section id="toc-wrapper" class="ps-0 pe-4">
105
<h2 class="panel-heading ps-3 mb-2">{{- site.data.locales[include.lang].panel.toc -}}</h2>
116
<nav id="toc"></nav>
127
</section>

_javascript/modules/components/toc.js

+28-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
export function toc() {
2-
if (document.querySelector('main h2, main h3')) {
3-
// see: https://github.com/tscanlin/tocbot#usage
4-
tocbot.init({
5-
tocSelector: '#toc',
6-
contentSelector: '.content',
7-
ignoreSelector: '[data-toc-skip]',
8-
headingSelector: 'h2, h3, h4',
9-
orderedList: false,
10-
scrollSmooth: false
11-
});
12-
13-
document.getElementById('toc-wrapper').classList.remove('d-none');
1+
import { TocMobile as mobile } from './toc/toc-mobile';
2+
import { TocDesktop as desktop } from './toc/toc-desktop';
3+
4+
const desktopMode = matchMedia('(min-width: 1200px)');
5+
6+
function refresh(e) {
7+
if (e.matches) {
8+
mobile.hidePopup();
9+
desktop.refresh();
10+
} else {
11+
mobile.refresh();
1412
}
1513
}
14+
15+
function init() {
16+
if (document.querySelector('main>article[data-toc="true"]') === null) {
17+
return;
18+
}
19+
20+
// Avoid create multiple instances of Tocbot. Ref: <https://github.com/tscanlin/tocbot/issues/203>
21+
if (desktopMode.matches) {
22+
desktop.init();
23+
} else {
24+
mobile.init();
25+
}
26+
27+
desktopMode.onchange = refresh;
28+
}
29+
30+
export { init as initToc };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class TocDesktop {
2+
/* Tocbot options Ref: https://github.com/tscanlin/tocbot#usage */
3+
static options = {
4+
tocSelector: '#toc',
5+
contentSelector: '.content',
6+
ignoreSelector: '[data-toc-skip]',
7+
headingSelector: 'h2, h3, h4',
8+
orderedList: false,
9+
scrollSmooth: false,
10+
headingsOffset: 16 * 2 // 2rem
11+
};
12+
13+
static refresh() {
14+
tocbot.refresh(this.options);
15+
}
16+
17+
static init() {
18+
if (document.getElementById('toc-wrapper')) {
19+
tocbot.init(this.options);
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* TOC button, topbar and popup for mobile devices
3+
*/
4+
5+
const $tocBar = document.getElementById('toc-bar');
6+
const $soloTrigger = document.getElementById('toc-solo-trigger');
7+
const $triggers = document.getElementsByClassName('toc-trigger');
8+
const $popup = document.getElementById('toc-popup');
9+
const $btnClose = document.getElementById('toc-popup-close');
10+
11+
const SCROLL_LOCK = 'overflow-hidden';
12+
const CLOSING = 'closing';
13+
14+
export class TocMobile {
15+
static invisible = true;
16+
static barHeight = 16 * 3; // 3rem
17+
18+
static options = {
19+
tocSelector: '#toc-popup-content',
20+
contentSelector: '.content',
21+
ignoreSelector: '[data-toc-skip]',
22+
headingSelector: 'h2, h3, h4',
23+
orderedList: false,
24+
scrollSmooth: false,
25+
collapseDepth: 4,
26+
headingsOffset: this.barHeight
27+
};
28+
29+
static initBar() {
30+
const observer = new IntersectionObserver(
31+
(entries) => {
32+
entries.forEach((entry) => {
33+
$tocBar.classList.toggle('invisible', entry.isIntersecting);
34+
});
35+
},
36+
{ rootMargin: `-${this.barHeight}px 0px 0px 0px` }
37+
);
38+
39+
observer.observe($soloTrigger);
40+
this.invisible = false;
41+
}
42+
43+
static listenAnchors() {
44+
const $anchors = document.getElementsByClassName('toc-link');
45+
[...$anchors].forEach((anchor) => {
46+
anchor.onclick = this.hidePopup;
47+
});
48+
}
49+
50+
static refresh() {
51+
if (this.invisible) {
52+
this.initComponents();
53+
}
54+
tocbot.refresh(this.options);
55+
this.listenAnchors();
56+
}
57+
58+
static showPopup() {
59+
TocMobile.lockScroll(true);
60+
$popup.showModal();
61+
const activeItem = $popup.querySelector('li.is-active-li');
62+
activeItem.scrollIntoView({ block: 'center' });
63+
}
64+
65+
static hidePopup() {
66+
if (!$popup.open) {
67+
return;
68+
}
69+
70+
$popup.toggleAttribute(CLOSING);
71+
72+
$popup.addEventListener(
73+
'animationend',
74+
() => {
75+
$popup.toggleAttribute(CLOSING);
76+
$popup.close();
77+
},
78+
{ once: true }
79+
);
80+
81+
TocMobile.lockScroll(false);
82+
}
83+
84+
static lockScroll(enable) {
85+
document.documentElement.classList.toggle(SCROLL_LOCK, enable);
86+
document.body.classList.toggle(SCROLL_LOCK, enable);
87+
}
88+
89+
static clickBackdrop(event) {
90+
const rect = event.target.getBoundingClientRect();
91+
if (
92+
event.clientX < rect.left ||
93+
event.clientX > rect.right ||
94+
event.clientY < rect.top ||
95+
event.clientY > rect.bottom
96+
) {
97+
TocMobile.hidePopup();
98+
}
99+
}
100+
101+
static initComponents() {
102+
this.initBar();
103+
104+
[...$triggers].forEach((trigger) => {
105+
trigger.onclick = this.showPopup;
106+
});
107+
108+
$popup.onclick = this.clickBackdrop;
109+
$btnClose.onclick = $popup.oncancel = this.hidePopup;
110+
}
111+
112+
static init() {
113+
tocbot.init(this.options);
114+
this.listenAnchors();
115+
this.initComponents();
116+
}
117+
}

_javascript/modules/plugins.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ export { initClipboard } from './components/clipboard';
33
export { loadImg } from './components/img-loading';
44
export { imgPopup } from './components/img-popup';
55
export { initLocaleDatetime } from './components/locale-datetime';
6-
export { toc } from './components/toc';
6+
export { initToc } from './components/toc';

_javascript/post.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { basic, initSidebar, initTopbar } from './modules/layouts';
1+
import { basic, initTopbar, initSidebar } from './modules/layouts';
2+
23
import {
34
loadImg,
45
imgPopup,
56
initLocaleDatetime,
67
initClipboard,
7-
toc
8+
initToc
89
} from './modules/plugins';
910

1011
loadImg();
11-
toc();
12+
initToc();
1213
imgPopup();
1314
initSidebar();
1415
initLocaleDatetime();

_layouts/post.html

+27-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
{% include lang.html %}
1313

14-
<article class="px-1">
14+
{% include toc-status.html %}
15+
16+
<article class="px-1" data-toc="{{ enable_toc }}">
1517
<header>
1618
<h1 data-toc-skip>{{ page.title }}</h1>
1719
{% if page.description %}
@@ -95,6 +97,30 @@ <h1 data-toc-skip>{{ page.title }}</h1>
9597
</div>
9698
</header>
9799

100+
{% if enable_toc %}
101+
<div id="toc-bar" class="d-flex align-items-center justify-content-between invisible">
102+
<span class="label text-truncate">{{ page.title }}</span>
103+
<button type="button" class="toc-trigger btn btn-link me-1">
104+
<i class="fa-solid fa-list-ul fa-fw"></i>
105+
</button>
106+
</div>
107+
108+
<button id="toc-solo-trigger" type="button" class="toc-trigger btn btn-outline-secondary btn-sm">
109+
<span class="label ps-2 pe-1">{{- site.data.locales[lang].panel.toc -}}</span>
110+
<i class="fa-solid fa-angle-right fa-fw"></i>
111+
</button>
112+
113+
<dialog id="toc-popup" class="p-0">
114+
<div class="header d-flex flex-row align-items-center justify-content-between">
115+
<div class="label text-truncate py-2 ms-4">{{- page.title -}}</div>
116+
<button id="toc-popup-close" type="button" class="btn btn-link">
117+
<i class="fas fa-close fa-fw"></i>
118+
</button>
119+
</div>
120+
<div id="toc-popup-content" class="px-4 py-3 pb-4"></div>
121+
</dialog>
122+
{% endif %}
123+
98124
<div class="content">
99125
{{ content }}
100126
</div>

_sass/addon/commons.scss

+1-3
Original file line numberDiff line numberDiff line change
@@ -908,9 +908,7 @@ $btn-mb: 0.5rem;
908908
}
909909

910910
#topbar {
911-
button i {
912-
color: #999999;
913-
}
911+
@extend %btn-color;
914912

915913
#breadcrumb {
916914
font-size: 1rem;

_sass/addon/module.scss

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
color: var(--heading-color);
99
font-weight: 400;
1010
font-family: $font-family-heading;
11+
scroll-margin-top: 3.5rem;
1112
}
1213

1314
%anchor {
@@ -134,6 +135,12 @@
134135
}
135136
}
136137

138+
%btn-color {
139+
button i {
140+
color: #999999;
141+
}
142+
}
143+
137144
/* ---------- scss mixin --------- */
138145

139146
@mixin mt-mb($value) {

_sass/colors/typography-dark.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
--btn-border-color: #2e2f31;
2323
--btn-backtotop-color: var(--text-color);
2424
--btn-backtotop-border-color: #212122;
25-
--btn-box-shadow: var(--main-bg);
2625
--card-header-bg: #292929;
2726
--checkbox-color: rgb(118, 120, 121);
2827
--checkbox-checked-color: var(--link-color);
@@ -60,6 +59,7 @@
6059

6160
/* Posts */
6261
--toc-highlight: rgb(116, 178, 243);
62+
--toc-popup-border-color: #373737;
6363
--tag-hover: rgb(43, 56, 62);
6464
--tb-odd-bg: #252526; /* odd rows of the posts' table */
6565
--tb-even-bg: rgb(31, 31, 34); /* even rows of the posts' table */

_sass/colors/typography-light.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
--btn-border-color: #e9ecef;
2323
--btn-backtotop-color: #686868;
2424
--btn-backtotop-border-color: #f1f1f1;
25-
--btn-box-shadow: #eaeaea;
2625
--checkbox-color: #c5c5c5;
2726
--checkbox-checked-color: #07a8f7;
2827
--img-bg: radial-gradient(
@@ -63,6 +62,7 @@
6362

6463
/* Posts */
6564
--toc-highlight: #0550ae;
65+
--toc-popup-border-color: lightgray;
6666
--btn-share-color: gray;
6767
--btn-share-hover-color: #0d6efd;
6868
--card-bg: white;

0 commit comments

Comments
 (0)