fix branching graph unmount bug

CsNodeButtons renders normally instead of portal
This commit is contained in:
nflnkr 2024-05-30 15:05:27 +03:00
parent 03cbf1d963
commit a6c1b4b93a
3 changed files with 60 additions and 105 deletions

@ -9,11 +9,9 @@ import {
cleardragQuestionContentId, cleardragQuestionContentId,
setModalQuestionParentContentId, setModalQuestionParentContentId,
setModalQuestionTargetContentId, setModalQuestionTargetContentId,
updateModalInfoWhyCantCreate,
updateOpenedModalSettingsId, updateOpenedModalSettingsId,
} from "@root/uiTools/actions"; } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store"; import { useUiTools } from "@root/uiTools/store";
import { ProblemIcon } from "@ui_kit/ProblemIcon";
import type { Core } from "cytoscape"; import type { Core } from "cytoscape";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
@ -27,24 +25,16 @@ import "./style/styles.css";
import { stylesheet } from "./style/stylesheet"; import { stylesheet } from "./style/stylesheet";
function CsComponent() { function CsComponent() {
const desireToOpenABranchingModal = useUiTools( const desireToOpenABranchingModal = useUiTools((state) => state.desireToOpenABranchingModal);
(state) => state.desireToOpenABranchingModal, const modalQuestionParentContentId = useUiTools((state) => state.modalQuestionParentContentId);
); const modalQuestionTargetContentId = useUiTools((state) => state.modalQuestionTargetContentId);
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 trashQuestions = useQuestionsStore((state) => state.questions);
const cyRef = useRef<Core | null>(null); const cyRef = useRef<Core | null>(null);
const { removeNode } = useRemoveNode({ cyRef }); const { removeNode } = useRemoveNode({ cyRef });
const csElements = useMemo(() => { const csElements = useMemo(() => {
const questions = trashQuestions.filter( const questions = trashQuestions.filter(
(question): question is AnyTypedQuizQuestion => (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
question.type !== null && question.type !== "result",
); );
return storeToNodes(questions); return storeToNodes(questions);
@ -54,9 +44,7 @@ function CsComponent() {
const cy = cyRef?.current; const cy = cyRef?.current;
if (desireToOpenABranchingModal) { if (desireToOpenABranchingModal) {
setTimeout(() => { setTimeout(() => {
cy cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true);
?.getElementById(desireToOpenABranchingModal)
?.data("eroticeyeblink", true);
}, 250); }, 250);
} else { } else {
cy?.elements().data("eroticeyeblink", false); cy?.elements().data("eroticeyeblink", false);
@ -64,10 +52,7 @@ function CsComponent() {
}, [desireToOpenABranchingModal]); }, [desireToOpenABranchingModal]);
useEffect(() => { useEffect(() => {
if ( if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
modalQuestionTargetContentId.length !== 0 &&
modalQuestionParentContentId.length !== 0
) {
if (!cyRef.current) return; if (!cyRef.current) return;
addNode({ addNode({
@ -93,12 +78,11 @@ function CsComponent() {
cyRef.current?.layout(layoutOptions).run(); cyRef.current?.layout(layoutOptions).run();
cyRef.current?.fit(undefined, 70); cyRef.current?.fit(undefined, 70);
}, },
[csElements], [csElements]
); );
return ( return (
<> <>
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
<Box mb="20px"> <Box mb="20px">
<Button <Button
sx={{ sx={{
@ -115,23 +99,32 @@ function CsComponent() {
Выровнять Выровнять
</Button> </Button>
</Box> </Box>
<CytoscapeComponent <Box
wheelSensitivity={0.1} sx={{
elements={csElements} position: "relative",
style={{
height: "480px", height: "480px",
background: "#F2F3F7", background: "#F2F3F7",
overflow: "hidden",
}} }}
stylesheet={stylesheet} >
layout={layoutOptions} <CytoscapeComponent
cy={(cy) => { wheelSensitivity={0.1}
cyRef.current = cy; elements={csElements}
}} style={{
autoungrabify={true} height: "100%",
autounselectify={true} width: "100%",
boxSelectionEnabled={false} overflow: "hidden",
/> }}
stylesheet={stylesheet}
layout={layoutOptions}
cy={(cy) => {
cyRef.current = cy;
}}
autoungrabify={true}
autounselectify={true}
boxSelectionEnabled={false}
/>
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
</Box>
<DeleteNodeModal removeNode={removeNode} /> <DeleteNodeModal removeNode={removeNode} />
</> </>
); );

@ -8,29 +8,14 @@ import {
updateOpenedModalSettingsId, updateOpenedModalSettingsId,
} from "@root/uiTools/actions"; } from "@root/uiTools/actions";
import { Core, EventObject, NodeSingular } from "cytoscape"; import { Core, EventObject, NodeSingular } from "cytoscape";
import { import { MutableRefObject, forwardRef, useEffect, useMemo, useRef } from "react";
MutableRefObject, import { addNode, isElementANode, isNodeInViewport, isQuestionProhibited, storeToNodes } from "./helper";
forwardRef,
useEffect,
useMemo,
useRef,
} from "react";
import { createPortal } from "react-dom";
import {
addNode,
isElementANode,
isNodeInViewport,
isQuestionProhibited,
storeToNodes,
} from "./helper";
const csButtonTypes = ["delete", "add", "settings", "select"] as const; const csButtonTypes = ["delete", "add", "settings", "select"] as const;
type CsButtonType = (typeof csButtonTypes)[number]; type CsButtonType = (typeof csButtonTypes)[number];
type CsNodeButtonsByType = Partial< type CsNodeButtonsByType = Partial<Record<CsButtonType, HTMLButtonElement | null>>;
Record<CsButtonType, HTMLButtonElement | null>
>;
type CsButtonsById = Record<string, CsNodeButtonsByType | undefined>; type CsButtonsById = Record<string, CsNodeButtonsByType | undefined>;
@ -44,10 +29,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
const buttons = useMemo(() => { const buttons = useMemo(() => {
const nodeElements = csElements.filter(isElementANode); const nodeElements = csElements.filter(isElementANode);
buttonRefsById.current = nodeElements.reduce<CsButtonsById>( buttonRefsById.current = nodeElements.reduce<CsButtonsById>((acc, node) => ((acc[node.data.id] = {}), acc), {});
(acc, node) => ((acc[node.data.id] = {}), acc),
{},
);
return ( return (
<Box <Box
sx={{ sx={{
@ -56,6 +38,10 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
left: 0, left: 0,
width: "100%", width: "100%",
height: "100%", height: "100%",
pointerEvents: "none",
"> *": {
pointerEvents: "auto",
},
}} }}
> >
{nodeElements.flatMap((csElement) => [ {nodeElements.flatMap((csElement) => [
@ -80,19 +66,18 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
cleardragQuestionContentId(); cleardragQuestionContentId();
}} }}
/>, />,
!csElement.data.isRoot && !csElement.data.isRoot && !isQuestionProhibited(csElement.data.qtype) && (
!isQuestionProhibited(csElement.data.qtype) && ( <CsSettingsButton
<CsSettingsButton key={`settings-${csElement.data.id}`}
key={`settings-${csElement.data.id}`} ref={(r) => {
ref={(r) => { const buttonData = buttonRefsById.current[csElement.data.id];
const buttonData = buttonRefsById.current[csElement.data.id]; if (buttonData) buttonData.settings = r;
if (buttonData) buttonData.settings = r; }}
}} onClick={() => {
onClick={() => { updateOpenedModalSettingsId(csElement.data.id);
updateOpenedModalSettingsId(csElement.data.id); }}
}} />
/> ),
),
//оболочка узла //оболочка узла
<CsSelectButton <CsSelectButton
key={`select-${csElement.data.id}`} key={`select-${csElement.data.id}`}
@ -102,12 +87,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
}} }}
onClick={() => { onClick={() => {
setModalQuestionParentContentId(csElement.data.id); setModalQuestionParentContentId(csElement.data.id);
setOpenedModalQuestions( setOpenedModalQuestions(!(isQuestionProhibited(csElement.data.type) && csElement.data.children > 0));
!(
isQuestionProhibited(csElement.data.type) &&
csElement.data.children > 0
),
);
}} }}
/>, />,
])} ])}
@ -150,10 +130,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
}; };
}, []); }, []);
const container = cyRef.current?.container(); return buttons;
const buttonsPortal = container ? createPortal(buttons, container) : null;
return buttonsPortal;
} }
const applyButtonStyleByType: Record< const applyButtonStyleByType: Record<
@ -162,10 +139,8 @@ const applyButtonStyleByType: Record<
> = { > = {
delete(button, node, zoom) { delete(button, node, zoom) {
const nodePosition = node.renderedPosition(); const nodePosition = node.renderedPosition();
const shiftX = const shiftX = node.renderedWidth() / 2 - (CLOSE_BUTTON_WIDTH / 2 + 6) * zoom;
node.renderedWidth() / 2 - (CLOSE_BUTTON_WIDTH / 2 + 6) * zoom; const shiftY = node.renderedHeight() / 2 - (CLOSE_BUTTON_HEIGHT / 2 + 6) * zoom;
const shiftY =
node.renderedHeight() / 2 - (CLOSE_BUTTON_HEIGHT / 2 + 6) * zoom;
if (!isNodeInViewport(node, 100)) { if (!isNodeInViewport(node, 100)) {
return button.style.setProperty("display", "none"); return button.style.setProperty("display", "none");
@ -176,7 +151,7 @@ const applyButtonStyleByType: Record<
button.style.setProperty("top", `${nodePosition.y}px`); button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty( button.style.setProperty(
"transform", "transform",
`translate3d(calc(-50% + ${shiftX}px), calc(-50% - ${shiftY}px), 0) scale(${zoom})`, `translate3d(calc(-50% + ${shiftX}px), calc(-50% - ${shiftY}px), 0) scale(${zoom})`
); );
}, },
add(button, node, zoom) { add(button, node, zoom) {
@ -190,15 +165,11 @@ const applyButtonStyleByType: Record<
button.style.setProperty("display", "flex"); button.style.setProperty("display", "flex");
button.style.setProperty("left", `${nodePosition.x}px`); button.style.setProperty("left", `${nodePosition.x}px`);
button.style.setProperty("top", `${nodePosition.y}px`); button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty( button.style.setProperty("transform", `translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`);
"transform",
`translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`,
);
}, },
settings(button, node, zoom) { settings(button, node, zoom) {
const nodePosition = node.renderedPosition(); const nodePosition = node.renderedPosition();
const shiftX = const shiftX = -node.renderedWidth() / 2 - (SETTINGS_BUTTON_WIDTH / 2) * zoom;
-node.renderedWidth() / 2 - (SETTINGS_BUTTON_WIDTH / 2) * zoom;
if (!isNodeInViewport(node, 100)) { if (!isNodeInViewport(node, 100)) {
return button.style.setProperty("display", "none"); return button.style.setProperty("display", "none");
@ -207,10 +178,7 @@ const applyButtonStyleByType: Record<
button.style.setProperty("display", "flex"); button.style.setProperty("display", "flex");
button.style.setProperty("left", `${nodePosition.x}px`); button.style.setProperty("left", `${nodePosition.x}px`);
button.style.setProperty("top", `${nodePosition.y}px`); button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty( button.style.setProperty("transform", `translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`);
"transform",
`translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`,
);
}, },
select(button, node, zoom) { select(button, node, zoom) {
const nodePosition = node.renderedPosition(); const nodePosition = node.renderedPosition();
@ -222,10 +190,7 @@ const applyButtonStyleByType: Record<
button.style.setProperty("display", "flex"); button.style.setProperty("display", "flex");
button.style.setProperty("left", `${nodePosition.x}px`); button.style.setProperty("left", `${nodePosition.x}px`);
button.style.setProperty("top", `${nodePosition.y}px`); button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty( button.style.setProperty("transform", `translate3d(-50%, -50%, 0) scale(${zoom})`);
"transform",
`translate3d(-50%, -50%, 0) scale(${zoom})`,
);
}, },
}; };

@ -7,13 +7,10 @@ import { FirstNodeField } from "./FirstNodeField";
export const BranchingMap = () => { export const BranchingMap = () => {
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const dragQuestionContentId = useUiTools( const dragQuestionContentId = useUiTools((state) => state.dragQuestionContentId);
(state) => state.dragQuestionContentId,
);
return ( return (
<Box <Box
id="cytoscape-container"
sx={{ sx={{
overflow: "hidden", overflow: "hidden",
padding: "20px", padding: "20px",