UNPKG

@studiometa/js-toolkit

Version:

A set of useful little bits of JavaScript to boost your project! 🚀

478 lines (477 loc) • 13.4 kB
import { getComponentElements, getEventTarget, addToQueue, addInstance, deleteInstance, addToRegistry, getInstances, findClosestInstance } from "./utils.js"; import { ChildrenManager, RefsManager, ServicesManager, EventsManager, OptionsManager } from "./managers/index.js"; import { getInstanceFromElement } from "../helpers/getInstanceFromElement.js"; import { noop, isDev, isFunction, isArray } from "../utils/index.js"; let id = 0; class Base { /** * This is a Base instance. */ static $isBase = true; /** * Config. * @link https://js-toolkit.studiometa.dev/api/configuration.html */ static config = { name: "Base", emits: [ // hook events "before-mounted", "mounted", "after-mounted", "updated", "before-destroyed", "destroyed", "after-destroyed", "terminated", // default services' events "ticked", "scrolled", "resized", "moved", "loaded", "keyed" ] }; /** * The instance id. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#id */ $id; /** * The root element. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#el */ $el; /** * The state of the component. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#isMounted */ $isMounted = false; /** * Is the component currently mounting itself and its children? * @private */ __isMounting = false; /** * Store the event handlers. * @private */ __eventHandlers = /* @__PURE__ */ new Map(); /** * Get the root instance of the app. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#root */ get $root() { if (!this.$parent) { return this; } let parent = this.$parent; let root = this.$parent; while (parent) { if (!parent.$parent) { root = parent; } parent = parent.$parent; } return root; } /** * The parent instance if the current instance is registered as a child component in a parent component. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#parent */ get $parent() { const parents = /* @__PURE__ */ new Set(); for (const instance of getInstances()) { if (!instance.$config.components) continue; if (Object.values(instance.$config.components).includes(this.constructor) && instance.$el.contains(this.$el)) { parents.add(instance); } } const closest = findClosestInstance(this.$el, parents); return closest ?? null; } /** * Merge configuration with the parents' configurations. * @private */ get __config() { let proto = Object.getPrototypeOf(this); let { config } = proto.constructor; while (proto.constructor.config && proto.constructor.$isBase) { config = { ...proto.constructor.config, ...config }; if (proto.constructor.config.options) { config.options = { ...proto.constructor.config.options, ...config.options }; } if (proto.constructor.config.emits && config.emits) { config.emits = [...proto.constructor.config.emits, ...config.emits]; } proto = Object.getPrototypeOf(proto); } config.options = config.options ?? {}; config.refs = config.refs ?? []; config.components = config.components ?? {}; return config; } /** * The normalized config of the component. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#config */ get $config() { return this.__config; } __services; /** * Services. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#services */ get $services() { return this.__services; } __refs; /** * Refs. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#refs */ get $refs() { return this.__refs.props; } __options; /** * Options. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#options */ get $options() { return this.__options.props; } __children; /** * Children. * @link https://js-toolkit.studiometa.dev/api/instance-properties.html#children */ get $children() { return this.__children.props; } __events; /** * Small helper to log stuff. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#log-content */ get $log() { return this.$options.log ? window.console.log.bind(window, `[${this.$id}]`) : noop; } /** * Small helper to make warning easier. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#warn-content */ get $warn() { return this.$options.log ? window.console.warn.bind(window, `[${this.$id}]`) : noop; } /** * Small helper to debug information. * @internal */ get __debug() { return isDev && this.$options.debug ? window.console.log.bind(window, `[debug] [${this.$id}]`) : noop; } /** * Get manager constructors. * @internal */ get __managers() { return { ChildrenManager, EventsManager, OptionsManager, RefsManager, ServicesManager }; } /** * Call an instance method and emit corresponding events. * @internal */ __callMethod(method, ...args) { if (isDev) { this.__debug("callMethod", method, ...args); } this.$emit(method, ...args); if (!isFunction(this[method])) { return null; } if (isDev) { this.__debug(method, this, ...args); } return this[method].call(this, ...args); } /** * Test if the given event has been bound to the instance. * * @param {string} event The event's name. * @return {boolean} Wether the given event has been bound or not. * @internal */ __hasEvent(event) { const eventHandlers = this.__eventHandlers.get(event); return eventHandlers && eventHandlers.size > 0; } /** * Class constructor where all the magic takes place. * * @param {HTMLElement} element The component's root element dd. * @link https://js-toolkit.studiometa.dev/api/instantiation.html */ constructor(element) { if (!element) { if (isDev) { throw new Error("The root element must be defined."); } return; } const { $config } = this; if ($config.name === "Base") { if (isDev) { throw new Error("The `config.name` property is required."); } return; } addToRegistry($config.name, this.constructor); this.$id = `${$config.name}-${id}`; id += 1; this.$el = element; this.$el.__base__ ??= /* @__PURE__ */ new Map(); this.$el.__base__.set($config.name, this); for (const service of ["Options", "Services", "Events", "Refs", "Children"]) { this[`__${service.toLowerCase()}`] = new this.__managers[`${service}Manager`](this); } this.$on("before-mounted", () => { addInstance(this); }); this.$on("after-destroyed", () => { deleteInstance(this); }); if (isDev) { this.__debug("constructor", this); } } /** * Trigger the `mounted` callback. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#mount */ async $mount() { if (this.$isMounted || this.__isMounting) { return this; } this.__isMounting = true; this.$emit("before-mounted"); if (isDev) { this.__debug("$mount"); } await Promise.all([ addToQueue(() => this.__children.registerAll()), addToQueue(() => this.__refs.registerAll()), addToQueue(() => this.__events.bindRootElement()), addToQueue(() => this.__services.enableAll()), addToQueue(() => this.__children.mountAll()) ]); await addToQueue(() => { this.$isMounted = true; this.__callMethod("mounted"); }); this.__isMounting = false; this.$emit("after-mounted"); return this; } /** * Update the instance children. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#update */ async $update() { if (isDev) { this.__debug("$update"); } await Promise.all([ // Undo addToQueue(() => this.__refs.unregisterAll()), addToQueue(() => this.__services.disableAll()), // Redo addToQueue(() => this.__children.registerAll()), addToQueue(() => this.__refs.registerAll()), addToQueue(() => this.__services.enableAll()), // Update addToQueue(() => this.__children.updateAll()) ]); await addToQueue(() => this.__callMethod("updated")); return this; } /** * Trigger the `destroyed` callback. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#destroy */ async $destroy() { if (!this.$isMounted) { return this; } if (isDev) { this.__debug("$destroy"); } this.$emit("before-destroyed"); this.$isMounted = false; await Promise.all([ addToQueue(() => this.__events.unbindRootElement()), addToQueue(() => this.__refs.unregisterAll()), addToQueue(() => this.__services.disableAll()), addToQueue(() => this.__children.destroyAll()) ]); await addToQueue(() => this.__callMethod("destroyed")); this.$emit("after-destroyed"); return this; } /** * Terminate a child instance when it is not needed anymore. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#terminate */ async $terminate() { if (isDev) { this.__debug("$terminate"); } await Promise.all([ // First, destroy the component. addToQueue(() => this.$destroy()), // Execute the `terminated` hook if it exists addToQueue(() => this.__callMethod("terminated")), // Delete instance addToQueue(() => this.$el.__base__.set(this.$config.name, "terminated")) ]); } /** * Add an emitted event. * * @param {string} event The event name. * @return {void} * @internal */ __addEmits(event) { const ctor = this.constructor; if (isArray(ctor.config.emits)) { ctor.config.emits.push(event); } else { ctor.config.emits = [event]; } } /** * Remove an emitted event. * * @param {string} event The event name. * @return {void} * @internal */ __removeEmits(event) { const ctor = this.constructor; const index = ctor.config.emits.indexOf(event); ctor.config.emits.splice(index, 1); } /** * Bind a listener function to an event. * * @param {string} event * Name of the event. * @param {EventListenerOrEventListenerObject} listener * Function to be called. * @param {boolean|AddEventListenerOptions} [options] * Options for the `removeEventListener` method. * @return {() => void} * A function to unbind the listener. * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#on-event-callback-options */ $on(event, listener, options) { if (isDev) { this.__debug("$on", event, listener, options); } let set = this.__eventHandlers.get(event); if (!set) { set = /* @__PURE__ */ new Set(); this.__eventHandlers.set(event, set); } set.add(listener); const target = getEventTarget(this, event, this.__config); target?.addEventListener(event, listener, options); if (isDev) { if (!target) { console.warn( `[${this.$id}]`, `The "${event}" event is missing from the configuration and is not a native`, `event for the root element of type \`${this.$el.constructor.name}\`.` ); } } return () => { this.$off(event, listener, options); }; } /** * Unbind a listener function from an event. * * @param {string} event * Name of the event. * @param {EventListenerOrEventListenerObject} listener * Function to be removed. * @param {boolean|EventListenerOptions} [options] * Options for the `removeEventListener` method. * @return {void} * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#off-event-callback-options */ $off(event, listener, options) { if (isDev) { this.__debug("$off", event, listener); } this.__eventHandlers.get(event)?.delete(listener); const target = getEventTarget(this, event, this.__config); target?.removeEventListener(event, listener, options); } /** * Emits an event. * * @param {string} event Name of the event. * @param {any[]} args The arguments to apply to the functions bound to this event. * @return {void} * @link https://js-toolkit.studiometa.dev/api/instance-methods.html#emit-event-args */ $emit(event, ...args) { if (isDev) { this.__debug("$emit", event, args); } this.$el.dispatchEvent( event instanceof Event ? event : new CustomEvent(event, { detail: args }) ); } /** * Register and mount all instances of the component. * @link https://js-toolkit.studiometa.dev/api/static-methods.html#register-nameorselector-string */ static $register(nameOrSelector) { addToRegistry(nameOrSelector ?? this.config.name, this); return getComponentElements(nameOrSelector ?? this.config.name).map( async (el) => getInstanceFromElement(el, this) ?? new this(el).$mount() ); } } export { Base }; //# sourceMappingURL=Base.js.map