import type { DOMElement } from './dom.js'; import { ClickEvent } from './events/click-event.js'; import type { EventHandlerProps } from './events/event-handlers.js'; import { nodeCache } from './node-cache.js'; /** Find the deepest DOM element whose painted rectangle contains (col, row). */ export function hitTest( node: DOMElement, col: number, row: number, ): DOMElement | null { const rect = nodeCache.get(node); if (!rect) return null; if ( col < rect.x || col >= rect.x + rect.width || row < rect.y || row >= rect.y + rect.height ) { return null; } for (let i = node.childNodes.length - 1; i >= 0; i--) { const child = node.childNodes[i]!; if (child.nodeName === '#text') continue; const hit = hitTest(child, col, row); if (hit) return hit; } return node; } /** Dispatch a synthetic click at (col, row) against the rendered tree. */ export function dispatchClick( root: DOMElement, col: number, row: number, cellIsBlank = false, ): boolean { let target: DOMElement | undefined = hitTest(root, col, row) ?? undefined; if (!target) return false; // The root is always ink-root, which holds the FocusManager. Walk up // looking for a focusable ancestor and promote it. if (root.focusManager) { let focusTarget: DOMElement | undefined = target; while (focusTarget) { if (typeof focusTarget.attributes['tabIndex'] === 'number') { root.focusManager.handleClickFocus(focusTarget); break; } focusTarget = focusTarget.parentNode; } } const event = new ClickEvent(col, row, cellIsBlank); let handled = false; while (target) { const handler = target._eventHandlers?.onClick as | ((event: ClickEvent) => void) | undefined; if (handler) { handled = true; // Recompute local coordinates for each handler — they're relative // to the element actually handling the event, not the original hit. const rect = nodeCache.get(target); if (rect) { event.localCol = col - rect.x; event.localRow = row - rect.y; } handler(event); if (event.didStopImmediatePropagation()) return true; } target = target.parentNode; } return handled; } /** Fire onMouseEnter / onMouseLeave as the pointer moves over the tree. */ export function dispatchHover( root: DOMElement, col: number, row: number, hovered: Set, ): void { const next = new Set(); let node: DOMElement | undefined = hitTest(root, col, row) ?? undefined; while (node) { const h = node._eventHandlers as EventHandlerProps | undefined; if (h?.onMouseEnter || h?.onMouseLeave) next.add(node); node = node.parentNode; } for (const old of hovered) { if (!next.has(old)) { hovered.delete(old); // If the node was unmounted between pointer events, it has no // parent any more; skip the handler rather than firing into a // detached subtree. if (old.parentNode) { (old._eventHandlers as EventHandlerProps | undefined)?.onMouseLeave?.(); } } } for (const n of next) { if (!hovered.has(n)) { hovered.add(n); (n._eventHandlers as EventHandlerProps | undefined)?.onMouseEnter?.(); } } }