UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

171 lines (170 loc) 6.45 kB
// Private key to store mixin instances const STORE = (window.Symbol || String)('__esl_mixins'); // Singleton for registry let global; /** Registry to store and initialize {@link ESLMixinElement} instances */ export class ESLMixinRegistry { constructor() { /** Map that stores available mixins under their identifier (attribute) */ this.store = new Map(); /** MutationObserver instance to track DOM changes and init mixins on-fly */ this.mutation$$ = new MutationObserver(this._onMutation.bind(this)); if (global) return global; // eslint-disable-next-line @typescript-eslint/no-this-alias global = this; } /** Array of registered mixin tags */ get observedAttributes() { const attrs = []; this.store.forEach((mixin, name) => attrs.push(name)); return attrs; } /** Registers mixin definition using {@link ESLMixinElement} constructor */ register(mixin) { if (!mixin.is || mixin.is.indexOf('-') === -1) { throw new DOMException(`[ESL]: Illegal mixin attribute name "${mixin.is}"`, 'NotSupportedError'); } const registered = this.store.get(mixin.is); if (registered && registered !== mixin) { throw new DOMException(`[ESL]: Attribute ${mixin.is} is already occupied by another mixin`, 'InUseAttributeError'); } if (!registered) { this.store.set(mixin.is, mixin); this.invalidateRecursive(document.documentElement, mixin.is); this.resubscribe(); } } /** Resubscribes DOM observer */ resubscribe(root = document.documentElement) { // Don't let flushed changes from being unhandled this._onMutation(this.mutation$$.takeRecords()); // Resubscribe for all observed attributes this.mutation$$.disconnect(); this.mutation$$.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: this.observedAttributes, attributeOldValue: true }); } /** * Invalidates all mixins on the element and subtree * @param root - root HTMLElement to start traversing * @param name - optional filter for mixin name */ invalidateRecursive(root = document.body, name) { var _a; if (!root) return; name ? this.invalidate(root, name) : this.invalidateAll(root); if (!((_a = root.children) === null || _a === void 0 ? void 0 : _a.length)) return; for (const child of root.children) { this.invalidateRecursive(child, name); } } /** * Invalidates all mixins on the element * @param el - host element to invalidate mixins */ invalidateAll(el) { const hasStore = Object.hasOwnProperty.call(el, STORE); this.store.forEach((mixin, name) => { if (el.hasAttribute(name)) { ESLMixinRegistry.init(el, mixin); } else if (hasStore) { ESLMixinRegistry.destroy(el, name); } }); } /** * Invalidates passed mixin name on the element * @param el - host element to invalidate mixin * @param name - mixin name to invalidate * @param oldValue - optional previous value of mixins attribute */ invalidate(el, name, oldValue = null) { const newValue = el.getAttribute(name); if (newValue === null) return ESLMixinRegistry.destroy(el, name); const instance = ESLMixinRegistry.get(el, name); if (instance) { instance.attributeChangedCallback(name, oldValue, newValue); } else { const type = this.store.get(name); type && ESLMixinRegistry.init(el, type); } } /** Handles DOM {@link MutationRecord} list */ _onMutation(mutations) { mutations.forEach((record) => { if (record.type === 'attributes' && record.attributeName) { this.invalidate(record.target, record.attributeName, record.oldValue); } if (record.type === 'childList') { record.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) this.invalidateRecursive(node); }); record.removedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) ESLMixinRegistry.destroyAll(node); }); } }); } /** @returns mixin instance by name */ static get(el, mixin = this.is) { const store = el[STORE]; return (store && store[mixin]) || null; } /** @returns all mixins initialized on passed host element */ static getAll(el) { const store = el[STORE]; return store ? Object.values(store) : []; } /** @returns if the passed mixin exists on the element */ static has(el, mixin) { return !!this.get(el, mixin); } /** Sets mixin instance to the element store */ static set(el, mixin) { if (!Object.hasOwnProperty.call(el, STORE)) Object.defineProperty(el, STORE, { value: {}, configurable: true }); const store = el[STORE]; store[mixin.constructor.is] = mixin; } /** Inits mixin instance on the element */ static init(el, Mixin) { if (this.has(el, Mixin.is)) return null; const instance = new Mixin(el); ESLMixinRegistry.set(el, instance); instance.connectedCallback(); return instance; } /** Destroys passed mixin on the element */ static destroy(el, mixin) { const store = el[STORE]; if (!store) return; const instance = store[mixin]; if (!instance) return; instance.disconnectedCallback(); delete store[mixin]; } /** Destroys all mixins on the element and its subtree */ static destroyAll(el) { var _a; const store = el[STORE]; store && Object.keys(store).forEach((name) => ESLMixinRegistry.destroy(el, name)); if (!((_a = el.children) === null || _a === void 0 ? void 0 : _a.length)) return; Array.prototype.forEach.call(el.children, (child) => ESLMixinRegistry.destroyAll(child)); } }