Skip to content

Commit a08a630

Browse files
authored
Merge pull request #26 from jmacuga/no-concurrent-edit-block
No concurrent edit block
2 parents 042f8bc + 372467d commit a08a630

16 files changed

+505
-411
lines changed

components/board/board.tsx

+86-271
Large diffs are not rendered by default.

components/board/components/object-edit-indicator.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,13 @@ export const ObjectEditIndicator = ({
103103
const minY = Math.min(...yPoints);
104104
const maxY = Math.max(...yPoints);
105105

106-
x = attrs.x + minX;
107-
y = attrs.y + minY;
106+
if (attrs.x == undefined || attrs.y == undefined) {
107+
x = minX;
108+
y = minY;
109+
} else {
110+
x = attrs.x + minX;
111+
y = attrs.y + minY;
112+
}
108113
width = maxX - minX;
109114
height = maxY - minY;
110115
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Line, Rect, Circle, Arrow, Text } from "react-konva";
2+
import { ShapeRendererProps } from "@/types/board";
3+
import { KonvaEventObject } from "konva/lib/Node";
4+
5+
export const ShapeRenderer = ({
6+
id,
7+
shape,
8+
mode,
9+
onMouseDown,
10+
onDragStart,
11+
onDragEnd,
12+
onTransformEnd,
13+
onTextDblClick,
14+
ref,
15+
}: ShapeRendererProps) => {
16+
const commonProps = {
17+
draggable: mode === "selecting",
18+
onMouseDown: onMouseDown as (e: KonvaEventObject<MouseEvent>) => void,
19+
onDragStart: onDragStart as (e: KonvaEventObject<MouseEvent>) => void,
20+
onDragEnd: onDragEnd as (e: KonvaEventObject<MouseEvent>) => void,
21+
onTransformEnd: onTransformEnd as (e: KonvaEventObject<MouseEvent>) => void,
22+
strokeScaleEnabled: false,
23+
ref: (node: any) => {
24+
if (ref) ref(node);
25+
shape.attrs.ref = node;
26+
},
27+
};
28+
29+
switch (shape.className.val) {
30+
case "Line":
31+
return <Line key={id} {...shape.attrs} {...commonProps} />;
32+
case "Rect":
33+
return <Rect key={id} {...shape.attrs} {...commonProps} />;
34+
case "Circle":
35+
return <Circle key={id} {...shape.attrs} {...commonProps} />;
36+
case "Arrow":
37+
return <Arrow key={id} {...shape.attrs} {...commonProps} />;
38+
case "Text":
39+
return (
40+
<Text
41+
key={id}
42+
{...shape.attrs}
43+
{...commonProps}
44+
onDblClick={onTextDblClick}
45+
/>
46+
);
47+
default:
48+
return null;
49+
}
50+
};

components/board/context/board-context.tsx

+86-13
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import React, {
66
useMemo,
77
useRef,
88
useEffect,
9+
useCallback,
910
} from "react";
1011
import Konva from "konva";
1112
import { v4 as uuidv4 } from "uuid";
1213
import { KonvaEventObject } from "konva/lib/Node";
1314
import { Vector2d } from "konva/lib/types";
14-
15+
import { BoardMode } from "@/types/board";
1516
type Props = {
1617
children: React.ReactNode;
1718
};
@@ -20,14 +21,6 @@ export type UserCursor = {
2021
x: number;
2122
y: number;
2223
};
23-
export type ModeType =
24-
| "drawing"
25-
| "erasing"
26-
| "selecting"
27-
| "shapes"
28-
| "panning"
29-
| "teams"
30-
| "text";
3124

3225
export type ShapeType = "rectangle" | "circle" | "arrow";
3326
export type Point = Vector2d;
@@ -39,8 +32,8 @@ interface BoardContextType {
3932
setTextColor: (color: string) => void;
4033
currentLineId: string;
4134
setCurrentLineId: (id: string) => void;
42-
mode: ModeType;
43-
setMode: (mode: ModeType) => void;
35+
mode: BoardMode;
36+
setBoardMode: (mode: BoardMode) => void;
4437
selectedShapeIds: string[];
4538
setSelectedShapeIds: (ids: string[] | ((prev: string[]) => string[])) => void;
4639
isShapeSelected: (id: string) => boolean;
@@ -55,6 +48,21 @@ interface BoardContextType {
5548
isOnline: boolean;
5649
setIsOnline: (online: boolean) => void;
5750
getPointerPosition: (e: KonvaEventObject<MouseEvent>) => Point | null;
51+
localPoints: number[];
52+
setLocalPoints: (points: number[] | ((prev: number[]) => number[])) => void;
53+
stagePosition: Vector2d;
54+
setStagePosition: (
55+
position: Vector2d | ((prev: Vector2d) => Vector2d)
56+
) => void;
57+
resetStagePosition: () => void;
58+
editingText: string | null;
59+
setEditingText: (text: string | null) => void;
60+
textPosition: Point | null;
61+
setTextPosition: (position: Point | null) => void;
62+
currentTextId: string | null;
63+
setCurrentTextId: (id: string | null) => void;
64+
textareaRef: React.RefObject<HTMLTextAreaElement>;
65+
isPanning: React.MutableRefObject<boolean>;
5866
}
5967

6068
export const BoardContext = createContext<BoardContextType>(
@@ -66,14 +74,58 @@ export const BoardContextProvider: React.FC<Props> = ({ children }) => {
6674
useState<React.RefObject<Konva.Stage | null> | null>(null);
6775
const [brushColor, setBrushColor] = useState<string>("rgb(0,0,0)");
6876
const [currentLineId, setCurrentLineId] = useState<string>(uuidv4());
69-
const [mode, setMode] = useState<ModeType>("selecting");
77+
const [mode, setMode] = useState<BoardMode>("selecting");
7078
const [selectedShapeIds, setSelectedShapeIds] = useState<string[]>([]);
7179
const [brushSize, setBrushSize] = useState<number>(2);
7280
const [shapeType, setShapeType] = useState<ShapeType>("rectangle");
7381
const [shapeColor, setShapeColor] = useState("rgb(0,0,0)");
7482
const [textColor, setTextColor] = useState("rgb(0,0,0)");
7583
const [textFontSize, setTextFontSize] = useState<number>(24);
7684
const [isOnline, setIsOnline] = useState<boolean>(true);
85+
const [localPoints, setLocalPoints] = useState<number[]>([]);
86+
const [stagePosition, setStagePosition] = useState<Vector2d>({ x: 0, y: 0 });
87+
const [editingText, setEditingText] = useState<string | null>(null);
88+
const [textPosition, setTextPosition] = useState<Point | null>(null);
89+
const [currentTextId, setCurrentTextId] = useState<string | null>(null);
90+
const textareaRef = useRef<HTMLTextAreaElement>(null);
91+
const isPanning = useRef<boolean>(false);
92+
93+
const getCursorForMode = (mode: BoardMode): string => {
94+
switch (mode) {
95+
case "panning":
96+
if (isPanning.current) {
97+
return "grabbing";
98+
}
99+
return "grab";
100+
case "erasing":
101+
return "crosshair";
102+
case "text":
103+
return "text";
104+
case "selecting":
105+
case "drawing":
106+
case "shapes":
107+
return "default";
108+
default:
109+
return "default";
110+
}
111+
};
112+
113+
const setBoardMode = useCallback((mode: BoardMode) => {
114+
setMode(mode);
115+
const container = document.querySelector(".konvajs-content") as HTMLElement;
116+
if (container) {
117+
container.style.cursor = getCursorForMode(mode);
118+
}
119+
if (mode !== "text") {
120+
setEditingText(null);
121+
setTextPosition(null);
122+
setCurrentTextId(null);
123+
}
124+
}, []);
125+
126+
const resetStagePosition = useCallback(() => {
127+
setStagePosition({ x: 0, y: 0 });
128+
}, []);
77129

78130
const isShapeSelected = (id: string): boolean => {
79131
return selectedShapeIds.includes(id);
@@ -101,7 +153,7 @@ export const BoardContextProvider: React.FC<Props> = ({ children }) => {
101153
currentLineId,
102154
setCurrentLineId,
103155
mode,
104-
setMode,
156+
setBoardMode,
105157
selectedShapeIds,
106158
setSelectedShapeIds,
107159
isShapeSelected,
@@ -118,6 +170,19 @@ export const BoardContextProvider: React.FC<Props> = ({ children }) => {
118170
textFontSize,
119171
setTextFontSize,
120172
getPointerPosition,
173+
localPoints,
174+
setLocalPoints,
175+
stagePosition,
176+
setStagePosition,
177+
resetStagePosition,
178+
editingText,
179+
setEditingText,
180+
textPosition,
181+
setTextPosition,
182+
currentTextId,
183+
setCurrentTextId,
184+
textareaRef,
185+
isPanning,
121186
}),
122187
[
123188
stageRef,
@@ -134,6 +199,14 @@ export const BoardContextProvider: React.FC<Props> = ({ children }) => {
134199
textFontSize,
135200
setTextFontSize,
136201
getPointerPosition,
202+
localPoints,
203+
setLocalPoints,
204+
stagePosition,
205+
resetStagePosition,
206+
editingText,
207+
textPosition,
208+
currentTextId,
209+
setBoardMode,
137210
]
138211
);
139212

components/board/drawing-toolbar.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import { ColorPalette } from "./components/color-palette";
88
const DrawingToolbar = () => {
99
const [isColorPaletteOpen, setIsColorPaletteOpen] = useState(false);
1010
const [sizeChangeAnimation, setSizeChangeAnimation] = useState(false);
11-
const { mode, setMode, setBrushColor, brushSize, setBrushSize, brushColor } =
12-
useContext(BoardContext);
11+
const {
12+
mode,
13+
setBoardMode,
14+
setBrushColor,
15+
brushSize,
16+
setBrushSize,
17+
brushColor,
18+
} = useContext(BoardContext);
1319

1420
useEffect(() => {
1521
setSizeChangeAnimation(true);
@@ -41,13 +47,13 @@ const DrawingToolbar = () => {
4147
{
4248
label: "Brush",
4349
icon: <Brush />,
44-
onClick: () => setMode("drawing"),
50+
onClick: () => setBoardMode("drawing"),
4551
isActive: mode === "drawing",
4652
},
4753
{
4854
label: "Eraser",
4955
icon: <Eraser />,
50-
onClick: () => setMode("erasing"),
56+
onClick: () => setBoardMode("erasing"),
5157
isActive: mode === "erasing",
5258
},
5359
...brushSizes.map((size) => ({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useCallback, useContext, useRef, useEffect } from "react";
2+
import { KonvaEventObject } from "konva/lib/Node";
3+
import { BoardMode } from "@/types/board";
4+
import { useDrawing } from "./use-drawing";
5+
import { useDragging } from "./use-dragging";
6+
import { useErasing } from "./use-erasing";
7+
import { useShape } from "./use-shape";
8+
import { useBoardPanning } from "./use-board-panning";
9+
import { useText } from "./use-text";
10+
import { v4 as uuidv4 } from "uuid";
11+
import { BoardContext } from "../context/board-context";
12+
import { useDeleting } from "./use-deleting";
13+
14+
export const useBoardInteractions = () => {
15+
const isDrawing = useRef(false);
16+
const { mode, setBoardMode, setSelectedShapeIds, setCurrentLineId } =
17+
useContext(BoardContext);
18+
const { startLine, drawLine, endLine } = useDrawing();
19+
const { handleDragStart, handleDragEnd } = useDragging();
20+
const { handleEraseStart, handleEraseMove, handleEraseEnd } = useErasing();
21+
const { addShape } = useShape();
22+
const { handleBoardPanStart, handleBoardPanMove, handleBoardPanEnd } =
23+
useBoardPanning();
24+
const { addText, handleTextBlur } = useText();
25+
const { handleDelete } = useDeleting();
26+
27+
const handleMouseDown = useCallback(
28+
(e: KonvaEventObject<MouseEvent>) => {
29+
if (mode === "drawing") {
30+
isDrawing.current = true;
31+
startLine(e);
32+
} else if (mode === "erasing") {
33+
handleEraseStart(e);
34+
} else if (mode === "shapes") {
35+
addShape(e);
36+
setBoardMode("selecting");
37+
} else if (mode === "panning") {
38+
handleBoardPanStart(e);
39+
} else if (mode === "text") {
40+
addText(e);
41+
} else if (mode === "selecting") {
42+
handleTextBlur();
43+
}
44+
},
45+
[
46+
mode,
47+
startLine,
48+
handleEraseStart,
49+
addShape,
50+
setBoardMode,
51+
handleBoardPanStart,
52+
addText,
53+
]
54+
);
55+
56+
const handleKeyDown = useCallback(
57+
(e: KeyboardEvent) => {
58+
if (mode !== "selecting") return;
59+
60+
if (e.key === "Delete" || e.key === "Backspace") {
61+
e.preventDefault();
62+
handleDelete();
63+
}
64+
},
65+
[mode, handleDelete]
66+
);
67+
68+
const handleMouseMove = useCallback(
69+
(e: KonvaEventObject<MouseEvent>) => {
70+
if (mode === "drawing") {
71+
if (!isDrawing.current) return;
72+
drawLine(e as KonvaEventObject<MouseEvent>);
73+
} else if (mode === "erasing") {
74+
handleEraseMove(e as KonvaEventObject<MouseEvent>);
75+
} else if (mode === "panning") {
76+
handleBoardPanMove(e);
77+
}
78+
},
79+
[mode, drawLine, handleEraseMove, handleBoardPanMove]
80+
);
81+
82+
const handleMouseUp = useCallback(
83+
(e: KonvaEventObject<MouseEvent>) => {
84+
if (mode === "drawing") {
85+
isDrawing.current = false;
86+
endLine();
87+
setCurrentLineId(uuidv4());
88+
} else if (mode === "erasing") {
89+
handleEraseEnd();
90+
} else if (mode === "panning") {
91+
handleBoardPanEnd(e);
92+
}
93+
},
94+
[mode, endLine, setCurrentLineId, handleEraseEnd, handleBoardPanEnd]
95+
);
96+
97+
const handleStageClick = useCallback(
98+
(e: KonvaEventObject<MouseEvent>) => {
99+
if (e.target === e.target.getStage()) {
100+
setSelectedShapeIds([]);
101+
}
102+
},
103+
[setSelectedShapeIds]
104+
);
105+
106+
const handleShapeMouseDown = useCallback(
107+
(e: KonvaEventObject<MouseEvent>) => {
108+
if (mode !== "selecting") return;
109+
e.cancelBubble = true;
110+
const shapeId = e.target.attrs.id;
111+
setSelectedShapeIds([shapeId]);
112+
},
113+
[mode, setSelectedShapeIds]
114+
);
115+
116+
return {
117+
handleMouseDown,
118+
handleMouseMove,
119+
handleMouseUp,
120+
handleStageClick,
121+
handleShapeMouseDown,
122+
handleDragStart,
123+
handleDragEnd,
124+
handleKeyDown,
125+
};
126+
};

0 commit comments

Comments
 (0)