161 lines
5.1 KiB
JavaScript
161 lines
5.1 KiB
JavaScript
import { onMounted, onUnmounted, onUpdated } from 'vue';
|
|
import { useAside } from './aside';
|
|
import { throttleAndDebounce } from '../support/utils';
|
|
// magic number to avoid repeated retrieval
|
|
const PAGE_OFFSET = 71;
|
|
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 {
|
|
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')) {
|
|
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);
|
|
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 links = [].slice.call(container.value.querySelectorAll('.outline-link'));
|
|
const anchors = [].slice
|
|
.call(document.querySelectorAll('.content .header-anchor'))
|
|
.filter((anchor) => {
|
|
return links.some((link) => {
|
|
return link.hash === anchor.hash && anchor.offsetParent !== null;
|
|
});
|
|
});
|
|
const scrollY = window.scrollY;
|
|
const innerHeight = window.innerHeight;
|
|
const offsetHeight = document.body.offsetHeight;
|
|
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;
|
|
// page bottom - highlight last one
|
|
if (anchors.length && isBottom) {
|
|
activateLink(anchors[anchors.length - 1].hash);
|
|
return;
|
|
}
|
|
for (let i = 0; i < anchors.length; i++) {
|
|
const anchor = anchors[i];
|
|
const nextAnchor = anchors[i + 1];
|
|
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor);
|
|
if (isActive) {
|
|
activateLink(hash);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
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 + 33 + 'px';
|
|
marker.value.style.opacity = '1';
|
|
}
|
|
else {
|
|
marker.value.style.top = '33px';
|
|
marker.value.style.opacity = '0';
|
|
}
|
|
}
|
|
}
|
|
function getAnchorTop(anchor) {
|
|
return anchor.parentElement.offsetTop - PAGE_OFFSET;
|
|
}
|
|
function isAnchorActive(index, anchor, nextAnchor) {
|
|
const scrollTop = window.scrollY;
|
|
if (index === 0 && scrollTop === 0) {
|
|
return [true, null];
|
|
}
|
|
if (scrollTop < getAnchorTop(anchor)) {
|
|
return [false, null];
|
|
}
|
|
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
|
|
return [true, anchor.hash];
|
|
}
|
|
return [false, null];
|
|
}
|