Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/components/project/CanvasBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ const CanvasBlockComponent = (props: CanvasBlockProps) => {
);

const onLongPress = useCallback(
(e: React.TouchEvent | TouchEvent) => {
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
handleContentContextMenu(e as unknown as React.MouseEvent);
},
[handleContentContextMenu],
Expand Down Expand Up @@ -1086,7 +1086,7 @@ const CanvasBlockComponent = (props: CanvasBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
27 changes: 14 additions & 13 deletions src/app/components/project/ChecklistBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,19 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => {
const status =
percentage === 100 ? "complete" : percentage > 0 ? "in-progress" : "idle";

const onLongPress = useCallback((e: React.TouchEvent | TouchEvent) => {
const target = e.target as HTMLElement;
const event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX:
"touches" in e ? e.touches[0].clientX : (e as MouseEvent).clientX,
clientY:
"touches" in e ? e.touches[0].clientY : (e as MouseEvent).clientY,
});
target.dispatchEvent(event);
}, []);
const onLongPress = useCallback(
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
const target = e.target as HTMLElement;
const event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: (e as PointerEvent).clientX,
clientY: (e as PointerEvent).clientY,
});
target.dispatchEvent(event);
},
[],
);

const touchHandlers = useTouchGestures({
onLongPress,
Expand Down Expand Up @@ -831,7 +832,7 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/project/ContactBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ const ContactBlock = memo(({ id, data, selected }: ContactBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/project/FileBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ const FileBlock = (props: CanvasBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/project/GitBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const GitBlock = (props: CanvasBlockProps) => {
);

const onLongPress = useCallback(
(e: React.TouchEvent | TouchEvent) => {
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
handleContentContextMenu(e as unknown as React.MouseEvent);
},
[handleContentContextMenu],
Expand Down Expand Up @@ -681,7 +681,7 @@ const GitBlock = (props: CanvasBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/project/NoteBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ const NoteBlock = memo(
onChange={handleTitleChange}
onFocus={() => setIsTitleEditing(true)}
onBlur={() => setIsTitleEditing(false)}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
disabled={isReadOnly}
/>
Expand Down
8 changes: 6 additions & 2 deletions src/app/components/project/PaletteBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ const PaletteBlock = memo(({ id, data, selected }: PaletteBlockProps) => {
);

const onLongPress = useCallback(
(e: React.TouchEvent | TouchEvent, x: number, y: number) => {
(
e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent,
x: number,
y: number,
) => {
if (isReadOnly) return;
const target = e.target as HTMLElement;
const colorItem = target.closest("[data-color-index]");
Expand Down Expand Up @@ -278,7 +282,7 @@ const PaletteBlock = memo(({ id, data, selected }: PaletteBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
84 changes: 37 additions & 47 deletions src/app/components/project/ProjectCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
} from "./hooks/useProjectCanvasState";
import { DEFAULT_VIEWPORT } from "./utils/constants";
import { useTouchGestures } from "./hooks/useTouchGestures";
import { useCanvasTouchViewport } from "./hooks/useCanvasTouchViewport";
const FIXED_EXTENT: [[number, number], [number, number]] = [
[-5000, -4000],
[8000, 5000],
Expand Down Expand Up @@ -279,22 +280,9 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) {

const routerRef = useRef(router);
const dictRef = useRef(dict);
const isTouchRef = useRef(false);

useEffect(() => {
const onTouch = () => {
isTouchRef.current = true;
};
const onMouse = () => {
isTouchRef.current = false;
};
document.addEventListener("touchstart", onTouch, true);
document.addEventListener("mousedown", onMouse, true);
return () => {
document.removeEventListener("touchstart", onTouch, true);
document.removeEventListener("mousedown", onMouse, true);
};
}, []);
// Track the last pointer type ("touch" | "pen" | "mouse") so we can
// suppress the browser context menu for non-mouse input.
const pointerTypeRef = useRef<string>("");

useEffect(() => {
dictRef.current = dict;
Expand Down Expand Up @@ -761,7 +749,11 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) {
);

const onLongPress = useCallback(
(e: React.TouchEvent | TouchEvent, x: number, y: number) => {
(
e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent,
x: number,
y: number,
) => {
if (isReadOnly) return;

// Clear any existing selection to prevent text selection on long press
Expand Down Expand Up @@ -854,34 +846,6 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) {
[onBlockContextMenu, originalOnPaneClick],
);

const onPinch = useCallback(
(delta: number, centerX: number, centerY: number) => {
if (isReadOnly) return;
const { x, y, zoom } = getViewport();

const sensitivity = 0.01; // Boosted
const factor = Math.pow(2, delta * sensitivity);
const nextZoom = Math.min(Math.max(zoom * factor, 0.1), 4);

if (nextZoom === zoom) return;

const rect = flowContainerRef.current?.getBoundingClientRect();
if (!rect) return;

const localX = centerX - rect.left;
const localY = centerY - rect.top;

const flowX = (localX - x) / zoom;
const flowY = (localY - y) / zoom;

const nextX = localX - flowX * nextZoom;
const nextY = localY - flowY * nextZoom;

setViewport({ x: nextX, y: nextY, zoom: nextZoom }, { duration: 0 });
},
[getViewport, setViewport, isReadOnly],
);

const touchHandlers = useTouchGestures({
onLongPress,
onDoubleTap: (e, x, y) => {
Expand Down Expand Up @@ -917,10 +881,24 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) {
clientY: y,
} as unknown as React.MouseEvent);
},
onPinch,
allowLongPress: false,
});

const canvasTouchViewportHandlers = useCanvasTouchViewport({
disabled: isReadOnly,
minZoom: 0.1,
maxZoom: 4,
getViewport,
setViewport,
onPaneDoubleTap: (x, y) => {
onPaneContextMenu({
preventDefault: () => {},
clientX: x,
clientY: y,
} as unknown as React.MouseEvent);
},
});

useEffect(() => {
const container = flowContainerRef.current;
if (!container) return;
Expand Down Expand Up @@ -1231,6 +1209,15 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) {
onDrop={handleExternalDrop}
tabIndex={0}
ref={flowContainerRef}
onPointerDownCapture={(e) => {
pointerTypeRef.current = e.pointerType;
canvasTouchViewportHandlers.onPointerDownCapture(e);
}}
onPointerMoveCapture={canvasTouchViewportHandlers.onPointerMoveCapture}
onPointerUpCapture={canvasTouchViewportHandlers.onPointerUpCapture}
onPointerCancelCapture={
canvasTouchViewportHandlers.onPointerCancelCapture
}
{...touchHandlers}
>
{isLoading && (
Expand Down Expand Up @@ -1311,7 +1298,10 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) {
onPointerMove={onPointerMove}
onPointerLeave={onPointerLeave}
onPaneContextMenu={(e) => {
if (isTouchRef.current) {
if (
pointerTypeRef.current === "touch" ||
pointerTypeRef.current === "pen"
) {
e.preventDefault();
return;
}
Expand Down
27 changes: 14 additions & 13 deletions src/app/components/project/ShellBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,18 +329,19 @@ const ShellBlock = memo(({ id, data, selected }: ShellBlockProps) => {
const isBeingMoved = !!data.movingUserColor;
const borderColor = isBeingMoved ? data.movingUserColor : "var(--border)";

const onLongPress = useCallback((e: React.TouchEvent | TouchEvent) => {
const target = e.target as HTMLElement;
const event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX:
"touches" in e ? e.touches[0].clientX : (e as MouseEvent).clientX,
clientY:
"touches" in e ? e.touches[0].clientY : (e as MouseEvent).clientY,
});
target.dispatchEvent(event);
}, []);
const onLongPress = useCallback(
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
const target = e.target as HTMLElement;
const event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: (e as PointerEvent).clientX,
clientY: (e as PointerEvent).clientY,
});
target.dispatchEvent(event);
},
[],
);

const touchHandlers = useTouchGestures({
onLongPress,
Expand Down Expand Up @@ -422,7 +423,7 @@ const ShellBlock = memo(({ id, data, selected }: ShellBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/project/SketchBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ const SketchBlock = memo((props: SketchBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
27 changes: 14 additions & 13 deletions src/app/components/project/SnippetBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,18 +289,19 @@ const SnippetBlock = memo(({ id, data, selected }: SnippetBlockProps) => {
const isBeingMoved = !!data.movingUserColor;
const borderColor = isBeingMoved ? data.movingUserColor : "var(--border)";

const onLongPress = useCallback((e: React.TouchEvent | TouchEvent) => {
const target = e.target as HTMLElement;
const event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX:
"touches" in e ? e.touches[0].clientX : (e as MouseEvent).clientX,
clientY:
"touches" in e ? e.touches[0].clientY : (e as MouseEvent).clientY,
});
target.dispatchEvent(event);
}, []);
const onLongPress = useCallback(
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
const target = e.target as HTMLElement;
const event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: (e as PointerEvent).clientX,
clientY: (e as PointerEvent).clientY,
});
target.dispatchEvent(event);
},
[],
);

const touchHandlers = useTouchGestures({
onLongPress,
Expand Down Expand Up @@ -431,7 +432,7 @@ const SnippetBlock = memo(({ id, data, selected }: SnippetBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title mr-2"
className="block-title nodrag mr-2"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/project/VercelBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ const VercelBlock = (props: CanvasBlockProps) => {
);

const onLongPress = useCallback(
(e: React.TouchEvent | TouchEvent) => {
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
handleContentContextMenu(e as unknown as React.MouseEvent);
},
[handleContentContextMenu],
Expand Down Expand Up @@ -486,7 +486,7 @@ const VercelBlock = (props: CanvasBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/project/VideoBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const VideoBlock = memo(({ id, data, selected }: VideoBlockProps) => {
);

const onLongPress = useCallback(
(e: React.TouchEvent | TouchEvent) => {
(e: React.PointerEvent | PointerEvent | React.TouchEvent | TouchEvent) => {
handleContentContextMenu(e as unknown as React.MouseEvent);
},
[handleContentContextMenu],
Expand Down Expand Up @@ -278,7 +278,7 @@ const VideoBlock = memo(({ id, data, selected }: VideoBlockProps) => {
<input
value={title}
onChange={handleTitleChange}
className="block-title"
className="block-title nodrag"
placeholder={dict.blocks.title || "..."}
readOnly={isReadOnly}
/>
Expand Down
Loading
Loading