frontPanel/src/pages/Questions/BranchingMap/CsComponent.tsx
2023-12-01 03:31:09 +03:00

579 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
import { useQuestionsStore } from "@root/questions/store";
import { clearDragQuestionId } from "@root/questions/actions";
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);
export const CsComponent = () => {
const quiz = useCurrentQuiz();
const { dragQuestionId, questions } = useQuestionsStore()
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
const cyRef = useRef<Core | null>(null);
const plusesContainer = useRef<HTMLDivElement | null>(null);
const crossesContainer = useRef<HTMLDivElement | null>(null);
const gearsContainer = useRef<HTMLDivElement | null>(null);
const addNode = ({ parentNodeId }: { parentNodeId: string }) => {
const cy = cyRef?.current
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
// if (Object.keys(currentNode.children).length > 1) {
// setOpenedModalSettings(question.index)
// } else {
// //Если ребёнок первый - добавляем его родителю как дефолтный
// parentQuestion.question.content.rule.default = Object.keys(newNode)[0].split("_").pop()
// updateQuestionsList(quiz, parentQuestion.index, parentQuestion.question);
// }
console.log(dragQuestionId)
// cy?.add({
// })
}
useEffect(() => {
if (startCreate) {
addNode({ parentNodeId: startCreate });
clearDragQuestionId()
setStartCreate("");
}
}, [startCreate]);
useEffect(() => {
if (startRemove) {
// removeNode(quiz, startRemove);
setStartRemove("");
}
}, [startRemove]);
useEffect(() => {
document.querySelector("#root")?.addEventListener("mouseup", clearDragQuestionId);
const cy = cyRef.current;
//cy?.add(storeToNodes(questions))
cy?.add(
[
{
"data": {
"id": "1",
"label": "2"
}
},
{
"data": {
"id": "1 2",
"label": "Вы идёте в школу"
}
},
{
"data": {
"id": "1 3",
"label": "1"
}
},
{
"data": {
"id": "1 2 4",
"label": "3"
}
},
{
"data": {
"id": "1 2 6",
"label": "5"
}
},
{
"data": {
"id": "1 3 5",
"label": "4"
}
},
{
"data": {
"id": "1 3 7",
"label": "6"
}
},
{
"data": {
"id": "1 2 6 9867874",
"label": "7"
}
},
{
"data": {
"id": "1 2 6 7398789",
"label": "8"
}
},
{
"data": {
"id": "1 2 6 9484789",
"label": "11"
}
},
{
"data": {
"source": "1",
"target": "1 2",
"id": "c4881f18-03cf-4ed1-bbc4-1741007f11c5"
}
},
{
"data": {
"source": "1",
"target": "1 3",
"id": "3cc5a94a-0192-4ea2-bdc6-ce1a157b76d4"
}
},
{
"data": {
"source": "1 2",
"target": "1 2 4",
"id": "1baf1bc6-eb40-4c81-b137-27cdd3a15e60"
}
},
{
"data": {
"source": "1 2",
"target": "1 2 6",
"id": "78af38cc-7609-401c-bbff-ebdb3f67ec14"
}
},
{
"data": {
"source": "1 3",
"target": "1 3 5",
"id": "a1c80f9f-7c4b-455c-8ba9-ef5dce5522b5"
}
},
{
"data": {
"source": "1 3",
"target": "1 3 7",
"id": "85ed3ee9-fdd1-4874-8e36-484db46bf1c5"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 9867874",
"id": "f139548a-abca-412b-9935-740f219a938d"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 7398789",
"id": "ec8dd60c-df49-447f-b85a-4ae00cde1ae9"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 9484789",
"id": "9b5ecc61-d0ca-4872-a2a4-4fd72835345e"
}
}
]
)
return () => {
document.querySelector("#root")?.removeEventListener("mouseup", clearDragQuestionId);
plusesContainer.current?.remove();
crossesContainer.current?.remove();
gearsContainer.current?.remove();
};
}, []);
const removeButtons = (id: string) => {
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 initialCS = () => {
const cy = cyRef.current;
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);
}
cy?.nodes()
.toArray()
?.forEach((item) => {
const node = item as NodeSingularWithPopper;
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());
plusesContainer.current?.appendChild(plusElement);
plusElement.addEventListener("mouseup", () =>
setStartCreate(node.id())
);
return plusElement;
},
});
const crossesPopper = node.popper({
popper: {
placement: "top-end",
modifiers: [
{ name: "flip", options: { boundary: node } },
{
name: "hide",
options: { enabled: true },
},
],
},
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);
crossElement.addEventListener("click", () => {
console.log('#' + node.id() + ' node', cy.edges('[source = "' + node.id() + '"]'))
console.log('[source = "' + node.id() + '"]')
console.log(cy?.collection)
// cy?.remove('[source = "'+node.id()+'"]')
console.log("папа")
console.log(cy?.$('[target = "' + node.id() + '"]').data().source)
cy?.remove('#' + node.id())
// setStartRemove(node.id())
}
);
node.on('remove', evt => {
console.log(cy.edges())
// cy?.remove('#'+evt.target.target())
})
return crossElement;
},
});
const gearsPopper = node.popper({
popper: {
placement: "left",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
const itemId = item.id();
// if (item.id() === elements[0].data.id) {
// return;
// }
const itemElement = gearsContainer.current?.querySelector(
`.popper-gear[data-id='${itemId}']`
);
if (itemElement) {
return itemElement;
}
const gearsElement = document.createElement("div");
gearsElement.classList.add("popper-gear");
gearsElement.setAttribute("data-id", item.id());
gearsContainer.current?.appendChild(gearsElement);
// gearsElement.addEventListener("click", () =>
// setOpenedModalSettings(
// findQuestionById(quiz, node.id().split("_").pop() || "").index
// )
// );
return gearsElement;
},
});
const update = async () => {
await plusesPopper.update();
await crossesPopper.update();
await gearsPopper.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] } },
{
name: "hide",
options: { enabled: true },
},
],
});
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 = `${33 * zoom}px`;
element.style.height = `${14 * zoom}px`;
element.style.fontSize = `${24 * zoom}px`;
});
};
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" }}
layout={{
name: 'preset',
positions: (e) => {
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(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)
if (e.data('layer') === 1)
console.log(e.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)
return {x:200*e.data('layer'),y: lastOffset+step}
} else {
parent.data('lastChild',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: (e) => console.log('ready',e), // 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
}}
stylesheet={stylesheet}
cy={(cy) => {
cyRef.current = cy;
}}
/>
);
};