@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
921 lines • 34.5 kB
JavaScript
import { Object3D, Vector3 } from "three";
import { isDevEnvironment } from "../engine/debug/index.js";
import { addComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../engine/engine_components.js";
import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
import { destroy, findByGuid, foreachComponent, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
import { isHotReloadEnabled, registerHotReloadType, unregisterHotReloadType } from "../engine/engine_hot_reload.js";
import * as main from "../engine/engine_mainloop_utils.js";
import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
import { Context, FrameEvent } from "../engine/engine_setup.js";
import * as threeutils from "../engine/engine_three_utils.js";
// export interface ISerializationCallbackReceiver {
// onBeforeSerialize?(): object | void;
// onAfterSerialize?();
// onBeforeDeserialize?(data?: any);
// onAfterDeserialize?();
// onDeserialize?(key: string, value: any): any | void;
// }
/**
* Base class for objects in Needle Engine. Extends {@link Object3D} from three.js.
* GameObjects can have components attached to them, which can be used to add functionality to the object.
* They manage their components and provide methods to add, remove and get components.
*
* All {@link Object3D} types loaded in Needle Engine have methods like {@link addComponent}.
* These methods are available directly on the GameObject instance:
* ```typescript
* target.addComponent(MyComponent);
* ```
*
* And can be called statically on the GameObject class as well:
* ```typescript
* GameObject.setActive(target, true);
* ```
*/
export class GameObject extends Object3D {
/**
* Unique identifier for this GameObject
*/
guid;
/**
* Checks if a GameObject has been destroyed
* @param go The GameObject to check
* @returns True if the GameObject has been destroyed
*/
static isDestroyed(go) {
return isDestroyed(go);
}
/**
* Sets the active state of a GameObject
* @param go The GameObject to modify
* @param active Whether the GameObject should be active
* @param processStart Whether to process the start callbacks if being activated
*/
static setActive(go, active, processStart = true) {
if (!go)
return;
setActive(go, active);
// TODO: do we still need this?:
main.updateIsActive(go);
if (active && processStart)
main.processStart(Context.Current, go);
}
/**
* Checks if the GameObject itself is active (same as go.visible)
* @param go The GameObject to check
* @returns True if the GameObject is active
*/
static isActiveSelf(go) {
return isActiveSelf(go);
}
/**
* Checks if the GameObject is active in the hierarchy (e.g. if any parent is invisible or not in the scene it will be false)
* @param go The GameObject to check
* @returns True if the GameObject is active in the hierarchy
*/
static isActiveInHierarchy(go) {
return isActiveInHierarchy(go);
}
/**
* Marks a GameObject to be rendered using instancing
* @param go The GameObject to mark
* @param instanced Whether the GameObject should use instanced rendering
*/
static markAsInstancedRendered(go, instanced) {
markAsInstancedRendered(go, instanced);
}
/**
* Checks if a GameObject is using instanced rendering
* @param instance The GameObject to check
* @returns True if the GameObject is using instanced rendering
*/
static isUsingInstancing(instance) { return isUsingInstancing(instance); }
/**
* Executes a callback for all components of the provided type on the provided object and its children
* @param instance Object to run the method on
* @param cb Callback to run on each component, "return undefined;" to continue and "return <anything>;" to break the loop
* @param recursive If true, the method will be run on all children as well
* @returns The last return value of the callback
*/
static foreachComponent(instance, cb, recursive = true) {
return foreachComponent(instance, cb, recursive);
}
/**
* Creates a new instance of the provided object that will be replicated to all connected clients
* @param instance Object to instantiate
* @param opts Options for the instantiation
* @returns The newly created instance or null if creation failed
*/
static instantiateSynced(instance, opts) {
if (!instance)
return null;
return syncInstantiate(instance, opts);
}
static instantiate(instance, opts = null) {
if ('isAssetReference' in instance) {
return instantiate(instance, opts);
}
return instantiate(instance, opts);
}
/**
* Destroys an object on all connected clients (if in a networked session)
* @param instance Object to destroy
* @param context Optional context to use
* @param recursive If true, all children will be destroyed as well
*/
static destroySynced(instance, context, recursive = true) {
if (!instance)
return;
const go = instance;
context = context ?? Context.Current;
syncDestroy(go, context.connection, recursive);
}
/**
* Destroys an object
* @param instance Object to destroy
* @param recursive If true, all children will be destroyed as well. Default: true
*/
static destroy(instance, recursive = true) {
return destroy(instance, recursive);
}
/**
* Adds an object to parent and ensures all components are properly registered
* @param instance Object to add
* @param parent Parent to add the object to
* @param context Optional context to use
*/
static add(instance, parent, context) {
if (!instance || !parent)
return;
if (instance === parent) {
console.warn("Can not add object to self", instance);
return;
}
if (!context) {
context = Context.Current;
}
parent.add(instance);
setActive(instance, true);
main.updateIsActive(instance);
if (context) {
GameObject.foreachComponent(instance, (comp) => {
main.addScriptToArrays(comp, context);
if (comp.__internalDidAwakeAndStart)
return;
if (context.new_script_start.includes(comp) === false) {
context.new_script_start.push(comp);
}
}, true);
}
else {
console.warn("Missing context");
}
}
/**
* Removes the object from its parent and deactivates all of its components
* @param instance Object to remove
*/
static remove(instance) {
if (!instance)
return;
instance.parent?.remove(instance);
setActive(instance, false);
main.updateIsActive(instance);
GameObject.foreachComponent(instance, (comp) => {
main.processRemoveFromScene(comp);
}, true);
}
/**
* Invokes a method on all components including children (if a method with that name exists)
* @param go GameObject to invoke the method on
* @param functionName Name of the method to invoke
* @param args Arguments to pass to the method
*/
static invokeOnChildren(go, functionName, ...args) {
this.invoke(go, functionName, true, args);
}
/**
* Invokes a method on all components that have a method matching the provided name
* @param go GameObject to invoke the method on
* @param functionName Name of the method to invoke
* @param children Whether to invoke on children as well
* @param args Arguments to pass to the method
*/
static invoke(go, functionName, children = false, ...args) {
if (!go)
return;
this.foreachComponent(go, c => {
const fn = c[functionName];
if (fn && typeof fn === "function") {
fn?.call(c, ...args);
}
}, children);
}
/** @deprecated use `addComponent` */
// eslint-disable-next-line deprecation/deprecation
static addNewComponent(go, type, init, callAwake = true) {
return addComponent(go, type, init, { callAwake });
}
/**
* Adds a new component (or moves an existing component) to the provided object
* @param go Object to add the component to
* @param instanceOrType If an instance is provided it will be moved to the new object, if a type is provided a new instance will be created
* @param init Optional init object to initialize the component with
* @param opts Optional options for adding the component
* @returns The added or moved component
*/
static addComponent(go, instanceOrType, init, opts) {
return addComponent(go, instanceOrType, init, opts);
}
/**
* Moves a component to a new object
* @param go GameObject to move the component to
* @param instance Component to move
* @returns The moved component
*/
static moveComponent(go, instance) {
return addComponent(go, instance);
}
/**
* Removes a component from its object
* @param instance Component to remove
* @returns The removed component
*/
static removeComponent(instance) {
removeComponent(instance.gameObject, instance);
return instance;
}
/**
* Gets or adds a component of the specified type
* @param go GameObject to get or add the component to
* @param typeName Constructor of the component type
* @returns The existing or newly added component
*/
static getOrAddComponent(go, typeName) {
return getOrAddComponent(go, typeName);
}
/**
* Gets a component on the provided object
* @param go GameObject to get the component from
* @param typeName Constructor of the component type
* @returns The component if found, otherwise null
*/
static getComponent(go, typeName) {
if (go === null)
return null;
// if names are minified we could also use the type store and work with strings everywhere
// not ideal, but I dont know a good/sane way to do this otherwise
// const res = TypeStore.get(typeName);
// if(res) typeName = res;
return getComponent(go, typeName);
}
/**
* Gets all components of the specified type on the provided object
* @param go GameObject to get the components from
* @param typeName Constructor of the component type
* @param arr Optional array to populate with the components
* @returns Array of components
*/
static getComponents(go, typeName, arr = null) {
if (go === null)
return arr ?? [];
return getComponents(go, typeName, arr);
}
/**
* Finds an object or component by its unique identifier
* @param guid Unique identifier to search for
* @param hierarchy Root object to search in
* @returns The found GameObject or Component, or null/undefined if not found
*/
static findByGuid(guid, hierarchy) {
const res = findByGuid(guid, hierarchy);
return res;
}
/**
* Finds the first object of the specified component type in the scene
* @param typeName Constructor of the component type
* @param context Context or root object to search in
* @param includeInactive Whether to include inactive objects in the search
* @returns The first matching component if found, otherwise null
*/
static findObjectOfType(typeName, context, includeInactive = true) {
return findObjectOfType(typeName, context ?? Context.Current, includeInactive);
}
/**
* Finds all objects of the specified component type in the scene
* @param typeName Constructor of the component type
* @param context Context or root object to search in
* @returns Array of matching components
*/
static findObjectsOfType(typeName, context) {
const arr = [];
findObjectsOfType(typeName, arr, context);
return arr;
}
/**
* Gets a component of the specified type in the gameObject's children hierarchy
* @param go GameObject to search in
* @param typeName Constructor of the component type
* @returns The first matching component if found, otherwise null
*/
static getComponentInChildren(go, typeName) {
return getComponentInChildren(go, typeName);
}
/**
* Gets all components of the specified type in the gameObject's children hierarchy
* @param go GameObject to search in
* @param typeName Constructor of the component type
* @param arr Optional array to populate with the components
* @returns Array of components
*/
static getComponentsInChildren(go, typeName, arr = null) {
return getComponentsInChildren(go, typeName, arr ?? undefined);
}
/**
* Gets a component of the specified type in the gameObject's parent hierarchy
* @param go GameObject to search in
* @param typeName Constructor of the component type
* @returns The first matching component if found, otherwise null
*/
static getComponentInParent(go, typeName) {
return getComponentInParent(go, typeName);
}
/**
* Gets all components of the specified type in the gameObject's parent hierarchy
* @param go GameObject to search in
* @param typeName Constructor of the component type
* @param arr Optional array to populate with the components
* @returns Array of components
*/
static getComponentsInParent(go, typeName, arr = null) {
return getComponentsInParent(go, typeName, arr);
}
/**
* Gets all components on the gameObject
* @param go GameObject to get components from
* @returns Array of all components
*/
static getAllComponents(go) {
const componentsList = go.userData?.components;
if (!componentsList)
return [];
const newList = [...componentsList];
return newList;
}
/**
* Iterates through all components on the gameObject
* @param go GameObject to iterate components on
* @returns Generator yielding each component
*/
static *iterateComponents(go) {
const list = go?.userData?.components;
if (list && Array.isArray(list)) {
for (let i = 0; i < list.length; i++) {
yield list[i];
}
}
}
}
/**
* Needle Engine component base class. Component's are the main building blocks of the Needle Engine.
* Derive from {@link Behaviour} to implement your own using the provided lifecycle methods.
* Components can be added to any {@link Object3D} using {@link addComponent} or {@link GameObject.addComponent}.
*
* The most common lifecycle methods are {@link update}, {@link awake}, {@link start}, {@link onEnable}, {@link onDisable} and {@link onDestroy}.
*
* XR specific callbacks include {@link onEnterXR}, {@link onLeaveXR}, {@link onUpdateXR}, {@link onXRControllerAdded} and {@link onXRControllerRemoved}.
*
* To receive pointer events implement {@link onPointerDown}, {@link onPointerUp}, {@link onPointerEnter}, {@link onPointerExit} and {@link onPointerMove}.
*
* @example
* ```typescript
* import { Behaviour } from "@needle-tools/engine";
* export class MyComponent extends Behaviour {
* start() {
* console.log("Hello World");
* }
* update() {
* console.log("Frame", this.context.time.frame);
* }
* }
* ```
*
* @group Components
*/
export class Component {
/**
* Indicates whether this object is a component
* @internal
*/
get isComponent() { return true; }
__context;
/**
* The context this component belongs to, providing access to the runtime environment
* including physics, timing utilities, camera, and scene
*/
get context() {
return this.__context ?? Context.Current;
}
set context(context) {
this.__context = context;
}
/**
* Shorthand accessor for the current scene from the context
* @returns The scene this component belongs to
*/
get scene() { return this.context.scene; }
/**
* The layer value of the GameObject this component is attached to
* Used for visibility and physics filtering
*/
get layer() {
return this.gameObject?.userData?.layer;
}
/**
* The name of the GameObject this component is attached to
* Used for debugging and finding objects
*/
get name() {
if (this.gameObject?.name) {
return this.gameObject.name;
}
return this.gameObject?.userData.name;
}
__name;
set name(str) {
if (this.gameObject) {
if (!this.gameObject.userData)
this.gameObject.userData = {};
this.gameObject.userData.name = str;
this.__name = str;
}
else {
this.__name = str;
}
}
/**
* The tag of the GameObject this component is attached to
* Used for categorizing objects and efficient lookup
*/
get tag() {
return this.gameObject?.userData.tag;
}
set tag(str) {
if (this.gameObject) {
if (!this.gameObject.userData)
this.gameObject.userData = {};
this.gameObject.userData.tag = str;
}
}
/**
* Indicates whether the GameObject is marked as static
* Static objects typically don't move and can be optimized by the engine
*/
get static() {
return this.gameObject?.userData.static;
}
set static(value) {
if (this.gameObject) {
if (!this.gameObject.userData)
this.gameObject.userData = {};
this.gameObject.userData.static = value;
}
}
// get hideFlags(): HideFlags {
// return this.gameObject?.hideFlags;
// }
/**
* Checks if this component is currently active (enabled and part of an active GameObject hierarchy)
* Components that are inactive won't receive lifecycle method calls
* @returns True if the component is enabled and all parent GameObjects are active
*/
get activeAndEnabled() {
if (this.destroyed)
return false;
if (this.__isEnabled === false)
return false;
if (!this.__isActiveInHierarchy)
return false;
// let go = this.gameObject;
// do {
// // console.log(go.name, go.visible)
// if (!go.visible) return false;
// go = go.parent as GameObject;
// }
// while (go);
return true;
}
get __isActive() {
return this.gameObject.visible;
}
get __isActiveInHierarchy() {
if (!this.gameObject)
return false;
const res = this.gameObject[activeInHierarchyFieldName];
if (res === undefined)
return true;
return res;
}
set __isActiveInHierarchy(val) {
if (!this.gameObject)
return;
this.gameObject[activeInHierarchyFieldName] = val;
}
/**
* Reference to the GameObject this component is attached to
* This is a three.js Object3D with additional GameObject functionality
*/
gameObject;
/**
* Unique identifier for this component instance,
* used for finding and tracking components
*/
guid = "invalid";
/**
* Identifier for the source asset that created this component.
* For example, URL to the glTF file this component was loaded from
*/
sourceId;
/**
* Called once when the component becomes active for the first time.
* This is the first lifecycle callback to be invoked
*/
awake() { }
/**
* Called every time the component becomes enabled or active in the hierarchy.
* Invoked after {@link awake} and before {@link start}.
*/
onEnable() { }
/**
* Called every time the component becomes disabled or inactive in the hierarchy.
* Invoked when the component or any parent GameObject becomes invisible
*/
onDisable() { }
/**
* Called when the component is destroyed.
* Use for cleanup operations like removing event listeners
*/
onDestroy() {
this.__destroyed = true;
}
/**
* Starts a coroutine that can yield to wait for events.
* Coroutines allow for time-based sequencing of operations without blocking.
* Coroutines are based on generator functions, a JavaScript language feature.
*
* @param routine Generator function to start
* @param evt Event to register the coroutine for (default: FrameEvent.Update)
* @returns The generator function that can be used to stop the coroutine
* @example
* Time-based sequencing of operations
* ```ts
* *myCoroutine() {
* yield WaitForSeconds(1); // wait for 1 second
* yield WaitForFrames(10); // wait for 10 frames
* yield new Promise(resolve => setTimeout(resolve, 1000)); // wait for a promise to resolve
* }
* ```
* @example
* Coroutine that logs a message every 5 frames
* ```ts
* onEnable() {
* this.startCoroutine(this.myCoroutine());
* }
* private *myCoroutine() {
* while(this.activeAndEnabled) {
* console.log("Hello World", this.context.time.frame);
* // wait for 5 frames
* for(let i = 0; i < 5; i++) yield;
* }
* }
* ```
*/
startCoroutine(routine, evt = FrameEvent.Update) {
return this.context.registerCoroutineUpdate(this, routine, evt);
}
/**
* Stops a coroutine that was previously started with startCoroutine
* @param routine The routine to be stopped
* @param evt The frame event the routine was registered with
*/
stopCoroutine(routine, evt = FrameEvent.Update) {
this.context.unregisterCoroutineUpdate(routine, evt);
}
/**
* Checks if this component has been destroyed
* @returns True if the component or its GameObject has been destroyed
*/
get destroyed() {
return this.__destroyed;
}
/**
* Destroys this component and removes it from its GameObject
* After destruction, the component will no longer receive lifecycle callbacks
*/
destroy() {
if (this.__destroyed)
return;
this.__internalDestroy();
}
/** @internal */
__didAwake = false;
/** @internal */
__didStart = false;
/** @internal */
__didEnable = false;
/** @internal */
__isEnabled = undefined;
/** @internal */
__destroyed = false;
/** @internal */
get __internalDidAwakeAndStart() { return this.__didAwake && this.__didStart; }
/** @internal */
constructor(init) {
this.__didAwake = false;
this.__didStart = false;
this.__didEnable = false;
this.__isEnabled = undefined;
this.__destroyed = false;
this._internalInit(init);
if (isHotReloadEnabled())
registerHotReloadType(this);
}
/** @internal */
__internalNewInstanceCreated(init) {
this.__didAwake = false;
this.__didStart = false;
this.__didEnable = false;
this.__isEnabled = undefined;
this.__destroyed = false;
this._internalInit(init);
return this;
}
/**
* Initializes component properties from an initialization object
* @param init Object with properties to copy to this component
* @internal
*/
_internalInit(init) {
if (typeof init === "object") {
for (const key of Object.keys(init)) {
const value = init[key];
// we don't want to allow overriding functions via init
if (typeof value === "function")
continue;
this[key] = value;
}
}
}
/** @internal */
__internalAwake() {
if (this.__didAwake)
return;
this.__didAwake = true;
this.awake();
}
/** @internal */
__internalStart() {
if (this.__didStart)
return;
this.__didStart = true;
if (this.start)
this.start();
}
/** @internal */
__internalEnable(isAddingToScene) {
if (this.__destroyed) {
if (isDevEnvironment()) {
console.warn("[Needle Engine Dev] Trying to enable destroyed component");
}
return false;
}
// Don't change enable before awake
// But a user can change enable during awake
if (!this.__didAwake)
return false;
if (this.__didEnable) {
// We dont want to change the enable state if we are adding to scene
// But we want to change the state when e.g. a user changes the enable state during awake
if (isAddingToScene !== true)
this.__isEnabled = true;
return false;
}
// console.trace("INTERNAL ENABLE");
this.__didEnable = true;
this.__isEnabled = true;
this.onEnable();
return true;
}
/** @internal */
__internalDisable(isRemovingFromScene) {
// Don't change enable before awake
// But a user can change enable during awake
if (!this.__didAwake)
return;
if (!this.__didEnable) {
// We dont want to change the enable state if we are removing from a scene
if (isRemovingFromScene !== true)
this.__isEnabled = false;
return;
}
this.__didEnable = false;
this.__isEnabled = false;
this.onDisable();
}
/** @internal */
__internalDestroy() {
if (this.__destroyed)
return;
this.__destroyed = true;
if (this.__didAwake) {
this.onDestroy?.call(this);
this.dispatchEvent(new CustomEvent("destroyed", { detail: this }));
}
destroyComponentInstance(this);
if (isHotReloadEnabled())
unregisterHotReloadType(this);
}
/**
* Controls whether this component is enabled
* Disabled components don't receive lifecycle callbacks
*/
get enabled() {
return typeof this.__isEnabled === "boolean" ? this.__isEnabled : true; // if it has no enabled field it is always enabled
}
set enabled(val) {
if (this.__destroyed) {
if (isDevEnvironment()) {
console.warn(`[Needle Engine Dev] Trying to ${val ? "enable" : "disable"} destroyed component`);
}
return;
}
// when called from animationclip we receive numbers
// due to interpolation they can be anything between 0 and 1
if (typeof val === "number") {
if (val >= 0.5)
val = true;
else
val = false;
}
// need to check here because codegen is calling this before everything is setup
if (!this.__didAwake) {
this.__isEnabled = val;
return;
}
if (val) {
this.__internalEnable();
}
else {
this.__internalDisable();
}
}
/**
* Gets the position of this component's GameObject in world space.
* Note: This is equivalent to calling `this.gameObject.worldPosition`
*/
get worldPosition() {
return threeutils.getWorldPosition(this.gameObject);
}
/**
* Sets the position of this component's GameObject in world space
* @param val The world position vector to set
*/
set worldPosition(val) {
threeutils.setWorldPosition(this.gameObject, val);
}
/**
* Sets the position of this component's GameObject in world space using individual coordinates
* @param x X-coordinate in world space
* @param y Y-coordinate in world space
* @param z Z-coordinate in world space
*/
setWorldPosition(x, y, z) {
threeutils.setWorldPositionXYZ(this.gameObject, x, y, z);
}
/**
* Gets the rotation of this component's GameObject in world space as a quaternion
* Note: This is equivalent to calling `this.gameObject.worldQuaternion`
*/
get worldQuaternion() {
return threeutils.getWorldQuaternion(this.gameObject);
}
/**
* Sets the rotation of this component's GameObject in world space using a quaternion
* @param val The world rotation quaternion to set
*/
set worldQuaternion(val) {
threeutils.setWorldQuaternion(this.gameObject, val);
}
/**
* Sets the rotation of this component's GameObject in world space using quaternion components
* @param x X component of the quaternion
* @param y Y component of the quaternion
* @param z Z component of the quaternion
* @param w W component of the quaternion
*/
setWorldQuaternion(x, y, z, w) {
threeutils.setWorldQuaternionXYZW(this.gameObject, x, y, z, w);
}
/**
* Gets the rotation of this component's GameObject in world space as Euler angles (in radians)
*/
get worldEuler() {
return threeutils.getWorldEuler(this.gameObject);
}
/**
* Sets the rotation of this component's GameObject in world space using Euler angles (in radians)
* @param val The world rotation Euler angles to set
*/
set worldEuler(val) {
threeutils.setWorldEuler(this.gameObject, val);
}
/**
* Gets the rotation of this component's GameObject in world space as Euler angles (in degrees)
* Note: This is equivalent to calling `this.gameObject.worldRotation`
*/
get worldRotation() {
return this.gameObject.worldRotation;
}
/**
* Sets the rotation of this component's GameObject in world space using Euler angles (in degrees)
* @param val The world rotation vector to set (in degrees)
*/
set worldRotation(val) {
this.setWorldRotation(val.x, val.y, val.z, true);
}
/**
* Sets the rotation of this component's GameObject in world space using individual Euler angles
* @param x X-axis rotation
* @param y Y-axis rotation
* @param z Z-axis rotation
* @param degrees Whether the values are in degrees (true) or radians (false)
*/
setWorldRotation(x, y, z, degrees = true) {
threeutils.setWorldRotationXYZ(this.gameObject, x, y, z, degrees);
}
static _forward = new Vector3();
/**
* Gets the forward direction vector (0,0,-1) of this component's GameObject in world space
*/
get forward() {
return Component._forward.set(0, 0, -1).applyQuaternion(this.worldQuaternion);
}
static _right = new Vector3();
/**
* Gets the right direction vector (1,0,0) of this component's GameObject in world space
*/
get right() {
return Component._right.set(1, 0, 0).applyQuaternion(this.worldQuaternion);
}
static _up = new Vector3();
/**
* Gets the up direction vector (0,1,0) of this component's GameObject in world space
*/
get up() {
return Component._up.set(0, 1, 0).applyQuaternion(this.worldQuaternion);
}
// EventTarget implementation:
/**
* Storage for event listeners registered to this component
* @private
*/
_eventListeners = new Map();
/**
* Registers an event listener for the specified event type
* @param type The event type to listen for
* @param listener The callback function to execute when the event occurs
*/
addEventListener(type, listener) {
this._eventListeners[type] = this._eventListeners[type] || [];
this._eventListeners[type].push(listener);
}
/**
* Removes a previously registered event listener
* @param type The event type the listener was registered for
* @param listener The callback function to remove
*/
removeEventListener(type, listener) {
if (!this._eventListeners[type])
return;
const index = this._eventListeners[type].indexOf(listener);
if (index >= 0)
this._eventListeners[type].splice(index, 1);
}
/**
* Dispatches an event to all registered listeners
* @param evt The event object to dispatch
* @returns Always returns false (standard implementation of EventTarget)
*/
dispatchEvent(evt) {
if (!evt || !this._eventListeners[evt.type])
return false;
const listeners = this._eventListeners[evt.type];
for (let i = 0; i < listeners.length; i++) {
listeners[i](evt);
}
return false;
}
}
// For legacy reasons we need to export this as well
// (and we don't use extend to inherit the component docs)
export { Component as Behaviour };
//# sourceMappingURL=Component.js.map