Skip to content

Commit

Permalink
Add table of contents
Browse files Browse the repository at this point in the history
Add table of contents

See EmielH#64
  • Loading branch information
lberki authored May 5, 2022
1 parent b9b4ec1 commit 551ff44
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 5 deletions.
1 change: 1 addition & 0 deletions assets/scss/tale.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions assets/scss/tale/_code.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ code {
}

code {
background-color: $grey-3;
background-color: $grey-4;
border-radius: 3px;
color: $code-color;
font-size: 85%;
Expand All @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion assets/scss/tale/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
89 changes: 89 additions & 0 deletions assets/scss/tale/_toc.scss
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 2 additions & 1 deletion assets/scss/tale/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion layouts/_default/single.html
Original file line number Diff line number Diff line change
@@ -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" . }}

Expand Down
13 changes: 13 additions & 0 deletions layouts/partials/toc.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{ if .Params.toc }}
<aside class="toc">
<script src="{{ "js/toc.js" | relURL }}"></script>
<div id="tocContainer">
<div id="tocTitle">
&#9776; <!-- Trigram for heaven, aka. hamburger menu -->
</div>
<div id="tocCollapsible">
{{ .TableOfContents }}
</div>
</div>
</aside>
{{ end }}
63 changes: 63 additions & 0 deletions static/js/toc.js
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit 551ff44

Please sign in to comment.