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; 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(null); const plusesContainer = useRef(null); const crossesContainer = useRef(null); const gearsContainer = useRef(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 ( { 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; }} /> ); };