Warp/node_modules/vitepress/dist/client/theme-default/composables/outline.js
2024-01-31 13:33:19 +07:00

178 lines
5.6 KiB
JavaScript

import { getScrollOffset } from 'vitepress';
import { onMounted, onUnmounted, onUpdated } from 'vue';
import { throttleAndDebounce } from '../support/utils';
import { useAside } from './aside';
// cached list of anchor elements from resolveHeaders
const resolvedHeaders = [];
export function resolveTitle(theme) {
return ((typeof theme.outline === 'object' &&
!Array.isArray(theme.outline) &&
theme.outline.label) ||
theme.outlineTitle ||
'On this page');
}
export function getHeaders(range) {
const headers = [
...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)')
]
.filter((el) => el.id && el.hasChildNodes())
.map((el) => {
const level = Number(el.tagName[1]);
return {
element: el,
title: serializeHeader(el),
link: '#' + el.id,
level
};
});
return resolveHeaders(headers, range);
}
function serializeHeader(h) {
let ret = '';
for (const node of h.childNodes) {
if (node.nodeType === 1) {
if (node.classList.contains('VPBadge') ||
node.classList.contains('header-anchor') ||
node.classList.contains('ignore-header')) {
continue;
}
ret += node.textContent;
}
else if (node.nodeType === 3) {
ret += node.textContent;
}
}
return ret.trim();
}
export function resolveHeaders(headers, range) {
if (range === false) {
return [];
}
const levelsRange = (typeof range === 'object' && !Array.isArray(range)
? range.level
: range) || 2;
const [high, low] = typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange;
headers = headers.filter((h) => h.level >= high && h.level <= low);
// clear previous caches
resolvedHeaders.length = 0;
// update global header list for active link rendering
for (const { element, link } of headers) {
resolvedHeaders.push({ element, link });
}
const ret = [];
outer: for (let i = 0; i < headers.length; i++) {
const cur = headers[i];
if (i === 0) {
ret.push(cur);
}
else {
for (let j = i - 1; j >= 0; j--) {
const prev = headers[j];
if (prev.level < cur.level) {
;
(prev.children || (prev.children = [])).push(cur);
continue outer;
}
}
ret.push(cur);
}
}
return ret;
}
export function useActiveAnchor(container, marker) {
const { isAsideEnabled } = useAside();
const onScroll = throttleAndDebounce(setActiveLink, 100);
let prevActiveLink = null;
onMounted(() => {
requestAnimationFrame(setActiveLink);
window.addEventListener('scroll', onScroll);
});
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash);
});
onUnmounted(() => {
window.removeEventListener('scroll', onScroll);
});
function setActiveLink() {
if (!isAsideEnabled.value) {
return;
}
const scrollY = window.scrollY;
const innerHeight = window.innerHeight;
const offsetHeight = document.body.offsetHeight;
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;
// resolvedHeaders may be repositioned, hidden or fix positioned
const headers = resolvedHeaders
.map(({ element, link }) => ({
link,
top: getAbsoluteTop(element)
}))
.filter(({ top }) => !Number.isNaN(top))
.sort((a, b) => a.top - b.top);
// no headers available for active link
if (!headers.length) {
activateLink(null);
return;
}
// page top
if (scrollY < 1) {
activateLink(null);
return;
}
// page bottom - highlight last link
if (isBottom) {
activateLink(headers[headers.length - 1].link);
return;
}
// find the last header above the top of viewport
let activeLink = null;
for (const { link, top } of headers) {
if (top > scrollY + getScrollOffset() + 4) {
break;
}
activeLink = link;
}
activateLink(activeLink);
}
function activateLink(hash) {
if (prevActiveLink) {
prevActiveLink.classList.remove('active');
}
if (hash == null) {
prevActiveLink = null;
}
else {
prevActiveLink = container.value.querySelector(`a[href="${decodeURIComponent(hash)}"]`);
}
const activeLink = prevActiveLink;
if (activeLink) {
activeLink.classList.add('active');
marker.value.style.top = activeLink.offsetTop + 39 + 'px';
marker.value.style.opacity = '1';
}
else {
marker.value.style.top = '33px';
marker.value.style.opacity = '0';
}
}
}
function getAbsoluteTop(element) {
let offsetTop = 0;
while (element !== document.body) {
if (element === null) {
// child element is:
// - not attached to the DOM (display: none)
// - set to fixed position (not scrollable)
// - body or html element (null offsetParent)
return NaN;
}
offsetTop += element.offsetTop;
element = element.offsetParent;
}
return offsetTop;
}