π File detail
ink/events/dispatcher.ts
π§© .tsπ 234 linesπΎ 6,004 bytesπ text
β Back to All Filesπ― Use case
This file lives under βink/β, which covers Ink terminal UI (layouts, TTY IO, keyboard, renderer components). On the API surface it exposes Dispatcher β mainly types, interfaces, or factory objects. Dependencies touch react-reconciler. It composes internal code from utils, event-handlers, and terminal-event (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { ContinuousEventPriority, DefaultEventPriority, DiscreteEventPriority, NoEventPriority,
π€ Exports (heuristic)
Dispatcher
π External import roots
Package roots from from "β¦" (relative paths omitted).
react-reconciler
π₯οΈ Source preview
import {
ContinuousEventPriority,
DefaultEventPriority,
DiscreteEventPriority,
NoEventPriority,
} from 'react-reconciler/constants.js'
import { logError } from '../../utils/log.js'
import { HANDLER_FOR_EVENT } from './event-handlers.js'
import type { EventTarget, TerminalEvent } from './terminal-event.js'
// --
type DispatchListener = {
node: EventTarget
handler: (event: TerminalEvent) => void
phase: 'capturing' | 'at_target' | 'bubbling'
}
function getHandler(
node: EventTarget,
eventType: string,
capture: boolean,
): ((event: TerminalEvent) => void) | undefined {
const handlers = node._eventHandlers
if (!handlers) return undefined
const mapping = HANDLER_FOR_EVENT[eventType]
if (!mapping) return undefined
const propName = capture ? mapping.capture : mapping.bubble
if (!propName) return undefined
return handlers[propName] as ((event: TerminalEvent) => void) | undefined
}
/**
* Collect all listeners for an event in dispatch order.
*
* Uses react-dom's two-phase accumulation pattern:
* - Walk from target to root
* - Capture handlers are prepended (unshift) β root-first
* - Bubble handlers are appended (push) β target-first
*
* Result: [root-cap, ..., parent-cap, target-cap, target-bub, parent-bub, ..., root-bub]
*/
function collectListeners(
target: EventTarget,
event: TerminalEvent,
): DispatchListener[] {
const listeners: DispatchListener[] = []
let node: EventTarget | undefined = target
while (node) {
const isTarget = node === target
const captureHandler = getHandler(node, event.type, true)
const bubbleHandler = getHandler(node, event.type, false)
if (captureHandler) {
listeners.unshift({
node,
handler: captureHandler,
phase: isTarget ? 'at_target' : 'capturing',
})
}
if (bubbleHandler && (event.bubbles || isTarget)) {
listeners.push({
node,
handler: bubbleHandler,
phase: isTarget ? 'at_target' : 'bubbling',
})
}
node = node.parentNode
}
return listeners
}
/**
* Execute collected listeners with propagation control.
*
* Before each handler, calls event._prepareForTarget(node) so event
* subclasses can do per-node setup.
*/
function processDispatchQueue(
listeners: DispatchListener[],
event: TerminalEvent,
): void {
let previousNode: EventTarget | undefined
for (const { node, handler, phase } of listeners) {
if (event._isImmediatePropagationStopped()) {
break
}
if (event._isPropagationStopped() && node !== previousNode) {
break
}
event._setEventPhase(phase)
event._setCurrentTarget(node)
event._prepareForTarget(node)
try {
handler(event)
} catch (error) {
logError(error)
}
previousNode = node
}
}
// --
/**
* Map terminal event types to React scheduling priorities.
* Mirrors react-dom's getEventPriority() switch.
*/
function getEventPriority(eventType: string): number {
switch (eventType) {
case 'keydown':
case 'keyup':
case 'click':
case 'focus':
case 'blur':
case 'paste':
return DiscreteEventPriority as number
case 'resize':
case 'scroll':
case 'mousemove':
return ContinuousEventPriority as number
default:
return DefaultEventPriority as number
}
}
// --
type DiscreteUpdates = <A, B>(
fn: (a: A, b: B) => boolean,
a: A,
b: B,
c: undefined,
d: undefined,
) => boolean
/**
* Owns event dispatch state and the capture/bubble dispatch loop.
*
* The reconciler host config reads currentEvent and currentUpdatePriority
* to implement resolveUpdatePriority, resolveEventType, and
* resolveEventTimeStamp β mirroring how react-dom's host config reads
* ReactDOMSharedInternals and window.event.
*
* discreteUpdates is injected after construction (by InkReconciler)
* to break the import cycle.
*/
export class Dispatcher {
currentEvent: TerminalEvent | null = null
currentUpdatePriority: number = DefaultEventPriority as number
discreteUpdates: DiscreteUpdates | null = null
/**
* Infer event priority from the currently-dispatching event.
* Called by the reconciler host config's resolveUpdatePriority
* when no explicit priority has been set.
*/
resolveEventPriority(): number {
if (this.currentUpdatePriority !== (NoEventPriority as number)) {
return this.currentUpdatePriority
}
if (this.currentEvent) {
return getEventPriority(this.currentEvent.type)
}
return DefaultEventPriority as number
}
/**
* Dispatch an event through capture and bubble phases.
* Returns true if preventDefault() was NOT called.
*/
dispatch(target: EventTarget, event: TerminalEvent): boolean {
const previousEvent = this.currentEvent
this.currentEvent = event
try {
event._setTarget(target)
const listeners = collectListeners(target, event)
processDispatchQueue(listeners, event)
event._setEventPhase('none')
event._setCurrentTarget(null)
return !event.defaultPrevented
} finally {
this.currentEvent = previousEvent
}
}
/**
* Dispatch with discrete (sync) priority.
* For user-initiated events: keyboard, click, focus, paste.
*/
dispatchDiscrete(target: EventTarget, event: TerminalEvent): boolean {
if (!this.discreteUpdates) {
return this.dispatch(target, event)
}
return this.discreteUpdates(
(t, e) => this.dispatch(t, e),
target,
event,
undefined,
undefined,
)
}
/**
* Dispatch with continuous priority.
* For high-frequency events: resize, scroll, mouse move.
*/
dispatchContinuous(target: EventTarget, event: TerminalEvent): boolean {
const previousPriority = this.currentUpdatePriority
try {
this.currentUpdatePriority = ContinuousEventPriority as number
return this.dispatch(target, event)
} finally {
this.currentUpdatePriority = previousPriority
}
}
}