import { CustomHTMLElement } from './lib/custom_html_element.mjs' export class SmonDashboard extends CustomHTMLElement {// {{{ static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor() {// {{{ super(true) const script = this.querySelector('script') this.data = JSON.parse(script.textContent) globalThis.foo = this.data this.widgets = new Map() // keyed by datapoint ID, value is an array of widgets. this.addColumns = 0 this.addRows = 0 this.editedWidget = null this.elEditDashboard.addEventListener('click', () => this.classList.toggle('edit')) this.elAddColumns.addEventListener('click', () => { this.addColumns += 5; this.render() }) this.elAddRows.addEventListener('click', () => { this.addRows += 5; this.render() }) this.elUpdateDashboard.addEventListener('click', () => this.saveDashboard()) this.elEditUpdate.addEventListener('click', () => this.editUpdate()) this.render() this.fetchData() // Retrieve one as fast as possible since setInterval doesn't run before an interval. setInterval(() => this.fetchData(), 1000) }// }}} render() {// {{{ const widgets = [] this.widgets = new Map() let maxX = 0 let maxY = 0 let occupiedCells = new Map() for (const wd of this.data.Widgets) { if (!this.widgets.has(wd.DatapointID)) this.widgets.set(wd.DatapointID, []) // widgetRefs modifies the array in the map directly. const widgetRefs = this.widgets.get(wd.DatapointID) const widget = SmonWidget.create(wd.Type, wd) widget.addEventListener('click', () => this.selectCell(widget)) widgetRefs.push(widget) widgets.push(widget) maxX = Math.max(maxX, wd.X) maxY = Math.max(maxY, wd.Y) occupiedCells.set(`${wd.X}x${wd.Y}`, true) for (let x = 0; x <= wd.SpanX; x++) { occupiedCells.set(`${wd.X + x}x${wd.Y}`, true) } } maxX += this.addColumns maxY += this.addRows // Empty elements are created to fill up the empty cells in the grid. // This is needed as placeholders for grid and dragdrop targets when editing. for (let x = 1; x <= maxX; x++) for (let y = 1; y <= maxY; y++) { if (occupiedCells.has(`${x}x${y}`)) continue const empty = new SmonWidgetEmpty(x, y) empty.addEventListener('click', () => this.selectCell(empty)) widgets.push(empty) } this.elGrid.replaceChildren(...widgets) }// }}} error(msg) {// {{{ this.elError.innerText = msg if (msg === '') this.elError.classList.remove('show') else this.elError.classList.add('show') }// }}} saveDashboard() {// {{{ }// }}} async editUpdate() {// {{{ await this.editedWidget.editUpdate() this.elEdit.close() this.editedWidget = null this.render() }// }}} selectCell(el) {// {{{ if (!this.classList.contains('edit')) return this.editedWidget = el const components = el.edit() this.elEditComponents.replaceChildren(...components) this.elEdit.showModal() }// }}} async fetchData() {// {{{ try { const res = await fetch(`/dashboard/values`) const json = await res.json() if (!json.OK) throw new Error(json.Error) for (const dp of json.Values) { const widgets = this.widgets.get(dp.ID) || [] for (const w of widgets) { w.setValue(dp.Value) w.render() } } this.error('') } catch (e) { console.error(e) this.error(e.message) // All widgets are set in a NULL state to visualize the error. const allDpIDs = this.widgets.keys() for (const id of allDpIDs) { const widgets = this.widgets.get(id) || [] for (const w of widgets) { w.setValue(null) w.render() } } } }// }}} }// }}} class SmonWidget extends CustomHTMLElement {// {{{ static create(t, data) {// {{{ switch (t) { case 0: return new SmonWidgetLabel(data) case 1: return new SmonWidgetOnOff(data) } }// }}} constructor(data) {// {{{ super(true) this.data = data this.value = null this.editDialog = null }// }}} render() {// {{{ this.style.gridColumn = this.data.X this.style.gridRow = this.data.Y if (this.data.SpanX > 0) this.style.gridColumn = `${this.data.X} / ${this.data.X + this.data.SpanX + 1}` if (this.data.Attributes.BorderBottom) { if (this.data.Attributes.Color) this.style.borderBottom = `1px solid ${this.data.Attributes.Color}` else this.style.borderBottom = `1px solid currentColor` } }// }}} setValue(v) {// {{{ this.value = v }// }}} edit() {// {{{ if (this.editDialog !== null) return const components = this.editWidget() return components }// }}} // Many widgets can have labels. // This function applies common attributes like size and color. applyLabelAttributes(el) {// {{{ if (this.data.Attributes?.Size) el.style.fontSize = this.data.Attributes.Size if (this.data.Attributes?.Color) el.style.color = this.data.Attributes.Color if (this.data.Attributes?.Underline) if (this.data.Attributes?.Color) el.style.borderBottom = `1px solid ${this.data.Attributes.Color}` else el.style.borderBottom = `1px solid currentColor` }// }}} }// }}} class SmonWidgetEmpty extends SmonWidget {// {{{ static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` ` }// }}} constructor(x, y) {// {{{ super({ Attributes: {}, X: x, Y: y, }) this.classList.add('empty') this.render() }// }}} render() {// {{{ super.render() }// }}} editWidget() {// {{{ const div = document.createElement('div') div.innerHTML = `
Add widget:
` return [div] }// }}} }// }}} class SmonWidgetLabel extends SmonWidget {// {{{ static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor(data) {// {{{ super(data) this.render() }// }}} render() {// {{{ super.render() this.elLabel.innerText = this.data.Attributes?.Label || '[set Label]' this.applyLabelAttributes(this.elLabel) }// }}} editWidget() {// {{{ this.editDiv = document.createElement('div') this.editDiv.innerHTML = `
Label:
` this.editDiv.querySelector('input').value = this.data.Attributes.Label return [this.editDiv] }// }}} async editUpdate() {// {{{ console.log('hum') this.data.Attributes.Label = this.editDiv.querySelector('input').value }// }}} }// }}} class SmonWidgetOnOff extends SmonWidget {// {{{ static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor(data) {// {{{ super(data) this.render() }// }}} render() {// {{{ super.render() let img switch (this.value) { case 1: case 'ON': img = 'widget_light_green.svg' break case 0: case 'OFF': img = 'widget_light_red.svg' break default: img = 'widget_light_off.svg' } this.elStatus.setAttribute('src', `/images/${_VERSION}/${img}`) this.elLabel.innerText = this.data.Attributes.Label this.applyLabelAttributes(this.elLabel) }// }}} editWidget() {// {{{ this.editDiv = document.createElement('div') this.editDiv.innerHTML = `
Label:
` this.editDiv.querySelector('input').value = this.data.Attributes.Label return [this.editDiv] }// }}} async editUpdate() {// {{{ this.data.Attributes.Label = this.editDiv.querySelector('input').value }// }}} }// }}} customElements.define("smon-widget", SmonWidget) customElements.define("smon-widget-empty", SmonWidgetEmpty) customElements.define("smon-widget-label", SmonWidgetLabel) customElements.define("smon-widget-onoff", SmonWidgetOnOff) customElements.define("smon-dashboard", SmonDashboard)