diff options
author | Fabian Tamp <fabian.tamp@gmail.com> | 2020-01-27 03:12:49 +0300 |
---|---|---|
committer | Fabian Tamp <fabian.tamp@gmail.com> | 2020-01-27 03:12:49 +0300 |
commit | de3eae439847ac6b4ea00cade2e4bb6393f36d81 (patch) | |
tree | 2dbc98d8b4eee70ed6a0b59a1c6e0167fbaa985b /js | |
parent | 175fab5477aa833e43472a0c4a9134f495f63960 (diff) |
Updates from capnfabs.github.io repo.
Diffstat (limited to 'js')
-rw-r--r-- | js/.jshintrc | 3 | ||||
-rw-r--r-- | js/anchorizeHeadings.js | 33 | ||||
-rw-r--r-- | js/floatingFootnotes.js | 123 | ||||
-rw-r--r-- | js/main.js | 5 | ||||
-rw-r--r-- | js/utils.js | 29 |
5 files changed, 193 insertions, 0 deletions
diff --git a/js/.jshintrc b/js/.jshintrc new file mode 100644 index 0000000..f165e8b --- /dev/null +++ b/js/.jshintrc @@ -0,0 +1,3 @@ +{ +"esversion": 6 +} diff --git a/js/anchorizeHeadings.js b/js/anchorizeHeadings.js new file mode 100644 index 0000000..cd3b8b6 --- /dev/null +++ b/js/anchorizeHeadings.js @@ -0,0 +1,33 @@ +import { docReady } from "./utils.js"; + +// Borrowed from https://github.com/gohugoio/gohugoioTheme/blob/2e7250ca437d4666329d3ca96708dd3a4ff59818/assets/js/anchorforid.js +function anchorForId(id) { + const anchor = document.createElement("a"); + anchor.className = "header-link"; + anchor.title = "Link to this section"; + anchor.href = "#" + id; + // Icon from https://useiconic.com/open#icons + anchor.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><path d="M5.88.03c-.18.01-.36.03-.53.09-.27.1-.53.25-.75.47a.5.5 0 1 0 .69.69c.11-.11.24-.17.38-.22.35-.12.78-.07 1.06.22.39.39.39 1.04 0 1.44l-1.5 1.5c-.44.44-.8.48-1.06.47-.26-.01-.41-.13-.41-.13a.5.5 0 1 0-.5.88s.34.22.84.25c.5.03 1.2-.16 1.81-.78l1.5-1.5c.78-.78.78-2.04 0-2.81-.28-.28-.61-.45-.97-.53-.18-.04-.38-.04-.56-.03zm-2 2.31c-.5-.02-1.19.15-1.78.75l-1.5 1.5c-.78.78-.78 2.04 0 2.81.56.56 1.36.72 2.06.47.27-.1.53-.25.75-.47a.5.5 0 1 0-.69-.69c-.11.11-.24.17-.38.22-.35.12-.78.07-1.06-.22-.39-.39-.39-1.04 0-1.44l1.5-1.5c.4-.4.75-.45 1.03-.44.28.01.47.09.47.09a.5.5 0 1 0 .44-.88s-.34-.2-.84-.22z" /></svg>'; + return anchor; +} + +function anchorizeHeadings() { + // If we've found more than 1 article, then abort. It probably means I've + // messed something up if this is the case, but I don't have enough + // confidence in the way I've set everything up to _not_ do this safety + // check. + const articles = document.querySelectorAll('section#main article'); + if (articles.length != 1) { + return; + } + // Keep this list of header classes in sync with style.css + const headers = articles[0].querySelectorAll('h2, h3, h4'); + Array.prototype.forEach.call(headers, function (el, i) { + var link = anchorForId(el.id); + el.appendChild(link); + }); +} + +export default function anchorizeOnReady() { + docReady(anchorizeHeadings); +} diff --git a/js/floatingFootnotes.js b/js/floatingFootnotes.js new file mode 100644 index 0000000..dcd8fe1 --- /dev/null +++ b/js/floatingFootnotes.js @@ -0,0 +1,123 @@ +import { docReady, onWindowResize } from "./utils.js"; +import { ResizeObserver } from '@juggle/resize-observer'; + +const ARTICLE_CONTENT_SELECTOR = "section#main"; +const FOOTNOTE_SECTION_SELECTOR = "section.footnotes[role=doc-endnotes]"; +const FLOATING_FOOTNOTE_MIN_WIDTH = 1260; + +// Computes an offset such that setting `top` on elemToAlign will put it +// in vertical alignment with targetAlignment. +function computeOffsetForAlignment(elemToAlign, targetAlignment) { + const offsetParentTop = elemToAlign.offsetParent.getBoundingClientRect().top; + // Distance between the top of the offset parent and the top of the target alignment + return targetAlignment.getBoundingClientRect().top - offsetParentTop; +} + +function setFootnoteOffsets(footnotes) { + // Keep track of the bottom of the last element, because we don't want to + // overlap footnotes. + let bottomOfLastElem = 0; + Array.prototype.forEach.call(footnotes, function (footnote, i) { + + // In theory, don't need to escape this because IDs can't contain + // quotes, in practice, not sure. ¯\_(ツ)_/¯ + + // Get the thing that refers to the footnote + const intextLink = document.querySelector("a.footnote-ref[href='#" + footnote.id + "']"); + // Find its "content parent"; nearest paragraph or list item or + // whatever. We use this for alignment because it looks much cleaner. + // If it doesn't, your paragraphs are too long :P + // Fallback - use the same height as the link. + const verticalAlignmentTarget = intextLink.closest('p,li') || intextLink; + + let offset = computeOffsetForAlignment(footnote, verticalAlignmentTarget); + if (offset < bottomOfLastElem) { + offset = bottomOfLastElem; + } + // computedStyle values are always in pixels, but have the suffix 'px'. + // offsetHeight doesn't include margins, but we want it to use them so + // we retain the style / visual fidelity when all the footnotes are + // crammed together. + bottomOfLastElem = + offset + + footnote.offsetHeight + + parseInt(window.getComputedStyle(footnote).marginBottom) + + parseInt(window.getComputedStyle(footnote).marginTop); + + footnote.style.top = offset + 'px'; + footnote.style.position = 'absolute'; + }); +} + +function clearFootnoteOffsets(footnotes) { + // Reset all + Array.prototype.forEach.call(footnotes, function (fn, i) { + fn.style.top = null; + fn.style.position = null; + }); +} + +// contract: this is idempotent; i.e. it won't wreck anything if you call it +// with the same value over and over again. Though maybe it'll wreck performance +// lol. +function updateFootnoteFloat(shouldFloat) { + const footnoteSection = document.querySelector(FOOTNOTE_SECTION_SELECTOR); + const footnotes = footnoteSection.querySelectorAll( + "li[role=doc-endnote]"); + + if (shouldFloat) { + // Do this first because we need styles applied before doing other + // calculations + footnoteSection.classList.add('floating-footnotes'); + setFootnoteOffsets(footnotes); + subscribeToUpdates(); + } else { + unsubscribeFromUpdates(); + clearFootnoteOffsets(footnotes); + footnoteSection.classList.remove('floating-footnotes'); + } +} + +function subscribeToUpdates() { + const article = document.querySelector(ARTICLE_CONTENT_SELECTOR); + // Watch for dimension changes on the thing that holds all the footnotes so + // we can reposition as required + resizeObserver.observe(article); +} + +function unsubscribeFromUpdates() { + resizeObserver.disconnect(); +} + +const notifySizeChange = function() { + // Default state, not expanded. + let bigEnough = false; + + return function () { + // Pixel width at which this looks good + let nowBigEnough = window.innerWidth >= FLOATING_FOOTNOTE_MIN_WIDTH; + if (nowBigEnough !== bigEnough) { + updateFootnoteFloat(nowBigEnough); + bigEnough = nowBigEnough; + } + }; +}(); + +const resizeObserver = new ResizeObserver((_entries, observer) => { + // By virtue of the fact that we're subscribed, we know this is true. + updateFootnoteFloat(true); +}); + +export default function enableFloatingFootnotes() { + docReady(() => { + const footnoteSection = document.querySelector(FOOTNOTE_SECTION_SELECTOR); + const article = document.querySelector(ARTICLE_CONTENT_SELECTOR); + const allowFloatingFootnotes = article && !article.classList.contains('no-floating-footnotes'); + + // only set it all up if there's actually a footnote section and + // we haven't explicitly disabled floating footnotes. + if (footnoteSection && allowFloatingFootnotes) { + onWindowResize(notifySizeChange); + } + }); +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..25bfcd2 --- /dev/null +++ b/js/main.js @@ -0,0 +1,5 @@ +import anchorizeHeadings from "./anchorizeHeadings.js"; +import enableFloatingFootnotes from "./floatingFootnotes.js"; + +enableFloatingFootnotes(); +anchorizeHeadings(); diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..d48d708 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,29 @@ +// borrowed from https://stackoverflow.com/questions/9899372/pure-javascript-equivalent-of-jquerys-ready-how-to-call-a-function-when-t +function docReady(fn) { + // see if DOM is already available + if (document.readyState === "complete" || document.readyState === "interactive") { + // call on next available tick + setTimeout(fn, 1); + } else { + document.addEventListener("DOMContentLoaded", fn); + } +} + +function windowLoaded(fn) { + // see if we're already loaded + if (document.readyState === "complete") { + // call on next available tick + setTimeout(fn, 1); + } else { + window.addEventListener("load", fn); + } +} + +function onWindowResize(fn) { + windowLoaded(function () { + window.addEventListener('resize', fn); + setTimeout(fn, 1); + }); +} + +export { docReady, windowLoaded, onWindowResize}; |