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

724 lines
22 KiB
TypeScript
Raw Normal View History

2023-11-29 15:45:15 +00:00
import { useEffect, useRef, useState } from "react";
import Cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import popper from "cytoscape-popper";
import { useCurrentQuiz } from "@root/quizes/hooks";
2023-12-01 08:12:59 +00:00
import { AnyQuizQuestion } from "@model/questionTypes/shared"
2023-11-29 15:45:15 +00:00
import { useQuestionsStore } from "@root/questions/store";
import { cleardragQuestionContentId, getQuestionById, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId } from "@root/questions/actions";
2023-11-29 15:45:15 +00:00
import { storeToNodes } from "./helper";
import "./styles.css";
import type {
Stylesheet,
Core,
NodeSingular,
AbstractEventObject,
ElementDefinition,
} from "cytoscape";
import { QuestionsList } from "../BranchingPanel/QuestionsList";
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;
};
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: ".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",
},
},
];
Cytoscape.use(popper);
interface Props {
modalQuestionParentContentId: string;
modalQuestionTargetContentId: string;
setOpenedModalQuestions: (open:boolean) => void;
setModalQuestionParentContentId: (id:string) => void;
setModalQuestionTargetContentId: (id:string) => void;
}
export const CsComponent = ({
modalQuestionParentContentId,
modalQuestionTargetContentId,
setOpenedModalQuestions,
setModalQuestionParentContentId,
setModalQuestionTargetContentId
}:Props) => {
2023-12-01 08:12:59 +00:00
console.log("Я существую")
2023-11-29 15:45:15 +00:00
const quiz = useCurrentQuiz();
const { dragQuestionContentId, questions } = useQuestionsStore()
2023-11-29 15:45:15 +00:00
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
const cyRef = useRef<Core | null>(null);
2023-12-01 08:12:59 +00:00
const layoutsContainer = useRef<HTMLDivElement | null>(null);
2023-11-29 15:45:15 +00:00
const plusesContainer = useRef<HTMLDivElement | null>(null);
const crossesContainer = useRef<HTMLDivElement | null>(null);
const gearsContainer = useRef<HTMLDivElement | null>(null);
useEffect(() =>{
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
console.log("был выбран вопрос " + modalQuestionTargetContentId)
}
2023-12-01 08:12:59 +00:00
}, [modalQuestionTargetContentId])
2023-12-01 08:12:59 +00:00
const addNode = ({ parentNodeContentId }: { parentNodeContentId: string }) => {
2023-11-29 15:45:15 +00:00
const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
const targetQuestion = { ...getQuestionByContentId(dragQuestionContentId) } as AnyQuizQuestion
console.log(parentNodeChildren, parentNodeContentId)
2023-12-01 08:12:59 +00:00
if (targetQuestion && targetQuestion && parentNodeContentId && parentNodeChildren !== undefined) {
console.log(targetQuestion, targetQuestion, parentNodeContentId, parentNodeChildren)
2023-12-01 08:12:59 +00:00
cy?.add([
{
data: {
id: targetQuestion.content.id,
2023-12-01 08:12:59 +00:00
label: targetQuestion.title || "noname"
}
},
{
data: {
source: parentNodeContentId,
target: targetQuestion.content.id
2023-12-01 08:12:59 +00:00
}
}
])
// clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
}
2023-11-29 15:45:15 +00:00
console.log(dragQuestionContentId)
}
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyQuizQuestion, parentNodeChildren:number }) => {
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.id, question => {
question.content.rule.parentId = parentNodeContentId
question.content.rule.main = []
})
2023-12-01 08:12:59 +00:00
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
if (parentNodeChildren > 1) {
updateOpenedModalSettingsId(parentNodeContentId)
2023-12-01 08:12:59 +00:00
} else {
//Если ребёнок первый - добавляем его родителю как дефолтный
updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
2023-12-01 08:12:59 +00:00
}
}
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
const cy = cyRef?.current
//получить можно только дочерние ноды
console.log(cy?.$('#'+targetNodeContentId))
console.log(cy?.$('#'+targetNodeContentId)?.data())
2023-12-01 08:12:59 +00:00
2023-11-29 15:45:15 +00:00
}
const clearDataAfterRemoveNode = () => {
2023-11-29 15:45:15 +00:00
}
2023-11-29 15:45:15 +00:00
useEffect(() => {
if (startCreate) {
addNode({ parentNodeContentId: startCreate });
cleardragQuestionContentId()
2023-11-29 15:45:15 +00:00
setStartCreate("");
}
}, [startCreate]);
useEffect(() => {
if (startRemove) {
// removeNode({ targetNodeContentId: startRemove });
2023-11-29 15:45:15 +00:00
setStartRemove("");
}
}, [startRemove]);
useEffect(() => {
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
2023-11-29 15:45:15 +00:00
const cy = cyRef.current;
// cy?.add(storeToNodes(questions))
cy?.add(
[
{
"data": {
"id": "1",
"label": "нет имени"
},
"position": {
"x": 250,
"y": 200
}
},
{
"data": {
"id": "1 2",
"label": "нет имени"
},
"position": {
"x": 500,
"y": 300
}
},
{
"data": {
"id": "1 3",
"label": "нет имени"
},
"position": {
"x": 500,
"y": 400
}
},
{
"data": {
"id": "1 2 4",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 100
}
},
{
"data": {
"id": "1 2 6",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 500
}
},
{
"data": {
"id": "1 3 5",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 300
}
},
{
"data": {
"id": "1 3 7",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 500
}
},
{
"data": {
"id": "1 2 6 9867874",
"label": "нет имени"
},
"position": {
"x": 1000,
"y": 300
}
},
{
"data": {
"id": "1 2 6 7398789",
"label": "нет имени"
},
"position": {
"x": 1000,
"y": 500
}
},
{
"data": {
"id": "1 2 6 9484789",
"label": "нет имени"
},
"position": {
"x": 1000,
"y": 700
}
},
{
"data": {
"source": "1",
"target": "1 2"
}
},
{
"data": {
"source": "1",
"target": "1 3"
}
},
{
"data": {
"source": "1 2",
"target": "1 2 4"
}
},
{
"data": {
"source": "1 2",
"target": "1 2 6"
}
},
{
"data": {
"source": "1 3",
"target": "1 3 5"
}
},
{
"data": {
"source": "1 3",
"target": "1 3 7"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 9867874"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 7398789"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 9484789"
}
}
]
)
//cy?.layout(ly).run()
2023-11-29 15:45:15 +00:00
return () => {
document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId);
2023-12-01 08:12:59 +00:00
layoutsContainer.current?.remove();
2023-11-29 15:45:15 +00:00
plusesContainer.current?.remove();
crossesContainer.current?.remove();
gearsContainer.current?.remove();
};
}, []);
const removeButtons = (id: string) => {
2023-12-01 08:12:59 +00:00
layoutsContainer.current
?.querySelector(`.popper-layout[data-id='${id}']`)
?.remove();
2023-11-29 15:45:15 +00:00
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();
};
2023-12-01 08:12:59 +00:00
const readyLO = (e) => {
//удаляем иконки
e.cy.nodes().forEach((ele: any) => {
const data = ele.data()
data.id && removeButtons(data.id);
})
initialPopperIcons(e)
// console.log('ready', e)
// console.log(e.cy.nodes())
// console.log(e.cy.nodes().data())
2023-12-01 08:12:59 +00:00
}
const initialPopperIcons = (e) => {
const cy = e.cy
2023-11-29 15:45:15 +00:00
const container =
(document.body.querySelector(
".__________cytoscape_container"
) as HTMLDivElement) || null;
if (!container) {
return;
}
container.style.overflow = "hidden";
2023-12-01 08:12:59 +00:00
if (!layoutsContainer.current) {
layoutsContainer.current = document.createElement("div");
layoutsContainer.current.setAttribute("id", "popper-layouts");
container.append(layoutsContainer.current);
}
2023-11-29 15:45:15 +00:00
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);
}
cy?.nodes()
.toArray()
?.forEach((item) => {
const node = item as NodeSingularWithPopper;
//console.log(node)
2023-12-01 08:12:59 +00:00
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.classList.add("popper-layout");
layoutElement.setAttribute("data-id", item.id());
// layoutElement.addEventListener("mouseup", () =>{}
// // setStartCreate(node.id())
// );
//console.log(layoutsContainer.current)
2023-12-01 08:12:59 +00:00
layoutsContainer.current?.appendChild(layoutElement);
return layoutElement;
},
});
2023-11-29 15:45:15 +00:00
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());
2023-12-01 08:12:59 +00:00
plusElement.addEventListener("mouseup", () => {
setStartCreate(node.id());
});
2023-11-29 15:45:15 +00:00
plusesContainer.current?.appendChild(plusElement);
return plusElement;
},
});
const crossesPopper = node.popper({
popper: {
placement: "top-end",
2023-12-01 08:12:59 +00:00
modifiers: [{ name: "flip", options: { boundary: node } }],
2023-11-29 15:45:15 +00:00
},
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());
crossesContainer.current?.appendChild(crossElement);
2023-12-01 08:12:59 +00:00
// crossElement.addEventListener("click", () =>
// setStartRemove(node.id())
// );
2023-11-29 15:45:15 +00:00
//console.log(crossElement)
2023-11-29 15:45:15 +00:00
return crossElement;
},
});
const gearsPopper = node.popper({
popper: {
placement: "left",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
//console.log(item.data())
2023-11-29 15:45:15 +00:00
const itemId = item.id();
//console.log(item.data())
//console.log(item.data().lastChild)
2023-12-01 08:12:59 +00:00
if (item.data().lastChild === NaN || item.data().lastChild === undefined) {
return;
}
2023-11-29 15:45:15 +00:00
const itemElement = gearsContainer.current?.querySelector(
`.popper-gear[data-id='${itemId}']`
);
if (itemElement) {
return itemElement;
}
2023-12-01 08:12:59 +00:00
const gearElement = document.createElement("div");
gearElement.classList.add("popper-gear");
gearElement.setAttribute("data-id", item.id());
gearsContainer.current?.appendChild(gearElement);
// gearElement.addEventListener("click", () => {
// setOpenedModalSettings(
// findQuestionById(quizId, node.id().split(" ").pop() || "").index
// );
// });
return gearElement;
2023-11-29 15:45:15 +00:00
},
});
const update = async () => {
await plusesPopper.update();
await crossesPopper.update();
await gearsPopper.update();
2023-12-01 08:12:59 +00:00
await layoutsPopper.update();
2023-11-29 15:45:15 +00:00
};
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] } },
],
});
2023-12-01 08:12:59 +00:00
layoutsPopper.setOptions({
modifiers: [
{ name: "flip", options: { boundary: node } },
{ name: "offset", options: { offset: [0, -130 * zoom] } },
],
});
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`;
});
2023-11-29 15:45:15 +00:00
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;
2023-12-01 08:12:59 +00:00
element.style.width = `${60 * zoom}px`;
element.style.height = `${40 * zoom}px`;
2023-11-29 15:45:15 +00:00
});
};
node?.on("position", update);
cy?.on("pan zoom resize render", onZoom);
});
};
return (
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
// elements={createGraphElements(tree, quiz)}
style={{ height: "480px", background: "#F2F3F7" }}
stylesheet={stylesheet}
layout={{
name: 'preset',
positions: (e) => {
//console.log("идёт расчёт позиции")
const id = e.id()
const incomming = e.cy().edges(`[target="${id}"]`)
const layer = 0
e.removeData('lastChild')
if (incomming.length === 0) {
const children = e.cy().edges(`[source="${id}"]`)
e.data('layer', layer)
e.data('children', children.targets().length)
const queue = []
children.forEach(n => {
queue.push({ task: n.target(), layer: layer + 1 })
})
while (queue.length) {
const task = queue.pop()
task.task.data('layer', task.layer)
const children = e.cy().edges(`[source="${task.task.id()}"]`)
task.task.data('children', children.targets().length)
if (children.length !== 0) {
children.forEach(n => queue.push({ task: n.target(), layer: task.layer + 1 }))
}
}
queue.push({ parent: e, children: children.targets() })
while (queue.length) {
const task = queue.pop()
if (task.children.length === 0) {
task.parent.data('subtreeWidth', task.parent.height())
continue
}
const unprocessed = task?.children.filter(e => e.data('subtreeWidth') === undefined)
if (unprocessed.length !== 0) {
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))
}
return { x: 200 * e.data('layer'), y: 0 }
} else {
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
//console.log('batya', parent)
// console.log(e.data('subtreeWidth'), e.id(),(parent.data('children')-1) )
const wing = parent.data('subtreeWidth') / 2
const lastOffset = parent.data('lastChild')
const step = wing * 2 / (parent.data('children') - 1)
console.log(parent.data('subtreeWidth'), e.id(), (parent.data('children') - 1), step, wing, e.data('layer'), parent.id(), lastOffset)
//e.removeData('subtreeWidth')
//console.log('poss', e.id(), 'children', parent.data('children'),'lo', lastOffset, 'v', wing)
if (lastOffset !== undefined) {
parent.data('lastChild', lastOffset + step)
//console.log('lastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChild')
// console.log('lastChild', lastOffset + step)
// return { x: 200 * e.data('layer'), y: (lastOffset + step) }
return { x: 200 * e.data('layer'), y: (lastOffset + step) }
} else {
parent.data('lastChild', parent.position().y - wing)
// console.log('lastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChild')
// console.log('lastChild', parent.position().y - wing)
// return { x: 200 * e.data('layer'), y: (parent.position().y - wing)}
return { x: 200 * e.data('layer'), y: (parent.position().y - wing) }
}
}
}, // 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: undefined, // 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 true; }, // 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
// stop: (e) => console.log('stop', e), // callback on layoutstop
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
}}
2023-11-29 15:45:15 +00:00
cy={(cy) => {
cyRef.current = cy;
}}
/>
);
};