diff options
author | lberki <lberki@users.noreply.github.com> | 2022-05-05 12:08:42 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-05 12:08:42 +0300 |
commit | c95051b93d756d5653ebe36a8f5f2d556b3843a0 (patch) | |
tree | 451aa9a3af1e363fbe239ceef35b9e58ba388d15 | |
parent | 93a5ae3a16c5654414af8c0c83699b165a2d2bcf (diff) |
Add table of contents
Add table of contents
See #64
-rw-r--r-- | assets/scss/tale.scss | 1 | ||||
-rw-r--r-- | assets/scss/tale/_code.scss | 4 | ||||
-rw-r--r-- | assets/scss/tale/_layout.scss | 6 | ||||
-rw-r--r-- | assets/scss/tale/_toc.scss | 89 | ||||
-rw-r--r-- | assets/scss/tale/_variables.scss | 3 | ||||
-rw-r--r-- | layouts/_default/single.html | 4 | ||||
-rw-r--r-- | layouts/partials/toc.html | 13 | ||||
-rw-r--r-- | static/js/toc.js | 63 |
8 files changed, 178 insertions, 5 deletions
diff --git a/assets/scss/tale.scss b/assets/scss/tale.scss index 9d1c12f..53f9dec 100644 --- a/assets/scss/tale.scss +++ b/assets/scss/tale.scss @@ -4,6 +4,7 @@ @import 'tale/post'; @import 'tale/syntax'; @import 'tale/layout'; +@import 'tale/toc'; @import 'tale/pagination'; @import 'tale/catalogue'; @import 'tale/disqus'; diff --git a/assets/scss/tale/_code.scss b/assets/scss/tale/_code.scss index 8d3db57..11f1c53 100644 --- a/assets/scss/tale/_code.scss +++ b/assets/scss/tale/_code.scss @@ -4,7 +4,7 @@ code { } code { - background-color: $grey-3; + background-color: $grey-4; border-radius: 3px; color: $code-color; font-size: 85%; @@ -24,7 +24,7 @@ pre code { } .highlight { - background-color: $grey-3; + background-color: $grey-4; border-radius: 3px; line-height: 1.4; margin: 0 0 1rem; diff --git a/assets/scss/tale/_layout.scss b/assets/scss/tale/_layout.scss index ba4a7b0..8d56834 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -4,7 +4,11 @@ width: 80%; } -main, +// Carve out an exception from the 80% narrowing of the viewport for the table +// of contents. This is ugly, but there isn't a better way, since the table of +// contents must be a child of the "main" element so that it its scrolling +// behavior is intuitive. +main > *, footer, .nav-container { display: block; diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss new file mode 100644 index 0000000..6492ba7 --- /dev/null +++ b/assets/scss/tale/_toc.scss @@ -0,0 +1,89 @@ +aside.toc { + position: sticky; + top: 0; + max-width: 30%; + + // We want the table of contents to be on the left (horizontally), to be + // positioned at a specific point (vertically), stick to the top on scroll + // and it should not affect the layout of other elements. + // "float: left" accomplishes the horizontal positioning, position in the + // element tree the vertical positioning and "position: sticky" does the + // stickiness. "height: 0" makes sure that the flow of other elements is not + // affected. + // + // There does not seem to be a better way to do this unless one is willing + // to implement scrolling in JS: "position" must be set to sticky so neither + // fixed, nor absolute, nor relative positioning can be used. + float: left; + height: 0; + overflow: display; +} + +#tocTitle { + // This is so that we can measure it in JS + width: fit-content; +} + +#tocContainer:hover { + width: calc(2rem + var(--measured-expanded-width)); +} + +#tocContainer:hover div#tocCollapsible { + height: var(--measured-height); + width: var(--measured-expanded-width); +} + +#tocContainer { + background: $grey-3; + border-radius: 1rem; + margin: 2rem; + padding: 1rem; + + // This makes the gap between the two contained divs vanish. Why that gap + // exists, no clue. + display: flex; + flex-direction: column; + + // We want to hide the table of contents before revealing it on hover + overflow: hidden; + + // In addition to the measured width of the title, we need to add the two + // rems for the border (we are using box-sizing: border-box). Also add a + // reasonable default value to minimize visual changes while the page is + // loading. + width: calc(2rem + var(--measured-title-width)); + --measured-title-width: 2.4rem; + + @include transition(all .1s ease-out); +} + +#tocContainer > div { + border-left: 0.4rem solid black; + padding-left: 1rem; +} + +#tocContainer div#tocCollapsible { + // Collapsed by default + height: 0; + + // If we did not force this element to a given width, it would keep + // re-wrapping during the opening/closing transition. + width: var(--measured-expanded-width); + + // No transition on width so that there is no re-wrapping during the + // opening/closing transition + @include transition(height .1s ease-out); +} + +nav#TableOfContents ul { + list-style-type: none; + padding-inline-start: 1rem; +} + +nav#TableOfContents > ul { + padding-inline-start: 0; +} + +nav#TableOfContents li { + margin-top: 0.4rem; +} diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 8a064f3..92d1114 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -4,7 +4,8 @@ $default-shade: #353535; $default-tint: #aaa; $grey-1: #979797; $grey-2: #e5e5e5; -$grey-3: #f9f9f9; +$grey-3: #f0f0f0; +$grey-4: #f9f9f9; $white: #fff; $blue: #4a9ae1; $shadow-color: rgba(0, 0, 0, .2); diff --git a/layouts/_default/single.html b/layouts/_default/single.html index dba8344..f5f6f6e 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -1,7 +1,9 @@ {{ define "main" }} <main> - <div class="post"> + {{ partial "toc.html" . }} + + <div class="post"> {{ partial "single/post-info.html" . }} {{ partial "single/title.html" . }} diff --git a/layouts/partials/toc.html b/layouts/partials/toc.html new file mode 100644 index 0000000..81d524a --- /dev/null +++ b/layouts/partials/toc.html @@ -0,0 +1,13 @@ +{{ if .Params.toc }} +<aside class="toc"> + <script src="{{ "js/toc.js" | relURL }}"></script> + <div id="tocContainer"> + <div id="tocTitle"> + ☰ <!-- Trigram for heaven, aka. hamburger menu --> + </div> + <div id="tocCollapsible"> + {{ .TableOfContents }} + </div> + </div> +</aside> +{{ end }} diff --git a/static/js/toc.js b/static/js/toc.js new file mode 100644 index 0000000..7229c04 --- /dev/null +++ b/static/js/toc.js @@ -0,0 +1,63 @@ +"use strict"; + +let oldWidth = -1, oldHeight = -1; + +function measureToc() { + if (window.innerWidth === oldWidth && window.innerHeight === oldHeight) { + // In addition to being a bit of optimization, this clause somehow prevents triggering a + // bug in Firefox 98.2.0 on Android that makes the stickiness of the table of contents + // wonky after scrolling to the bottom, then scrolling up. + return; + } + + oldWidth = window.innerWidth; + oldHeight = window.innerHeight; + + let tocContainer = document.getElementById("tocContainer"); + let tocTitle = document.getElementById("tocTitle"); + let tocCollapsible = document.getElementById("tocCollapsible"); + + // Set relevant elements to automatic sizing. + tocContainer.style.width = "fit-content"; + tocCollapsible.style.width = "fit-content"; + tocCollapsible.style.height = "fit-content"; + + // Apparently, this call is necessary so that the CSS properties set above take effect. + requestAnimationFrame(() => { + // These properties apparently round to the nearest integer but rounding down would make + // them wrap text differently than when autosizing. Add a pixel because it's better to wrap + // too little than to wrap too much, + let titleWidth = tocTitle.offsetWidth + 1; + let collapsibleWidth = tocCollapsible.offsetWidth + 1; + let collapsibleHeight = tocCollapsible.offsetHeight + 1; + + // Make sure the TOC width cannot shrink under that of the title. Sadly, + // calc() does not have min() / max() operators. + let clampedCollapsibleWidth = Math.max(collapsibleWidth, titleWidth); + + tocContainer.style.setProperty("--measured-title-width", titleWidth + "px"); + tocContainer.style.setProperty("--measured-expanded-width", clampedCollapsibleWidth + "px"); + tocCollapsible.style.setProperty("--measured-expanded-width", clampedCollapsibleWidth + "px"); + tocCollapsible.style.setProperty("--measured-height", collapsibleHeight + "px"); + + tocContainer.style.removeProperty("width"); + tocCollapsible.style.removeProperty("width"); + tocCollapsible.style.removeProperty("height"); + }); +} + +let resizeTimeout = null; + +function measureTocAfterResize() { + // Chrome sometimes does not finish layout when the resize event handler is called, so wait a + // bit before recalculating sizes. This does not usually result in weird visual effects because + // it's really hard to resize a window while hovering over the table of contents so we can + // assume that it's closed when the measurement is running. + if (resizeTimeout != null) { + clearTimeout(resizeTimeout); + } + resizeTimeout = setTimeout(measureToc, 200); +} + +window.addEventListener("load", measureToc); +window.addEventListener("resize", measureTocAfterResize); |