refactor cytoscape component and hooks
This commit is contained in:
parent
c38c75af9a
commit
b8b5bc5d2e
@ -1,251 +1,155 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { devlog } from "@frontend/kitui";
|
||||||
import Cytoscape from "cytoscape";
|
|
||||||
import CytoscapeComponent from "react-cytoscapejs";
|
|
||||||
import popper from "cytoscape-popper";
|
|
||||||
import { Button, Box } from "@mui/material";
|
|
||||||
import { withErrorBoundary } from "react-error-boundary";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
|
||||||
|
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
|
||||||
import { updateRootContentId } from "@root/quizes/actions";
|
|
||||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { Box, Button } from "@mui/material";
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
|
||||||
import {
|
import {
|
||||||
deleteQuestion,
|
clearRuleForAll
|
||||||
updateQuestion,
|
|
||||||
getQuestionByContentId,
|
|
||||||
clearRuleForAll,
|
|
||||||
createResult,
|
|
||||||
} from "@root/questions/actions";
|
} from "@root/questions/actions";
|
||||||
import {
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
updateModalInfoWhyCantCreate,
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
updateOpenedModalSettingsId
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
} from "@root/uiTools/actions";
|
import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQuestionTargetContentId, updateModalInfoWhyCantCreate, updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||||
import { cleardragQuestionContentId } from "@root/uiTools/actions";
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
import { updateDeleteId } from "@root/uiTools/actions";
|
|
||||||
|
|
||||||
import { DeleteNodeModal } from "../DeleteNodeModal";
|
|
||||||
import { ProblemIcon } from "@ui_kit/ProblemIcon";
|
import { ProblemIcon } from "@ui_kit/ProblemIcon";
|
||||||
|
import type { Core, PresetLayoutOptions, SingularData } from "cytoscape";
|
||||||
import { useRemoveNode } from "./hooks/useRemoveNode";
|
import Cytoscape from "cytoscape";
|
||||||
|
import popper, { getPopperInstance } from "cytoscape-popper";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
|
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";
|
import { usePopper } from "./hooks/usePopper";
|
||||||
|
import { useRemoveNode } from "./hooks/useRemoveNode";
|
||||||
import { storeToNodes } from "./helper";
|
|
||||||
import { stylesheet } from "./style/stylesheet";
|
|
||||||
import "./style/styles.css";
|
import "./style/styles.css";
|
||||||
|
import { stylesheet } from "./style/stylesheet";
|
||||||
import type { Core } from "cytoscape";
|
|
||||||
|
|
||||||
Cytoscape.use(popper);
|
Cytoscape.use(popper);
|
||||||
|
|
||||||
interface CsComponentProps {
|
type PopperInstance = ReturnType<getPopperInstance<SingularData>>;
|
||||||
modalQuestionParentContentId: string;
|
|
||||||
modalQuestionTargetContentId: string;
|
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
|
||||||
setModalQuestionParentContentId: (id: string) => void;
|
|
||||||
setModalQuestionTargetContentId: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CsComponent({
|
function CsComponent() {
|
||||||
modalQuestionParentContentId,
|
|
||||||
modalQuestionTargetContentId,
|
|
||||||
setOpenedModalQuestions,
|
|
||||||
setModalQuestionParentContentId,
|
|
||||||
setModalQuestionTargetContentId
|
|
||||||
}: CsComponentProps) {
|
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
const desireToOpenABranchingModal = useUiTools(state => state.desireToOpenABranchingModal);
|
||||||
const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic } = useUiTools()
|
const canCreatePublic = useUiTools(state => state.canCreatePublic);
|
||||||
const trashQuestions = useQuestionsStore().questions
|
const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId);
|
||||||
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null)
|
const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId);
|
||||||
const [startCreate, setStartCreate] = useState("");
|
const trashQuestions = useQuestionsStore(state => state.questions);
|
||||||
const [startRemove, setStartRemove] = useState("");
|
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null);
|
||||||
|
const [isPanningCy, setIsPanningCy] = useState<boolean>(false);
|
||||||
|
|
||||||
const cyRef = useRef<Core | null>(null);
|
const cyRef = useRef<Core | null>(null);
|
||||||
const layoutsContainer = useRef<HTMLDivElement | null>(null);
|
const popperContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const plusesContainer = useRef<HTMLDivElement | null>(null);
|
const popperInstancesRef = useRef<PopperInstance[]>([]);
|
||||||
const crossesContainer = useRef<HTMLDivElement | null>(null);
|
|
||||||
const gearsContainer = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const { layoutOptions } = usePopper({
|
const { createPoppers, removeAllPoppers, removePoppersById } = usePopper({
|
||||||
layoutsContainer,
|
cyRef,
|
||||||
plusesContainer,
|
quizId: quiz?.backendId,
|
||||||
crossesContainer,
|
runCyLayout,
|
||||||
gearsContainer,
|
popperContainerRef,
|
||||||
setModalQuestionParentContentId,
|
popperInstancesRef,
|
||||||
setOpenedModalQuestions,
|
|
||||||
setStartCreate,
|
|
||||||
setStartRemove,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function runCyLayout() {
|
||||||
|
cyRef.current?.layout(layoutOptions).run();
|
||||||
|
createPoppers();
|
||||||
|
};
|
||||||
|
|
||||||
const { removeNode } = useRemoveNode({
|
const { removeNode } = useRemoveNode({
|
||||||
cyRef,
|
cyRef,
|
||||||
layoutOptions,
|
runCyLayout,
|
||||||
layoutsContainer,
|
removeButtons: removePoppersById,
|
||||||
plusesContainer,
|
|
||||||
crossesContainer,
|
|
||||||
gearsContainer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
// if (!canCreatePublic) updateModalInfoWhyCantCreate(true)
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const cy = cyRef?.current
|
const cy = cyRef?.current;
|
||||||
if (desireToOpenABranchingModal) {
|
if (desireToOpenABranchingModal) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true)
|
cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true);
|
||||||
}, 250)
|
}, 250);
|
||||||
} else {
|
} else {
|
||||||
cy?.elements().data("eroticeyeblink", false)
|
cy?.elements().data("eroticeyeblink", false);
|
||||||
}
|
}
|
||||||
}, [desireToOpenABranchingModal])
|
}, [desireToOpenABranchingModal]);
|
||||||
useLayoutEffect(() => {
|
|
||||||
updateOpenedModalSettingsId()
|
|
||||||
// updateRootContentId(quiz.id, "")
|
|
||||||
// clearRuleForAll()
|
|
||||||
}, [])
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
|
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
|
||||||
addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId })
|
if (!cyRef.current || !quiz) return;
|
||||||
|
|
||||||
|
const es = addNode({
|
||||||
|
cy: cyRef.current,
|
||||||
|
quizId: quiz.backendId,
|
||||||
|
parentNodeContentId: modalQuestionParentContentId,
|
||||||
|
targetNodeContentId: modalQuestionTargetContentId,
|
||||||
|
});
|
||||||
|
runCyLayout();
|
||||||
|
if (es) cyRef.current.fit(es, 100);
|
||||||
}
|
}
|
||||||
setModalQuestionParentContentId("")
|
setModalQuestionParentContentId("");
|
||||||
setModalQuestionTargetContentId("")
|
setModalQuestionTargetContentId("");
|
||||||
}, [modalQuestionTargetContentId])
|
}, [modalQuestionTargetContentId, quiz?.backendId]);
|
||||||
|
|
||||||
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
|
useEffect(function onMount() {
|
||||||
if (quiz) {
|
updateOpenedModalSettingsId();
|
||||||
|
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
|
||||||
//запрещаем работу родителя-ребенка если это один и тот же вопрос
|
|
||||||
if (parentNodeContentId === targetNodeContentId) return
|
|
||||||
|
|
||||||
|
|
||||||
const cy = cyRef?.current
|
|
||||||
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
|
|
||||||
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
|
|
||||||
const targetQuestion = { ...getQuestionByContentId(targetNodeContentId || dragQuestionContentId) } as AnyTypedQuizQuestion
|
|
||||||
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
|
|
||||||
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
|
|
||||||
cy?.data('changed', true)
|
|
||||||
createResult(quiz.backendId, targetQuestion.content.id)
|
|
||||||
const es = cy?.add([
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
id: targetQuestion.content.id,
|
|
||||||
label: targetQuestion.title === "" || targetQuestion.title === " " ? "noname" : targetQuestion.title
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
source: parentNodeContentId,
|
|
||||||
target: targetQuestion.content.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
cy?.layout(layoutOptions).run()
|
|
||||||
cy?.center(es)
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar("Добавляемый вопрос не найден")
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar("Квиз не найден")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => {
|
|
||||||
|
|
||||||
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion
|
|
||||||
|
|
||||||
|
|
||||||
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
|
|
||||||
trashQuestions.forEach((targetQuestion) => {
|
|
||||||
if (targetQuestion.type === "result" && targetQuestion.content.rule.parentId === parentQuestion.content.id) {
|
|
||||||
updateQuestion(targetQuestion.id, (q) => q.content.usage = false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
//предупреждаем добавленный вопрос о том, кто его родитель
|
|
||||||
updateQuestion(targetQuestion.content.id, question => {
|
|
||||||
question.content.rule.parentId = parentNodeContentId
|
|
||||||
question.content.rule.main = []
|
|
||||||
//Это листик. Сбросим ему на всякий случай не листиковые поля
|
|
||||||
question.content.rule.children = []
|
|
||||||
question.content.rule.default = ""
|
|
||||||
})
|
|
||||||
|
|
||||||
const noChild = parentQuestion.content.rule.children.length === 0
|
|
||||||
|
|
||||||
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
|
|
||||||
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) updateQuestion(parentNodeContentId, question => {
|
|
||||||
question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id]
|
|
||||||
//единственному ребёнку даём дефолт по-умолчанию
|
|
||||||
question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!noChild) {//детей больше 1
|
|
||||||
//- предупреждаем стор вопросов об открытии модалки ветвления
|
|
||||||
updateOpenedModalSettingsId(targetQuestion.content.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (startCreate) {
|
|
||||||
addNode({ parentNodeContentId: startCreate });
|
|
||||||
cleardragQuestionContentId();
|
|
||||||
setStartCreate("");
|
|
||||||
}
|
|
||||||
}, [startCreate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (startRemove) {
|
|
||||||
updateDeleteId(startRemove);
|
|
||||||
setStartRemove("");
|
|
||||||
}
|
|
||||||
}, [startRemove]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document
|
|
||||||
.querySelector("#root")
|
|
||||||
?.addEventListener("mouseup", cleardragQuestionContentId);
|
|
||||||
const cy = cyRef.current;
|
const cy = cyRef.current;
|
||||||
const eles = cy?.add(
|
if (!cy) return;
|
||||||
|
|
||||||
|
cy.add(
|
||||||
storeToNodes(
|
storeToNodes(
|
||||||
questions.filter(
|
questions.filter((question) => question.type && question.type !== "result") as AnyTypedQuizQuestion[],
|
||||||
(question) => question.type && question.type !== "result"
|
),
|
||||||
) as AnyTypedQuizQuestion[]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
cy?.data("changed", true);
|
runCyLayout();
|
||||||
// cy.data('changed', true)
|
cy.fit();
|
||||||
const elecs = eles?.layout(layoutOptions).run();
|
|
||||||
cy?.on("add", () => cy.data("changed", true));
|
|
||||||
cy?.fit();
|
|
||||||
//cy?.layout().run()
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document
|
document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId);
|
||||||
.querySelector("#root")
|
removeAllPoppers();
|
||||||
?.removeEventListener("mouseup", cleardragQuestionContentId);
|
|
||||||
layoutsContainer.current?.remove();
|
|
||||||
plusesContainer.current?.remove();
|
|
||||||
crossesContainer.current?.remove();
|
|
||||||
gearsContainer.current?.remove();
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(function attachDragHandlers() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}, [isPanningCy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box mb="20px">
|
||||||
mb="20px">
|
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
height: "27px",
|
height: "27px",
|
||||||
@ -255,23 +159,28 @@ function CsComponent({
|
|||||||
}}
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
cyRef.current?.fit();
|
||||||
cyRef.current?.fit()
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Выровнять
|
Выровнять
|
||||||
</Button>
|
</Button>
|
||||||
<ProblemIcon blink={!canCreatePublic} onClick={() => updateModalInfoWhyCantCreate(true)} />
|
<ProblemIcon
|
||||||
|
blink={!canCreatePublic}
|
||||||
|
onClick={() => updateModalInfoWhyCantCreate(true)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<CytoscapeComponent
|
<CytoscapeComponent
|
||||||
wheelSensitivity={0.1}
|
wheelSensitivity={0.1}
|
||||||
elements={[]}
|
elements={[]}
|
||||||
// elements={createGraphElements(tree, quiz)}
|
// elements={createGraphElements(tree, quiz)}
|
||||||
style={{ height: "480px", background: "#F2F3F7" }}
|
style={{
|
||||||
|
height: "480px",
|
||||||
|
background: "#F2F3F7",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
stylesheet={stylesheet}
|
stylesheet={stylesheet}
|
||||||
layout={(layoutOptions)}
|
layout={layoutOptions}
|
||||||
cy={(cy) => {
|
cy={(cy) => {
|
||||||
cyRef.current = cy;
|
cyRef.current = cy;
|
||||||
}}
|
}}
|
||||||
@ -280,22 +189,44 @@ function CsComponent({
|
|||||||
<DeleteNodeModal removeNode={removeNode} />
|
<DeleteNodeModal removeNode={removeNode} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function Clear() {
|
function Clear() {
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
if (quiz) {
|
if (quiz) {
|
||||||
updateRootContentId(quiz?.id, "");
|
updateRootContentId(quiz?.id, "");
|
||||||
}
|
}
|
||||||
clearRuleForAll()
|
clearRuleForAll();
|
||||||
return <></>
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withErrorBoundary(CsComponent, {
|
export default withErrorBoundary(CsComponent, {
|
||||||
fallback: <Clear />,
|
fallback: <Clear />,
|
||||||
onError: (error, info) => {
|
onError: (error, info) => {
|
||||||
enqueueSnackbar("Дерево порвалось")
|
enqueueSnackbar("Дерево порвалось");
|
||||||
console.log(info)
|
devlog(info);
|
||||||
console.log(error)
|
devlog(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
@ -1,69 +1,65 @@
|
|||||||
import { Box } from "@mui/material"
|
import { Box } from "@mui/material";
|
||||||
import { useEffect, useRef, useLayoutEffect } from "react";
|
import { clearRuleForAll, createResult, updateQuestion } from "@root/questions/actions";
|
||||||
import { deleteQuestion, clearRuleForAll, updateQuestion, createResult } from "@root/questions/actions"
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions"
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { updateRootContentId } from "@root/quizes/actions"
|
import { setOpenedModalQuestions, updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
|
||||||
import { useQuestionsStore } from "@root/questions/store"
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
|
|
||||||
interface Props {
|
export const FirstNodeField = () => {
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
|
||||||
modalQuestionTargetContentId: string;
|
|
||||||
}
|
|
||||||
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
|
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
updateOpenedModalSettingsId()
|
if (!quiz) return;
|
||||||
updateRootContentId(quiz.id, "")
|
|
||||||
clearRuleForAll()
|
updateOpenedModalSettingsId();
|
||||||
}, [])
|
updateRootContentId(quiz.id, "");
|
||||||
|
clearRuleForAll();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const { questions } = useQuestionsStore()
|
const { dragQuestionContentId } = useUiTools();
|
||||||
const { dragQuestionContentId } = useUiTools()
|
|
||||||
const Container = useRef<HTMLDivElement | null>(null);
|
const Container = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const modalOpen = () => setOpenedModalQuestions(true)
|
const modalOpen = () => setOpenedModalQuestions(true);
|
||||||
|
|
||||||
const newRootNode = () => {
|
const newRootNode = () => {
|
||||||
if (quiz) {
|
if (quiz) {
|
||||||
if (dragQuestionContentId) {
|
if (dragQuestionContentId) {
|
||||||
updateRootContentId(quiz?.id, dragQuestionContentId)
|
updateRootContentId(quiz?.id, dragQuestionContentId);
|
||||||
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
|
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root");
|
||||||
createResult(quiz?.backendId, dragQuestionContentId)
|
createResult(quiz?.backendId, dragQuestionContentId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enqueueSnackbar("Нет информации о взятом опроснике")
|
enqueueSnackbar("Нет информации о взятом опроснике");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Container.current?.addEventListener("mouseup", newRootNode)
|
Container.current?.addEventListener("mouseup", newRootNode);
|
||||||
Container.current?.addEventListener("click", modalOpen)
|
Container.current?.addEventListener("click", modalOpen);
|
||||||
return () => {
|
return () => {
|
||||||
Container.current?.removeEventListener("mouseup", newRootNode)
|
Container.current?.removeEventListener("mouseup", newRootNode);
|
||||||
Container.current?.removeEventListener("click", modalOpen)
|
Container.current?.removeEventListener("click", modalOpen);
|
||||||
}
|
};
|
||||||
}, [dragQuestionContentId])
|
}, [dragQuestionContentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (quiz) {
|
if (quiz) {
|
||||||
|
|
||||||
if (modalQuestionTargetContentId) {
|
if (modalQuestionTargetContentId) {
|
||||||
updateRootContentId(quiz?.id, modalQuestionTargetContentId)
|
updateRootContentId(quiz?.id, modalQuestionTargetContentId);
|
||||||
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root")
|
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root");
|
||||||
createResult(quiz?.backendId, modalQuestionTargetContentId)
|
createResult(quiz?.backendId, modalQuestionTargetContentId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enqueueSnackbar("Нет информации о взятом опроснике")
|
enqueueSnackbar("Нет информации о взятом опроснике");
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [modalQuestionTargetContentId])
|
}, [modalQuestionTargetContentId]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -82,5 +78,5 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
|
|||||||
>
|
>
|
||||||
+
|
+
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,28 +1,39 @@
|
|||||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"
|
import { devlog } from "@frontend/kitui";
|
||||||
|
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||||
|
import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain, UntypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
import { Quiz } from "@model/quiz/quiz";
|
||||||
|
import { createResult, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||||
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
|
import { Core } from "cytoscape";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
interface Nodes {
|
interface Nodes {
|
||||||
data: {
|
data: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
parent?: string;
|
parent?: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
interface Edges {
|
interface Edges {
|
||||||
data: {
|
data: {
|
||||||
source: string;
|
source: string;
|
||||||
target: string;
|
target: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
||||||
const nodes: Nodes[] = []
|
const nodes: Nodes[] = [];
|
||||||
const edges: Edges[] = []
|
const edges: Edges[] = [];
|
||||||
questions.forEach((question) => {
|
questions.forEach((question) => {
|
||||||
if (question.content.rule.parentId) {
|
if (question.content.rule.parentId) {
|
||||||
nodes.push({data: {
|
nodes.push({
|
||||||
|
data: {
|
||||||
id: question.content.id,
|
id: question.content.id,
|
||||||
label: question.title === "" || question.title === " " ? "noname" : question.title
|
label: question.title === "" || question.title === " " ? "noname" : question.title
|
||||||
}})
|
}
|
||||||
|
});
|
||||||
// nodes.push({
|
// nodes.push({
|
||||||
// data: {
|
// data: {
|
||||||
// id: "delete" + question.content.id,
|
// id: "delete" + question.content.id,
|
||||||
@ -30,11 +41,249 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
|||||||
// parent: question.content.id,
|
// parent: question.content.id,
|
||||||
// }
|
// }
|
||||||
// },)
|
// },)
|
||||||
if (question.content.rule.parentId !== "root") edges.push({data: {
|
if (question.content.rule.parentId !== "root") edges.push({
|
||||||
|
data: {
|
||||||
source: question.content.rule.parentId,
|
source: question.content.rule.parentId,
|
||||||
target: question.content.id
|
target: question.content.id
|
||||||
}})
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
return [...nodes, ...edges];
|
return [...nodes, ...edges];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clearDataAfterAddNode({
|
||||||
|
parentNodeContentId,
|
||||||
|
targetQuestion,
|
||||||
|
}: {
|
||||||
|
parentNodeContentId: string;
|
||||||
|
targetQuestion: AnyTypedQuizQuestion;
|
||||||
|
}) {
|
||||||
|
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion;
|
||||||
|
|
||||||
|
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
|
||||||
|
useQuestionsStore.getState().questions.filter(
|
||||||
|
(question): question is QuizQuestionResult => question.type === "result"
|
||||||
|
).forEach((targetQuestion) => {
|
||||||
|
if (
|
||||||
|
targetQuestion.content.rule.parentId === parentQuestion.content.id
|
||||||
|
) {
|
||||||
|
updateQuestion<QuizQuestionResult>(targetQuestion.id, (q) => (q.content.usage = false));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//предупреждаем добавленный вопрос о том, кто его родитель
|
||||||
|
updateQuestion(targetQuestion.content.id, (question) => {
|
||||||
|
question.content.rule.parentId = parentNodeContentId;
|
||||||
|
question.content.rule.main = [];
|
||||||
|
//Это листик. Сбросим ему на всякий случай не листиковые поля
|
||||||
|
question.content.rule.children = [];
|
||||||
|
question.content.rule.default = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const noChild = parentQuestion.content.rule.children.length === 0;
|
||||||
|
|
||||||
|
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
|
||||||
|
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id))
|
||||||
|
updateQuestion(parentNodeContentId, (question) => {
|
||||||
|
question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id];
|
||||||
|
//единственному ребёнку даём дефолт по-умолчанию
|
||||||
|
question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!noChild) {
|
||||||
|
//детей больше 1
|
||||||
|
//- предупреждаем стор вопросов об открытии модалки ветвления
|
||||||
|
updateOpenedModalSettingsId(targetQuestion.content.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clearDataAfterRemoveNode({
|
||||||
|
quiz,
|
||||||
|
trashQuestions,
|
||||||
|
targetQuestionContentId,
|
||||||
|
parentQuestionContentId,
|
||||||
|
}: {
|
||||||
|
quiz: Quiz | undefined;
|
||||||
|
trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[],
|
||||||
|
targetQuestionContentId: string;
|
||||||
|
parentQuestionContentId: string;
|
||||||
|
}) {
|
||||||
|
updateQuestion(targetQuestionContentId, (question) => {
|
||||||
|
question.content.rule.parentId = "";
|
||||||
|
question.content.rule.children = [];
|
||||||
|
question.content.rule.main = [];
|
||||||
|
question.content.rule.default = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
//Ищём родителя
|
||||||
|
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
|
||||||
|
|
||||||
|
//Делаем результат родителя активным
|
||||||
|
const parentResult = trashQuestions.find(
|
||||||
|
(q): q is QuizQuestionResult => q.type === "result" && q.content.rule.parentId === parentQuestionContentId,
|
||||||
|
);
|
||||||
|
if (parentResult) {
|
||||||
|
updateQuestion<QuizQuestionResult>(parentResult.content.id, (q) => {
|
||||||
|
q.content.usage = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createResult(quiz?.backendId, parentQuestionContentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//чистим rule родителя
|
||||||
|
if (!parentQuestion?.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChildren = [...parentQuestion.content.rule.children];
|
||||||
|
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
|
||||||
|
|
||||||
|
const newRule: QuestionBranchingRule = {
|
||||||
|
children: newChildren,
|
||||||
|
default:
|
||||||
|
parentQuestion.content.rule.default === targetQuestionContentId
|
||||||
|
? ""
|
||||||
|
: parentQuestion.content.rule.default,
|
||||||
|
//удаляем условия перехода от родителя к этому вопросу,
|
||||||
|
main: parentQuestion.content.rule.main.filter(
|
||||||
|
(data: QuestionBranchingRuleMain) => data.next !== targetQuestionContentId,
|
||||||
|
),
|
||||||
|
parentId: parentQuestion.content.rule.parentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateQuestion(parentQuestionContentId, (PQ) => {
|
||||||
|
PQ.content.rule = newRule;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function calcNodePosition(node: any) {
|
||||||
|
const id = node.id();
|
||||||
|
const incomming = node.cy().edges(`[target="${id}"]`);
|
||||||
|
const layer = 0;
|
||||||
|
node.removeData("lastChild");
|
||||||
|
|
||||||
|
if (incomming.length === 0) {
|
||||||
|
if (node.cy().data("firstNode") === undefined) node.cy().data("firstNode", "root");
|
||||||
|
node.data("root", true);
|
||||||
|
const children = node.cy().edges(`[source="${id}"]`).targets();
|
||||||
|
node.data("layer", layer);
|
||||||
|
node.data("children", children.length);
|
||||||
|
const queue: any[] = [];
|
||||||
|
children.forEach((n: any) => {
|
||||||
|
queue.push({ task: n, layer: layer + 1 });
|
||||||
|
});
|
||||||
|
while (queue.length) {
|
||||||
|
const task = queue.pop();
|
||||||
|
task.task.data("layer", task.layer);
|
||||||
|
task.task.removeData("subtreeWidth");
|
||||||
|
const children = node.cy().edges(`[source="${task.task.id()}"]`).targets();
|
||||||
|
task.task.data("children", children.length);
|
||||||
|
if (children.length !== 0) {
|
||||||
|
children.forEach((n: any) => queue.push({ task: n, layer: task.layer + 1 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue.push({ parent: node, children: children });
|
||||||
|
while (queue.length) {
|
||||||
|
const task = queue.pop();
|
||||||
|
if (task.children.length === 0) {
|
||||||
|
task.parent.data("subtreeWidth", task.parent.height() + 50);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const unprocessed = task?.children.filter((node: any) => {
|
||||||
|
return node.data("subtreeWidth") === undefined;
|
||||||
|
});
|
||||||
|
if (unprocessed.length !== 0) {
|
||||||
|
queue.push(task);
|
||||||
|
unprocessed.forEach((t: any) => {
|
||||||
|
queue.push({
|
||||||
|
parent: t,
|
||||||
|
children: t.cy().edges(`[source="${t.id()}"]`).targets(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
task?.parent.data(
|
||||||
|
"subtreeWidth",
|
||||||
|
task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = { x: 0, y: 0 };
|
||||||
|
node.data("oldPos", pos);
|
||||||
|
|
||||||
|
queue.push({ task: children, parent: node });
|
||||||
|
while (queue.length) {
|
||||||
|
const task = queue.pop();
|
||||||
|
const oldPos = task.parent.data("oldPos");
|
||||||
|
let yoffset = oldPos.y - task.parent.data("subtreeWidth") / 2;
|
||||||
|
task.task.forEach((n: any) => {
|
||||||
|
const width = n.data("subtreeWidth");
|
||||||
|
|
||||||
|
n.data("oldPos", {
|
||||||
|
x: 250 * n.data("layer"),
|
||||||
|
y: yoffset + width / 2,
|
||||||
|
});
|
||||||
|
yoffset += width;
|
||||||
|
queue.push({
|
||||||
|
task: n.cy().edges(`[source="${n.id()}"]`).targets(),
|
||||||
|
parent: n,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
} else {
|
||||||
|
const opos = node.data("oldPos");
|
||||||
|
if (opos) {
|
||||||
|
return opos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addNode = ({
|
||||||
|
cy,
|
||||||
|
quizId,
|
||||||
|
parentNodeContentId,
|
||||||
|
targetNodeContentId,
|
||||||
|
}: {
|
||||||
|
cy: Core;
|
||||||
|
quizId: number;
|
||||||
|
parentNodeContentId: string;
|
||||||
|
targetNodeContentId?: string;
|
||||||
|
}) => {
|
||||||
|
//запрещаем работу родителя-ребенка если это один и тот же вопрос
|
||||||
|
if (parentNodeContentId === targetNodeContentId) return;
|
||||||
|
devlog("@addNode");
|
||||||
|
|
||||||
|
const parentNodeChildren = cy.$('edge[source = "' + parentNodeContentId + '"]')?.length;
|
||||||
|
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
|
||||||
|
const targetQuestion = {
|
||||||
|
...getQuestionByContentId(targetNodeContentId || useUiTools.getState().dragQuestionContentId),
|
||||||
|
} as AnyTypedQuizQuestion;
|
||||||
|
|
||||||
|
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
|
||||||
|
clearDataAfterAddNode({ parentNodeContentId, targetQuestion });
|
||||||
|
createResult(quizId, targetQuestion.content.id);
|
||||||
|
const es = cy.add([
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
id: targetQuestion.content.id,
|
||||||
|
label:
|
||||||
|
targetQuestion.title === "" || targetQuestion.title === " "
|
||||||
|
? "noname"
|
||||||
|
: targetQuestion.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
source: parentNodeContentId,
|
||||||
|
target: targetQuestion.content.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return es;
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Добавляемый вопрос не найден");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,23 +1,8 @@
|
|||||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
import { cleardragQuestionContentId, setModalQuestionParentContentId, setOpenedModalQuestions, updateDeleteId, updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||||
|
import type { AbstractEventObject, Core, NodeSingular, SingularData } from "cytoscape";
|
||||||
import type { MutableRefObject } from "react";
|
import { getPopperInstance } from "cytoscape-popper";
|
||||||
import type {
|
import { type MutableRefObject } from "react";
|
||||||
PresetLayoutOptions,
|
import { addNode } from "../helper";
|
||||||
LayoutEventObject,
|
|
||||||
NodeSingular,
|
|
||||||
AbstractEventObject,
|
|
||||||
} from "cytoscape";
|
|
||||||
|
|
||||||
type usePopperArgs = {
|
|
||||||
layoutsContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
plusesContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
crossesContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
gearsContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
setModalQuestionParentContentId: (id: string) => void;
|
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
|
||||||
setStartCreate: (id: string) => void;
|
|
||||||
setStartRemove: (id: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PopperItem = {
|
type PopperItem = {
|
||||||
id: () => string;
|
id: () => string;
|
||||||
@ -36,82 +21,58 @@ type PopperConfig = {
|
|||||||
content: (items: PopperItem[]) => void;
|
content: (items: PopperItem[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Popper = {
|
type PopperInstance = ReturnType<getPopperInstance<SingularData>>;
|
||||||
update: () => Promise<void>;
|
|
||||||
setOptions: (modifiers: { modifiers?: Modifier[] }) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NodeSingularWithPopper = NodeSingular & {
|
type NodeSingularWithPopper = NodeSingular & {
|
||||||
popper: (config: PopperConfig) => Popper;
|
popper: (config: PopperConfig) => PopperInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePopper = ({
|
export const usePopper = ({
|
||||||
layoutsContainer,
|
cyRef,
|
||||||
plusesContainer,
|
quizId,
|
||||||
crossesContainer,
|
popperContainerRef,
|
||||||
gearsContainer,
|
popperInstancesRef,
|
||||||
setModalQuestionParentContentId,
|
runCyLayout,
|
||||||
setOpenedModalQuestions,
|
}: {
|
||||||
setStartCreate,
|
cyRef: MutableRefObject<Core | null>;
|
||||||
setStartRemove,
|
quizId: number | undefined,
|
||||||
}: usePopperArgs) => {
|
popperContainerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
const removeButtons = (id: string) => {
|
popperInstancesRef: MutableRefObject<PopperInstance[]>;
|
||||||
layoutsContainer.current
|
runCyLayout: () => void;
|
||||||
?.querySelector(`.popper-layout[data-id='${id}']`)
|
}) => {
|
||||||
?.remove();
|
const removePoppersById = (id: string) => {
|
||||||
plusesContainer.current
|
popperContainerRef.current?.querySelector(`.popper-layout[data-id='${id}']`)?.remove();
|
||||||
?.querySelector(`.popper-plus[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
crossesContainer.current
|
|
||||||
?.querySelector(`.popper-cross[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
gearsContainer.current
|
|
||||||
?.querySelector(`.popper-gear[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialPopperIcons = ({ cy }: LayoutEventObject) => {
|
const removeAllPoppers = () => {
|
||||||
const container =
|
cyRef.current?.removeListener("zoom render");
|
||||||
(document.body.querySelector(
|
|
||||||
".__________cytoscape_container"
|
popperInstancesRef.current.forEach(p => p.destroy());
|
||||||
) as HTMLDivElement) || null;
|
popperInstancesRef.current = [];
|
||||||
|
popperContainerRef.current?.remove();
|
||||||
|
popperContainerRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPoppers = () => {
|
||||||
|
removeAllPoppers();
|
||||||
|
|
||||||
|
const cy = cyRef.current;
|
||||||
|
if (!cy) return;
|
||||||
|
|
||||||
|
const container = cy.container();
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
|
console.warn("Cannot create popper container");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.style.overflow = "hidden";
|
if (!popperContainerRef.current) {
|
||||||
|
popperContainerRef.current = document.createElement("div");
|
||||||
if (!plusesContainer.current) {
|
popperContainerRef.current.setAttribute("id", "poppers-container");
|
||||||
plusesContainer.current = document.createElement("div");
|
container.append(popperContainerRef.current);
|
||||||
plusesContainer.current.setAttribute("id", "popper-pluses");
|
|
||||||
container.append(plusesContainer.current);
|
|
||||||
}
|
|
||||||
if (!crossesContainer.current) {
|
|
||||||
crossesContainer.current = document.createElement("div");
|
|
||||||
crossesContainer.current.setAttribute("id", "popper-crosses");
|
|
||||||
container.append(crossesContainer.current);
|
|
||||||
}
|
|
||||||
if (!gearsContainer.current) {
|
|
||||||
gearsContainer.current = document.createElement("div");
|
|
||||||
gearsContainer.current.setAttribute("id", "popper-gears");
|
|
||||||
container.append(gearsContainer.current);
|
|
||||||
}
|
|
||||||
if (!layoutsContainer.current) {
|
|
||||||
layoutsContainer.current = document.createElement("div");
|
|
||||||
layoutsContainer.current.setAttribute("id", "popper-layouts");
|
|
||||||
container.append(layoutsContainer.current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = cy.extent();
|
cy.nodes().forEach((item) => {
|
||||||
const nodesInView = cy.nodes().filter((n) => {
|
|
||||||
const bb = n.boundingBox();
|
|
||||||
return (
|
|
||||||
bb.x2 > ext.x1 && bb.x1 < ext.x2 && bb.y2 > ext.y1 && bb.y1 < ext.y2
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
nodesInView.toArray()?.forEach((item) => {
|
|
||||||
const node = item as NodeSingularWithPopper;
|
const node = item as NodeSingularWithPopper;
|
||||||
|
|
||||||
const layoutsPopper = node.popper({
|
const layoutsPopper = node.popper({
|
||||||
@ -119,11 +80,10 @@ export const usePopper = ({
|
|||||||
placement: "left",
|
placement: "left",
|
||||||
modifiers: [{ name: "flip", options: { boundary: node } }],
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
},
|
},
|
||||||
content: ([item]) => {
|
content: (items) => {
|
||||||
|
const item = items[0];
|
||||||
const itemId = item.id();
|
const itemId = item.id();
|
||||||
const itemElement = layoutsContainer.current?.querySelector(
|
const itemElement = popperContainerRef.current?.querySelector(`.popper-layout[data-id='${itemId}']`);
|
||||||
`.popper-layout[data-id='${itemId}']`
|
|
||||||
);
|
|
||||||
if (itemElement) {
|
if (itemElement) {
|
||||||
return itemElement;
|
return itemElement;
|
||||||
}
|
}
|
||||||
@ -137,11 +97,12 @@ export const usePopper = ({
|
|||||||
setModalQuestionParentContentId(item.id());
|
setModalQuestionParentContentId(item.id());
|
||||||
setOpenedModalQuestions(true);
|
setOpenedModalQuestions(true);
|
||||||
});
|
});
|
||||||
layoutsContainer.current?.appendChild(layoutElement);
|
popperContainerRef.current?.appendChild(layoutElement);
|
||||||
|
|
||||||
return layoutElement;
|
return layoutElement;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
popperInstancesRef.current.push(layoutsPopper);
|
||||||
|
|
||||||
const plusesPopper = node.popper({
|
const plusesPopper = node.popper({
|
||||||
popper: {
|
popper: {
|
||||||
@ -150,9 +111,7 @@ export const usePopper = ({
|
|||||||
},
|
},
|
||||||
content: ([item]) => {
|
content: ([item]) => {
|
||||||
const itemId = item.id();
|
const itemId = item.id();
|
||||||
const itemElement = plusesContainer.current?.querySelector(
|
const itemElement = popperContainerRef.current?.querySelector(`.popper-plus[data-id='${itemId}']`);
|
||||||
`.popper-plus[data-id='${itemId}']`
|
|
||||||
);
|
|
||||||
if (itemElement) {
|
if (itemElement) {
|
||||||
return itemElement;
|
return itemElement;
|
||||||
}
|
}
|
||||||
@ -162,14 +121,24 @@ export const usePopper = ({
|
|||||||
plusElement.setAttribute("data-id", item.id());
|
plusElement.setAttribute("data-id", item.id());
|
||||||
plusElement.style.zIndex = "1";
|
plusElement.style.zIndex = "1";
|
||||||
plusElement.addEventListener("mouseup", () => {
|
plusElement.addEventListener("mouseup", () => {
|
||||||
setStartCreate(node.id());
|
if (!cy || !quizId) return;
|
||||||
|
|
||||||
|
const es = addNode({
|
||||||
|
cy,
|
||||||
|
quizId,
|
||||||
|
parentNodeContentId: node.id(),
|
||||||
|
});
|
||||||
|
runCyLayout();
|
||||||
|
if (es) cy.fit(es, 100);
|
||||||
|
cleardragQuestionContentId();
|
||||||
});
|
});
|
||||||
|
|
||||||
plusesContainer.current?.appendChild(plusElement);
|
popperContainerRef.current?.appendChild(plusElement);
|
||||||
|
|
||||||
return plusElement;
|
return plusElement;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
popperInstancesRef.current.push(plusesPopper);
|
||||||
|
|
||||||
const crossesPopper = node.popper({
|
const crossesPopper = node.popper({
|
||||||
popper: {
|
popper: {
|
||||||
@ -178,9 +147,7 @@ export const usePopper = ({
|
|||||||
},
|
},
|
||||||
content: ([item]) => {
|
content: ([item]) => {
|
||||||
const itemId = item.id();
|
const itemId = item.id();
|
||||||
const itemElement = crossesContainer.current?.querySelector(
|
const itemElement = popperContainerRef.current?.querySelector(`.popper-cross[data-id='${itemId}']`);
|
||||||
`.popper-cross[data-id='${itemId}']`
|
|
||||||
);
|
|
||||||
if (itemElement) {
|
if (itemElement) {
|
||||||
return itemElement;
|
return itemElement;
|
||||||
}
|
}
|
||||||
@ -189,15 +156,17 @@ export const usePopper = ({
|
|||||||
crossElement.classList.add("popper-cross");
|
crossElement.classList.add("popper-cross");
|
||||||
crossElement.setAttribute("data-id", item.id());
|
crossElement.setAttribute("data-id", item.id());
|
||||||
crossElement.style.zIndex = "2";
|
crossElement.style.zIndex = "2";
|
||||||
crossesContainer.current?.appendChild(crossElement);
|
popperContainerRef.current?.appendChild(crossElement);
|
||||||
crossElement.addEventListener("mouseup", () => {
|
crossElement.addEventListener("mouseup", () => {
|
||||||
setStartRemove(node.id());
|
updateDeleteId(node.id());
|
||||||
});
|
});
|
||||||
|
|
||||||
return crossElement;
|
return crossElement;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let gearsPopper: Popper | null = null;
|
popperInstancesRef.current.push(crossesPopper);
|
||||||
|
|
||||||
|
let gearsPopper: PopperInstance | null = null;
|
||||||
if (node.data().root !== true) {
|
if (node.data().root !== true) {
|
||||||
gearsPopper = node.popper({
|
gearsPopper = node.popper({
|
||||||
popper: {
|
popper: {
|
||||||
@ -207,9 +176,7 @@ export const usePopper = ({
|
|||||||
content: ([item]) => {
|
content: ([item]) => {
|
||||||
const itemId = item.id();
|
const itemId = item.id();
|
||||||
|
|
||||||
const itemElement = gearsContainer.current?.querySelector(
|
const itemElement = popperContainerRef.current?.querySelector(`.popper-gear[data-id='${itemId}']`);
|
||||||
`.popper-gear[data-id='${itemId}']`
|
|
||||||
);
|
|
||||||
if (itemElement) {
|
if (itemElement) {
|
||||||
return itemElement;
|
return itemElement;
|
||||||
}
|
}
|
||||||
@ -218,7 +185,7 @@ export const usePopper = ({
|
|||||||
gearElement.classList.add("popper-gear");
|
gearElement.classList.add("popper-gear");
|
||||||
gearElement.setAttribute("data-id", item.id());
|
gearElement.setAttribute("data-id", item.id());
|
||||||
gearElement.style.zIndex = "1";
|
gearElement.style.zIndex = "1";
|
||||||
gearsContainer.current?.appendChild(gearElement);
|
popperContainerRef.current?.appendChild(gearElement);
|
||||||
gearElement.addEventListener("mouseup", () => {
|
gearElement.addEventListener("mouseup", () => {
|
||||||
updateOpenedModalSettingsId(item.id());
|
updateOpenedModalSettingsId(item.id());
|
||||||
});
|
});
|
||||||
@ -226,19 +193,13 @@ export const usePopper = ({
|
|||||||
return gearElement;
|
return gearElement;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
popperInstancesRef.current.push(gearsPopper);
|
||||||
}
|
}
|
||||||
const update = async () => {
|
|
||||||
await plusesPopper.update();
|
|
||||||
await crossesPopper.update();
|
|
||||||
await gearsPopper?.update();
|
|
||||||
await layoutsPopper.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onZoom = (event: AbstractEventObject) => {
|
const onZoom = (event: AbstractEventObject) => {
|
||||||
|
if (event.cy.data("dragging")) return;
|
||||||
const zoom = event.cy.zoom();
|
const zoom = event.cy.zoom();
|
||||||
|
|
||||||
//update();
|
|
||||||
|
|
||||||
crossesPopper.setOptions({
|
crossesPopper.setOptions({
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{ name: "flip", options: { boundary: node } },
|
{ name: "flip", options: { boundary: node } },
|
||||||
@ -255,7 +216,7 @@ export const usePopper = ({
|
|||||||
plusesPopper.setOptions({
|
plusesPopper.setOptions({
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{ name: "flip", options: { boundary: node } },
|
{ name: "flip", options: { boundary: node } },
|
||||||
{ name: "offset", options: { offset: [0, 0 * zoom] } },
|
{ name: "offset", options: { offset: [0, 0] } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
gearsPopper?.setOptions({
|
gearsPopper?.setOptions({
|
||||||
@ -265,17 +226,13 @@ export const usePopper = ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
layoutsContainer.current
|
popperContainerRef.current?.querySelectorAll(".popper-layout").forEach((item) => {
|
||||||
?.querySelectorAll("#popper-layouts > .popper-layout")
|
|
||||||
.forEach((item) => {
|
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${130 * zoom}px`;
|
element.style.width = `${130 * zoom}px`;
|
||||||
element.style.height = `${130 * zoom}px`;
|
element.style.height = `${130 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
plusesContainer.current
|
popperContainerRef.current?.querySelectorAll(".popper-plus").forEach((item) => {
|
||||||
?.querySelectorAll("#popper-pluses > .popper-plus")
|
|
||||||
.forEach((item) => {
|
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${40 * zoom}px`;
|
element.style.width = `${40 * zoom}px`;
|
||||||
element.style.height = `${40 * zoom}px`;
|
element.style.height = `${40 * zoom}px`;
|
||||||
@ -283,9 +240,7 @@ export const usePopper = ({
|
|||||||
element.style.borderRadius = `${6 * zoom}px`;
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
crossesContainer.current
|
popperContainerRef.current?.querySelectorAll(".popper-cross").forEach((item) => {
|
||||||
?.querySelectorAll("#popper-crosses > .popper-cross")
|
|
||||||
.forEach((item) => {
|
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${24 * zoom}px`;
|
element.style.width = `${24 * zoom}px`;
|
||||||
element.style.height = `${24 * zoom}px`;
|
element.style.height = `${24 * zoom}px`;
|
||||||
@ -293,187 +248,17 @@ export const usePopper = ({
|
|||||||
element.style.borderRadius = `${6 * zoom}px`;
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
gearsContainer?.current
|
popperContainerRef?.current?.querySelectorAll(".popper-gear").forEach((item) => {
|
||||||
?.querySelectorAll("#popper-gears > .popper-gear")
|
|
||||||
.forEach((item) => {
|
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${60 * zoom}px`;
|
element.style.width = `${60 * zoom}px`;
|
||||||
element.style.height = `${40 * zoom}px`;
|
element.style.height = `${40 * zoom}px`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
//node?.on("position", update);
|
cy.on("zoom render", onZoom);
|
||||||
let pressed = false;
|
|
||||||
let hide = false;
|
|
||||||
cy?.on("mousedown", () => {
|
|
||||||
pressed = true;
|
|
||||||
});
|
|
||||||
cy?.on("mouseup", () => {
|
|
||||||
pressed = false;
|
|
||||||
hide = false;
|
|
||||||
|
|
||||||
const gc = gearsContainer.current;
|
|
||||||
if (gc) gc.style.display = "block";
|
|
||||||
const pc = plusesContainer.current;
|
|
||||||
const xc = crossesContainer.current;
|
|
||||||
const lc = layoutsContainer.current;
|
|
||||||
if (pc) pc.style.display = "block";
|
|
||||||
if (xc) xc.style.display = "block";
|
|
||||||
if (lc) lc.style.display = "block";
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
|
|
||||||
cy?.on("mousemove", () => {
|
|
||||||
if (pressed && !hide) {
|
|
||||||
hide = true;
|
|
||||||
const gc = gearsContainer.current;
|
|
||||||
if (gc) gc.style.display = "none";
|
|
||||||
const pc = plusesContainer.current;
|
|
||||||
const xc = crossesContainer.current;
|
|
||||||
const lc = layoutsContainer.current;
|
|
||||||
if (pc) pc.style.display = "none";
|
|
||||||
if (xc) xc.style.display = "none";
|
|
||||||
if (lc) lc.style.display = "block";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cy?.on("zoom render", onZoom);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const readyLO = (event: LayoutEventObject) => {
|
|
||||||
if (event.cy.data("firstNode") === "nonroot") {
|
|
||||||
event.cy.data("firstNode", "root");
|
|
||||||
event.cy
|
|
||||||
.nodes()
|
|
||||||
.sort((a, b) => (a.data("root") ? 1 : -1))
|
|
||||||
.layout(layoutOptions)
|
|
||||||
.run();
|
|
||||||
} else {
|
|
||||||
event.cy.data("changed", false);
|
|
||||||
event.cy.removeData("firstNode");
|
|
||||||
}
|
|
||||||
|
|
||||||
//удаляем иконки
|
return { removeAllPoppers, removePoppersById, createPoppers };
|
||||||
event.cy.nodes().forEach((ele: any) => {
|
|
||||||
const data = ele.data();
|
|
||||||
data.id && removeButtons(data.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
initialPopperIcons(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const layoutOptions: PresetLayoutOptions = {
|
|
||||||
name: "preset",
|
|
||||||
|
|
||||||
positions: (node) => {
|
|
||||||
if (!node.cy().data("changed")) {
|
|
||||||
return node.data("oldPos");
|
|
||||||
}
|
|
||||||
const id = node.id();
|
|
||||||
const incomming = node.cy().edges(`[target="${id}"]`);
|
|
||||||
const layer = 0;
|
|
||||||
node.removeData("lastChild");
|
|
||||||
|
|
||||||
if (incomming.length === 0) {
|
|
||||||
if (node.cy().data("firstNode") === undefined)
|
|
||||||
node.cy().data("firstNode", "root");
|
|
||||||
node.data("root", true);
|
|
||||||
const children = node.cy().edges(`[source="${id}"]`).targets();
|
|
||||||
node.data("layer", layer);
|
|
||||||
node.data("children", children.length);
|
|
||||||
const queue = [];
|
|
||||||
children.forEach((n) => {
|
|
||||||
queue.push({ task: n, layer: layer + 1 });
|
|
||||||
});
|
|
||||||
while (queue.length) {
|
|
||||||
const task = queue.pop();
|
|
||||||
task.task.data("layer", task.layer);
|
|
||||||
task.task.removeData("subtreeWidth");
|
|
||||||
const children = node
|
|
||||||
.cy()
|
|
||||||
.edges(`[source="${task.task.id()}"]`)
|
|
||||||
.targets();
|
|
||||||
task.task.data("children", children.length);
|
|
||||||
if (children.length !== 0) {
|
|
||||||
children.forEach((n) =>
|
|
||||||
queue.push({ task: n, layer: task.layer + 1 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queue.push({ parent: node, children: children });
|
|
||||||
while (queue.length) {
|
|
||||||
const task = queue.pop();
|
|
||||||
if (task.children.length === 0) {
|
|
||||||
task.parent.data("subtreeWidth", task.parent.height() + 50);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const unprocessed = task?.children.filter((node) => {
|
|
||||||
return node.data("subtreeWidth") === undefined;
|
|
||||||
});
|
|
||||||
if (unprocessed.length !== 0) {
|
|
||||||
queue.push(task);
|
|
||||||
unprocessed.forEach((t) => {
|
|
||||||
queue.push({
|
|
||||||
parent: t,
|
|
||||||
children: t.cy().edges(`[source="${t.id()}"]`).targets(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
task?.parent.data(
|
|
||||||
"subtreeWidth",
|
|
||||||
task.children.reduce((p, n) => p + n.data("subtreeWidth"), 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = { x: 0, y: 0 };
|
|
||||||
node.data("oldPos", pos);
|
|
||||||
|
|
||||||
queue.push({ task: children, parent: node });
|
|
||||||
while (queue.length) {
|
|
||||||
const task = queue.pop();
|
|
||||||
const oldPos = task.parent.data("oldPos");
|
|
||||||
let yoffset = oldPos.y - task.parent.data("subtreeWidth") / 2;
|
|
||||||
task.task.forEach((n) => {
|
|
||||||
const width = n.data("subtreeWidth");
|
|
||||||
|
|
||||||
n.data("oldPos", {
|
|
||||||
x: 250 * n.data("layer"),
|
|
||||||
y: yoffset + width / 2,
|
|
||||||
});
|
|
||||||
yoffset += width;
|
|
||||||
queue.push({
|
|
||||||
task: n.cy().edges(`[source="${n.id()}"]`).targets(),
|
|
||||||
parent: n,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
node.cy().data("changed", false);
|
|
||||||
return pos;
|
|
||||||
} else {
|
|
||||||
const opos = node.data("oldPos");
|
|
||||||
if (opos) {
|
|
||||||
return opos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, // map of (node id) => (position obj); or function(node){ return somPos; }
|
|
||||||
zoom: undefined, // the zoom level to set (prob want fit = false if set)
|
|
||||||
pan: 1, // the pan level to set (prob want fit = false if set)
|
|
||||||
fit: false, // whether to fit to viewport
|
|
||||||
padding: 30, // padding on fit
|
|
||||||
animate: false, // whether to transition the node positions
|
|
||||||
animationDuration: 500, // duration of animation in ms if enabled
|
|
||||||
animationEasing: undefined, // easing of animation if enabled
|
|
||||||
animateFilter: function (node, i) {
|
|
||||||
return false;
|
|
||||||
}, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
|
||||||
ready: readyLO, // callback on layoutready
|
|
||||||
transform: function (node, position) {
|
|
||||||
return position;
|
|
||||||
}, // transform a given node position. Useful for changing flow direction in discrete layouts
|
|
||||||
};
|
|
||||||
|
|
||||||
return { layoutOptions };
|
|
||||||
};
|
};
|
||||||
|
@ -1,126 +1,36 @@
|
|||||||
import {
|
import { devlog } from "@frontend/kitui";
|
||||||
deleteQuestion,
|
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||||
updateQuestion,
|
import { clearRuleForAll, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
|
||||||
getQuestionByContentId,
|
|
||||||
clearRuleForAll,
|
|
||||||
createResult,
|
|
||||||
} from "@root/questions/actions";
|
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
|
||||||
import { updateRootContentId } from "@root/quizes/actions";
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import type { CollectionReturnValue, Core, SingularElementArgument } from "cytoscape";
|
||||||
import type { MutableRefObject } from "react";
|
import type { MutableRefObject } from "react";
|
||||||
import type {
|
import { clearDataAfterRemoveNode } from "../helper";
|
||||||
Core,
|
|
||||||
CollectionReturnValue,
|
|
||||||
PresetLayoutOptions,
|
|
||||||
} from "cytoscape";
|
|
||||||
import type {
|
|
||||||
AnyTypedQuizQuestion,
|
|
||||||
QuestionBranchingRule,
|
|
||||||
QuestionBranchingRuleMain,
|
|
||||||
} from "../../../../model/questionTypes/shared";
|
|
||||||
|
|
||||||
type UseRemoveNodeArgs = {
|
type UseRemoveNodeArgs = {
|
||||||
cyRef: MutableRefObject<Core | null>;
|
cyRef: MutableRefObject<Core | null>;
|
||||||
layoutOptions: PresetLayoutOptions;
|
runCyLayout: () => void;
|
||||||
layoutsContainer: MutableRefObject<HTMLDivElement | null>;
|
removeButtons: (id: string) => void;
|
||||||
plusesContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
crossesContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
gearsContainer: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRemoveNode = ({
|
export const useRemoveNode = ({
|
||||||
cyRef,
|
cyRef,
|
||||||
layoutOptions,
|
runCyLayout,
|
||||||
layoutsContainer,
|
removeButtons,
|
||||||
plusesContainer,
|
|
||||||
crossesContainer,
|
|
||||||
gearsContainer,
|
|
||||||
}: UseRemoveNodeArgs) => {
|
}: UseRemoveNodeArgs) => {
|
||||||
const { questions: trashQuestions } = useQuestionsStore();
|
const { questions: trashQuestions } = useQuestionsStore();
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
const removeButtons = (id: string) => {
|
|
||||||
layoutsContainer.current
|
|
||||||
?.querySelector(`.popper-layout[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
plusesContainer.current
|
|
||||||
?.querySelector(`.popper-plus[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
crossesContainer.current
|
|
||||||
?.querySelector(`.popper-cross[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
gearsContainer.current
|
|
||||||
?.querySelector(`.popper-gear[data-id='${id}']`)
|
|
||||||
?.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearDataAfterRemoveNode = ({
|
|
||||||
targetQuestionContentId,
|
|
||||||
parentQuestionContentId,
|
|
||||||
}: {
|
|
||||||
targetQuestionContentId: string;
|
|
||||||
parentQuestionContentId: string;
|
|
||||||
}) => {
|
|
||||||
updateQuestion(targetQuestionContentId, (question) => {
|
|
||||||
question.content.rule.parentId = "";
|
|
||||||
question.content.rule.children = [];
|
|
||||||
question.content.rule.main = [];
|
|
||||||
question.content.rule.default = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
//Ищём родителя
|
|
||||||
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
|
|
||||||
|
|
||||||
//Делаем результат родителя активным
|
|
||||||
const parentResult = trashQuestions.find(q => q.type === "result" && q.content.rule.parentId === parentQuestionContentId)
|
|
||||||
if (parentResult) {
|
|
||||||
updateQuestion(parentResult.content.id, q => {
|
|
||||||
q.content.usage = true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
createResult(quiz?.backendId, parentQuestionContentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
//чистим rule родителя
|
|
||||||
if (!parentQuestion?.type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newChildren = [...parentQuestion.content.rule.children];
|
|
||||||
newChildren.splice(
|
|
||||||
parentQuestion.content.rule.children.indexOf(targetQuestionContentId),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
const newRule: QuestionBranchingRule = {
|
|
||||||
children: newChildren,
|
|
||||||
default:
|
|
||||||
parentQuestion.content.rule.default === targetQuestionContentId
|
|
||||||
? ""
|
|
||||||
: parentQuestion.content.rule.default,
|
|
||||||
//удаляем условия перехода от родителя к этому вопросу,
|
|
||||||
main: parentQuestion.content.rule.main.filter(
|
|
||||||
(data: QuestionBranchingRuleMain) =>
|
|
||||||
data.next !== targetQuestionContentId
|
|
||||||
),
|
|
||||||
parentId: parentQuestion.content.rule.parentId,
|
|
||||||
};
|
|
||||||
|
|
||||||
updateQuestion(parentQuestionContentId, (PQ) => {
|
|
||||||
PQ.content.rule = newRule;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeNode = (targetNodeContentId: string) => {
|
const removeNode = (targetNodeContentId: string) => {
|
||||||
const deleteNodes: string[] = [];
|
const deleteNodes: string[] = [];
|
||||||
const deleteEdges: any = [];
|
const deleteEdges: SingularElementArgument[] = [];
|
||||||
const cy = cyRef?.current;
|
const cy = cyRef?.current;
|
||||||
|
|
||||||
const findChildrenToDelete = (node: CollectionReturnValue) => {
|
const findChildrenToDelete = (node: CollectionReturnValue) => {
|
||||||
//Узнаём грани, идущие от этой ноды
|
//Узнаём грани, идущие от этой ноды
|
||||||
cy?.$('edge[source = "' + node.id() + '"]')
|
cy
|
||||||
|
?.$('edge[source = "' + node.id() + '"]')
|
||||||
?.toArray()
|
?.toArray()
|
||||||
.forEach((edge) => {
|
.forEach((edge) => {
|
||||||
const edgeData = edge.data();
|
const edgeData = edge.data();
|
||||||
@ -143,12 +53,8 @@ export const useRemoveNode = ({
|
|||||||
|
|
||||||
const targetQuestion = getQuestionByContentId(targetNodeContentId);
|
const targetQuestion = getQuestionByContentId(targetNodeContentId);
|
||||||
|
|
||||||
if (
|
if (targetQuestion?.type && targetQuestion.content.rule.parentId === "root" && quiz) {
|
||||||
targetQuestion?.type &&
|
updateRootContentId(quiz.id, "");
|
||||||
targetQuestion.content.rule.parentId === "root" &&
|
|
||||||
quiz
|
|
||||||
) {
|
|
||||||
updateRootContentId(quiz?.id, "");
|
|
||||||
updateQuestion(targetNodeContentId, (question) => {
|
updateQuestion(targetNodeContentId, (question) => {
|
||||||
question.content.rule.parentId = "";
|
question.content.rule.parentId = "";
|
||||||
question.content.rule.main = [];
|
question.content.rule.main = [];
|
||||||
@ -162,20 +68,17 @@ export const useRemoveNode = ({
|
|||||||
?.toArray()?.[0]
|
?.toArray()?.[0]
|
||||||
?.data()?.source;
|
?.data()?.source;
|
||||||
if (targetNodeContentId && parentQuestionContentId) {
|
if (targetNodeContentId && parentQuestionContentId) {
|
||||||
if (
|
if (quiz && cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0) {
|
||||||
quiz &&
|
devlog(parentQuestionContentId);
|
||||||
cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0
|
|
||||||
) {
|
|
||||||
console.log(parentQuestionContentId)
|
|
||||||
//createFrontResult(quiz.backendId, parentQuestionContentId);
|
//createFrontResult(quiz.backendId, parentQuestionContentId);
|
||||||
}
|
}
|
||||||
clearDataAfterRemoveNode({
|
clearDataAfterRemoveNode({
|
||||||
|
quiz,
|
||||||
|
trashQuestions,
|
||||||
targetQuestionContentId: targetNodeContentId,
|
targetQuestionContentId: targetNodeContentId,
|
||||||
parentQuestionContentId,
|
parentQuestionContentId,
|
||||||
});
|
});
|
||||||
cy?.remove(cy?.$("#" + targetNodeContentId))
|
cy?.remove(cy?.$("#" + targetNodeContentId));
|
||||||
.layout(layoutOptions)
|
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,20 +102,18 @@ export const useRemoveNode = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
removeButtons(targetNodeContentId);
|
removeButtons(targetNodeContentId);
|
||||||
cy?.data("changed", true);
|
runCyLayout();
|
||||||
cy?.layout(layoutOptions).run();
|
|
||||||
|
|
||||||
//делаем result всех потомков неактивными
|
//делаем result всех потомков неактивными
|
||||||
trashQuestions.forEach((qr) => {
|
trashQuestions.forEach((qr) => {
|
||||||
if (
|
if (
|
||||||
qr.type === "result" &&
|
qr.type === "result" &&
|
||||||
(deleteNodes.includes(qr.content.rule.parentId || "") ||
|
(deleteNodes.includes(qr.content.rule.parentId || "") ||
|
||||||
(targetQuestion?.type &&
|
(targetQuestion?.type && qr.content.rule.parentId === targetQuestion.content.id))
|
||||||
qr.content.rule.parentId === targetQuestion.content.id))
|
|
||||||
) {
|
) {
|
||||||
updateQuestion(qr.content.id, q => {
|
updateQuestion<QuizQuestionResult>(qr.content.id, (q) => {
|
||||||
q.content.usage = false
|
q.content.usage = false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { FirstNodeField } from "./FirstNodeField";
|
|
||||||
import CsComponent from "./CsComponent";
|
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
|
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
|
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
|
||||||
|
import CsComponent from "./CsComponent";
|
||||||
|
import { FirstNodeField } from "./FirstNodeField";
|
||||||
|
|
||||||
export const BranchingMap = () => {
|
export const BranchingMap = () => {
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const { dragQuestionContentId } = useUiTools();
|
const dragQuestionContentId = useUiTools(state => state.dragQuestionContentId);
|
||||||
const [modalQuestionParentContentId, setModalQuestionParentContentId] =
|
|
||||||
useState<string>("");
|
|
||||||
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] =
|
|
||||||
useState<string>("");
|
|
||||||
const [openedModalQuestions, setOpenedModalQuestions] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -31,24 +24,11 @@ export const BranchingMap = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{quiz?.config.haveRoot ? (
|
{quiz?.config.haveRoot ? (
|
||||||
<CsComponent
|
<CsComponent />
|
||||||
modalQuestionParentContentId={modalQuestionParentContentId}
|
|
||||||
modalQuestionTargetContentId={modalQuestionTargetContentId}
|
|
||||||
setOpenedModalQuestions={setOpenedModalQuestions}
|
|
||||||
setModalQuestionParentContentId={setModalQuestionParentContentId}
|
|
||||||
setModalQuestionTargetContentId={setModalQuestionTargetContentId}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FirstNodeField
|
<FirstNodeField />
|
||||||
setOpenedModalQuestions={setOpenedModalQuestions}
|
|
||||||
modalQuestionTargetContentId={modalQuestionTargetContentId}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<BranchingQuestionsModal
|
<BranchingQuestionsModal />
|
||||||
openedModalQuestions={openedModalQuestions}
|
|
||||||
setOpenedModalQuestions={setOpenedModalQuestions}
|
|
||||||
setModalQuestionTargetContentId={setModalQuestionTargetContentId}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#popper-pluses > .popper-plus {
|
.popper-plus {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -9,13 +9,13 @@
|
|||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-pluses > .popper-plus::before {
|
.popper-plus::before {
|
||||||
content: "+";
|
content: "+";
|
||||||
color: rgba(154, 154, 175, 0.5);
|
color: rgba(154, 154, 175, 0.5);
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-crosses > .popper-cross {
|
.popper-cross {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -25,14 +25,14 @@
|
|||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-crosses > .popper-cross::before {
|
.popper-cross::before {
|
||||||
content: "+";
|
content: "+";
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-gears > .popper-gear {
|
.popper-gear {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,22 +1,16 @@
|
|||||||
import { Box, Modal, Button, Typography } from "@mui/material";
|
import { Box, Modal, Button, Typography } from "@mui/material";
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
|
import { setModalQuestionTargetContentId, setOpenedModalQuestions } from "@root/uiTools/actions";
|
||||||
|
|
||||||
interface Props {
|
|
||||||
openedModalQuestions: boolean;
|
|
||||||
setModalQuestionTargetContentId: (contentId: string) => void;
|
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BranchingQuestionsModal = ({
|
export const BranchingQuestionsModal = () => {
|
||||||
openedModalQuestions,
|
|
||||||
setOpenedModalQuestions,
|
|
||||||
setModalQuestionTargetContentId,
|
|
||||||
}: Props) => {
|
|
||||||
const trashQuestions = useQuestionsStore().questions;
|
const trashQuestions = useQuestionsStore().questions;
|
||||||
const questions = trashQuestions.filter(
|
const questions = trashQuestions.filter(
|
||||||
(question) => question.type !== "result"
|
(question) => question.type !== "result"
|
||||||
);
|
);
|
||||||
|
const openedModalQuestions = useUiTools(state => state.openedModalQuestions);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpenedModalQuestions(false);
|
setOpenedModalQuestions(false);
|
||||||
|
@ -498,7 +498,7 @@ export const clearRuleForAll = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createResult = async (
|
export const createResult = async (
|
||||||
quizId: number,
|
quizId: number | undefined,
|
||||||
parentContentId?: string
|
parentContentId?: string
|
||||||
) => requestQueue.enqueue(async () => {
|
) => requestQueue.enqueue(async () => {
|
||||||
if (!quizId || !parentContentId) {
|
if (!quizId || !parentContentId) {
|
||||||
@ -543,4 +543,3 @@ export const createResult = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,3 +38,7 @@ export const updateCanCreatePublic = (can: boolean) => useUiTools.setState({ can
|
|||||||
|
|
||||||
export const updateModalInfoWhyCantCreate = (can: boolean) => useUiTools.setState({ openModalInfoWhyCantCreate: can });
|
export const updateModalInfoWhyCantCreate = (can: boolean) => useUiTools.setState({ openModalInfoWhyCantCreate: can });
|
||||||
export const updateDeleteId = (deleteNodeId: string | null = null) => useUiTools.setState({ deleteNodeId });
|
export const updateDeleteId = (deleteNodeId: string | null = null) => useUiTools.setState({ deleteNodeId });
|
||||||
|
|
||||||
|
export const setModalQuestionParentContentId = (modalQuestionParentContentId: string) => useUiTools.setState({ modalQuestionParentContentId });
|
||||||
|
export const setModalQuestionTargetContentId = (modalQuestionTargetContentId: string) => useUiTools.setState({ modalQuestionTargetContentId });
|
||||||
|
export const setOpenedModalQuestions = (open: boolean) => useUiTools.setState({ openedModalQuestions: open });
|
||||||
|
@ -11,6 +11,9 @@ export type UiTools = {
|
|||||||
whyCantCreatePublic: Record<string, WhyCantCreatePublic>//ид вопроса и список претензий к нему
|
whyCantCreatePublic: Record<string, WhyCantCreatePublic>//ид вопроса и список претензий к нему
|
||||||
openModalInfoWhyCantCreate: boolean;
|
openModalInfoWhyCantCreate: boolean;
|
||||||
deleteNodeId: string | null;
|
deleteNodeId: string | null;
|
||||||
|
modalQuestionParentContentId: string;
|
||||||
|
modalQuestionTargetContentId: string;
|
||||||
|
openedModalQuestions: boolean;
|
||||||
};
|
};
|
||||||
export type WhyCantCreatePublic = {
|
export type WhyCantCreatePublic = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -28,6 +31,9 @@ const initialState: UiTools = {
|
|||||||
whyCantCreatePublic: {},
|
whyCantCreatePublic: {},
|
||||||
openModalInfoWhyCantCreate: false,
|
openModalInfoWhyCantCreate: false,
|
||||||
deleteNodeId: null,
|
deleteNodeId: null,
|
||||||
|
modalQuestionParentContentId: "",
|
||||||
|
modalQuestionTargetContentId: "",
|
||||||
|
openedModalQuestions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUiTools = create<UiTools>()(
|
export const useUiTools = create<UiTools>()(
|
||||||
|
Loading…
Reference in New Issue
Block a user