add support for setting points, tokens, and broadcasting control msgs

Control messages are messages that are broadcast to all clients and have
no clearly defined content.  The idea is that this can be used to
control a monitor without having to keep adding support for specific
commands on the protocol layer.
This also changes some of the existing messages and adds another
ridiculous convenience layer to our HTML/JS templating: data-eval.
We should probably just bite the bullet and use some reactive framework.
This commit is contained in:
ducklet 2021-02-01 23:07:49 +01:00
parent f406627042
commit b00f8a357c
4 changed files with 242 additions and 65 deletions

View file

@ -17,12 +17,50 @@ const buzzer_key = {
name: "space bar",
}
const q = (selector, root) => (root || document).querySelector(selector)
const on = (event, cb) => document.addEventListener(event, cb)
function isString(x) {
return typeof x === "string"
}
function isEmpty(x) {
return (Array.isArray(x) ? x : Object.keys(x)).length === 0
}
const q = (selector, root = document) => root.querySelector(selector)
const _evlisteners = {}
/**
* @param {String} event [description]
* @param {Function} cb [description]
* @param {?{once: bool, target: Element = document}} options
*/
function on(event, cb, options = {}) {
const target = options.target || document
if ((_evlisteners[target] ?? {})[event]) {
console.warn(`Replacing previous event listener for '${event}' on target:`, target)
off(event, target)
}
target.addEventListener(event, cb, { once: !!options.once })
if (!_evlisteners[target]) {
_evlisteners[target] = {}
}
_evlisteners[target][event] = cb
}
function off(event, target = document) {
if (!_evlisteners[target] || !_evlisteners[target][event]) {
return
}
target.removeEventListener(event, _evlisteners[target][event])
delete _evlisteners[target][event]
if (isEmpty(_evlisteners[target])) {
delete _evlisteners[target]
}
}
function node(type, { appendTo, cls, text, data, style, ...attrs } = {}) {
let elem = typeof type === "string" ? document.createElement(type) : type
let elem = isString(type) ? document.createElement(type) : type
if (cls) {
if (typeof cls === "string") {
if (isString(cls)) {
elem.className = cls
} else {
for (const [name, on] of Object.entries(cls)) {
@ -63,15 +101,19 @@ let socket,
session_key
function hide(e) {
e = typeof e === "string" ? q(`#${e}`) : e
e = isString(e) ? q(`#${e}`) : e
e.style.display = "none"
}
function show(e) {
e = typeof e === "string" ? q(`#${e}`) : e
e = isString(e) ? q(`#${e}`) : e
e.style.display = "block"
}
function find_client(client_id) {
return clients.find((c) => c.id === client_id)
}
function session_id() {
const match = /^#?(.+)/.exec(document.location.hash)
return match ? match[1] : null
@ -103,23 +145,25 @@ function clear(container) {
}
}
let ul
const player_list = q("#info ul")
function redraw_clients(me, clients) {
if (!me) {
return
}
clear(ul)
clear(player_list)
const player_tpl = q("template#player").content.firstElementChild
for (const c of clients) {
node(player_tpl.cloneNode(), {
text: c.name,
const player_el = node(player_tpl.cloneNode(true), {
data: { cid: c.id },
appendTo: ul,
appendTo: player_list,
cls: {
me: c.id === me.id,
inactive: !c.active,
},
})
q(".name", player_el).textContent = c.name
q(".points", player_el).textContent = c.points
q(".tokens", player_el).textContent = c.tokens
}
}
@ -133,10 +177,10 @@ function highlight(client_id, until_ns) {
console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms)
return
}
for (const li of ul.children) {
for (const li of player_list.children) {
if (li.dataset.cid === client_id) {
li.classList.add("buzzing")
if (Object.keys(highlights).length) {
if (!isEmpty(highlights)) {
li.classList.add("too-late")
} else {
li.classList.add("first")
@ -200,11 +244,61 @@ function setup_ui() {
if (storage["my_name"]) {
username_el.value = storage["my_name"]
}
username_el.addEventListener("change", (event) => {
set_name(event.target.value)
})
on(
"change",
(event) => {
set_name(event.target.value)
},
{ target: username_el },
)
}
ul = q("#info ul")
function disable_player_ui() {
off("focus")
off("blur")
off("keydown")
off("keyup")
node(q("body"), { cls: { player: 0 } })
}
function enable_admin_ui() {
const body = q("body")
if (body.classList.contains("admin")) {
return
}
body.classList.add("admin")
const named = Object.fromEntries(
Array.from(document.querySelectorAll("[name]")).map((el) => [el.name, el]),
)
on(
"click",
({ target }) => {
if (!target.dataset.eval) {
return
}
eval(target.dataset.eval)
},
{ target: q("#points-admin") },
)
on(
"click",
({ target }) => {
let node = target
while (!node.dataset.cid && node.parentElement) {
node = node.parentElement
}
if (!target.dataset.eval) {
return
}
const client = find_client(node.dataset.cid)
if (!client) {
return
}
eval(target.dataset.eval)
},
{ target: q("#info .players") },
)
}
function set_name(name) {
@ -234,44 +328,45 @@ function setup_ws() {
})
socket.addEventListener("message", function (event) {
const msg = JSON.parse(event.data)
if (msg.type === "time") {
servertime = msg.value
const { type, value } = msg
if (type === "time") {
servertime = value.time
toffset_ms = performance.now()
} else if (msg.type === "id") {
me = { id: msg.id, key: msg.key, path: msg.path }
} else if (type === "id") {
me = value
storage["my_uid"] = me.id
storage["my_key"] = me.key
storage["my_sid"] = session_id_from_url(me.path)
redraw_clients(me, clients)
} else if (msg.type === "session_key") {
session_key = { path: msg.path, key: msg.key }
} else if (type === "session_key") {
session_key = value
storage["session_path"] = session_key.path
storage["session_key"] = session_key.key
} else if (msg.type === "buzz") {
const buzztime_ns = msg.time
const client_id = msg.client
disable_player_ui()
enable_admin_ui()
} else if (type === "buzz") {
const buzztime_ns = value.time
const client_id = value.client
const duration_ns = 3 * s_ns
const until_ns = buzztime_ns + duration_ns
highlight(client_id, until_ns)
} else if (msg.type === "clients") {
clients = msg.value
} else if (type === "clients") {
clients = value.clients
redraw_clients(me, clients)
} else if (msg.type === "client") {
const client = { name: msg.name, id: msg.id, active: msg.active }
for (const c of clients) {
if (c.id === client.id) {
c.name = client.name
redraw_clients(me, clients)
return
}
} else if (type === "client") {
const client = value.client
const idx = clients.findIndex((c) => c.id === client.id)
if (idx >= 0) {
clients[idx] = client
} else {
clients.push(client)
}
clients.push(client)
redraw_clients(me, clients)
} else if (msg.type === "error") {
console.error(`Error: ${msg.reason}`)
} else if (type === "error") {
console.error(`Error: ${value.reason}`)
const errorbox = q("#error")
q("code", errorbox).textContent = JSON.stringify(msg, null, 2)
show(errorbox)
q("body").classList.add("error")
} else {
console.error(`Unknown message: ${event.data}`)
}