Skip to content

Commit

Permalink
feat: 모바일 환경에서 더보기 버튼을 통한 노드.간선 편집 (#199)
Browse files Browse the repository at this point in the history
* feat: 터치디바이스 여부 확인 로직

* feat: 모바일환경에서 더보기 버튼 클릭 시 context-menu 표시

* feat: 더보기 버튼 클릭 시 clientX, clientY값 정의하여 전달

* feat: 간선 삭제 버튼 스타일 수정

* feat: 삭제 버튼 터치 핸들러 연결
  • Loading branch information
CatyJazzy authored Dec 3, 2024
1 parent 8b7feac commit cbd1cd1
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 5 deletions.
63 changes: 62 additions & 1 deletion packages/frontend/src/components/Edge.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,70 @@
import { useEffect, useState } from "react";
import { Group, Line } from "react-konva";
import { Circle, Group, Line, Text } from "react-konva";

import Konva from "konva";
import { KonvaEventObject } from "konva/lib/Node";
import type { Edge } from "shared/types";

type EdgeProps = Edge & Konva.LineConfig;

const BUTTON_RADIUS = 12;

type EdgeEditButtonProps = {
points: number[];
onTap: (edgeId: string) => void;
};

function EdgeEditButton({ points, onTap }: EdgeEditButtonProps) {
const [isTouch, setIsTouch] = useState(false);

useEffect(() => {
setIsTouch("ontouchstart" in window || navigator.maxTouchPoints > 0);
}, []);

if (!isTouch || points.length < 4) return null;

const handleTap = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
const targetGroup = e.target
.findAncestor("Group")
.findAncestor("Group") as Konva.Group;

if (!targetGroup) return;

const targetEdge = targetGroup.children.find(
(konvaNode) => konvaNode.attrs.name === "edge",
);

if (!targetEdge) return;

onTap(targetEdge.attrs.id);
};

const middleX = (points[0] + points[2]) / 2;
const middleY = (points[1] + points[3]) / 2;

return (
<Group x={middleX} y={middleY} onTap={handleTap}>
<Circle
radius={BUTTON_RADIUS}
fill="#FAF9F7"
stroke="#FFFFFF"
strokeWidth={2}
/>
<Text
fontSize={16}
fill="#787878"
text="×"
align="center"
verticalAlign="middle"
width={BUTTON_RADIUS * 2}
height={BUTTON_RADIUS * 2}
offsetX={BUTTON_RADIUS}
offsetY={BUTTON_RADIUS - 1}
/>
</Group>
);
}

