From 3c4566c12b9fa18d5e42bf6ea5ccb9e37ec9cd96 Mon Sep 17 00:00:00 2001 From: Rachel <508861+Ryex@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:08:43 -0700 Subject: [PATCH] More device card stuff --- www/src/js/virtual_machine/device.ts | 498 ++++++++++++++++++++------- www/src/scss/dark.scss | 23 ++ 2 files changed, 397 insertions(+), 124 deletions(-) diff --git a/www/src/js/virtual_machine/device.ts b/www/src/js/virtual_machine/device.ts index ac4e7f1..86111a1 100644 --- a/www/src/js/virtual_machine/device.ts +++ b/www/src/js/virtual_machine/device.ts @@ -1,145 +1,395 @@ -import { Offcanvas } from 'bootstrap'; -import { VirtualMachine, VirtualMachineUI } from '.'; -import { DeviceRef, VM } from 'ic10emu_wasm'; - +import { Offcanvas } from "bootstrap"; +import { VirtualMachine, VirtualMachineUI } from "."; +import { DeviceRef, VM } from "ic10emu_wasm"; class VMDeviceUI { - ui: VirtualMachineUI; - summary: HTMLDivElement; - canvasEl: HTMLDivElement; - deviceCountEl: HTMLElement; - canvas: Offcanvas; - private _deviceSummaryCards: Map; + ui: VirtualMachineUI; + summary: HTMLDivElement; + canvasEl: HTMLDivElement; + deviceCountEl: HTMLElement; + canvas: Offcanvas; + private _deviceSummaryCards: Map; + private _offCanvaseCards: Map< + number, + { col: HTMLElement; card: VMDeviceCard } + >; - constructor(ui: VirtualMachineUI) { - const that = this; - that.ui = ui; - this.summary = document.getElementById('vmDeviceSummary') as HTMLDivElement; - this.canvasEl = document.getElementById('vmDevicesOCBody') as HTMLDivElement; - this.deviceCountEl = document.getElementById('vmViewDeviceCount'); - this.canvas = new Offcanvas(this.canvasEl); - this._deviceSummaryCards = new Map(); + constructor(ui: VirtualMachineUI) { + const that = this; + that.ui = ui; + this.summary = document.getElementById("vmDeviceSummary") as HTMLDivElement; + this.canvasEl = document.getElementById( + "vmDevicesOCBody", + ) as HTMLDivElement; + this.deviceCountEl = document.getElementById("vmViewDeviceCount"); + this.canvas = new Offcanvas(this.canvasEl); + this._deviceSummaryCards = new Map(); + this._offCanvaseCards = new Map(); + } + + update(active_ic: DeviceRef) { + const devices = window.VM.devices; + this.deviceCountEl.textContent = `(${devices.size})`; + for (const [id, device] of devices) { + if (!this._deviceSummaryCards.has(id)) { + this._deviceSummaryCards.set(id, new VMDeviceSummaryCard(this, device)); + } + if (!this._offCanvaseCards.has(id)) { + const col = document.createElement("div"); + col.classList.add("col"); + col.id = `${this.canvasEl.id}_col${id}` + this.canvasEl.appendChild(col); + this._offCanvaseCards.set(id, { + col, + card: new VMDeviceCard(this, col, device), + }); + } } - - update(active_ic: DeviceRef) { - const devices = window.VM.devices; - this.deviceCountEl.innerText = `(${devices.size})` - for (const [id, device] of devices) { - if (!this._deviceSummaryCards.has(id)) { - this._deviceSummaryCards.set(id, new VMDeviceSummaryCard(this, device)); - } - } - this._deviceSummaryCards.forEach((card, _id) => { card.update(active_ic)}); - } - + this._deviceSummaryCards.forEach((card, id, cards) => { + if (!devices.has(id)) { + card.destroy(); + cards.delete(id); + } else { + card.update(active_ic); + } + }, this); + this._offCanvaseCards.forEach((card, id, cards) => { + if (!devices.has(id)) { + card.card.destroy(); + card.col.remove(); + cards.delete(id); + } else { + card.card.update(active_ic); + } + }, this); + } } class VMDeviceSummaryCard { - root: HTMLDivElement; - viewBtn: HTMLButtonElement; - deviceUI: VMDeviceUI; - device: DeviceRef; - badges: HTMLSpanElement[]; - constructor(deviceUI: VMDeviceUI, device: DeviceRef) { - // const that = this; - this.deviceUI = deviceUI; - this.device = device; - this.root = document.createElement('div'); - this.root.classList.add("hstack", "gap-2", "bg-light-subtle", "border", "border-secondary-subtle", "rounded"); - this.viewBtn = document.createElement('button'); - this.viewBtn.type = "button"; - this.viewBtn.classList.add("btn", "btn-success" ); - this.root.appendChild(this.viewBtn); - this.deviceUI.summary.appendChild(this.root); - this.badges = []; + root: HTMLDivElement; + viewBtn: HTMLButtonElement; + deviceUI: VMDeviceUI; + device: DeviceRef; + badges: HTMLSpanElement[]; + constructor(deviceUI: VMDeviceUI, device: DeviceRef) { + // const that = this; + this.deviceUI = deviceUI; + this.device = device; + this.root = document.createElement("div"); + this.root.classList.add( + "hstack", + "gap-2", + "bg-light-subtle", + "border", + "border-secondary-subtle", + "rounded", + ); + this.viewBtn = document.createElement("button"); + this.viewBtn.type = "button"; + this.viewBtn.classList.add("btn", "btn-success"); + this.root.appendChild(this.viewBtn); + this.deviceUI.summary.appendChild(this.root); + this.badges = []; - this.update(window.VM.activeIC); + this.update(window.VM.activeIC); + } + + update(active_ic: DeviceRef) { + const that = this; + // clear previous badges + this.badges.forEach((badge) => badge.remove()); + this.badges = []; + + //update name + var deviceName = this.device.name ?? this.device.prefabName ?? ""; + if (deviceName) { + deviceName = `: ${deviceName}`; + } + const btnTxt = `Device ${this.device.id}${deviceName}`; + this.viewBtn.textContent = btnTxt; + + // regenerate badges + this.device.connections.forEach((conn, index) => { + if (typeof conn === "object") { + var badge = document.createElement("span"); + badge.classList.add("badge", "text-bg-light"); + badge.textContent = `Net ${index}:${conn.CableNetwork}`; + that.badges.push(badge); + that.root.appendChild(badge); + } + }); + + if (this.device.id === active_ic.id) { + var badge = document.createElement("span"); + badge.classList.add("badge", "text-bg-success"); + badge.textContent = "db"; + that.badges.push(badge); + that.root.appendChild(badge); } - update (active_ic: DeviceRef) { - - const that = this; - // clear previous badges - this.badges.forEach(badge => badge.remove()); - this.badges = [] - - //update name - var deviceName = this.device.name ?? this.device.prefabName ?? ""; - if (deviceName) { - deviceName = `: ${deviceName}` - } - const btnTxt = `Device ${this.device.id}${deviceName}` - this.viewBtn.innerText = btnTxt; - - // regenerate badges - this.device.connections.forEach((conn, index) => { - if ( typeof conn === "object") { - var badge = document.createElement('span'); - badge.classList.add("badge", "text-bg-light"); - badge.innerText = `Net ${index}:${conn.CableNetwork}`; - that.badges.push(badge); - that.root.appendChild(badge); - } - - }); - - if (this.device.id === active_ic.id) { - var badge = document.createElement('span'); - badge.classList.add("badge", "text-bg-success"); - badge.innerText = "db"; - that.badges.push(badge); - that.root.appendChild(badge); - } - - active_ic.pins?.forEach((id, index) => { - if (that.device.id === id) { - var badge = document.createElement('span'); - badge.classList.add("badge", "text-bg-success"); - badge.innerText = `d${index}`; - that.badges.push(badge); - that.root.appendChild(badge); - } - }); - - } - - destroy() { - this.root.remove(); - } + active_ic.pins?.forEach((id, index) => { + if (that.device.id === id) { + var badge = document.createElement("span"); + badge.classList.add("badge", "text-bg-success"); + badge.textContent = `d${index}`; + that.badges.push(badge); + that.root.appendChild(badge); + } + }); + } + destroy() { + this.root.remove(); + } } class VMDeviceCard { - ui: VMDeviceUI; - container: HTMLElement; - root: HTMLDivElement; + ui: VMDeviceUI; + container: HTMLElement; + device: DeviceRef; + root: HTMLDivElement; + nav: HTMLUListElement; - header: HTMLHeadingElement; - device: DeviceRef; - nameInput: HTMLInputElement; - nameHash: HTMLSpanElement; - badges: HTMLSpanElement[]; - fieldsContainer: HTMLDivElement; - slotsContainer: HTMLDivElement; - pinsContainer: HTMLDivElement; - networksContainer: HTMLDivElement; + header: HTMLDivElement; + nameInput: HTMLInputElement; + nameHash: HTMLSpanElement; + body: HTMLDivElement; + badges: HTMLSpanElement[]; + fieldsContainer: HTMLDivElement; + slotsContainer: HTMLDivElement; + pinsContainer: HTMLDivElement; + networksContainer: HTMLDivElement; + reagentsContainer: HTMLDivElement; + nav_id: string; + navTabs: { [key: string]: { li: HTMLLIElement; button: HTMLButtonElement } }; + paneContainer: HTMLDivElement; + tabPanes: { [key: string]: HTMLElement }; + image: HTMLImageElement; + image_err: boolean; + title: HTMLHeadingElement; + fieldEls: Map; - constructor(ui: VMDeviceUI, container: HTMLElement, device: DeviceRef) { - this.ui = ui; - this.container = container; - this.device = device; + constructor(ui: VMDeviceUI, container: HTMLElement, device: DeviceRef) { + this.ui = ui; + this.container = container; + this.device = device; + this.nav_id = `${this.container.id}_vmDeviceCard${this.device.id}`; - this.root = document.createElement('div'); + this.root = document.createElement("div"); + this.root.classList.add("card"); - this.header = document.createElement('h5'); - this.nameInput = document.createElement('input'); - this.nameHash = document.createElement('span'); - this.badges = []; - this.fieldsContainer = document.createElement('div'); - this.slotsContainer = document.createElement('div'); - this.pinsContainer = document.createElement('div'); - this.networksContainer = document.createElement('div'); + this.header = document.createElement("div"); + this.header.classList.add("card-header", "hstack"); + this.image = document.createElement("img"); + this.image_err = false; + this.image.src = `/img/stationpedia/${this.device.prefabName}.png`; + this.image.onerror = this.onImageErr; + this.image.width = 48; + this.image.classList.add("me-2"); + this.header.appendChild(this.image); + + this.title = document.createElement("h5"); + this.title.textContent = `Device ${this.device.id} : ${this.device.prefabName ?? ""}`; + this.header.appendChild(this.title); + + this.nameInput = document.createElement("input"); + this.nameHash = document.createElement("span"); + + this.root.appendChild(this.header); + + this.body = document.createElement("div"); + this.body.classList.add("card-body"); + this.root.appendChild(this.body); + + this.nav = document.createElement("ul"); + this.nav.classList.add("nav", "nav-tabs"); + this.nav.role = "tablist"; + this.nav.id = this.nav_id; + this.navTabs = {}; + this.tabPanes = {}; + + this.body.appendChild(this.nav); + + this.paneContainer = document.createElement("div"); + this.paneContainer.id = `${this.nav_id}_tabs`; + + this.body.appendChild(this.paneContainer); + + this.badges = []; + this.fieldsContainer = document.createElement("div"); + this.fieldsContainer.id = `${this.nav_id}_fields`; + this.fieldsContainer.classList.add("vstack"); + this.fieldEls = new Map(); + this.slotsContainer = document.createElement("div"); + this.slotsContainer.id = `${this.nav_id}_slots`; + this.slotsContainer.classList.add("vstack"); + this.reagentsContainer = document.createElement("div"); + this.reagentsContainer.id = `${this.nav_id}_reagents`; + this.reagentsContainer.classList.add("vstack"); + this.networksContainer = document.createElement("div"); + this.networksContainer.id = `${this.nav_id}_networks`; + this.networksContainer.classList.add("vstack"); + this.pinsContainer = document.createElement("div"); + this.pinsContainer.id = `${this.nav_id}_pins`; + this.pinsContainer.classList.add("vstack"); + + this.addTab("Fields", this.fieldsContainer); + this.addTab("Slots", this.slotsContainer); + this.addTab("Networks", this.networksContainer); + + this.update(window.VM.activeIC); + + // do last to minimise reflows + this.container.appendChild(this.root); + } + + onImageErr(e: Event) { + this.image_err = true; + console.log("Image load error", e); + } + + addNav(name: string, target: string) { + if (!(name in this.navTabs)) { + var li = document.createElement("li"); + li.classList.add("nav-item"); + li.role = "presentation"; + var button = document.createElement("button"); + button.classList.add("nav-link"); + if (!(Object.keys(this.navTabs).length > 0)) { + button.classList.add("active"); + button.tabIndex = 0; + } else { + button.tabIndex = -1; + } + button.id = `${this.nav_id}_tab_${name}`; + button.setAttribute("data-bs-toggle", "tab"); + button.setAttribute("data-bs-target", `#${target}`); + button.type = "button"; + button.role = "tab"; + button.setAttribute("aria-controls", target); + button.setAttribute( + "aria-selected", + Object.keys(this.navTabs).length > 0 ? "false" : "true", + ); + button.textContent = name; + li.appendChild(button); + this.nav.appendChild(li); + this.navTabs[name] = { li, button }; + return true; } + return false; + } + + removeNav(name: string) { + if (name in this.navTabs) { + this.navTabs[name].li.remove(); + delete this.navTabs[name]; + return true; + } + return false; + } + + addTab(name: string, tab: HTMLElement) { + const paneName = `${this.nav_id}_pane_${name}`; + if (this.addNav(name, paneName)) { + if (name in this.tabPanes) { + this.tabPanes[name].remove(); + } + const pane = document.createElement("div"); + pane.classList.add("tap-pane", "fade"); + if (!(Object.keys(this.tabPanes).length > 0)) { + pane.classList.add("show", "active"); + } + pane.id = paneName; + pane.role = "tabpanel"; + pane.setAttribute("aria-labelledby", `${this.nav_id}_tab_${name}`); + pane.tabIndex = 0; + + this.paneContainer.appendChild(pane); + pane.appendChild(tab); + this.tabPanes[name] = tab; + } + } + + removeTab(name: string) { + let result = this.removeNav(name); + if (name in this.tabPanes) { + this.tabPanes[name].remove(); + delete this.tabPanes[name]; + return true; + } + return result; + } + + update(active_ic: DeviceRef) { + if (this.device.pins) { + this.addTab("Pins", this.pinsContainer); + } else { + this.removeTab("Pins"); + } + + // fields + for (const [name, _field] of this.device.fields) { + if (!this.fieldEls.has(name)) { + const field = new VMDeviceField(this.device, name, this, this.fieldsContainer); + this.fieldEls.set(name, field); + } + } + this.fieldEls.forEach((field, name, map) => { + if(!this.device.fields.has(name)) { + field.destroy(); + map.delete(name); + } else { + field.update(active_ic); + } + }, this); + + + // TODO Reagents + } + + destroy() { + this.root.remove(); + } } -export { VMDeviceUI } +class VMDeviceField { + container: HTMLElement; + card: VMDeviceCard; + device: DeviceRef; + field: string; + root: HTMLDivElement; + name: HTMLSpanElement; + fieldType: HTMLSpanElement; + input: HTMLInputElement; + constructor(device: DeviceRef, field: string, card: VMDeviceCard, container: HTMLElement) { + this.device = device; + this.field = field; + this.card = card; + this.container = container; + this.root = document.createElement('div'); + this.root.classList.add("input-group", "input-group-sm"); + this.name = document.createElement('span'); + this.name.classList.add("input-group-text", "field_name"); + this.name.textContent = this.field; + this.root.appendChild(this.name); + this.fieldType = document.createElement('span'); + this.fieldType.classList.add("input-group-text", "field_type"); + this.fieldType.textContent = device.fields.get(this.field)?.field_type; + this.root.appendChild(this.fieldType); + this.input = document.createElement('input'); + this.input.type = "text"; + this.input.value = this.device.fields.get(this.field)?.value.toString(); + this.root.appendChild(this.input); + + this.container.appendChild(this.root); + } + destroy () { + this.root.remove(); + } + update(_active_ic: DeviceRef) { + this.input.value = this.device.fields.get(this.field)?.value.toString(); + } +} + +export { VMDeviceUI }; diff --git a/www/src/scss/dark.scss b/www/src/scss/dark.scss index 7c627a4..d45c44b 100644 --- a/www/src/scss/dark.scss +++ b/www/src/scss/dark.scss @@ -503,3 +503,26 @@ code { line-height: 0.5rem; font-size: 0.65rem; } + +#vmDevices { + height: 40vh; +} + +#vmDevicesOCBody .card { + width: 24rem; +} + +#vmDevicesOCBody .input-group-text.field_name { + // width: 7rem; + width: 10rem; + padding-right: auto; +} + +#vmDevicesOCBody .input-group-text { + width: 5rem; +} + +#vmDevicesOCBody input { + width: 7rem; + background-color: var(--bs-body-bg); +}