mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 15:28:57 +00:00
feat: implement TOC drawer feature (#657)
* feat: add toc drawer button * feat: add TOC dialog functionality and related styles * feat: change back-to-top button with scroll progress indicator * feat: update color-scheme handling in theme switcher * fix: code review for checking mistakes and optimization * fix: the issue where the TOC was still clickable even when it was hidden.
This commit is contained in:
@@ -63,6 +63,10 @@ img,video {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
details {
|
||||
&.center {
|
||||
summary {
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 961px) {
|
||||
#toc-drawer-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
%page-style {
|
||||
width: ROUND(80%, 2px) !important;
|
||||
|
||||
@@ -218,14 +218,14 @@
|
||||
|
||||
&::before {
|
||||
color: $single-link-hover-color;
|
||||
|
||||
|
||||
[data-theme='dark'] & {
|
||||
color: $single-link-hover-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> ol {
|
||||
margin: 0;
|
||||
padding: 0.4em 1em 0.4em 2.25em;
|
||||
@@ -252,3 +252,72 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#toc-dialog {
|
||||
transition:
|
||||
display 0.2s allow-discrete,
|
||||
overlay 0.2s allow-discrete,
|
||||
translate 0.2s,
|
||||
opacity 0.2s 0.4s;
|
||||
opacity: 0;
|
||||
translate: 100vw 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin-left: 4rem;
|
||||
border: none;
|
||||
|
||||
.toc {
|
||||
max-width: 100%;
|
||||
margin-inline: 1rem;
|
||||
|
||||
.toc-title {
|
||||
font-size: $toc-title-font-size * 1.5;
|
||||
line-height: $toc-title-font-size * 2;
|
||||
margin-block: $toc-title-font-size;
|
||||
}
|
||||
|
||||
.toc-content {
|
||||
font-size: max($toc-content-font-size, 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
&::backdrop {
|
||||
transition:
|
||||
display 0.5s allow-discrete,
|
||||
overlay 0.5s allow-discrete,
|
||||
opacity 0.2s 0.4s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&[open],
|
||||
&[open]::backdrop {
|
||||
opacity: 1;
|
||||
transition:
|
||||
display 0.2s allow-discrete,
|
||||
overlay 0.2s allow-discrete,
|
||||
translate 0.2s,
|
||||
opacity 0.2s;
|
||||
}
|
||||
|
||||
&[open] {
|
||||
translate: 0 0;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
|
||||
&[open],
|
||||
&[open]::backdrop {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&[open] {
|
||||
translate: 100vw 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html:has(#toc-dialog[open]) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
.fixed-button {
|
||||
.fixed-buttons {
|
||||
position: fixed;
|
||||
right: 1.5rem;
|
||||
bottom: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
@include z-index(1);
|
||||
}
|
||||
|
||||
.fixed-button {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
color: $global-font-secondary-color;
|
||||
color: $global-font-color;
|
||||
background-color: $header-background-color;
|
||||
@include z-index(1);
|
||||
@include border-radius(1.25rem);
|
||||
@include transition(color 0.4s ease);
|
||||
@include blur;
|
||||
@@ -25,7 +32,7 @@
|
||||
}
|
||||
|
||||
[data-theme='dark'] & {
|
||||
color: $global-font-secondary-color-dark;
|
||||
color: $global-font-color-dark;
|
||||
background-color: $header-background-color-dark;
|
||||
|
||||
&:hover,
|
||||
@@ -36,9 +43,47 @@
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
// Ring stroke width
|
||||
--b2t-stroke: 0.5rem;
|
||||
// Ring background color
|
||||
--b2t-bg: #{$global-border-color};
|
||||
// Progress color
|
||||
--b2t-prog: #FF7359;
|
||||
// 2π×50 ≈ 314 (radius 50 for viewBox 100)
|
||||
--b2t-circumference: 314;
|
||||
|
||||
.view-comments {
|
||||
bottom: 4.5rem;
|
||||
[data-theme='dark'] & {
|
||||
--b2t-bg: #{$global-border-color-dark};
|
||||
--b2t-prog: #{$global-font-color-dark};
|
||||
}
|
||||
|
||||
// SVG circular progress ring
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// Rotate to start from top
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.bg {
|
||||
fill: none;
|
||||
stroke: var(--b2t-bg);
|
||||
stroke-width: var(--b2t-stroke);
|
||||
}
|
||||
|
||||
// Progress ring
|
||||
.progress {
|
||||
fill: none;
|
||||
stroke: var(--b2t-prog);
|
||||
stroke-width: var(--b2t-stroke);
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: var(--b2t-circumference);
|
||||
stroke-dashoffset: calc(var(--b2t-circumference) - var(--b2t-circumference) * var(--scroll-percent, 0) / 100);
|
||||
transition: stroke-dashoffset .1s linear;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ import params from '@params';
|
||||
params.defaultTheme === 'dark';
|
||||
}
|
||||
isDark && (document.documentElement.dataset.theme = 'dark');
|
||||
document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
|
||||
})();
|
||||
|
||||
+42
-13
@@ -91,6 +91,7 @@ class FixIt {
|
||||
this.util.forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => {
|
||||
$themeSwitch.addEventListener('click', () => {
|
||||
document.documentElement.dataset.theme = this.isDark ? 'light' : 'dark';
|
||||
document.documentElement.style.setProperty('color-scheme', this.isDark ? 'light' : 'dark');
|
||||
this.isDark = !this.isDark;
|
||||
window.localStorage?.setItem('theme', this.isDark ? 'dark' : 'light');
|
||||
for (let event of this.switchThemeEventSet) {
|
||||
@@ -650,12 +651,40 @@ class FixIt {
|
||||
const animation = ['animate__faster'];
|
||||
const tocHidden = $toc.classList.contains('toc-hidden');
|
||||
animation.push(tocHidden ? 'animate__fadeIn' : 'animate__fadeOut');
|
||||
$tocContentAuto.classList.remove(tocHidden ? 'animate__fadeOut' : 'animate__fadeIn');
|
||||
this.util.animateCSS($tocContentAuto, animation, true);
|
||||
if (tocHidden) {
|
||||
$tocContentAuto.classList.remove('d-none', 'animate__fadeOut');
|
||||
} else {
|
||||
$tocContentAuto.classList.remove('animate__fadeIn');
|
||||
}
|
||||
this.util.animateCSS($tocContentAuto, animation, true, () => {
|
||||
$tocContentAuto.classList.contains('animate__fadeOut') && $tocContentAuto.classList.add('d-none');
|
||||
});
|
||||
$toc.classList.toggle('toc-hidden');
|
||||
}, false);
|
||||
}
|
||||
|
||||
initTocDialog() {
|
||||
// HTMLDialogElement
|
||||
const dialog = document.querySelector("#toc-dialog");
|
||||
const openButton = document.querySelector("#toc-drawer-button");
|
||||
if (!dialog || !openButton) {
|
||||
return;
|
||||
}
|
||||
openButton.addEventListener("click", () => {
|
||||
dialog.showModal();
|
||||
document.activeElement?.blur();
|
||||
});
|
||||
dialog.addEventListener("click", (e) => {
|
||||
dialog.close();
|
||||
});
|
||||
dialog.addEventListener("keydown", (e) => {
|
||||
// ensure Escape key closes the dialog (for robustness)
|
||||
if (e.key === "Escape" && dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* It's a dirty hack to fix the bug of APlayer and smoothScroll.
|
||||
* see https://github.com/hugo-fixit/FixIt/issues/292
|
||||
@@ -1134,6 +1163,7 @@ class FixIt {
|
||||
this.fixTocScroll();
|
||||
this.initToc();
|
||||
this.initTocListener();
|
||||
this.initTocDialog();
|
||||
}
|
||||
this.initPangu();
|
||||
this.initMathJax();
|
||||
@@ -1237,7 +1267,6 @@ class FixIt {
|
||||
onScroll() {
|
||||
const $headers = [];
|
||||
const ACCURACY = 20;
|
||||
const $fixedButtons = document.querySelector('.fixed-buttons');
|
||||
const $backToTop = document.querySelector('.back-to-top');
|
||||
const $readingProgressBar = document.querySelector('.reading-progress-bar');
|
||||
if (document.body.dataset.headerDesktop === 'auto') {
|
||||
@@ -1275,20 +1304,19 @@ class FixIt {
|
||||
if ($readingProgressBar) {
|
||||
$readingProgressBar.style.setProperty('--progress', `${scrollPercent.toFixed(2)}%`);
|
||||
}
|
||||
// whether to show fixed buttons
|
||||
if ($fixedButtons) {
|
||||
// whether to show back to top button
|
||||
if ($backToTop) {
|
||||
if (scrollPercent > 1) {
|
||||
$fixedButtons.classList.remove('d-none', 'animate__fadeOut');
|
||||
this.util.animateCSS($fixedButtons, ['animate__fadeIn'], true);
|
||||
$backToTop.classList.remove('d-none', 'animate__fadeOut');
|
||||
this.util.animateCSS($backToTop, ['animate__fadeIn'], true);
|
||||
} else {
|
||||
$fixedButtons.classList.remove('animate__fadeIn');
|
||||
this.util.animateCSS($fixedButtons, ['animate__fadeOut'], true, () => {
|
||||
$fixedButtons.classList.contains('animate__fadeOut') && $fixedButtons.classList.add('d-none');
|
||||
$backToTop.classList.remove('animate__fadeIn');
|
||||
this.util.animateCSS($backToTop, ['animate__fadeOut'], true, () => {
|
||||
$backToTop.classList.contains('animate__fadeOut') && $backToTop.classList.add('d-none');
|
||||
});
|
||||
}
|
||||
if ($backToTop) {
|
||||
$backToTop.querySelector('span').innerText = `${Math.round(scrollPercent)}%`;
|
||||
}
|
||||
// [todo] Shares the scrollPercent variable with readingProgressBar
|
||||
$backToTop.style.setProperty('--scroll-percent', scrollPercent.toFixed(2));
|
||||
}
|
||||
for (let event of this.scrollEventSet) {
|
||||
event();
|
||||
@@ -1372,6 +1400,7 @@ class FixIt {
|
||||
this.fixTocScroll();
|
||||
this.initToc();
|
||||
this.initTocListener();
|
||||
this.initTocDialog();
|
||||
}
|
||||
this.onScroll();
|
||||
this.onResize();
|
||||
|
||||
@@ -988,7 +988,7 @@ style = "" # ["", "mp", "identicon", "monsterid", "wavatar", "retro", "blank", "
|
||||
# FixIt 0.2.16 | NEW Back to top
|
||||
[params.backToTop]
|
||||
enable = true
|
||||
# Scroll percent label in b2t button
|
||||
# FixIt 0.4.0 | CHANGED whether to show the scroll percent indicator around the back to top button
|
||||
scrollpercent = false
|
||||
|
||||
# FixIt 0.2.16 | NEW Reading progress bar
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
<div class="widgets">
|
||||
{{- $backToTop := .Site.Params.backToTop -}}
|
||||
{{- $params := partial "function/params.html" -}}
|
||||
{{- $comment := .Store.Get "comment" -}}
|
||||
{{- if $backToTop.enable | or $comment.enable -}}
|
||||
<div class="fixed-buttons animate__faster d-none">
|
||||
<div class="fixed-buttons">
|
||||
{{- /* back to top button */ -}}
|
||||
{{- if $backToTop.enable -}}
|
||||
<div class="fixed-button back-to-top" role="button" aria-label="{{ T `baseof.backToTop` }}">
|
||||
<div class="fixed-button back-to-top animate__faster" role="button" aria-label="{{ T `baseof.backToTop` }}">
|
||||
{{- dict "Class" "fa-solid fa-arrow-up fa-fw" | partial "plugin/icon.html" -}}
|
||||
<span class="variant-numeric{{ if not $backToTop.scrollpercent }} d-none{{ end }}">0%</span>
|
||||
{{- if $backToTop.scrollpercent -}}
|
||||
<svg viewBox="0 0 100 100">
|
||||
<circle class="bg" cx="50" cy="50" r="50"></circle>
|
||||
<circle class="progress" cx="50" cy="50" r="50"></circle>
|
||||
</svg>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- /* toc dialog button */ -}}
|
||||
{{- if .Page.Store.Get "enableTocDialog" -}}
|
||||
<div id="toc-drawer-button" class="fixed-button toc-drawer-button{{ with $params.password }} encrypted-hidden{{ end }}" role="button" aria-label="{{ T `single.contents` }}">
|
||||
{{- dict "Class" "fa-solid fa-bars fa-fw" | partial "plugin/icon.html" -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- /* comment button */ -}}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
{{- $toc := .Store.Get "toc" -}}
|
||||
{{- $tableOfContents := .Fragments.ToHTML ($toc.startlevel | int) ($toc.endlevel | int) ($toc.ordered | default false) -}}
|
||||
{{- $showToc := $toc.enable | and (ne $tableOfContents `<nav id="TableOfContents"></nav>`) -}}
|
||||
{{- $tableOfContents = dict "Content" $tableOfContents "Ruby" $params.ruby "Fraction" $params.fraction "Fontawesome" $params.fontawesome | partial "function/content.html" | safeHTML -}}
|
||||
|
||||
<aside class="aside-collection animate__animated animate__fadeIn animate__faster" aria-label="{{ T "collections" }}">
|
||||
{{- /* Collection List */ -}}
|
||||
@@ -162,7 +163,7 @@
|
||||
<span>{{ dict "Class" "details-icon fa-solid fa-angle-right" | partial "plugin/icon.html" }}</span>
|
||||
</div>
|
||||
<div class="details-content toc-content" id="toc-content-static">
|
||||
{{- dict "Content" $tableOfContents "Ruby" $params.ruby "Fraction" $params.fraction "Fontawesome" $params.fontawesome | partial "function/content.html" | safeHTML -}}
|
||||
{{- $tableOfContents -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
@@ -232,4 +233,17 @@
|
||||
{{- /* Custom block after post toc */ -}}
|
||||
{{- block "custom-post__toc:after" . }}{{ end -}}
|
||||
</aside>
|
||||
|
||||
{{- /* TOC Dialog */ -}}
|
||||
{{- if $showToc -}}
|
||||
{{- .Page.Store.Set "enableTocDialog" true -}}
|
||||
<dialog id="toc-dialog" aria-labelledby="toc-dialog-title" role="dialog">
|
||||
<div class="toc">
|
||||
<h2 class="toc-title">{{ T "single.contents" }}</h2>
|
||||
<div class="toc-content" id="toc-content-drawer">
|
||||
{{- replace $tableOfContents `id="TableOfContents"` "" | safeHTML -}}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
Reference in New Issue
Block a user