Source: packages/events/src/FederatedEventTarget.ts

import { utils } from '@pixi/core';
import { DisplayObject } from '@pixi/display';
import { EventSystem } from './EventSystem';
import { FederatedEvent } from './FederatedEvent';

import type { AllFederatedEventMap } from './FederatedEventMap';
import type { FederatedPointerEvent } from './FederatedPointerEvent';
import type { FederatedWheelEvent } from './FederatedWheelEvent';

export type Cursor = 'auto'
| 'default'
| 'none'
| 'context-menu'
| 'help'
| 'pointer'
| 'progress'
| 'wait'
| 'cell'
| 'crosshair'
| 'text'
| 'vertical-text'
| 'alias'
| 'copy'
| 'move'
| 'no-drop'
| 'not-allowed'
| 'e-resize'
| 'n-resize'
| 'ne-resize'
| 'nw-resize'
| 's-resize'
| 'se-resize'
| 'sw-resize'
| 'w-resize'
| 'ns-resize'
| 'ew-resize'
| 'nesw-resize'
| 'col-resize'
| 'nwse-resize'
| 'row-resize'
| 'all-scroll'
| 'zoom-in'
| 'zoom-out'
| 'grab'
| 'grabbing';

// @ignore - This is documented elsewhere.
export interface IHitArea
{
    contains(x: number, y: number): boolean;
}

/**
 * Function type for handlers, e.g., onclick
 * @memberof PIXI
 */
export type FederatedEventHandler<T= FederatedPointerEvent> = (event: T) => void;

/**
 * The type of interaction a DisplayObject can be. For more information on values and their meaning,
 * see DisplayObject's eventMode property.
 * @memberof PIXI
 * @since 7.2.0
 */
export type EventMode = 'none' | 'passive' | 'auto' | 'static' | 'dynamic';

/**
 * Describes the shape for a PIXI.FederatedEvent's' `eventTarget`.
 * @memberof PIXI
 */
export interface FederatedEventTarget extends utils.EventEmitter, EventTarget
{
    /** The cursor preferred when the mouse pointer is hovering over. */
    cursor: Cursor | string;

    /** The parent of this event target. */
    readonly parent?: FederatedEventTarget;

    /** The children of this event target. */
    readonly children?: ReadonlyArray<FederatedEventTarget>;

    /** Whether this event target should fire UI events. */
    interactive: boolean
    _internalInteractive: boolean;
    /** The mode of interaction for this object */
    eventMode: EventMode;
    _internalEventMode: EventMode;

    /** Returns true if the DisplayObject has interactive 'static' or 'dynamic' */
    isInteractive: () => boolean;

    /** Whether this event target has any children that need UI events. This can be used optimize event propagation. */
    interactiveChildren: boolean;

    /** The hit-area specifies the area for which pointer events should be captured by this event target. */
    hitArea: IHitArea | null;

    // In Angular projects, zone.js is monkey patching the `EventTarget`
    // by adding its own `removeAllListeners(event?: string): void;` method,
    // so we have to override this signature when extending both `EventTarget` and `utils.EventEmitter`
    // to make it compatible with Angular projects
    // @see https://github.com/pixijs/pixijs/issues/8794

    /** Remove all listeners, or those of the specified event. */
    removeAllListeners(event?: string | symbol): this;

