sector_long_form-1.0.x-dev/components/toc/toc.js
components/toc/toc.js
(function () {
'use strict';
/**
* A pager for the long form document.
*/
Drupal.behaviors.long_form_toc = {
log: (message) => {
const prefix = 'sector_toc: ';
const debug = false;
if (debug) {
if (message.isObject) {
console.log(prefix, message);
}
else {
console.log(prefix + message);
}
}
},
attach: (context, settings) => {
const { sector_long_form: config } = settings;
const table = document.querySelector(`.${config.toc_class}`)
const chunks = document.querySelectorAll(`.${config.chunker_class}:has(> h2[id])`);
// build list
chunks.forEach(chunk => {
const heading = chunk.querySelector('h2[id]');
const li = document.createElement('li');
li.innerHTML = `<a href="#${heading.getAttribute('id')}">${heading.textContent}</a>`
const subheadings = chunk.querySelectorAll(`h2[id] ~ :is(h3[id])`)
if (subheadings.length > 0) {
const subtable = document.createElement('ol');
subheadings.forEach(subheading => {
const sli = document.createElement('li');
sli.innerHTML = `<a href="#${subheading.getAttribute('id')}">${subheading.textContent}</a>`
subtable.appendChild(sli)
})
li.appendChild(subtable);
}
table.appendChild(li);
})
table.querySelectorAll('a').forEach(a => a.addEventListener('click', () => {
}))
// Add event handler to toc links.
table.querySelectorAll('a').forEach(anchor => {
anchor.addEventListener('click', (evt) => {
evt.preventDefault();
const new_heading_id = evt.target.getAttribute('href');
Drupal.behaviors.long_form_toc.log('clicked id=' + new_heading_id);
updateHeadingInUrl(new_heading_id.replace('#', ''));
document.querySelector(new_heading_id)?.scrollIntoView({
behavior: 'smooth',
block: "start", inline: "nearest"
})
})
});
function tocLocationHashChange() {
const hash = location.hash;
Drupal.behaviors.long_form_toc.log("tocLocationHashChange() hash=" + hash);
const elem = document.querySelector(hash);
// If hash matches an element in node body field.
if (elem) {
setActive(`.${config.toc_class}`, hash);
}
else {
Drupal.behaviors.long_form_toc.log('tocLocationHashChange() no hash==toc match');
// Find parent chunker section, then make h2 active.
//let h2 = $(hash).parents(`.${config.chunker_class}`).find('h2');
let h2 = document.querySelector(hash)?.closest(`.${config.chunker_class}`).querySelector('h2')
Drupal.behaviors.long_form_toc.log('nearest h2=' + h2);
let h2_id = h2.getAttribute('id');
// Make toc with matching href 'active'.
setActive(`.${config.toc_class}`, '#' + h2_id);
}
}
// Set hyperlink and parent list item active in ToC.
function setActive(toc_selector, hash) {
// If hash matches a toc entry.
const activeLink = table.querySelector(`a[href$="${hash}"]`);
let activeHeading = null;
if (activeLink) {
activeHeading = hash;
}
// Remove active from any non-matched link.
table.querySelectorAll(`a:not([href$="${activeHeading}"])`).forEach(other => other.classList.remove('active'));
//$(`.${config.toc_class} a[href!="${hash_toc}"]`).removeClass('active');
// Remove active class from any active list items.
table.querySelectorAll('li.active').forEach(other => other.classList.remove('active'))
// Add active class to matched anchor, and parent list item.
Drupal.behaviors.long_form_toc.log('toc block, open link with hash=' + activeHeading);
activeLink.classList.add('active');
}
// On page load, do we have a URL fragment?
// e.g. arriving from bookmark, or refresh existing page?
if (location.hash) {
Drupal.behaviors.long_form_toc.log('detected hash in url, triggering local hashchange event');
tocLocationHashChange();
}
// When a hashchange event is fired open the correct h2/h3 header link in the ToC block.
window.addEventListener("hashchange", tocLocationHashChange, false);
// When the whole page has loaded create a new observer.
window.addEventListener("load", createObserver, false);
window.addEventListener("resize", createObserver, false);
// Prepare variables and create the observer.
function createObserver() {
let observer;
let headings = document.querySelectorAll(`.${config.chunker_class} :is(h2[id], h3[id])`);
let options = {
// root: document.querySelector('.prose'),
rootMargin: '0px',
threshold: 1.0
}
observer = new IntersectionObserver(handleObserver, options);
headings.forEach(heading => {
observer.observe(heading);
});
}
function handleObserver(entries, observer){
const intersecting = entries.filter(({ isIntersecting, intersectionRatio }) => isIntersecting && intersectionRatio === 1)
intersecting.forEach(entry => {
// Get a new heading after scrolling.
let new_heading_id = entry.target.getAttribute('id');
// Check isIntersecting to be sure the target currently intersects the root.
// Check intersectionRatio to know how much of the target element is actually visible
// within the root's intersection rectangle.
Drupal.behaviors.long_form_toc.log('intersecting heading_id=' + new_heading_id);
if (new_heading_id !== null) {
updateHeadingInUrl(new_heading_id);
}
});
}
async function updateHeadingInUrl(new_heading_id) {
const url = window.location.href.split('#')[0];
// Get the current heading from url.
let old_heading_id = window.location.hash.replace('#', '');
// Update heading in url if a new one is visible on a page.
if (new_heading_id !== old_heading_id) {
// Update heading in url.
history.replaceState(null, null, url + '#' + new_heading_id);
// Fire the new hashchange event.
window.dispatchEvent(new HashChangeEvent("hashchange"));
}
}
}
};
})(drupalSettings);