diff --git a/packages/react-sketch-canvas/src/Canvas/index.tsx b/packages/react-sketch-canvas/src/Canvas/index.tsx index 8186d9d..e3bf5e8 100644 --- a/packages/react-sketch-canvas/src/Canvas/index.tsx +++ b/packages/react-sketch-canvas/src/Canvas/index.tsx @@ -7,10 +7,17 @@ import { ExportImageOptions, ExportImageType, Point, + TouchExtends, } from "../types"; import { CanvasProps, CanvasRef } from "./types"; import { useThrottledCallback } from "./utils"; +const TOUCH_TYPE_MAP: Record = { + 'direct': 'touch', + 'stylus': 'pen', + default: 'mouse' +} + const loadImage = (url: string): Promise => new Promise((resolve, reject) => { const img = new Image(); @@ -81,7 +88,7 @@ export const Canvas = React.forwardRef((props, ref) => { // Converts mouse coordinates to relative coordinate based on the absolute position of svg const getCoordinates = useCallback( - (pointerEvent: React.PointerEvent): Point => { + (pointerEvent: TouchEvent | MouseEvent): Point => { const boundingArea = canvasRef.current?.getBoundingClientRect(); canvasSizeRef.current = boundingArea ? { @@ -96,10 +103,20 @@ export const Canvas = React.forwardRef((props, ref) => { if (!boundingArea) { return { x: 0, y: 0 }; } + let x = 0; + let y = 0; + + if (pointerEvent instanceof TouchEvent) { + x = pointerEvent.touches[0].pageX; + y = pointerEvent.touches[0].pageY; + } else { + x = pointerEvent.pageX; + y = pointerEvent.pageY; + } return { - x: pointerEvent.pageX - boundingArea.left - scrollLeft, - y: pointerEvent.pageY - boundingArea.top - scrollTop, + x: x - boundingArea.left - scrollLeft, + y: y - boundingArea.top - scrollTop, }; }, [], @@ -108,38 +125,48 @@ export const Canvas = React.forwardRef((props, ref) => { /* Mouse Handlers - Mouse down, move and up */ const handlePointerDown = useCallback( - (event: React.PointerEvent): void => { + (event: TouchEvent | MouseEvent): void => { + if (event instanceof TouchEvent && event.touches.length > 1) { + return; + } + if (readOnly) return; // Allow only chosen pointer type if ( - allowOnlyPointerType !== "all" && - event.pointerType !== allowOnlyPointerType + allowOnlyPointerType !== "all" ) { - return; + if (event instanceof TouchEvent) { + const touch = event.touches[0] as TouchExtends; + if (TOUCH_TYPE_MAP[touch.touchType] !== allowOnlyPointerType) { + return; + } + } } - if (event.pointerType === "mouse" && event.button !== 0) return; + event.preventDefault(); - const isEraser = - // eslint-disable-next-line no-bitwise - event.pointerType === "pen" && (event.buttons & 32) === 32; const point = getCoordinates(event); - onPointerDown(point, isEraser); + onPointerDown(point); }, - [allowOnlyPointerType, getCoordinates, onPointerDown], + [allowOnlyPointerType, getCoordinates, onPointerDown, readOnly], ); const handlePointerMove = useThrottledCallback( - (event: React.PointerEvent): void => { - if (!isDrawing) return; + (event: MouseEvent | TouchEvent): void => { + event.preventDefault(); + if (!isDrawing || readOnly) return; // Allow only chosen pointer type if ( - allowOnlyPointerType !== "all" && - event.pointerType !== allowOnlyPointerType + allowOnlyPointerType !== "all" ) { - return; + if (event instanceof TouchEvent) { + const touch = event.touches[0] as TouchExtends; + if (TOUCH_TYPE_MAP[touch.touchType] !== allowOnlyPointerType) { + return; + } + } } const point = getCoordinates(event); @@ -147,24 +174,27 @@ export const Canvas = React.forwardRef((props, ref) => { onPointerMove(point); }, throttleTime, - [allowOnlyPointerType, getCoordinates, isDrawing, onPointerMove], + [allowOnlyPointerType, getCoordinates, isDrawing, onPointerMove, readOnly], ); const handlePointerUp = useCallback( - (event: React.PointerEvent | PointerEvent): void => { - if (event.pointerType === "mouse" && event.button !== 0) return; - + (event: TouchEvent | MouseEvent): void => { + if (readOnly) return; // Allow only chosen pointer type if ( - allowOnlyPointerType !== "all" && - event.pointerType !== allowOnlyPointerType + allowOnlyPointerType !== "all" ) { - return; + if (event instanceof TouchEvent) { + const touch = event.touches[0] as TouchExtends; + if (TOUCH_TYPE_MAP[touch.touchType] !== allowOnlyPointerType) { + return; + } + } } onPointerUp(); }, - [allowOnlyPointerType, onPointerUp], + [allowOnlyPointerType, onPointerUp, readOnly], ); /* Mouse Handlers ends */ @@ -268,11 +298,31 @@ export const Canvas = React.forwardRef((props, ref) => { /* Add event listener to Mouse up and Touch up to release drawing even when point goes out of canvas */ React.useEffect(() => { - document.addEventListener("pointerup", handlePointerUp); - return () => { - document.removeEventListener("pointerup", handlePointerUp); - }; - }, [handlePointerUp]); + const el = canvasRef.current; + if (el) { + el.addEventListener("touchstart", handlePointerDown); + el.addEventListener("mousedown", handlePointerDown); + + el.addEventListener("touchmove", handlePointerMove); + el.addEventListener("mousemove", handlePointerMove); + + el.addEventListener("touchend", handlePointerUp); + el.addEventListener("mouseup", handlePointerUp); + document.addEventListener("mouseup", handlePointerUp); + return () => { + el.removeEventListener("touchstart", handlePointerDown); + el.removeEventListener("mousedown", handlePointerDown); + + el.removeEventListener("touchmove", handlePointerMove); + el.removeEventListener("mousemove", handlePointerMove); + + el.removeEventListener("touchend", handlePointerUp); + el.removeEventListener("mouseup", handlePointerUp); + document.removeEventListener("mouseup", handlePointerUp); + }; + } + return () => { } + }, [handlePointerDown, handlePointerMove, handlePointerUp]); const eraserPaths = React.useMemo( () => paths.filter((path) => !path.drawMode), @@ -308,13 +358,12 @@ export const Canvas = React.forwardRef((props, ref) => { className={className} style={{ touchAction: "none", + userSelect: "none", + WebkitTouchCallout: "none", width, height, ...style, }} - onPointerDown={readOnly ? undefined : handlePointerDown} - onPointerMove={readOnly ? undefined : handlePointerMove} - onPointerUp={readOnly ? undefined : handlePointerUp} > ) => void, + T extends (event: MouseEvent | TouchEvent) => void, >( callback: T, delay: number, @@ -11,7 +11,7 @@ export function useThrottledCallback< const timeoutRef = useRef | null>(null); const throttledCallback = useCallback( - (event: React.PointerEvent) => { + (event: MouseEvent | TouchEvent) => { const now = Date.now(); if (now - lastCallRef.current >= delay) { lastCallRef.current = now; diff --git a/packages/react-sketch-canvas/src/types/canvas.ts b/packages/react-sketch-canvas/src/types/canvas.ts index 07f5da0..671a4f1 100644 --- a/packages/react-sketch-canvas/src/types/canvas.ts +++ b/packages/react-sketch-canvas/src/types/canvas.ts @@ -69,3 +69,7 @@ export interface CanvasPath { */ readonly endTimestamp?: number; } + +export interface TouchExtends extends Touch { + touchType: "direct" | "stylus"; +}