diff options
author | metamuffin <metamuffin@disroot.org> | 2023-09-07 20:24:21 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-09-07 20:24:21 +0200 |
commit | f5fa4f7d58344c2dc722d1f37c1d7a008f6ee9b3 (patch) | |
tree | c7ac2a7497670745c73f927abf264d63a5a4805d /client-web/source | |
parent | 2d0761b8932f11b01e241e2db3a8f08250efe878 (diff) | |
download | keks-meet-f5fa4f7d58344c2dc722d1f37c1d7a008f6ee9b3.tar keks-meet-f5fa4f7d58344c2dc722d1f37c1d7a008f6ee9b3.tar.bz2 keks-meet-f5fa4f7d58344c2dc722d1f37c1d7a008f6ee9b3.tar.zst |
new element creation helper
Diffstat (limited to 'client-web/source')
-rw-r--r-- | client-web/source/chat.ts | 16 | ||||
-rw-r--r-- | client-web/source/helper.ts | 79 | ||||
-rw-r--r-- | client-web/source/index.ts | 11 | ||||
-rw-r--r-- | client-web/source/logger.ts | 4 | ||||
-rw-r--r-- | client-web/source/menu.ts | 27 | ||||
-rw-r--r-- | client-web/source/preferences/ui.ts | 25 | ||||
-rw-r--r-- | client-web/source/resource/file.ts | 22 | ||||
-rw-r--r-- | client-web/source/resource/track.ts | 16 | ||||
-rw-r--r-- | client-web/source/room.ts | 6 | ||||
-rw-r--r-- | client-web/source/room_watches.ts | 10 | ||||
-rw-r--r-- | client-web/source/user/local.ts | 6 | ||||
-rw-r--r-- | client-web/source/user/mod.ts | 10 |
12 files changed, 108 insertions, 124 deletions
diff --git a/client-web/source/chat.ts b/client-web/source/chat.ts index 7721df3..bbb668f 100644 --- a/client-web/source/chat.ts +++ b/client-web/source/chat.ts @@ -6,7 +6,7 @@ /// <reference lib="dom" /> import { ChatMessage } from "../../common/packets.d.ts"; -import { ediv, esection, espan, image_view, notify } from "./helper.ts"; +import { e, image_view, notify } from "./helper.ts"; import { log } from "./logger.ts"; import { chat_control } from "./menu.ts"; import { PREFS } from "./preferences/mod.ts"; @@ -26,12 +26,12 @@ export class Chat { send.type = "text" send.placeholder = "send a message..." - const messages = ediv({ class: "messages", aria_live: "polite" }) - const controls = ediv({ class: "controls" }) + const messages = e("div", { class: "messages", aria_live: "polite" }) + const controls = e("div", { class: "controls" }) controls.append(send) messages.append(document.createElement("hr")) - this.element = esection({ class: "chat", aria_label: "chat", role: "dialog" }, messages, controls) + this.element = e("section", { class: "chat", aria_label: "chat", role: "dialog" }, messages, controls) this.messages = messages this.controls = controls this.send_el = send @@ -76,13 +76,13 @@ export class Chat { add_message(sender: User, message: ChatMessage) { const els = [] - if (message.text) els.push(espan(message.text, { class: "text" })) + if (message.text) els.push(e("span", { class: "text" }, message.text)) if (message.image) els.push(image_view(message.image, { class: "image" })) chat_control(true) - const e = ediv({ class: "message" }, espan(sender.display_name, { class: "author" }), ": ", ...els) - this.messages.append(e) - e.scrollIntoView({ block: "end", behavior: "smooth", inline: "end" }) + const el = e("div", { class: "message" }, e("span", { class: "author" }, sender.display_name), ": ", ...els) + this.messages.append(el) + el.scrollIntoView({ block: "end", behavior: "smooth", inline: "end" }) let body_str = "(empty message)" if (message.text) body_str = message.text diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts index b4fd72f..ecfdf95 100644 --- a/client-web/source/helper.ts +++ b/client-web/source/helper.ts @@ -7,14 +7,15 @@ import { PREFS } from "./preferences/mod.ts"; -const elem = <K extends keyof HTMLElementTagNameMap>(s: K): HTMLElementTagNameMap[K] => document.createElement(s) - -interface Opts<El> { +interface Opts<E> { class?: string[] | string, - id?: string, src?: string, + id?: string, + src?: string, for?: string, - onclick?: (e: El) => void, - onchange?: (e: El) => void, + type?: string, + href?: string, + onclick?: (e: E) => void, + onchange?: (e: E) => void, role?: "dialog" aria_label?: string aria_live?: "polite" | "assertive" @@ -22,70 +23,34 @@ interface Opts<El> { aria_popup?: "menu" } -function apply_opts<El extends HTMLElement>(e: El, o: Opts<El> | undefined) { - if (!o) return +function apply_opts<E extends HTMLElement>(e: E, o: Opts<E>) { if (o.id) e.id = o.id if (o.onclick) e.onclick = () => o.onclick!(e) if (o.onchange) e.onchange = () => o.onchange!(e) - if (o.aria_label) e.ariaLabel = o.aria_label - if (o.aria_live) e.ariaLive = o.aria_live if (o.for) (e as unknown as HTMLLabelElement).htmlFor = o.for - if (o.aria_modal) e.ariaModal = "true" - if (o.aria_popup) e.ariaHasPopup = o.aria_popup + if (o.type && e instanceof HTMLInputElement) e.type = o.type + if (o.href && e instanceof HTMLAnchorElement) e.href = o.href; if (typeof o?.class == "string") e.classList.add(o.class) if (typeof o?.class == "object") e.classList.add(...o.class) + if (o.aria_modal) e.ariaModal = "true" + if (o.aria_popup) e.ariaHasPopup = o.aria_popup + if (o.aria_label) e.ariaLabel = o.aria_label + if (o.aria_live) e.ariaLive = o.aria_live } -const element = <K extends keyof HTMLElementTagNameMap>(s: K) => (opts?: Opts<HTMLElementTagNameMap[K]>) => { - const e = elem(s) - apply_opts(e, opts) - return e -} -const elem_with_content = <K extends keyof HTMLElementTagNameMap>(s: K) => (c: string, opts?: Opts<HTMLElementTagNameMap[K]>, ...cs: (HTMLElement | string)[]) => { - const e = element(s)(opts) - e.textContent = c - for (const c of cs) { - e.append(c) - } - return e -} -const elem_with_children = <K extends keyof HTMLElementTagNameMap>(s: K) => (opts?: Opts<HTMLElementTagNameMap[K]>, ...cs: (HTMLElement | string)[]) => { - const e = element(s)(opts) - for (const c of cs) { - e.append(c) +export function e<K extends keyof HTMLElementTagNameMap>(name: K, opts: Opts<HTMLElementTagNameMap[K]>, ...children: (HTMLElement | string)[]): HTMLElementTagNameMap[K] { + const el = document.createElement(name) + apply_opts(el, opts) + for (const c of children) { + if (typeof c == "string") el.textContent += c; + else el.append(c) } - return e + return el } -export const ep = elem_with_content("p") -export const eh1 = elem_with_content("h1") -export const eh2 = elem_with_content("h2") -export const eh3 = elem_with_content("h3") -export const eh4 = elem_with_content("h4") -export const eh5 = elem_with_content("h5") -export const eh6 = elem_with_content("h6") -export const epre = elem_with_content("pre") -export const ediv = elem_with_children("div") -export const efooter = elem_with_children("footer") -export const esection = elem_with_children("section") -export const enav = elem_with_children("nav") -export const etr = elem_with_children("tr") -export const etd = elem_with_children("td") -export const eth = elem_with_children("th") -export const espan = elem_with_content("span") -export const elabel = elem_with_content("label") -export const ebutton = elem_with_content("button") -export const ebr = () => document.createElement("br") -export const einput = (type: string, opts: Opts<HTMLInputElement>) => { - const i = element("input")(opts) - i.type = type - return i -} - - export function image_view(url: string, opts?: Opts<HTMLElement>): HTMLElement { const img = document.createElement("img") - apply_opts(img, opts) + apply_opts(img, opts ?? {}) img.src = url img.alt = `Image (click to open)` img.addEventListener("click", () => { diff --git a/client-web/source/index.ts b/client-web/source/index.ts index 639f0c9..f5f6b2d 100644 --- a/client-web/source/index.ts +++ b/client-web/source/index.ts @@ -6,7 +6,7 @@ /// <reference lib="dom" /> import { init_serviceworker } from "./sw/client.ts"; -import { ediv } from "./helper.ts"; +import { e } from "./helper.ts"; import { setup_keybinds } from "./keybinds.ts"; import { log, LOGGER_CONTAINER } from "./logger.ts" import { load_params, PREFS } from "./preferences/mod.ts"; @@ -61,8 +61,8 @@ export async function main() { document.body.querySelectorAll(".loading").forEach(e => e.remove()) const room_secret = load_params().rsecret - if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, "WebRTC not supported.") if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, "This page is not in a 'Secure Context'") + if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, "WebRTC not supported.") if (!globalThis.crypto.subtle) return log({ scope: "crypto", error: true }, "SubtleCrypto not availible") if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, "Your browser does not support the Service Worker API, forced automatic updates are unavoidable.") if (room_secret.length < 8) log({ scope: "crypto", warn: true }, "Room name is very short. e2ee is insecure!") @@ -81,10 +81,13 @@ export async function main() { r = new Room(conn, rtc_config) + conn.control_handler = (a) => r.control_handler(a) + conn.relay_handler = (a, b) => r.relay_handler(a, b) + setup_keybinds(r) r.on_ready = () => { - const sud = ediv({ class: "side-ui" }) - const center = ediv({ class: "main" }, r.element, info_br(), sud) + const sud = e("div", { class: "side-ui" }) + const center = e("div", { class: "main" }, r.element, info_br(), sud) document.body.append(center, control_bar(r, sud)) } diff --git a/client-web/source/logger.ts b/client-web/source/logger.ts index 08dba66..20138b5 100644 --- a/client-web/source/logger.ts +++ b/client-web/source/logger.ts @@ -5,9 +5,9 @@ */ /// <reference lib="dom" /> -import { ediv } from "./helper.ts"; +import { e } from "./helper.ts"; -export const LOGGER_CONTAINER = ediv({ class: "logger-container" }) +export const LOGGER_CONTAINER = e("div", { class: "logger-container" }) const log_scope_color = { "*": "#ff4a7c", diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts index 7bb5b43..583f28b 100644 --- a/client-web/source/menu.ts +++ b/client-web/source/menu.ts @@ -5,12 +5,13 @@ */ /// <reference lib="dom" /> -import { ebutton, ediv, efooter, einput, elabel, enav, ep } from "./helper.ts" +import { e } from "./helper.ts" import { VERSION } from "./index.ts" import { ui_preferences } from "./preferences/ui.ts" import { create_file_res } from "./resource/file.ts"; import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts"; import { Room } from "./room.ts" +import { ui_room_watches } from "./room_watches.ts"; export function info_br() { const item = (name: string, cb: (() => void) | string) => { @@ -25,8 +26,8 @@ export function info_br() { return p } - return efooter({ class: "info-br" }, - ep(`keks-meet ${VERSION}`, { class: "version" }), + return e("footer", { class: "info-br" }, + e("p", { class: "version" }, `keks-meet ${VERSION}`), item("License", "https://codeberg.org/metamuffin/keks-meet/raw/branch/master/COPYING"), item("Source code", "https://codeberg.org/metamuffin/keks-meet"), item("Documentation", "https://codeberg.org/metamuffin/keks-meet/src/branch/master/readme.md"), @@ -36,24 +37,26 @@ export function info_br() { export let chat_control: (s?: boolean) => void; export function control_bar(room: Room, side_ui_container: HTMLElement): HTMLElement { - const leave = ebutton("Leave", { class: "leave", onclick() { window.location.href = "/" } }) + const leave = e("button", { class: "leave", onclick() { window.location.href = "/" } }, "Leave") const chat = side_ui(side_ui_container, room.chat.element, "Chat") const prefs = side_ui(side_ui_container, ui_preferences(), "Settings") + const rwatches = side_ui(side_ui_container, ui_room_watches(), "Known Rooms") const local_controls = [ //ediv({ class: "local-controls", aria_label: "local resources" }, - ebutton("Microphone", { onclick: () => room.local_user.await_add_resource(create_mic_res()) }), - ebutton("Camera", { onclick: () => room.local_user.await_add_resource(create_camera_res()) }), - ebutton("Screen", { onclick: () => room.local_user.await_add_resource(create_screencast_res()) }), - ebutton("File", { onclick: () => room.local_user.await_add_resource(create_file_res()) }), + e("button", { onclick: () => room.local_user.await_add_resource(create_mic_res()) }, "Microphone"), + e("button", { onclick: () => room.local_user.await_add_resource(create_camera_res()) }, "Camera"), + e("button", { onclick: () => room.local_user.await_add_resource(create_screencast_res()) }, "Screen"), + e("button", { onclick: () => room.local_user.await_add_resource(create_file_res()) }, "File"), ] chat_control = chat.set_state; - return enav({ class: "control-bar" }, leave, chat.el, prefs.el, ...local_controls) + return e("nav", { class: "control-bar" }, leave, chat.el, prefs.el, rwatches.el, ...local_controls) } export interface SideUI { el: HTMLElement, set_state: (s?: boolean) => void } export function side_ui(container: HTMLElement, content: HTMLElement, label: string): SideUI { // TODO: close other side uis - const tray = ediv({ class: "side-tray" }, content) - const checkbox = einput("checkbox", { + const tray = e("div", { class: "side-tray" }, content) + const checkbox = e("input", { + type: "checkbox", onchange: () => { if (checkbox.checked) { tray.classList.add("animate-in") @@ -69,7 +72,7 @@ export function side_ui(container: HTMLElement, content: HTMLElement, label: str } }) return { - el: elabel(label, { class: "side-ui-control" }, checkbox), + el: e("label", { class: "side-ui-control" }, label, checkbox), set_state(s) { checkbox.checked = s ?? !checkbox.checked; if (checkbox.onchange) checkbox.onchange(undefined as unknown as Event) } } } diff --git a/client-web/source/preferences/ui.ts b/client-web/source/preferences/ui.ts index 5b8fb5f..ba23489 100644 --- a/client-web/source/preferences/ui.ts +++ b/client-web/source/preferences/ui.ts @@ -5,7 +5,7 @@ */ /// <reference lib="dom" /> -import { ebr, ebutton, ediv, eh2, elabel, espan, etd, etr } from "../helper.ts"; +import { e } from "../helper.ts"; import { PREF_DECLS } from "./decl.ts"; import { change_pref, on_pref_changed, PrefDecl, PREFS } from "./mod.ts"; @@ -78,21 +78,26 @@ export function ui_preferences(): HTMLElement { use_opt_ = use_opt; } - const label = elabel(decl.description ?? `[${key}]`, { for: id }) - return etr({ class: "pref" }, etd({}, label), etd({}, use_opt_ ?? ""), etd({}, prim_control ?? "")) + const label = e("label", { for: id }, decl.description ?? `[${key}]`) + return e("tr", { class: "pref" }, e("td", {}, label), e("td", {}, use_opt_ ?? ""), e("td", {}, prim_control ?? "")) }) - const notification_perm = Notification.permission == "granted" ? ediv() : ediv({}, - espan("For keks-meet to send notifications, it needs you to grant permission: "), - ebutton("Grant", { onclick: () => Notification.requestPermission() }), + const notification_perm = Notification.permission == "granted" ? e("div", {}) : e("div", {}, + e("span", {}, "For keks-meet to send notifications, it needs you to grant permission: "), + e("button", { onclick: () => Notification.requestPermission() }, "Grant"), ) - const reset = ediv({}, - espan("Want to clear all settings? Use this:"), - ebutton("RESET", { onclick: () => { if (confirm("really clear all preferences?")) { localStorage.clear(); window.location.reload() } } }), + const reset = e("div", {}, + e("span", {}, "Want to clear all settings? Use this:"), + e("button", { onclick: () => { if (confirm("really clear all preferences?")) { localStorage.clear(); window.location.reload() } } }, "RESET"), ) const table = document.createElement("table") table.append(...rows) - return ediv({ class: "preferences" }, eh2("Settings"), notification_perm, ebr(), table, ebr(), reset) + return e("div", { class: "preferences" }, + e("h2", {}, "Settings"), + notification_perm, e("br", {}), + table, e("br", {}), + reset + ) } diff --git a/client-web/source/resource/file.ts b/client-web/source/resource/file.ts index 4deb1b6..701383b 100644 --- a/client-web/source/resource/file.ts +++ b/client-web/source/resource/file.ts @@ -5,7 +5,7 @@ */ /// <reference lib="dom" /> -import { display_filesize, ebutton, ediv, espan, sleep } from "../helper.ts"; +import { display_filesize, e, sleep } from "../helper.ts"; import { log } from "../logger.ts"; import { StreamDownload } from "../download_stream.ts"; import { RemoteUser } from "../user/remote.ts"; @@ -16,17 +16,17 @@ const MAX_CHUNK_SIZE = 1 << 15; export const resource_file: ResourceHandlerDecl = { kind: "file", new_remote(info, user, enable) { - const download_button = ebutton("Download", { + const download_button = e("button", { onclick: self => { enable() self.textContent = "Downloading…" self.disabled = true } - }) + }, "Download") return { info, - el: ediv({}, - espan(`File: ${JSON.stringify(info.label)} (${display_filesize(info.size!)})`), + el: e("div", {}, + e("span", {}, `File: ${JSON.stringify(info.label)} (${display_filesize(info.size!)})`), download_button, ), on_statechange(_s) { }, @@ -114,15 +114,15 @@ export function create_file_res(): Promise<LocalResource> { } function file_res_inner(file: File): LocalResource { - const transfers_el = ediv({}) + const transfers_el = e("div", {}) const transfers_abort = new Set<() => void>() return { info: { kind: "file", id: Math.random().toString(), label: file.name, size: file.size }, destroy() { transfers_abort.forEach(abort => abort()) }, - el: ediv({ class: "file" }, - espan(`Sharing file: ${JSON.stringify(file.name)}`), + el: e("div", { class: "file" }, + e("span", {}, `Sharing file: ${JSON.stringify(file.name)}`), transfers_el ), on_request(user, create_channel) { @@ -189,10 +189,10 @@ function file_res_inner(file: File): LocalResource { } function transfer_status_el(remote: RemoteUser) { - const status = espan("…") - const bar = ediv({ class: "progress-bar" }); + const status = e("span", {}, "…") + const bar = e("div", { class: "progress-bar" }); return { - el: ediv({ class: "transfer-status" }, status, bar), + el: e("div", { class: "transfer-status" }, status, bar), set status(s: string) { status.textContent = `${remote.display_name}: ${s}` }, diff --git a/client-web/source/resource/track.ts b/client-web/source/resource/track.ts index 6c461b7..aa2643a 100644 --- a/client-web/source/resource/track.ts +++ b/client-web/source/resource/track.ts @@ -5,7 +5,7 @@ */ /// <reference lib="dom" /> import { ProvideInfo } from "../../../common/packets.d.ts"; -import { ebutton, ediv, elabel } from "../helper.ts"; +import { e } from "../helper.ts"; import { log } from "../logger.ts"; import { on_pref_changed, PREFS } from "../preferences/mod.ts"; import { get_rnnoise_node } from "../rnnoise.ts"; @@ -18,20 +18,20 @@ export const resource_track: ResourceHandlerDecl = { let enable_label = `Enable ${info.track_kind}` if (info.label) enable_label += ` "${info.label}"` - const enable_button = ebutton(enable_label, { + const enable_button = e("button", { onclick: self => { self.disabled = true; self.textContent = "Awaiting track…"; enable() } - }) + }, enable_label) return { info, - el: ediv({}, enable_button), + el: e("div", {}, enable_button), on_statechange() { }, on_enable(track, disable) { this.el.removeChild(enable_button) - this.el.append(ebutton("Disable", { + this.el.append(e("button", { onclick: (self) => { disable() this.el.appendChild(enable_button) @@ -40,7 +40,7 @@ export const resource_track: ResourceHandlerDecl = { enable_button.textContent = enable_label; self.remove() } - })) + }), "Disable") if (!(track instanceof TrackHandle)) return console.warn("aservuoivasretuoip"); this.el.append(create_track_display(track)) } @@ -51,7 +51,7 @@ export const resource_track: ResourceHandlerDecl = { export function new_local_track(info: ProvideInfo, track: TrackHandle, ...extra_controls: HTMLElement[]): LocalResource { return { info, - el: ediv({}, + el: e("div", {}, create_track_display(track), ...extra_controls ), @@ -181,7 +181,7 @@ export async function create_mic_res() { if (mute.checked) gain.gain.value = Number.MIN_VALUE else gain.gain.value = PREFS.microphone_gain } - const mute_label = elabel("Mute", { class: "check-button" }) + const mute_label = e("label", { class: "check-button" }, "Mute") mute_label.prepend(mute) return new_local_track({ id: t.id, kind: "track", track_kind: "audio", label: "Microphone" }, t, mute_label) diff --git a/client-web/source/room.ts b/client-web/source/room.ts index 9d0f33d..ba18162 100644 --- a/client-web/source/room.ts +++ b/client-web/source/room.ts @@ -11,7 +11,7 @@ import { LocalUser } from "./user/local.ts"; import { ClientboundPacket, RelayMessage } from "../../common/packets.d.ts"; import { SignalingConnection } from "./protocol/mod.ts"; import { Chat } from "./chat.ts"; -import { ediv } from "./helper.ts"; +import { e } from "./helper.ts"; export class Room { public remote_users: Map<number, RemoteUser> = new Map() @@ -23,9 +23,7 @@ export class Room { public on_ready = () => { }; constructor(public signaling: SignalingConnection, public rtc_config: RTCConfiguration) { - this.element = ediv({ class: "room", aria_label: "user list" }) - this.signaling.control_handler = (a) => this.control_handler(a) - this.signaling.relay_handler = (a, b) => this.relay_handler(a, b) + this.element = e("div", { class: "room", aria_label: "user list" }) } control_handler(packet: ClientboundPacket) { diff --git a/client-web/source/room_watches.ts b/client-web/source/room_watches.ts new file mode 100644 index 0000000..331022d --- /dev/null +++ b/client-web/source/room_watches.ts @@ -0,0 +1,10 @@ +import { e } from "./helper.ts"; + +export function ui_room_watches(): HTMLElement { + const listing = e("div", {}) + + return e("div", { class: "room-watches" }, + e("h2", {}, "Known Rooms"), + listing + ) +} diff --git a/client-web/source/user/local.ts b/client-web/source/user/local.ts index 15c9390..a1d43bd 100644 --- a/client-web/source/user/local.ts +++ b/client-web/source/user/local.ts @@ -13,7 +13,7 @@ import { User } from "./mod.ts"; import { create_camera_res, create_mic_res, create_screencast_res } from "../resource/track.ts"; import { LocalResource } from "../resource/mod.ts"; import { PREFS } from "../preferences/mod.ts"; -import { ebutton } from "../helper.ts"; +import { e } from "../helper.ts"; export class LocalUser extends User { resources: Map<string, LocalResource> = new Map() @@ -66,14 +66,14 @@ export class LocalUser extends User { r.el.classList.add("resource") r.el.classList.add(`resource-${r.info.kind}`) r.el.append( - ebutton("Stop", { + e("button", { onclick: () => { r.destroy() this.el.removeChild(r.el); this.resources.delete(provide.id) this.room.signaling.send_relay({ provide_stop: { id: provide.id } }) } - }), + }, "Stop"), ) } } diff --git a/client-web/source/user/mod.ts b/client-web/source/user/mod.ts index 93042ca..67b3cd5 100644 --- a/client-web/source/user/mod.ts +++ b/client-web/source/user/mod.ts @@ -5,7 +5,7 @@ */ /// <reference lib="dom" /> -import { ediv, epre, espan } from "../helper.ts"; +import { e } from "../helper.ts"; import { Room } from "../room.ts"; export class User { @@ -14,12 +14,12 @@ export class User { get name() { return this._name } get display_name() { return this.name ?? "Unknown" } - name_el = espan(this.display_name) - stats_el = epre("") - el = ediv({ class: "user" }) + name_el = e("span", {}, this.display_name) + stats_el = e("pre", {}) + el = e("div", { class: "user" }) constructor(public room: Room, public id: number) { - const info_el = ediv({ class: "info" }) + const info_el = e("div", { class: "info" }) this.name_el.textContent = this.display_name this.name_el.classList.add("name") info_el.append(this.name_el, this.stats_el) |