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' /** * 找到其渲染矩形包含 (col, row) 的最深的 DOM 元素。 * * 使用 renderNodeToOutput 填充的 nodeCache — 矩形在屏幕 * 坐标中,所有偏移(包括 scrollTop 平移)已应用。 * 子元素以逆序遍历,以便后期兄弟元素(画在上面)获胜。 * 不在 nodeCache 中的节点(此帧未渲染,或缺少 yogaNode) * 及其子树被跳过。 * * 即使命中节点没有 onClick 也返回命中节点 — dispatchClick 通过 * parentNode 向上查找处理器。 */ 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 } /** * 在 (col, row) 处对根进行命中测试,并将 ClickEvent 从最深的 * 包含节点向上冒泡到 parentNode。只有具有 onClick 处理器的节点触发。 * 当处理器调用 stopImmediatePropagation() 时停止。如果至少一个 * onClick 处理器触发则返回 true。 */ 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 // 点击转焦点:找到最近的 focusable 祖先并聚焦它。 // root 始终是 ink-root,它拥有 FocusManager。 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 } /** * 当指针移动时触发 onMouseEnter/onMouseLeave。类似于 DOM * mouseenter/mouseleave:不冒泡 — 在子元素之间移动不会 * 在父元素上重新触发。从命中节点向上遍历,收集每个 * 有悬停处理器的祖先;与先前悬停集合对比; * 对退出的节点触发 leave,对进入的节点触发 enter。 * * 就地改变 `hovered`,以便调用者(App 实例)可以跨调用保留它。 * 当命中为空时清除集合(光标移动到未渲染的间隙或根矩形之外)。 */ 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?.() } } }