UNPKG

bona

Version:

Super lightweight microframework focused on static websites and landing pages.

329 lines (285 loc) 9.66 kB
import Base from '../base'; export default class Core extends Base { /** * @typedef {Object} BonaOptions * @property {boolean} [init] Initialize an app immediately. * @property {Object[]} [define] Array of components to define. */ /** * Initializes the core. * * @param {BonaOptions} [options] Options. */ constructor(options) { super(); /** @type {BonaOptions} **/ this.options = { init: true, define: null, ...options, }; this.store = new Map(); this.registry = new Map(); this.isFullyLoaded = false; if (this.options.define) this.defineAll(this.options.define); if (this.options.init) this.init(); } /** * Performs basic hooks. * * @return {Promise} */ async init() { await this.waitFullLoad(); await this.executeAll('init'); await this.executeAll('enter'); await this.executeAll('complete'); } /** * Refresh component store. * * @param {boolean} [fireRefresh] Fire refresh hook in instances. * @param {boolean} [fireDestroy] Fire destroy hook when detach non-existent nodes. * @param {boolean} [detachNodes] Detach non-existent nodes from store. * @return {Promise} */ async refresh(fireRefresh = true, fireDestroy = true, detachNodes = true) { const preExecutors = []; const executors = []; this.registry.forEach((options, namespace) => { const instances = this.store.get(namespace) || []; const els = new Set(options.assign ? this.queryAll(options.assign) : []); instances.forEach((instance) => { if (options.assign) { if (instance.el?.isConnected) { if (fireRefresh) executors.push(this.executeInstance(instance, 'refresh')); els.delete(instance.el); } else { if (detachNodes) preExecutors.push(this.detach(instance, fireDestroy)); } } else { if (fireRefresh) executors.push(this.executeInstance(instance, 'refresh')); } }); els.forEach((el) => { executors.push(this.attach(namespace, el)); }); }); await Promise.all(preExecutors); if (fireRefresh) this.trigger('refresh'); return Promise.all(executors); } /** * Execute hooks in all instances. * * @param {string} [hook] Hook name. * @return {Promise} */ async executeAll(hook = 'init') { const executors = []; this.trigger(hook); this.store.forEach((instances) => { instances.forEach((instance) => executors.push(this.executeInstance(instance, hook))); }); return Promise.all(executors); } /** * Execute hook in an instance. * * @param {Component} instance Component instance. * @param {string} [hook] Hook name. * @return {Promise} */ async executeInstance(instance, hook = 'init') { const method = 'on' + hook.charAt(0).toUpperCase() + hook.slice(1); if (instance[method]) return (instance._executors[hook] = instance[method]()); } /** * Wait until the component hook is completed. * * @param {string} namespace Namespace. * @param {string} [hook] Hook name. * @param {number} [index] Instance index. * @return {Promise} */ async wait(namespace, hook = 'init', index = 0) { return this.waitInstance(this.get(namespace, index), hook); } /** * Wait until hooks are completed in all instances. * * @param {string} namespace Namespace. * @param {string} [hook] Hook name. * @return {Promise} */ async waitAll(namespace, hook = 'init') { const executors = []; this.store.get(namespace).forEach((instance) => { executors.push(this.waitInstance(instance, hook)); }); return Promise.all(executors); } /** * Wait until the hook is completed in an instance. * * @param {Component} instance Component instance. * @param {string} [hook] Hook name. * @return {Promise} */ async waitInstance(instance, hook = 'init') { return instance._executors[hook]; } /** * Wait until the page has fully loaded. * * @return {Promise} */ async waitFullLoad() { await new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', () => resolve()); } }); this.isFullyLoaded = true; } /** * Create component instance in the store. * * @param {string} namespace Namespace. * @param {HTMLElement} [el] Element. * @param {Object} [options] Options that will pass to instance. * @param {boolean} [fireInit] Fire init hook. * @return {Component} Component instance. */ async attach(namespace, el, options, fireInit = true) { const factory = this.registry.get(namespace).component; const instance = new factory(this, el, { ...this.registry.get(namespace).options, ...options, }); if (!this.store.has(namespace)) this.store.set(namespace, new Set()); this.store.get(namespace).add(instance); instance._namespace = namespace; if (fireInit) await this.executeInstance(instance, 'init'); return instance; } /** * Remove component instance from store. * * @param {Component} instance Component instance. * @param {boolean} [fireDestroy] Fire destroy hook. * @return {Component} Component instance. */ async detach(instance, fireDestroy = true) { this.store.get(instance._namespace).delete(instance); if (fireDestroy) await this.executeInstance(instance, 'destroy'); return instance; } /** * Register component. * * @param {string} namespace Namespace. * @param {Object} component Component class. * @param {string} [assign] Assigned DOM selector. * @param {Object} [options] The options object that will be passed to each instance. */ define(namespace, component, assign, options) { this.registry.set(namespace, { assign, component, options, }); if (assign) { const els = this.queryAll(assign); els.forEach((el) => { this.attach(namespace, el, null, this.isFullyLoaded); }); } else { this.attach(namespace, null, null, this.isFullyLoaded); } } /** * Register array of components. */ defineAll(arr) { arr.forEach((item) => { this.define(item.namespace, item.component, item.assign, item.options); }); } /** * Return component instance. * * @param {string} namespace Namespace. * @param {number} [index] Instance index. * @return {Component|*} Component instance. */ get(namespace, index = 0) { const instances = this.store.get(namespace); return instances ? [...instances][index] : null; } /** * Return all component instances. * * @param {string} namespace Namespace. * @return {Component[]|*[]} Array of component instances. */ getAll(namespace) { return this.store.get(namespace); } /** * Find component instance that matches a CSS selector. * * @param {string|Element} selector Selector or node. * @param {string} [namespace] Namespace. * @param {number} [index] Instance index. * @return {Component|*} Component instance. */ find(selector, namespace, index = 0) { const instance = this.findAll(selector, namespace); return instance ? instance[index] : null; } /** * Find all component instances that match a CSS selector. * * @param {string|Element} selector Selector or node. * @param {string} [namespace] Namespace. * @return {Component[]|*[]} Array of component instances. */ findAll(selector, namespace) { const store = namespace ? [this.store.get(namespace) || []] : this.store; const result = []; store.forEach((instances) => { instances.forEach((instance) => { if (!instance.el) return; if (typeof selector === 'string' ? instance.el.matches(selector) : instance.el === selector) { result.push(instance); } }); }); return result; } /** * Returns the first element that matches a CSS selector. * * @param {string|Object} selector Selector. * @return {null|Element|Object} */ query(selector) { if (typeof selector === 'string') return document.querySelector(selector); if (typeof selector === 'object') return selector; return null; } /** * Returns all elements that match a CSS selector. * * @param {string|Object} selector Selector. * @return {Element[]|Object} */ queryAll(selector) { if (typeof selector === 'string') return Array.from(document.querySelectorAll(selector)); if (typeof selector === 'object') return selector; return []; } }