init claude-code
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user