You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

79 lines
3.1 KiB
JavaScript

// Action: Table of Contents Crawler
import { tocStore, tocActiveId } from './stores.js';
export function tocCrawler(node, args) {
let queryElements = 'h2, h3, h4, h5, h6';
let scrollTarget = 'body';
let headings;
let permalinks = [];
function init() {
// Set accepted list of query elements
// (IMPORTANT: must proceed resetting `headings` below)
if (args?.queryElements)
queryElements = args.queryElements;
// Set the desired scroll target to monitor
if (args?.scrollTarget)
scrollTarget = args.scrollTarget;
// Reset local values
headings = node.querySelectorAll(queryElements);
permalinks = [];
// Query and process the headings
queryHeadings();
}
function queryHeadings() {
headings?.forEach((elemHeading) => {
// If heading has ignore attribute, skip it
if (elemHeading.hasAttribute('data-toc-ignore'))
return;
// If generate mode and heading ID not present, assign one
if (args?.mode === 'generate' && !elemHeading.id) {
const newHeadingId = elemHeading.firstChild?.textContent
?.trim()
.replaceAll(/[^a-zA-Z0-9 ]/g, '')
.replaceAll(' ', '-')
.toLowerCase();
const prefix = args.prefix ? `${args.prefix}-` : '';
const suffix = args.suffix ? `-${args.suffix}` : '';
elemHeading.id = prefix + newHeadingId + suffix;
}
// Push heading data to the permalink array
permalinks.push({
element: elemHeading.nodeName.toLowerCase(),
id: elemHeading.id,
text: elemHeading.firstChild?.textContent?.trim() || ''
});
});
// Set the store with the permalink array
tocStore.set(permalinks);
}
// Listens to scroll event, determines top-most heading, provides that to the `tocActiveId` store
function onWindowScroll(e) {
if (!headings?.length)
return;
const targetElem = e.target;
if (!(targetElem instanceof HTMLElement))
throw new Error('scrollTarget is not an HTMLElement');
const scrollableTop = targetElem.getBoundingClientRect().top || 0;
const headingSizeThreshold = 40; // px
for (const elemHeading of headings) {
const headerBoundTop = elemHeading.getBoundingClientRect().top;
const offsetTop = headerBoundTop - scrollableTop + headingSizeThreshold;
if (offsetTop >= 0)
return tocActiveId.set(elemHeading.id);
}
}
// Lifecycle
init();
if (scrollTarget)
document.querySelector(scrollTarget)?.addEventListener('scroll', onWindowScroll);
return {
update(newArgs) {
args = newArgs;
init();
},
destroy() {
if (scrollTarget)
document.querySelector(scrollTarget)?.removeEventListener('scroll', onWindowScroll);
}
};
}