UNPKG

nexwidget

Version:

An ESNext Web Component library.

267 lines (266 loc) 8.83 kB
import { noChange, render } from 'lit-html/lit-html.js'; import { Debouncer } from 'nexbounce/nexbounce.js'; export * from 'lit-html/lit-html.js'; export * from './lib/add-pending-task.js'; export * from './lib/css-tag.js'; export class Nexwidget extends HTMLElement { static #reactives = new WeakMap([[Nexwidget, new Set()]]); static #attributes = new WeakMap([[Nexwidget, new Map()]]); static get styles() { return []; } static get reactives() { Nexwidget.#ensureReactives(this); return [...Nexwidget.#reactives.get(this)]; } static get attributes() { Nexwidget.#ensureAttributes(this); return [...Nexwidget.#attributes.get(this).keys()]; } static get propertyKeysForObservedAttributes() { const reactives = new Set(this.reactives); return this.attributes.filter((key) => reactives.has(key)); } static get observedAttributes() { return this.propertyKeysForObservedAttributes.map(Nexwidget.#camelToKebab); } static #camelToKebab(name) { return name .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .replace(/([A-Z])([A-Z])(?=[a-z])/g, '$1-$2') .toLowerCase(); } static #ensureReactives(Class) { const isEnsured = Nexwidget.#reactives.has(Class); if (!isEnsured) { const SuperClass = Reflect.getPrototypeOf(Class); const isSuperClassEnsured = Nexwidget.#reactives.has(SuperClass); if (!isSuperClassEnsured && SuperClass !== Nexwidget) Nexwidget.#ensureReactives(SuperClass); Nexwidget.#reactives.set(Class, Nexwidget.#reactives.get(SuperClass)); } } static #ensureAttributes(Class) { const isEnsured = Nexwidget.#attributes.has(Class); if (!isEnsured) { const SuperClass = Reflect.getPrototypeOf(Class); const isSuperClassEnsured = Nexwidget.#attributes.has(SuperClass); if (!isSuperClassEnsured && SuperClass !== Nexwidget) Nexwidget.#ensureAttributes(SuperClass); Nexwidget.#attributes.set(Class, Nexwidget.#attributes.get(SuperClass)); } } static registerAs(tagName) { customElements.define(tagName, this); } static createReactives(reactives) { Nexwidget.#ensureReactives(this); const reactivesSet = new Set(reactives); reactivesSet.forEach((key) => { const descriptor = Reflect.getOwnPropertyDescriptor(this.prototype, key); const internalKey = !descriptor?.get ? Symbol(key) : null; Nexwidget.#reactives.set( this, new Set([...Nexwidget.#reactives.get(this), key]), ); Reflect.defineProperty(this.prototype, key, { configurable: true, enumerable: true, get() { if (internalKey !== null) return this[internalKey]; else return descriptor.get.call(this); }, set(value) { const prevValue = this[key]; descriptor?.set?.call(this, value); if (internalKey && prevValue !== value) { this[internalKey] = value; this.#render(); } }, }); }); } static createAttributes(attributes) { Nexwidget.#ensureAttributes(this); const attributesMap = new Map( attributes.map(({ key, type }) => [key, type]), ); attributesMap.forEach((type, key) => { const descriptor = Reflect.getOwnPropertyDescriptor(this.prototype, key); Nexwidget.#attributes.set( this, new Map([...Nexwidget.#attributes.get(this), [key, type]]), ); Reflect.defineProperty(this.prototype, key, { configurable: true, enumerable: true, get() { descriptor?.get?.call(this); return this.#getPropertyValueFromAttribute(key); }, set(value) { descriptor?.set?.call(this, value); this.#setAttributeFromProperty(key, value); }, }); }); } #renderRoot = this.attachShadow({ mode: 'open' }); #renderDebouncer = new Debouncer(); #isRenderEnabled = false; #isMounted = false; #rootPart; #removedController; #unmountedController; #slotObserver = new MutationObserver(this.slotChangedCallback.bind(this)); #animation; get removedSignal() { return this.#removedController?.signal; } get unmountedSignal() { return this.#unmountedController?.signal; } get template() { return noChange; } get mountAnimation() { return this.updateOrSlotChangeAnimation; } get updateOrSlotChangeAnimation() { return null; } get usesNexwidget() { return true; } #adoptStyles() { const { styles } = this.constructor; this.#renderRoot.adoptedStyleSheets = [...styles]; } //@ts-ignore #getPropertyValueFromAttribute(key) { const type = Nexwidget.#attributes.get(this.constructor).get(key); const attributeKey = Nexwidget.#camelToKebab(key); switch (type) { case 'boolean': return this.hasAttribute(attributeKey); case 'string': case 'number': const typeConstructorName = type.charAt(0).toUpperCase() + type.slice(1); const typeConstructor = globalThis[typeConstructorName]; return this.hasAttribute(attributeKey) ? typeConstructor(this.getAttribute(attributeKey)) : null; default: throw new RangeError(`Invalid type for attribute.`); } } //@ts-ignore #setAttributeFromProperty(key, value) { const type = Nexwidget.#attributes.get(this.constructor).get(key); const attributeKey = Nexwidget.#camelToKebab(key); if (value === undefined) throw new TypeError(`Attribute value cannot be undefined.`); else if (value !== null && typeof value !== type) throw new TypeError(`Attribute value doesn't match its type.`); else switch (type) { case 'boolean': if (value) this.setAttribute(attributeKey, ''); else if (value === null) throw new TypeError(`Boolean attribute cannot be null.`); else this.removeAttribute(attributeKey); break; case 'string': case 'number': if (value === null) this.removeAttribute(attributeKey); else this.setAttribute(attributeKey, String(value)); break; default: throw new RangeError(`Invalid type for attribute.`); } } #render() { this.#renderDebouncer.enqueue(() => { if (this.#isRenderEnabled) { this.#rootPart = render(this.template, this.#renderRoot, { host: this, }); requestAnimationFrame(() => { this.updatedCallback(); if (!this.#isMounted) { this.slotChangedCallback(); this.#isMounted = true; this.mountedCallback(); this.#slotObserver.observe(this, { subtree: true, characterData: true, childList: true, }); } }); } }); } #cleanupRender() { if (!this.#isRenderEnabled) { this.#slotObserver.disconnect(); this.#isMounted = false; this.unmountedCallback(); } } getCSSProperty(key) { return getComputedStyle(this).getPropertyValue(key); } attributeChangedCallback(_key, oldValue, newValue) { if (oldValue !== newValue) this.#render(); } connectedCallback() { this.#adoptStyles(); this.addedCallback(); this.#isRenderEnabled = true; this.#render(); } disconnectedCallback() { this.removedCallback(); this.#isRenderEnabled = false; this.#cleanupRender(); } addedCallback() { this.#removedController = new AbortController(); } updatedCallback() { this.#animation?.cancel(); if (this.updateOrSlotChangeAnimation !== null) this.#animation = this.animate( this.updateOrSlotChangeAnimation.keyframes, this.updateOrSlotChangeAnimation.options, ); } slotChangedCallback() { this.#animation?.cancel(); if (this.updateOrSlotChangeAnimation !== null) this.#animation = this.animate( this.updateOrSlotChangeAnimation.keyframes, this.updateOrSlotChangeAnimation.options, ); } mountedCallback() { this.#rootPart?.setConnected(true); this.#animation?.cancel(); if (this.mountAnimation !== null) this.#animation = this.animate( this.mountAnimation.keyframes, this.mountAnimation.options, ); this.#unmountedController = new AbortController(); } removedCallback() { this.#removedController?.abort(); } unmountedCallback() { this.#rootPart?.setConnected(false); this.#unmountedController?.abort(); } }