UNPKG

@mundorum/oid

Version:

Web components based on the Digital Content Component (DCC) model for the Mundorum space.

1,620 lines 50.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const html = (strings, ...values) => String.raw({ raw: strings }, ...values); const css = (strings, ...values) => String.raw({ raw: strings }, ...values); class Bus { constructor() { this._listeners = {}; this._listenersRgx = []; this._providers = {}; this._pendingCnx = {}; } /* Message-oriented communication ********************************/ subscribe(subscribed, handler2) { if (subscribed != null) { let topics = {}; if (typeof subscribed === "string" && handler2 != null) topics[subscribed] = handler2; else if (typeof subscribed === "object") topics = subscribed; const listenersRgx = this._listenersRgx.slice(); const listeners = { ...this._listeners }; for (const tp in topics) { if (topics[tp] != null) { if (tp.includes("+") || tp.includes("#")) listenersRgx.push([Bus._convertRegExp(tp), topics[tp], tp]); else { if (listeners[tp] == null) listeners[tp] = []; else listeners[tp] = listeners[tp].slice(); listeners[tp].push(topics[tp]); } } } this._listenersRgx = listenersRgx; this._listeners = listeners; } } unsubscribe(subscribed) { if (subscribed != null) { let topics = {}; if (typeof subscribed === "string" && handler != null) topics[subscribed] = handler; else if (typeof subscribed === "object") topics = subscribed; const listenersRgx = this._listenersRgx.slice(); const listeners = { ...this._listeners }; for (const tp in topics) { if (tp.includes("+") || tp.includes("#")) { for (const l in listenersRgx) { if (listenersRgx[l][1] === topics[tp] && listenersRgx[l][2] == tp) { listenersRgx.splice(l, 1); break; } } } else if (listeners[tp] != null) { for (const l in listeners[tp]) { if (listeners[tp][l] === topics[tp]) { listeners[tp] = listeners[tp].toSplice(l, 1); break; } } } } this._listenersRgx = listenersRgx; this._listeners = listeners; } } async publish(topic, message) { if (this._listeners[topic] != null) for (const handler2 of this._listeners[topic]) handler2(topic, message); const listenersRgx = this._listenersRgx; for (const l of listenersRgx) { const match = l[0].exec(topic); if (match != null && match[0] === topic) l[1](topic, message); } } /* Message analysis services *************************/ static _convertRegExp(filter) { return new RegExp(filter.replace(/\//g, "\\/").replace(/\+/g, "[^/]+").replace(/#/g, ".+")); } /* Connection-oriented communication ***********************************/ /* * Components declare provided services. Each interface defines a type of * service. The same component can have several interfaces/services: * cInterface: interface provided by the component * id: unique id of the component instance that offers the service * provider: the component or component subobject that implements * the interface/service */ provide(cInterface, id, provider) { let status = false; if (id != null && cInterface != null && provider != null) { const key = cInterface + "#" + id; if (this._providers[key] == null) { status = true; this._providers[key] = provider; if (this._pendingCnx[key] != null) { for (let c of this._pendingCnx[key]) c.connectionReady(cInterface, id, provider); delete this._pendingCnx[key]; } } } return status; } /* * Removes a provided service (usually, when the component is destroyed) */ withhold(cInterface, id) { let status = false; if (id != null && cInterface != null) { const key = cInterface + "#" + id; if (this._providers[key]) { status = true; delete this._providers[key]; } } return status; } /* * Connects a component to another one based on the id and a provided service. * id: id of the component that offers the service * cInterface: label related to the provided interface * callback: component that will be notified as soon as the interface is * connected */ connect(cInterface, id, callback) { let status = false; if (id != null && cInterface != null && callback != null) { const key = `${cInterface}#${id}`; if (this._providers[key]) callback.connectionReady(cInterface, id, this._providers[key]); else if (this._pendingCnx[key]) this._pendingCnx[key].push(callback); else this._pendingCnx[key] = [callback]; status = true; } return status; } /* * Triggers a interface defined by an id and component, sending an optional * message to it. */ async invoke(cInterface, id, notice, message) { const key = `${cInterface}#${id}`; if (this._providers[key] != null) return await this._providers[key].handleInvoke(cInterface, notice, message); else return null; } } Bus.i = new Bus(); const sphereSetup = { id: "default", bus: Bus.i, stylesheets: "<auto>", stydefault: ["stylesheets:oid.min.css"], assets: "/assets/" }; class Sphere { constructor(id, bus, stylesheets, stydefault, assets) { this._id = id || null; this._bus = bus ? bus : Sphere.i.bus; this._stydefault = stydefault ? stydefault : Sphere.i.stydefault; this._stylesheets = stylesheets ? stylesheets : Sphere.i.stylesheets; if (this._stylesheets === "<auto>" && this._stydefault) { const styleName = this._stydefault[0].split(/[:\/]/).pop(); const links = document.querySelectorAll('link[rel="stylesheet"]'); for (const link of links) { if (link.href && link.href.endsWith(styleName)) { this._stylesheets = link.href.slice(0, -styleName.length); break; } } } this._assets = assets ? assets : Sphere.i.assets; } static create(id, bus, stylesheets, stydefault, assets) { if (id != null && Sphere._spheres[id] != null) throw new Error(`Sphere with id ${id} already exists`); const sphere = new Sphere(id, bus, stylesheets, stydefault, assets); if (id != null) Sphere._spheres[id] = sphere; return sphere; } static get(id) { return id ? Sphere._spheres[id] || null : null; } get id() { return this._id; } get bus() { return this._bus; } set stylesheets(newValue) { this._stylesheets = newValue; } get stylesheets() { return this._stylesheets; } set stydefault(newValue) { this._stydefault = newValue; } get stydefault() { return this._stydefault; } set assets(newValue) { this._assets = newValue; } get assets() { return this._assets; } } Sphere._spheres = {}; Sphere.i = Sphere.create( sphereSetup.id, sphereSetup.bus, sphereSetup.stylesheets, sphereSetup.stydefault, sphereSetup.assets ); class OidSphere extends HTMLElement { connectedCallback() { if (this.hasAttribute("global")) { this._sphere = Sphere.i; if (this.hasAttribute("stylesheets")) Sphere.i.stydefault = this.getAttribute("stylesheets"); if (this.hasAttribute("stydefault")) Sphere.i.stydefault = this.getAttribute("stydefault"); if (this.hasAttribute("assets")) Sphere.i.assets = this.getAttribute("assets"); } else { const id = this.getAttribute("id"); if (Sphere.get(id)) this._sphere = Sphere.get(id); else this._sphere = Sphere.create( this.getAttribute("id"), new Bus(), this.getAttribute("stylesheets"), this.getAttribute("stydefault"), this.getAttribute("assets") ); } } get id() { return this._sphere.id; } get stylesheets() { return this._sphere.stylesheets; } get stydefault() { return this._sphere.stydefault; } get assets() { return this._sphere.assets; } get sphere() { return this._sphere; } } OidSphere.elementTag = "oid-sphere"; customElements.define(OidSphere.elementTag, OidSphere); class Primitive extends HTMLElement { constructor() { super(); this._sphere = null; } get _bus() { return this._sphere == null ? null : this._sphere.bus; } connectedCallback() { let ag = this._findAggregator(OidSphere); if (ag != null) this._sphere = ag.sphere; else this._sphere = Sphere.i; } _findAggregator(agClass) { let parent = this.parentNode != null ? this.parentNode : this instanceof DocumentFragment ? this.host : null; while (parent != null && !(parent instanceof agClass)) parent = parent.parentNode != null ? parent.parentNode : parent instanceof DocumentFragment ? parent.host : null; return parent; } /* * Bus Proxy */ _subscribe(subscribed, handler2) { if (this._bus != null) this._bus.subscribe(subscribed, handler2); } _unsubscribe(subscribed, handler2) { this._bus.unsubscribe(subscribed, handler2); } async _publish(topic, message) { await this._bus.publish(topic, message); } _provide(cInterface, id, provider) { this._bus.provide(cInterface, id, provider); } _withhold(cInterface, id) { this._bus.withhold(cInterface, id); } _connect(cInterface, id, callback) { this._bus.connect(cInterface, id, callback); } async _invoke(cInterface, id, notice, message) { return await this._bus.invoke(cInterface, id, notice, message); } } class OidBase extends Primitive { constructor() { super(); this._mapTopicNotice = {}; this._rgxTopicNotice = []; this._mapNoticeTopic = {}; this._receiveHandler = {}; this._provideHandler = {}; this._connected = {}; this._convertNotice = this._convertNotice.bind(this); this.handleNotice = this.handleNotice.bind(this); } async connectedCallback() { super.connectedCallback(); await this._initialize(); } disconnectedCallback() { this._finalize(); } async _initialize() { const spec = this.constructor.spec; if (spec) { this._buildHandlers(this._receiveHandler, spec.receive); this._buildProviders(); this._buildProvidersHandlers(); this._buildEventDispatchers(spec.dispatcher); } if (spec && spec.properties) { for (const [prop, def] of Object.entries(spec.properties)) if (def.default != null && !this.hasAttribute(prop)) this[prop] = def.default; } if (this.hasAttribute("custom")) this._custom = await Oid.getCustom(spec.id, this.getAttribute("custom")); if (this.hasAttribute("publish")) this._publishNoticeTopic(this.getAttribute("publish")); if (this.hasAttribute("subscribe")) this._subscribeTopicNotice(this.getAttribute("subscribe")); if (this.hasAttribute("connect")) this._connectInterface(this.getAttribute("connect")); } _buildProviders() { const spec = this.constructor.spec; if (spec.provide != null && this.id) for (const p in spec.provide) this._provide(p, this.id, this); } _buildProvidersHandlers() { const spec = this.constructor.spec; if (spec.provide != null) for (const p in spec.provide) { this._buildHandlers( this._provideHandler, spec.provide[p].operations, p ); } } _removeProviders() { const spec = this.constructor.spec; if (spec.provide != null && this.id) for (const p in spec.provide) this._withhold(p, this.id); } _buildHandlers(handlerSet, handlersSpec, cInterface) { if (handlersSpec != null) { const prefix = cInterface == null ? "" : cInterface + "."; if (Array.isArray(handlersSpec)) { for (const notice of handlersSpec) if (handlerSet[prefix + notice] == null) handlerSet[prefix + notice] = this["handle" + notice[0].toUpperCase() + notice.slice(1)].bind(this); } else { for (const [notice, noticeSpec] of Object.entries(handlersSpec)) { if (handlerSet[prefix + notice] == null) { const meth = typeof noticeSpec === "string" ? noticeSpec : noticeSpec.handler != null ? noticeSpec.handler : "handle" + notice[0].toUpperCase() + notice.slice(1); handlerSet[prefix + notice] = this[meth].bind(this); } } } } } _buildEventDispatchers(dispatcherTempl) { if (dispatcherTempl) { this._dispatcher = []; for (const [atr, event, dispatch] of dispatcherTempl) this._dispatcher.push([atr, event, dispatch.bind(this)]); } } _finalize() { this._removeProviders(); for (const topic in this._mapTopicNotice) if (this._mapTopicNotice[topic] != topic) this._unsubscribe(topic, this._convertNotice); else this._unsubscribe(topic, this.handleNotice); } // call setter every time an observed attribute changes attributeChangedCallback(name, oldValue, newValue) { const jsName = name.replace( /-([a-z])/g, (match, letter) => letter.toUpperCase() ); this[jsName] = newValue; } static get observedAttributes() { return ["id"]; } get id() { return this._id; } set id(newValue) { if (this._id != null && this._bus != null) this._removeProviders(); this._id = newValue; if (this._bus != null) this._buildProviders(); } get publish() { return this.getAttribute("publish"); } /* set publish (newValue) { this._publishProp = newValue if (this._bus != null) this._publishNoticeTopic(newValue) } */ get subscribe() { return this.getAttribute("subscribe"); } /* set subscribe (newValue) { this._subscribeProp = newValue if (this._bus != null) this._subscribeTopicNotice(newValue) } */ get connect() { return this.getAttribute("connect"); } /* set connect (newValue) { this._connectProp = newValue if (this._bus != null) this._connectInterface(newValue) } */ handleGet(notice, message) { if (message.property != null) return this[message.property]; else return null; } handleSet(notice, message) { if (message.property != null && message.value != null) this[message.property] = message.value; } _subscribeTopicNotice(topicNotice) { const tpnts = topicNotice.split(";"); for (const tn of tpnts) { const parts = tn.split("~"); if (parts.length > 1) { const topic = parts[0].trim(); if (topic.includes("+") || topic.includes("#")) this._rgxTopicNotice.push( [Bus._convertRegExp(topic), parts[1].trim(), topic] ); else this._mapTopicNotice[topic] = parts[1].trim(); this._subscribe(topic, this._convertNotice); } else { const topic = tn.trim(); this._mapTopicNotice[topic] = topic; this._subscribe(topic, this.handleNotice); } } } _publishNoticeTopic(noticeTopic) { const nttps = noticeTopic.split(";"); for (const nt of nttps) { const parts = nt.split("~"); if (parts.length > 1) this._mapNoticeTopic[parts[0].trim()] = parts[1].trim(); else this._mapNoticeTopic[nt.trim()] = nt.trim(); } } _connectInterface(idInterface) { let status = true; const idint = idInterface.split(";"); for (const ii of idint) { const parts = ii.split("#"); if (parts.length > 1) this._connect(parts[0].trim(), parts[1].trim(), this); else status = false; } return status; } _notify(notice, message) { if (this._mapNoticeTopic[notice] != null) this._publish(this._mapNoticeTopic[notice], message); } _convertNotice(topic, message) { if (this._mapTopicNotice[topic] != null) this.handleNotice(this._mapTopicNotice[topic], message); else for (const [rgx, notice] of this._rgxTopicNotice) { const match = rgx.exec(topic); if (match != null && match[0] === topic) { this.handleNotice(notice, message); break; } } } connectTo(cInterface, component) { if (component.id) this._connect(cInterface, component.id, this); } connectionReady(cInterface, id, component) { if (this._connected[cInterface] == null) this._connected[cInterface] = []; this._connected[cInterface].push(id); } async _invoke(cInterface, notice, message) { const intSpec = Oid.getInterface(cInterface); if (this._connected[cInterface] != null) { if (intSpec.response != null && intSpec.response === true) { const responses = []; for (const id of this._connected[cInterface]) responses.push(await this._bus.invoke(cInterface, id, notice, message)); return responses; } else { for (const id of this._connected[cInterface]) return await this._bus.invoke(cInterface, id, notice, message); } } } handleNotice(notice, message) { if (this._receiveHandler[notice] != null) this._receiveHandler[notice](notice, message); } async handleInvoke(cInterface, notice, message) { let response = null; if (this._provideHandler[`${cInterface}.${notice}`] != null) response = await this._provideHandler[`${cInterface}.${notice}`](notice, message); return response; } _customExists(field) { return this._custom != null && this._custom.hasOwnProperty(field); } _getCustomField(field) { return this._custom == null || this._custom[field] == null ? null : this._custom[field]; } _callCustom(operation, parameters) { if (this._custom != null && this._custom[operation] != null) return this._custom[operation](this, parameters); } } class OidWeb extends OidBase { } class OidUI extends OidWeb { async connectedCallback() { await super.connectedCallback(); this._stylesheets = this.constructor.spec.stylesheets.replace('href="default"', 'href="' + this._sphere.stydefault + '"').replace('href="stylesheets:', 'href="' + this._sphere.stylesheets); this.render(); } render() { const spec = this.constructor.spec; let template = spec != null ? spec.template : null; if (this.template) { const tmpl = this.template(); if (tmpl != this._template) { this._template = tmpl; const td = Oid.prepareDispatchers(tmpl, this.constructor); this._templatePre = td.template; this._buildEventDispatchers(td.dispatcher, this); } template = this._templatePre; } this._presentation = null; if (spec != null && template != null) { let customTemplate = this._getCustomField("style"); customTemplate = customTemplate ? `<style>${customTemplate}</style>` : ""; const html2 = (this._stylesheets + spec.styles + customTemplate + template).replace( /{{[ \t]*(url:)?[ \t]*this\.([^}]*)}}/g, (match, p1, p2) => { p2 = p2 ? p2.trim() : ""; let value = this[p2] ? this[p2] : ""; if (p1 === "url:") value = value.replace("assets:", this._sphere.assets); return value; } ); if (spec.shadow === false) { this.innerHTML = html2; this._presentation = this.querySelector("#oid-prs") || this; } else this._presentation = this._shadowHTML(html2); if (this._dispatcher) { const query = spec.shadow === false ? this : this.shadowRoot; for (const [atr, event, dispatch] of this._dispatcher) { const target = query.querySelector("[" + atr + "]"); target.addEventListener(event, dispatch); } } } } _shadowHTML(html2) { const clone = OidUI.prepareShadow(this, html2); return this.shadowRoot.querySelector("#oid-prs") || clone; } static prepareShadow(owner, html2) { const template = document.createElement("template"); template.innerHTML = html2; const clone = document.importNode(template.content, true); if (!owner.shadowRoot) owner.attachShadow({ mode: "open" }); else owner.shadowRoot.innerHTML = ""; owner.shadowRoot.appendChild(clone); return clone; } } const _Oid = class _Oid { static cInterface(spec) { if (spec != null) _Oid._interfaceReg[spec.id] = spec; } static getInterface(cInterface) { return _Oid._interfaceReg[cInterface]; } static component(spec) { spec = Object.assign({}, _Oid._defaultSpec, spec); let impl = spec.implementation; if (impl == null) { const inh = spec.ui === false || spec.template == null ? spec.element == null ? OidBase : OidWeb : OidUI; const className = spec.element[0].toUpperCase() + spec.element.slice(1).replace(/-([a-z])/g, (match, letter) => letter.toUpperCase()); impl = class extends inh { }; Object.defineProperty(impl, "name", { value: className }); } const observed = impl.observedAttributes.slice(); if (spec.properties) { Object.defineProperty(impl, "observedAttributes", { get: function() { return this.observed; } }); for (const pname in spec.properties) { const property = spec.properties[pname]; const jsName = pname.replace( /-([a-z])/g, (match, letter) => letter.toUpperCase() ); Object.defineProperty( impl.prototype, jsName, property.readonly ? { get: function() { return this["_" + jsName]; } } : impl.prototype.render == null ? { get: function() { return this["_" + jsName]; }, set: function(newValue) { this["_" + jsName] = newValue; } } : { get: function() { return this["_" + jsName]; }, set: function(newValue) { const old = this["_" + jsName]; this["_" + jsName] = newValue; if (old != newValue && this._sphere) this.render(); } } ); if (property.attribute == null || property.attribute !== false) observed.push(pname); } } spec.provide = spec.provide == null ? _Oid.defaultInterface : spec.provide.concat(_Oid.defaultInterface); if (spec.provide) { const provideSpec = {}; for (const p of spec.provide) { const cInterface = _Oid._interfaceReg[p]; if (cInterface == null) throw new Error("Unknown interface id: " + p); else provideSpec[p] = cInterface; } spec.provide = provideSpec; } _Oid.stylePreprocess(spec); const td = _Oid.prepareDispatchers(spec.template, impl); spec.template = td.template; if (td.dispatcher) spec.dispatcher = td.dispatcher; Object.assign(impl, { spec, observed }); if (spec.element == null) spec.element = "internal-" + spec.id.replace(":", "-"); customElements.define(spec.element, impl); _Oid._oidReg[spec.id] = impl; } static componentSet(id, complementarySpec) { if (id != null && _Oid._oidReg[id] != null) { const spec = _Oid._oidReg[id].spec; for (const p in complementarySpec) spec[p] = complementarySpec[p]; _Oid.stylePreprocess(spec); } } // styles and template preprocessing static stylePreprocess(spec) { let sty = ""; if (spec.stylesheets) { let ss = spec.stylesheets; if (!Array.isArray(ss)) ss = [ss]; for (const s of ss) sty += `<link href="${s}" rel="stylesheet">`; } spec.stylesheets = sty; spec.styles = spec.styles ? `<style>${spec.styles}</style>` : ""; } static prepareDispatchers(template, impl) { let dispatcher = null; if (template) { let atrn = 1; const te = template.split( /@([^= >]*)[ \t]*(?:=[ \t]*{{[ \t]*this\.([^}]*)[ \t]*}})?/ ); if (te.length > 1) { dispatcher = []; let ntempl = ""; for (let i = 0; i + 2 < te.length; i += 3) { ntempl += te[i] + _Oid.eventAttribute + atrn + " "; const evt = te[i + 1].trim(); const funcName = te[i + 2] != null ? te[i + 2].trim() : "_on" + evt[0].toUpperCase() + evt.substring(1); dispatcher.push([ _Oid.eventAttribute + atrn, evt, impl.prototype[funcName] ]); atrn++; } template = ntempl + te[te.length - 1]; } } return { template, dispatcher }; } static create(componentId, properties) { const impl = _Oid._oidReg[componentId]; if (impl == null) throw new Error("Unknown component id: " + componentId); const instance = document.createElement(impl.spec.element); if (properties != null) { for (const p in properties) instance.setAttribute(p, properties[p]); } return instance; } static customize(id, spec) { if (id != null && _Oid._oidReg[id] != null && spec != null && spec.cid != null) { _Oid._oidCustom[id + "." + spec.cid] = spec; if (_Oid._customQueue[id + "." + spec.cid] != null) { _Oid._customQueue[id + "." + spec.cid](); delete _Oid._customQueue[id + "." + spec.cid]; } } } static async getCustom(id, cid) { if (id == null || cid == null) return null; if (_Oid._oidCustom[id + "." + cid] == null) { const promise = new Promise((resolve, reject) => { const callback = function() { resolve(); }; _Oid._customQueue[id + "." + cid] = callback; }); await promise; } return _Oid._oidCustom[id + "." + cid]; } static setDefault(spec) { this._defaultSpec = spec; } static addDefault(spec) { for (const p in spec) { if (this._defaultSpec[p] == null) this._defaultSpec[p] = spec[p]; else if (Array.isArray(this._defaultSpec[p])) this._defaultSpec[p] = this._defaultSpec[p].concat(spec[p]); else if (typeof this._defaultSpec[p] === "object") Object.assign(this._defaultSpec[p], spec[p]); else this._defaultSpec[p] = spec[p]; } } }; __publicField(_Oid, "eventAttribute", "oidevent_"); __publicField(_Oid, "defaultInterface", ["itf:oid"]); __publicField(_Oid, "_interfaceReg", {}); __publicField(_Oid, "_oidReg", {}); __publicField(_Oid, "_oidCustom", {}); __publicField(_Oid, "_customQueue", {}); __publicField(_Oid, "_defaultSpec", {}); let Oid = _Oid; Oid.cInterface({ id: "itf:oid", operations: { "get": { response: true }, "set": { response: false } }, flow: "pull" }); Oid.cInterface({ id: "itf:transfer", operations: ["send"], flow: "push" }); Oid.cInterface({ id: "itf:storage", operations: ["store", "load"], flow: "pull" }); Oid.cInterface({ id: "itf:iterate", operations: { "first": { response: true }, "next": { response: true } }, flow: "pull", connections: "1" }); class OidPlay extends OidSphere { connectedCallback() { super.connectedCallback(); const html2 = this._prepareHTML(); const template = OidPlay.template.replace("{stylesheet}", this.stylesheet).replace( "{code}", OidPlay.code.replace("{html}", html2).replace("{rows}", this._rows(html2)) ).replace("{console}", this.messages ? OidPlay.console.replace("{rows}", this.rows ? this.rows : OidPlay.rows) : ""); OidUI.prepareShadow(this, template); this._scriptPanel = this.shadowRoot.querySelector("#script"); this._unlockScript = this._unlockScript.bind(this); this._scriptPanel.addEventListener("click", this._unlockScript); this._buttonRender = this.shadowRoot.querySelector("#btn-render"); this._buttonRender.addEventListener("click", this._computeRender.bind(this)); this._observer = new MutationObserver(this._scriptUpdated.bind(this)); this._observer.observe( this, { attributes: true, childList: true, subtree: true } ); this.sphere.bus.subscribe("#", this._busMonitor.bind(this)); } static get observedAttributes() { return OidSphere.observedAttributes.concat(["messages", "rows"]); } get messages() { return this.hasAttribute("messages"); } set messages(newValue) { this.setAttribute("messages", newValue); } get rows() { return this.getAttribute("rows"); } set rows(newValue) { this.setAttribute("rows", newValue); } _scriptUpdated(mutationsList, observer) { const html2 = this._prepareHTML(); this._scriptPanel.value = html2; this._scriptPanel.rows = this._rows(html2); } _rows(html2) { const lines = html2.split(/\r\n|\r|\n/); let rows = 0; lines.forEach((l) => { rows += Math.floor(l.length / 45) + 1; }); return rows; } _prepareHTML() { let html2 = this.innerHTML.replace('=""', "").replace(/^[\r\n]+/, "").replace(/[\r\n]+$/, ""); if (html2.startsWith(" ") || html2.startsWith(" ")) { const indent = html2.match(/^[ \t]+/); html2 = html2.replace(new RegExp("^" + indent, "gm"), ""); } return html2; } _unlockScript() { this._scriptPanel.removeEventListener("click", this._unlockScript); this._scriptPanel.readOnly = false; this._buttonRender.style.display = "initial"; } _computeRender() { this.shadowRoot.querySelector("#render").innerHTML = this._scriptPanel.value; } _busMonitor(topic, message) { if (topic != "bus/monitor") this.sphere.bus.publish("bus/monitor", { value: `[${topic}] ${JSON.stringify(message)}` }); } } OidPlay.rows = 5; OidPlay.code = html`<div style="width:100%;display:flex"> <textarea id="script" class="code" style="width:100%;cursor:pointer" rows="{rows}" readonly>{html}</textarea> <button id="btn-render" class="btn btn-secondary" style="width:auto;display:none">Render</button> </div>`; OidPlay.template = html`<link rel="stylesheet" href="{stylesheet}"> {code} <div id="render"><slot></slot></div> {console}`; OidPlay.console = html`<div id="msg-pnl" style="width:100%"> <b>Messages on the Bus</b><br> <console-oid rows="{rows}" class="code" prompt="" subscribe="bus/monitor~display"></console-oid> </div>`; OidPlay.elementTag = "oid-play"; customElements.define(OidPlay.elementTag, OidPlay); class SubmitOid extends OidWeb { async handleSubmit(topic, message) { const toSubmit = {}; const schema = this._getCustomField("schema"); let form = null; if (schema != null) { for (const s of Object.keys(schema)) { const field = document.querySelector(`#${s}`); if (field != null) toSubmit[s] = field.value; } } else { form = this.parentNode; while (form != null && form.nodeName.toLowerCase() !== "form") form = form.parentNode; if (form != null) for (const f of form) { if (f.type === "radio" || f.type === "checkbox") { if (f.checked) { if (f.type === "checkbox" || !f.hasAttribute("name")) toSubmit[f.id] = f.value; else toSubmit[f.name] = f.value; } } else toSubmit[f.id] = f.value; } } if (!this._customExists("pre") || this._callCustom("pre", toSubmit) === true) { this._notify("submit", toSubmit); this._notify("dispatch", { value: toSubmit }); this._invoke("itf:transfer", "send", { value: toSubmit }); } } handleUpdate(topic, message) { if (message.value) { let form = this.parentNode; while (form != null && form.nodeName.toLowerCase() !== "form") form = form.parentNode; if (form != null) for (const f of form) { if (f.type === "radio" || f.type === "checkbox") { if (f.checked) { if (f.type === "checkbox" || !f.hasAttribute("name")) f.value = message.value[f.id]; else f.value = message.value[f.name]; } } else f.value = message.value[f.id]; } } } } Oid.component( { id: "oid:submit", element: "submit-oid", receive: ["submit", "update"], implementation: SubmitOid } ); class FileOid extends OidUI { template() { let tmpl = this.selection === "both" ? html`<div class="group">` : ""; if (this.selection === "drop" || this.selection === "both") tmpl += html`<div class="drop" @dragover @drop>{{this.dropLabel}}</div>`; if (this.selection === "dialog" || this.selection === "both") tmpl += html`<input type="file" class="selector" @change></input>`; if (this.selection === "both") tmpl += "</div>"; return tmpl; } _onDragover(event) { if (this.pre) this._presentation.innerHTML = this.dropPre; event.preventDefault(); } async _onDrop(event) { event.preventDefault(); if (this.post) this._presentation.innerHTML = this.dropPost; let file = null; if (event.dataTransfer.items) { for (const item of event.dataTransfer.items) { if (item.kind === "file") file = item.getAsFile(); } } else file = event.dataTransfer.files[0]; let content = await file.text(); if (this.filetype === "json") content = JSON.parse(content); this._notify("dispatch", { value: content }); this._invoke("itf:transfer", "send", { value: content }); } _onChange(event) { const result = {}; const file = event.target.files[0]; if (!file) result.error = "No file selected"; else { const reader = new FileReader(); reader.onload = (e) => { result.value = e.target.result; if (this.filetype === "json") result.value = JSON.parse(result.value); }; reader.onerror = (e) => { result.error = `Error reading file: ${e.target.error}`; }; reader.readAsText(file); } this._notify("dispatch", result); this._invoke("itf:transfer", "send", result); } handleRetrieve(topic, message) { } handleStore(topic, message) { let content = message.value || ""; if (this.filetype === "json") content = JSON.stringify(content); const a = document.createElement("a"); a.style.display = "none"; document.body.appendChild(a); a.href = window.URL.createObjectURL( new Blob([content], { type: "text/plain" }) ); a.setAttribute("download", message.filename || this.filename); a.click(); window.URL.revokeObjectURL(a.href); document.body.removeChild(a); } } Oid.component( { id: "oid:file", element: "file-oid", properties: { selection: { default: "both" }, // none, drop, dialog, both dropLabel: { default: "Drop Zone" }, dropPre: { default: "Drop your file here" }, dropPost: { default: "File loaded" }, filename: { default: "file.txt" }, filetype: { default: "plain" } // plain, json }, receive: ["retrieve", "store"], implementation: FileOid, styles: css` .group { display: flex; width: 100%; height: 100%; } .drop { width: 50%; border: 5px solid; } .selector { flex: 1; }` } ); class LocalStorageOid extends OidWeb { async handleLoad(topic, message) { const key = `${this.dataset}:${message.collection || this.collection}:${message.key || this.key}`; const content = JSON.parse(localStorage.getItem(key)); this._notify("dispatch", { value: content }); this._invoke("itf:transfer", "send", { value: content }); return { value: content }; } async handleStore(topic, message) { let status = false; if (message.value) { const key = `${this.dataset}:${message.collection || this.collection}:${message.key || this.key}`; localStorage.setItem(key, JSON.stringify(message.value)); status = true; } return status; } } Oid.component( { id: "oid:local-storage", element: "local-storage-oid", receive: ["load", "store"], provide: ["itf:storage"], properties: { dataset: { default: "default-mundorum-oid" }, collection: { default: "default" }, key: { default: "default" } }, implementation: LocalStorageOid } ); class JSONFileOid extends OidWeb { async handleLoad(topic, message) { const result = {}; if (message.filename == null && this.filename == null) result.error = "No file name provided"; else { const filename = message.filename || this.filename; const response = await fetch(`${filename}`); if (!response.ok) result.error = "Failed to load JSON"; else result.value = await response.json(); } this._notify("dispatch", result); this._invoke("itf:transfer", "send", result); return result; } } Oid.component( { id: "oid:json-file", element: "json-file-oid", receive: ["load"], properties: { filename: {} }, implementation: JSONFileOid } ); class ImageOid extends OidUI { _onClick() { this._notify("click", { value: this.label || this.source }); } _onMouseenter() { this._notify("mouseenter", { value: this.label || this.source }); } _onMouseleave() { this._notify("mouseleave", { value: this.label || this.source }); } } Oid.component( { id: "oid:image", element: "image-oid", properties: { source: { default: "assets:images/image.svg" }, label: {} }, implementation: ImageOid, stylesheets: "default", template: html` <image id="oid-prs" src="{{url:this.source}}" alt="{{this.label}}" @click @mouseenter @mouseleave>` } ); class ButtonOid extends OidUI { _onClick() { this._callCustom("click"); this._notify("click", { value: this.value || this.label }); this._invoke("itf:transfer", "send", { value: this.value || this.label }); } _onMouseenter() { this._notify("mouseenter", { value: this.value || this.label }); } _onMouseleave() { this._notify("mouseleave", { value: this.value || this.label }); } } Oid.component( { id: "oid:button", element: "button-oid", properties: { label: {}, value: {} }, implementation: ButtonOid, stylesheets: "default", template: html` <button id="oid-prs" class="btn btn-primary" @click @mouseenter @mouseleave> {{this.label}} </button>` } ); class ConsoleOid extends OidUI { handleSend(topic, message) { if (this._presentation && message && message.value) { if (typeof message.value === "object") message.value = JSON.stringify(message.value); } this._presentation.value += (this.prompt.length > 0 ? `${this.prompt} ` : "") + `${message.value} `; } } Oid.component({ id: "oid:console", element: "console-oid", properties: { prompt: { default: ">" }, rows: { default: 10 } }, receive: { "display": "handleSend" }, provide: ["itf:transfer"], implementation: ConsoleOid, styles: css` .console { width: 100%; font-family: "Courier New", monospace; font-size: 1em; background-color: lightgray }`, template: html` <textarea class="console" rows="{{this.rows}}" id="oid-prs" readonly></textarea>` }); class OidUIInput extends OidUI { connectedCallback() { this._value = this.getAttribute("value") || false; super.connectedCallback(); } static get observedAttributes() { return OidUI.observedAttributes.concat( ["variable", "value"] ); } get variable() { return this._variable; } set variable(newValue) { this._variable = newValue; } get value() { return this._value; } set value(newValue) { this._value = newValue; } } class SwitchOid extends OidUIInput { connectedCallback() { super.connectedCallback(); if (this.hasAttribute("value")) { this.value = !(this.getAttribute("value") === "off"); this.render(); } this._notify( "initial", { value: this.value ? this.on : this.off } ); } render() { super.render(); this._input = this._presentation.querySelector("#oid-input"); this._input.checked = this.value; } handleInvert(topic, message) { this.value = !this.value; if (this.value) this._input.checked = true; else this._input.checked = false; this._notifyState(); } handleOn() { this.value = true; this._input.checked = true; this._notifyState(); } handleOff() { this.value = false; this._input.checked = false; this._notifyState(); } handleRepublish(topic, message) { if (this.value) this._notify("republish", message); } _onInput() { this._value = this._input.checked; this._notifyState(); } _notifyState() { const state = this._value ? this.on : this.off; this._notify("change", { value: state }); this._notify(state, { value: state }); } } Oid.component( { id: "oid:switch", element: "switch-oid", // properties: variable and value inherited from OidUIInput properties: { on: { default: "on" }, off: { default: "off" } }, receive: ["invert", "on", "off", "republish"], implementation: SwitchOid, styles: css` .switch { position: relative; display: inline-block; width: 60px; height: 34px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: .4s; transition: .4s; } .slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; -webkit-transition: .4s; transition: .4s; } input:checked + .slider { background-color: #2196F3; } input:focus + .slider { box-shadow: 0 0 1px #2196F3; } input:checked + .slider:before { -webkit-transform: translateX(26px); -ms-transform: translateX(26px); transform: translateX(26px); } .slider.round { border-radius: 34px; } .slider.round:before { border-radius: 50%; }`, template: html` <label id="oid-prs" class="switch"> <input id="oid-input" type="checkbox" @input> <span class="slider round"></span> </label>` } ); class SliderOid extends OidUIInput { connectedCallback() { super.connectedCallback(); if (!this.hasAttribute("value")) { this.value = Math.round((parseInt("" + this.min) + parseInt("" + this.max)) / 2); this.render(); } this._notify("initial", { value: this.value }); } render() { super.render(); this._input = this._presentation.querySelector("#oid-input"); if (this.hasAttribute("index")) { this._index = this._presentation.querySelector("#index"); this._index.innerHTML = this.value; } } _onInput() { this._value = this._input.value; if (this.hasAttribute("index")) this._index.innerHTML = this.value; this._notify("change", { value: this.value }); } } Oid.component( { id: "oid:slider", element: "slider-oid", properties: { min: { default: 0 }, max: { default: 100 }, index: { default: false } }, implementation: SliderOid, stylesheets: "default", template: html` <div id="oid-prs" style="width:100%; display:flex; flex-direction:row"> <span id="index" style="flex:initial"></span> <input type="range" id="oid-input" min="{{this.min}}" max="{{this.max}}" value="{{this.value}}" style="flex:auto" @input> </div>` } ); class SplitPaneOid extends OidUI { constructor() { super(); this._x = 0; this._y = 0; this._sideASize = 0; this._onMousemove = this._onMousemove.bind(this); this._onMouseup = this._onMouseup.bind(this); } async connectedCallback() { await super.connectedCallback(); this._horz = !(this.split === "vertical"); this._resizer = this.shadowRoot.getElementById("resizer"); this._sidea = this.shadowRoot.getElementById("sidea"); this._sideb = this.shadowRoot.getElementById("sideb"); if (this._horz) { this._resizer.style.cursor = "ew-resize"; this._resizer.style.height = "100%"; this._resizer.classList.add("divide-x"); this._sidea.style.width = this.proportion; } else { this._resizer.style.cursor = "ns-resize"; this._presentation.style.flexDirection = "column"; this._resizer.style.width = "100%"; this._resizer.classList.add("divide-y"); this._sidea.style.height = this.proportion; } } _onMousedown(event) { this._x = event.clientX; this._y = event.clientY; this._sideASize = this._horz ? this._sidea.getBoundingClientRect().width : this._sidea.getBoundingClientRect().height; this.shadowRoot.addEventListener("mousemove", this._onMousemove); this.shadowRoot.addEventListener("mouseup", this._onMouseup); } _onMousemove(event) { const dx = event.clientX - this._x; const dy = event.clientY - this._y; if (this._horz) { const newSideAWidth = (this._sideASize + dx) * 100 / this._presentation.getBoundingClientRect().width; this._sidea.style.width = newSideAWidth + "%"; this._resizer.style.cursor = "col-resize"; document.body.style.cursor = "col-resize"; } else { const newSideAHeight = (this._sideASize + dy) * 100 / this._presentation.getBoundingClientRect().height; this._sidea.style.height = newSideAHeight + "%"; this._resizer.style.cursor = "row-resize"; document.body.style.cursor = "row-resize"; } this._sidea.style.userSelect = "none"; this._sidea.style.pointerEvents = "none"; this._sideb.style.userSelect = "none"; this._sideb.style.pointerEvents = "none"; } _onMouseup(event) { this.shadowRoot.removeEventListener("mousemove", this._onMousemove); this.shadowRoot.removeEventListener("mouseup", this._onMouseup); this._resizer.style.cursor = this._horz ? "ew-resize" : "ns-resize"; document.body.style.removeProperty("cursor"); this._sidea.style.removeProperty("user-select"); this._sidea.style.removeProperty("pointer-events"); this._sideb.style.removeProperty("user-select"); this._sideb.style.removeProperty("pointer-events"); this._notify( "resize", { awidth: this._sidea.style.width, aheight: this._sidea.style.height, bwidth: this._sideb.style.width, bheight: this._sideb.style.height } ); } } Oid.component({ id: "oid:split-pane", element: "split-pane-oid", properties: { split: { default: "horizontal" }, proportion: { default: "50%" } }, implementation: SplitPaneOid, stylesheets: "default", styles: css` .group { display: flex; width: 100%; height: 100%; } .pane-b { flex: 1; }`, template: html` <div id="oid-prs" class="group"> <div class="bg-base" id="sidea"><slot name="side-a"></slot></div> <div class="divide" id="resizer" @mousedown></div> <div class="pane-b bg-base" id="sideb"><slot name="side-b"></slot></div> </div>` }); class StatePaneOid extends OidUI { async connectedCallback() { super.connectedCallback(); this._toInitial = this.initial; this._updateVisualState(); this._observer = new MutationObserver( this._updateVisualState.bind(this) ); this._observer.observe( this, { childList: true, subtree: true } ); } _updateVisualState() { if (this._toInitial != null) { this._state = this.querySelector(`#${this._toInitial}`); if (this._state != null) this._toInitial = null; } const children = this.querySelectorAll("*"); for (let i = 0; i < children.length; i++) { const child = children[i]; if (this._state == null) this._state = child; if (child === this._state && child.style.display != null) { child.style.removeProperty("display"); } else if (child.style.display == null || child.style.display !== "none") { child.style.display = "none"; } } } handleFirst() { this._state = this.firstElementChild; this._updateVisualState(); } handleNext() { if (this._state != null) { let next = this._state.nextElementSibling; if (next == null) next = this.firstElementChild; this._state = next; this._updateVisualState(); } } handlePrev() { if (this._state != null) { let prev = this._state.previousElementSibling; if (prev == null) prev = this.lastElementChild; this._state = prev; this._updateVisualState(); } } handleLast() { this._state = this.lastElementChild; this._updateVisualState(); } handleState(topic, message) { this._state = this.querySelector(`#${message.value}`); this._updateVisualState(); } } Oid.component({ id: "oid:state-pane", element: "state-pane-oid", properties: { initial: {} }, receive: ["first", "next", "prev", "last", "state"], implementation: StatePaneOid, stylesheets: "default" }); class PlatformOid extends OidWeb { connectedCallback() { super.connectedCallback(); this._space = document.querySelector(`#${this.space}`); } handleUpdate(topic, message) { var _a; if (this.space != null && ((_a = message.value) == null ? void 0 : _a[this.source])) this.space.innerHTML = message.value[this.source]; } } Oid.component({ id: "oid:pl