UNPKG

@ou-imdt/utils

Version:

Utility library for interactive media development

94 lines (74 loc) 3.24 kB
import { defaultState } from '../Base.js'; export const delegatedAttributePrefix = Symbol('delegatedAttributePrefix'); export const delegatedAttributes = Symbol('delegatedAttributes'); export const childAttributes = Symbol('childAttributes'); // better name? /** * Passes attributes to child components* where the attribute is observed on the child and * unobserved on the parent. Child components must be declared within static components field. * Removes attributes prefixed with [delegatedAttributePrefix] and stores/exposes as name/value * pairs within [delegatedAttributes] hash for use/reflection internally. * @mixin */ export default (superClass) => class DelegateAttributesMixin extends superClass { static get [defaultState]() { return { ...super[defaultState], [delegatedAttributes]: {}, [childAttributes]: new Set() }; } static [delegatedAttributePrefix] = 'delegate-'; get unobservedAttributes() { return this.getAttributeNames().filter((name) => !this.constructor.observedAttributes.includes(name)); } #mutationObserver = new MutationObserver((mutationList) => { for (const mutation of mutationList) { const { attributeName, oldValue } = mutation; const newValue = this.getAttribute(attributeName); this.#delegateAttribute(attributeName, newValue, oldValue); } }); connectedCallback() { super.connectedCallback(); for (const name of this.getAttributeNames()) { this.#delegateAttribute(name, this.getAttribute(name)); } this.#mutationObserver.observe(this, { attributes: true, attributeOldValue: true }); } disconnectedCallback() { super.disconnectedCallback(); this[delegatedAttributes] = {}; this[childAttributes].clear(); this.#mutationObserver.disconnect(); } #delegateAttribute(name, newValue, oldValue = null) { if (newValue === oldValue) return; const prefix = this.constructor[delegatedAttributePrefix]; if (name.startsWith(prefix)) { const key = name.replace(prefix, ''); this[delegatedAttributes] = { ...this[delegatedAttributes], [key]: newValue }; // new object in case shallow reactivity return; } if (this.constructor.components === null) return false; if (!this.unobservedAttributes.includes(name)) return false; const isObserved = Object.values(this.constructor.components).map(component => { if (!component.observedAttributes.includes(name)) return false; // oldValue ??= component[defaultState][name]; // won't work as attr name requestAnimationFrame(() => { const elements = [ ...this.querySelectorAll(component.tag), // keep these sync?? ...this.shadowRoot.querySelectorAll(component.tag) ]; elements.forEach(el => { // console.log(name, newValue, oldValue, el.getAttribute(name)); if (oldValue !== null && el.getAttribute(name) !== oldValue) return; // TODO include check for initial value instead to prevent overwriting predefined values? el.setAttribute(name, newValue); }); }); return true; }); if (!isObserved.includes(true)) return; this[childAttributes].add(name); } }