first commit

This commit is contained in:
H
2026-04-03 13:01:19 +08:00
commit 538eced414
2575 changed files with 645911 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
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<DOMElement>,
): void {
const next = new Set<DOMElement>()
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?.()
}
}
}