import * as toposort from 'toposort'; const DEBUG_MODE = /localhost/.test(window.location.href) ? 2 : 0; export class UtilRegistry { _registeredUtils = new Array(); _activeUtilInstances = new Array(); _appInstance; /** * function registerUtil * * utils need to have at least these properties: * name: string | utils name, e.g. 'example' * selector: string | utils selector, e.g. '[uw-example]' * setup: Function | utils setup function, see below * * optional util properties: * start: Function | utils start function, see below * * setup function must return instance object with at least these properties: * name: string | utils name * element: HTMLElement | element the util is applied to * destroy: Function | function to destroy the util and remove any listeners * * (optional) start function for registering event listeners * * @param util Object Utility that should be added to the registry */ register(util) { if (DEBUG_MODE > 2) { console.log('registering util "' + util.name + '"'); console.log({ util }); } this._registeredUtils.push(util); } deregister(name, destroy) { const utilIndex = this._findUtilIndex(name); if (utilIndex >= 0) { if (destroy === true) { this._destroyUtilInstances(name); } this._registeredUtils.splice(utilIndex, 1); } } setApp(appInstance) { this._appInstance = appInstance; } initAll(scope = document.body) { let startedInstances = new Array(); const setupInstances = this._registeredUtils.map((util) => this.setup(util, scope)).flat(); const orderedInstances = setupInstances.filter(_isStartOrdered); if (DEBUG_MODE > 3) { console.log({ setupInstances, orderedInstances }); } const startDependencies = new Array(); for (const utilInstance of orderedInstances) { for (const otherInstance of setupInstances) { const startOrder = _startOrder(utilInstance, otherInstance); if (typeof startOrder !== 'undefined') startDependencies.push(startOrder); } } if (DEBUG_MODE > 2) { console.log('starting instances', { setupInstances, startDependencies, order: toposort.array(setupInstances, startDependencies) }); } toposort.array(setupInstances, startDependencies).forEach((utilInstance) => { if (utilInstance) { if (DEBUG_MODE > 2) { console.log('starting utilInstance', { util: utilInstance.util.name, utilInstance }); } const instance = utilInstance.instance; if (instance && typeof instance.start === 'function') { instance.start.bind(instance)(); startedInstances.push(instance); } } }); if (DEBUG_MODE > 1) { console.info('initialized js util instances:'); console.table(setupInstances); } return startedInstances; } setup(util, scope = document.body) { if (DEBUG_MODE > 2) { console.log('setting up util', { util }); } let instances = new Array(); if (util) { const elements = this._findUtilElements(util, scope); elements.forEach((element) => { let utilInstance = null; try { utilInstance = new util(element, this._appInstance); } catch(err) { if (DEBUG_MODE > 0) { console.error('Error while trying to initialize a utility!', { util , element, err }); console.error(err.stack); } utilInstance = null; } if (utilInstance) { if (DEBUG_MODE > 2) { console.info('Got utility instance for utility "' + util.name + '"', { utilInstance }); } instances.push({ util: util, scope: scope, element: element, instance: utilInstance }); } }); } this._activeUtilInstances.push(...instances); return instances; } find(name) { return this._registeredUtils.find((util) => util.name === name); } _findUtilElements(util, scope) { if (scope && scope.matches(util.selector)) { return [scope]; } return Array.from(scope.querySelectorAll(util.selector)); } _findUtilIndex(name) { return this._registeredUtils.findIndex((util) => util.name === name); } _destroyUtilInstances(name) { this._activeUtilInstances .map((util, index) => ({ util: util, index: index, })) .filter((activeUtil) => activeUtil.util.name === name) .forEach((activeUtil) => { // destroy util instance activeUtil.util.destroy(); delete this._activeUtilInstances[activeUtil.index]; }); // get rid of now empty array slots this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); } } function _startOrder(utilInstance, otherInstance) { if (utilInstance.element !== otherInstance.element && !(utilInstance.element.contains(otherInstance.element) || otherInstance.element.contains(utilInstance.element))) return undefined; if (utilInstance === otherInstance) return undefined; if (!_isStartOrdered(utilInstance) || !otherInstance.instance || !otherInstance.util) return undefined; function orderParam(name) { if (typeof utilInstance.instance[name] === 'function') return !!utilInstance.instance[name](otherInstance.instance); if (typeof utilInstance.util[name] === 'function') return !!utilInstance.util[name](otherInstance.instance); else if (Array.isArray(utilInstance.instance[name])) return utilInstance.instance[name].some(constr => otherInstance.util === constr); else if (Array.isArray(utilInstance.util[name])) return utilInstance.util[name].some(constr => otherInstance.util === constr); return false; } const after = orderParam('startAfter'); const before = orderParam('startBefore'); if (DEBUG_MODE > 3) { console.log('compared instances for ordering', { utilInstance, otherInstance }, { after, before }); } if (after && before) { console.error({ utilInstance, otherInstance }); throw new Error(`Incompatible start ordering: ${utilInstance.instance.constructor.name} and ${otherInstance.instance.constructor.name}`); } else if (after) return [otherInstance, utilInstance]; else if (before) return [utilInstance, otherInstance]; return undefined; } function _isStartOrdered(utilInstance) { if (!utilInstance || !utilInstance.instance || !utilInstance.util) return false; function isOrderParam(name) { return typeof utilInstance.instance[name] === 'function' || typeof utilInstance.util[name] === 'function' || Array.isArray(utilInstance.instance[name]) || Array.isArray(utilInstance.util[name]); } return isOrderParam('startBefore') || isOrderParam('startAfter'); }