metamorphosis
Version:
A css variable management library that helps create and organize variables into easily configurable themes.
163 lines (162 loc) • 4.52 kB
JavaScript
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
};