79 lines
3.1 KiB
JavaScript
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);
|
|
}
|
|
};
|
|
}
|