214 lines
6.0 KiB
JavaScript
214 lines
6.0 KiB
JavaScript
import { Select } from "antd";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import "./shop-info.section-navigator.styles.scss";
|
|
|
|
const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active";
|
|
|
|
export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) {
|
|
const { t } = useTranslation();
|
|
const targetMapRef = useRef(new Map());
|
|
const highlightedTargetRef = useRef(null);
|
|
const [options, setOptions] = useState([]);
|
|
const [selectedSection, setSelectedSection] = useState(undefined);
|
|
|
|
useEffect(() => {
|
|
const tabsContainer = tabsRef.current;
|
|
if (!tabsContainer) return undefined;
|
|
|
|
let animationFrameId = 0;
|
|
|
|
const refreshOptions = () => {
|
|
const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active");
|
|
if (!activePane) {
|
|
targetMapRef.current = new Map();
|
|
setOptions([]);
|
|
return;
|
|
}
|
|
|
|
const nextTargetMap = new Map();
|
|
const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row"))
|
|
.filter((card) => {
|
|
return shouldIncludeCardInNavigator(card, activePane);
|
|
})
|
|
.map((card, index) => {
|
|
const { title, depth, searchLabel } = getCardNavigatorInfo(card, activePane);
|
|
const value = `${activeTabKey}-shop-info-section-${index}`;
|
|
|
|
nextTargetMap.set(value, card);
|
|
|
|
return {
|
|
label: renderNavigatorOptionLabel(title, depth),
|
|
labelText: title,
|
|
searchLabel,
|
|
depth,
|
|
value
|
|
};
|
|
});
|
|
|
|
targetMapRef.current = nextTargetMap;
|
|
setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions));
|
|
};
|
|
|
|
const scheduleRefresh = () => {
|
|
cancelAnimationFrame(animationFrameId);
|
|
animationFrameId = requestAnimationFrame(refreshOptions);
|
|
};
|
|
|
|
scheduleRefresh();
|
|
|
|
const observer = new MutationObserver(scheduleRefresh);
|
|
observer.observe(tabsContainer, {
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
attributes: true,
|
|
attributeFilter: ["class"]
|
|
});
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animationFrameId);
|
|
observer.disconnect();
|
|
};
|
|
}, [activeTabKey, tabsRef]);
|
|
|
|
useEffect(() => {
|
|
clearHighlightedTarget(highlightedTargetRef);
|
|
setSelectedSection(undefined);
|
|
}, [activeTabKey]);
|
|
|
|
const handleSectionChange = (value) => {
|
|
setSelectedSection(value);
|
|
|
|
clearHighlightedTarget(highlightedTargetRef);
|
|
if (!value) return;
|
|
|
|
const target = targetMapRef.current.get(value);
|
|
if (target) {
|
|
target.classList.add(HIGHLIGHT_CLASS);
|
|
highlightedTargetRef.current = target;
|
|
target.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "start"
|
|
});
|
|
}
|
|
|
|
window.setTimeout(() => {
|
|
setSelectedSection(undefined);
|
|
}, 0);
|
|
};
|
|
|
|
return (
|
|
<div className="shop-info-section-navigator">
|
|
<Select
|
|
allowClear
|
|
showSearch
|
|
value={selectedSection}
|
|
placeholder={t("bodyshop.labels.jump_to_section")}
|
|
options={options}
|
|
popupMatchSelectWidth={false}
|
|
disabled={options.length === 0}
|
|
filterOption={(input, option) => option?.searchLabel?.toLowerCase().includes(input.toLowerCase())}
|
|
onChange={handleSectionChange}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getOwnCardTitleNode(card) {
|
|
const headNode = Array.from(card.children).find((child) => child.classList?.contains("ant-card-head"));
|
|
return headNode?.querySelector(".ant-card-head-title");
|
|
}
|
|
|
|
function getOwnCardTitle(card) {
|
|
return getOwnCardTitleNode(card)?.textContent?.trim();
|
|
}
|
|
|
|
function getAncestorCards(card, activePane) {
|
|
const ancestors = [];
|
|
let currentCard = card.parentElement?.closest(".imex-form-row");
|
|
|
|
while (currentCard && activePane.contains(currentCard)) {
|
|
ancestors.push(currentCard);
|
|
currentCard = currentCard.parentElement?.closest(".imex-form-row");
|
|
}
|
|
|
|
return ancestors.reverse();
|
|
}
|
|
|
|
function getCardDepth(card, activePane) {
|
|
return getAncestorCards(card, activePane).length;
|
|
}
|
|
|
|
function isVisibleCard(card) {
|
|
return card.offsetParent !== null;
|
|
}
|
|
|
|
function isNavigatorEligibleSubsection(card) {
|
|
return (
|
|
!card.classList.contains("imex-form-row--compact") &&
|
|
!card.classList.contains("imex-form-row--title-only") &&
|
|
!card.querySelector(":scope > .ant-card-actions")
|
|
);
|
|
}
|
|
|
|
function shouldIncludeCardInNavigator(card, activePane) {
|
|
const title = getOwnCardTitle(card);
|
|
if (!title || !isVisibleCard(card)) return false;
|
|
|
|
const depth = getCardDepth(card, activePane);
|
|
if (depth === 0) return true;
|
|
if (depth === 1) return isNavigatorEligibleSubsection(card);
|
|
|
|
return false;
|
|
}
|
|
|
|
function getCardNavigatorInfo(card, activePane) {
|
|
const title = getOwnCardTitle(card);
|
|
const ancestors = getAncestorCards(card, activePane);
|
|
const depth = ancestors.length;
|
|
const parentTitle = depth === 1 ? getOwnCardTitle(ancestors[0]) : null;
|
|
|
|
return {
|
|
title,
|
|
depth,
|
|
searchLabel: parentTitle ? `${parentTitle} ${title}` : title
|
|
};
|
|
}
|
|
|
|
function renderNavigatorOptionLabel(title, depth) {
|
|
return (
|
|
<span
|
|
className={[
|
|
"shop-info-section-navigator__option",
|
|
depth > 0 ? "shop-info-section-navigator__option--subsection" : null
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
>
|
|
<span className="shop-info-section-navigator__option-label">{title}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function clearHighlightedTarget(highlightedTargetRef) {
|
|
if (highlightedTargetRef.current) {
|
|
highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
|
|
highlightedTargetRef.current = null;
|
|
}
|
|
}
|
|
|
|
function areOptionsEqual(currentOptions, nextOptions) {
|
|
if (currentOptions.length !== nextOptions.length) return false;
|
|
|
|
return currentOptions.every((option, index) => {
|
|
const nextOption = nextOptions[index];
|
|
return (
|
|
option.labelText === nextOption.labelText &&
|
|
option.searchLabel === nextOption.searchLabel &&
|
|
option.depth === nextOption.depth &&
|
|
option.value === nextOption.value
|
|
);
|
|
});
|
|
}
|