Skip to content

Commit

Permalink
feat: show toc on mobile screens (#1964)
Browse files Browse the repository at this point in the history
  • Loading branch information
kungfux authored Oct 11, 2024
1 parent 740bd84 commit 8a064a5
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 30 deletions.
10 changes: 10 additions & 0 deletions _includes/toc-status.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% comment %}
Determine TOC state and return it through variable "enable_toc"
{% endcomment %}

{% assign enable_toc = false %}
{% if site.toc and page.toc %}
{% if page.content contains '<h2' or page.content contains '<h3' %}
{% assign enable_toc = true %}
{% endif %}
{% endif %}
9 changes: 2 additions & 7 deletions _includes/toc.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
{% assign enable_toc = false %}
{% if site.toc and page.toc %}
{% if page.content contains '<h2' or page.content contains '<h3' %}
{% assign enable_toc = true %}
{% endif %}
{% endif %}
{% include toc-status.html %}

{% if enable_toc %}
<section id="toc-wrapper" class="d-none ps-0 pe-4">
<section id="toc-wrapper" class="ps-0 pe-4">
<h2 class="panel-heading ps-3 mb-2">{{- site.data.locales[include.lang].panel.toc -}}</h2>
<nav id="toc"></nav>
</section>
Expand Down
41 changes: 28 additions & 13 deletions _javascript/modules/components/toc.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
export function toc() {
if (document.querySelector('main h2, main h3')) {
// see: https://github.com/tscanlin/tocbot#usage
tocbot.init({
tocSelector: '#toc',
contentSelector: '.content',
ignoreSelector: '[data-toc-skip]',
headingSelector: 'h2, h3, h4',
orderedList: false,
scrollSmooth: false
});

document.getElementById('toc-wrapper').classList.remove('d-none');
import { TocMobile as mobile } from './toc/toc-mobile';
import { TocDesktop as desktop } from './toc/toc-desktop';

const desktopMode = matchMedia('(min-width: 1200px)');

function refresh(e) {
if (e.matches) {
mobile.hidePopup();
desktop.refresh();
} else {
mobile.refresh();
}
}

function init() {
if (document.querySelector('main>article[data-toc="true"]') === null) {
return;
}

// Avoid create multiple instances of Tocbot. Ref: <https://github.com/tscanlin/tocbot/issues/203>
if (desktopMode.matches) {
desktop.init();
} else {
mobile.init();
}

desktopMode.onchange = refresh;
}

export { init as initToc };
22 changes: 22 additions & 0 deletions _javascript/modules/components/toc/toc-desktop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class TocDesktop {
/* Tocbot options Ref: https://github.com/tscanlin/tocbot#usage */
static options = {
tocSelector: '#toc',
contentSelector: '.content',
ignoreSelector: '[data-toc-skip]',
headingSelector: 'h2, h3, h4',
orderedList: false,
scrollSmooth: false,
headingsOffset: 16 * 2 // 2rem
};

static refresh() {
tocbot.refresh(this.options);
}

static init() {
if (document.getElementById('toc-wrapper')) {
tocbot.init(this.options);
}
}
}
117 changes: 117 additions & 0 deletions _javascript/modules/components/toc/toc-mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* TOC button, topbar and popup for mobile devices
*/

const $tocBar = document.getElementById('toc-bar');
const $soloTrigger = document.getElementById('toc-solo-trigger');
const $triggers = document.getElementsByClassName('toc-trigger');
const $popup = document.getElementById('toc-popup');
const $btnClose = document.getElementById('toc-popup-close');

const SCROLL_LOCK = 'overflow-hidden';
const CLOSING = 'closing';

export class TocMobile {
static invisible = true;
static barHeight = 16 * 3; // 3rem

static options = {
tocSelector: '#toc-popup-content',
contentSelector: '.content',
ignoreSelector: '[data-toc-skip]',
headingSelector: 'h2, h3, h4',
orderedList: false,
scrollSmooth: false,
collapseDepth: 4,
headingsOffset: this.barHeight
};

static initBar() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
$tocBar.classList.toggle('invisible', entry.isIntersecting);
});
},
{ rootMargin: `-${this.barHeight}px 0px 0px 0px` }
);

observer.observe($soloTrigger);
this.invisible = false;
}

static listenAnchors() {
const $anchors = document.getElementsByClassName('toc-link');
[...$anchors].forEach((anchor) => {
anchor.onclick = this.hidePopup;
});
}

static refresh() {
if (this.invisible) {
this.initComponents();
}
tocbot.refresh(this.options);
this.listenAnchors();
}

static showPopup() {
TocMobile.lockScroll(true);
$popup.showModal();
const activeItem = $popup.querySelector('li.is-active-li');
activeItem.scrollIntoView({ block: 'center' });
}

static hidePopup() {
if (!$popup.open) {
return;
}

$popup.toggleAttribute(CLOSING);

$popup.addEventListener(
'animationend',
() => {
$popup.toggleAttribute(CLOSING);
$popup.close();
},
{ once: true }
);

TocMobile.lockScroll(false);
}

static lockScroll(enable) {
document.documentElement.classList.toggle(SCROLL_LOCK, enable);
document.body.classList.toggle(SCROLL_LOCK, enable);
}

