bona
Version:
Super lightweight microframework focused on static websites and landing pages.
329 lines (285 loc) • 9.66 kB
JavaScript
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 [];
}
}