diff --git a/src/events/eventSystem.ts b/src/events/eventSystem.ts index 77bc4f1..93db7d8 100644 --- a/src/events/eventSystem.ts +++ b/src/events/eventSystem.ts @@ -2,80 +2,16 @@ /* Event System */ /* ************ */ -import type { VDOMInstance } from "../core/types"; -import { PORTAL } from "../core/types"; - -/** - * Mapping of MiniReact event names to their corresponding native DOM event names. - * This ensures consistent event naming across different browsers. - */ -const MINI_REACT_EVENT_TO_NATIVE_EVENT = { - /** Mouse click event */ - onClick: "click", - /** Mouse button pressed down */ - onMouseDown: "mousedown", - /** Mouse button released */ - onMouseUp: "mouseup", - /** Mouse enters element */ - onMouseOver: "mouseover", - /** Mouse leaves element */ - onMouseOut: "mouseout", - /** Mouse enters element (no bubbling) */ - onMouseEnter: "mouseenter", - /** Mouse leaves element (no bubbling) */ - onMouseLeave: "mouseleave", - /** Double click event */ - onDoubleClick: "dblclick", - /** Context menu event (right-click) */ - onContextMenu: "contextmenu", - /** Form input value changed */ - onChange: "change", - /** Form input being typed */ - onInput: "input", - /** Form submission */ - onSubmit: "submit", - /** Element gains focus */ - onFocus: "focus", - /** Element loses focus */ - onBlur: "blur", - /** Key pressed down */ - onKeyDown: "keydown", - /** Key released */ - onKeyUp: "keyup", - /** Key press (deprecated but supported) */ - onKeyPress: "keypress", - /** Resource finished loading */ - onLoad: "load", - /** Error occurred during loading */ - onError: "error", - /** Mouse wheel scrolled */ - onWheel: "wheel", - /** Element scrolled */ - onScroll: "scroll", - /** Window/element resized */ - onResize: "resize", - /** Touch contact started */ - onTouchStart: "touchstart", - /** Touch contact moved */ - onTouchMove: "touchmove", - /** Touch contact ended */ - onTouchEnd: "touchend", - /** Touch contact cancelled */ - onTouchCancel: "touchcancel", -} as const; - -/** - * Union type of all supported MiniReact event names. - * These are the prop names used in JSX (e.g., onClick, onSubmit). - */ -type MiniReactEventName = keyof typeof MINI_REACT_EVENT_TO_NATIVE_EVENT; - -/** - * Union type of all corresponding native DOM event names. - * These are the actual event names used with addEventListener. - */ -type NativeEventName = - (typeof MINI_REACT_EVENT_TO_NATIVE_EVENT)[MiniReactEventName]; +import type { Fiber } from "../fiber/types"; +import { WorkTag } from "../fiber/types"; +import { + type EventHandler, + type InternalSyntheticEvent, + MINI_REACT_EVENT_TO_NATIVE_EVENT, + type MiniReactEventName, + type NativeEventName, + type SyntheticEvent, +} from "./types"; /** * Events that need to be captured in the capture phase for proper handling @@ -97,83 +33,12 @@ const PASSIVE_EVENTS = new Set([ "touchmove", ]); -/** - * Synthetic event interface that wraps native DOM events with additional - * MiniReact-compatible functionality and normalized behavior across browsers. - * - * @template T - The type of the target element (defaults to Element) - */ -interface SyntheticEvent { - /** The original native DOM event */ - nativeEvent: Event; - /** The element that triggered the event */ - target: T; - /** The element that the event handler is attached to */ - currentTarget: T; - /** The type of event (e.g., "click", "keydown") */ - type: string; - /** Whether the event bubbles up the DOM tree */ - bubbles: boolean; - /** Whether the event's default action can be prevented */ - cancelable: boolean; - /** Whether preventDefault() has been called */ - defaultPrevented: boolean; - /** The event phase (1=capture, 2=target, 3=bubble) */ - eventPhase: number; - /** Whether the event was generated by user action */ - isTrusted: boolean; - /** Timestamp when the event was created */ - timeStamp: number; - - /** - * Prevents the default action associated with the event. - * For example, prevents form submission or link navigation. - */ - preventDefault(): void; - - /** - * Stops the event from propagating to parent elements. - * Prevents both capture and bubble propagation. - */ - stopPropagation(): void; - - /** - * Stops the event from propagating to parent elements and - * prevents other handlers on the same element from executing. - */ - stopImmediatePropagation(): void; -} - -/** - * Type for event handler functions that receive synthetic events - */ -type EventHandler = (event: SyntheticEvent) => void; - -/** - * Internal interface that extends SyntheticEvent with private propagation state properties. - * These properties are used internally by the event system to track propagation state. - * @private - */ -interface InternalSyntheticEvent extends SyntheticEvent { - /** Internal flag indicating if stopPropagation() was called */ - readonly _propagationStopped: boolean; - /** Internal flag indicating if stopImmediatePropagation() was called */ - readonly _immediatePropagationStopped: boolean; -} - /** * Creates a synthetic event wrapper around a native DOM event. * This provides MiniReact-compatible event handling with normalized behavior. * * @param nativeEvent - The native DOM event to wrap * @returns A synthetic event with MiniReact-compatible interface - * - * @example - * ```typescript - * const syntheticEvent = createSyntheticEvent(domEvent); - * syntheticEvent.preventDefault(); // Prevents default behavior - * syntheticEvent.stopPropagation(); // Stops event bubbling - * ``` */ function createSyntheticEvent(nativeEvent: Event): SyntheticEvent { let defaultPrevented = false; @@ -229,7 +94,7 @@ function createSyntheticEvent(nativeEvent: Event): SyntheticEvent { * * This system uses a single event listener on the root container to handle * all events, then routes them to the appropriate handlers based on the - * event target and VDOM tree structure. + * event target and fiber tree structure. * * Features: * - Event delegation for performance @@ -245,11 +110,11 @@ class EventSystem { /** Set of native event types that have been registered for delegation */ private registeredEvents = new Set(); - /** Maps VDOM instances to their corresponding DOM nodes */ - private instanceToNode = new WeakMap(); + /** Maps Fiber nodes to their corresponding DOM nodes */ + private fiberToNode = new WeakMap(); - /** Maps DOM nodes to their corresponding VDOM instances */ - private nodeToInstance = new WeakMap(); + /** Maps DOM nodes to their corresponding Fiber nodes */ + private nodeToFiber = new WeakMap(); /** Cached bound handler function to ensure same reference for add/remove event listeners */ private boundHandleDelegatedEvent: (event: Event) => void; @@ -267,11 +132,6 @@ class EventSystem { * If a different container was previously used, cleans up the old one first. * * @param container - The root DOM element to attach event listeners to - * - * @example - * ```typescript - * eventSystem.initialize(document.getElementById('root')); - * ``` */ initialize(container: Element): void { // If we already have a root container, clean up first @@ -281,6 +141,14 @@ class EventSystem { this.rootContainer = container; } + /** + * Enables fiber-based event handling mode. + * Kept for API compatibility with createRoot. + */ + enableFiberMode(): void { + // No-op: fiber mode is now the only mode + } + /** * Adds event delegation to a container (for portals) * This allows portal containers to delegate events to the main event system @@ -303,49 +171,36 @@ class EventSystem { } /** - * Registers a VDOM instance with its corresponding DOM node. - * This creates the mapping needed for event delegation to work properly. - * - * @param instance - The VDOM instance to register - * @param domNode - The DOM node associated with the instance + * Registers a Fiber with its corresponding DOM node. + * This creates the mapping needed for fiber-based event delegation. * - * @example - * ```typescript - * eventSystem.registerInstance(vdomInstance, buttonElement); - * ``` + * @param fiber - The Fiber to register + * @param domNode - The DOM node associated with the fiber */ - registerInstance(instance: VDOMInstance, domNode: Node): void { - this.instanceToNode.set(instance, domNode); + registerFiber(fiber: Fiber, domNode: Node): void { + this.fiberToNode.set(fiber, domNode); if (domNode) { - this.nodeToInstance.set(domNode, instance); + this.nodeToFiber.set(domNode, fiber); } } /** - * Unregisters a VDOM instance and removes its DOM node mapping. - * Should be called when elements are removed from the DOM to prevent memory leaks. + * Unregisters a Fiber and removes its DOM node mapping. + * Should be called when fibers are deleted to prevent memory leaks. * - * @param instance - The VDOM instance to unregister - * - * @example - * ```typescript - * eventSystem.unregisterInstance(vdomInstance); - * ``` + * @param fiber - The Fiber to unregister */ - unregisterInstance(instance: VDOMInstance): void { - const domNode = this.instanceToNode.get(instance); + unregisterFiber(fiber: Fiber): void { + const domNode = this.fiberToNode.get(fiber); if (domNode) { - this.nodeToInstance.delete(domNode); + this.nodeToFiber.delete(domNode); } - this.instanceToNode.delete(instance); + this.fiberToNode.delete(fiber); } /** * Ensures that an event listener is attached for the specified native event type. * Uses event delegation by attaching a single listener to the root container. - * - * @param nativeEventName - The native DOM event name to listen for - * @private */ private ensureEventListener(nativeEventName: NativeEventName): void { if (!this.registeredEvents.has(nativeEventName)) { @@ -375,103 +230,78 @@ class EventSystem { /** * Determines the appropriate event listener options for a given event type. - * Some events need special handling (capture phase, passive listeners). - * - * @param eventName - The native event name to get options for - * @returns Event listener options or false for default behavior - * @private */ private getEventOptions( eventName: NativeEventName, ): boolean | AddEventListenerOptions { - // Some events need to be captured in capture phase if (CAPTURE_EVENTS.has(eventName)) { return { capture: true }; } - - // Passive events for better performance if (PASSIVE_EVENTS.has(eventName)) { return { passive: true }; } - return false; } /** * Main event delegation handler that processes all delegated events. * Creates synthetic events and routes them through the proper capture/bubble phases. - * - * @param nativeEvent - The native DOM event that was triggered - * @private */ private handleDelegatedEvent(nativeEvent: Event): void { const target = nativeEvent.target as Node; if (!target) return; - // Create synthetic event const syntheticEvent = createSyntheticEvent(nativeEvent); - // Find the path from target to root container - const eventPath = this.getEventPath(target); - - // Convert native event name to MiniReact event name const reactEventName = this.getReactEventName( nativeEvent.type as NativeEventName, ); if (!reactEventName) return; - // Collect all event handlers along the path - const eventHandlers = this.collectEventHandlers(eventPath, reactEventName); + const eventPath = this.getEventPathFiber(target); + + const eventHandlers = this.collectEventHandlersFiber( + eventPath, + reactEventName, + ); - // Execute event handlers (capture then bubble) - this.executeEventHandlers(eventHandlers, syntheticEvent); + this.executeEventHandlersFiber(eventHandlers, syntheticEvent); } /** - * Builds the event path for a given target node, respecting React tree structure. - * For regular elements, follows DOM hierarchy. - * For portal children, follows React component hierarchy instead. - * - * @param target - The DOM node where the event originated - * @returns Array of VDOM instances in the event path (capture order) - * @private + * Builds the event path for a given target node using fiber tree structure. + * For portal children, follows React component hierarchy instead of DOM. */ - private getEventPath(target: Node): VDOMInstance[] { - const path: VDOMInstance[] = []; + private getEventPathFiber(target: Node): Fiber[] { + const path: Fiber[] = []; let currentNode: Node | null = target; - // Walk up the DOM tree and collect VDOM instances - // For portals, the target might not be contained in the main root container while (currentNode) { - const instance = this.nodeToInstance.get(currentNode); - if (instance) { - path.unshift(instance); // Add to beginning for capture order + const fiber = this.nodeToFiber.get(currentNode); + if (fiber) { + path.unshift(fiber); - // Check if this instance is a child of a portal - // If so, we need to continue up the React tree, not DOM tree - const portalParent = this.findPortalParent(instance); + const portalParent = this.findPortalParentFiber(fiber); if (portalParent) { - // Add portal parent to path and continue up React tree path.unshift(portalParent); - // Now continue up the React parent chain instead of DOM - let reactParent = portalParent.parent; + let reactParent = portalParent.return; while (reactParent) { - path.unshift(reactParent); - reactParent = reactParent.parent; + if ( + reactParent.tag === WorkTag.HostComponent || + reactParent.tag === WorkTag.FunctionComponent + ) { + path.unshift(reactParent); + } + reactParent = reactParent.return; } - break; // Exit DOM traversal since we're now in React tree + break; } } - // Continue up the DOM tree currentNode = currentNode.parentNode; - // If we've left the main container and don't have an instance, - // but we haven't found a portal parent yet, continue searching - // This handles the case where portal children are in different containers - if (!this.rootContainer?.contains(currentNode) && !instance) { - // If we're outside the main container and have no instance, stop + if (!this.rootContainer?.contains(currentNode) && !fiber) { break; } } @@ -480,90 +310,61 @@ class EventSystem { } /** - * Finds if a given instance is a child of a portal by walking up the MiniReact tree. - * Returns the portal instance if found, null otherwise. - * - * @param instance - The VDOM instance to check - * @returns The portal instance if this is a portal child, null otherwise - * @private + * Finds if a given fiber is a child of a portal by walking up the fiber tree. */ - private findPortalParent(instance: VDOMInstance): VDOMInstance | null { - let current = instance.parent; + private findPortalParentFiber(fiber: Fiber): Fiber | null { + let current = fiber.return; while (current) { - if (current.element.type === PORTAL) { + if (current.tag === WorkTag.HostPortal) { return current; } - current = current.parent; + current = current.return; } return null; } /** - * Converts a native DOM event name to its corresponding MiniReact event name. - * - * @param nativeEventName - The native DOM event name (e.g., "click") - * @returns The corresponding MiniReact event name (e.g., "onClick") or null if not supported - * @private + * Collects all event handlers for a given MiniReact event name along the fiber path. */ - private getReactEventName( - nativeEventName: NativeEventName, - ): MiniReactEventName | null { - for (const [reactName, nativeName] of Object.entries( - MINI_REACT_EVENT_TO_NATIVE_EVENT, - )) { - if (nativeName === nativeEventName) { - return reactName as MiniReactEventName; - } - } - return null; - } - - /** - * Collects all event handlers for a given MiniReact event name along the event path. - * Handles both capture handlers (e.g., onClickCapture) and bubble handlers (e.g., onClick). - * - * @param eventPath - Array of VDOM instances in the event path - * @param reactEventName - The MiniReact event name to collect handlers for - * @returns Array of handler objects with instance, function, and capture flag - * @private - */ - private collectEventHandlers( - eventPath: VDOMInstance[], + private collectEventHandlersFiber( + eventPath: Fiber[], reactEventName: MiniReactEventName, ): Array<{ - instance: VDOMInstance; + fiber: Fiber; handler: EventHandler; capture?: boolean; }> { const handlers: Array<{ - instance: VDOMInstance; + fiber: Fiber; handler: EventHandler; capture?: boolean; }> = []; - for (const instance of eventPath) { - const props = instance.element.props as Record; + for (const fiber of eventPath) { + const props = (fiber.pendingProps ?? fiber.memoizedProps) as Record< + string, + unknown + > | null; + if (!props) continue; - // Check for capture handler (e.g., onClickCapture) const captureEventName = `${reactEventName}Capture`; if ( props[captureEventName] && typeof props[captureEventName] === "function" ) { handlers.push({ - instance, + fiber, handler: props[captureEventName] as EventHandler, capture: true, }); } - // Check for bubble handler (e.g., onClick) if ( props[reactEventName] && typeof props[reactEventName] === "function" ) { handlers.push({ - instance, + fiber, handler: props[reactEventName] as EventHandler, capture: false, }); @@ -574,16 +375,11 @@ class EventSystem { } /** - * Executes collected event handlers in the proper order (capture then bubble). - * Respects event propagation stopping and handles currentTarget updates. - * - * @param eventHandlers - Array of handler objects to execute - * @param syntheticEvent - The synthetic event to pass to handlers - * @private + * Executes collected fiber event handlers in the proper order (capture then bubble). */ - private executeEventHandlers( + private executeEventHandlersFiber( eventHandlers: Array<{ - instance: VDOMInstance; + fiber: Fiber; handler: EventHandler; capture?: boolean; }>, @@ -592,14 +388,13 @@ class EventSystem { const internalEvent = syntheticEvent as InternalSyntheticEvent; // Execute capture handlers first (in capture order) - for (const { instance, handler, capture } of eventHandlers) { + for (const { fiber, handler, capture } of eventHandlers) { if (capture) { - const domNode = this.instanceToNode.get(instance); + const domNode = this.fiberToNode.get(fiber); if (domNode) { syntheticEvent.currentTarget = domNode as Element; handler(syntheticEvent); - // Check if immediate propagation was stopped if (internalEvent._immediatePropagationStopped) { return; } @@ -607,20 +402,18 @@ class EventSystem { } } - // Check if propagation was stopped during capture phase if (internalEvent._propagationStopped) { return; } // Execute bubble handlers in reverse order (bubble up from target) const bubbleHandlers = eventHandlers.filter((h) => !h.capture).reverse(); - for (const { instance, handler } of bubbleHandlers) { - const domNode = this.instanceToNode.get(instance); + for (const { fiber, handler } of bubbleHandlers) { + const domNode = this.fiberToNode.get(fiber); if (domNode) { syntheticEvent.currentTarget = domNode as Element; handler(syntheticEvent); - // Check if propagation was stopped if (internalEvent._immediatePropagationStopped) { return; } @@ -631,18 +424,25 @@ class EventSystem { } } + /** + * Converts a native DOM event name to its corresponding MiniReact event name. + */ + private getReactEventName( + nativeEventName: NativeEventName, + ): MiniReactEventName | null { + for (const [reactName, nativeName] of Object.entries( + MINI_REACT_EVENT_TO_NATIVE_EVENT, + )) { + if (nativeName === nativeEventName) { + return reactName as MiniReactEventName; + } + } + return null; + } + /** * Checks if an element's props contain event handlers that need delegation. * Automatically registers event listeners for any found event handlers. - * - * @param props - The props object to check for event handlers - * @returns True if any event handlers were found, false otherwise - * - * @example - * ```typescript - * const hasEvents = eventSystem.hasEventHandlers({ onClick: () => {}, onKeyDown: () => {} }); - * // Returns true and registers listeners for 'click' and 'keydown' - * ``` */ hasEventHandlers(props: Record): boolean { let hasHandlers = false; @@ -662,16 +462,9 @@ class EventSystem { /** * Cleans up the event system by removing all event listeners and clearing mappings. - * Should be called when the event system is no longer needed to prevent memory leaks. - * - * @example - * ```typescript - * eventSystem.cleanup(); // Removes all listeners and clears state - * ``` */ cleanup(): void { if (this.rootContainer) { - // Remove all delegated event listeners for (const eventName of this.registeredEvents) { this.rootContainer.removeEventListener( eventName, @@ -680,28 +473,13 @@ class EventSystem { } } this.registeredEvents.clear(); - this.instanceToNode = new WeakMap(); - this.nodeToInstance = new WeakMap(); + this.fiberToNode = new WeakMap(); + this.nodeToFiber = new WeakMap(); this.rootContainer = null; } } /** * Global singleton instance of the event system. - * This is the main interface used throughout the MiniReact application. - * - * @example - * ```typescript - * import { eventSystem } from './eventSystem'; - * - * // Initialize with root container - * eventSystem.initialize(document.getElementById('root')); - * - * // Register a VDOM instance - * eventSystem.registerInstance(instance, domNode); - * ``` */ export const eventSystem = new EventSystem(); - -export type { SyntheticEvent, MiniReactEventName }; -export { MINI_REACT_EVENT_TO_NATIVE_EVENT };