    /** Handler for 'click' event */
    onclick: FederatedEventHandler | null;
    /** Handler for 'mousedown' event */
    onmousedown: FederatedEventHandler | null;
    /** Handler for 'mouseenter' event */
    onmouseenter: FederatedEventHandler | null;
    /** Handler for 'mouseleave' event */
    onmouseleave: FederatedEventHandler | null;
    /** Handler for 'mousemove' event */
    onmousemove: FederatedEventHandler | null;
    /** Handler for 'globalmousemove' event */
    onglobalmousemove: FederatedEventHandler | null;
    /** Handler for 'mouseout' event */
    onmouseout: FederatedEventHandler | null;
    /** Handler for 'mouseover' event */
    onmouseover: FederatedEventHandler | null;
    /** Handler for 'mouseup' event */
    onmouseup: FederatedEventHandler | null;
    /** Handler for 'mouseupoutside' event */
    onmouseupoutside: FederatedEventHandler | null;
    /** Handler for 'pointercancel' event */
    onpointercancel: FederatedEventHandler | null;
    /** Handler for 'pointerdown' event */
    onpointerdown: FederatedEventHandler | null;
    /** Handler for 'pointerenter' event */
    onpointerenter: FederatedEventHandler | null;
    /** Handler for 'pointerleave' event */
    onpointerleave: FederatedEventHandler | null;
    /** Handler for 'pointermove' event */
    onpointermove: FederatedEventHandler | null;
    /** Handler for 'globalpointermove' event */
    onglobalpointermove: FederatedEventHandler | null;
    /** Handler for 'pointerout' event */
    onpointerout: FederatedEventHandler | null;
    /** Handler for 'pointerover' event */
    onpointerover: FederatedEventHandler | null;
    /** Handler for 'pointertap' event */
    onpointertap: FederatedEventHandler | null;
    /** Handler for 'pointerup' event */
    onpointerup: FederatedEventHandler | null;
    /** Handler for 'pointerupoutside' event */
    onpointerupoutside: FederatedEventHandler | null;
    /** Handler for 'rightclick' event */
    onrightclick: FederatedEventHandler | null;
    /** Handler for 'rightdown' event */
    onrightdown: FederatedEventHandler | null;
    /** Handler for 'rightup' event */
    onrightup: FederatedEventHandler | null;
    /** Handler for 'rightupoutside' event */
    onrightupoutside: FederatedEventHandler | null;
    /** Handler for 'tap' event */
    ontap: FederatedEventHandler | null;
    /** Handler for 'touchcancel' event */
    ontouchcancel: FederatedEventHandler | null;
    /** Handler for 'touchend' event */
    ontouchend: FederatedEventHandler | null;
    /** Handler for 'touchendoutside' event */
    ontouchendoutside: FederatedEventHandler | null;
    /** Handler for 'touchmove' event */
    ontouchmove: FederatedEventHandler | null;
    /** Handler for 'globaltouchmove' event */
    onglobaltouchmove: FederatedEventHandler | null;
    /** Handler for 'touchstart' event */
    ontouchstart: FederatedEventHandler | null;
    /** Handler for 'wheel' event */
    onwheel: FederatedEventHandler<FederatedWheelEvent> | null;
}

type AddListenerOptions = boolean | AddEventListenerOptions;
type RemoveListenerOptions = boolean | EventListenerOptions;

export interface IFederatedDisplayObject
    extends Omit<FederatedEventTarget, 'parent' | 'children' | keyof utils.EventEmitter | 'cursor'>
{
    addEventListener<K extends keyof AllFederatedEventMap>(
        type: K,
        listener: (e: AllFederatedEventMap[K]) => any,
        options?: AddListenerOptions
    ): void;
    addEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: AddListenerOptions
    ): void;
    removeEventListener<K extends keyof AllFederatedEventMap>(
        type: K,
        listener: (e: AllFederatedEventMap[K]) => any,
        options?: RemoveListenerOptions
    ): void;
    removeEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: RemoveListenerOptions
    ): void;
}

function convertEventModeToInteractiveMode(mode: EventMode): boolean
{
    return mode === 'dynamic' || mode === 'static';
}

