add styles for TOC and integrate TOC into page layout
authorDan Allen <dan@opendevise.com>
Tue, 31 Mar 2020 10:38:55 +0000 (04:38 -0600)
committerDan Allen <dan@opendevise.com>
Tue, 31 Mar 2020 21:24:24 +0000 (15:24 -0600)
- 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
src/css/main.css
src/css/site.css
src/css/toc.css [new file with mode: 0644]
src/css/vars.css
src/js/02-on-this-page.js
src/partials/main.hbs
src/partials/toc.hbs [new file with mode: 0644]

index 394a3e1..b0bf2c9 100644 (file)
@@ -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;
   }
 }
 
index d48eea0..b8ea66c 100644 (file)
@@ -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);
+  }
 }
index 07c9d93..e73f175 100644 (file)
@@ -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 (file)
index 0000000..55b4b9c
--- /dev/null
@@ -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;
+}
index 376cdbb..888cea8 100644 (file)
   --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);
   --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);
   --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 */
index 60f40e9..0403a26 100644 (file)
@@ -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))
     }, 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])
+  }
 })()
index 8e274c6..04752de 100644 (file)
@@ -1,4 +1,7 @@
 <main class="article">
 {{> toolbar}}
+  <div class="content">
 {{> article}}
+{{> toc}}
+  </div>
 </main>
diff --git a/src/partials/toc.hbs b/src/partials/toc.hbs
new file mode 100644 (file)
index 0000000..2b29190
--- /dev/null
@@ -0,0 +1,3 @@
+<aside class="toc sidebar" data-title="{{or page.attributes.toctitle 'Contents'}}" data-levels="{{{or page.attributes.toclevels 2}}}">
+  <div class="toc-menu"></div>
+</aside>