diff --git a/index.js b/index.js index fa263efc..df6cbfde 100644 --- a/index.js +++ b/index.js @@ -1,98 +1,105 @@ +// @ts-check + class TonicTemplate { - constructor(rawText, templateStrings, unsafe) { - this.isTonicTemplate = true; - this.unsafe = unsafe; - this.rawText = rawText; - this.templateStrings = templateStrings; - } - valueOf() { - return this.rawText; - } - toString() { - return this.rawText; + constructor (rawText, templateStrings, unsafe) { + this.isTonicTemplate = true + this.unsafe = unsafe + this.rawText = rawText + this.templateStrings = templateStrings } + + valueOf () { return this.rawText } + toString () { return this.rawText } } -export class Tonic extends window.HTMLElement { - static _tags = ""; - static _refIds = []; - static _data = {}; - static _states = {}; - static _children = {}; - static _reg = {}; - static _stylesheetRegistry = []; - static _index = 0; - static version = "14.1.5"; - static SPREAD = /\.\.\.\s?(__\w+__\w+__)/g; - static ESC = /["&'<>`/]/g; - static AsyncFunctionGenerator = async function* () { - }.constructor; - static AsyncFunction = async function() { - }.constructor; - static MAP = { '"': """, "&": "&", "'": "'", "<": "<", ">": ">", "`": "`", "/": "/" }; - constructor() { - super(); - const state = Tonic._states[super.id]; - delete Tonic._states[super.id]; - this._state = state || {}; - this.preventRenderOnReconnect = false; - this.props = {}; - this.elements = [...this.children]; - this.elements.__children__ = true; - this.nodes = [...this.childNodes]; - this.nodes.__children__ = true; - this._events(); + +class Tonic extends window.HTMLElement { + static _tags = '' + static _refIds = [] + static _data = {} + static _states = {} + static _children = {} + static _reg = {} + static _stylesheetRegistry = [] + static _index = 0 + // eslint-disable-next-line no-undef + static version = VERSION ?? null + static SPREAD = /\.\.\.\s?(__\w+__\w+__)/g + static ESC = /["&'<>`/]/g + static AsyncFunctionGenerator = async function * () {}.constructor + static AsyncFunction = async function () {}.constructor + static MAP = { '"': '"', '&': '&', '\'': ''', '<': '<', '>': '>', '`': '`', '/': '/' } + + constructor () { + super() + const state = Tonic._states[super.id] + delete Tonic._states[super.id] + this._state = state || {} + this.preventRenderOnReconnect = false + this.props = {} + this.elements = [...this.children] + this.elements.__children__ = true + this.nodes = [...this.childNodes] + this.nodes.__children__ = true + this._events() } - get isTonicComponent() { - return true; + + get isTonicComponent () { + return true } - static _createId() { - return `tonic${Tonic._index++}`; + static _createId () { + return `tonic${Tonic._index++}` } + static _normalizeAttrs (o, x = {}) { [...o].forEach(o => (x[o.name] = o.value)) return x } - _checkId() { - const _id = super.id; + + _checkId () { + const _id = super.id if (!_id) { - const html = this.outerHTML.replace(this.innerHTML, "..."); - throw new Error(`Component: ${html} has no id`); + const html = this.outerHTML.replace(this.innerHTML, '...') + throw new Error(`Component: ${html} has no id`) } - return _id; + return _id } - get state() { - return this._checkId(), this._state; + + get state () { + return (this._checkId(), this._state) } - set state(newState) { - this._state = (this._checkId(), newState); + + set state (newState) { + this._state = (this._checkId(), newState) } - _events() { - const hp = Object.getOwnPropertyNames(window.HTMLElement.prototype); + + _events () { + const hp = Object.getOwnPropertyNames(window.HTMLElement.prototype) for (const p of this._props) { - if (hp.indexOf("on" + p) === -1) - continue; - this.addEventListener(p, this); + if (hp.indexOf('on' + p) === -1) continue + this.addEventListener(p, this) } } - _prop(o) { - const id = this._id; - const p = `__${id}__${Tonic._createId()}__`; - Tonic._data[id] = Tonic._data[id] || {}; - Tonic._data[id][p] = o; - return p; + + _prop (o) { + const id = this._id + const p = `__${id}__${Tonic._createId()}__` + Tonic._data[id] = Tonic._data[id] || {} + Tonic._data[id][p] = o + return p } - _placehold(r) { - const id = this._id; - const ref = `placehold:${id}:${Tonic._createId()}__`; - Tonic._children[id] = Tonic._children[id] || {}; - Tonic._children[id][ref] = r; - return ref; + + _placehold (r) { + const id = this._id + const ref = `placehold:${id}:${Tonic._createId()}__` + Tonic._children[id] = Tonic._children[id] || {} + Tonic._children[id][ref] = r + return ref } - static match(el, s) { - if (!el.matches) - el = el.parentElement; - return el.matches(s) ? el : el.closest(s); + + static match (el, s) { + if (!el.matches) el = el.parentElement + return el.matches(s) ? el : el.closest(s) } static getTagName (camelName) { @@ -101,268 +108,292 @@ export class Tonic extends window.HTMLElement { static getPropertyNames (proto) { const props = [] - while (proto && proto !== Tonic.prototype) { - props.push(...Object.getOwnPropertyNames(proto)); - proto = Object.getPrototypeOf(proto); + props.push(...Object.getOwnPropertyNames(proto)) + proto = Object.getPrototypeOf(proto) } - return props; + return props } - static add(c, htmlName) { - const hasValidName = htmlName || c.name && c.name.length > 1; + + static add (c, htmlName) { + const hasValidName = htmlName || (c.name && c.name.length > 1) if (!hasValidName) { - throw Error("Mangling. https://bit.ly/2TkJ6zP"); + throw Error('Mangling. https://bit.ly/2TkJ6zP') } if (!htmlName) htmlName = Tonic.getTagName(c.name) if (!Tonic.ssr && window.customElements.get(htmlName)) { - throw new Error(`Cannot Tonic.add(${c.name}, '${htmlName}') twice`); + throw new Error(`Cannot Tonic.add(${c.name}, '${htmlName}') twice`) } + if (!c.prototype || !c.prototype.isTonicComponent) { - const tmp = { [c.name]: class extends Tonic { - } }[c.name]; - tmp.prototype.render = c; - c = tmp; + const tmp = { [c.name]: class extends Tonic {} }[c.name] + tmp.prototype.render = c + c = tmp } - c.prototype._props = Tonic.getPropertyNames(c.prototype); - Tonic._reg[htmlName] = c; - Tonic._tags = Object.keys(Tonic._reg).join(); - window.customElements.define(htmlName, c); - if (typeof c.stylesheet === "function") { - Tonic.registerStyles(c.stylesheet); + + c.prototype._props = Tonic.getPropertyNames(c.prototype) + + Tonic._reg[htmlName] = c + Tonic._tags = Object.keys(Tonic._reg).join() + window.customElements.define(htmlName, c) + + if (typeof c.stylesheet === 'function') { + Tonic.registerStyles(c.stylesheet) } - return c; + + return c } - static registerStyles(stylesheetFn) { - if (Tonic._stylesheetRegistry.includes(stylesheetFn)) - return; - Tonic._stylesheetRegistry.push(stylesheetFn); - const styleNode = document.createElement("style"); - if (Tonic.nonce) - styleNode.setAttribute("nonce", Tonic.nonce); - styleNode.appendChild(document.createTextNode(stylesheetFn())); - if (document.head) - document.head.appendChild(styleNode); + + static registerStyles (stylesheetFn) { + if (Tonic._stylesheetRegistry.includes(stylesheetFn)) return + Tonic._stylesheetRegistry.push(stylesheetFn) + + const styleNode = document.createElement('style') + if (Tonic.nonce) styleNode.setAttribute('nonce', Tonic.nonce) + styleNode.appendChild(document.createTextNode(stylesheetFn())) + if (document.head) document.head.appendChild(styleNode) } - static escape(s) { - return s.replace(Tonic.ESC, (c) => Tonic.MAP[c]); + + static escape (s) { + return s.replace(Tonic.ESC, c => Tonic.MAP[c]) } - static unsafeRawString(s, templateStrings) { - return new TonicTemplate(s, templateStrings, true); + + static unsafeRawString (s, templateStrings) { + return new TonicTemplate(s, templateStrings, true) } - dispatch(eventName, detail = null) { - const opts = { bubbles: true, detail }; - this.dispatchEvent(new window.CustomEvent(eventName, opts)); + + dispatch (eventName, detail = null) { + const opts = { bubbles: true, detail } + this.dispatchEvent(new window.CustomEvent(eventName, opts)) } - html(strings, ...values) { - const refs = (o) => { - if (o && o.__children__) - return this._placehold(o); - if (o && o.isTonicTemplate) - return o.rawText; + + html (strings, ...values) { + const refs = o => { + if (o && o.__children__) return this._placehold(o) + if (o && o.isTonicTemplate) return o.rawText switch (Object.prototype.toString.call(o)) { - case "[object HTMLCollection]": - case "[object NodeList]": - return this._placehold([...o]); - case "[object Array]": { - if (o.every((x) => x.isTonicTemplate && !x.unsafe)) { - return new TonicTemplate(o.join(""), null, false); + case '[object HTMLCollection]': + case '[object NodeList]': return this._placehold([...o]) + case '[object Array]': + if (o.every(x => x.isTonicTemplate && !x.unsafe)) { + return new TonicTemplate(o.join('\n'), null, false) } - return this._prop(o); - } - case "[object Object]": - case "[object Function]": - return this._prop(o); - case "[object NamedNodeMap]": - return this._prop(Tonic._normalizeAttrs(o)); - case "[object Number]": - return `${o}__float`; - case "[object String]": - return Tonic.escape(o); - case "[object Boolean]": - return `${o}__boolean`; - case "[object Null]": - return `${o}__null`; - case "[object HTMLElement]": - return this._placehold([o]); + return this._prop(o) + case '[object Object]': + case '[object Function]': return this._prop(o) + case '[object NamedNodeMap]': + return this._prop(Tonic._normalizeAttrs(o)) + case '[object Number]': return `${o}__float` + case '[object String]': return Tonic.escape(o) + case '[object Boolean]': return `${o}__boolean` + case '[object Null]': return `${o}__null` + case '[object HTMLElement]': + return this._placehold([o]) } - if (typeof o === "object" && o && o.nodeType === 1 && typeof o.cloneNode === "function") { - return this._placehold([o]); + if ( + typeof o === 'object' && o && o.nodeType === 1 && + typeof o.cloneNode === 'function' + ) { + return this._placehold([o]) } - return o; - }; - const out = []; + return o + } + + const out = [] for (let i = 0; i < strings.length - 1; i++) { - out.push(strings[i], refs(values[i])); + out.push(strings[i], refs(values[i])) } - out.push(strings[strings.length - 1]); - const htmlStr = out.join("").replace(Tonic.SPREAD, (_, p) => { - const o = Tonic._data[p.split("__")[1]][p]; + out.push(strings[strings.length - 1]) + + const htmlStr = out.join('').replace(Tonic.SPREAD, (_, p) => { + const o = Tonic._data[p.split('__')[1]][p] return Object.entries(o).map(([key, value]) => { - const k = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); - if (value === true) - return k; - else if (value) - return `${k}="${Tonic.escape(String(value))}"`; - else - return ""; - }).filter(Boolean).join(" "); - }); - return new TonicTemplate(htmlStr, strings, false); + const k = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + if (value === true) return k + else if (value) return `${k}="${Tonic.escape(String(value))}"` + else return '' + }).filter(Boolean).join(' ') + }) + return new TonicTemplate(htmlStr, strings, false) } - scheduleReRender(oldProps) { - if (this.pendingReRender) - return this.pendingReRender; - this.pendingReRender = new Promise((resolve) => setTimeout(() => { - if (!this.isInDocument(this.shadowRoot || this)) - return; - const p = this._set(this.shadowRoot || this, this.render); - this.pendingReRender = null; + + scheduleReRender (oldProps) { + if (this.pendingReRender) return this.pendingReRender + + this.pendingReRender = new Promise(resolve => setTimeout(() => { + if (!this.isInDocument(this.shadowRoot || this)) return + const p = this._set(this.shadowRoot || this, this.render) + this.pendingReRender = null + if (p && p.then) { return p.then(() => { - this.updated && this.updated(oldProps); - resolve(this); - }); + this.updated && this.updated(oldProps) + resolve(this) + }) } - this.updated && this.updated(oldProps); - resolve(this); - }, 0)); - return this.pendingReRender; + + this.updated && this.updated(oldProps) + resolve(this) + }, 0)) + + return this.pendingReRender } - reRender(o = this.props) { - const oldProps = { ...this.props }; - this.props = typeof o === "function" ? o(oldProps) : o; - return this.scheduleReRender(oldProps); + + reRender (o = this.props) { + const oldProps = { ...this.props } + this.props = typeof o === 'function' ? o(oldProps) : o + return this.scheduleReRender(oldProps) } - handleEvent(e) { - this[e.type](e); + + handleEvent (e) { + this[e.type](e) } - _drainIterator(target, iterator) { + + _drainIterator (target, iterator) { return iterator.next().then((result) => { - this._set(target, null, result.value); - if (result.done) - return; - return this._drainIterator(target, iterator); - }); + this._set(target, null, result.value) + if (result.done) return + return this._drainIterator(target, iterator) + }) } _set (target, render, content = '') { this.willRender && this.willRender() for (const node of target.querySelectorAll(Tonic._tags)) { - if (!node.isTonicComponent) - continue; - const id = node.getAttribute("id"); - if (!id || !Tonic._refIds.includes(id)) - continue; - Tonic._states[id] = node.state; + if (!node.isTonicComponent) continue + + const id = node.getAttribute('id') + if (!id || !Tonic._refIds.includes(id)) continue + Tonic._states[id] = node.state } + if (render instanceof Tonic.AsyncFunction) { - return render.call(this, this.html, this.props).then((content2) => this._apply(target, content2)); + return (render + .call(this, this.html, this.props) + .then(content => this._apply(target, content)) + ) } else if (render instanceof Tonic.AsyncFunctionGenerator) { - return this._drainIterator(target, render.call(this)); + return this._drainIterator(target, render.call(this)) } else if (render === null) { - this._apply(target, content); + this._apply(target, content) } else if (render instanceof Function) { - this._apply(target, render.call(this, this.html, this.props) || ""); + this._apply(target, render.call(this, this.html, this.props) || '') } } - _apply(target, content) { + + _apply (target, content) { if (content && content.isTonicTemplate) { - content = content.rawText; - } else if (typeof content === "string") { - content = Tonic.escape(content); + content = content.rawText + } else if (typeof content === 'string') { + content = Tonic.escape(content) } - if (typeof content === "string") { + + if (typeof content === 'string') { if (this.stylesheet) { - content = `${content}`; + content = `${content}` } - target.innerHTML = content; + + target.innerHTML = content + if (this.styles) { - const styles = this.styles(); - for (const node of target.querySelectorAll("[styles]")) { - for (const s of node.getAttribute("styles").split(/\s+/)) { - Object.assign(node.style, styles[s.trim()]); + const styles = this.styles() + for (const node of target.querySelectorAll('[styles]')) { + for (const s of node.getAttribute('styles').split(/\s+/)) { + Object.assign(node.style, styles[s.trim()]) } } } - const children = Tonic._children[this._id] || {}; + + const children = Tonic._children[this._id] || {} + const walk = (node, fn) => { if (node.nodeType === 3) { - const id = node.textContent.trim(); - if (children[id]) - fn(node, children[id], id); + const id = node.textContent.trim() + if (children[id]) fn(node, children[id], id) } - const childNodes = node.childNodes; - if (!childNodes) - return; + + const childNodes = node.childNodes + if (!childNodes) return + for (let i = 0; i < childNodes.length; i++) { - walk(childNodes[i], fn); + walk(childNodes[i], fn) } - }; - walk(target, (node, children2, id) => { - for (const child of children2) { - node.parentNode.insertBefore(child, node); + } + + walk(target, (node, children, id) => { + for (const child of children) { + node.parentNode.insertBefore(child, node) } - delete Tonic._children[this._id][id]; - node.parentNode.removeChild(node); - }); + delete Tonic._children[this._id][id] + node.parentNode.removeChild(node) + }) } else { - target.innerHTML = ""; - target.appendChild(content.cloneNode(true)); + target.innerHTML = '' + target.appendChild(content.cloneNode(true)) } } - connectedCallback() { - this.root = this.shadowRoot || this; + + connectedCallback () { + this.root = this.shadowRoot || this // here for back compat + if (super.id && !Tonic._refIds.includes(super.id)) { - Tonic._refIds.push(super.id); + Tonic._refIds.push(super.id) } - const cc = (s) => s.replace(/-(.)/g, (_, m) => m.toUpperCase()); + const cc = s => s.replace(/-(.)/g, (_, m) => m.toUpperCase()) + for (const { name: _name, value } of this.attributes) { - const name = cc(_name); - const p = this.props[name] = value; + const name = cc(_name) + const p = this.props[name] = value + if (/__\w+__\w+__/.test(p)) { - const { 1: root } = p.split("__"); - this.props[name] = Tonic._data[root][p]; + const { 1: root } = p.split('__') + this.props[name] = Tonic._data[root][p] } else if (/\d+__float/.test(p)) { - this.props[name] = parseFloat(p, 10); - } else if (p === "null__null") { - this.props[name] = null; + this.props[name] = parseFloat(p, 10) + } else if (p === 'null__null') { + this.props[name] = null } else if (/\w+__boolean/.test(p)) { - this.props[name] = p.includes("true"); + this.props[name] = p.includes('true') } else if (/placehold:\w+:\w+__/.test(p)) { - const { 1: root } = p.split(":"); - this.props[name] = Tonic._children[root][p][0]; + const { 1: root } = p.split(':') + this.props[name] = Tonic._children[root][p][0] } } + this.props = Object.assign( this.defaults ? this.defaults() : {}, this.props - ); - this._id = this._id || Tonic._createId(); - this.willConnect && this.willConnect(); - if (!this.isInDocument(this.root)) - return; + ) + + this._id = this._id || Tonic._createId() + + this.willConnect && this.willConnect() + + if (!this.isInDocument(this.root)) return if (!this.preventRenderOnReconnect) { if (!this._source) { - this._source = this.innerHTML; + this._source = this.innerHTML } else { - this.innerHTML = this._source; + this.innerHTML = this._source } - const p = this._set(this.root, this.render); - if (p && p.then) - return p.then(() => this.connected && this.connected()); + const p = this._set(this.root, this.render) + if (p && p.then) return p.then(() => this.connected && this.connected()) } - this.connected && this.connected(); + + this.connected && this.connected() } - isInDocument(target) { - const root = target.getRootNode(); - return root === document || root.toString() === "[object ShadowRoot]"; + + isInDocument (target) { + const root = target.getRootNode() + return root === document || root.toString() === '[object ShadowRoot]' } - disconnectedCallback() { - this.disconnected && this.disconnected(); - delete Tonic._data[this._id]; - delete Tonic._children[this._id]; + + disconnectedCallback () { + this.disconnected && this.disconnected() + delete Tonic._data[this._id] + delete Tonic._children[this._id] } } -export default Tonic; +export default Tonic