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";
|
2023-12-19 15:00:51 +00:00
|
|
|
|
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";
|
2024-01-09 16:41:35 +00:00
|
|
|
|
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";
|
2023-12-20 17:31:51 +00:00
|
|
|
|
import { usePopper } from "./hooks/usePopper";
|
2024-01-05 16:48:35 +00:00
|
|
|
|
import { useRemoveNode } from "./hooks/useRemoveNode";
|
2023-12-20 18:38:28 +00:00
|
|
|
|
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-09 16:41:35 +00:00
|
|
|
|
Cytoscape.use(popper);
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-01-09 16:41:35 +00:00
|
|
|
|
type PopperInstance = ReturnType<getPopperInstance<SingularData>>;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
|
|
|
|
|
function CsComponent() {
|
|
|
|
|
|
const quiz = useCurrentQuiz();
|
|
|
|
|
|
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 [isPanningCy, setIsPanningCy] = useState<boolean>(false);
|
|
|
|
|
|
|
|
|
|
|
|
const cyRef = useRef<Core | null>(null);
|
|
|
|
|
|
const popperContainerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
const popperInstancesRef = useRef<PopperInstance[]>([]);
|
|
|
|
|
|
|
2024-01-09 16:41:35 +00:00
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
|
|
const { createPoppers, removeAllPoppers } = usePopper({
|
2024-01-05 16:48:35 +00:00
|
|
|
|
cyRef,
|
|
|
|
|
|
popperContainerRef,
|
|
|
|
|
|
popperInstancesRef,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const { removeNode } = useRemoveNode({
|
|
|
|
|
|
cyRef,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2024-01-09 16:41:35 +00:00
|
|
|
|
if (!cyRef.current) return;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-01-09 16:41:35 +00:00
|
|
|
|
addNode({
|
2024-01-05 16:48:35 +00:00
|
|
|
|
parentNodeContentId: modalQuestionParentContentId,
|
|
|
|
|
|
targetNodeContentId: modalQuestionTargetContentId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
setModalQuestionParentContentId("");
|
|
|
|
|
|
setModalQuestionTargetContentId("");
|
2024-01-09 16:41:35 +00:00
|
|
|
|
}, [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();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2024-01-09 16:41:35 +00:00
|
|
|
|
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;
|
|
|
|
|
|
cy.data("dragging", true);
|
|
|
|
|
|
};
|
|
|
|
|
|
const onPointerUp = () => {
|
|
|
|
|
|
isPointerDown = false;
|
|
|
|
|
|
cy.data("dragging", false);
|
|
|
|
|
|
setIsPanningCy(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
const handleMove = () => {
|
|
|
|
|
|
setIsPanningCy(isPointerDown);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(function poppersLifecycle() {
|
|
|
|
|
|
if (isPanningCy) {
|
|
|
|
|
|
removeAllPoppers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
createPoppers();
|
|
|
|
|
|
}
|
2024-01-09 16:41:35 +00:00
|
|
|
|
}, [isPanningCy, createPoppers]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
cyRef.current?.layout(layoutOptions).run();
|
|
|
|
|
|
cyRef.current?.fit(undefined, 70);
|
|
|
|
|
|
createPoppers();
|
|
|
|
|
|
}, [cyElements, createPoppers]);
|
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}
|
2024-01-09 16:41:35 +00:00
|
|
|
|
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} />
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
2023-12-01 19:56:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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 <></>;
|
2023-12-07 00:03:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default withErrorBoundary(CsComponent, {
|
2024-01-05 16:48:35 +00:00
|
|
|
|
fallback: <Clear />,
|
|
|
|
|
|
onError: (error, info) => {
|
|
|
|
|
|
enqueueSnackbar("Дерево порвалось");
|
|
|
|
|
|
devlog(info);
|
|
|
|
|
|
devlog(error);
|
|
|
|
|
|
},
|
2023-12-09 19:31:06 +00:00
|
|
|
|
});
|
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,
|
|
|
|
|
|
};
|