diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index 6eb4bbf2..c89d61e9 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -3,9 +3,9 @@ import Cytoscape from "cytoscape"; import CytoscapeComponent from "react-cytoscapejs"; import popper from "cytoscape-popper"; import { useCurrentQuiz } from "@root/quizes/hooks"; - +import { AnyQuizQuestion } from "@model/questionTypes/shared" import { useQuestionsStore } from "@root/questions/store"; -import { clearDragQuestionId } from "@root/questions/actions"; +import { clearDragQuestionId, getQuestionById, updateQuestion, updateOpenedModalSettingsId } from "@root/questions/actions"; import { storeToNodes } from "./helper"; @@ -93,6 +93,7 @@ const stylesheet: Stylesheet[] = [ Cytoscape.use(popper); export const CsComponent = () => { + console.log("Я существую") const quiz = useCurrentQuiz(); const { dragQuestionId, questions } = useQuestionsStore() @@ -100,27 +101,53 @@ export const CsComponent = () => { 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 addNode = ({ parentNodeId }: { parentNodeId: string }) => { const cy = cyRef?.current + const parentNodeChildren = cy?.$('edge[source = "' + parentNodeId + '"]').length + const targetQuestion = { ...getQuestionById(dragQuestionId) } as AnyQuizQuestion + console.log(parentNodeChildren, parentNodeId) + + if (targetQuestion && targetQuestion && parentNodeId && parentNodeChildren !== undefined) { + cy?.add([ + { + data: { + id: targetQuestion.id, + label: targetQuestion.title || "noname" + } + }, + { + data: { + source: parentNodeId, + target: targetQuestion.id + } + } + ]) + + + //Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления + if (parentNodeChildren > 1) { + updateOpenedModalSettingsId(parentNodeId) + } else { + //Если ребёнок первый - добавляем его родителю как дефолтный + updateQuestion(parentNodeId, question => question.content.rule.default = targetQuestion.id) + } + + //предупреждаем добавленный вопрос о том, кто его родитель + updateQuestion(targetQuestion.id, question => question.content.rule.parentId = parentNodeId) + + } + - //Если детей больше 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(() => { @@ -141,137 +168,12 @@ export const CsComponent = () => { 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" - } - } - ] - ) + cy?.add(storeToNodes(questions)) + cy?.layout(ly).run() return () => { document.querySelector("#root")?.removeEventListener("mouseup", clearDragQuestionId); + layoutsContainer.current?.remove(); plusesContainer.current?.remove(); crossesContainer.current?.remove(); gearsContainer.current?.remove(); @@ -280,6 +182,9 @@ export const CsComponent = () => { const removeButtons = (id: string) => { + layoutsContainer.current + ?.querySelector(`.popper-layout[data-id='${id}']`) + ?.remove(); plusesContainer.current ?.querySelector(`.popper-plus[data-id='${id}']`) ?.remove(); @@ -291,8 +196,99 @@ export const CsComponent = () => { ?.remove(); }; - const initialCS = () => { - const cy = cyRef.current; + + + + 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()) + } + + const [ly, setly] = useState({ + 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(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) || 1 } + } else { + parent.data('lastChild', parent.position().y - wing) + return { x: 200 * e.data('layer'), y: (parent.position().y - wing) || 200 } + } + } + }, // 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 + }) + + + const initialPopperIcons = (e) => { + const cy = e.cy + const container = (document.body.querySelector( ".__________cytoscape_container" @@ -304,6 +300,11 @@ export const CsComponent = () => { container.style.overflow = "hidden"; + if (!layoutsContainer.current) { + layoutsContainer.current = document.createElement("div"); + layoutsContainer.current.setAttribute("id", "popper-layouts"); + container.append(layoutsContainer.current); + } if (!plusesContainer.current) { plusesContainer.current = document.createElement("div"); plusesContainer.current.setAttribute("id", "popper-pluses"); @@ -320,11 +321,38 @@ export const CsComponent = () => { container.append(gearsContainer.current); } - cy?.nodes() .toArray() ?.forEach((item) => { const node = item as NodeSingularWithPopper; + console.log(node) + + 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) + layoutsContainer.current?.appendChild(layoutElement); + + return layoutElement; + }, + }); const plusesPopper = node.popper({ popper: { @@ -343,10 +371,10 @@ export const CsComponent = () => { const plusElement = document.createElement("div"); plusElement.classList.add("popper-plus"); plusElement.setAttribute("data-id", item.id()); + plusElement.addEventListener("mouseup", () => { + setStartCreate(node.id()); + }); plusesContainer.current?.appendChild(plusElement); - plusElement.addEventListener("mouseup", () => - setStartCreate(node.id()) - ); return plusElement; }, @@ -355,13 +383,7 @@ export const CsComponent = () => { const crossesPopper = node.popper({ popper: { placement: "top-end", - modifiers: [ - { name: "flip", options: { boundary: node } }, - { - name: "hide", - options: { enabled: true }, - }, - ], + modifiers: [{ name: "flip", options: { boundary: node } }], }, content: ([item]) => { const itemId = item.id(); @@ -376,29 +398,11 @@ export const CsComponent = () => { 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()) - }) + // crossElement.addEventListener("click", () => + // setStartRemove(node.id()) + // ); + console.log(crossElement) return crossElement; }, }); @@ -409,10 +413,13 @@ export const CsComponent = () => { modifiers: [{ name: "flip", options: { boundary: node } }], }, content: ([item]) => { + console.log(item.data()) const itemId = item.id(); - // if (item.id() === elements[0].data.id) { - // return; - // } + console.log(item.data()) + console.log(item.data().lastChild) + if (item.data().lastChild === NaN || item.data().lastChild === undefined) { + return; + } const itemElement = gearsContainer.current?.querySelector( `.popper-gear[data-id='${itemId}']` @@ -421,16 +428,17 @@ export const CsComponent = () => { 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 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; }, }); @@ -438,6 +446,7 @@ export const CsComponent = () => { await plusesPopper.update(); await crossesPopper.update(); await gearsPopper.update(); + await layoutsPopper.update(); }; const onZoom = (event: AbstractEventObject) => { @@ -449,13 +458,24 @@ export const CsComponent = () => { modifiers: [ { name: "flip", options: { boundary: node } }, { name: "offset", options: { offset: [-5 * zoom, -30 * zoom] } }, - { - name: "hide", - options: { enabled: true }, - }, ], }); + 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`; + }); + plusesContainer.current ?.querySelectorAll("#popper-pluses > .popper-plus") .forEach((item) => { @@ -480,9 +500,8 @@ export const CsComponent = () => { ?.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`; + element.style.width = `${60 * zoom}px`; + element.style.height = `${40 * zoom}px`; }); }; @@ -497,79 +516,8 @@ export const CsComponent = () => { 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} + layout={ly} cy={(cy) => { cyRef.current = cy; }} diff --git a/src/pages/Questions/BranchingMap/FirstNodeField.tsx b/src/pages/Questions/BranchingMap/FirstNodeField.tsx index ecc468d4..24849f8a 100644 --- a/src/pages/Questions/BranchingMap/FirstNodeField.tsx +++ b/src/pages/Questions/BranchingMap/FirstNodeField.tsx @@ -1,15 +1,19 @@ import { Box } from "@mui/material" -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { updateDragQuestionId, updateQuestion } from "@root/questions/actions" import { updateRootInfo } from "@root/quizes/actions" import { useQuestionsStore } from "@root/questions/store" import { useCurrentQuiz } from "@root/quizes/hooks"; import { enqueueSnackbar } from "notistack"; -export const FirstNodeField = () => { +interface Props { + setOpenedModalQuestionsId:(str: string|null) => void +} +export const FirstNodeField = ({setOpenedModalQuestionsId}:Props) => { const { dragQuestionId } = useQuestionsStore() const Container = useRef(null); const quiz = useCurrentQuiz(); + console.log(dragQuestionId) const newRootNode = () => { @@ -32,6 +36,7 @@ console.log(dragQuestionId) return ( { + console.log(questions) const nodes: Nodes[] = [] const edges: Edges[] = [] questions.forEach((question) => { + console.log(question) if (question.content.rule.parentId) { nodes.push({data: { id: question.id, label: question.title ? question.title : "noname" }}) - nodes.push({data:{ - id: "111", - label: "111" - }}) - nodes.push({data:{ - id: "222", - label: "222" - }}) - edges.push({data: { - source: question.id, - target: "111" - }}) - nodes.push({data:{ - id: "333", - label: "333" - }}) - edges.push({data: { - source: "111", - target: "333" - }}) - edges.push({data: { - source: question.id, - target: "222" - }}) if (question.content.rule.parentId !== "root") edges.push({data: { source: question.content.rule.parentId, target: question.id }}) } }) + console.log([...nodes, ...edges]) return [...nodes, ...edges]; } \ No newline at end of file diff --git a/src/pages/Questions/BranchingMap/index.tsx b/src/pages/Questions/BranchingMap/index.tsx index 41bcd129..4486bf4d 100644 --- a/src/pages/Questions/BranchingMap/index.tsx +++ b/src/pages/Questions/BranchingMap/index.tsx @@ -3,11 +3,13 @@ import { FirstNodeField } from "./FirstNodeField"; import { CsComponent } from "./CsComponent"; import { useQuestionsStore } from "@root/questions/store" import { useCurrentQuiz } from "@root/quizes/hooks"; +import { useState } from "react"; export const BranchingMap = () => { const quiz = useCurrentQuiz(); const { dragQuestionId } = useQuestionsStore() + const [openedModalQuestionsId, setOpenedModalQuestionsId] = useState() return ( { quiz?.config.haveRoot ? : - + } diff --git a/src/pages/Questions/BranchingPanel/QuestionsList.tsx b/src/pages/Questions/BranchingPanel/QuestionsList.tsx index 0085546d..76ab5152 100644 --- a/src/pages/Questions/BranchingPanel/QuestionsList.tsx +++ b/src/pages/Questions/BranchingPanel/QuestionsList.tsx @@ -4,6 +4,7 @@ import { ReactComponent as CheckedIcon } from "@icons/checked.svg"; import { useQuestionsStore } from "@root/questions/store"; import { updateDragQuestionId } from "@root/questions/actions"; import { useEffect } from "react"; +import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared"; const getItemStyle = (isDragging:any, draggableStyle:any) => ({ @@ -18,6 +19,7 @@ const getItemStyle = (isDragging:any, draggableStyle:any) => ({ // styles we need to apply on draggables ...draggableStyle }); +type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion export const QuestionsList = () => { const { questions } = useQuestionsStore() @@ -49,7 +51,7 @@ export const QuestionsList = () => { }} > {/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/} - {questions.map(({ title, id, content }, index) => ( + {questions.filter((q:AnyQuestion) => q.type).map(({ title, id, content }, index) => (