From 0c5ccbf1ccaee9df874196d428ca84c43593c9c0 Mon Sep 17 00:00:00 2001 From: Dan Allen Date: Tue, 31 Mar 2020 04:38:55 -0600 Subject: [PATCH] add styles for TOC and integrate TOC into page layout - use flexbox to layout TOC - add scroll spy behavior to sidebar TOC - introduce additional breakpoint for sidebar TOC width (desktop and widescreen) - activate last entry in toc if scroll has reached bottom of page - introduce variables for toc (top, width, etc) - add utility class to hide element when in TOC - look for -toc class on body; rename before-toc class to is-before-toc - use article top as threshold for activating entry in TOC - disable scrollbar on toc in Firefox - scroll list instead of menu - hide scrollbar on TOC - don't add embedded toc if element is found with ID "toc" --- src/css/doc.css | 1 + src/css/main.css | 24 ++++++++++++ src/css/site.css | 1 + src/css/toc.css | 95 +++++++++++++++++++++++++++++++++++++++++++++++ src/css/vars.css | 11 +++++- src/js/02-on-this-page.js | 75 ++++++++++++++++++------------------- src/partials/main.hbs | 3 ++ src/partials/toc.hbs | 3 ++ 8 files changed, 174 insertions(+), 39 deletions(-) create mode 100644 src/css/toc.css create mode 100644 src/partials/toc.hbs diff --git a/src/css/doc.css b/src/css/doc.css index 394a3e1..b0bf2c9 100644 --- a/src/css/doc.css +++ b/src/css/doc.css @@ -12,6 +12,7 @@ font-size: var(--doc-font-size--desktop); margin: var(--doc-margin--desktop); max-width: var(--doc-max-width--desktop); + min-width: 0; } } diff --git a/src/css/main.css b/src/css/main.css index d48eea0..b8ea66c 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -1,7 +1,31 @@ +@media screen and (max-width: 1023px) { + aside.toc.sidebar { + display: none; + } +} + @media screen and (min-width: 1024px) { main { flex: auto; /* min-width: 0 required for flexbox to constrain overflowing elements */ min-width: 0; } + + main > .content { + display: flex; + } + + aside.toc.embedded { + display: none; + } + + aside.toc.sidebar { + flex: 1 0 var(--toc-width); + } +} + +@media screen and (min-width: 1216px) { + aside.toc.sidebar { + flex-basis: var(--toc-width--widescreen); + } } diff --git a/src/css/site.css b/src/css/site.css index 07c9d93..e73f175 100644 --- a/src/css/site.css +++ b/src/css/site.css @@ -8,6 +8,7 @@ @import "toolbar.css"; @import "breadcrumbs.css"; @import "page-versions.css"; +@import "toc.css"; @import "doc.css"; @import "header.css"; @import "footer.css"; diff --git a/src/css/toc.css b/src/css/toc.css new file mode 100644 index 0000000..55b4b9c --- /dev/null +++ b/src/css/toc.css @@ -0,0 +1,95 @@ +.toc-menu { + color: var(--toc-font-color); +} + +.toc.sidebar .toc-menu { + margin-right: 0.75rem; + position: sticky; + top: var(--toc-top); +} + +.toc .toc-menu h3 { + color: var(--toc-heading-font-color); + font-size: calc(16 / var(--rem-base) * 1rem); + font-weight: var(--body-font-weight-bold); + line-height: 1.3; + margin: 0 -0.5px; + padding-bottom: 0.25rem; +} + +.toc.sidebar .toc-menu h3 { + display: flex; + flex-direction: column; + height: 2.5rem; + justify-content: flex-end; +} + +.toc .toc-menu ul { + font-size: calc(15 / var(--rem-base) * 1rem); + line-height: var(--toc-line-height); + list-style: none; + margin: 0; + padding: 0; +} + +.toc.sidebar .toc-menu ul { + max-height: var(--toc-height); + overflow-y: auto; + scrollbar-width: none; +} + +.toc .toc-menu ul::-webkit-scrollbar { + width: 0; +} + +@media screen and (min-width: 1024px) { + .toc .toc-menu h3 { + font-size: calc(15 / var(--rem-base) * 1rem); + } + + .toc .toc-menu ul { + font-size: calc(13.5 / var(--rem-base) * 1rem); + } +} + +.toc .toc-menu li { + margin: 0; +} + +.toc .toc-menu li[data-level="2"] a { + padding-left: 1.25rem; +} + +.toc .toc-menu li[data-level="3"] a { + padding-left: 2rem; +} + +.toc .toc-menu a { + color: inherit; + border-left: 2px solid var(--toc-border-color); + display: inline-block; + padding: 0.25rem 0 0.25rem 0.5rem; + text-decoration: none; +} + +.sidebar.toc .toc-menu a { + display: block; + outline: none; +} + +.toc .toc-menu a:hover { + color: var(--link-font-color); +} + +.toc .toc-menu a.is-active { + border-left-color: var(--link-font-color); + color: var(--doc-font-color); +} + +.sidebar.toc .toc-menu a:focus { + background: var(--panel-background); +} + +.toc .toc-menu .is-hidden-toc { + display: none !important; +} diff --git a/src/css/vars.css b/src/css/vars.css index 376cdbb..888cea8 100644 --- a/src/css/vars.css +++ b/src/css/vars.css @@ -58,6 +58,11 @@ --toolbar-muted-color: var(--color-gray-30); --page-version-menu-background: var(--color-smoke-70); --page-version-missing-font-color: var(--color-gray-30); + /* toc */ + --toc-font-color: var(--nav-muted-color); + --toc-heading-font-color: var(--doc-font-color); + --toc-border-color: var(--panel-border-color); + --toc-line-height: 1.2; /* admonitions */ --caution-color: #a0439c; --caution-on-color: var(--color-white); @@ -110,7 +115,7 @@ --footer-background: var(--color-smoke-90); --footer-font-color: var(--color-gray-70); --footer-link-font-color: var(--color-jet-80); - /* dimensions */ + /* dimensions and positioning */ --navbar-height: calc(63 / var(--rem-base) * 1rem); --toolbar-height: calc(45 / var(--rem-base) * 1rem); --drawer-height: var(--toolbar-height); @@ -121,6 +126,10 @@ --nav-panel-height: calc(var(--nav-height) - var(--drawer-height)); --nav-panel-height--desktop: calc(var(--nav-height--desktop) - var(--drawer-height)); --nav-width: calc(270 / var(--rem-base) * 1rem); + --toc-top: calc(var(--body-top) + var(--toolbar-height)); + --toc-height: calc(100vh - var(--toc-top) - 2.5rem); + --toc-width: calc(162 / var(--rem-base) * 1rem); + --toc-width--widescreen: calc(216 / var(--rem-base) * 1rem); --doc-max-width: calc(720 / var(--rem-base) * 1rem); --doc-max-width--desktop: calc(828 / var(--rem-base) * 1rem); /* stacking */ diff --git a/src/js/02-on-this-page.js b/src/js/02-on-this-page.js index 60f40e9..0403a26 100644 --- a/src/js/02-on-this-page.js +++ b/src/js/02-on-this-page.js @@ -1,27 +1,21 @@ -/* Copyright (c) 2018 OpenDevise Inc. and individual contributors. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ ;(function () { 'use strict' var sidebar = document.querySelector('aside.toc.sidebar') if (!sidebar) return - var doc + if (document.querySelector('body.-toc')) return sidebar.parentNode.removeChild(sidebar) && undefined + var levels = parseInt(sidebar.dataset.levels || 2) + if (levels < 0) return + var article = document.querySelector('article.doc') var headings - if ( - document.querySelector('.body.-toc') || - !(headings = find('h1[id].sect0, .sect1 > h2[id]', (doc = document.querySelector('article.doc')))).length - ) { - sidebar.parentNode.removeChild(sidebar) - return + var headingSelector = [] + for (var l = 0; l <= levels; l++) headingSelector.push(l ? '.sect' + l + ' > h' + (l + 1) + '[id]' : 'h1[id].sect0') + if (!(headings = find(headingSelector.join(', '), article)).length) { + return sidebar.parentNode.removeChild(sidebar) && undefined } + var lastActiveFragment var links = {} - var menu - var list = headings.reduce(function (accum, heading) { var link = toArray(heading.childNodes).reduce(function (target, child) { if (child.nodeName !== 'A') target.appendChild(child.cloneNode(true)) @@ -29,49 +23,50 @@ }, document.createElement('a')) links[(link.href = '#' + heading.id)] = link var listItem = document.createElement('li') + listItem.dataset.level = parseInt(heading.nodeName.slice(1)) - 1 listItem.appendChild(link) accum.appendChild(listItem) return accum }, document.createElement('ul')) - if (!(menu = sidebar && sidebar.querySelector('.toc-menu'))) { + var menu = sidebar.querySelector('.toc-menu') + if (!menu) { menu = document.createElement('div') menu.className = 'toc-menu' } var title = document.createElement('h3') - title.textContent = 'On This Page' + title.textContent = sidebar.dataset.title || 'Contents' menu.appendChild(title) menu.appendChild(list) - if (sidebar) { - window.addEventListener('load', function () { - onScroll() - window.addEventListener('scroll', onScroll) - }) - } - - var startOfContent = doc.querySelector('h1.page ~ :not(.labels)') + var startOfContent = !document.getElementById('toc') && article.querySelector('h1.page ~ :not(.is-before-toc)') if (startOfContent) { var embeddedToc = document.createElement('aside') embeddedToc.className = 'toc embedded' embeddedToc.appendChild(menu.cloneNode(true)) - doc.insertBefore(embeddedToc, startOfContent) + startOfContent.parentNode.insertBefore(embeddedToc, startOfContent) } + window.addEventListener('load', function () { + onScroll() + window.addEventListener('scroll', onScroll) + }) + function onScroll () { - // NOTE doc.parentNode.offsetTop ~= doc.parentNode.getBoundingClientRect().top + window.pageYOffset - //var targetPosition = doc.parentNode.offsetTop - // NOTE no need to compensate wheen using spacer above [id] elements - var targetPosition = 0 var activeFragment - headings.some(function (heading) { - if (Math.floor(heading.getBoundingClientRect().top) <= targetPosition) { + var scrolledBy = window.pageYOffset + var buffer = getStyleValueAsInt(document.documentElement, 'fontSize') + if (scrolledBy && window.innerHeight + scrolledBy + 2 >= document.documentElement.scrollHeight) { + activeFragment = '#' + headings[headings.length - 1].id + } else { + var targetPosition = article.offsetTop + headings.some(function (heading) { + var headingTop = heading.getBoundingClientRect().top + getStyleValueAsInt(heading, 'paddingTop') + if (targetPosition < headingTop - buffer) return true activeFragment = '#' + heading.id - } else { - return true - } - }) + }) + } if (activeFragment) { if (activeFragment !== lastActiveFragment) { if (lastActiveFragment) { @@ -79,8 +74,8 @@ } var activeLink = links[activeFragment] activeLink.classList.add('is-active') - if (menu.scrollHeight > menu.offsetHeight) { - menu.scrollTop = Math.max(0, activeLink.offsetTop + activeLink.offsetHeight - menu.offsetHeight) + if (list.scrollHeight > list.offsetHeight) { + list.scrollTop = Math.max(0, activeLink.offsetTop + activeLink.offsetHeight - list.offsetHeight) } lastActiveFragment = activeFragment } @@ -97,4 +92,8 @@ function toArray (collection) { return [].slice.call(collection) } + + function getStyleValueAsInt (el, prop) { + return parseInt(window.getComputedStyle(el)[prop]) + } })() diff --git a/src/partials/main.hbs b/src/partials/main.hbs index 8e274c6..04752de 100644 --- a/src/partials/main.hbs +++ b/src/partials/main.hbs @@ -1,4 +1,7 @@
{{> toolbar}} +
{{> article}} +{{> toc}} +
diff --git a/src/partials/toc.hbs b/src/partials/toc.hbs new file mode 100644 index 0000000..2b29190 --- /dev/null +++ b/src/partials/toc.hbs @@ -0,0 +1,3 @@ + -- 2.11.0