@redocly/theme
Version:
Shared UI components lib
216 lines • 9.62 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useActiveHeading = useActiveHeading;
const react_1 = require("react");
const react_router_dom_1 = require("react-router-dom");
const utils_1 = require("../utils");
function useActiveHeading(contentElement, displayedHeadings) {
const [heading, setHeading] = (0, react_1.useState)(undefined);
const [headingElements, setHeadingElements] = (0, react_1.useState)([]);
const headingElementsRef = (0, react_1.useRef)({});
const displayedHeadingsRef = (0, react_1.useRef)(displayedHeadings);
const lockObserver = (0, react_1.useRef)(false);
const location = (0, react_router_dom_1.useLocation)();
(0, react_1.useEffect)(() => {
setHeading(undefined);
headingElementsRef.current = {};
}, [location.pathname]);
(0, react_1.useEffect)(() => {
displayedHeadingsRef.current = displayedHeadings;
}, [displayedHeadings]);
const getVisibleHeadings = () => {
const visibleHeadings = [];
for (const key in headingElementsRef.current) {
const headingElement = headingElementsRef.current[key];
if (headingElement.isIntersecting) {
visibleHeadings.push(headingElement);
}
}
return visibleHeadings;
};
const isTopOfPage = () => window.scrollY <= 50;
// Returns viewport ratio for intersection observer based on screen height
const getViewportRatio = () => {
const vh = window.innerHeight;
if (vh >= 1400)
return 0.4;
if (vh >= 1000)
return 0.3;
return 0.25;
};
const getIndexFromId = (0, react_1.useCallback)((id) => {
return headingElements.findIndex((item) => item.id === id);
}, [headingElements]);
const findHeaders = (allContent) => {
const allHeaders = allContent.querySelectorAll('.heading-anchor');
const headers = Array.from(allHeaders);
if (displayedHeadingsRef.current && displayedHeadingsRef.current.length > 0) {
const displayedIds = displayedHeadingsRef.current.map((h) => h === null || h === void 0 ? void 0 : h.id).filter(Boolean);
return headers.filter((header) => displayedIds.includes(header.id));
}
return headers;
};
const calculateVirtualPositions = (0, react_1.useCallback)((headers) => {
var _a;
if (!headers.length)
return {};
const navbarHeight = ((_a = document.querySelector('nav')) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().height) || 0;
const viewportHeight = window.innerHeight;
const triggerOffset = navbarHeight + viewportHeight * 0.25;
const lastHeader = headers[headers.length - 1];
const lastHeaderTop = lastHeader.offsetTop;
const maxScrollableHeight = document.body.scrollHeight - viewportHeight;
const requiredUplift = Math.max(0, lastHeaderTop - maxScrollableHeight + triggerOffset);
const virtualPositions = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const headerTop = header.offsetTop;
const normalizedPosition = headers.length === 1 ? 0 : i / (headers.length - 1);
const adjustmentFactor = (0, utils_1.enhancedSmoothstep)(normalizedPosition, 0.4);
const uplift = requiredUplift * adjustmentFactor;
const virtualTop = headerTop - uplift;
virtualPositions[header.id] = virtualTop;
}
return virtualPositions;
}, []);
const getTriggerOffset = (0, react_1.useCallback)(() => {
var _a;
const navbarHeight = ((_a = document.querySelector('nav')) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().height) || 0;
return navbarHeight + window.innerHeight * getViewportRatio();
}, []);
const findActiveHeaderByVirtualPosition = (0, react_1.useCallback)((headers, scrollY, triggerOffset) => {
const virtualPositions = calculateVirtualPositions(headers);
let activeHeader = headers[0];
for (const header of headers) {
const virtualTop = virtualPositions[header.id] || header.offsetTop;
if (virtualTop <= scrollY + triggerOffset) {
activeHeader = header;
}
else {
break;
}
}
return activeHeader;
}, [calculateVirtualPositions]);
const intersectionCallback = (0, react_1.useCallback)((entries) => {
var _a, _b, _c;
if (lockObserver.current) {
return;
}
headingElementsRef.current = entries.reduce((map, entry) => {
map[entry.target.id] = entry;
return map;
}, headingElementsRef.current);
if (isTopOfPage()) {
const newHeading = ((_a = headingElements[0]) === null || _a === void 0 ? void 0 : _a.id) || undefined;
setHeading(newHeading);
return;
}
const visibleHeadings = getVisibleHeadings();
if (!visibleHeadings.length) {
const triggerOffset = getTriggerOffset();
if (isTopOfPage()) {
setHeading(((_b = headingElements[0]) === null || _b === void 0 ? void 0 : _b.id) || undefined);
return;
}
const totalHeight = window.scrollY + window.innerHeight;
if (totalHeight >= document.body.scrollHeight - 10) {
setHeading(((_c = headingElements[headingElements.length - 1]) === null || _c === void 0 ? void 0 : _c.id) || undefined);
return;
}
const activeHeader = findActiveHeaderByVirtualPosition(headingElements, window.scrollY, triggerOffset);
setHeading(activeHeader.id);
return;
}
if (visibleHeadings.length === 1) {
setHeading(visibleHeadings[0].target.id);
return;
}
visibleHeadings.sort((a, b) => {
return getIndexFromId(a.target.id) - getIndexFromId(b.target.id);
});
const firstVisibleHeading = visibleHeadings[0];
if (getIndexFromId(firstVisibleHeading.target.id) === 0 && isTopOfPage()) {
setHeading(firstVisibleHeading.target.id);
return;
}
const totalHeight = scrollY + window.innerHeight;
if (totalHeight >= document.body.scrollHeight - 10) {
setHeading(visibleHeadings[visibleHeadings.length - 1].target.id);
return;
}
setHeading(visibleHeadings[0].target.id);
}, [getIndexFromId, headingElements, getTriggerOffset, findActiveHeaderByVirtualPosition]);
const handleHeadingClick = (0, react_1.useCallback)((headingId) => {
lockObserver.current = true;
setHeading(headingId);
const headingElement = headingElements.find((el) => el.id === headingId);
if (headingElement && headingElement.scrollIntoView) {
headingElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
setTimeout(() => {
headingElementsRef.current = {};
lockObserver.current = false;
}, 300);
}, [headingElements]);
(0, react_1.useEffect)(() => {
var _a;
if (!contentElement) {
return;
}
const headers = findHeaders(contentElement);
setHeadingElements(headers);
if (headers.length > 0 && !heading) {
if (isTopOfPage()) {
setHeading(headers[0].id);
}
else {
const totalHeight = scrollY + window.innerHeight;
if (totalHeight >= document.body.scrollHeight - 10) {
setHeading(((_a = headers[headers.length - 1]) === null || _a === void 0 ? void 0 : _a.id) || undefined);
}
else {
const triggerOffset = getTriggerOffset();
const activeHeader = findActiveHeaderByVirtualPosition(headers, scrollY, triggerOffset);
setHeading(activeHeader.id);
}
}
}
}, [
contentElement,
location,
heading,
calculateVirtualPositions,
getTriggerOffset,
findActiveHeaderByVirtualPosition,
]);
(0, react_1.useEffect)(() => {
if (!(headingElements === null || headingElements === void 0 ? void 0 : headingElements.length)) {
return;
}
headingElementsRef.current = {};
const triggerOffset = getTriggerOffset();
const observer = new IntersectionObserver(intersectionCallback, {
rootMargin: `-${triggerOffset}px 0px -${window.innerHeight * getViewportRatio()}px 0px`,
threshold: [0, 0.1, 0.3, 0.5, 0.7, 1],
});
headingElements.forEach((element) => {
observer.observe(element);
});
const handleScroll = () => {
var _a;
if (lockObserver.current)
return;
if (isTopOfPage()) {
setHeading(((_a = headingElements[0]) === null || _a === void 0 ? void 0 : _a.id) || undefined);
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
observer.disconnect();
window.removeEventListener('scroll', handleScroll);
};
}, [headingElements, intersectionCallback, getTriggerOffset]);
return { heading, handleHeadingClick };
}
//# sourceMappingURL=use-active-heading.js.map