import type React from "react"; import { createCanvas, ReactCanvas } from "../plugins/ReactCanvas"; import type { DOMContextProperties } from "../plugins/contextProperties"; import { pointerPlugin } from "../plugins/pointerPlugin"; import { convertPoint as convertCoordinates } from "../utils/pointUtils"; type CanvasMode> = C extends ReactCanvas ? Mode : never; type CanvasData> = C extends ReactCanvas ? Data : never; type CanvasContextProperties> = C extends ReactCanvas ? ContextProperties : never; type CanvasEventHandlerMap> = C extends ReactCanvas ? EventHandlerMap : never; export type CanvasSetupOptions> = { contextProperties?: Partial>; data?: Partial>; mode?: CanvasMode; }; export type CanvasHarness> = { handlers: Partial>; getData: () => CanvasData; setData: React.Dispatch>>; getMode: () => CanvasMode; setMode: React.Dispatch>>; getReadOnly: () => boolean; getShouldHandleKeyboardEvents: () => boolean; contextProperties: CanvasContextProperties; }; function matchesKey(event: React.KeyboardEvent, key: string) { if (key.startsWith("Mod-")) { const [, withoutMod] = key.split("Mod-"); return (event.metaKey || event.ctrlKey) && event.key === withoutMod; } return event.key === key; } function createHandleKeyboardEvent(): DOMContextProperties["handleKeyboardEvent"] { return (keyMap) => (event) => { Object.entries(keyMap).forEach(([key, command]) => { if (!matchesKey(event, key)) return; const handler = typeof command === "function" ? command : command.command; if (!handler) return; const result = handler(); if (result !== "fallthrough") { event.preventDefault?.(); } }); }; } function createEvent( props: T, onPreventDefault?: () => void ): T & { preventDefault: () => void; defaultPrevented: boolean } { const event = { defaultPrevented: false, ...props, } as T & { preventDefault: () => void; defaultPrevented: boolean }; (event as any).preventDefault = () => { (event as any).defaultPrevented = true; onPreventDefault?.(); }; return event; } export function createTestCanvas>( buildCanvas: (canvas: ReturnType) => C, options: CanvasSetupOptions = {} ): CanvasHarness { const canvas = buildCanvas(createCanvas()); let mode: CanvasMode = (options.mode ?? canvas.initialMode) as CanvasMode; let data: CanvasData = { ...(canvas.initialData as CanvasData), ...(options.data ?? {}), } as CanvasData; const getMode = () => mode; const setMode: React.Dispatch>> = ( value ) => { mode = value instanceof Function ? value(mode) : value; }; const getData = () => data; const setData: React.Dispatch>> = ( value ) => { data = value instanceof Function ? value(data) : value; }; let readOnly = false; let shouldHandleKeyboardEvents = true; const extraContext = (options.contextProperties ?? {}) as Partial< CanvasContextProperties >; const domOverrides = extraContext as Partial; const getScreenPoint: DOMContextProperties["getScreenPoint"] = domOverrides.getScreenPoint ?? ((input) => ({ x: input.clientX ?? 0, y: input.clientY ?? 0 })); const domContext: DOMContextProperties = { canvasRef: { current: null }, handleKeyboardEvent: domOverrides.handleKeyboardEvent ?? createHandleKeyboardEvent(), setReadOnly: (value) => { readOnly = value; domOverrides.setReadOnly?.(value); }, setShouldHandleKeyboardEvents: (value) => { shouldHandleKeyboardEvents = value; domOverrides.setShouldHandleKeyboardEvents?.(value); }, getScreenPoint, getCanvasPoint: domOverrides.getCanvasPoint ?? ((input) => { const current = getData(); const point = getScreenPoint(input); return convertCoordinates( current.origin, current.zoom, point, "canvas" ); }), convertPoint: domOverrides.convertPoint ?? ((point, targetCoordinateSystem) => { const current = getData(); return convertCoordinates( current.origin, current.zoom, point, targetCoordinateSystem ); }), setPointerCapture: domOverrides.setPointerCapture ?? (() => undefined), releasePointerCapture: domOverrides.releasePointerCapture ?? (() => undefined), }; const contextProperties = { ...domContext, ...extraContext, getMode, setMode, getData, setData, } as CanvasContextProperties; const handlers = canvas.pluginSystem .clone() .addPlugin(pointerPlugin()) .getHandlers(contextProperties as any); return { handlers: handlers as Partial>, getData, setData, getMode, setMode, getReadOnly: () => readOnly, getShouldHandleKeyboardEvents: () => shouldHandleKeyboardEvents, contextProperties, }; } export function createPointerEvent({ x = 0, y = 0, button = 0, pointerId = 1, altKey = false, metaKey = false, ctrlKey = false, shiftKey = false, target = null, }: Partial<{ x: number; y: number; button: number; pointerId: number; altKey: boolean; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; target: EventTarget | null; }> = {}) { const nativeEvent = createEvent( { clientX: x, clientY: y, button, pointerId, altKey, metaKey, ctrlKey, shiftKey, target, } as PointerEvent ); return createEvent( { nativeEvent, clientX: x, clientY: y, button, pointerId, altKey, metaKey, ctrlKey, shiftKey, } as React.PointerEvent, () => { nativeEvent.preventDefault?.(); } ); } export function createWheelEvent({ deltaX = 0, deltaY = 0, deltaMode = 0, metaKey = false, ctrlKey = false, }: Partial<{ deltaX: number; deltaY: number; deltaMode: number; metaKey: boolean; ctrlKey: boolean; }> = {}) { const nativeEvent = createEvent( { deltaX, deltaY, deltaMode, metaKey, ctrlKey } as WheelEvent ); return createEvent( { nativeEvent, deltaX, deltaY, deltaMode, metaKey, ctrlKey } as React.WheelEvent, () => { nativeEvent.preventDefault?.(); } ); } export function createKeyboardEvent( key: string, options: Partial<{ metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; }> = {} ) { const event = createEvent( { key, metaKey: options.metaKey ?? false, ctrlKey: options.ctrlKey ?? false, shiftKey: options.shiftKey ?? false, } as React.KeyboardEvent ); (event as any).nativeEvent = event; return event; } export function createBeforeInputEvent({ inputType, data, }: { inputType: string; data?: string; }) { return createEvent({ inputType, data } as InputEvent); }