add JavaScript that generates the "on this page" sidebar
authorDan Allen <dan@opendevise.com>
Mon, 19 Nov 2018 21:07:56 +0000 (14:07 -0700)
committerDan Allen <dan@opendevise.com>
Tue, 31 Mar 2020 10:42:10 +0000 (04:42 -0600)
This script was originally developed for Couchbase by OpenDevise. This
commit contributes the script to the Antora project on behalf of
Couchbase. The script will now be licensed under the terms of the MPL
2.0 license to match the license of the Antora project itself.

src/js/02-fragment-jumper.js [deleted file]
src/js/02-on-this-page.js [new file with mode: 0644]
src/js/03-fragment-jumper.js [new file with mode: 0644]
src/js/03-page-versions.js [deleted file]
src/js/04-mobile-navbar.js [deleted file]
src/js/04-page-versions.js [new file with mode: 0644]
src/js/05-mobile-navbar.js [new file with mode: 0644]

diff --git a/src/js/02-fragment-jumper.js b/src/js/02-fragment-jumper.js
deleted file mode 100644 (file)
index 5f9462a..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-;(function () {
-  'use strict'
-
-  var article = document.querySelector('article.doc')
-  var toolbar = document.querySelector('.toolbar')
-
-  function decodeFragment (hash) {
-    return hash && (~hash.indexOf('%') ? decodeURIComponent(hash) : hash).slice(1)
-  }
-
-  function computePosition (el, sum) {
-    if (article.contains(el)) {
-      return computePosition(el.offsetParent, el.offsetTop + sum)
-    } else {
-      return sum
-    }
-  }
-
-  function jumpToAnchor (e) {
-    if (e) {
-      window.location.hash = '#' + this.id
-      e.preventDefault()
-    }
-    window.scrollTo(0, computePosition(this, 0) - toolbar.getBoundingClientRect().bottom)
-  }
-
-  window.addEventListener('load', function jumpOnLoad (e) {
-    var fragment, target
-    if ((fragment = decodeFragment(window.location.hash)) && (target = document.getElementById(fragment))) {
-      jumpToAnchor.bind(target)()
-      setTimeout(jumpToAnchor.bind(target), 0)
-    }
-    window.removeEventListener('load', jumpOnLoad)
-  })
-
-  Array.prototype.slice.call(document.querySelectorAll('a[href^="#"]')).forEach(function (el) {
-    var fragment, target
-    if ((fragment = decodeFragment(el.hash)) && (target = document.getElementById(fragment))) {
-      el.addEventListener('click', jumpToAnchor.bind(target))
-    }
-  })
-})()
diff --git a/src/js/02-on-this-page.js b/src/js/02-on-this-page.js
new file mode 100644 (file)
index 0000000..60f40e9
--- /dev/null
@@ -0,0 +1,100 @@
+/* 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
+  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 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))
+      return target
+    }, document.createElement('a'))
+    links[(link.href = '#' + heading.id)] = link
+    var listItem = document.createElement('li')
+    listItem.appendChild(link)
+    accum.appendChild(listItem)
+    return accum
+  }, document.createElement('ul'))
+
+  if (!(menu = sidebar && sidebar.querySelector('.toc-menu'))) {
+    menu = document.createElement('div')
+    menu.className = 'toc-menu'
+  }
+
+  var title = document.createElement('h3')
+  title.textContent = 'On This Page'
+  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)')
+  if (startOfContent) {
+    var embeddedToc = document.createElement('aside')
+    embeddedToc.className = 'toc embedded'
+    embeddedToc.appendChild(menu.cloneNode(true))
+    doc.insertBefore(embeddedToc, startOfContent)
+  }
+
+  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) {
+        activeFragment = '#' + heading.id
+      } else {
+        return true
+      }
+    })
+    if (activeFragment) {
+      if (activeFragment !== lastActiveFragment) {
+        if (lastActiveFragment) {
+          links[lastActiveFragment].classList.remove('is-active')
+        }
+        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)
+        }
+        lastActiveFragment = activeFragment
+      }
+    } else if (lastActiveFragment) {
+      links[lastActiveFragment].classList.remove('is-active')
+      lastActiveFragment = undefined
+    }
+  }
+
+  function find (selector, from) {
+    return toArray((from || document).querySelectorAll(selector))
+  }
+
+  function toArray (collection) {
+    return [].slice.call(collection)
+  }
+})()
diff --git a/src/js/03-fragment-jumper.js b/src/js/03-fragment-jumper.js
new file mode 100644 (file)
index 0000000..5f9462a
--- /dev/null
@@ -0,0 +1,42 @@
+;(function () {
+  'use strict'
+
+  var article = document.querySelector('article.doc')
+  var toolbar = document.querySelector('.toolbar')
+
+  function decodeFragment (hash) {
+    return hash && (~hash.indexOf('%') ? decodeURIComponent(hash) : hash).slice(1)
+  }
+
+  function computePosition (el, sum) {
+    if (article.contains(el)) {
+      return computePosition(el.offsetParent, el.offsetTop + sum)
+    } else {
+      return sum
+    }
+  }
+
+  function jumpToAnchor (e) {
+    if (e) {
+      window.location.hash = '#' + this.id
+      e.preventDefault()
+    }
+    window.scrollTo(0, computePosition(this, 0) - toolbar.getBoundingClientRect().bottom)
+  }
+
+  window.addEventListener('load', function jumpOnLoad (e) {
+    var fragment, target
+    if ((fragment = decodeFragment(window.location.hash)) && (target = document.getElementById(fragment))) {
+      jumpToAnchor.bind(target)()
+      setTimeout(jumpToAnchor.bind(target), 0)
+    }
+    window.removeEventListener('load', jumpOnLoad)
+  })
+
+  Array.prototype.slice.call(document.querySelectorAll('a[href^="#"]')).forEach(function (el) {
+    var fragment, target
+    if ((fragment = decodeFragment(el.hash)) && (target = document.getElementById(fragment))) {
+      el.addEventListener('click', jumpToAnchor.bind(target))
+    }
+  })
+})()
diff --git a/src/js/03-page-versions.js b/src/js/03-page-versions.js
deleted file mode 100644 (file)
index 6b8678b..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-;(function () {
-  'use strict'
-
-  var toggle = document.querySelector('.page-versions .version-menu-toggle')
-  if (!toggle) return
-
-  var selector = document.querySelector('.page-versions')
-
-  toggle.addEventListener('click', function (e) {
-    selector.classList.toggle('is-active')
-    // don't let this event get smothered
-    e.stopPropagation()
-  })
-
-  document.documentElement.addEventListener('click', function () {
-    selector.classList.remove('is-active')
-  })
-})()
diff --git a/src/js/04-mobile-navbar.js b/src/js/04-mobile-navbar.js
deleted file mode 100644 (file)
index ba5cb09..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-document.addEventListener('DOMContentLoaded', function () {
-  var navbarToggles = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0)
-  if (navbarToggles.length === 0) return
-  navbarToggles.forEach(function (el) {
-    el.addEventListener('click', function (e) {
-      e.stopPropagation()
-      el.classList.toggle('is-active')
-      document.getElementById(el.dataset.target).classList.toggle('is-active')
-      document.documentElement.classList.toggle('is-clipped--navbar')
-    })
-  })
-})
diff --git a/src/js/04-page-versions.js b/src/js/04-page-versions.js
new file mode 100644 (file)
index 0000000..6b8678b
--- /dev/null
@@ -0,0 +1,18 @@
+;(function () {
+  'use strict'
+
+  var toggle = document.querySelector('.page-versions .version-menu-toggle')
+  if (!toggle) return
+
+  var selector = document.querySelector('.page-versions')
+
+  toggle.addEventListener('click', function (e) {
+    selector.classList.toggle('is-active')
+    // don't let this event get smothered
+    e.stopPropagation()
+  })
+
+  document.documentElement.addEventListener('click', function () {
+    selector.classList.remove('is-active')
+  })
+})()
diff --git a/src/js/05-mobile-navbar.js b/src/js/05-mobile-navbar.js
new file mode 100644 (file)
index 0000000..ba5cb09
--- /dev/null
@@ -0,0 +1,12 @@
+document.addEventListener('DOMContentLoaded', function () {
+  var navbarToggles = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0)
+  if (navbarToggles.length === 0) return
+  navbarToggles.forEach(function (el) {
+    el.addEventListener('click', function (e) {
+      e.stopPropagation()
+      el.classList.toggle('is-active')
+      document.getElementById(el.dataset.target).classList.toggle('is-active')
+      document.documentElement.classList.toggle('is-clipped--navbar')
+    })
+  })
+})