import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; import type { MutableRefObject } from "react"; import type { PresetLayoutOptions, LayoutEventObject, NodeSingular, AbstractEventObject, } from "cytoscape"; type usePopperArgs = { layoutsContainer: MutableRefObject; plusesContainer: MutableRefObject; crossesContainer: MutableRefObject; gearsContainer: MutableRefObject; 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; 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", () => { 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 = node .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: node, 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((node) => { 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: node }); 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 }; };