source dump of claude code
at main 130 lines 4.2 kB view raw
1import type { DOMElement } from './dom.js' 2import { ClickEvent } from './events/click-event.js' 3import type { EventHandlerProps } from './events/event-handlers.js' 4import { nodeCache } from './node-cache.js' 5 6/** 7 * Find the deepest DOM element whose rendered rect contains (col, row). 8 * 9 * Uses the nodeCache populated by renderNodeToOutput — rects are in screen 10 * coordinates with all offsets (including scrollTop translation) already 11 * applied. Children are traversed in reverse so later siblings (painted on 12 * top) win. Nodes not in nodeCache (not rendered this frame, or lacking a 13 * yogaNode) are skipped along with their subtrees. 14 * 15 * Returns the hit node even if it has no onClick — dispatchClick walks up 16 * via parentNode to find handlers. 17 */ 18export function hitTest( 19 node: DOMElement, 20 col: number, 21 row: number, 22): DOMElement | null { 23 const rect = nodeCache.get(node) 24 if (!rect) return null 25 if ( 26 col < rect.x || 27 col >= rect.x + rect.width || 28 row < rect.y || 29 row >= rect.y + rect.height 30 ) { 31 return null 32 } 33 // Later siblings paint on top; reversed traversal returns topmost hit. 34 for (let i = node.childNodes.length - 1; i >= 0; i--) { 35 const child = node.childNodes[i]! 36 if (child.nodeName === '#text') continue 37 const hit = hitTest(child, col, row) 38 if (hit) return hit 39 } 40 return node 41} 42 43/** 44 * Hit-test the root at (col, row) and bubble a ClickEvent from the deepest 45 * containing node up through parentNode. Only nodes with an onClick handler 46 * fire. Stops when a handler calls stopImmediatePropagation(). Returns 47 * true if at least one onClick handler fired. 48 */ 49export function dispatchClick( 50 root: DOMElement, 51 col: number, 52 row: number, 53 cellIsBlank = false, 54): boolean { 55 let target: DOMElement | undefined = hitTest(root, col, row) ?? undefined 56 if (!target) return false 57 58 // Click-to-focus: find the closest focusable ancestor and focus it. 59 // root is always ink-root, which owns the FocusManager. 60 if (root.focusManager) { 61 let focusTarget: DOMElement | undefined = target 62 while (focusTarget) { 63 if (typeof focusTarget.attributes['tabIndex'] === 'number') { 64 root.focusManager.handleClickFocus(focusTarget) 65 break 66 } 67 focusTarget = focusTarget.parentNode 68 } 69 } 70 const event = new ClickEvent(col, row, cellIsBlank) 71 let handled = false 72 while (target) { 73 const handler = target._eventHandlers?.onClick as 74 | ((event: ClickEvent) => void) 75 | undefined 76 if (handler) { 77 handled = true 78 const rect = nodeCache.get(target) 79 if (rect) { 80 event.localCol = col - rect.x 81 event.localRow = row - rect.y 82 } 83 handler(event) 84 if (event.didStopImmediatePropagation()) return true 85 } 86 target = target.parentNode 87 } 88 return handled 89} 90 91/** 92 * Fire onMouseEnter/onMouseLeave as the pointer moves. Like DOM 93 * mouseenter/mouseleave: does NOT bubble — moving between children does 94 * not re-fire on the parent. Walks up from the hit node collecting every 95 * ancestor with a hover handler; diffs against the previous hovered set; 96 * fires leave on the nodes exited, enter on the nodes entered. 97 * 98 * Mutates `hovered` in place so the caller (App instance) can hold it 99 * across calls. Clears the set when the hit is null (cursor moved into a 100 * non-rendered gap or off the root rect). 101 */ 102export function dispatchHover( 103 root: DOMElement, 104 col: number, 105 row: number, 106 hovered: Set<DOMElement>, 107): void { 108 const next = new Set<DOMElement>() 109 let node: DOMElement | undefined = hitTest(root, col, row) ?? undefined 110 while (node) { 111 const h = node._eventHandlers as EventHandlerProps | undefined 112 if (h?.onMouseEnter || h?.onMouseLeave) next.add(node) 113 node = node.parentNode 114 } 115 for (const old of hovered) { 116 if (!next.has(old)) { 117 hovered.delete(old) 118 // Skip handlers on detached nodes (removed between mouse events) 119 if (old.parentNode) { 120 ;(old._eventHandlers as EventHandlerProps | undefined)?.onMouseLeave?.() 121 } 122 } 123 } 124 for (const n of next) { 125 if (!hovered.has(n)) { 126 hovered.add(n) 127 ;(n._eventHandlers as EventHandlerProps | undefined)?.onMouseEnter?.() 128 } 129 } 130}