frontPanel/src/pages/Questions/BranchingMap/CsComponent.tsx

204 lines
6.8 KiB
TypeScript
Raw Normal View History

2024-01-05 16:48:35 +00:00
import { devlog } from "@frontend/kitui";
2023-12-20 10:46:38 +00:00
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
2024-01-05 16:48:35 +00:00
import { Box, Button } from "@mui/material";
2023-12-20 10:46:38 +00:00
import {
2024-01-05 16:48:35 +00:00
clearRuleForAll
2023-12-20 10:46:38 +00:00
} from "@root/questions/actions";
2024-01-05 16:48:35 +00:00
import { useQuestionsStore } from "@root/questions/store";
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQuestionTargetContentId, updateModalInfoWhyCantCreate, updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import { ProblemIcon } from "@ui_kit/ProblemIcon";
2024-01-05 16:48:35 +00:00
import type { Core, PresetLayoutOptions, SingularData } from "cytoscape";
import Cytoscape from "cytoscape";
import popper, { getPopperInstance } from "cytoscape-popper";
import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2024-01-05 16:48:35 +00:00
import CytoscapeComponent from "react-cytoscapejs";
import { withErrorBoundary } from "react-error-boundary";
import { DeleteNodeModal } from "../DeleteNodeModal";
import { addNode, calcNodePosition, storeToNodes } from "./helper";
import { usePopper } from "./hooks/usePopper";
2024-01-05 16:48:35 +00:00
import { useRemoveNode } from "./hooks/useRemoveNode";
import "./style/styles.css";
2024-01-05 16:48:35 +00:00
import { stylesheet } from "./style/stylesheet";
2023-11-29 15:45:15 +00:00
2024-01-05 16:48:35 +00:00
2024-01-10 10:02:03 +00:00
Cytoscape.use(popper);
2024-01-05 16:48:35 +00:00
function CsComponent() {
const desireToOpenABranchingModal = useUiTools(state => state.desireToOpenABranchingModal);
const canCreatePublic = useUiTools(state => state.canCreatePublic);
const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId);
const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId);
const trashQuestions = useQuestionsStore(state => state.questions);
const cyRef = useRef<Core | null>(null);
const questions = useMemo(() => trashQuestions.filter(
(question) => question.type !== "result" && question.type !== null
), [trashQuestions]);
const cyElements = useMemo(() => {
const q = questions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
);
return storeToNodes(q);
}, [questions]);
2024-01-10 17:11:47 +00:00
const { recreatePoppers, removeAllPoppers } = usePopper({ cyRef });
2024-01-05 16:48:35 +00:00
2024-01-10 10:02:03 +00:00
const { removeNode } = useRemoveNode({ cyRef });
2024-01-05 16:48:35 +00:00
useLayoutEffect(() => {
const cy = cyRef?.current;
if (desireToOpenABranchingModal) {
setTimeout(() => {
cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true);
}, 250);
} else {
cy?.elements().data("eroticeyeblink", false);
}
}, [desireToOpenABranchingModal]);
useEffect(() => {
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
if (!cyRef.current) return;
2024-01-05 16:48:35 +00:00
addNode({
2024-01-05 16:48:35 +00:00
parentNodeContentId: modalQuestionParentContentId,
targetNodeContentId: modalQuestionTargetContentId,
});
}
setModalQuestionParentContentId("");
setModalQuestionTargetContentId("");
}, [modalQuestionTargetContentId]);
2024-01-05 16:48:35 +00:00
useEffect(function onMount() {
updateOpenedModalSettingsId();
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
return () => {
document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId);
removeAllPoppers();
};
}, []);
useEffect(function removePoppersOnDrag() {
2024-01-05 16:48:35 +00:00
const cy = cyRef.current;
if (!cy) return;
let isPointerDown = false;
const onPointerDown = () => {
isPointerDown = true;
};
const onPointerUp = () => {
2024-01-10 17:11:47 +00:00
if (isPointerDown) recreatePoppers();
2024-01-05 16:48:35 +00:00
isPointerDown = false;
};
const handleMove = () => {
2024-01-10 17:11:47 +00:00
if (isPointerDown) removeAllPoppers();
2024-01-05 16:48:35 +00:00
};
cy.on("vmousedown", onPointerDown);
cy.on("vmousemove", handleMove);
document.addEventListener("pointerup", onPointerUp);
return () => {
cy.off("vmousedown", onPointerDown);
cy.off("vmousemove", handleMove);
document.removeEventListener("pointerup", onPointerUp);
};
2024-01-10 17:11:47 +00:00
}, [recreatePoppers, removeAllPoppers]);
useEffect(() => {
cyRef.current?.layout(layoutOptions).run();
cyRef.current?.fit(undefined, 70);
2024-01-10 17:11:47 +00:00
recreatePoppers();
}, [cyElements, recreatePoppers]);
2024-01-05 16:48:35 +00:00
return (
<>
<Box mb="20px">
<Button
sx={{
height: "27px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
}}
variant="text"
onClick={() => {
cyRef.current?.fit();
}}
>
Выровнять
</Button>
<ProblemIcon
blink={!canCreatePublic}
onClick={() => updateModalInfoWhyCantCreate(true)}
/>
</Box>
<CytoscapeComponent
wheelSensitivity={0.1}
elements={cyElements}
2024-01-05 16:48:35 +00:00
style={{
height: "480px",
background: "#F2F3F7",
overflow: "hidden",
}}
stylesheet={stylesheet}
layout={layoutOptions}
cy={(cy) => {
cyRef.current = cy;
}}
autoungrabify={true}
/>
<DeleteNodeModal removeNode={removeNode} />
</>
);
}
2024-01-05 16:48:35 +00:00
function Clear() {
const quiz = useCurrentQuiz();
2023-12-22 21:14:48 +00:00
if (quiz) {
2024-01-05 16:48:35 +00:00
updateRootContentId(quiz?.id, "");
2023-11-29 15:45:15 +00:00
}
2024-01-05 16:48:35 +00:00
clearRuleForAll();
return <></>;
}
export default withErrorBoundary(CsComponent, {
2024-01-05 16:48:35 +00:00
fallback: <Clear />,
onError: (error, info) => {
enqueueSnackbar("Дерево порвалось");
devlog(info);
devlog(error);
},
});
2024-01-05 16:48:35 +00:00
const layoutOptions: PresetLayoutOptions = {
name: "preset",
positions: calcNodePosition,
zoom: undefined,
pan: 1,
fit: false,
padding: 30,
animate: false,
animationDuration: 500,
animationEasing: undefined,
animateFilter: () => false,
ready: event => {
if (event.cy.data("firstNode") === "nonroot") {
event.cy.data("firstNode", "root");
event.cy.nodes().sort((a, b) => (a.data("root") ? 1 : -1));
} else {
event.cy.removeData("firstNode");
}
},
transform: (_, p) => p,
};