export const FederatedDisplayObject: IFederatedDisplayObject = {

    /**
     * Property-based event handler for the `click` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onclick = (event) => {
     *  //some function here that happens on click
     * }
     */
    onclick: null,

    /**
     * Property-based event handler for the `mousedown` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmousedown = (event) => {
     *  //some function here that happens on mousedown
     * }
     */
    onmousedown: null,

    /**
     * Property-based event handler for the `mouseenter` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmouseenter = (event) => {
     *  //some function here that happens on mouseenter
     * }
     */
    onmouseenter: null,

    /**
     * Property-based event handler for the `mouseleave` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmouseleave = (event) => {
     *  //some function here that happens on mouseleave
     * }
     */
    onmouseleave: null,

    /**
     * Property-based event handler for the `mousemove` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmousemove = (event) => {
     *  //some function here that happens on mousemove
     * }
     */
    onmousemove: null,

    /**
     * Property-based event handler for the `globalmousemove` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onglobalmousemove = (event) => {
     *  //some function here that happens on globalmousemove
     * }
     */
    onglobalmousemove: null,

    /**
     * Property-based event handler for the `mouseout` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmouseout = (event) => {
     *  //some function here that happens on mouseout
     * }
     */
    onmouseout: null,

    /**
     * Property-based event handler for the `mouseover` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmouseover = (event) => {
     *  //some function here that happens on mouseover
     * }
     */
    onmouseover:  null,

    /**
     * Property-based event handler for the `mouseup` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmouseup = (event) => {
     *  //some function here that happens on mouseup
     * }
     */
    onmouseup:  null,

    /**
     * Property-based event handler for the `mouseupoutside` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onmouseupoutside = (event) => {
     *  //some function here that happens on mouseupoutside
     * }
     */
    onmouseupoutside:  null,

    /**
     * Property-based event handler for the `pointercancel` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointercancel = (event) => {
     *  //some function here that happens on pointercancel
     * }
     */
    onpointercancel:  null,

    /**
     * Property-based event handler for the `pointerdown` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerdown = (event) => {
     *  //some function here that happens on pointerdown
     * }
     */
    onpointerdown:  null,

    /**
     * Property-based event handler for the `pointerenter` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerenter = (event) => {
     *  //some function here that happens on pointerenter
     * }
     */
    onpointerenter:  null,

    /**
     * Property-based event handler for the `pointerleave` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerleave = (event) => {
     *  //some function here that happens on pointerleave
     * }
     */
    onpointerleave:  null,

    /**
     * Property-based event handler for the `pointermove` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointermove = (event) => {
     *  //some function here that happens on pointermove
     * }
     */
    onpointermove:  null,

    /**
     * Property-based event handler for the `globalpointermove` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onglobalpointermove = (event) => {
     *  //some function here that happens on globalpointermove
     * }
     */
    onglobalpointermove:  null,

    /**
     * Property-based event handler for the `pointerout` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerout = (event) => {
     *  //some function here that happens on pointerout
     * }
     */
    onpointerout:  null,

    /**
     * Property-based event handler for the `pointerover` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerover = (event) => {
     *  //some function here that happens on pointerover
     * }
     */
    onpointerover:  null,

    /**
     * Property-based event handler for the `pointertap` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointertap = (event) => {
     *  //some function here that happens on pointertap
     * }
     */
    onpointertap:  null,

    /**
     * Property-based event handler for the `pointerup` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerup = (event) => {
     *  //some function here that happens on pointerup
     * }
     */
    onpointerup:  null,

    /**
     * Property-based event handler for the `pointerupoutside` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onpointerupoutside = (event) => {
     *  //some function here that happens on pointerupoutside
     * }
     */
    onpointerupoutside:  null,

    /**
     * Property-based event handler for the `rightclick` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onrightclick = (event) => {
     *  //some function here that happens on rightclick
     * }
     */
    onrightclick:  null,

    /**
     * Property-based event handler for the `rightdown` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onrightdown = (event) => {
     *  //some function here that happens on rightdown
     * }
     */
    onrightdown:  null,

    /**
     * Property-based event handler for the `rightup` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onrightup = (event) => {
     *  //some function here that happens on rightup
     * }
     */
    onrightup:  null,

    /**
     * Property-based event handler for the `rightupoutside` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onrightupoutside = (event) => {
     *  //some function here that happens on rightupoutside
     * }
     */
    onrightupoutside:  null,

    /**
     * Property-based event handler for the `tap` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.ontap = (event) => {
     *  //some function here that happens on tap
     * }
     */
    ontap:  null,

    /**
     * Property-based event handler for the `touchcancel` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.ontouchcancel = (event) => {
     *  //some function here that happens on touchcancel
     * }
     */
    ontouchcancel:  null,

    /**
     * Property-based event handler for the `touchend` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.ontouchend = (event) => {
     *  //some function here that happens on touchend
     * }
     */
    ontouchend:  null,

    /**
     * Property-based event handler for the `touchendoutside` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.ontouchendoutside = (event) => {
     *  //some function here that happens on touchendoutside
     * }
     */
    ontouchendoutside:  null,

    /**
     * Property-based event handler for the `touchmove` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.ontouchmove = (event) => {
     *  //some function here that happens on touchmove
     * }
     */
    ontouchmove:  null,

    /**
     * Property-based event handler for the `globaltouchmove` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onglobaltouchmove = (event) => {
     *  //some function here that happens on globaltouchmove
     * }
     */
    onglobaltouchmove:  null,

    /**
     * Property-based event handler for the `touchstart` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.ontouchstart = (event) => {
     *  //some function here that happens on touchstart
     * }
     */
    ontouchstart:  null,

    /**
     * Property-based event handler for the `wheel` event.
     * @memberof PIXI.DisplayObject#
     * @default null
     * @example
     * this.onwheel = (event) => {
     *  //some function here that happens on wheel
     * }
     */
    onwheel:  null,
    /**
     * @ignore
     */
    _internalInteractive: undefined,
    /**
     * Enable interaction events for the DisplayObject. Touch, pointer and mouse
     * @memberof PIXI.DisplayObject#
     */
    get interactive()
    {
        return this._internalInteractive ?? convertEventModeToInteractiveMode(EventSystem.defaultEventMode);
    },
    set interactive(value: boolean)
    {
        if (process.env.DEBUG)
        {
            utils.deprecation(
                '7.2.0',
                // eslint-disable-next-line max-len
                `Setting interactive is deprecated, use eventMode = 'none'/'passive'/'auto'/'static'/'dynamic' instead.`
            );
        }

        this._internalInteractive = value;
        this.eventMode = value ? 'static' : 'auto';
    },
    /**
     * @ignore
     */
    _internalEventMode: undefined,
    /**
     * Enable interaction events for the DisplayObject. Touch, pointer and mouse.
     * This now replaces the `interactive` property.
     * There are 5 types of interaction settings:
     * - `'none'`: Ignores all interaction events, even on its children.
     * - `'passive'`: Does not emit events and ignores all hit testing on itself and non-interactive children.
     * Interactive children will still emit events.
     * - `'auto'`: Does not emit events but is hit tested if parent is interactive. Same as `interactive = false` in v7
     * - `'static'`: Emit events and is hit tested. Same as `interaction = true` in v7
     * - `'dynamic'`: Emits events and is hit tested but will also receive mock interaction events fired from a ticker to
     * allow for interaction when the mouse isn't moving
     * @example
     * import { Sprite } from 'pixi.js';
     *
     * const sprite = new Sprite(texture);
     * sprite.eventMode = 'static';
     * sprite.on('tap', (event) => {
     *     // Handle event
     * });
     * @memberof PIXI.DisplayObject#
     * @since 7.2.0
     */
    get eventMode()
    {
        return this._internalEventMode ?? EventSystem.defaultEventMode;
    },
    set eventMode(value)
    {
        this._internalInteractive = convertEventModeToInteractiveMode(value);
        this._internalEventMode = value;
    },

    /**
     * Determines if the displayObject is interactive or not
     * @returns {boolean} Whether the displayObject is interactive or not
     * @memberof PIXI.DisplayObject#
     * @since 7.2.0
     * @example
     * import { Sprite } from 'pixi.js';
     * const sprite = new Sprite(texture);
     * sprite.eventMode = 'static';
     * sprite.isInteractive(); // true
     *
     * sprite.eventMode = 'dynamic';
     * sprite.isInteractive(); // true
     *
     * sprite.eventMode = 'none';
     * sprite.isInteractive(); // false
     *
     * sprite.eventMode = 'passive';
     * sprite.isInteractive(); // false
     *
     * sprite.eventMode = 'auto';
     * sprite.isInteractive(); // false
     */
    isInteractive()
    {
        return this.eventMode === 'static' || this.eventMode === 'dynamic';
    },

    /**
     * Determines if the children to the displayObject can be clicked/touched
     * Setting this to false allows PixiJS to bypass a recursive `hitTest` function
     * @memberof PIXI.Container#
     */
    interactiveChildren: true,

    /**
     * Interaction shape. Children will be hit first, then this shape will be checked.
     * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds.
     * @example
     * import { Rectangle, Sprite } from 'pixi.js';
     *
     * const sprite = new Sprite(texture);
     * sprite.interactive = true;
     * sprite.hitArea = new Rectangle(0, 0, 100, 100);
     * @member {PIXI.IHitArea}
     * @memberof PIXI.DisplayObject#
     */
    hitArea: null,

    /**
     * Unlike `on` or `addListener` which are methods from EventEmitter, `addEventListener`
     * seeks to be compatible with the DOM's `addEventListener` with support for options.
     * **IMPORTANT:** _Only_ available if using the `@pixi/events` package.
     * @memberof PIXI.DisplayObject
     * @param type - The type of event to listen to.
     * @param listener - The listener callback or object.
     * @param options - Listener options, used for capture phase.
     * @example
     * // Tell the user whether they did a single, double, triple, or nth click.
     * button.addEventListener('click', {
     *     handleEvent(e): {
     *         let prefix;
     *
     *         switch (e.detail) {
     *             case 1: prefix = 'single'; break;
     *             case 2: prefix = 'double'; break;
     *             case 3: prefix = 'triple'; break;
     *             default: prefix = e.detail + 'th'; break;
     *         }
     *
     *         console.log('That was a ' + prefix + 'click');
     *     }
     * });
     *
     * // But skip the first click!
     * button.parent.addEventListener('click', function blockClickOnce(e) {
     *     e.stopImmediatePropagation();
     *     button.parent.removeEventListener('click', blockClickOnce, true);
     * }, {
     *     capture: true,
     * });
     */
    addEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: AddListenerOptions
    )
    {
        const capture = (typeof options === 'boolean' && options)
            || (typeof options === 'object' && options.capture);
        const signal = typeof options === 'object' ? options.signal : undefined;
        const once = typeof options === 'object' ? (options.once === true) : false;
        const context = typeof listener === 'function' ? undefined : listener;

        type = capture ? `${type}capture` : type;
        const listenerFn = typeof listener === 'function' ? listener : listener.handleEvent;

        const emitter = (this as unknown as utils.EventEmitter);

        if (signal)
        {
            signal.addEventListener('abort', () =>
            {
                emitter.off(type, listenerFn, context);
            });
        }

        if (once)
        {
            emitter.once(type, listenerFn, context);
        }
        else
        {
            emitter.on(type, listenerFn, context);
        }
    },

    /**
     * Unlike `off` or `removeListener` which are methods from EventEmitter, `removeEventListener`
     * seeks to be compatible with the DOM's `removeEventListener` with support for options.
     * **IMPORTANT:** _Only_ available if using the `@pixi/events` package.
     * @memberof PIXI.DisplayObject
     * @param type - The type of event the listener is bound to.
     * @param listener - The listener callback or object.
     * @param options - The original listener options. This is required to deregister a capture phase listener.
     */
    removeEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: RemoveListenerOptions
    )
    {
        const capture = (typeof options === 'boolean' && options)
            || (typeof options === 'object' && options.capture);
        const context = typeof listener === 'function' ? undefined : listener;

        type = capture ? `${type}capture` : type;
        listener = typeof listener === 'function' ? listener : listener.handleEvent;

        (this as unknown as utils.EventEmitter).off(type, listener, context);
    },

    /**
     * Dispatch the event on this PIXI.DisplayObject using the event's PIXI.EventBoundary.
     *
     * The target of the event is set to `this` and the `defaultPrevented` flag is cleared before dispatch.
     *
     * **IMPORTANT:** _Only_ available if using the `@pixi/events` package.
     * @memberof PIXI.DisplayObject
     * @param e - The event to dispatch.
     * @returns Whether the preventDefault() method was not invoked.
     * @example
     * // Reuse a click event!
     * button.dispatchEvent(clickEvent);
     */
    dispatchEvent(e: Event): boolean
    {
        if (!(e instanceof FederatedEvent))
        {
            throw new Error('DisplayObject cannot propagate events outside of the Federated Events API');
        }

        e.defaultPrevented = false;
        e.path = null;
        e.target = this as unknown as FederatedEventTarget;
        e.manager.dispatchEvent(e);

        return !e.defaultPrevented;
    }
};

DisplayObject.mixin(FederatedDisplayObject);