DuyTa's picture
Upload folder using huggingface_hub
bc20498 verified
raw
history blame
13.1 kB
import { addMeltEventListener, makeElement, createElHelpers, executeCallbacks, } from '../../internal/helpers/index.js';
import { dequal } from 'dequal';
import { derived, writable } from 'svelte/store';
import { safeOnMount } from '../../internal/helpers/lifecycle.js';
import { withGet } from '../../internal/helpers/withGet.js';
const defaults = {
exclude: ['h1'],
scrollOffset: 0,
scrollBehaviour: 'smooth',
activeType: 'lowest',
};
export function createTableOfContents(args) {
const argsWithDefaults = { ...defaults, ...args };
const { selector, exclude, activeType, scrollBehaviour, scrollOffset, headingFilterFn, scrollFn, } = argsWithDefaults;
const { name } = createElHelpers('table-of-contents');
// Variables
const possibleHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
let headingsList = [];
let elementsList = [];
/** Lookup to see which heading an element belongs to. */
let elementHeadingLU = {};
/** Lookup to see which parent headings a heading has. */
let headingParentsLU = {};
/** List of the active parent indexes. */
const activeParentIdxs = withGet.writable([]);
/** List of the indexes of the visible elements. */
const visibleElementIdxs = withGet.writable([]);
let elementTarget = null;
let mutationObserver = null;
let observer = null;
const observer_threshold = 0.01;
// Stores
const activeHeadingIdxs = withGet(writable([]));
const headingsTree = withGet(writable([]));
// Helpers
function generateInitialLists(elementTarget) {
let headingsList = [];
let elementsList = [];
const includedHeadings = possibleHeadings.filter((h) => !exclude.includes(h));
const targetHeaders = elementTarget?.querySelectorAll(includedHeadings.join(', '));
// Create a unique ID for each heading which doesn't have one.
targetHeaders?.forEach((el) => {
if (!el.id) {
const uniqueID = el.innerText
.replaceAll(/[^a-zA-Z0-9 ]/g, '')
.replaceAll(' ', '-')
.toLowerCase();
el.id = `${uniqueID}`;
}
headingsList.push(el);
});
headingsList = [...headingsList];
if (headingFilterFn) {
headingsList = headingsList.filter((heading) => headingFilterFn(heading));
}
// Get all elements in our elementTarget and convert it from an HTMLCollection to an array.
elementsList = [].slice.call(elementTarget?.getElementsByTagName('*'));
// Filter the array, so that only the allowed headings and elements with no children are in the list to avoid problems with elements that wrap around others.
elementsList = elementsList.filter((el) => includedHeadings.includes(el.nodeName.toLowerCase()) || el.children.length === 0);
// We don't care about elements before our first header element, so we can remove those as well.
elementsList.splice(0, elementsList.indexOf(headingsList[0]));
return {
headingsList,
elementsList,
};
}
/**
* Create a tree view of our headings so that the hierarchy is represented.
* @param arr An array of heading elements.
* @param startIndex The parent elements original index in the array.
*/
function createTree(arr, startIndex = 0) {
const tree = [];
let i = 0;
while (i < arr.length) {
const node = {
title: arr[i].innerText,
index: startIndex + i,
id: arr[i].id,
node: arr[i],
children: [],
};
let j = i + 1;
while (j < arr.length &&
parseInt(arr[j].tagName.charAt(1)) > parseInt(arr[i].tagName.charAt(1))) {
j++;
}
// Recursive call.
node.children = createTree(arr.slice(i + 1, j), startIndex + i + 1);
tree.push(node);
i = j;
}
return tree;
}
/**
* Scrolls to the element specified by the selector.
* The offset and scroll behaviour are determined by the
* builder arguments.
*
* Source: https://stackoverflow.com/questions/49820013/javascript-scrollintoview-smooth-scroll-and-offset?answertab=scoredesc#tab-top
*
* @param selector The id of the element.
*/
function scrollToTargetAdjusted(selector) {
const element = document.getElementById(selector);
if (element) {
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.scrollY - scrollOffset;
window.scrollTo({
top: offsetPosition,
behavior: scrollBehaviour,
});
}
}
const shouldHighlightParents = activeType === 'highest-parents' ||
activeType === 'lowest-parents' ||
activeType === 'all-parents';
function handleElementObservation(entries) {
// Iterate through all elements that crossed the observer_threshold.
for (let i = 0; i < entries.length; i++) {
// Get the index of the observed element in our elementsList, as well as the ToC heading it belongs to.
const el_idx = elementsList.indexOf(entries[i].target);
const toc_idx = elementHeadingLU[el_idx];
let tempVisibleElementIdxs = visibleElementIdxs.get();
if (entries[i].intersectionRatio >= observer_threshold) {
// Only add the observed element to the visibleElementIdxs list if it isn't added yet.
if (tempVisibleElementIdxs.indexOf(el_idx) === -1) {
tempVisibleElementIdxs = [...tempVisibleElementIdxs, el_idx];
visibleElementIdxs.set(tempVisibleElementIdxs);
// Only add active parents if parent headings should be highlighted.
if (shouldHighlightParents && headingParentsLU[toc_idx]) {
activeParentIdxs.update((prev) => {
return [...prev, ...headingParentsLU[toc_idx]];
});
}
}
}
else {
// Remove the observed element from the visibleElementIdxs list if the intersection ratio is below the threshold.
tempVisibleElementIdxs = tempVisibleElementIdxs.filter((item) => item !== el_idx);
visibleElementIdxs.set(tempVisibleElementIdxs);
// Remove all parents of obsIndex from the activeParentIdxs list.
if (shouldHighlightParents && headingParentsLU[toc_idx]) {
activeParentIdxs.update((prev) => {
const newArr = [...prev];
headingParentsLU[toc_idx]?.forEach((parent) => {
const index = newArr.indexOf(parent);
newArr.splice(index, 1);
});
return newArr;
});
}
}
}
const allActiveHeaderIdxs = Array.from(new Set(visibleElementIdxs.get().map((idx) => elementHeadingLU[idx])));
let activeHeaderIdxs;
if (allActiveHeaderIdxs.length === 0) {
activeHeaderIdxs = [];
}
else {
switch (activeType) {
case 'highest':
activeHeaderIdxs = [Math.min(...allActiveHeaderIdxs)];
break;
case 'lowest':
activeHeaderIdxs = [Math.max(...allActiveHeaderIdxs)];
break;
case 'all':
activeHeaderIdxs = allActiveHeaderIdxs;
break;
case 'all-parents': {
const parentIdxs = allActiveHeaderIdxs.flatMap((idx) => headingParentsLU[idx] ?? []);
activeHeaderIdxs = [...allActiveHeaderIdxs, ...parentIdxs];
break;
}
default: {
const activeHeaderIdx = activeType === 'highest-parents'
? Math.min(...allActiveHeaderIdxs)
: Math.max(...allActiveHeaderIdxs);
if (headingParentsLU[activeHeaderIdx]) {
activeHeaderIdxs = [...headingParentsLU[activeHeaderIdx], activeHeaderIdx];
}
else {
activeHeaderIdxs = [activeHeaderIdx];
}
}
}
}
// Set store to active indexes.
activeHeadingIdxs.set(activeHeaderIdxs);
}
function initialization() {
observer?.disconnect();
/** Get all parents for each heading element, by checking
* which previous headings in the list have a lower H value,
* so H1 < H2 < H3 < ...
*/
headingsList.forEach((h, i) => {
headingParentsLU[i] = null;
let current_heading = h.tagName;
let parents = [];
for (let j = i - 1; j >= 0; j--) {
if (headingsList[j].tagName < current_heading) {
current_heading = headingsList[j].tagName;
parents = [...parents, j];
}
}
headingParentsLU[i] = parents.length > 0 ? parents : null;
// Find all elements between the current heading and the next one and assign them the current heading.
const startIndex = elementsList.indexOf(headingsList[i]);
const endIndex = i !== headingsList.length - 1
? elementsList.indexOf(headingsList[i + 1])
: elementsList.length;
for (let j = startIndex; j < endIndex; j++) {
elementHeadingLU[j] = i;
}
});
headingsTree.set(createTree(headingsList));
if (activeType !== 'none') {
// Create observer and observe all elements.
observer = new IntersectionObserver(handleElementObservation, {
root: null,
threshold: observer_threshold,
});
elementsList.forEach((el) => observer?.observe(el));
}
}
function mutationHandler() {
const newElementTarget = document.querySelector(selector);
if (!newElementTarget)
return;
const { headingsList: newHeadingsList, elementsList: newElementsList } = generateInitialLists(newElementTarget);
if (dequal(headingsList, newHeadingsList))
return;
// Update lists and LUs and re-run initialization.
headingsList = newHeadingsList;
elementsList = newElementsList;
headingParentsLU = {};
elementHeadingLU = {};
initialization();
}
safeOnMount(() => {
elementTarget = document.querySelector(selector);
if (!elementTarget)
return;
({ headingsList, elementsList } = generateInitialLists(elementTarget));
initialization();
mutationObserver = new MutationObserver(mutationHandler);
mutationObserver.observe(elementTarget, { childList: true, subtree: true });
return () => {
observer?.disconnect();
mutationObserver?.disconnect();
};
});
// Elements
const item = makeElement(name('item'), {
stores: activeHeadingIdxs,
returned: ($activeHeadingIdxs) => {
return (id) => {
const idx = headingsList.findIndex((heading) => heading.id === id);
const active = $activeHeadingIdxs.includes(idx);
return {
'data-id': id,
'data-active': active ? '' : undefined,
};
};
},
action: (node) => {
const id = node.getAttribute('data-id');
const unsub = executeCallbacks(addMeltEventListener(node, 'click', (e) => {
e.preventDefault();
if (scrollFn) {
scrollFn(`${id}`);
}
else {
scrollToTargetAdjusted(`${id}`);
}
// Add items hash to URL
if (id) {
history.pushState({}, '', `#${id}`);
}
}));
return {
destroy: unsub,
};
},
});
// Helpers
const isActive = derived(activeHeadingIdxs, ($activeHeadingIdxs) => {
return (headingId) => {
const idx = headingsList.findIndex((heading) => heading.id === headingId);
return $activeHeadingIdxs.includes(idx);
};
});
return {
elements: {
item,
},
states: {
activeHeadingIdxs,
headingsTree,
},
helpers: {
isActive,
},
};
}