import { rectContainsPoint } from "@noya-app/noya-geometry"; import { BaseContextProperties, createCanvasPlugin, NoneMode, PointerData, } from "@noya-app/noya-infinite-canvas"; type BaseDOMElementRef = { getBoundingClientRect: () => DOMRect; }; type DOMEventMode = | NoneMode | { type: "domPointerDown"; element: DomElementAtPoint }; type DOMEventData = { domElementRefs: Record>; hoveredDomElementId: string | undefined; } & Pick; interface DOMEventContextProperties extends BaseContextProperties< DOMEventMode, DOMEventData > { onClickDomElement?: ( event: React.MouseEvent, options: { id: string; ref?: React.RefObject; } ) => void; } export function domEventPlugin() { return createCanvasPlugin({ name: "domEvent", data: { domElementRefs: {}, hoveredDomElementId: undefined, }, handlers: (context: DOMEventContextProperties) => ({ onPointerMove(event) { const data = context.getData(); const hoveredDomElement = getDomElementAtPoint(data, event); if (hoveredDomElement?.id !== data.hoveredDomElementId) { context.setData({ ...data, hoveredDomElementId: hoveredDomElement?.id, }); } }, onPointerDown(event) { if (!context.onClickDomElement) return; const data = context.getData(); const clickedDomElement = getDomElementAtPoint(data, event); if (clickedDomElement) { context.setMode({ type: "domPointerDown", element: clickedDomElement, }); event.preventDefault(); event.stopPropagation(); } }, onPointerUp(event) { const mode = context.getMode(); if (mode.type !== "domPointerDown") return; const data = context.getData(); const clickedDomElement = getDomElementAtPoint(data, event); if (clickedDomElement?.id !== mode.element.id) return; context.onClickDomElement?.(event, mode.element); context.setMode({ type: "none" }); }, onPointerLeave() { const mode = context.getMode(); if (mode.type !== "none") return; const data = context.getData(); if (data.hoveredDomElementId) { context.setData((data) => ({ ...data, hoveredDomElementId: undefined, })); } }, }), }); } type DomElementAtPoint = { id: string; ref: React.RefObject; }; function getDomElementAtPoint( data: DOMEventData, event: React.PointerEvent ): DomElementAtPoint | undefined { const pointer = { x: event.clientX, y: event.clientY }; const hoveredDomElement = Object.entries(data.domElementRefs).find( ([_, ref]) => { if (!ref.current) return false; const rect = ref.current.getBoundingClientRect(); return rectContainsPoint(rect, pointer); } ); return hoveredDomElement ? { id: hoveredDomElement[0], ref: hoveredDomElement[1], } : undefined; }