Skip to content

Commit cdecf7c

Browse files
committed
feat: add basic touchscreen support
This is basically a migration from mouse events to [pointer events]( https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events ). The `PointerEvent` interface inherits all of the `MouseEvent` properties, meaning that existing code can essentially be left as-is. The only major change is making sure we only respond to the "primary" pointer. Known issues include: * stylus hover is not detected * touchscreens do not have a concept of hover, making it difficult to e.g. resize areas * no touch gesture support, e.g. "pinch-to-zoom"
1 parent 075a98d commit cdecf7c

File tree

8 files changed

+95
-67
lines changed

8 files changed

+95
-67
lines changed

src/components/EditorCanvas/Area.jsx

+10-10
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import ColorPalette from "../ColorPicker";
2121
import { useTranslation } from "react-i18next";
2222

23-
export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
23+
export default function Area({ data, onPointerDown, setResize, setInitCoords }) {
2424
const [hovered, setHovered] = useState(false);
2525
const { layout } = useLayout();
2626
const { settings } = useSettings();
@@ -35,8 +35,8 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
3535
y: data.y,
3636
width: data.width,
3737
height: data.height,
38-
mouseX: e.clientX / transform.zoom,
39-
mouseY: e.clientY / transform.zoom,
38+
pointerX: e.clientX / transform.zoom,
39+
pointerY: e.clientY / transform.zoom,
4040
});
4141
};
4242

@@ -85,16 +85,16 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
8585