static clickBackdrop(event) {
const rect = event.target.getBoundingClientRect();
if (
event.clientX < rect.left ||
event.clientX > rect.right ||
event.clientY < rect.top ||
event.clientY > rect.bottom
) {
TocMobile.hidePopup();
}
}

static initComponents() {
this.initBar();

[...$triggers].forEach((trigger) => {
trigger.onclick = this.showPopup;
});

$popup.onclick = this.clickBackdrop;
$btnClose.onclick = $popup.oncancel = this.hidePopup;
}

static init() {
tocbot.init(this.options);
this.listenAnchors();
this.initComponents();
}
}
2 changes: 1 addition & 1 deletion _javascript/modules/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export { initClipboard } from './components/clipboard';
export { loadImg } from './components/img-loading';
export { imgPopup } from './components/img-popup';
export { initLocaleDatetime } from './components/locale-datetime';
export { toc } from './components/toc';
export { initToc } from './components/toc';
7 changes: 4 additions & 3 deletions _javascript/post.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { basic, initSidebar, initTopbar } from './modules/layouts';
import { basic, initTopbar, initSidebar } from './modules/layouts';

import {
loadImg,
imgPopup,
initLocaleDatetime,
initClipboard,
toc
initToc
} from './modules/plugins';

loadImg();
toc();
initToc();
imgPopup();
initSidebar();
initLocaleDatetime();
Expand Down
28 changes: 27 additions & 1 deletion _layouts/post.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

{% include lang.html %}

<article class="px-1">
{% include toc-status.html %}

<article class="px-1" data-toc="{{ enable_toc }}">
<header>
<h1 data-toc-skip>{{ page.title }}</h1>
{% if page.description %}
Expand Down Expand Up @@ -95,6 +97,30 @@ <h1 data-toc-skip>{{ page.title }}</h1>
</div>
</header>

{% if enable_toc %}
<div id="toc-bar" class="d-flex align-items-center justify-content-between invisible">
<span class="label text-truncate">{{ page.title }}</span>
<button type="button" class="toc-trigger btn btn-link me-1">
<i class="fa-solid fa-list-ul fa-fw"></i>
</button>
</div>

<button id="toc-solo-trigger" type="button" class="toc-trigger btn btn-outline-secondary btn-sm">
<span class="label ps-2 pe-1">{{- site.data.locales[lang].panel.toc -}}</span>
<i class="fa-solid fa-angle-right fa-fw"></i>
</button>

<dialog id="toc-popup" class="p-0">
<div class="header d-flex flex-row align-items-center justify-content-between">
<div class="label text-truncate py-2 ms-4">{{- page.title -}}</div>
<button id="toc-popup-close" type="button" class="btn btn-link">
<i class="fas fa-close fa-fw"></i>
</button>
</div>
<div id="toc-popup-content" class="px-4 py-3 pb-4"></div>
</dialog>
{% endif %}

<div class="content">
{{ content }}
</div>
Expand Down
4 changes: 1 addition & 3 deletions _sass/addon/commons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -908,9 +908,7 @@ $btn-mb: 0.5rem;
}

#topbar {
button i {
color: #999999;
}
@extend %btn-color;

#breadcrumb {
font-size: 1rem;
Expand Down
7 changes: 7 additions & 0 deletions _sass/addon/module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
color: var(--heading-color);
font-weight: 400;
font-family: $font-family-heading;
scroll-margin-top: 3.5rem;
}

%anchor {
Expand Down Expand Up @@ -134,6 +135,12 @@
}
}

%btn-color {
button i {
color: #999999;
}
}

/* ---------- scss mixin --------- */

@mixin mt-mb($value) {
Expand Down
2 changes: 1 addition & 1 deletion _sass/colors/typography-dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
--btn-border-color: #2e2f31;
--btn-backtotop-color: var(--text-color);
--btn-backtotop-border-color: #212122;
--btn-box-shadow: var(--main-bg);
--card-header-bg: #292929;
--checkbox-color: rgb(118, 120, 121);
--checkbox-checked-color: var(--link-color);
Expand Down Expand Up @@ -60,6 +59,7 @@

/* Posts */
--toc-highlight: rgb(116, 178, 243);
--toc-popup-border-color: #373737;
--tag-hover: rgb(43, 56, 62);
--tb-odd-bg: #252526; /* odd rows of the posts' table */
--tb-even-bg: rgb(31, 31, 34); /* even rows of the posts' table */
Expand Down
2 changes: 1 addition & 1 deletion _sass/colors/typography-light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
--btn-border-color: #e9ecef;
--btn-backtotop-color: #686868;
--btn-backtotop-border-color: #f1f1f1;
--btn-box-shadow: #eaeaea;
--checkbox-color: #c5c5c5;
--checkbox-checked-color: #07a8f7;
--img-bg: radial-gradient(
Expand Down Expand Up @@ -63,6 +62,7 @@

/* Posts */
--toc-highlight: #0550ae;
--toc-popup-border-color: lightgray;
--btn-share-color: gray;
--btn-share-hover-color: #0d6efd;
--card-bg: white;
Expand Down
Loading

0 comments on commit 8a064a5

Please sign in to comment.