import { useEffect, useLayoutEffect, useRef, useState } from "react"; import Cytoscape from "cytoscape"; import { Button } from "@mui/material"; import CytoscapeComponent from "react-cytoscapejs"; import popper from "cytoscape-popper"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { updateRootContentId } from "@root/quizes/actions"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { useQuestionsStore } from "@root/questions/store"; import { deleteQuestion, updateQuestion, getQuestionByContentId, clearRuleForAll, createFrontResult, } from "@root/questions/actions"; import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; import { cleardragQuestionContentId } from "@root/uiTools/actions"; import { withErrorBoundary } from "react-error-boundary"; import { useRemoveNode } from "./hooks/useRemoveNode"; import { usePopper } from "./hooks/usePopper"; import { storeToNodes } from "./helper"; import { stylesheet } from "./stylesheet"; import "./styles.css"; import type { Stylesheet, Core, NodeSingular, AbstractEventObject, ElementDefinition, LayoutEventObject, } from "cytoscape"; import { enqueueSnackbar } from "notistack"; import { useUiTools } from "@root/uiTools/store"; // 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; // setOptions: (modifiers: { modifiers?: Modifier[] }) => void; // }; // type NodeSingularWithPopper = NodeSingular & { // popper: (config: PopperConfig) => Popper; // }; Cytoscape.use(popper); type CsComponentProps = { modalQuestionParentContentId: string; modalQuestionTargetContentId: string; setOpenedModalQuestions: (open: boolean) => void; setModalQuestionParentContentId: (id: string) => void; setModalQuestionTargetContentId: (id: string) => void; }; function CsComponent({ modalQuestionParentContentId, modalQuestionTargetContentId, setOpenedModalQuestions, setModalQuestionParentContentId, setModalQuestionTargetContentId, }: CsComponentProps) { const quiz = useCurrentQuiz(); const { dragQuestionContentId, desireToOpenABranchingModal } = useUiTools(); const trashQuestions = useQuestionsStore().questions; const questions = trashQuestions.filter( (question) => question.type !== "result" && question.type !== null ); const [startCreate, setStartCreate] = useState(""); const [startRemove, setStartRemove] = useState(""); const cyRef = useRef(null); const layoutsContainer = useRef(null); const plusesContainer = useRef(null); const crossesContainer = useRef(null); const gearsContainer = useRef(null); // const { layoutOptions } = usePopper({ // layoutsContainer, // plusesContainer, // crossesContainer, // gearsContainer, // setModalQuestionParentContentId, // setOpenedModalQuestions, // setStartCreate, // setStartRemove, // }); const layoutOptions = {}; const removeNode = () => {}; // const { removeNode } = useRemoveNode({ // cyRef, // layoutOptions, // layoutsContainer, // plusesContainer, // crossesContainer, // gearsContainer, // }); useLayoutEffect(() => { const cy = cyRef?.current; if (desireToOpenABranchingModal) { setTimeout(() => { cy?.getElementById(desireToOpenABranchingModal)?.data( "eroticeyeblink", true ); }, 250); } else { cy?.elements().data("eroticeyeblink", false); } }, [desireToOpenABranchingModal]); useLayoutEffect(() => { updateOpenedModalSettingsId(); // updateRootContentId(quiz.id, "") // clearRuleForAll() }, []); useEffect(() => { if ( modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0 ) { addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId, }); } setModalQuestionParentContentId(""); setModalQuestionTargetContentId(""); }, [modalQuestionTargetContentId]); const addNode = ({ parentNodeContentId, targetNodeContentId, }: { parentNodeContentId: string; targetNodeContentId?: string; }) => { //запрещаем работу родителя-ребенка если это один и тот же вопрос 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); if (quiz) { createFrontResult(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(); console.log(es); cy?.center(es); } 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 ) { console.log("deleteQ", targetQuestion.id); deleteQuestion(targetQuestion.id); } }); //предупреждаем добавленный вопрос о том, кто его родитель updateQuestion(targetQuestion.content.id, (question) => { question.content.rule.parentId = parentNodeContentId; question.content.rule.main = []; }); //предупреждаем родителя о новом потомке (если он ещё не знает о нём) if ( !parentQuestion.content.rule.children.includes(targetQuestion.content.id) ) updateQuestion(parentNodeContentId, (question) => { question.content.rule.children = [ ...question.content.rule.children, targetQuestion.content.id, ]; }); //Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления if (parentQuestion.content.rule.children >= 1) { updateOpenedModalSettingsId(targetQuestion.content.id); } }; // const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => { // console.log("старт удаление") // const deleteNodes: string[] = [] // const deleteEdges: any = [] // const cy = cyRef?.current // const findChildrenToDelete = (node) => { // //Узнаём грани, идущие от этой ноды // 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) // }) // } // findChildrenToDelete(cy?.getElementById(targetNodeContentId)) // const targetQuestion = getQuestionByContentId(targetNodeContentId) // if (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 (cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0) // createFrontResult(quiz.backendId, parentQuestionContentId) // clearDataAfterRemoveNode({ targetQuestionContentId: targetNodeContentId, parentQuestionContentId }) // cy?.remove(cy?.$('#' + targetNodeContentId)).layout(lyopts).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(lyopts).run() // //удаляем result всех потомков // trashQuestions.forEach((qr) => { // if (qr.type === "result") { // if (deleteNodes.includes(qr.content.rule.parentId) || qr.content.rule.parentId === targetQuestion.content.id) { // deleteQuestion(qr.id); // } // } // }) // } // 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) // const newRule = {} // const newChildren = [...parentQuestion.content.rule.children] // newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1); // newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== targetQuestionContentId) //удаляем условия перехода от родителя к этому вопросу // newRule.parentId = parentQuestion.content.rule.parentId // newRule.default = parentQuestion.content.rule.default === targetQuestionContentId ? "" : parentQuestion.content.rule.default // newRule.children = newChildren // updateQuestion(parentQuestionContentId, (PQ) => { // PQ.content.rule = newRule // }) // } useEffect(() => { if (startCreate) { addNode({ parentNodeContentId: startCreate }); cleardragQuestionContentId(); setStartCreate(""); } }, [startCreate]); useEffect(() => { if (startRemove) { removeNode(startRemove); setStartRemove(""); } }, [startRemove]); // const readyLO = (e) => { // if (e.cy.data('firstNode') === 'nonroot') { // e.cy.data('firstNode', 'root') // e.cy.nodes().sort((a, b) => (a.data('root') ? 1 : -1)).layout(lyopts).run() // } else { // e.cy.data('changed', false) // e.cy.removeData('firstNode') // } // //удаляем иконки // e.cy.nodes().forEach((ele: any) => { // const data = ele.data() // data.id && removeButtons(data.id); // }) // initialPopperIcons(e) // } // const lyopts = { // name: 'preset', // positions: (e) => { // if (!e.cy().data('changed')) { // return e.data('oldPos') // } // const id = e.id() // const incomming = e.cy().edges(`[target="${id}"]`) // const layer = 0 // e.removeData('lastChild') // if (incomming.length === 0) { // if (e.cy().data('firstNode') === undefined) // e.cy().data('firstNode', 'root') // e.data('root', true) // const children = e.cy().edges(`[source="${id}"]`).targets() // e.data('layer', layer) // e.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 (e.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 } // e.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 }) // }) // } // e.cy().data('changed', false) // return pos // } else { // const opos = e.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: true, // 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 // } useEffect(() => { document .querySelector("#root") ?.addEventListener("mouseup", cleardragQuestionContentId); const cy = cyRef.current; const eles = cy?.add( storeToNodes( questions.filter( (question) => question.type && question.type !== "result" ) as AnyTypedQuizQuestion[] ) ); cy?.data("changed", true); // cy.data('changed', true) const elecs = eles?.layout(layoutOptions).run(); cy?.on("add", () => cy.data("changed", true)); cy?.fit(); //cy?.layout().run() return () => { document .querySelector("#root") ?.removeEventListener("mouseup", cleardragQuestionContentId); layoutsContainer.current?.remove(); plusesContainer.current?.remove(); crossesContainer.current?.remove(); gearsContainer.current?.remove(); }; }, []); // 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); // }); // }; return ( <> { cyRef.current = cy; }} autoungrabify={true} /> {/* */} ); } function Clear() { const quiz = useCurrentQuiz(); if (quiz) { updateRootContentId(quiz.id, ""); } clearRuleForAll(); return <>; } export default withErrorBoundary(CsComponent, { fallback: , onError: (error, info) => { enqueueSnackbar("Дерево порвалось"); console.log(info); console.log(error); }, });