8686
return (
8787
<g
88-
onMouseEnter={() => setHovered(true)}
89-
onMouseLeave={() => setHovered(false)}
88+
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
89+
onPointerLeave={(e) => e.isPrimary &&setHovered(false)}
9090
>
9191
<foreignObject
9292
key={data.id}
9393
x={data.x}
9494
y={data.y}
9595
width={data.width > 0 ? data.width : 0}
9696
height={data.height > 0 ? data.height : 0}
97-
onMouseDown={onMouseDown}
97+
onPointerDown={onPointerDown}
9898
>
9999
<div
100100
className={`border-2 ${
@@ -149,7 +149,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
149149
stroke="#5891db"
150150
strokeWidth={2}
151151
cursor="nwse-resize"
152-
onMouseDown={(e) => handleResize(e, "tl")}
152+
onPointerDown={(e) => e.isPrimary && handleResize(e, "tl")}
153153
/>
154154
<circle
155155
cx={data.x + data.width}
@@ -159,7 +159,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
159159
stroke="#5891db"
160160
strokeWidth={2}
161161
cursor="nesw-resize"
162-
onMouseDown={(e) => handleResize(e, "tr")}
162+
onPointerDown={(e) => e.isPrimary && handleResize(e, "tr")}
163163
/>
164164
<circle
165165
cx={data.x}
@@ -169,7 +169,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
169169
stroke="#5891db"
170170
strokeWidth={2}
171171
cursor="nesw-resize"
172-
onMouseDown={(e) => handleResize(e, "bl")}
172+
onPointerDown={(e) => e.isPrimary && handleResize(e, "bl")}
173173
/>
174174
<circle
175175
cx={data.x + data.width}
@@ -179,7 +179,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
179179
stroke="#5891db"
180180
strokeWidth={2}
181181
cursor="nwse-resize"
182-
onMouseDown={(e) => handleResize(e, "br")}
182+
onPointerDown={(e) => e.isPrimary && handleResize(e, "br")}
183183
/>
184184
</>
185185
)}

src/components/EditorCanvas/Canvas.jsx

+56-34
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,21 @@ export default function Canvas() {
6868
y: 0,
6969
width: 0,
7070
height: 0,
71-
mouseX: 0,
72-
mouseY: 0,
71+
pointerX: 0,
72+
pointerY: 0,
7373
});
7474
const [cursor, setCursor] = useState("default");
7575

7676
const canvas = useRef(null);
7777

78-
const handleMouseDownOnElement = (e, id, type) => {
78+
/**
79+
* @param {PointerEvent} e
80+
* @param {*} id
81+
* @param {ObjectType[keyof ObjectType]} type
82+
*/
83+
const handlePointerDownOnElement = (e, id, type) => {
84+
if (!e.isPrimary) return;
85+
7986
const { clientX, clientY } = e;
8087
if (type === ObjectType.TABLE) {
8188
const table = tables.find((t) => t.id === id);
@@ -122,7 +129,12 @@ export default function Canvas() {
122129
}));
123130
};
124131

125-
const handleMouseMove = (e) => {
132+
/**
133+
* @param {PointerEvent} e
134+
*/
135+
const handlePointerMove = (e) => {
136+
if (!e.isPrimary) return;
137+
126138
if (linking) {
127139
const rect = canvas.current.getBoundingClientRect();
128140
setLinkingLine({
@@ -164,34 +176,39 @@ export default function Canvas() {
164176
} else if (areaResize.id !== -1) {
165177
if (areaResize.dir === "none") return;
166178
let newDims = { ...initCoords };
167-
delete newDims.mouseX;
168-
delete newDims.mouseY;
169-
const mouseX = e.clientX / transform.zoom;
170-
const mouseY = e.clientY / transform.zoom;
179+
delete newDims.pointerX;
180+
delete newDims.pointerY;
181+
const pointerX = e.clientX / transform.zoom;
182+
const pointerY = e.clientY / transform.zoom;
171183
setPanning({ isPanning: false, x: 0, y: 0 });
172184
if (areaResize.dir === "br") {
173-
newDims.width = initCoords.width + (mouseX - initCoords.mouseX);
174-
newDims.height = initCoords.height + (mouseY - initCoords.mouseY);
185+
newDims.width = initCoords.width + (pointerX - initCoords.pointerX);
186+
newDims.height = initCoords.height + (pointerY - initCoords.pointerY);
175187
} else if (areaResize.dir === "tl") {
176-
newDims.x = initCoords.x + (mouseX - initCoords.mouseX);
177-
newDims.y = initCoords.y + (mouseY - initCoords.mouseY);
178-
newDims.width = initCoords.width - (mouseX - initCoords.mouseX);
179-
newDims.height = initCoords.height - (mouseY - initCoords.mouseY);
188+
newDims.x = initCoords.x + (pointerX - initCoords.pointerX);
189+
newDims.y = initCoords.y + (pointerY - initCoords.pointerY);
190+
newDims.width = initCoords.width - (pointerX - initCoords.pointerX);
191+
newDims.height = initCoords.height - (pointerY - initCoords.pointerY);
180192
} else if (areaResize.dir === "tr") {
181-
newDims.y = initCoords.y + (mouseY - initCoords.mouseY);
182-
newDims.width = initCoords.width + (mouseX - initCoords.mouseX);
183-
newDims.height = initCoords.height - (mouseY - initCoords.mouseY);
193+
newDims.y = initCoords.y + (pointerY - initCoords.pointerY);
194+
newDims.width = initCoords.width + (pointerX - initCoords.pointerX);
195+
newDims.height = initCoords.height - (pointerY - initCoords.pointerY);
184196
} else if (areaResize.dir === "bl") {
185-
newDims.x = initCoords.x + (mouseX - initCoords.mouseX);
186-
newDims.width = initCoords.width - (mouseX - initCoords.mouseX);
187-
newDims.height = initCoords.height + (mouseY - initCoords.mouseY);
197+
newDims.x = initCoords.x + (pointerX - initCoords.pointerX);
198+
newDims.width = initCoords.width - (pointerX - initCoords.pointerX);
199+
newDims.height = initCoords.height + (pointerY - initCoords.pointerY);
188200
}
189201

190202
updateArea(areaResize.id, { ...newDims });
191203
}
192204
};
193205

194-
const handleMouseDown = (e) => {
206+
/**
207+
* @param {PointerEvent} e
208+
*/
209+
const handlePointerDown = (e) => {
210+
if (!e.isPrimary) return;
211+
195212
// don't pan if the sidesheet for editing a table is open
196213
if (
197214
selectedElement.element === ObjectType.TABLE &&
@@ -268,7 +285,12 @@ export default function Canvas() {
268285
}
269286
};
270287

271-
const handleMouseUp = () => {
288+
/**
289+
* @param {PointerEvent} e
290+
*/
291+
const handlePointerUp = (e) => {
292+
if (!e.isPrimary) return;
293+
272294
if (coordsDidUpdate(dragging.element)) {
273295
const info = getMovedElementDetails();
274296
setUndoStack((prev) => [
@@ -344,8 +366,8 @@ export default function Canvas() {
344366
y: 0,
345367
width: 0,
346368
height: 0,
347-
mouseX: 0,
348-
mouseY: 0,
369+
pointerX: 0,
370+
pointerY: 0,
349371
});
350372
};
351373

@@ -411,12 +433,12 @@ export default function Canvas() {
411433
const theme = localStorage.getItem("theme");
412434

413435
return (
414-
<div className="flex-grow h-full" id="canvas">
436+
<div className="flex-grow h-full touch-none" id="canvas">
415437
<div ref={canvas} className="w-full h-full">
416438
<svg
417-
onMouseMove={handleMouseMove}
418-
onMouseDown={handleMouseDown}
419-
onMouseUp={handleMouseUp}
439+
onPointerMove={handlePointerMove}
440+
onPointerDown={handlePointerDown}
441+
onPointerUp={handlePointerUp}
420442
className="w-full h-full"
421443
style={{
422444
cursor: cursor,
@@ -464,8 +486,8 @@ export default function Canvas() {
464486
<Area
465487
key={a.id}
466488
data={a}
467-
onMouseDown={(e) =>
468-
handleMouseDownOnElement(e, a.id, ObjectType.AREA)
489+
onPointerDown={(e) =>
490+
handlePointerDownOnElement(e, a.id, ObjectType.AREA)
469491
}
470492
setResize={setAreaResize}
471493
setInitCoords={setInitCoords}
@@ -481,8 +503,8 @@ export default function Canvas() {
481503
setHoveredTable={setHoveredTable}
482504
handleGripField={handleGripField}
483505
setLinkingLine={setLinkingLine}
484-
onMouseDown={(e) =>
485-
handleMouseDownOnElement(e, table.id, ObjectType.TABLE)
506+
onPointerDown={(e) =>
507+
handlePointerDownOnElement(e, table.id, ObjectType.TABLE)
486508
}
487509
/>
488510
))}
@@ -497,8 +519,8 @@ export default function Canvas() {
497519
<Note
498520
key={n.id}
499521
data={n}
500-
onMouseDown={(e) =>
501-
handleMouseDownOnElement(e, n.id, ObjectType.NOTE)
522+
onPointerDown={(e) =>
523+
handlePointerDownOnElement(e, n.id, ObjectType.NOTE)
502524
}
503525
/>
504526
))}

src/components/EditorCanvas/Note.jsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from "../../hooks";
2222
import { useTranslation } from "react-i18next";
2323

24-
export default function Note({ data, onMouseDown }) {
24+
export default function Note({ data, onPointerDown }) {
2525
const w = 180;
2626
const r = 3;
2727
const fold = 24;
@@ -83,8 +83,8 @@ export default function Note({ data, onMouseDown }) {
8383

8484
return (
8585
<g
86-
onMouseEnter={() => setHovered(true)}
87-
onMouseLeave={() => setHovered(false)}
86+
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
87+
onPointerLeave={(e) => e.isPrimary && setHovered(false)}
8888
>
8989
<path
9090
d={`M${data.x + fold} ${data.y} L${data.x + w - r} ${
@@ -133,7 +133,7 @@ export default function Note({ data, onMouseDown }) {
133133
y={data.y}
134134
width={w}
135135
height={data.height}
136-
onMouseDown={onMouseDown}
136+
onPointerDown={onPointerDown}
137137
>
138138
<div className="text-gray-900 select-none w-full h-full cursor-move px-3 py-2">
139139
<div className="flex justify-between gap-1 w-full">

src/components/EditorCanvas/Table.jsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function Table(props) {
2222
const [hoveredField, setHoveredField] = useState(-1);
2323
const {
2424
tableData,
25-
onMouseDown,
25+
onPointerDown,
2626
setHoveredTable,
2727
handleGripField,
2828
setLinkingLine,
@@ -67,7 +67,7 @@ export default function Table(props) {
6767
width={settings.tableWidth}
6868
height={height}
6969
className="group drop-shadow-lg rounded-md cursor-move"
70-
onMouseDown={onMouseDown}
70+
onPointerDown={onPointerDown}
7171
>
7272
<div
7373
onDoubleClick={openEditor}
@@ -266,14 +266,18 @@ export default function Table(props) {
266266
? ""
267267
: "border-b border-gray-400"
268268
} group h-[36px] px-2 py-1 flex justify-between items-center gap-1 w-full overflow-hidden`}
269-
onMouseEnter={() => {
269+
onPointerEnter={(e) => {
270+
if (!e.isPrimary) return;
271+
270272
setHoveredField(index);
271273
setHoveredTable({
272274
tableId: tableData.id,
273275
field: index,
274276
});
275277
}}
276-
onMouseLeave={() => {
278+
onPointerLeave={(e) => {
279+
if (!e.isPrimary) return;
280+
277281
setHoveredField(-1);
278282
}}
279283
>
@@ -284,7 +288,9 @@ export default function Table(props) {
284288
>
285289
<button
286290
className="flex-shrink-0 w-[10px] h-[10px] bg-[#2f68adcc] rounded-full"
287-
onMouseDown={() => {
291+
onPointerDown={(e) => {
292+
if (!e.isPrimary) return;
293+
288294
handleGripField(index);
289295
setLinkingLine((prev) => ({
290296
...prev,

src/components/EditorHeader/ControlPanel.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1526,8 +1526,8 @@ export default function ControlPanel({
15261526
)}
15271527
<div
15281528
className="text-xl me-1"
1529-
onMouseEnter={() => setShowEditName(true)}
1530-
onMouseLeave={() => setShowEditName(false)}
1529+
onPointerEnter={(e) => e.isPrimary && setShowEditName(true)}
1530+
onPointerLeave={(e) => e.isPrimary && setShowEditName(false)}
15311531
onClick={() => setModal(MODAL.RENAME)}
15321532
>
15331533
{window.name.split(" ")[0] === "t" ? "Templates/" : "Diagrams/"}

src/components/EditorSidePanel/SidePanel.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export default function SidePanel({ width, resize, setResize }) {
8383
className={`flex justify-center items-center p-1 h-auto hover-2 cursor-col-resize ${
8484
resize && "bg-semi-grey-2"
8585
}`}
86-
onMouseDown={() => setResize(true)}
86+
onPointerDown={(e) => e.isPrimary && setResize(true)}
8787
>
8888
<div className="w-1 border-x border-color h-1/6" />
8989
</div>

src/components/SimpleCanvas.jsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ function Table({ table, grab }) {
2424
width={tableWidth}
2525
height={height}
2626
className="drop-shadow-lg rounded-md cursor-move"
27-
onMouseDown={grab}
28-
onMouseEnter={() => setIsHovered(true)}
29-
onMouseLeave={() => setIsHovered(false)}
27+
onPointerDown={(e) => e.isPrimary && grab(e)}
28+
onPointerEnter={(e) => e.isPrimary && setIsHovered(true)}
29+
onPointerLeave={(e) => e.isPrimary && setIsHovered(false)}
3030
>
3131
<div
3232
className={`border-2 ${
@@ -46,8 +46,8 @@ function Table({ table, grab }) {
4646
className={`${
4747
i === table.fields.length - 1 ? "" : "border-b border-gray-400"
4848
} h-[36px] px-2 py-1 flex justify-between`}
49-
onMouseEnter={() => setHoveredField(i)}
50-
onMouseLeave={() => setHoveredField(-1)}
49+
onPointerEnter={(e) => e.isPrimary && setHoveredField(i)}
50+
onPointerLeave={(e) => e.isPrimary && setHoveredField(-1)}
5151
>
5252
<div className={hoveredField === i ? "text-zinc-500" : ""}>
5353
<button
@@ -185,9 +185,9 @@ export default function SimpleCanvas({ diagram, zoom }) {
185185
return (
186186
<svg
187187
className="w-full h-full cursor-grab"
188-
onMouseUp={releaseTable}
189-
onMouseMove={moveTable}
190-
onMouseLeave={releaseTable}
188+
onPointerUp={(e) => e.isPrimary && releaseTable()}
189+
onPointerMove={(e) => e.isPrimary && moveTable()}
190+
onPointerLeave={(e) => e.isPrimary && releaseTable()}
191191
>
192192
<defs>
193193
<pattern

src/components/Workspace.jsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,9 @@ export default function WorkSpace() {
349349
/>
350350
<div
351351
className="flex h-full overflow-y-auto"
352-
onMouseUp={() => setResize(false)}
353-
onMouseLeave={() => setResize(false)}
354-
onMouseMove={handleResize}
352+
onPointerUp={(e) => e.isPrimary && setResize(false)}
353+
onPointerLeave={(e) => e.isPrimary && setResize(false)}
354+
onPointerMove={(e) => e.isPrimary && handleResize(e)}
355355
>
356356
{layout.sidebar && (
357357
<SidePanel resize={resize} setResize={setResize} width={width} />

0 commit comments

Comments
 (0)