function calculateOffsets(
from: { x: number; y: number },
to: { x: number; y: number },
Expand All @@ -26,6 +85,7 @@ export default function Edge({
to,
id,
onContextMenu,
onDelete,
...rest
}: EdgeProps) {
const [points, setPoints] = useState<number[]>([]);
Expand Down Expand Up @@ -66,6 +126,7 @@ export default function Edge({
name="edge"
id={id}
/>
<EdgeEditButton points={points} onTap={onDelete} />
</Group>
);
}
95 changes: 93 additions & 2 deletions packages/frontend/src/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { Circle, Group, KonvaNodeEvents, Text } from "react-konva";
import { useNavigate } from "react-router-dom";

import Konva from "konva";
import {
KonvaEventObject,
Node as KonvaNode,
NodeConfig,
} from "konva/lib/Node";
import { Vector2d } from "konva/lib/types";

const RADIUS = 64;
const MORE_BUTTON_RADIUS = 12;

type NodeProps = {
id: string;
Expand Down Expand Up @@ -72,6 +78,7 @@ Node.Text = function NodeText({
fontSize,
fontStyle,
width,
...rest
}: NodeTextProps) {
const ref = useRef<Konva.Text>(null);
const [offset, setOffset] = useState<Konva.Vector2d | undefined>(undefined);
Expand All @@ -95,10 +102,76 @@ Node.Text = function NodeText({
align="center"
wrap="none"
ellipsis
{...rest}
/>
);
};

type NodeMoreButtonProps = {
onTap?:
| ((evt: KonvaEventObject<PointerEvent, KonvaNode<NodeConfig>>) => void)
| undefined;
content: string;
};

Node.MoreButton = function NodeMoreButton({ content }: NodeMoreButtonProps) {
const [isTouch, setIsTouch] = useState(false);

useEffect(() => {
setIsTouch("ontouchstart" in window || navigator.maxTouchPoints > 0);
}, []);

if (!isTouch) return null;

const handleTap = (e: KonvaEventObject<Event>) => {
e.cancelBubble = true;

const parentNode = e.target.findAncestor("Group").findAncestor("Group");

if (!parentNode) return;

const absolutePosition = parentNode.getAbsolutePosition();

const contextMenuEvent = new MouseEvent("contextmenu", {
button: 2,
buttons: 2,
clientX: absolutePosition.x,
clientY: absolutePosition.y,
bubbles: true,
});

parentNode.fire("contextmenu", {
evt: contextMenuEvent,
target: parentNode,
});
};

return (
<Group x={RADIUS - 20} y={-RADIUS + 20} onTap={handleTap}>
<Circle
x={0}
y={0}
radius={MORE_BUTTON_RADIUS}
fill="#FFFFFF"
stroke="#DED8D3"
strokeWidth={1.5}
/>
<Text
x={0}
y={1}
fontSize={16}
text={content}
align="center"
verticalAlign="middle"
width={MORE_BUTTON_RADIUS * 2}
height={MORE_BUTTON_RADIUS * 2}
offsetX={MORE_BUTTON_RADIUS}
offsetY={MORE_BUTTON_RADIUS}
/>
</Group>
);
};

export type HeadNodeProps = {
id: string;
name: string;
Expand Down Expand Up @@ -126,7 +199,15 @@ export type NoteNodeProps = {
name: string;
} & NodeHandlers;

export function NoteNode({ id, x, y, name, src, ...rest }: NoteNodeProps) {
export function NoteNode({
id,
x,
y,
name,
src,
onContextMenu,
...rest
}: NoteNodeProps) {
// TODO: src 적용 필요
const navigate = useNavigate();
return (
Expand All @@ -139,10 +220,12 @@ export function NoteNode({ id, x, y, name, src, ...rest }: NoteNodeProps) {
navigate(`/note/${src}`);
}
}}
onContextMenu={onContextMenu}
{...rest}
>
<Node.Circle radius={RADIUS} fill="#FAF9F7" stroke="#DED8D3" />
<Node.Text width={RADIUS * 2} fontSize={16} content={name} />
<Node.MoreButton onTap={onContextMenu} content="⋮" />
</Node>
);
}
Expand All @@ -161,6 +244,7 @@ export function SubspaceNode({
y,
name,
src,
onContextMenu,
...rest
}: SubspaceNodeProps) {
const navigate = useNavigate();
Expand All @@ -171,10 +255,17 @@ export function SubspaceNode({
x={x}
y={y}
onClick={() => navigate(`/space/${src}`)}
onContextMenu={onContextMenu}
{...rest}
>
<Node.Circle radius={RADIUS} fill="#FFF4BB" stroke="#F9D46B" />
<Node.Text width={RADIUS * 2} fontSize={16} fontStyle="700" content={name} />
<Node.Text
width={RADIUS * 2}
fontSize={16}
fontStyle="700"
content={name}
/>
<Node.MoreButton onTap={onContextMenu} content="⋮" />
</Node>
);
}
7 changes: 5 additions & 2 deletions packages/frontend/src/components/space/SpaceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
};
}, [autofitTo]);

const handleContextMenu = (e: KonvaEventObject<MouseEvent>) => {
const handleContextMenu = (e: KonvaEventObject<MouseEvent | Event>) => {
clearSelection();

const { target } = e;
Expand All @@ -159,7 +159,9 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
return;
}

const group = target.findAncestor("Group");
// Mobile 환경에서는 group을 대상으로 임의로 이벤트 발생시킴
const group =
target instanceof Konva.Group ? target : target.findAncestor("Group");

const nodeId = group?.attrs?.id as string | undefined;

Expand Down Expand Up @@ -299,6 +301,7 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
to={edge.to}
nodes={nodes}
onContextMenu={handleContextMenu}
onDelete={deleteEdge}
/>
));

Expand Down

0 comments on commit cbd1cd1

Please sign in to comment.