class Renderer extends EventEmitter { static #WIDGETS = new Map(); static #VIRTUAL_WIDGETS = new Set(); /** * Register widget class * @param {string} name * @param {typeof Widget} cls * @param {boolean} virtual */ static register(name, cls, virtual = false) { Renderer.#WIDGETS.set(name, cls); if (virtual) Renderer.#VIRTUAL_WIDGETS.add(name); } /** @type {Device} */ device; #widgets; #idMap; #idMapExt; #files; #filesLoaded; constructor(device) { super(); this.device = device; this.#widgets = []; this.#idMap = new Map(); this.#idMapExt = new Map(); this.#files = []; this.#filesLoaded = false; } update(controls) { this.close(); this.#widgets.length = 0; this.#idMap.clear(); this.#idMapExt.clear(); this.#files.length = 0; this.#filesLoaded = false; this._makeWidgets(this.#widgets, 'col', controls); this.#filesLoaded = true; this.#loadFiles(); } #updateWWidth(type, data) { switch (type) { case 'row': let sumw = 0; for (const ctrl of data) { if (!ctrl.type || Renderer.#VIRTUAL_WIDGETS.has(ctrl.type)) continue; if (!ctrl.wwidth) ctrl.wwidth = 1; sumw += ctrl.wwidth; } for (const ctrl of data) { if (!ctrl.type || Renderer.#VIRTUAL_WIDGETS.has(ctrl.type)) continue; ctrl.wwidth_t = ctrl.wwidth * 100 / sumw; } break; case 'col': for (const ctrl of data) { if (!ctrl.type || Renderer.#VIRTUAL_WIDGETS.has(ctrl.type)) continue; ctrl.wwidth_t = 100; } break; } } /** * Generate widgets from layout. * @param {Widget[]} cont * @param {'row' | 'col'} type * @param {object[]} data */ _makeWidgets(cont, type, data, isExt = false) { this.#updateWWidth(type, data); const idMap = isExt ? this.#idMapExt : this.#idMap; for (const ctrl of data) { if (!ctrl.type) continue; const cls = Renderer.#WIDGETS.get(ctrl.type); if (cls === undefined) { console.log('W: Missing widget:', ctrl); continue; } const obj = new cls(ctrl, this); idMap.set(obj.id, obj) cont.push(obj); } } async _set(widget, value, ack = true) { try { await this.device.set(widget.id, value); } catch (e) { console.log(e); if (ack) widget._handleSetError(e); } if (ack) widget._handleAck(); } /** * Build HTML tree from widgets. * @returns {HTMLElement} */ build(){ const res = []; for (const w of this.#widgets) { const $w = w.build(); if ($w) res.push($w); } return res; } /** * Закрытие рендерера (остановка таймеров). Нужно вызвать перед удалением рендерера. */ close() { for (const w of this.#idMap.values()) { w.close(); } } /** * Обработчик пакета update с устройства * @param {string} id Widget id * @param {object} data */ handleUpdate(id, data) { const w = this.#idMap.get(id); if (w) w.update(data); } /** * Register an UI file to load. * @param {Widget} widget * @param {string} path * @param {string} type * @param {(string) => undefined} callback */ _addFile(widget, path, type, callback) { let has = this.#files.some(f => f.widget.id == widget.id); if (!has) this.#files.push({ widget, path, type, callback }); this.#loadFiles(); } async #loadFiles() { if (!this.#filesLoaded) return; while (this.#files.length) { const file = this.#files.shift(); let res; try { res = await this.device.fetch(file.path, file.type, file.widget._handleFileProgress.bind(file.widget)); } catch (e) { console.log(e); file.widget._handleFileError(e); continue; } file.widget._handleFileLoaded(res); file.callback(res); } } _getPlugin(name) { const widget = this.#idMap.get(name); if (!widget || !(widget instanceof PluginWidget)) return undefined; return widget.widgetClass; } }