export class El extends HTMLElement {
static els = {}
static stash = {}
static tags = {}
static keys = new WeakMap
static styles = {}
static deps = {}
static Raw = class Raw extends String {}
constructor() {
super()
this._id = `${this.tagName}:${this.getAttribute('key') || Math.random().toString(36).slice(2)}`
El.style = El.style || El.importStyle()
this.$html = Object.assign(this.$html.bind(this), { raw: x => new El.Raw(x) })
this._cache = { d: {}, clear: _ => this._cache.d = {} }
this._memoize()
this.$update = this.$update.bind(this)
}
connectedCallback() {
El._contextId = this._id
this._unstash()
this.created && !this._created && this.created()
this._created = true
El.els[this._id] = this
this._update()
this.mounted && this.mounted()
if (El.tags[this.tagName] && !this.getAttribute('key'))
console.warn(`Each ${this.tagName} should have a unique \`key\` attribute`)
El.tags[this.tagName] = true
}
disconnectedCallback() {
this.unmounted && this.unmounted()
}
_memoize() {
const descriptors = Object.getOwnPropertyDescriptors(this.constructor.prototype)
for (const [key, d] of Object.entries(descriptors).filter(x => x[1].get))
Object.defineProperty(this.constructor.prototype, key, {
get() {
return (key in this._cache.d) ? this._cache.d[key] : (this._cache.d[key] = d.get.call(this))
}
})
this.constructor.prototype._memoize = new Function;
}
$update() {
this._queued = this._queued || requestAnimationFrame(_ => this._update() || delete this._queued)
}
_update() {
El._contextId = this._id
this._cache.clear();
this._unstash()
const html = this.render && this.render(this.$html);
const shadow = this.shadowRoot || this.attachShadow({ mode: 'open' })
El.styles[this.tagName] = El.styles[this.tagName] ||
``
El.morph(shadow, document.createRange().createContextualFragment(El.styles[this.tagName] + html))
this._unstash()
El._contextId = null
}
_unstash() {
const camel = s => s.replace(/-\w/g, c => c[1].toUpperCase())
const _contextId = El._contextId;
El._contextId = this._id;
for (const el of [...(this.shadowRoot || this).querySelectorAll('*'), this])
for (const attr of el.attributes)
if (attr.value in El.stash) el[camel(attr.name)] = El.stash[attr.value]
else if (attr.name in el.__proto__) {}
else try { el[camel(attr.name)] = attr.value } catch {}
El._contextId = _contextId
}
get $refs() {
return new Proxy({}, { get: (obj, key) => this.shadowRoot.querySelector(`[ref="${key}"]`) });
}
$watch(_, fn) {
if (!El.dep._path) return;
El.deps[El.dep._path] = El.deps[El.dep._path] || {};
El.deps[El.dep._path][Math.random()] = fn;
El.dep._path = null;
}
$observable() {
return El.observable(...arguments);
}
$nextTick() {
return El.nextTick();
}
$html(strings, ...vals) {
for (const [i] of strings.entries()) {
if ((typeof vals[i]).match(/object|function/) && strings[i].endsWith('=')) {
vals[i] = typeof vals[i] == 'function' ? vals[i].bind(this) : vals[i]
const key = El.keys.get(vals[i].__target__ || vals[i]) || 'el:' + Math.random().toString(36).slice(2)
El.keys.set(vals[i].__target__ || vals[i], key)
El.stash[key] = vals[i]
vals[i] = JSON.stringify(key)
}
else if (strings[i].endsWith('=')) vals[i] = JSON.stringify(vals[i])
else if (vals[i] instanceof Array) vals[i] = vals[i].join('')
else vals[i] = El.escape(vals[i])
}
return new El.Raw(strings.map((s, i) => [s, vals[i]].join``).join``)
}
static importStyle() {
let src = ''
for (const el of document.querySelectorAll('style, link[rel="stylesheet"]'))
src += el.tagName == 'STYLE' ? el.innerHTML : `\n@import url(${el.href});\n`
return src;
}
static notify(path) {
for (const id in El.deps[path] || {}) setTimeout(_ => El.deps[path][id]())
}
static dep(path) {
El.dep._path = !El._contextId && path
if (!El._contextId) return true
const contextId = El._contextId
El.deps[path] = El.deps[path] || {}
return El.deps[path][El._contextId] = _ => El.els[contextId].$update()
}
static observable(x, path = Math.random().toString(36).slice(2)) {
if ((typeof x != 'object' || x === null) && El.dep(path)) return x
return new Proxy(x, {
set(x, key) {
return El.notify(path + '/' + key) || Reflect.set(...arguments);
},
get(x, key) {
return x.__target__ ? x[key]
: typeof key == "symbol" ? Reflect.get(...arguments)
: (key in x.constructor.prototype && El.dep(path + '/' + key)) ? x[key]
: (key == '__target__') ? x
: El.observable(x[key], path + '/' + key);
}
});
}
static morph(l, r) {
let ls = 0, rs = 0, le = l.childNodes.length, re = r.childNodes.length
const lc = [...l.childNodes], rc = [...r.childNodes]
const content = e => e.nodeType == 3 ? e.textContent : e.nodeType == 1 ? e.outerHTML : ''
const key = e => e.nodeType == 1 && customElements.get(e.tagName.toLowerCase()) && e.getAttribute('key') || NaN
for (const a of r.attributes || [])
if (l.getAttribute(a.name) != a.value) {
l.setAttribute(a.name, a.value)
if (l.constructor.prototype.hasOwnProperty(a.name) && typeof l[a.name] == 'boolean') l[a.name] = true
l.$update && l.$update()
}
for (const a of l.attributes || [])
if (!r.hasAttribute(a.name)) {
l.removeAttribute(a.name)
if (l.constructor.prototype.hasOwnProperty(a.name) && typeof l[a.name] == 'boolean') l[a.name] = false
}
while (ls < le || rs < re)
if (ls == le) l.insertBefore(lc.find(l => key(l) == key(rc[rs])) || rc[rs], lc[ls]) && rs++
else if (rs == re) l.removeChild(lc[ls++])
else if (content(lc[ls]) == content(rc[rs])) ls++ & rs++
else if (content(lc[le - 1]) == content(rc[re - 1])) le-- & re--
else if (lc[ls] && rc[rs].children && lc[ls].tagName == rc[rs].tagName) El.morph(lc[ls++], rc[rs++])
else lc[ls++].replaceWith(rc[rs++])
}
static nextTick(f) {
return new Promise(r => setTimeout(_ => requestAnimationFrame(_ => { f && f(); r() })))
}
static zcss(...args) {
let lines = [], stack = [], open, opened, close
const src = args.join('').replace(/,\n/gs, ',')
for (let line of src.split(/\n/)) {
line = line.replace(/(.+,.+){/, ":is($1){")
if (line.match(/^\s*@[msdk].*\{/))
opened = open = close = (opened && !lines.push('}')) || lines.push(line) & 0
else if (line.match(/\{\s*$/)) open = stack.push(line.replace('{','').trim()) | 1
else if (line.match(/\s*\}\s*$/)) close = (!stack.pop() && lines.push('}')) | 1
else {
if (!line.trim()) continue
if (opened && (open || close)) opened = close = lines.push('}') & 0
if (open || close) opened = !(open = lines.push(stack.join` `.replace(/ &/g, '') + '{') & 0)
lines.push(line)
}
}
return close && lines.push('}') && lines.join('\n')
}
static escape(v) {
return v instanceof El.Raw ? v : v === 0 ? v : String(v || '').replace(/[<>'"]/g, c => `${c.charCodeAt(0)};`)
}
}