refactor: Cytoscape components
This commit is contained in:
parent
fa2dedeed6
commit
2e7c4d4de6
File diff suppressed because it is too large
Load Diff
@ -1,94 +1,98 @@
|
|||||||
import { Box } from "@mui/material"
|
import { Box } from "@mui/material";
|
||||||
import { useEffect, useRef, useLayoutEffect } from "react";
|
import { useEffect, useRef, useLayoutEffect } from "react";
|
||||||
import { deleteQuestion, clearRuleForAll, updateQuestion } from "@root/questions/actions"
|
import {
|
||||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions"
|
deleteQuestion,
|
||||||
import { updateRootContentId } from "@root/quizes/actions"
|
clearRuleForAll,
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
updateQuestion,
|
||||||
import { useQuestionsStore } from "@root/questions/store"
|
} from "@root/questions/actions";
|
||||||
|
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||||
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
setOpenedModalQuestions: (open: boolean) => void;
|
||||||
modalQuestionTargetContentId: string;
|
modalQuestionTargetContentId: string;
|
||||||
}
|
}
|
||||||
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
|
export const FirstNodeField = ({
|
||||||
const quiz = useCurrentQuiz();
|
setOpenedModalQuestions,
|
||||||
|
modalQuestionTargetContentId,
|
||||||
|
}: Props) => {
|
||||||
useLayoutEffect(() => {
|
const quiz = useCurrentQuiz();
|
||||||
updateOpenedModalSettingsId()
|
|
||||||
console.log("first render firstComponent")
|
|
||||||
updateRootContentId(quiz.id, "")
|
|
||||||
clearRuleForAll()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
|
||||||
const { questions } = useQuestionsStore()
|
|
||||||
const { dragQuestionContentId } = useUiTools()
|
|
||||||
const Container = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const modalOpen = () => setOpenedModalQuestions(true)
|
|
||||||
|
|
||||||
const newRootNode = () => {
|
|
||||||
if (quiz) {
|
|
||||||
if (dragQuestionContentId) {
|
|
||||||
updateRootContentId(quiz?.id, dragQuestionContentId)
|
|
||||||
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
|
|
||||||
//если были результаты - удалить
|
|
||||||
questions.forEach((q) => {
|
|
||||||
if (q.type === 'result') deleteQuestion(q.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar("Нет информации о взятом опроснике")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
updateOpenedModalSettingsId();
|
||||||
|
console.log("first render firstComponent");
|
||||||
|
if (quiz) {
|
||||||
|
updateRootContentId(quiz.id, "");
|
||||||
}
|
}
|
||||||
|
clearRuleForAll();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const { questions } = useQuestionsStore();
|
||||||
Container.current?.addEventListener("mouseup", newRootNode)
|
const { dragQuestionContentId } = useUiTools();
|
||||||
Container.current?.addEventListener("click", modalOpen)
|
const Container = useRef<HTMLDivElement | null>(null);
|
||||||
return () => {
|
|
||||||
Container.current?.removeEventListener("mouseup", newRootNode)
|
|
||||||
Container.current?.removeEventListener("click", modalOpen)
|
|
||||||
}
|
|
||||||
}, [dragQuestionContentId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const modalOpen = () => setOpenedModalQuestions(true);
|
||||||
if (quiz) {
|
|
||||||
|
|
||||||
if (modalQuestionTargetContentId) {
|
const newRootNode = () => {
|
||||||
updateRootContentId(quiz?.id, modalQuestionTargetContentId)
|
if (quiz && dragQuestionContentId) {
|
||||||
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root")
|
updateRootContentId(quiz?.id, dragQuestionContentId);
|
||||||
//если были результаты - удалить
|
updateQuestion(
|
||||||
questions.forEach((q) => {
|
dragQuestionContentId,
|
||||||
if (q.type === 'result') deleteQuestion(q.id)
|
(question) => (question.content.rule.parentId = "root")
|
||||||
})
|
);
|
||||||
}
|
//если были результаты - удалить
|
||||||
} else {
|
questions.forEach((q) => {
|
||||||
enqueueSnackbar("Нет информации о взятом опроснике")
|
if (q.type === "result") deleteQuestion(q.id);
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Нет информации о взятом опроснике");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}, [modalQuestionTargetContentId])
|
useEffect(() => {
|
||||||
|
Container.current?.addEventListener("mouseup", newRootNode);
|
||||||
|
Container.current?.addEventListener("click", modalOpen);
|
||||||
|
return () => {
|
||||||
|
Container.current?.removeEventListener("mouseup", newRootNode);
|
||||||
|
Container.current?.removeEventListener("click", modalOpen);
|
||||||
|
};
|
||||||
|
}, [dragQuestionContentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (quiz && modalQuestionTargetContentId) {
|
||||||
|
updateRootContentId(quiz?.id, modalQuestionTargetContentId);
|
||||||
|
updateQuestion(
|
||||||
|
modalQuestionTargetContentId,
|
||||||
|
(question) => (question.content.rule.parentId = "root")
|
||||||
|
);
|
||||||
|
//если были результаты - удалить
|
||||||
|
questions.forEach((q) => {
|
||||||
|
if (q.type === "result") deleteQuestion(q.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Нет информации о взятом опроснике");
|
||||||
|
}
|
||||||
|
}, [modalQuestionTargetContentId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={Container}
|
ref={Container}
|
||||||
sx={{
|
sx={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: "#f2f3f7",
|
backgroundColor: "#f2f3f7",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: "#4d4d4d",
|
color: "#4d4d4d",
|
||||||
fontSize: "50px"
|
fontSize: "50px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
482
src/pages/Questions/BranchingMap/hooks/usePopper.ts
Normal file
482
src/pages/Questions/BranchingMap/hooks/usePopper.ts
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||||
|
|
||||||
|
import type { MutableRefObject } from "react";
|
||||||
|
import type {
|
||||||
|
PresetLayoutOptions,
|
||||||
|
LayoutEventObject,
|
||||||
|
NodeSingular,
|
||||||
|
NodePositionMap,
|
||||||
|
NodePositionFunction,
|
||||||
|
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 = {
|
||||||
|
id: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Modifier = {
|
||||||
|
name: string;
|
||||||
|
options: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PopperConfig = {
|
||||||
|
popper: {
|
||||||
|
placement: string;
|
||||||
|
modifiers?: Modifier[];
|
||||||
|
};
|
||||||
|
content: (items: PopperItem[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Popper = {
|
||||||
|
update: () => Promise<void>;
|
||||||
|
setOptions: (modifiers: { modifiers?: Modifier[] }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeSingularWithPopper = NodeSingular & {
|
||||||
|
popper: (config: PopperConfig) => Popper;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePopper = ({
|
||||||
|
layoutsContainer,
|
||||||
|
plusesContainer,
|
||||||
|
crossesContainer,
|
||||||
|
gearsContainer,
|
||||||
|
setModalQuestionParentContentId,
|
||||||
|
setOpenedModalQuestions,
|
||||||
|
setStartCreate,
|
||||||
|
setStartRemove,
|
||||||
|
}: usePopperArgs) => {
|
||||||
|
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 initialPopperIcons = ({ cy }: LayoutEventObject) => {
|
||||||
|
const container =
|
||||||
|
(document.body.querySelector(
|
||||||
|
".__________cytoscape_container"
|
||||||
|
) as HTMLDivElement) || null;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.overflow = "hidden";
|
||||||
|
|
||||||
|
if (!plusesContainer.current) {
|
||||||
|
plusesContainer.current = document.createElement("div");
|
||||||
|
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();
|
||||||
|
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 layoutsPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "left",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = layoutsContainer.current?.querySelector(
|
||||||
|
`.popper-layout[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutElement = document.createElement("div");
|
||||||
|
layoutElement.style.zIndex = "0";
|
||||||
|
layoutElement.classList.add("popper-layout");
|
||||||
|
layoutElement.setAttribute("data-id", item.id());
|
||||||
|
layoutElement.addEventListener("mouseup", () => {
|
||||||
|
//Узнаём грани, идущие от этой ноды
|
||||||
|
setModalQuestionParentContentId(item.id());
|
||||||
|
setOpenedModalQuestions(true);
|
||||||
|
});
|
||||||
|
layoutsContainer.current?.appendChild(layoutElement);
|
||||||
|
|
||||||
|
return layoutElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const plusesPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "right",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = plusesContainer.current?.querySelector(
|
||||||
|
`.popper-plus[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plusElement = document.createElement("div");
|
||||||
|
plusElement.classList.add("popper-plus");
|
||||||
|
plusElement.setAttribute("data-id", item.id());
|
||||||
|
plusElement.style.zIndex = "1";
|
||||||
|
plusElement.addEventListener("mouseup", () => {
|
||||||
|
setStartCreate(node.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
plusesContainer.current?.appendChild(plusElement);
|
||||||
|
|
||||||
|
return plusElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const crossesPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "top-end",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = crossesContainer.current?.querySelector(
|
||||||
|
`.popper-cross[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossElement = document.createElement("div");
|
||||||
|
crossElement.classList.add("popper-cross");
|
||||||
|
crossElement.setAttribute("data-id", item.id());
|
||||||
|
crossElement.style.zIndex = "2";
|
||||||
|
crossesContainer.current?.appendChild(crossElement);
|
||||||
|
crossElement.addEventListener("mouseup", () => {
|
||||||
|
setStartRemove(node.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
return crossElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let gearsPopper: Popper | null = null;
|
||||||
|
if (node.data().root !== true) {
|
||||||
|
gearsPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "left",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
|
||||||
|
const itemElement = gearsContainer.current?.querySelector(
|
||||||
|
`.popper-gear[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gearElement = document.createElement("div");
|
||||||
|
gearElement.classList.add("popper-gear");
|
||||||
|
gearElement.setAttribute("data-id", item.id());
|
||||||
|
gearElement.style.zIndex = "1";
|
||||||
|
gearsContainer.current?.appendChild(gearElement);
|
||||||
|
gearElement.addEventListener("mouseup", (e) => {
|
||||||
|
console.log("up");
|
||||||
|
updateOpenedModalSettingsId(item.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
return gearElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const update = async () => {
|
||||||
|
await plusesPopper.update();
|
||||||
|
await crossesPopper.update();
|
||||||
|
await gearsPopper?.update();
|
||||||
|
await layoutsPopper.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onZoom = (event: AbstractEventObject) => {
|
||||||
|
const zoom = event.cy.zoom();
|
||||||
|
|
||||||
|
//update();
|
||||||
|
|
||||||
|
crossesPopper.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [-5 * zoom, -30 * zoom] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutsPopper.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [0, -130 * zoom] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
plusesPopper.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [0, 0 * zoom] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
gearsPopper?.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [0, 0] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutsContainer.current
|
||||||
|
?.querySelectorAll("#popper-layouts > .popper-layout")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${130 * zoom}px`;
|
||||||
|
element.style.height = `${130 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
plusesContainer.current
|
||||||
|
?.querySelectorAll("#popper-pluses > .popper-plus")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${40 * zoom}px`;
|
||||||
|
element.style.height = `${40 * zoom}px`;
|
||||||
|
element.style.fontSize = `${40 * zoom}px`;
|
||||||
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
crossesContainer.current
|
||||||
|
?.querySelectorAll("#popper-crosses > .popper-cross")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${24 * zoom}px`;
|
||||||
|
element.style.height = `${24 * zoom}px`;
|
||||||
|
element.style.fontSize = `${24 * zoom}px`;
|
||||||
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
gearsContainer?.current
|
||||||
|
?.querySelectorAll("#popper-gears > .popper-gear")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${60 * zoom}px`;
|
||||||
|
element.style.height = `${40 * zoom}px`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//node?.on("position", update);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
//удаляем иконки
|
||||||
|
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 = e
|
||||||
|
.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: e, 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((e) => {
|
||||||
|
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: e });
|
||||||
|
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 };
|
||||||
|
};
|
||||||
211
src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts
Normal file
211
src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import {
|
||||||
|
deleteQuestion,
|
||||||
|
updateQuestion,
|
||||||
|
getQuestionByContentId,
|
||||||
|
clearRuleForAll,
|
||||||
|
createFrontResult,
|
||||||
|
} from "@root/questions/actions";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
|
|
||||||
|
import type { MutableRefObject } from "react";
|
||||||
|
import type {
|
||||||
|
Core,
|
||||||
|
CollectionReturnValue,
|
||||||
|
PresetLayoutOptions,
|
||||||
|
} from "cytoscape";
|
||||||
|
import type {
|
||||||
|
QuestionBranchingRule,
|
||||||
|
QuestionBranchingRuleMain,
|
||||||
|
} from "../../../../model/questionTypes/shared";
|
||||||
|
|
||||||
|
type UseRemoveNodeArgs = {
|
||||||
|
cyRef: MutableRefObject<Core | null>;
|
||||||
|
layoutOptions: PresetLayoutOptions;
|
||||||
|
layoutsContainer: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
plusesContainer: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
crossesContainer: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
gearsContainer: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveNode = ({
|
||||||
|
cyRef,
|
||||||
|
layoutOptions,
|
||||||
|
layoutsContainer,
|
||||||
|
plusesContainer,
|
||||||
|
crossesContainer,
|
||||||
|
gearsContainer,
|
||||||
|
}: UseRemoveNodeArgs) => {
|
||||||
|
const { questions: trashQuestions } = useQuestionsStore();
|
||||||
|
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 = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
//чистим rule родителя
|
||||||
|
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
|
||||||
|
|
||||||
|
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 deleteNodes: string[] = [];
|
||||||
|
const deleteEdges: any = [];
|
||||||
|
const cy = cyRef?.current;
|
||||||
|
|
||||||
|
const findChildrenToDelete = (node: CollectionReturnValue) => {
|
||||||
|
//Узнаём грани, идущие от этой ноды
|
||||||
|
cy?.$('edge[source = "' + node.id() + '"]')
|
||||||
|
?.toArray()
|
||||||
|
.forEach((edge) => {
|
||||||
|
const edgeData = edge.data();
|
||||||
|
|
||||||
|
//записываем id грани для дальнейшего удаления
|
||||||
|
deleteEdges.push(edge);
|
||||||
|
//ищем ноду на конце грани, записываем её ID для дальнейшего удаления
|
||||||
|
const targetNode = cy?.$("#" + edgeData.target);
|
||||||
|
deleteNodes.push(targetNode.data().id);
|
||||||
|
//вызываем функцию для анализа потомков уже у этой ноды
|
||||||
|
findChildrenToDelete(targetNode);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const elementToDelete = cy?.getElementById(targetNodeContentId);
|
||||||
|
|
||||||
|
if (elementToDelete) {
|
||||||
|
findChildrenToDelete(elementToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetQuestion = getQuestionByContentId(targetNodeContentId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
targetQuestion?.type &&
|
||||||
|
targetQuestion.content.rule.parentId === "root" &&
|
||||||
|
quiz
|
||||||
|
) {
|
||||||
|
updateRootContentId(quiz?.id, "");
|
||||||
|
updateQuestion(targetNodeContentId, (question) => {
|
||||||
|
question.content.rule.parentId = "";
|
||||||
|
question.content.rule.main = [];
|
||||||
|
question.content.rule.children = [];
|
||||||
|
question.content.rule.default = "";
|
||||||
|
});
|
||||||
|
trashQuestions.forEach((q) => {
|
||||||
|
if (q.type === "result") {
|
||||||
|
deleteQuestion(q.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearRuleForAll();
|
||||||
|
} else {
|
||||||
|
const parentQuestionContentId = cy
|
||||||
|
?.$('edge[target = "' + targetNodeContentId + '"]')
|
||||||
|
?.toArray()?.[0]
|
||||||
|
?.data()?.source;
|
||||||
|
if (targetNodeContentId && parentQuestionContentId) {
|
||||||
|
if (
|
||||||
|
quiz &&
|
||||||
|
cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0
|
||||||
|
) {
|
||||||
|
createFrontResult(quiz.backendId, parentQuestionContentId);
|
||||||
|
}
|
||||||
|
clearDataAfterRemoveNode({
|
||||||
|
targetQuestionContentId: targetNodeContentId,
|
||||||
|
parentQuestionContentId,
|
||||||
|
});
|
||||||
|
cy?.remove(cy?.$("#" + targetNodeContentId))
|
||||||
|
.layout(layoutOptions)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке
|
||||||
|
|
||||||
|
deleteNodes.forEach((nodeId) => {
|
||||||
|
//Ноды
|
||||||
|
cy?.remove(cy?.$("#" + nodeId));
|
||||||
|
removeButtons(nodeId);
|
||||||
|
updateQuestion(nodeId, (question) => {
|
||||||
|
question.content.rule.parentId = "";
|
||||||
|
question.content.rule.main = [];
|
||||||
|
question.content.rule.default = "";
|
||||||
|
question.content.rule.children = [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteEdges.forEach((edge: any) => {
|
||||||
|
//Грани
|
||||||
|
cy?.remove(edge);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeButtons(targetNodeContentId);
|
||||||
|
cy?.data("changed", true);
|
||||||
|
cy?.layout(layoutOptions).run();
|
||||||
|
|
||||||
|
//удаляем result всех потомков
|
||||||
|
trashQuestions.forEach((qr) => {
|
||||||
|
if (
|
||||||
|
qr.type === "result" &&
|
||||||
|
(deleteNodes.includes(qr.content.rule.parentId || "") ||
|
||||||
|
(targetQuestion?.type &&
|
||||||
|
qr.content.rule.parentId === targetQuestion.content.id))
|
||||||
|
) {
|
||||||
|
deleteQuestion(qr.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { removeNode };
|
||||||
|
};
|
||||||
@ -14,8 +14,6 @@ export const BranchingMap = () => {
|
|||||||
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
|
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
|
||||||
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
|
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
id="cytoscape-container"
|
id="cytoscape-container"
|
||||||
|
|||||||
53
src/pages/Questions/BranchingMap/stylesheet.ts
Normal file
53
src/pages/Questions/BranchingMap/stylesheet.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { Stylesheet } from "cytoscape";
|
||||||
|
|
||||||
|
export const stylesheet: Stylesheet[] = [
|
||||||
|
{
|
||||||
|
selector: "node",
|
||||||
|
style: {
|
||||||
|
shape: "round-rectangle",
|
||||||
|
width: 130,
|
||||||
|
height: 130,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
label: "data(label)",
|
||||||
|
"font-size": "16",
|
||||||
|
color: "#4D4D4D",
|
||||||
|
"text-halign": "center",
|
||||||
|
"text-valign": "center",
|
||||||
|
"text-wrap": "wrap",
|
||||||
|
"text-max-width": "80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "[?eroticeyeblink]",
|
||||||
|
style: {
|
||||||
|
"border-width": "4px",
|
||||||
|
"border-style": "solid",
|
||||||
|
"border-color": "#7e2aea",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".multiline-auto",
|
||||||
|
style: {
|
||||||
|
"text-wrap": "wrap",
|
||||||
|
"text-max-width": "80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "edge",
|
||||||
|
style: {
|
||||||
|
width: 30,
|
||||||
|
"line-color": "#DEDFE7",
|
||||||
|
"curve-style": "taxi",
|
||||||
|
"taxi-direction": "horizontal",
|
||||||
|
"taxi-turn": 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ":selected",
|
||||||
|
style: {
|
||||||
|
"border-style": "solid",
|
||||||
|
"border-width": 1.5,
|
||||||
|
"border-color": "#9A9AAF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -6,14 +6,12 @@ interface Props {
|
|||||||
openedModalQuestions: boolean;
|
openedModalQuestions: boolean;
|
||||||
setModalQuestionTargetContentId: (contentId: string) => void;
|
setModalQuestionTargetContentId: (contentId: string) => void;
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
setOpenedModalQuestions: (open: boolean) => void;
|
||||||
setModalQuestionParentContentId: (open: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BranchingQuestionsModal = ({
|
export const BranchingQuestionsModal = ({
|
||||||
openedModalQuestions,
|
openedModalQuestions,
|
||||||
setOpenedModalQuestions,
|
setOpenedModalQuestions,
|
||||||
setModalQuestionTargetContentId,
|
setModalQuestionTargetContentId,
|
||||||
setModalQuestionParentContentId,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const trashQuestions = useQuestionsStore().questions;
|
const trashQuestions = useQuestionsStore().questions;
|
||||||
const questions = trashQuestions.filter(
|
const questions = trashQuestions.filter(
|
||||||
|
|||||||
@ -8,14 +8,16 @@ export type UiTools = {
|
|||||||
openBranchingPanel: boolean;
|
openBranchingPanel: boolean;
|
||||||
desireToOpenABranchingModal: string | null;
|
desireToOpenABranchingModal: string | null;
|
||||||
editSomeQuestion: string | null;
|
editSomeQuestion: string | null;
|
||||||
|
lastDeletionNodeTime: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: UiTools = {
|
const initialState: UiTools = {
|
||||||
openedModalSettingsId: null as null,
|
openedModalSettingsId: null,
|
||||||
dragQuestionContentId: null,
|
dragQuestionContentId: null,
|
||||||
openBranchingPanel: false,
|
openBranchingPanel: false,
|
||||||
desireToOpenABranchingModal: null as null,
|
desireToOpenABranchingModal: null,
|
||||||
editSomeQuestion: null as null,
|
editSomeQuestion: null,
|
||||||
|
lastDeletionNodeTime: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUiTools = create<UiTools>()(
|
export const useUiTools = create<UiTools>()(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user