import { ClickEvent } from '../events/click-event.js' import type { EventHandlerProps } from '../events/event-handlers.js' import type { DOMElement } from './dom.js' import { nodeCache } from './node-cache.js' /** * Find the deepest DOM element whose rendered rect 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 } /** * Hit-test the root at (col, row) and bubble a ClickEvent from the deepest * containing node up through parentNode. */ 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 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 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. */ 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 (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?.() } } }