UNPKG

@redocly/theme

Version:

Shared UI components lib

318 lines 14.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useCodeWalkthroughSteps = useCodeWalkthroughSteps; exports.getGroupMarkers = getGroupMarkers; const react_1 = require("react"); const react_router_dom_1 = require("react-router-dom"); const js_utils_1 = require("../../utils/js-utils"); const code_walkthrough_1 = require("../../constants/code-walkthrough"); function useCodeWalkthroughSteps({ steps, enableDeepLink, root, }) { const location = (0, react_router_dom_1.useLocation)(); const navigate = (0, react_router_dom_1.useNavigate)(); const searchParams = (0, react_1.useMemo)(() => new URLSearchParams(location.search), [location.search]); const observerRef = (0, react_1.useRef)(null); const filtersElementRef = (0, react_1.useRef)(null); const lockObserver = (0, react_1.useRef)(false); // Track observed elements in case new observer needs to be created const observedElementsRef = (0, react_1.useRef)(new Set()); const [activeStep, setActiveStep] = (0, react_1.useState)(enableDeepLink ? searchParams.get(code_walkthrough_1.ACTIVE_STEP_QUERY_PARAM) : null); const stepsMap = (0, react_1.useMemo)(() => { const map = new Map(); steps.forEach((step, index) => { map.set(step.id, Object.assign(Object.assign({}, step), { index })); }); return map; // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(steps)]); const options = (0, react_1.useMemo)(() => { var _a, _b; if (!(0, js_utils_1.isBrowser)()) { return null; } const filtersElementHeight = ((_a = filtersElementRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0; const navbarHeight = ((_b = document.querySelector('nav')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect().height) || 0; return { filtersElementHeight, navbarHeight, }; }, []); const [visibleSteps, setVisibleSteps] = (0, react_1.useState)([]); const [markers, setMarkers] = (0, react_1.useState)({}); (0, react_1.useEffect)(() => { var _a, _b, _c, _d, _e; if (!root.current || !visibleSteps.length || !options) { return; } const markersMinTopOffset = options.filtersElementHeight + options.navbarHeight; const rootHeight = (_b = (_a = root.current) === null || _a === void 0 ? void 0 : _a.clientHeight) !== null && _b !== void 0 ? _b : 0; const lastStepOffset = (_e = (_d = (_c = visibleSteps[visibleSteps.length - 1]) === null || _c === void 0 ? void 0 : _c.compRef) === null || _d === void 0 ? void 0 : _d.offsetTop) !== null && _e !== void 0 ? _e : 0; const deficit = Math.max(lastStepOffset - (rootHeight - window.innerHeight), 0); const groups = getGroups(visibleSteps); let markers = groups.flatMap((group) => getGroupMarkers(group)); if (deficit) { const startOffset = markersMinTopOffset; const endOffset = Math.max(rootHeight - window.innerHeight, 0); markers = distributeMarkers({ endOffset, markers, startOffset: markersMinTopOffset < endOffset ? startOffset : 0, }); } setMarkers(markers.reduce((acc, marker, index) => { var _a; const step = visibleSteps[index]; acc[step.id] = { offset: marker, height: markers[index + 1] || !step.compRef ? ((_a = markers[index + 1]) !== null && _a !== void 0 ? _a : rootHeight) - marker : step.compRef.clientHeight, }; return acc; }, {})); // eslint-disable-next-line react-hooks/exhaustive-deps }, [visibleSteps, root.current, options]); const registerMarker = (0, react_1.useCallback)((stepId, element) => { if (observerRef.current) { const step = stepsMap.get(stepId); if (step) { step.markerRef = element; } observerRef.current.observe(element); observedElementsRef.current.add(element); } }, [stepsMap]); const removeMarker = (0, react_1.useCallback)((stepId, element) => { if (observerRef.current) { const step = stepsMap.get(stepId); if (step) { step.markerRef = undefined; } observerRef.current.unobserve(element); observedElementsRef.current.delete(element); } }, [stepsMap]); const registerStep = (0, react_1.useCallback)((stepId, element) => { const step = stepsMap.get(stepId); if (!step) { return; } step.compRef = element; setVisibleSteps((prevSteps) => (0, js_utils_1.insertAt)(prevSteps, step.index, step)); }, [stepsMap]); const removeStep = (0, react_1.useCallback)((stepId) => { const step = stepsMap.get(stepId); if (!step) { return; } step.compRef = undefined; setVisibleSteps((prevSteps) => (0, js_utils_1.removeElement)(prevSteps, step)); setActiveStep((prevStep) => (prevStep === stepId ? null : prevStep)); }, [stepsMap]); const observerCallback = (0, react_1.useCallback)((entries) => { var _a, _b, _c; if (lockObserver.current || !visibleSteps.length) { return; } if (visibleSteps.length < 2) { setActiveStep(((_a = visibleSteps[0]) === null || _a === void 0 ? void 0 : _a.id) || null); return; } for (const entry of entries) { const stepId = (_c = (_b = entry.target) === null || _b === void 0 ? void 0 : _b.dataset) === null || _c === void 0 ? void 0 : _c.stepId; if (!stepId) { continue; } const { intersectionRatio, boundingClientRect, rootBounds, isIntersecting } = entry; const step = stepsMap.get(stepId); if (!step) { continue; } const stepIndex = visibleSteps.findIndex((renderedStep) => renderedStep.id === stepId); const { next } = (0, js_utils_1.getAdjacentValues)(visibleSteps, stepIndex); const intersectionAtTop = (rootBounds === null || rootBounds === void 0 ? void 0 : rootBounds.bottom) !== undefined && boundingClientRect.top < rootBounds.top; const stepGoesIn = isIntersecting; if (intersectionRatio > 0.8 && intersectionRatio < 1 && intersectionAtTop) { setActiveStep(step.id); break; } if (intersectionRatio < 1 && intersectionRatio !== 0 && intersectionAtTop) { let newStep = null; if (stepGoesIn) { newStep = step.id; } else if (next) { newStep = next.id; } setActiveStep((prevStep) => newStep || prevStep); break; } } }, [stepsMap, visibleSteps]); (0, react_1.useEffect)(() => { var _a; if (!options) { return; } const newObserver = new IntersectionObserver(observerCallback, { threshold: [0.3, 0.8, 0.9, 0.95], rootMargin: `-${options.filtersElementHeight + options.navbarHeight}px 0px 0px 0px`, }); for (const observedElement of observedElementsRef.current.values()) { newObserver.observe(observedElement); } (_a = observerRef.current) === null || _a === void 0 ? void 0 : _a.disconnect(); observerRef.current = newObserver; }, [observerCallback, markers, options]); (0, react_1.useEffect)(() => { var _a, _b, _c; if (!options) { return; } const rootTopOffset = (_b = (_a = root.current) === null || _a === void 0 ? void 0 : _a.offsetTop) !== null && _b !== void 0 ? _b : 0; if (!activeStep && rootTopOffset <= options.navbarHeight) { setActiveStep(((_c = visibleSteps[0]) === null || _c === void 0 ? void 0 : _c.id) || null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeStep, root.current, options, visibleSteps]); /** * Update the URL search params with the current state of the filters and inputs */ (0, react_1.useEffect)(() => { if (!enableDeepLink) { return; } const newSearchParams = new URLSearchParams(Array.from(searchParams.entries())); if (activeStep) { newSearchParams.set(code_walkthrough_1.ACTIVE_STEP_QUERY_PARAM, activeStep); } else { newSearchParams.delete(code_walkthrough_1.ACTIVE_STEP_QUERY_PARAM); } const newSearch = newSearchParams.toString(); if (newSearch === location.search.substring(1)) return; navigate({ search: newSearch }, { replace: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeStep]); return { registerStep, removeStep, markers, registerMarker, removeMarker, lockObserver, filtersElementRef, activeStep, setActiveStep, }; } /** * This function analyzes the offset and height of each step to determine * when a new group should be created. A new group is started when there is a free space * between the two steps, treating it as the content of the next group header. * * @param steps - An array of `CodeWalkthroughStep` objects * * @returns An array of `StepsGroup` objects, each containing the offset from the top of the relative * block, the free space at the top of the group, the total space used by the steps within the group * and the steps themselves with relative offset and height. */ function getGroups(steps) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; if (!steps.length) { return []; } const firstStepOffset = (_c = (_b = (_a = steps[0]) === null || _a === void 0 ? void 0 : _a.compRef) === null || _b === void 0 ? void 0 : _b.offsetTop) !== null && _c !== void 0 ? _c : 0; const firstStepHeight = (_f = (_e = (_d = steps[0]) === null || _d === void 0 ? void 0 : _d.compRef) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 0; const secondStepOffset = (_j = (_h = (_g = steps[1]) === null || _g === void 0 ? void 0 : _g.compRef) === null || _h === void 0 ? void 0 : _h.offsetTop) !== null && _j !== void 0 ? _j : 0; const margin = Math.max(secondStepOffset - firstStepOffset - firstStepHeight, 0); let groupIndex = 0; const groups = [ { offset: 0, freeSpace: firstStepOffset, usedSpace: 0, steps: [], }, ]; for (let i = 0; i < steps.length; i++) { let currentGroup = groups[groupIndex]; const step = steps[i]; const stepHeight = (_l = (_k = step.compRef) === null || _k === void 0 ? void 0 : _k.clientHeight) !== null && _l !== void 0 ? _l : 0; const stepOffset = (_o = (_m = step.compRef) === null || _m === void 0 ? void 0 : _m.offsetTop) !== null && _o !== void 0 ? _o : 0; const prevStepOffset = currentGroup.freeSpace + currentGroup.usedSpace; if (prevStepOffset !== Math.max(stepOffset - currentGroup.offset, 0)) { const offset = currentGroup.offset + currentGroup.freeSpace + currentGroup.usedSpace; groupIndex++; groups[groupIndex] = { offset, freeSpace: Math.max(stepOffset - offset, 0), usedSpace: 0, steps: [], }; currentGroup = groups[groupIndex]; } currentGroup.steps.push({ offset: stepOffset - currentGroup.offset, height: stepHeight, ref: step.compRef, }); currentGroup.usedSpace += stepHeight + margin; } return groups; } function getGroupMarkers(group) { if (!group.steps.length) { return []; } if (group.steps.length === 1) { return [group.offset + group.steps[0].offset - group.freeSpace]; } const availableFreeSpace = group.freeSpace > 0.3 * window.innerHeight ? 0.3 * window.innerHeight : group.freeSpace; const unusedFreeSpace = group.freeSpace - availableFreeSpace; const lastStepOffset = group.steps[group.steps.length - 1].offset; // distribute group free space between steps return distributeMarkers({ startOffset: 0, endOffset: lastStepOffset - unusedFreeSpace, markers: group.steps.map((step) => step.offset), additionalSteps: [(marker) => group.offset + unusedFreeSpace + marker], }); } /** * Distribute markers preserving the relationship throughout the available space * @param startOffset - the starting point of the available space * @param endOffset - the end point of the available space * @param markers - the markers to distribute * @param additionalSteps - additional steps to apply to the markers * * @returns array of markers positions */ function distributeMarkers({ endOffset, markers, startOffset, additionalSteps = [], }) { return markers.map((marker) => { const normalizedOffset = getNormalizedNumber({ min: markers[0], max: markers[markers.length - 1], value: marker, }); const availableSpace = endOffset - startOffset; let result = startOffset + normalizedOffset * availableSpace; for (const additionalStep of additionalSteps) { result = additionalStep(result); } return result; }); } /** * Normalize a number between a min and max value * @param min - the minimum value of the distribution * @param max - the maximum value of the distribution * @param value - the value to normalize * * @returns normalized number between 0 and 1 */ function getNormalizedNumber(options) { const { min, max, value } = options; return (value - min) / (max - min); } //# sourceMappingURL=use-code-walkthrough-steps.js.map