From b8b5bc5d2e53dd27ad6a986ea358bf064c828288 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 5 Jan 2024 19:48:35 +0300 Subject: [PATCH 1/9] refactor cytoscape component and hooks --- .../Questions/BranchingMap/CsComponent.tsx | 501 ++++++------- .../Questions/BranchingMap/FirstNodeField.tsx | 76 +- src/pages/Questions/BranchingMap/helper.ts | 277 +++++++- .../Questions/BranchingMap/hooks/usePopper.ts | 665 ++++++------------ .../BranchingMap/hooks/useRemoveNode.ts | 301 +++----- src/pages/Questions/BranchingMap/index.tsx | 74 +- .../Questions/BranchingMap/style/styles.css | 10 +- .../BranchingQuestionsModal/index.tsx | 150 ++-- src/stores/questions/actions.ts | 3 +- src/stores/uiTools/actions.ts | 4 + src/stores/uiTools/store.ts | 20 +- 11 files changed, 963 insertions(+), 1118 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index f5ab0a57..f86c3ace 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -1,301 +1,232 @@ -import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import Cytoscape from "cytoscape"; -import CytoscapeComponent from "react-cytoscapejs"; -import popper from "cytoscape-popper"; -import { Button, Box } from "@mui/material"; -import { withErrorBoundary } from "react-error-boundary"; -import { enqueueSnackbar } from "notistack"; - -import { useCurrentQuiz } from "@root/quizes/hooks"; -import { updateRootContentId } from "@root/quizes/actions"; +import { devlog } from "@frontend/kitui"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; -import { useQuestionsStore } from "@root/questions/store"; -import { useUiTools } from "@root/uiTools/store"; +import { Box, Button } from "@mui/material"; import { - deleteQuestion, - updateQuestion, - getQuestionByContentId, - clearRuleForAll, - createResult, + clearRuleForAll } from "@root/questions/actions"; -import { - updateModalInfoWhyCantCreate, - updateOpenedModalSettingsId -} from "@root/uiTools/actions"; -import { cleardragQuestionContentId } from "@root/uiTools/actions"; -import { updateDeleteId } from "@root/uiTools/actions"; - -import { DeleteNodeModal } from "../DeleteNodeModal"; +import { useQuestionsStore } from "@root/questions/store"; +import { updateRootContentId } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQuestionTargetContentId, updateModalInfoWhyCantCreate, updateOpenedModalSettingsId } from "@root/uiTools/actions"; +import { useUiTools } from "@root/uiTools/store"; import { ProblemIcon } from "@ui_kit/ProblemIcon"; - -import { useRemoveNode } from "./hooks/useRemoveNode"; +import type { Core, PresetLayoutOptions, SingularData } from "cytoscape"; +import Cytoscape from "cytoscape"; +import popper, { getPopperInstance } from "cytoscape-popper"; +import { enqueueSnackbar } from "notistack"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import CytoscapeComponent from "react-cytoscapejs"; +import { withErrorBoundary } from "react-error-boundary"; +import { DeleteNodeModal } from "../DeleteNodeModal"; +import { addNode, calcNodePosition, storeToNodes } from "./helper"; import { usePopper } from "./hooks/usePopper"; - -import { storeToNodes } from "./helper"; -import { stylesheet } from "./style/stylesheet"; +import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; +import { stylesheet } from "./style/stylesheet"; -import type { Core } from "cytoscape"; +Cytoscape.use(popper); -Cytoscape.use(popper); +type PopperInstance = ReturnType>; -interface CsComponentProps { - modalQuestionParentContentId: string; - modalQuestionTargetContentId: string; - setOpenedModalQuestions: (open: boolean) => void; - setModalQuestionParentContentId: (id: string) => void; - setModalQuestionTargetContentId: (id: string) => void; +function CsComponent() { + const quiz = useCurrentQuiz(); + const desireToOpenABranchingModal = useUiTools(state => state.desireToOpenABranchingModal); + const canCreatePublic = useUiTools(state => state.canCreatePublic); + const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId); + const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); + const trashQuestions = useQuestionsStore(state => state.questions); + const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null); + const [isPanningCy, setIsPanningCy] = useState(false); + + const cyRef = useRef(null); + const popperContainerRef = useRef(null); + const popperInstancesRef = useRef([]); + + const { createPoppers, removeAllPoppers, removePoppersById } = usePopper({ + cyRef, + quizId: quiz?.backendId, + runCyLayout, + popperContainerRef, + popperInstancesRef, + }); + + function runCyLayout() { + cyRef.current?.layout(layoutOptions).run(); + createPoppers(); + }; + + const { removeNode } = useRemoveNode({ + cyRef, + runCyLayout, + removeButtons: removePoppersById, + }); + + useLayoutEffect(() => { + const cy = cyRef?.current; + if (desireToOpenABranchingModal) { + setTimeout(() => { + cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true); + }, 250); + } else { + cy?.elements().data("eroticeyeblink", false); + } + }, [desireToOpenABranchingModal]); + + useEffect(() => { + if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) { + if (!cyRef.current || !quiz) return; + + const es = addNode({ + cy: cyRef.current, + quizId: quiz.backendId, + parentNodeContentId: modalQuestionParentContentId, + targetNodeContentId: modalQuestionTargetContentId, + }); + runCyLayout(); + if (es) cyRef.current.fit(es, 100); + } + setModalQuestionParentContentId(""); + setModalQuestionTargetContentId(""); + }, [modalQuestionTargetContentId, quiz?.backendId]); + + useEffect(function onMount() { + updateOpenedModalSettingsId(); + document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId); + const cy = cyRef.current; + if (!cy) return; + + cy.add( + storeToNodes( + questions.filter((question) => question.type && question.type !== "result") as AnyTypedQuizQuestion[], + ), + ); + runCyLayout(); + cy.fit(); + + return () => { + document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId); + removeAllPoppers(); + }; + }, []); + + useEffect(function attachDragHandlers() { + const cy = cyRef.current; + if (!cy) return; + + let isPointerDown = false; + + const onPointerDown = () => { + isPointerDown = true; + cy.data("dragging", true); + }; + const onPointerUp = () => { + isPointerDown = false; + cy.data("dragging", false); + setIsPanningCy(false); + }; + const handleMove = () => { + setIsPanningCy(isPointerDown); + }; + + cy.on("vmousedown", onPointerDown); + cy.on("vmousemove", handleMove); + document.addEventListener("pointerup", onPointerUp); + + return () => { + cy.off("vmousedown", onPointerDown); + cy.off("vmousemove", handleMove); + document.removeEventListener("pointerup", onPointerUp); + }; + }, []); + + useEffect(function poppersLifecycle() { + if (isPanningCy) { + removeAllPoppers(); + } else { + createPoppers(); + } + }, [isPanningCy]); + + return ( + <> + + + updateModalInfoWhyCantCreate(true)} + /> + + + { + cyRef.current = cy; + }} + autoungrabify={true} + /> + + + ); } -function CsComponent({ - modalQuestionParentContentId, - modalQuestionTargetContentId, - setOpenedModalQuestions, - setModalQuestionParentContentId, - setModalQuestionTargetContentId -}: CsComponentProps) { - const quiz = useCurrentQuiz(); - - const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic } = 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 { removeNode } = useRemoveNode({ - cyRef, - layoutOptions, - layoutsContainer, - plusesContainer, - crossesContainer, - gearsContainer, - }); - - - useEffect(() => { - return () => { - // if (!canCreatePublic) updateModalInfoWhyCantCreate(true) - } - }, []); - - 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 (quiz) { - - //запрещаем работу родителя-ребенка если это один и тот же вопрос - 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) - createResult(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() - cy?.center(es) - } else { - enqueueSnackbar("Добавляемый вопрос не найден") - } - - } 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) { - updateQuestion(targetQuestion.id, (q) => q.content.usage = false); - } - }) - - //предупреждаем добавленный вопрос о том, кто его родитель - updateQuestion(targetQuestion.content.id, question => { - question.content.rule.parentId = parentNodeContentId - question.content.rule.main = [] - //Это листик. Сбросим ему на всякий случай не листиковые поля - question.content.rule.children = [] - question.content.rule.default = "" - }) - - const noChild = parentQuestion.content.rule.children.length === 0 - - //предупреждаем родителя о новом потомке (если он ещё не знает о нём) - if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) updateQuestion(parentNodeContentId, question => { - question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id] - //единственному ребёнку даём дефолт по-умолчанию - question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default - }) - - if (!noChild) {//детей больше 1 - //- предупреждаем стор вопросов об открытии модалки ветвления - updateOpenedModalSettingsId(targetQuestion.content.id) - } - - - } - - - useEffect(() => { - if (startCreate) { - addNode({ parentNodeContentId: startCreate }); - cleardragQuestionContentId(); - setStartCreate(""); - } - }, [startCreate]); - - useEffect(() => { - if (startRemove) { - updateDeleteId(startRemove); - setStartRemove(""); - } - }, [startRemove]); - - 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(); - }; - }, []); - - - return ( - <> - - - updateModalInfoWhyCantCreate(true)} /> - - - { - cyRef.current = cy; - }} - autoungrabify={true} - /> - - - ); -}; - function Clear() { - const quiz = useCurrentQuiz(); - if (quiz) { - updateRootContentId(quiz?.id, ""); - } - clearRuleForAll() - return <> + 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) - }, + fallback: , + onError: (error, info) => { + enqueueSnackbar("Дерево порвалось"); + devlog(info); + devlog(error); + }, }); + +const layoutOptions: PresetLayoutOptions = { + name: "preset", + positions: calcNodePosition, + zoom: undefined, + pan: 1, + fit: false, + padding: 30, + animate: false, + animationDuration: 500, + animationEasing: undefined, + animateFilter: () => false, + ready: event => { + if (event.cy.data("firstNode") === "nonroot") { + event.cy.data("firstNode", "root"); + event.cy.nodes().sort((a, b) => (a.data("root") ? 1 : -1)); + } else { + event.cy.removeData("firstNode"); + } + }, + transform: (_, p) => p, +}; diff --git a/src/pages/Questions/BranchingMap/FirstNodeField.tsx b/src/pages/Questions/BranchingMap/FirstNodeField.tsx index ba14fc18..45e63b6f 100644 --- a/src/pages/Questions/BranchingMap/FirstNodeField.tsx +++ b/src/pages/Questions/BranchingMap/FirstNodeField.tsx @@ -1,69 +1,65 @@ -import { Box } from "@mui/material" -import { useEffect, useRef, useLayoutEffect } from "react"; -import { deleteQuestion, clearRuleForAll, updateQuestion, createResult } from "@root/questions/actions" -import { updateOpenedModalSettingsId } from "@root/uiTools/actions" -import { updateRootContentId } from "@root/quizes/actions" -import { useCurrentQuiz } from "@root/quizes/hooks" -import { useQuestionsStore } from "@root/questions/store" -import { enqueueSnackbar } from "notistack"; +import { Box } from "@mui/material"; +import { clearRuleForAll, createResult, updateQuestion } from "@root/questions/actions"; +import { updateRootContentId } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { setOpenedModalQuestions, updateOpenedModalSettingsId } from "@root/uiTools/actions"; import { useUiTools } from "@root/uiTools/store"; +import { enqueueSnackbar } from "notistack"; +import { useEffect, useLayoutEffect, useRef } from "react"; -interface Props { - setOpenedModalQuestions: (open: boolean) => void; - modalQuestionTargetContentId: string; -} -export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => { +export const FirstNodeField = () => { const quiz = useCurrentQuiz(); - + const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); useLayoutEffect(() => { - updateOpenedModalSettingsId() - updateRootContentId(quiz.id, "") - clearRuleForAll() - }, []) + if (!quiz) return; + + updateOpenedModalSettingsId(); + updateRootContentId(quiz.id, ""); + clearRuleForAll(); + }, []); - const { questions } = useQuestionsStore() - const { dragQuestionContentId } = useUiTools() + const { dragQuestionContentId } = useUiTools(); const Container = useRef(null); - const modalOpen = () => setOpenedModalQuestions(true) + const modalOpen = () => setOpenedModalQuestions(true); const newRootNode = () => { if (quiz) { if (dragQuestionContentId) { - updateRootContentId(quiz?.id, dragQuestionContentId) - updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root") - createResult(quiz?.backendId, dragQuestionContentId) + updateRootContentId(quiz?.id, dragQuestionContentId); + updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root"); + createResult(quiz?.backendId, dragQuestionContentId); } } else { - enqueueSnackbar("Нет информации о взятом опроснике") + enqueueSnackbar("Нет информации о взятом опроснике"); } - } + }; useEffect(() => { - Container.current?.addEventListener("mouseup", newRootNode) - Container.current?.addEventListener("click", modalOpen) - return () => { - Container.current?.removeEventListener("mouseup", newRootNode) - Container.current?.removeEventListener("click", modalOpen) - } - }, [dragQuestionContentId]) + Container.current?.addEventListener("mouseup", newRootNode); + Container.current?.addEventListener("click", modalOpen); + return () => { + Container.current?.removeEventListener("mouseup", newRootNode); + Container.current?.removeEventListener("click", modalOpen); + }; + }, [dragQuestionContentId]); useEffect(() => { if (quiz) { if (modalQuestionTargetContentId) { - updateRootContentId(quiz?.id, modalQuestionTargetContentId) - updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root") - createResult(quiz?.backendId, modalQuestionTargetContentId) + updateRootContentId(quiz?.id, modalQuestionTargetContentId); + updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root"); + createResult(quiz?.backendId, modalQuestionTargetContentId); } } else { - enqueueSnackbar("Нет информации о взятом опроснике") + enqueueSnackbar("Нет информации о взятом опроснике"); } - }, [modalQuestionTargetContentId]) + }, [modalQuestionTargetContentId]); return ( @@ -82,5 +78,5 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon > + - ) -} + ); +}; diff --git a/src/pages/Questions/BranchingMap/helper.ts b/src/pages/Questions/BranchingMap/helper.ts index 0423d219..4dc5c2d6 100644 --- a/src/pages/Questions/BranchingMap/helper.ts +++ b/src/pages/Questions/BranchingMap/helper.ts @@ -1,28 +1,39 @@ -import { AnyTypedQuizQuestion } from "@model/questionTypes/shared" +import { devlog } from "@frontend/kitui"; +import { QuizQuestionResult } from "@model/questionTypes/result"; +import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain, UntypedQuizQuestion } from "@model/questionTypes/shared"; +import { Quiz } from "@model/quiz/quiz"; +import { createResult, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; +import { useQuestionsStore } from "@root/questions/store"; +import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; +import { useUiTools } from "@root/uiTools/store"; +import { Core } from "cytoscape"; +import { enqueueSnackbar } from "notistack"; interface Nodes { data: { id: string; label: string; parent?: string; - } + }; } interface Edges { data: { source: string; target: string; - } + }; } export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => { - const nodes: Nodes[] = [] - const edges: Edges[] = [] + const nodes: Nodes[] = []; + const edges: Edges[] = []; questions.forEach((question) => { if (question.content.rule.parentId) { - nodes.push({data: { - id: question.content.id, - label: question.title === "" || question.title === " " ? "noname" : question.title - }}) + nodes.push({ + data: { + id: question.content.id, + label: question.title === "" || question.title === " " ? "noname" : question.title + } + }); // nodes.push({ // data: { // id: "delete" + question.content.id, @@ -30,11 +41,249 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => { // parent: question.content.id, // } // },) - if (question.content.rule.parentId !== "root") edges.push({data: { - source: question.content.rule.parentId, - target: question.content.id - }}) + if (question.content.rule.parentId !== "root") edges.push({ + data: { + source: question.content.rule.parentId, + target: question.content.id + } + }); } - }) + }); return [...nodes, ...edges]; +}; + +export function clearDataAfterAddNode({ + parentNodeContentId, + targetQuestion, +}: { + parentNodeContentId: string; + targetQuestion: AnyTypedQuizQuestion; +}) { + const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion; + + //смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен + useQuestionsStore.getState().questions.filter( + (question): question is QuizQuestionResult => question.type === "result" + ).forEach((targetQuestion) => { + if ( + targetQuestion.content.rule.parentId === parentQuestion.content.id + ) { + updateQuestion(targetQuestion.id, (q) => (q.content.usage = false)); + } + }); + + //предупреждаем добавленный вопрос о том, кто его родитель + updateQuestion(targetQuestion.content.id, (question) => { + question.content.rule.parentId = parentNodeContentId; + question.content.rule.main = []; + //Это листик. Сбросим ему на всякий случай не листиковые поля + question.content.rule.children = []; + question.content.rule.default = ""; + }); + + const noChild = parentQuestion.content.rule.children.length === 0; + + //предупреждаем родителя о новом потомке (если он ещё не знает о нём) + if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) + updateQuestion(parentNodeContentId, (question) => { + question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id]; + //единственному ребёнку даём дефолт по-умолчанию + question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default; + }); + + if (!noChild) { + //детей больше 1 + //- предупреждаем стор вопросов об открытии модалки ветвления + updateOpenedModalSettingsId(targetQuestion.content.id); + } +}; + +export function clearDataAfterRemoveNode({ + quiz, + trashQuestions, + targetQuestionContentId, + parentQuestionContentId, +}: { + quiz: Quiz | undefined; + trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[], + targetQuestionContentId: string; + parentQuestionContentId: string; +}) { + updateQuestion(targetQuestionContentId, (question) => { + question.content.rule.parentId = ""; + question.content.rule.children = []; + question.content.rule.main = []; + question.content.rule.default = ""; + }); + + //Ищём родителя + const parentQuestion = getQuestionByContentId(parentQuestionContentId); + + //Делаем результат родителя активным + const parentResult = trashQuestions.find( + (q): q is QuizQuestionResult => q.type === "result" && q.content.rule.parentId === parentQuestionContentId, + ); + if (parentResult) { + updateQuestion(parentResult.content.id, (q) => { + q.content.usage = true; + }); + } else { + createResult(quiz?.backendId, parentQuestionContentId); + } + + //чистим rule родителя + if (!parentQuestion?.type) { + return; + } + + const newChildren = [...parentQuestion.content.rule.children]; + newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1); + + const newRule: QuestionBranchingRule = { + children: newChildren, + default: + parentQuestion.content.rule.default === targetQuestionContentId + ? "" + : parentQuestion.content.rule.default, + //удаляем условия перехода от родителя к этому вопросу, + main: parentQuestion.content.rule.main.filter( + (data: QuestionBranchingRuleMain) => data.next !== targetQuestionContentId, + ), + parentId: parentQuestion.content.rule.parentId, + }; + + updateQuestion(parentQuestionContentId, (PQ) => { + PQ.content.rule = newRule; + }); +}; + +export function calcNodePosition(node: any) { + 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: any[] = []; + children.forEach((n: any) => { + 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: any) => 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: any) => { + return node.data("subtreeWidth") === undefined; + }); + if (unprocessed.length !== 0) { + queue.push(task); + unprocessed.forEach((t: any) => { + queue.push({ + parent: t, + children: t.cy().edges(`[source="${t.id()}"]`).targets(), + }); + }); + continue; + } + + task?.parent.data( + "subtreeWidth", + task.children.reduce((p: any, n: any) => 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: any) => { + 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, + }); + }); + } + return pos; + } else { + const opos = node.data("oldPos"); + if (opos) { + return opos; + } + } } + +export const addNode = ({ + cy, + quizId, + parentNodeContentId, + targetNodeContentId, +}: { + cy: Core; + quizId: number; + parentNodeContentId: string; + targetNodeContentId?: string; +}) => { + //запрещаем работу родителя-ребенка если это один и тот же вопрос + if (parentNodeContentId === targetNodeContentId) return; + devlog("@addNode"); + + const parentNodeChildren = cy.$('edge[source = "' + parentNodeContentId + '"]')?.length; + //если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа + const targetQuestion = { + ...getQuestionByContentId(targetNodeContentId || useUiTools.getState().dragQuestionContentId), + } as AnyTypedQuizQuestion; + + if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) { + clearDataAfterAddNode({ parentNodeContentId, targetQuestion }); + createResult(quizId, 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, + }, + }, + ]); + return es; + } else { + enqueueSnackbar("Добавляемый вопрос не найден"); + } +}; diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index 615532a1..a506cbcd 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -1,479 +1,264 @@ -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; -}; +import { cleardragQuestionContentId, setModalQuestionParentContentId, setOpenedModalQuestions, updateDeleteId, updateOpenedModalSettingsId } from "@root/uiTools/actions"; +import type { AbstractEventObject, Core, NodeSingular, SingularData } from "cytoscape"; +import { getPopperInstance } from "cytoscape-popper"; +import { type MutableRefObject } from "react"; +import { addNode } from "../helper"; type PopperItem = { - id: () => string; + id: () => string; }; type Modifier = { - name: string; - options: unknown; + name: string; + options: unknown; }; type PopperConfig = { - popper: { - placement: string; - modifiers?: Modifier[]; - }; - content: (items: PopperItem[]) => void; + popper: { + placement: string; + modifiers?: Modifier[]; + }; + content: (items: PopperItem[]) => void; }; -type Popper = { - update: () => Promise; - setOptions: (modifiers: { modifiers?: Modifier[] }) => void; -}; +type PopperInstance = ReturnType>; type NodeSingularWithPopper = NodeSingular & { - popper: (config: PopperConfig) => Popper; + popper: (config: PopperConfig) => PopperInstance; }; 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(); - }; + cyRef, + quizId, + popperContainerRef, + popperInstancesRef, + runCyLayout, +}: { + cyRef: MutableRefObject; + quizId: number | undefined, + popperContainerRef: MutableRefObject; + popperInstancesRef: MutableRefObject; + runCyLayout: () => void; +}) => { + const removePoppersById = (id: string) => { + popperContainerRef.current?.querySelector(`.popper-layout[data-id='${id}']`)?.remove(); + }; - const initialPopperIcons = ({ cy }: LayoutEventObject) => { - const container = - (document.body.querySelector( - ".__________cytoscape_container" - ) as HTMLDivElement) || null; + const removeAllPoppers = () => { + cyRef.current?.removeListener("zoom render"); - if (!container) { - return; - } + popperInstancesRef.current.forEach(p => p.destroy()); + popperInstancesRef.current = []; + popperContainerRef.current?.remove(); + popperContainerRef.current = null; + }; - container.style.overflow = "hidden"; + const createPoppers = () => { + removeAllPoppers(); - 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 cy = cyRef.current; + if (!cy) return; - 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 - ); - }); + const container = cy.container(); - nodesInView.toArray()?.forEach((item) => { - const node = item as NodeSingularWithPopper; + if (!container) { + console.warn("Cannot create popper container"); + return; + } - 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; - } + if (!popperContainerRef.current) { + popperContainerRef.current = document.createElement("div"); + popperContainerRef.current.setAttribute("id", "poppers-container"); + container.append(popperContainerRef.current); + } - 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); + cy.nodes().forEach((item) => { + const node = item as NodeSingularWithPopper; - return layoutElement; - }, - }); + const layoutsPopper = node.popper({ + popper: { + placement: "left", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: (items) => { + const item = items[0]; + const itemId = item.id(); + const itemElement = popperContainerRef.current?.querySelector(`.popper-layout[data-id='${itemId}']`); + if (itemElement) { + return itemElement; + } - 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 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); + }); + popperContainerRef.current?.appendChild(layoutElement); - 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()); - }); + return layoutElement; + }, + }); + popperInstancesRef.current.push(layoutsPopper); - plusesContainer.current?.appendChild(plusElement); + const plusesPopper = node.popper({ + popper: { + placement: "right", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: ([item]) => { + const itemId = item.id(); + const itemElement = popperContainerRef.current?.querySelector(`.popper-plus[data-id='${itemId}']`); + if (itemElement) { + return itemElement; + } - return plusElement; - }, - }); + const plusElement = document.createElement("div"); + plusElement.classList.add("popper-plus"); + plusElement.setAttribute("data-id", item.id()); + plusElement.style.zIndex = "1"; + plusElement.addEventListener("mouseup", () => { + if (!cy || !quizId) return; - 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 es = addNode({ + cy, + quizId, + parentNodeContentId: node.id(), + }); + runCyLayout(); + if (es) cy.fit(es, 100); + cleardragQuestionContentId(); + }); - 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()); - }); + popperContainerRef.current?.appendChild(plusElement); - 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(); + return plusElement; + }, + }); + popperInstancesRef.current.push(plusesPopper); - const itemElement = gearsContainer.current?.querySelector( - `.popper-gear[data-id='${itemId}']` - ); - if (itemElement) { - return itemElement; + const crossesPopper = node.popper({ + popper: { + placement: "top-end", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: ([item]) => { + const itemId = item.id(); + const itemElement = popperContainerRef.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"; + popperContainerRef.current?.appendChild(crossElement); + crossElement.addEventListener("mouseup", () => { + updateDeleteId(node.id()); + }); + + return crossElement; + }, + }); + popperInstancesRef.current.push(crossesPopper); + + let gearsPopper: PopperInstance | 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 = popperContainerRef.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"; + popperContainerRef.current?.appendChild(gearElement); + gearElement.addEventListener("mouseup", () => { + updateOpenedModalSettingsId(item.id()); + }); + + return gearElement; + }, + }); + popperInstancesRef.current.push(gearsPopper); } - 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()); - }); + const onZoom = (event: AbstractEventObject) => { + if (event.cy.data("dragging")) return; + const zoom = event.cy.zoom(); - return gearElement; - }, + 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] } }, + ], + }); + gearsPopper?.setOptions({ + modifiers: [ + { name: "flip", options: { boundary: node } }, + { name: "offset", options: { offset: [0, 0] } }, + ], + }); + + popperContainerRef.current?.querySelectorAll(".popper-layout").forEach((item) => { + const element = item as HTMLDivElement; + element.style.width = `${130 * zoom}px`; + element.style.height = `${130 * zoom}px`; + }); + + popperContainerRef.current?.querySelectorAll(".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`; + }); + + popperContainerRef.current?.querySelectorAll(".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`; + }); + + popperContainerRef?.current?.querySelectorAll(".popper-gear").forEach((item) => { + const element = item as HTMLDivElement; + element.style.width = `${60 * zoom}px`; + element.style.height = `${40 * zoom}px`; + }); + }; + + cy.on("zoom render", onZoom); }); - } - 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 }; + return { removeAllPoppers, removePoppersById, createPoppers }; }; diff --git a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts index 60997905..4288a922 100644 --- a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts +++ b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts @@ -1,221 +1,122 @@ -import { - deleteQuestion, - updateQuestion, - getQuestionByContentId, - clearRuleForAll, - createResult, -} from "@root/questions/actions"; +import { devlog } from "@frontend/kitui"; +import { QuizQuestionResult } from "@model/questionTypes/result"; +import { clearRuleForAll, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; -import { useCurrentQuiz } from "@root/quizes/hooks"; import { updateRootContentId } from "@root/quizes/actions"; - +import { useCurrentQuiz } from "@root/quizes/hooks"; +import type { CollectionReturnValue, Core, SingularElementArgument } from "cytoscape"; import type { MutableRefObject } from "react"; -import type { - Core, - CollectionReturnValue, - PresetLayoutOptions, -} from "cytoscape"; -import type { - AnyTypedQuizQuestion, - QuestionBranchingRule, - QuestionBranchingRuleMain, -} from "../../../../model/questionTypes/shared"; +import { clearDataAfterRemoveNode } from "../helper"; type UseRemoveNodeArgs = { - cyRef: MutableRefObject; - layoutOptions: PresetLayoutOptions; - layoutsContainer: MutableRefObject; - plusesContainer: MutableRefObject; - crossesContainer: MutableRefObject; - gearsContainer: MutableRefObject; + cyRef: MutableRefObject; + runCyLayout: () => void; + removeButtons: (id: string) => void; }; export const useRemoveNode = ({ - cyRef, - layoutOptions, - layoutsContainer, - plusesContainer, - crossesContainer, - gearsContainer, + cyRef, + runCyLayout, + removeButtons, }: UseRemoveNodeArgs) => { - const { questions: trashQuestions } = useQuestionsStore(); - const quiz = useCurrentQuiz(); + const { questions: trashQuestions } = useQuestionsStore(); + const quiz = useCurrentQuiz(); - 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 removeNode = (targetNodeContentId: string) => { + const deleteNodes: string[] = []; + const deleteEdges: SingularElementArgument[] = []; + const cy = cyRef?.current; - 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 = ""; - }); + const findChildrenToDelete = (node: CollectionReturnValue) => { + //Узнаём грани, идущие от этой ноды + cy + ?.$('edge[source = "' + node.id() + '"]') + ?.toArray() + .forEach((edge) => { + const edgeData = edge.data(); - //Ищём родителя - const parentQuestion = getQuestionByContentId(parentQuestionContentId); + //записываем id грани для дальнейшего удаления + deleteEdges.push(edge); + //ищем ноду на конце грани, записываем её ID для дальнейшего удаления + const targetNode = cy?.$("#" + edgeData.target); + deleteNodes.push(targetNode.data().id); + //вызываем функцию для анализа потомков уже у этой ноды + findChildrenToDelete(targetNode); + }); + }; - //Делаем результат родителя активным - const parentResult = trashQuestions.find(q => q.type === "result" && q.content.rule.parentId === parentQuestionContentId) - if (parentResult) { - updateQuestion(parentResult.content.id, q => { - q.content.usage = true - }) - } else { - createResult(quiz?.backendId, parentQuestionContentId) - } + const elementToDelete = cy?.getElementById(targetNodeContentId); - //чистим rule родителя - if (!parentQuestion?.type) { - return; - } - - const newChildren = [...parentQuestion.content.rule.children]; - newChildren.splice( - parentQuestion.content.rule.children.indexOf(targetQuestionContentId), - 1 - ); - - const newRule: QuestionBranchingRule = { - children: newChildren, - default: - parentQuestion.content.rule.default === targetQuestionContentId - ? "" - : parentQuestion.content.rule.default, - //удаляем условия перехода от родителя к этому вопросу, - main: parentQuestion.content.rule.main.filter( - (data: QuestionBranchingRuleMain) => - data.next !== targetQuestionContentId - ), - parentId: parentQuestion.content.rule.parentId, - }; - - updateQuestion(parentQuestionContentId, (PQ) => { - PQ.content.rule = newRule; - }); - }; - - const removeNode = (targetNodeContentId: string) => { - const deleteNodes: string[] = []; - const deleteEdges: any = []; - const cy = cyRef?.current; - - const findChildrenToDelete = (node: CollectionReturnValue) => { - //Узнаём грани, идущие от этой ноды - 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); - }); - }; - - const elementToDelete = cy?.getElementById(targetNodeContentId); - - if (elementToDelete) { - findChildrenToDelete(elementToDelete); - } - - const targetQuestion = getQuestionByContentId(targetNodeContentId); - - if ( - targetQuestion?.type && - 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 = ""; - }); - clearRuleForAll(); - } else { - const parentQuestionContentId = cy - ?.$('edge[target = "' + targetNodeContentId + '"]') - ?.toArray()?.[0] - ?.data()?.source; - if (targetNodeContentId && parentQuestionContentId) { - if ( - quiz && - cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0 - ) { - console.log(parentQuestionContentId) - //createFrontResult(quiz.backendId, parentQuestionContentId); + if (elementToDelete) { + findChildrenToDelete(elementToDelete); } - clearDataAfterRemoveNode({ - targetQuestionContentId: targetNodeContentId, - parentQuestionContentId, + + const targetQuestion = getQuestionByContentId(targetNodeContentId); + + if (targetQuestion?.type && 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 = ""; + }); + clearRuleForAll(); + } else { + const parentQuestionContentId = cy + ?.$('edge[target = "' + targetNodeContentId + '"]') + ?.toArray()?.[0] + ?.data()?.source; + if (targetNodeContentId && parentQuestionContentId) { + if (quiz && cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0) { + devlog(parentQuestionContentId); + //createFrontResult(quiz.backendId, parentQuestionContentId); + } + clearDataAfterRemoveNode({ + quiz, + trashQuestions, + targetQuestionContentId: targetNodeContentId, + parentQuestionContentId, + }); + cy?.remove(cy?.$("#" + targetNodeContentId)); + } + } + + //После всех манипуляций удаляем грани и ноды из 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 = []; + }); }); - cy?.remove(cy?.$("#" + targetNodeContentId)) - .layout(layoutOptions) - .run(); - } - } - //После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке + deleteEdges.forEach((edge: any) => { + //Грани + cy?.remove(edge); + }); - 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 = []; - }); - }); + removeButtons(targetNodeContentId); + runCyLayout(); - deleteEdges.forEach((edge: any) => { - //Грани - cy?.remove(edge); - }); + //делаем result всех потомков неактивными + trashQuestions.forEach((qr) => { + if ( + qr.type === "result" && + (deleteNodes.includes(qr.content.rule.parentId || "") || + (targetQuestion?.type && qr.content.rule.parentId === targetQuestion.content.id)) + ) { + updateQuestion(qr.content.id, (q) => { + q.content.usage = false; + }); + } + }); + }; - removeButtons(targetNodeContentId); - cy?.data("changed", true); - cy?.layout(layoutOptions).run(); - - //делаем result всех потомков неактивными - trashQuestions.forEach((qr) => { - if ( - qr.type === "result" && - (deleteNodes.includes(qr.content.rule.parentId || "") || - (targetQuestion?.type && - qr.content.rule.parentId === targetQuestion.content.id)) - ) { - updateQuestion(qr.content.id, q => { - q.content.usage = false - }) - } - }); - }; - - return { removeNode }; + return { removeNode }; }; diff --git a/src/pages/Questions/BranchingMap/index.tsx b/src/pages/Questions/BranchingMap/index.tsx index 6e6c137b..fffb8c18 100644 --- a/src/pages/Questions/BranchingMap/index.tsx +++ b/src/pages/Questions/BranchingMap/index.tsx @@ -1,54 +1,34 @@ import { Box } from "@mui/material"; -import { FirstNodeField } from "./FirstNodeField"; -import CsComponent from "./CsComponent"; import { useCurrentQuiz } from "@root/quizes/hooks"; -import { useEffect, useState } from "react"; -import { BranchingQuestionsModal } from "../BranchingQuestionsModal"; import { useUiTools } from "@root/uiTools/store"; +import { BranchingQuestionsModal } from "../BranchingQuestionsModal"; +import CsComponent from "./CsComponent"; +import { FirstNodeField } from "./FirstNodeField"; export const BranchingMap = () => { - const quiz = useCurrentQuiz(); - const { dragQuestionContentId } = useUiTools(); - const [modalQuestionParentContentId, setModalQuestionParentContentId] = - useState(""); - const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = - useState(""); - const [openedModalQuestions, setOpenedModalQuestions] = - useState(false); + const quiz = useCurrentQuiz(); + const dragQuestionContentId = useUiTools(state => state.dragQuestionContentId); - return ( - - {quiz?.config.haveRoot ? ( - - ) : ( - - )} - - - ); + return ( + + {quiz?.config.haveRoot ? ( + + ) : ( + + )} + + + ); }; diff --git a/src/pages/Questions/BranchingMap/style/styles.css b/src/pages/Questions/BranchingMap/style/styles.css index 98e76b8d..5e9f735a 100644 --- a/src/pages/Questions/BranchingMap/style/styles.css +++ b/src/pages/Questions/BranchingMap/style/styles.css @@ -1,4 +1,4 @@ -#popper-pluses > .popper-plus { +.popper-plus { cursor: pointer; display: flex; align-items: center; @@ -9,13 +9,13 @@ font-size: 0px; } -#popper-pluses > .popper-plus::before { +.popper-plus::before { content: "+"; color: rgba(154, 154, 175, 0.5); font-size: inherit; } -#popper-crosses > .popper-cross { +.popper-cross { cursor: pointer; display: flex; align-items: center; @@ -25,14 +25,14 @@ font-size: 0px; } -#popper-crosses > .popper-cross::before { +.popper-cross::before { content: "+"; transform: rotate(45deg); color: #fff; font-size: inherit; } -#popper-gears > .popper-gear { +.popper-gear { cursor: pointer; display: flex; align-items: center; diff --git a/src/pages/Questions/BranchingQuestionsModal/index.tsx b/src/pages/Questions/BranchingQuestionsModal/index.tsx index ff140d9a..63e6e47f 100644 --- a/src/pages/Questions/BranchingQuestionsModal/index.tsx +++ b/src/pages/Questions/BranchingQuestionsModal/index.tsx @@ -1,90 +1,84 @@ import { Box, Modal, Button, Typography } from "@mui/material"; import { useQuestionsStore } from "@root/questions/store"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; +import { useUiTools } from "@root/uiTools/store"; +import { setModalQuestionTargetContentId, setOpenedModalQuestions } from "@root/uiTools/actions"; -interface Props { - openedModalQuestions: boolean; - setModalQuestionTargetContentId: (contentId: string) => void; - setOpenedModalQuestions: (open: boolean) => void; -} -export const BranchingQuestionsModal = ({ - openedModalQuestions, - setOpenedModalQuestions, - setModalQuestionTargetContentId, -}: Props) => { - const trashQuestions = useQuestionsStore().questions; - const questions = trashQuestions.filter( - (question) => question.type !== "result" - ); +export const BranchingQuestionsModal = () => { + const trashQuestions = useQuestionsStore().questions; + const questions = trashQuestions.filter( + (question) => question.type !== "result" + ); + const openedModalQuestions = useUiTools(state => state.openedModalQuestions); - const handleClose = () => { - setOpenedModalQuestions(false); - }; + const handleClose = () => { + setOpenedModalQuestions(false); + }; - const typedQuestions: AnyTypedQuizQuestion[] = questions.filter( - (question) => - question.type && - !question.content.rule.parentId && - question.type !== "result" - ) as AnyTypedQuizQuestion[]; + const typedQuestions: AnyTypedQuizQuestion[] = questions.filter( + (question) => + question.type && + !question.content.rule.parentId && + question.type !== "result" + ) as AnyTypedQuizQuestion[]; - if (typedQuestions.length === 0) return <>; + if (typedQuestions.length === 0) return <>; - return ( - - - - {typedQuestions.map((question) => ( - - ))} - - - - ); + > + + {typedQuestions.map((question) => ( + + ))} + + + + ); }; diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index 97e4b7fb..a90e2b54 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -498,7 +498,7 @@ export const clearRuleForAll = () => { }; export const createResult = async ( - quizId: number, + quizId: number | undefined, parentContentId?: string ) => requestQueue.enqueue(async () => { if (!quizId || !parentContentId) { @@ -543,4 +543,3 @@ export const createResult = async ( } } }); - diff --git a/src/stores/uiTools/actions.ts b/src/stores/uiTools/actions.ts index cd6da193..a386b455 100644 --- a/src/stores/uiTools/actions.ts +++ b/src/stores/uiTools/actions.ts @@ -38,3 +38,7 @@ export const updateCanCreatePublic = (can: boolean) => useUiTools.setState({ can export const updateModalInfoWhyCantCreate = (can: boolean) => useUiTools.setState({ openModalInfoWhyCantCreate: can }); export const updateDeleteId = (deleteNodeId: string | null = null) => useUiTools.setState({ deleteNodeId }); + +export const setModalQuestionParentContentId = (modalQuestionParentContentId: string) => useUiTools.setState({ modalQuestionParentContentId }); +export const setModalQuestionTargetContentId = (modalQuestionTargetContentId: string) => useUiTools.setState({ modalQuestionTargetContentId }); +export const setOpenedModalQuestions = (open: boolean) => useUiTools.setState({ openedModalQuestions: open }); diff --git a/src/stores/uiTools/store.ts b/src/stores/uiTools/store.ts index 52803348..1499aa58 100644 --- a/src/stores/uiTools/store.ts +++ b/src/stores/uiTools/store.ts @@ -10,7 +10,10 @@ export type UiTools = { canCreatePublic: boolean; whyCantCreatePublic: Record//ид вопроса и список претензий к нему openModalInfoWhyCantCreate: boolean; -deleteNodeId: string | null; + deleteNodeId: string | null; + modalQuestionParentContentId: string; + modalQuestionTargetContentId: string; + openedModalQuestions: boolean; }; export type WhyCantCreatePublic = { name: string; @@ -27,13 +30,16 @@ const initialState: UiTools = { canCreatePublic: false, whyCantCreatePublic: {}, openModalInfoWhyCantCreate: false, -deleteNodeId: null, + deleteNodeId: null, + modalQuestionParentContentId: "", + modalQuestionTargetContentId: "", + openedModalQuestions: false, }; export const useUiTools = create()( - devtools(() => initialState, { - name: "UiTools", - enabled: process.env.NODE_ENV === "development", - trace: process.env.NODE_ENV === "development", - }) + devtools(() => initialState, { + name: "UiTools", + enabled: process.env.NODE_ENV === "development", + trace: process.env.NODE_ENV === "development", + }) ); From 588e21ef1e6d8099a4262170c8a041b6b8f0cedd Mon Sep 17 00:00:00 2001 From: nflnkr Date: Tue, 9 Jan 2024 19:41:35 +0300 Subject: [PATCH 2/9] derive graph state from store instead of updating it manually --- .../Questions/BranchingMap/CsComponent.tsx | 63 +++++++++---------- .../Questions/BranchingMap/FirstNodeField.tsx | 4 +- src/pages/Questions/BranchingMap/helper.ts | 32 +--------- .../Questions/BranchingMap/hooks/usePopper.ts | 31 +++------ .../BranchingMap/hooks/useRemoveNode.ts | 27 ++------ src/stores/questions/actions.ts | 4 +- 6 files changed, 47 insertions(+), 114 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index f86c3ace..faa660d5 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -14,7 +14,7 @@ import type { Core, PresetLayoutOptions, SingularData } from "cytoscape"; import Cytoscape from "cytoscape"; import popper, { getPopperInstance } from "cytoscape-popper"; import { enqueueSnackbar } from "notistack"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import CytoscapeComponent from "react-cytoscapejs"; import { withErrorBoundary } from "react-error-boundary"; import { DeleteNodeModal } from "../DeleteNodeModal"; @@ -24,9 +24,9 @@ import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; import { stylesheet } from "./style/stylesheet"; -Cytoscape.use(popper); +Cytoscape.use(popper); -type PopperInstance = ReturnType>; +type PopperInstance = ReturnType>; function CsComponent() { const quiz = useCurrentQuiz(); @@ -35,30 +35,32 @@ function CsComponent() { const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId); const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore(state => state.questions); - const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null); const [isPanningCy, setIsPanningCy] = useState(false); const cyRef = useRef(null); const popperContainerRef = useRef(null); const popperInstancesRef = useRef([]); - const { createPoppers, removeAllPoppers, removePoppersById } = usePopper({ + const questions = useMemo(() => trashQuestions.filter( + (question) => question.type !== "result" && question.type !== null + ), [trashQuestions]); + + const cyElements = useMemo(() => { + const q = questions.filter( + (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result" + ); + + return storeToNodes(q); + }, [questions]); + + const { createPoppers, removeAllPoppers } = usePopper({ cyRef, - quizId: quiz?.backendId, - runCyLayout, popperContainerRef, popperInstancesRef, }); - function runCyLayout() { - cyRef.current?.layout(layoutOptions).run(); - createPoppers(); - }; - const { removeNode } = useRemoveNode({ cyRef, - runCyLayout, - removeButtons: removePoppersById, }); useLayoutEffect(() => { @@ -74,34 +76,20 @@ function CsComponent() { useEffect(() => { if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) { - if (!cyRef.current || !quiz) return; + if (!cyRef.current) return; - const es = addNode({ - cy: cyRef.current, - quizId: quiz.backendId, + addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId, }); - runCyLayout(); - if (es) cyRef.current.fit(es, 100); } setModalQuestionParentContentId(""); setModalQuestionTargetContentId(""); - }, [modalQuestionTargetContentId, quiz?.backendId]); + }, [modalQuestionTargetContentId]); useEffect(function onMount() { updateOpenedModalSettingsId(); document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId); - const cy = cyRef.current; - if (!cy) return; - - cy.add( - storeToNodes( - questions.filter((question) => question.type && question.type !== "result") as AnyTypedQuizQuestion[], - ), - ); - runCyLayout(); - cy.fit(); return () => { document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId); @@ -109,7 +97,7 @@ function CsComponent() { }; }, []); - useEffect(function attachDragHandlers() { + useEffect(function removePoppersOnDrag() { const cy = cyRef.current; if (!cy) return; @@ -145,7 +133,13 @@ function CsComponent() { } else { createPoppers(); } - }, [isPanningCy]); + }, [isPanningCy, createPoppers]); + + useEffect(() => { + cyRef.current?.layout(layoutOptions).run(); + cyRef.current?.fit(undefined, 70); + createPoppers(); + }, [cyElements, createPoppers]); return ( <> @@ -172,8 +166,7 @@ function CsComponent() { { if (dragQuestionContentId) { updateRootContentId(quiz?.id, dragQuestionContentId); updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root"); - createResult(quiz?.backendId, dragQuestionContentId); + createResult(dragQuestionContentId); } } else { enqueueSnackbar("Нет информации о взятом опроснике"); @@ -53,7 +53,7 @@ export const FirstNodeField = () => { if (modalQuestionTargetContentId) { updateRootContentId(quiz?.id, modalQuestionTargetContentId); updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root"); - createResult(quiz?.backendId, modalQuestionTargetContentId); + createResult(modalQuestionTargetContentId); } } else { enqueueSnackbar("Нет информации о взятом опроснике"); diff --git a/src/pages/Questions/BranchingMap/helper.ts b/src/pages/Questions/BranchingMap/helper.ts index 4dc5c2d6..b8f8874d 100644 --- a/src/pages/Questions/BranchingMap/helper.ts +++ b/src/pages/Questions/BranchingMap/helper.ts @@ -99,12 +99,10 @@ export function clearDataAfterAddNode({ }; export function clearDataAfterRemoveNode({ - quiz, trashQuestions, targetQuestionContentId, parentQuestionContentId, }: { - quiz: Quiz | undefined; trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[], targetQuestionContentId: string; parentQuestionContentId: string; @@ -128,7 +126,7 @@ export function clearDataAfterRemoveNode({ q.content.usage = true; }); } else { - createResult(quiz?.backendId, parentQuestionContentId); + createResult(parentQuestionContentId); } //чистим rule родителя @@ -242,47 +240,23 @@ export function calcNodePosition(node: any) { } export const addNode = ({ - cy, - quizId, parentNodeContentId, targetNodeContentId, }: { - cy: Core; - quizId: number; parentNodeContentId: string; targetNodeContentId?: string; }) => { //запрещаем работу родителя-ребенка если это один и тот же вопрос if (parentNodeContentId === targetNodeContentId) return; - devlog("@addNode"); - const parentNodeChildren = cy.$('edge[source = "' + parentNodeContentId + '"]')?.length; //если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа const targetQuestion = { ...getQuestionByContentId(targetNodeContentId || useUiTools.getState().dragQuestionContentId), } as AnyTypedQuizQuestion; - if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) { + if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId) { clearDataAfterAddNode({ parentNodeContentId, targetQuestion }); - createResult(quizId, 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, - }, - }, - ]); - return es; + createResult(targetQuestion.content.id); } else { enqueueSnackbar("Добавляемый вопрос не найден"); } diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index a506cbcd..80715108 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -1,7 +1,7 @@ import { cleardragQuestionContentId, setModalQuestionParentContentId, setOpenedModalQuestions, updateDeleteId, updateOpenedModalSettingsId } from "@root/uiTools/actions"; import type { AbstractEventObject, Core, NodeSingular, SingularData } from "cytoscape"; import { getPopperInstance } from "cytoscape-popper"; -import { type MutableRefObject } from "react"; +import { useCallback, type MutableRefObject } from "react"; import { addNode } from "../helper"; type PopperItem = { @@ -29,31 +29,23 @@ type NodeSingularWithPopper = NodeSingular & { export const usePopper = ({ cyRef, - quizId, popperContainerRef, popperInstancesRef, - runCyLayout, }: { cyRef: MutableRefObject; - quizId: number | undefined, popperContainerRef: MutableRefObject; popperInstancesRef: MutableRefObject; - runCyLayout: () => void; }) => { - const removePoppersById = (id: string) => { - popperContainerRef.current?.querySelector(`.popper-layout[data-id='${id}']`)?.remove(); - }; - - const removeAllPoppers = () => { + const removeAllPoppers = useCallback(() => { cyRef.current?.removeListener("zoom render"); popperInstancesRef.current.forEach(p => p.destroy()); popperInstancesRef.current = []; popperContainerRef.current?.remove(); popperContainerRef.current = null; - }; + }, []); - const createPoppers = () => { + const createPoppers = useCallback(() => { removeAllPoppers(); const cy = cyRef.current; @@ -121,15 +113,7 @@ export const usePopper = ({ plusElement.setAttribute("data-id", item.id()); plusElement.style.zIndex = "1"; plusElement.addEventListener("mouseup", () => { - if (!cy || !quizId) return; - - const es = addNode({ - cy, - quizId, - parentNodeContentId: node.id(), - }); - runCyLayout(); - if (es) cy.fit(es, 100); + addNode({ parentNodeContentId: node.id() }); cleardragQuestionContentId(); }); @@ -257,8 +241,7 @@ export const usePopper = ({ cy.on("zoom render", onZoom); }); - }; + }, []); - - return { removeAllPoppers, removePoppersById, createPoppers }; + return { removeAllPoppers, createPoppers }; }; diff --git a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts index 4288a922..2c31a007 100644 --- a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts +++ b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts @@ -10,24 +10,19 @@ import { clearDataAfterRemoveNode } from "../helper"; type UseRemoveNodeArgs = { cyRef: MutableRefObject; - runCyLayout: () => void; - removeButtons: (id: string) => void; }; export const useRemoveNode = ({ cyRef, - runCyLayout, - removeButtons, }: UseRemoveNodeArgs) => { const { questions: trashQuestions } = useQuestionsStore(); const quiz = useCurrentQuiz(); const removeNode = (targetNodeContentId: string) => { const deleteNodes: string[] = []; - const deleteEdges: SingularElementArgument[] = []; const cy = cyRef?.current; - const findChildrenToDelete = (node: CollectionReturnValue) => { + const deleteNodesRecursively = (node: CollectionReturnValue) => { //Узнаём грани, идущие от этой ноды cy ?.$('edge[source = "' + node.id() + '"]') @@ -35,20 +30,18 @@ export const useRemoveNode = ({ .forEach((edge) => { const edgeData = edge.data(); - //записываем id грани для дальнейшего удаления - deleteEdges.push(edge); //ищем ноду на конце грани, записываем её ID для дальнейшего удаления const targetNode = cy?.$("#" + edgeData.target); deleteNodes.push(targetNode.data().id); //вызываем функцию для анализа потомков уже у этой ноды - findChildrenToDelete(targetNode); + deleteNodesRecursively(targetNode); }); }; const elementToDelete = cy?.getElementById(targetNodeContentId); if (elementToDelete) { - findChildrenToDelete(elementToDelete); + deleteNodesRecursively(elementToDelete); } const targetQuestion = getQuestionByContentId(targetNodeContentId); @@ -73,12 +66,10 @@ export const useRemoveNode = ({ //createFrontResult(quiz.backendId, parentQuestionContentId); } clearDataAfterRemoveNode({ - quiz, trashQuestions, targetQuestionContentId: targetNodeContentId, parentQuestionContentId, }); - cy?.remove(cy?.$("#" + targetNodeContentId)); } } @@ -86,23 +77,13 @@ export const useRemoveNode = ({ 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); - runCyLayout(); + }); //делаем result всех потомков неактивными trashQuestions.forEach((qr) => { diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index a90e2b54..773b270b 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -16,6 +16,8 @@ import { QuestionsStore, useQuestionsStore } from "./store"; import { useUiTools } from "../uiTools/store"; import { withErrorBoundary } from "react-error-boundary"; import { QuizQuestionResult } from "@model/questionTypes/result"; +import { useQuizPreviewStore } from "@root/quizPreview"; +import { useQuizStore } from "@root/quizes/store"; export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { @@ -498,9 +500,9 @@ export const clearRuleForAll = () => { }; export const createResult = async ( - quizId: number | undefined, parentContentId?: string ) => requestQueue.enqueue(async () => { + const quizId = useQuizStore.getState().editQuizId; if (!quizId || !parentContentId) { console.error("Нет данных для создания результата. quizId: ", quizId, ", quizId: ", parentContentId) } From 5543c9980a8b9b6ff09d8d610e95c695eea7684e Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 10 Jan 2024 13:02:03 +0300 Subject: [PATCH 3/9] minor fix --- .../Questions/BranchingMap/CsComponent.tsx | 17 +++-------------- .../Questions/BranchingMap/hooks/usePopper.ts | 9 ++++----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index faa660d5..1a0d0f6b 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -24,22 +24,17 @@ import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; import { stylesheet } from "./style/stylesheet"; + Cytoscape.use(popper); -type PopperInstance = ReturnType>; - function CsComponent() { - const quiz = useCurrentQuiz(); const desireToOpenABranchingModal = useUiTools(state => state.desireToOpenABranchingModal); const canCreatePublic = useUiTools(state => state.canCreatePublic); const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId); const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore(state => state.questions); const [isPanningCy, setIsPanningCy] = useState(false); - const cyRef = useRef(null); - const popperContainerRef = useRef(null); - const popperInstancesRef = useRef([]); const questions = useMemo(() => trashQuestions.filter( (question) => question.type !== "result" && question.type !== null @@ -53,15 +48,9 @@ function CsComponent() { return storeToNodes(q); }, [questions]); - const { createPoppers, removeAllPoppers } = usePopper({ - cyRef, - popperContainerRef, - popperInstancesRef, - }); + const { createPoppers, removeAllPoppers } = usePopper({ cyRef }); - const { removeNode } = useRemoveNode({ - cyRef, - }); + const { removeNode } = useRemoveNode({ cyRef }); useLayoutEffect(() => { const cy = cyRef?.current; diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index 80715108..7093fe13 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -1,7 +1,7 @@ import { cleardragQuestionContentId, setModalQuestionParentContentId, setOpenedModalQuestions, updateDeleteId, updateOpenedModalSettingsId } from "@root/uiTools/actions"; import type { AbstractEventObject, Core, NodeSingular, SingularData } from "cytoscape"; import { getPopperInstance } from "cytoscape-popper"; -import { useCallback, type MutableRefObject } from "react"; +import { useCallback, type MutableRefObject, useRef } from "react"; import { addNode } from "../helper"; type PopperItem = { @@ -29,13 +29,12 @@ type NodeSingularWithPopper = NodeSingular & { export const usePopper = ({ cyRef, - popperContainerRef, - popperInstancesRef, }: { cyRef: MutableRefObject; - popperContainerRef: MutableRefObject; - popperInstancesRef: MutableRefObject; }) => { + const popperContainerRef = useRef(null); + const popperInstancesRef = useRef([]); + const removeAllPoppers = useCallback(() => { cyRef.current?.removeListener("zoom render"); From a2296190b6612e4a2739597e35e733550f11c5a7 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 10 Jan 2024 20:11:47 +0300 Subject: [PATCH 4/9] improve popper detaching on panning --- .../Questions/BranchingMap/CsComponent.tsx | 23 +++++-------------- .../Questions/BranchingMap/hooks/usePopper.ts | 5 ++-- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index 1a0d0f6b..5f4ac59a 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -33,7 +33,6 @@ function CsComponent() { const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId); const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore(state => state.questions); - const [isPanningCy, setIsPanningCy] = useState(false); const cyRef = useRef(null); const questions = useMemo(() => trashQuestions.filter( @@ -48,7 +47,7 @@ function CsComponent() { return storeToNodes(q); }, [questions]); - const { createPoppers, removeAllPoppers } = usePopper({ cyRef }); + const { recreatePoppers, removeAllPoppers } = usePopper({ cyRef }); const { removeNode } = useRemoveNode({ cyRef }); @@ -94,15 +93,13 @@ function CsComponent() { const onPointerDown = () => { isPointerDown = true; - cy.data("dragging", true); }; const onPointerUp = () => { + if (isPointerDown) recreatePoppers(); isPointerDown = false; - cy.data("dragging", false); - setIsPanningCy(false); }; const handleMove = () => { - setIsPanningCy(isPointerDown); + if (isPointerDown) removeAllPoppers(); }; cy.on("vmousedown", onPointerDown); @@ -114,21 +111,13 @@ function CsComponent() { cy.off("vmousemove", handleMove); document.removeEventListener("pointerup", onPointerUp); }; - }, []); - - useEffect(function poppersLifecycle() { - if (isPanningCy) { - removeAllPoppers(); - } else { - createPoppers(); - } - }, [isPanningCy, createPoppers]); + }, [recreatePoppers, removeAllPoppers]); useEffect(() => { cyRef.current?.layout(layoutOptions).run(); cyRef.current?.fit(undefined, 70); - createPoppers(); - }, [cyElements, createPoppers]); + recreatePoppers(); + }, [cyElements, recreatePoppers]); return ( <> diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index 7093fe13..52cc6fd0 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -44,7 +44,7 @@ export const usePopper = ({ popperContainerRef.current = null; }, []); - const createPoppers = useCallback(() => { + const recreatePoppers = useCallback(() => { removeAllPoppers(); const cy = cyRef.current; @@ -180,7 +180,6 @@ export const usePopper = ({ } const onZoom = (event: AbstractEventObject) => { - if (event.cy.data("dragging")) return; const zoom = event.cy.zoom(); crossesPopper.setOptions({ @@ -242,5 +241,5 @@ export const usePopper = ({ }); }, []); - return { removeAllPoppers, createPoppers }; + return { removeAllPoppers, recreatePoppers }; }; From c673b5be9f7a536f5b3525e7362473ebbd98520d Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 10 Jan 2024 21:23:38 +0300 Subject: [PATCH 5/9] minor changes --- .../Questions/BranchingMap/CsComponent.tsx | 54 +++++-------------- src/pages/Questions/BranchingMap/helper.ts | 28 ++++++++-- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index 5f4ac59a..bfae9171 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -1,24 +1,22 @@ import { devlog } from "@frontend/kitui"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { Box, Button } from "@mui/material"; -import { - clearRuleForAll -} from "@root/questions/actions"; +import { clearRuleForAll } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; import { updateRootContentId } from "@root/quizes/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQuestionTargetContentId, updateModalInfoWhyCantCreate, updateOpenedModalSettingsId } from "@root/uiTools/actions"; import { useUiTools } from "@root/uiTools/store"; import { ProblemIcon } from "@ui_kit/ProblemIcon"; -import type { Core, PresetLayoutOptions, SingularData } from "cytoscape"; +import type { Core } from "cytoscape"; import Cytoscape from "cytoscape"; -import popper, { getPopperInstance } from "cytoscape-popper"; +import popper from "cytoscape-popper"; import { enqueueSnackbar } from "notistack"; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; import CytoscapeComponent from "react-cytoscapejs"; import { withErrorBoundary } from "react-error-boundary"; import { DeleteNodeModal } from "../DeleteNodeModal"; -import { addNode, calcNodePosition, storeToNodes } from "./helper"; +import { addNode, layoutOptions, storeToNodes } from "./helper"; import { usePopper } from "./hooks/usePopper"; import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; @@ -34,22 +32,16 @@ function CsComponent() { const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore(state => state.questions); const cyRef = useRef(null); - - const questions = useMemo(() => trashQuestions.filter( - (question) => question.type !== "result" && question.type !== null - ), [trashQuestions]); + const { recreatePoppers, removeAllPoppers } = usePopper({ cyRef }); + const { removeNode } = useRemoveNode({ cyRef }); const cyElements = useMemo(() => { - const q = questions.filter( + const questions = trashQuestions.filter( (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result" ); - return storeToNodes(q); - }, [questions]); - - const { recreatePoppers, removeAllPoppers } = usePopper({ cyRef }); - - const { removeNode } = useRemoveNode({ cyRef }); + return storeToNodes(questions); + }, [trashQuestions]); useLayoutEffect(() => { const cy = cyRef?.current; @@ -77,10 +69,10 @@ function CsComponent() { useEffect(function onMount() { updateOpenedModalSettingsId(); - document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId); + document.addEventListener("pointerup", cleardragQuestionContentId); return () => { - document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId); + document.removeEventListener("pointerup", cleardragQuestionContentId); removeAllPoppers(); }; }, []); @@ -179,25 +171,3 @@ export default withErrorBoundary(CsComponent, { devlog(error); }, }); - -const layoutOptions: PresetLayoutOptions = { - name: "preset", - positions: calcNodePosition, - zoom: undefined, - pan: 1, - fit: false, - padding: 30, - animate: false, - animationDuration: 500, - animationEasing: undefined, - animateFilter: () => false, - ready: event => { - if (event.cy.data("firstNode") === "nonroot") { - event.cy.data("firstNode", "root"); - event.cy.nodes().sort((a, b) => (a.data("root") ? 1 : -1)); - } else { - event.cy.removeData("firstNode"); - } - }, - transform: (_, p) => p, -}; diff --git a/src/pages/Questions/BranchingMap/helper.ts b/src/pages/Questions/BranchingMap/helper.ts index b8f8874d..13259c87 100644 --- a/src/pages/Questions/BranchingMap/helper.ts +++ b/src/pages/Questions/BranchingMap/helper.ts @@ -1,14 +1,13 @@ -import { devlog } from "@frontend/kitui"; import { QuizQuestionResult } from "@model/questionTypes/result"; import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain, UntypedQuizQuestion } from "@model/questionTypes/shared"; -import { Quiz } from "@model/quiz/quiz"; import { createResult, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; import { useUiTools } from "@root/uiTools/store"; -import { Core } from "cytoscape"; +import { PresetLayoutOptions } from "cytoscape"; import { enqueueSnackbar } from "notistack"; + interface Nodes { data: { id: string; @@ -16,6 +15,7 @@ interface Nodes { parent?: string; }; } + interface Edges { data: { source: string; @@ -52,6 +52,28 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => { return [...nodes, ...edges]; }; +export const layoutOptions: PresetLayoutOptions = { + name: "preset", + positions: calcNodePosition, + zoom: undefined, + pan: 1, + fit: false, + padding: 30, + animate: false, + animationDuration: 500, + animationEasing: undefined, + animateFilter: () => false, + ready: event => { + if (event.cy.data("firstNode") === "nonroot") { + event.cy.data("firstNode", "root"); + event.cy.nodes().sort((a, b) => (a.data("root") ? 1 : -1)); + } else { + event.cy.removeData("firstNode"); + } + }, + transform: (_, p) => p, +}; + export function clearDataAfterAddNode({ parentNodeContentId, targetQuestion, From d565a50a66033f25c8b483ae8a9f1864efa135c1 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 10 Jan 2024 21:28:52 +0300 Subject: [PATCH 6/9] fix removePoppersOnDrag --- src/pages/Questions/BranchingMap/CsComponent.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index bfae9171..44cc4a7d 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -82,16 +82,23 @@ function CsComponent() { if (!cy) return; let isPointerDown = false; + let isDragging = false; const onPointerDown = () => { isPointerDown = true; }; const onPointerUp = () => { - if (isPointerDown) recreatePoppers(); + if (isDragging) { + isDragging = false; + recreatePoppers(); + } isPointerDown = false; }; const handleMove = () => { - if (isPointerDown) removeAllPoppers(); + if (isPointerDown) { + isDragging = true; + removeAllPoppers(); + } }; cy.on("vmousedown", onPointerDown); From 9155f1b8ed9dc0b9095da0dfd3cd7b1de0e6ec80 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 10 Jan 2024 21:29:38 +0300 Subject: [PATCH 7/9] fix adding nodes --- src/pages/Questions/BranchingMap/CsComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index 44cc4a7d..3dc68f5b 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -69,10 +69,10 @@ function CsComponent() { useEffect(function onMount() { updateOpenedModalSettingsId(); - document.addEventListener("pointerup", cleardragQuestionContentId); + document.addEventListener("mouseup", cleardragQuestionContentId); return () => { - document.removeEventListener("pointerup", cleardragQuestionContentId); + document.removeEventListener("mouseup", cleardragQuestionContentId); removeAllPoppers(); }; }, []); From 36ba2dfb6142a3c9dd96facf622a0f2985a55ef1 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 10 Jan 2024 21:32:00 +0300 Subject: [PATCH 8/9] replace mouseup with pointerup --- src/pages/Questions/BranchingMap/CsComponent.tsx | 4 ++-- src/pages/Questions/BranchingMap/FirstNodeField.tsx | 4 ++-- src/pages/Questions/BranchingMap/hooks/usePopper.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index 3dc68f5b..44cc4a7d 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -69,10 +69,10 @@ function CsComponent() { useEffect(function onMount() { updateOpenedModalSettingsId(); - document.addEventListener("mouseup", cleardragQuestionContentId); + document.addEventListener("pointerup", cleardragQuestionContentId); return () => { - document.removeEventListener("mouseup", cleardragQuestionContentId); + document.removeEventListener("pointerup", cleardragQuestionContentId); removeAllPoppers(); }; }, []); diff --git a/src/pages/Questions/BranchingMap/FirstNodeField.tsx b/src/pages/Questions/BranchingMap/FirstNodeField.tsx index be56e6f5..f5800e03 100644 --- a/src/pages/Questions/BranchingMap/FirstNodeField.tsx +++ b/src/pages/Questions/BranchingMap/FirstNodeField.tsx @@ -39,10 +39,10 @@ export const FirstNodeField = () => { }; useEffect(() => { - Container.current?.addEventListener("mouseup", newRootNode); + Container.current?.addEventListener("pointerup", newRootNode); Container.current?.addEventListener("click", modalOpen); return () => { - Container.current?.removeEventListener("mouseup", newRootNode); + Container.current?.removeEventListener("pointerup", newRootNode); Container.current?.removeEventListener("click", modalOpen); }; }, [dragQuestionContentId]); diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index 52cc6fd0..08df0a1b 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -83,7 +83,7 @@ export const usePopper = ({ layoutElement.style.zIndex = "0"; layoutElement.classList.add("popper-layout"); layoutElement.setAttribute("data-id", item.id()); - layoutElement.addEventListener("mouseup", () => { + layoutElement.addEventListener("pointerup", () => { //Узнаём грани, идущие от этой ноды setModalQuestionParentContentId(item.id()); setOpenedModalQuestions(true); @@ -111,7 +111,7 @@ export const usePopper = ({ plusElement.classList.add("popper-plus"); plusElement.setAttribute("data-id", item.id()); plusElement.style.zIndex = "1"; - plusElement.addEventListener("mouseup", () => { + plusElement.addEventListener("pointerup", () => { addNode({ parentNodeContentId: node.id() }); cleardragQuestionContentId(); }); @@ -140,7 +140,7 @@ export const usePopper = ({ crossElement.setAttribute("data-id", item.id()); crossElement.style.zIndex = "2"; popperContainerRef.current?.appendChild(crossElement); - crossElement.addEventListener("mouseup", () => { + crossElement.addEventListener("pointerup", () => { updateDeleteId(node.id()); }); @@ -169,7 +169,7 @@ export const usePopper = ({ gearElement.setAttribute("data-id", item.id()); gearElement.style.zIndex = "1"; popperContainerRef.current?.appendChild(gearElement); - gearElement.addEventListener("mouseup", () => { + gearElement.addEventListener("pointerup", () => { updateOpenedModalSettingsId(item.id()); }); From 3889c06be1f70909d81114ec60bcba0f569f7b8f Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 17 Jan 2024 18:42:25 +0300 Subject: [PATCH 9/9] use custom poppers --- .../Questions/BranchingMap/CsComponent.tsx | 58 +--- .../Questions/BranchingMap/CsNodeButtons.tsx | 291 ++++++++++++++++++ src/pages/Questions/BranchingMap/helper.ts | 39 ++- .../Questions/BranchingMap/hooks/usePopper.ts | 1 + 4 files changed, 333 insertions(+), 56 deletions(-) create mode 100644 src/pages/Questions/BranchingMap/CsNodeButtons.tsx diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index 44cc4a7d..b4033d76 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -9,22 +9,18 @@ import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQu import { useUiTools } from "@root/uiTools/store"; import { ProblemIcon } from "@ui_kit/ProblemIcon"; import type { Core } from "cytoscape"; -import Cytoscape from "cytoscape"; -import popper from "cytoscape-popper"; import { enqueueSnackbar } from "notistack"; import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; import CytoscapeComponent from "react-cytoscapejs"; import { withErrorBoundary } from "react-error-boundary"; import { DeleteNodeModal } from "../DeleteNodeModal"; +import CsNodeButtons from "./CsNodeButtons"; import { addNode, layoutOptions, storeToNodes } from "./helper"; -import { usePopper } from "./hooks/usePopper"; import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; import { stylesheet } from "./style/stylesheet"; -Cytoscape.use(popper); - function CsComponent() { const desireToOpenABranchingModal = useUiTools(state => state.desireToOpenABranchingModal); const canCreatePublic = useUiTools(state => state.canCreatePublic); @@ -32,10 +28,9 @@ function CsComponent() { const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore(state => state.questions); const cyRef = useRef(null); - const { recreatePoppers, removeAllPoppers } = usePopper({ cyRef }); const { removeNode } = useRemoveNode({ cyRef }); - const cyElements = useMemo(() => { + const csElements = useMemo(() => { const questions = trashQuestions.filter( (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result" ); @@ -73,53 +68,17 @@ function CsComponent() { return () => { document.removeEventListener("pointerup", cleardragQuestionContentId); - removeAllPoppers(); }; }, []); - useEffect(function removePoppersOnDrag() { - const cy = cyRef.current; - if (!cy) return; - - let isPointerDown = false; - let isDragging = false; - - const onPointerDown = () => { - isPointerDown = true; - }; - const onPointerUp = () => { - if (isDragging) { - isDragging = false; - recreatePoppers(); - } - isPointerDown = false; - }; - const handleMove = () => { - if (isPointerDown) { - isDragging = true; - removeAllPoppers(); - } - }; - - cy.on("vmousedown", onPointerDown); - cy.on("vmousemove", handleMove); - document.addEventListener("pointerup", onPointerUp); - - return () => { - cy.off("vmousedown", onPointerDown); - cy.off("vmousemove", handleMove); - document.removeEventListener("pointerup", onPointerUp); - }; - }, [recreatePoppers, removeAllPoppers]); - - useEffect(() => { + useEffect(function rerunLayout() { cyRef.current?.layout(layoutOptions).run(); cyRef.current?.fit(undefined, 70); - recreatePoppers(); - }, [cyElements, recreatePoppers]); + }, [csElements]); return ( <> +