@redocly/theme
Version:
Shared UI components lib
318 lines • 14.3 kB
JavaScript
;
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