UNPKG

metamorphosis

Version:

A css variable management library that helps create and organize variables into easily configurable themes.

163 lines (162 loc) 4.52 kB
import { keys } from "@alanscodelog/utils/keys"; import { Base } from "./Base.js"; import { InterpolatedVars } from "./InterpolatedVars.js"; import { escapeKey } from "./utils.js"; class Theme extends Base { ready = false; els = []; css = {}; value = {}; options = { /** For replacing invalid css variable key characters. */ escapeChar: "-" }; _listeners = { change: [] }; constructor(value, opts = {}) { super(); this.add(value); this.setOpts(opts); this.recompute(false); this.ready = true; } setOpts(value = {}) { this.options = { ...this.options, ...value }; if (!this.ready) return; this.notify(); } add(value) { for (const key of keys(value)) { this._add(key, value[key]); } } _add(key, value) { if (this.value[key]) throw new Error(`Key ${key} already exists in theme. Use set to change the value.`); if (this.ready) { this.value[key]?.removeDep(this); } this.value[key] = value; value.addDep(this); if (this.ready) { this.notify(); } } remove(key) { if (!this.value[key]) return; if (this.ready) { this.value[key]?.removeDep(this); } const value = this.value[key]; this._generateCss(this.css, key, this.options.escapeChar, value, { remove: true }); delete this.value[key]; if (this.ready) { this.notify(); } } set(key, value) { if (this.ready) { this.value[key]?.removeDep(this); } this.value[key] = value; this._generateCss(this.css, key, this.options.escapeChar, value); value.addDep(this); if (this.ready) { this.notify({ recompute: false }); } } notify({ recompute = true } = {}) { if (!this.ready) return; if (recompute) this.recompute(false); for (const listener of this._listeners.change) { listener(); } for (const el of this.els) { this._lastPropertiesSet = Theme.setElVariables(el, this.css, this._lastPropertiesSet); } } on(type, cb) { this._listeners[type].push(cb); } off(type, cb) { const i = this._listeners[type].findIndex(cb); if (i > -1) { this._listeners[type].splice(i, 1); } } _generateCss(res, key, sep, value, { remove = false } = {}) { if (value instanceof InterpolatedVars) { for (const k of Object.keys(value.interpolated)) { if (remove) { delete res[`--${escapeKey(k, sep)}`]; } else { res[`--${escapeKey(k, sep)}`] = value.interpolated[k]; } } } else { if (remove) { delete res[`--${escapeKey(key, sep)}`]; } else { res[`--${escapeKey(key, sep)}`] = value.css; } } } /** * The theme can force dependencies to recompute. * * This should not be needed unless you want to recompute based of some external state. * * Please file a bug report otherwise. */ recompute(force = true) { const res = {}; for (const [key, val] of Object.entries(this.value)) { if (force) { val.recompute(); } this._generateCss(res, key, this.options.escapeChar, val); } this.css = res; } _lastPropertiesSet = []; // todo move to utils? /** * Set css variables on an element. * * Careful that the css properties are prefixed with `--`, otherwise they might conflict with other style properties. * * Can be passed a list of already set properties to remove. Returns a list of properties that were set. */ static setElVariables(el, css, lastPropertiesSet = []) { for (const prop of lastPropertiesSet) { el.style.removeProperty(prop); } const newLastPropertiesSet = []; for (const key of keys(css)) { el.style.setProperty(key, css[key]); newLastPropertiesSet.push(key); } return newLastPropertiesSet; } /** * Attach to an element and automatically set and update the theme's properties on it. * * If no element is passed, attaches to `document.documentElement`. */ attach(el = document.documentElement) { this.els.push(el); this._lastPropertiesSet = Theme.setElVariables(el, this.css, this._lastPropertiesSet); } detach(el = document.documentElement) { const existing = this.els.indexOf(el); if (existing >= 0) { this.els.splice(existing, 1); for (const prop of this._lastPropertiesSet) { el.style.removeProperty(prop); } } else { console.warn("Was not attached to element:", el); } } } export { Theme };