|
|
|
/*
|
|
|
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
Set a new ToC entry.
|
|
|
|
Clear any previously highlighted ToC items, set the new one,
|
|
|
|
and adjust the ToC scroll position.
|
|
|
|
*/
|
|
|
|
function setTocEntry(newEntry) {
|
|
|
|
const activeEntries = document.querySelectorAll("#toc a.active");
|
|
|
|
for (const activeEntry of activeEntries) {
|
|
|
|
activeEntry.classList.remove('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
newEntry.classList.add('active');
|
|
|
|
// don't scroll the sidebar nav if the main content is not scrolled
|
|
|
|
const nav = document.querySelector("#td-section-nav");
|
|
|
|
const content = document.querySelector("html");
|
|
|
|
if (content.scrollTop !== 0) {
|
|
|
|
nav.scrollTop = newEntry.offsetTop - 100;
|
|
|
|
} else {
|
|
|
|
nav.scrollTop = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Test whether a node is in the viewport
|
|
|
|
*/
|
|
|
|
function isInViewport(node) {
|
|
|
|
const rect = node.getBoundingClientRect();
|
|
|
|
return (
|
|
|
|
rect.top >= 0 &&
|
|
|
|
rect.left >= 0 &&
|
|
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
The callback we pass to the IntersectionObserver constructor.
|
|
|
|
|
|
|
|
Called when any of our observed nodes starts or stops intersecting
|
|
|
|
with the viewport.
|
|
|
|
*/
|
|
|
|
function handleIntersectionUpdate(entries) {
|
|
|
|
|
|
|
|
/*
|
|
|
|
Special case: If the current URL hash matches a ToC entry, and
|
|
|
|
the corresponding heading is visible in the viewport, then that is
|
|
|
|
made the current ToC entry, and we don't even look at the intersection
|
|
|
|
observer data.
|
|
|
|
This means that if the user has clicked on a ToC entry,
|
|
|
|
we won't unselect it through the intersection observer.
|
|
|
|
*/
|
|
|
|
const hash = document.location.hash;
|
|
|
|
if (hash) {
|
|
|
|
let tocEntryForHash = document.querySelector(`nav li a[href="${hash}"]`);
|
|
|
|
// if the hash isn't a direct match for a ToC item, check the data attributes
|
|
|
|
if (!tocEntryForHash) {
|
|
|
|
const fragment = hash.substring(1);
|
|
|
|
tocEntryForHash = document.querySelector(`nav li a[data-${fragment}]`);
|
|
|
|
}
|
|
|
|
if (tocEntryForHash) {
|
|
|
|
const headingForHash = document.querySelector(hash);
|
|
|
|
if (headingForHash && isInViewport(headingForHash)) {
|
|
|
|
setTocEntry(tocEntryForHash);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let newEntry = null;
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
if (entry.intersectionRatio > 0) {
|
|
|
|
const heading = entry.target;
|
|
|
|
/*
|
|
|
|
This sidebar nav consists of two sections:
|
|
|
|
* at the top, a sitenav containing links to other pages
|
|
|
|
* under that, the ToC for the current page
|
|
|
|
|
|
|
|
Since the sidebar scrolls to match the document position,
|
|
|
|
the sitenav will tend to scroll off the screen.
|
|
|
|
|
|
|
|
If the user has scrolled up to (or near) the top of the page,
|
|
|
|
we want to show the sitenav so.
|
|
|
|
|
|
|
|
So: if the H1 (title) for the current page has started
|
|
|
|
intersecting, then always scroll the sidebar back to the top.
|
|
|
|
*/
|
|
|
|
if (heading.tagName === "H1" && heading.parentNode.tagName === "DIV") {
|
|
|
|
const nav = document.querySelector("#td-section-nav");
|
|
|
|
nav.scrollTop = 0;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
/*
|
|
|
|
Otherwise, get the ToC entry for the first entry that
|
|
|
|
entered the viewport, if there was one.
|
|
|
|
*/
|
|
|
|
const id = entry.target.getAttribute('id');
|
|
|
|
let tocEntry = document.querySelector(`nav li a[href="#${id}"]`);
|
|
|
|
// if the id isn't a direct match for a ToC item,
|
|
|
|
// check the ToC entry's `data-*` attributes
|
|
|
|
if (!tocEntry) {
|
|
|
|
tocEntry = document.querySelector(`nav li a[data-${id}]`);
|
|
|
|
}
|
|
|
|
if (tocEntry && !newEntry) {
|
|
|
|
newEntry = tocEntry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newEntry) {
|
|
|
|
setTocEntry(newEntry);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Track when headings enter the viewport, and use this to update the highlight
|
|
|
|
for the corresponding ToC entry.
|
|
|
|
*/
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
const toc = document.querySelector("#toc");
|
|
|
|
toc.addEventListener("click", event => {
|
|
|
|
if (event.target.tagName === "A") {
|
|
|
|
setTocEntry(event.target);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver(handleIntersectionUpdate);
|
|
|
|
|
|
|
|
document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((section) => {
|
|
|
|
observer.observe(section);
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|