UNPKG

@redocly/theme

Version:

Shared UI components lib

216 lines 9.62 kB
"use strict"; 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