all repos — felt @ 7a294373c5340edcfebfec5cd21a2c0ef0cdda8f

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

clean up dom access and IDs
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmS3lAoACgkQO3+8IhRO
Y5jIOw//Q8RUloxGMxor/tmxsvrexXOte7toHFwF7zRz4AWM4LtAsbMXbj0q2F9e
h/2jt0aYmgxjRDRn8IZFM/HnM+p0cXwFwPC3aeAck+627T/1EPtCdtGygN41Qm6c
daBHu3SOAkF/2SuN2n9F9wSVkgpOV5uRwiBp85RJB8g6ZutkIlEbdkfYG+s+xjya
z6cjZb17Xs9ftboi62zOclmUQUTbwV5FoiwsRDvrR2oNkeiYsezuAaxXbJisffFs
ggR+eKG7UqZzXyBGY1Gkq3+gQipHcyO+Q32mzrlnycWAQZGT/gZH9gkVger6GtTb
HVsleTA+FlSCnnO1jQZBROfh1vIlhyrcQeJms5o8owWNcK5d6VjVUjJYoVBKAtDb
FHwisKs74M3aHaIXNULAq7B9K3Y36lg2Frw0G0U9azkHMXSPpuYZAn2QfhkSgi9O
jCF7BuNYLKHmym/W2UAvqr9P3v/40VlZui38fzkGbdVV4L/+0p7RbuYX1pRb05Vb
Aw6H1L15arbh0r2bD+f8BPlu7bwon/CZw9yBLT+MgGOmQMn9EjpDZdsOhZDuxN0m
dz7C9DyCg5c2HtTTRiZoGDIJ8qh4n8nINMuNPaWBma9OVKfWK9Bmv8EfRmM4qQK8
DcpxkMS2dbgvIBAtHhr2Tmqv3is9fqOoTpqDTB8LHBnRpQXUtVg=
=DpqG
-----END PGP SIGNATURE-----
commit

7a294373c5340edcfebfec5cd21a2c0ef0cdda8f

parent

331150a0ffc139f0d09f0ecac27f44f02a1bbd33

M static/admin.jsstatic/admin.js

@@ -1,28 +1,10 @@

let adminToken = null; -const adminWrapper = document.getElementById("adminWrapper"); -const adminZone = document.getElementById("adminZone"); -const spriteZone = document.getElementById("spriteZone"); -const createTableForm = document.getElementById("createTableForm"); -const createTokenForm = document.getElementById("createTokenForm"); -const tokenWrapper = document.getElementById("adminWrapper_tokens"); -const newTableName = document.getElementById("newTableName"); -const newTablePass = document.getElementById("newTablePass"); -const tokenSpriteDropdown = document.getElementById("token_combobox"); -const tokenName = document.getElementById("token_name"); -const tokenWidth = document.getElementById("token_width"); -const tokenHeight = document.getElementById("token_height"); -const tokenCX = document.getElementById("token_cx"); -const tokenCY = document.getElementById("token_cy"); -const previewZone = document.getElementById("tokenPreview_zone"); -const tokenAspect = document.getElementById("tokenKeepAspect"); -const aspectLockLabel = document.getElementById("aspectLockLabel"); -const tokenZone = document.getElementById("tokenZone"); -async function rebindUi(name, pass) { +async function loadAdmin(name, pass) { try { const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); - const res = await fetch(`/admin/api/table/${name}?passcode=${pass}`, { + const tableData = await fetch(`/admin/api/table/${name}?passcode=${pass}`, { method: 'GET', headers: headers, });

@@ -37,71 +19,70 @@ method: 'GET',

headers: headers, }); - let infoHtml = "" - if (res.ok) { - document.getElementById("input_table_name").value = name; - document.getElementById("input_table_pass").value = pass; + let mgmtHTML = "" + if (tableData.ok) { + $("input_table_name").value = name; + $("input_table_pass").value = pass; dial(); - const table = await res.json() - infoHtml = "<a href='#' onclick='getTables();return false;'>&larr; table list</a><br>"; - infoHtml += `<textarea id='auxMsgZone'>${table.auxMsg}</textarea><br><button onclick='publishAuxMsg()'>Set Status</button>` - infoHtml += "<button onclick='destroyTable()'>Destroy Table</button><br/>"; - infoHtml += "<input id='map_img_upload' type='file'/><button onclick='uploadMapImg()'>Upload Map</button><br/>" + + const table = await tableData.json() + + mgmtHTML = `<a href='#' onclick='getTables();return false;'>&larr; table list</a><br>\n` + + `<textarea id='aux_msg_input'>${table.auxMsg}</textarea><br><button onclick='publishAuxMsg()'>Set Status</button>\n` + + `<button onclick='destroyTable()'>Destroy Table</button><br/>\n` + + `<input id='map_img_upload' type='file'/><button onclick='uploadImg("map")'>Upload Map</button><br/>\n` + if (mapImgs.ok) { - infoHtml += "<label>Available Maps</label>"; const imgs = (await mapImgs.json()).sort(); - infoHtml += "<ul class='two_btn_list'>"; - for (const i of imgs) { + mgmtHTML = imgs.reduce((s, i) => { const parts = i.split("/"); - infoHtml += `<li><a href="${i}" target="_blank">${parts[parts.length - 1]}</a> <button onclick="sendMapImg('${i}');">Set</button> <button onclick="deleteImg('${i}')">Delete</button></li>\n`; - } - infoHtml += "</ul>"; + return s + `<li><a href="${i}" target="_blank">${parts[parts.length - 1]}</a> <button onclick="sendMapImg('${i}');">Set</button> <button onclick="deleteImg('${i}')">Delete</button></li>\n`; + }, mgmtHTML + "<label>Available Maps</label><ul class='two_btn_list'>") + "</ul>"; + } else { - infoHtml += "<label>Maps couldn't be retrieved</label>"; + mgmtHTML += "<label>Maps couldn't be retrieved</label>"; } - adminZone.innerHTML = infoHtml; + $("table_management").innerHTML = mgmtHTML; + + let spriteListHTML = `<input id='token_img_upload' type='file'/><button onclick='uploadImg("token")'>Upload Sprite</button><br/>`; - let tokenListHTML = "<input id='token_img_upload' type='file'/><button onclick='uploadTokenImg()'>Upload Sprite</button><br/>"; if (tokenImgs.ok) { - const tokens = (await tokenImgs.json()).sort(); - tokenListHTML += "<ul class='single_btn_list'>"; - for (const t of tokens) { + const sprites = (await tokenImgs.json()).sort(); + spriteListHTML = sprites.reduce((s, t) => { const parts = t.split("/"); - tokenListHTML += `<li><a href="${t}" target="_blank">${parts[parts.length - 1]}</a> <button onclick="deleteImg('${t}')">Delete</button></li>\n` - } - tokenListHTML += "</ul>"; - fillSpriteDropdown(tokens); + return s + `<li><a href="${t}" target="_blank">${parts[parts.length - 1]}</a> <button onclick="deleteImg('${t}')">Delete</button></li>\n`; + }, spriteListHTML + "<ul class='single_btn_list'>") + "</ul>"; + fillSpriteDropdown(sprites); } else { - tokenListHTML += "<label>Sprites couldn't be retrieved</label>" + spriteListHTML += "<label>Sprites couldn't be retrieved</label>" } - spriteZone.innerHTML = tokenListHTML; - tokenWrapper.style.display = "inline"; + $("sprite_zone").innerHTML = spriteListHTML; + $("token_management").style.display = "inline"; } else { console.log(res.status); } } catch (err) { + console.dir(err) setErr(`${err.name}: ${err.message}`); } } -function fillSpriteDropdown(tokens) { - tokens = tokens.sort(); +function fillSpriteDropdown(sprites) { + const dropdown = $("sprite_dropdown"); let options = "<option value=''>select</option>"; - for (const t of tokens) { + dropdown.innerHTML = sprites.reduce((s, t) => { const parts = t.split("/"); - const o = `<option value="${t}">${parts[parts.length - 1]}</option>\n`; - options += o; - } - tokenSpriteDropdown.innerHTML = options; + return s + `<option value="${t}">${parts[parts.length - 1]}</option>\n`; + }, "<option value=''>select</option>"); } function previewSprite(source) { if (source) { switch (source.id) { - case "token_combobox": + case "sprite_dropdown": reinitializeSpritePreview(); break; case "token_cx":

@@ -116,18 +97,24 @@ }

} function toggleAspectLock() { - if (tokenKeepAspect.checked) { - aspectLockLabel.innerHTML = "&#128274;" - } else { - aspectLockLabel.innerHTML = "&#128275;" - } + try { + const locked = $("token_aspect_lock").checked; + $("aspect_lock_label").innerHTML = locked ? "&#128274;" : "&#128275;" + } catch {} } function scaleSpritePreview(source) { - if (mapImg && mapImg._image) { + const tokenHeight = $("token_height"); + const tokenWidth = $("token_width"); + const tokenCX = $("token_cx"); + const tokenCY = $("token_cy"); + const tokenAspect = $("token_aspect_lock"); + const preview = $("token_admin_preview"); + + if (mapImg && mapImg._image && tokenHeight && tokenWidth && tokenCX && tokenCY && tokenAspect && preview) { const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; const keepAspect = tokenAspect.checked; - const img = previewZone.children[0]; + const img = preview.children[0]; tokenHeight.value = Math.floor(Number(tokenHeight.value)) tokenWidth.value = Math.floor(Number(tokenWidth.value))

@@ -165,12 +152,20 @@ }

function drawTokenOrigin() { - if (tokenSpriteDropdown.selectedIndex >= 0) { + const dropdown = $("sprite_dropdown"); + const tokenWidth = $("token_width"); + const tokenHeight = $("token_height"); + const tokenCX = $("token_cx"); + const tokenCY = $("token_cy"); + const preview = $("token_admin_preview"); + + if (dropdown && tokenWidth && tokenHeight && tokenCX && tokenCY && preview + && dropdown.selectedIndex >= 0) { tokenCX.value = Math.floor(Number(tokenCX.value)) tokenCY.value = Math.floor(Number(tokenCY.value)) - const img = previewZone.children[0]; + const img = preview.children[0]; const x = Number(tokenWidth.value) / Number(tokenCX.value); const y = Number(tokenHeight.value) / Number(tokenCY.value);

@@ -182,67 +177,82 @@ originImg.style.position = "absolute";

originImg.style.left = (origin.x - 2) + "px"; originImg.style.top = (origin.y - 2) + "px"; - if (previewZone.children.length > 1) { - previewZone.replaceChild(originImg, previewZone.children[1]); + if (preview.children.length > 1) { + preview.replaceChild(originImg, preview.children[1]); } else { - previewZone.appendChild(originImg); + preview.appendChild(originImg); } } } function reinitializeSpritePreview(existing = false) { - const img = document.createElement("img"); - img.src = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; + + const dropdown = $("sprite_dropdown"); + const tokenName = $("token_name"); + const tokenWidth = $("token_width"); + const tokenHeight = $("token_height"); + const tokenCX = $("token_cx"); + const tokenCY = $("token_cy"); + const preview = $("token_admin_preview"); - if (!existing) { - const tokenNameParts = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].text.split("."); - tokenNameParts.pop(); - tokenName.value = tokenNameParts.join("."); - } + if (dropdown && tokenName && tokenWidth && tokenHeight && tokenCX && tokenCY && preview) { + + const img = document.createElement("img"); + img.src = dropdown[dropdown.selectedIndex].value; + + if (!existing) { + const tokenNameParts = dropdown[dropdown.selectedIndex].text.split("."); + tokenNameParts.pop(); + tokenName.value = tokenNameParts.join("."); + } - img.onload = () => { - const w = img.naturalWidth; - const h = img.naturalHeight; + img.onload = () => { + const w = img.naturalWidth; + const h = img.naturalHeight; - if (!existing) { - tokenWidth.value = w; - tokenHeight.value = h; - tokenCX.value = "" - tokenCY.value = "" + if (!existing) { + tokenWidth.value = w; + tokenHeight.value = h; + tokenCX.value = ""; + tokenCY.value = ""; + } + scaleSpritePreview(); } - scaleSpritePreview(); - } - previewZone.innerHTML = ""; - previewZone.appendChild(img); + preview.innerHTML = ""; + preview.appendChild(img); + } } function createToken() { - const w = Number(tokenWidth.value); - const h = Number(tokenHeight.value); - const oX = Number(tokenCX.value); - const oY = Number(tokenCY.value); - const img = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; - const name = tokenName.value; + const dropdown = $("sprite_dropdown"); + try { + const w = Number($("token_width").value); + const h = Number($("token_height").value); + const oX = Number($("token_cx").value); + const oY = Number($("token_cy").value); + const img = dropdown[dropdown.selectedIndex].value; + const name = $("token_name").value; - if (!isNaN(w) && !isNaN(h) && !isNaN(oX) && !isNaN(oY) && img && name) { - // send it on the websocket and wait for it to come back - const [x, y] = [0, 0]; - sendToken({ - w: w, - h: h, - ox:oX, - oy:oY, - x: x, - y: y, - sprite: img, - name: name, - active: false} - ); - setTokenCreateFormVisible(false); - return; - } - setErr("All token fields are required"); + if (!isNaN(w) && !isNaN(h) && !isNaN(oX) && !isNaN(oY) && img && name) { + // send it on the websocket and wait for it to come back + const [x, y] = [0, 0]; + sendToken({ + w: w, + h: h, + ox:oX, + oy:oY, + x: x, + y: y, + sprite: img, + name: name, + active: false + }); + setTokenCreateFormVisible(false); + return; + } + setErr("All token fields are required"); + } catch {} } function destroyToken(id) {

@@ -257,21 +267,27 @@ }

} function previewExistingToken(id) { - const existing = tokens.find(t=>t.t.id == id); - if (existing) { - tokenWidth.value = existing.t.w; - tokenHeight.value = existing.t.h; - tokenCX.value = existing.t.oX; - tokenCY.value = existing.t.oY; - tokenName.value = existing.t.name; - for (let i = 0; i < tokenSpriteDropdown.options.length; i++) { - if (tokenSpriteDropdown.options[i].value == existing.t.sprite) { - tokenSpriteDropdown.selectedIndex = i; - break; + try { + const existing = tokens.find(t=>t.t.id == id); + const dropdown = $("sprite_dropdown"); + if (existing) { + $("token_width").value = existing.t.w; + $("token_height").value = existing.t.h; + $("token_cx").value = existing.t.oX; + $("token_cy").value = existing.t.oY; + $("token_name").value = existing.t.name; + for (let i = 0; i < dropdown.options.length; i++ ) { + if (dropdown.options[i].value === existing.t.sprite) { + dropdown.selectedIndex = i; + break; + } } + reinitializeSpritePreview(true); } - reinitializeSpritePreview(true); + } catch (err) { + console.log(err); } + } function copyToken(id) {

@@ -280,20 +296,19 @@ previewExistingToken(id);

} function renderTokenMasterList() { - if (tokenZone) { - let tokenMasterListHTML = ""; - const scroll = tokenZone.scrollTop; - for (const t of tokens) { - tokenMasterListHTML += `<li><a href="#" onclick="previewExistingToken('${t.t.id}');return false">${t.t.name}</a><button onclick="copyToken('${t.t.id}')">Copy</button><button onclick="destroyToken('${t.t.id}')">Destroy</button></li>\n`; - } - tokenZone.innerHTML = tokenMasterListHTML; - tokenZone.scrollTop = scroll; + const tokenList = $("token_list"); + if (tokenList) { + const scroll = tokenList.scrollTop; + tokenList.innerHTML = tokens.reduce((s, t) => { + return s + `<li><a href="#" onclick="previewExistingToken('${t.t.id}');return false">${t.t.name}</a><button onclick="copyToken('${t.t.id}')">Copy</button><button onclick="destroyToken('${t.t.id}')">Destroy</button></li>\n`; + }, ""); + tokenList.scrollTop = scroll; } } function publishAuxMsg() { - const txtArea = document.getElementById("auxMsgZone"); - if (txtArea != null) { + const txtArea = $("aux_msg_input"); + if (txtArea) { publish({auxMsg: txtArea.value, auth: adminToken.access_token}); } }

@@ -306,54 +321,30 @@ function sendToken(t) {

publish({token: t, auth: adminToken.access_token}); } -async function uploadMapImg() { +async function uploadImg(imgType) { try { - var input = document.getElementById("map_img_upload"); + var input = $(`${imgType}_img_upload`); var data = new FormData(); data.append('file', input.files[0]); data.append('name', tableKey.name); data.append('passcode', tableKey.passcode); const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); - res = await fetch(`/admin/api/upload/${tableKey.name}/map/`, { + res = await fetch(`/admin/api/upload/${tableKey.name}/${imgType}/`, { headers: headers, method: "POST", body: data, }); if (res.ok) { - // refresh so we can see the new entry in the list - rebindUi(tableKey.name, tableKey.passcode); + loadAdmin(tableKey.name, tableKey.passcode); } else { - throw new Error("Something went wrong uploading the map BG..."); + throw new Error("Something went wrong uploading the ${imgType} image..."); } } catch (err) { setErr(`${err.name}: ${err.message}`); } } -async function uploadTokenImg() { - try { - var input = document.getElementById("token_img_upload"); - var data = new FormData(); - data.append('file', input.files[0]); - data.append('name', tableKey.name); - data.append('passcode', tableKey.passcode); - const headers = new Headers(); - headers.set('Authorization', 'Bearer ' + adminToken.access_token); - res = await fetch(`/admin/api/upload/${tableKey.name}/token/`, { - headers: headers, - method: "POST", - body: data, - }); - if (res.ok) { - // refresh so we can see the new entry in the list - rebindUi(tableKey.name, tableKey.passcode); - } else { - throw new Error("Something went wrong uploading the token sprite..."); - } - } catch (err) { - setErr(`${err.name}: ${err.message}`); - } -} + async function deleteImg(url) { try { if (url.startsWith("/uploads/")) {

@@ -371,8 +362,7 @@ method: "DELETE",

}); if (res.ok) { - // refresh UI - rebindUi(tableKey.name, tableKey.passcode); + loadAdmin(tableKey.name, tableKey.passcode); } else { throw new Error ("Something went wrong deleting the image..."); }

@@ -393,12 +383,10 @@ headers: headers,

body: JSON.stringify(tableKey) }); if (res.ok) { - conn.close(1000); - initializeMap(""); + leave(); getTables(); - } else { - setErr(await res.json()); + setErr("Error destroying table"); } } } catch (err) {

@@ -406,49 +394,58 @@ setErr(`${err.name}: ${err.message}`);

} } +function sortByName(a, b) { + return (a.name < b.name) + ? -1 + : ((a.name > b.name) + ? 1 + : 0) +} + async function getTables() { try { + const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); const res = await fetch('/admin/api/table/', { method: 'GET', headers: headers }); + if (res.ok) { - const tableList = await res.json(); - tableList.sort((a,b)=>{ - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } - return 0; - }); - let tableListHTML = "<ul>\n"; - for (const t of tableList) { - tableListHTML += `<li><a href="#" onclick="rebindUi('${t.name}','${t.passcode}');return false;">${t.name}</a></li>\n` - } + const tblMgmt = $("table_management"); + const tableList = (await res.json()).sort(sortByName); + + tblMgmt.innerHTML = tableList.reduce((s, t)=>{ + return s + `<li><a href="#" onclick="loadAdmin('${t.name}','${t.passcode}');return false;">${t.name}</a></li>\n` + }, "<ul>\n" ) + "</ul>"; tableListHTML += "</ul>" - adminZone.innerHTML = tableListHTML; - tokenWrapper.style.display = "none"; + tblMgmt.style.display = "none"; + } else { // fail silently } + } catch { // fail silently } } async function doLogin() { - const adminUsrInput = document.getElementById("name_entry"); - const adminPassInput = document.getElementById("input_admin_pass"); + const adminUsrInput = $("name_entry"); + const adminPassInput = $("input_admin_pass"); + const adminUI = $("admin_ui"); + const tblMgmt = $("table_management"); + const tokenMgmt = $("token_management"); - if (adminUsrInput && adminPassInput) { + if (adminUsrInput && adminPassInput && adminUI && tblMgmt && tokenMgmt) { adminToken = await getAdminToken(adminUsrInput.value, adminPassInput.value); if (adminToken) { getTables(); - adminWrapper.style.display="inline"; - adminZone.style.display = "block"; + + tblMgmt.style.display = "block"; + tokenMgmt.style.display = "none"; + adminUI.style.display="inline"; closeErr(); replaceAdminModal(); } else {

@@ -458,7 +455,7 @@ }

} function replaceAdminModal() { - const adminModal = document.getElementById("admin_modal"); + const adminModal = $("admin_modal"); if (adminModal) { adminModal.innerHTML = "<a href='./'>Logout</a>"; }

@@ -483,61 +480,66 @@ }

} function setTableCreateFormVisible(v) { - if (createTableForm) { - createTableForm.style.display = v ? "block" : "none"; - } - if (!v) { - if (newTableName) { - newTableName.value = ""; + try { + $("table_creation_form").style.display = v ? "block" : "none"; + if (!v) { + $("new_table_name").value = ""; + $("new_table_pass").value = ""; } - if (newTablePass) { - newTablePass.value = ""; - } - } + } catch {} } function setTokenCreateFormVisible(v) { - if (createTokenForm && tokenZone) { + try { if (v) { - // clear the form when displaying because we may have values from a preview of an existing token - tokenWidth.value = ""; - tokenHeight.value = ""; - tokenCX.value = ""; - tokenCY.value = ""; - tokenName.value = ""; - tokenSpriteDropdown.selectedIndex = 0; + $("token_width").value = ""; + $("token_height").value = ""; + $("token_cx").value = ""; + $("token_cy").value = ""; + $("token_name").value = ""; + $("sprite_dropdown").selectedIndex = 0; } - createTokenForm.style.display = v ? "block" : "none"; - tokenZone.style.display = v ? "none" : "block"; - previewZone.innerHTML = ""; - } + $("token_creation_form").style.display = v ? "block" : "none"; + $("token_list").style.display = v ? "none" : "block"; + $("token_admin_preview").innerHTML = ""; + } catch {} } async function createTable() { - const headers = new Headers(); - headers.set('Authorization', 'Bearer ' + adminToken.access_token); + const newTableName = $("new_table_name"); + const newTablePass = $("new_table_pass"); + + if (newTableName && newTablePass) { + const headers = new Headers(); + headers.set('Authorization', 'Bearer ' + adminToken.access_token); - const formData = new FormData(); - formData.set("name", newTableName.value); - formData.set("passcode", newTablePass.value); - - let bodyStr = "{"; - for (const pair of formData.entries()) { - bodyStr += `"${pair[0]}": "${pair[1]}",`; - } - bodyStr = bodyStr.slice(0, -1); - bodyStr += "}"; - const res = await fetch('/admin/api/table/', { - method: 'POST', - headers: headers, - body: bodyStr, - }); - if (res.ok) { - getTables(); - setTableCreateFormVisible(false); - } else if (res.status === 422) { - setErr('Table name and passcode must be only alphanumeric and underscores'); - } else { - setErr('Error creating table'); + const formData = new FormData(); + formData.set("name", newTableName.value); + formData.set("passcode", newTablePass.value); + + let bodyStr = "{"; + for (const pair of formData.entries()) { + bodyStr += `"${pair[0]}": "${pair[1]}",`; + } + bodyStr = bodyStr.slice(0, -1); + bodyStr += "}"; + + try { + const res = await fetch('/admin/api/table/', { + method: 'POST', + headers: headers, + body: bodyStr, + }); + if (res.ok) { + getTables(); + setTableCreateFormVisible(false); + } else if (res.status === 422) { + setErr("Table name and passcode must be only alphanumeric and underscores"); + } else { + setErr("Error creating table"); + } + } catch { + setErr("Error creating table"); + } } }
M static/dice.jsstatic/dice.js

@@ -1,10 +1,10 @@

function rollDice() { - const name = document.getElementById("name_entry"); - const numDice = document.getElementById("num_dice"); - const faces = document.getElementById("dice_faces"); - const note = document.getElementById("dice_note"); + const name = $("name_entry"); + const numDice = $("num_dice"); + const faces = $("dice_faces"); + const note = $("dice_note"); - if (conn == null || table == null) { + if (!conn || !tableKey.name) { setErr("Looks like you haven't joined a table yet."); return; }
M static/index.htmlstatic/index.html

@@ -16,7 +16,7 @@ <p>Enable Javascript in your browser to get your game on!</p></div>

</noscript> <div id="map"></div> - <div id="errWrapper" style='display:none'><button id="closeErr" onclick="closeErr()">x</button><div id="errDiv"></div></div> + <div id="err_wrapper" style='display:none'><button id="err_close" onclick="closeErr()">x</button><div id="err_div"></div></div> <nav> <section id="user_section">

@@ -74,9 +74,9 @@

<details class="ui_win"><summary>status</summary><pre id="aux"></pre></details><br/> <details class="ui_win"><summary>token select</summary> - <button id="tokenPreview_alt_clear" onclick="dismissPreview()" style="display: none;">Dismiss Preview</button> - <div id="tokenPreview_alt"></div> - <input hidden id="tokenPreview_alt_id"/> + <button id="token_preview_clear" onclick="dismissPreview()" style="display: none;">Dismiss Preview</button> + <div id="token_preview"></div> + <input hidden id="token_preview_id"/> <ul id="token_select" class="single_btn_list"></ul> </details><br/> </div>

@@ -92,38 +92,38 @@ </form>

</details> <br/> - <div id="adminWrapper" style="display:none;"> + <div id="admin_ui" style="display:none;"> <details id="admin_table_win" class="ui_win admin_win"><summary>table</summary> <button onclick="setTableCreateFormVisible(true)">New Table</button> - <form onsubmit="return false" id="createTableForm" style="display:none;"> - <label>Name<input id="newTableName"/></label><br/> - <label>Passcode<input id="newTablePass" type="password"/></label><br/> + <form onsubmit="return false" id="table_creation_form" style="display:none;"> + <label>Name<input id="new_table_name"/></label><br/> + <label>Passcode<input id="new_table_pass" type="password"/></label><br/> <button type="submit" onclick="createTable()">Create</button> <button onclick="setTableCreateFormVisible(false)">Cancel</button> </form> - <div id="adminZone"></div> + <div id="table_management"></div> </details><br/> - <div id="adminWrapper_tokens"> + <div id="token_management"> <details id="admin_token_win" class="ui_win admin_win"><summary>tokens</summary> <button onclick="setTokenCreateFormVisible(true)">New Token</button> - <form onsubmit="return false" id="createTokenForm" style="display:none;"> - <label>Sprite<select id="token_combobox" onchange="previewSprite(this)"></select></label><br/> + <form onsubmit="return false" id="token_creation_form" style="display:none;"> + <label>Sprite<select id="sprite_dropdown" onchange="previewSprite(this)"></select></label><br/> <label>Name<input id="token_name"/></label><br/> - <label>Width<input type="number" id="token_width" min="1" max="9999" step="1" onchange="previewSprite(this)"/></label><label id="aspectLockLabel" for="tokenKeepAspect">&#128274;</label><input type="checkbox" checked id="tokenKeepAspect" onchange="toggleAspectLock()"/><br/> + <label>Width<input type="number" id="token_width" min="1" max="9999" step="1" onchange="previewSprite(this)"/></label><label id="aspect_lock_label" for="token_aspect_lock">&#128274;</label><input type="checkbox" checked id="token_aspect_lock" onchange="toggleAspectLock()"/><br/> <label>Height<input type="number" id="token_height" min="1" step="1" max="9999" onchange="previewSprite(this)"/></label><br/> <label>cX<input type="number" id="token_cx" min="0" max="9999" step="1" onchange="previewSprite(this)"/></label><br/> <label>cY<input type="number" id="token_cy" min="0" max="9999" step="1" onchange="previewSprite(this)"/></label><br/> <button type="submit" onclick="createToken()">Create</button> <button onclick="setTokenCreateFormVisible(false)">Cancel</button> </form> - <div id="tokenPreview_zone"></div> - <ul id="tokenZone" class="two_btn_list"></ul> + <div id="token_admin_preview"></div> + <ul id="token_list" class="two_btn_list"></ul> </details><br/> <details id="admin_sprite_win" class="ui_win admin_win"><summary>sprites</summary> - <div id="spriteZone"></div> + <div id="sprite_zone"></div> </details> </div>
M static/map.jsstatic/map.js

@@ -8,18 +8,32 @@ function initializeMap(mapImgUrl) {

let init = false; if (!map) { init = true; - map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple, attributionControl: false, zoomControl: false }); - map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();scaleAltSpritePreview();}); + map = L.map("map", { + minZoom: 0, + maxZoom: 4, + crs: L.CRS.Simple, + attributionControl: false, + zoomControl: false + }); + map.on("zoomend", () => { + resizeMarkers(); + scaleSpritePreview(); + scaleAltSpritePreview(); + }); } + if (mapImg) { mapImg.removeFrom(map); } + mapImg = L.imageOverlay(mapImgUrl, worldBounds); mapImg.addTo(map); map.setMaxBounds(cameraBounds); + if (init) { map.setView([0,0], 2); } + resizeMarkers(); }

@@ -92,7 +106,6 @@ const existing = tokens.find(t=>t.t.id == tokenId);

if (existing) { const self = Object.assign({}, existing.t); self.active = !self.active; - console.log(self); publish({token: stripToken(self)}); } }

@@ -104,7 +117,6 @@ const self = Object.assign({}, existing.t);

const realPos = existing.m.getLatLng(); self.x = realPos.lng; self.y = realPos.lat; - publish({token: stripToken(self)}); } }

@@ -131,7 +143,9 @@ title: token.name,

draggable: true, autoPan: true }); + marker.on("moveend", ()=>{moveToken(token.id)}); + const node = document.createElement('div'); node.innerText = token.name; marker.bindPopup(node);
M static/socket.jsstatic/socket.js

@@ -1,177 +1,18 @@

-let tableKey = { +const tableKey = { name: "", passcode: "" } let conn = null; let offline = false; -let msgQ = []; - -const lagDiv = document.getElementById("lag"); -const secondaryPreviewZone = document.getElementById("tokenPreview_alt"); -const secondaryPreviewIdInput = document.getElementById("tokenPreview_alt_id"); -const dismissPreviewBtn = document.getElementById("tokenPreview_alt_clear"); - -function showTableModal(show) { - const modal = document.getElementById("table_modal"); - if (modal) { - modal.style.display = show ? "block" : "none"; - } -} - -function fmtLeading(n) { - return n < 10 ? "0" + n : String(n); -} - -function formatDice(r) { - const date = new Date(r.timestamp) - const p = document.createElement("p"); - - const month = date.getMonth() + 1; - const day = date.getDate(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - const seconds = date.getSeconds(); - - p.innerHTML = `${date.getFullYear()}-${fmtLeading(month)}-${fmtLeading(day)} ${fmtLeading(hours)}:${fmtLeading(minutes)}:${fmtLeading(seconds)} ${r.player} rolled ${r.roll.length}d${r.faces} ${(r.note ? "(" + r.note + ")" : "")}<br>[${r.roll}] (total ${r.roll.reduce((a,c)=>a+c,0)})`; - - return p; -} - -function logDice(dice) { - const diceLog = document.getElementById("dice_log"); - if (!Array.isArray(dice)) { - dice = [ dice ]; - } else { - if (diceLog) { - diceLog.innerHTML = ""; - } - } - if (diceLog) { - for(const r of dice) { - try{ - const p = formatDice(r); - diceLog.append(p); - p.scrollIntoView(); - } catch{} - } - } -} - -function setAuxMsg(msg) { - const auxDiv = document.getElementById("aux"); - if (auxDiv) { - auxDiv.innerText = msg; - } -} - -function updateTokens(tokens) { - // update internal token array and map - processTokens(tokens); - // update token select window - renderTokenSelect(); - // if admin, update token master list - renderTokenMasterList(); -} - -function renderTokenSelect() { - const tokenSelect = document.getElementById("token_select"); - let tokenSelectHTML = "" - const scroll = tokenSelect.scrollTop; - for (const t of tokens) { - tokenSelectHTML += `<li><a href="#" onclick="initSpritePreviewById('${t.t.id}')">${t.t.name}</a><button onclick="toggleActive('${t.t.id}');return false">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\n`; - } - tokenSelect.innerHTML = tokenSelectHTML; - tokenSelect.scrollTop = scroll; -} - -// the following few functions aren't socket related but they directly relate to the previous function - -function initSpritePreviewById(id) { - const token = tokens.find(t=>t.t.id == id); - let img = null; - - if (token && id) { - - img = document.createElement("img"); - img.src = token.t.sprite; - secondaryPreviewIdInput.value = id; - - - img.onload = () => { - scaleAltSpritePreview(); - } - } - dismissPreviewBtn.style.display = (token && id) ? "block" : "none"; - secondaryPreviewZone.innerHTML = ""; - if (img) { - secondaryPreviewZone.appendChild(img); - } -} - -function dismissPreview() { - initSpritePreviewById(null); -} - -function scaleAltSpritePreview() { - if (mapImg && mapImg._image) { - const id = secondaryPreviewIdInput.value; - const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; - const img = secondaryPreviewZone.children[0]; - const token = tokens.find(t=>t.t.id == id); - if (img && token) { - img.width = token.t.w * scaleFactor; - img.height = token.t.h * scaleFactor; - - } - } -} - -function makeUpToDate(msg) { - if (msg) { - - // map image has to be set before tokens can be handled! - if (msg.mapImg) { - setMapImg(msg.mapImg); - } - - if (msg.auxMsg) { - setAuxMsg(msg.auxMsg); - } - - if (msg.diceRolls) { - logDice(msg.diceRolls); - } else if (msg.diceRoll) { - logDice(msg.diceRoll); - } - - if (msg.tokens) { - updateTokens(msg.tokens); - } else if (msg.token) { - updateTokens([msg.token]); - } - } -} - -function setMapImg(url) { - initializeMap(url); -} - -function isTableKeyValid(name, passcode) { - const r = /^[a-zA-Z0-9_]+$/; - return r.test(name) && r.test(passcode); -} - -function leave() { - conn.close(1000); -} +const msgQ = []; function dial() { // get tableKey from UI - const tblNameInput = document.getElementById("input_table_name"); - const tblPassInput = document.getElementById("input_table_pass"); - const joinTblBtn = document.getElementById("table_join"); - const leaveTblBtn = document.getElementById("table_leave"); + const tblNameInput = $("input_table_name"); + const tblPassInput = $("input_table_pass"); + const joinTblBtn = $("table_join"); + const leaveTblBtn = $("table_leave"); if (tblNameInput && tblPassInput && tblNameInput.value && tblPassInput.value) { if (isTableKeyValid(tblNameInput.value, tblPassInput.value)) {

@@ -191,8 +32,11 @@ if (e.code == 1006 && e.wasClean) {

setErr("Table not found - check the name and passcode are correct"); } else if (e.code > 1001) { - lagDiv.style.display = "block"; - setTimeout(dial, 1000) + const lagDiv = $("lag"); + if (lagDiv) { + lagDiv.style.display = "block"; + setTimeout(dial, 1000) + } } else { tblNameInput.readOnly = false;

@@ -200,7 +44,7 @@ tblPassInput.readOnly = false;

joinTblBtn.style.display = adminToken ? "none" : "inline"; leaveTblBtn.style.display = "none"; - tabletop = document.getElementById("tabletop"); + tabletop = $("tabletop"); if (tabletop) { tabletop.style.display = "none"; }

@@ -218,22 +62,29 @@ }

}); conn.addEventListener("open", e => { + offline = false; tblNameInput.readOnly = true; tblPassInput.readOnly = true; joinTblBtn.style.display = "none"; leaveTblBtn.style.display = adminToken ? "none" : "inline"; - lagDiv.style.display = "none"; + lagDiv = $("lag"); + if (lagDiv) { + lagDiv.style.display = "none"; + } + while (msgQ.some(m=>m)) { publish(msgQ[0]); msgQ.shift(); } + closeErr(); - tabletop = document.getElementById("tabletop"); + tabletop = $("tabletop"); if (tabletop) { tabletop.style.display = "block"; } }); + conn.addEventListener("error", e => { setErr(`Websocket error`); conn.close(3000);

@@ -283,3 +134,163 @@ setErr("Failed to publish message");

} } } + +function makeUpToDate(msg) { + if (msg) { + // map image has to be set before tokens can be handled! + if (msg.mapImg) { + setMapImg(msg.mapImg); + } + + if (msg.auxMsg) { + setAuxMsg(msg.auxMsg); + } + + if (msg.diceRolls) { + logDice(msg.diceRolls); + } else if (msg.diceRoll) { + logDice(msg.diceRoll); + } + + if (msg.tokens) { + updateTokens(msg.tokens); + } else if (msg.token) { + updateTokens([msg.token]); + } + } +} + +function fmtLeading(n) { + return n < 10 ? "0" + n : String(n); +} + +function formatDice(r) { + const date = new Date(r.timestamp) + const p = document.createElement("p"); + + const month = date.getMonth() + 1; + const day = date.getDate(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + + p.innerHTML = `${date.getFullYear()}-${fmtLeading(month)}-${fmtLeading(day)} ${fmtLeading(hours)}:${fmtLeading(minutes)}:${fmtLeading(seconds)} ${r.player} rolled ${r.roll.length}d${r.faces} ${(r.note ? "(" + r.note + ")" : "")}<br>[${r.roll}] (total ${r.roll.reduce((a,c)=>a+c,0)})`; + + return p; +} + +function logDice(dice) { + const diceLog = $("dice_log"); + if (!Array.isArray(dice)) { + dice = [ dice ]; + } else { + if (diceLog) { + diceLog.innerHTML = ""; + } + } + if (diceLog) { + for(const r of dice) { + try{ + const p = formatDice(r); + diceLog.append(p); + p.scrollIntoView(); + } catch{} + } + } +} + +function setAuxMsg(msg) { + const auxDiv = $("aux"); + if (auxDiv) { + auxDiv.innerText = msg; + } +} + +function updateTokens(tokens) { + // update internal token array and map + processTokens(tokens); + // update token select window + renderTokenSelect(); + // if admin, update token master list + renderTokenMasterList(); +} + +function renderTokenSelect() { + const tokenSelect = $("token_select"); + + if (tokenSelect) { + const scroll = tokenSelect.scrollTop; + tokenSelect.innerHTML = tokens.reduce((s, t) => { + return s + `<li><a href="#" onclick="initSpritePreviewById('${t.t.id}')">${t.t.name}</a><button onclick="toggleActive('${t.t.id}');return false">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\n`; + }, ""); + tokenSelect.scrollTop = scroll; + } +} + +// the following few functions aren't socket related but they directly relate to the previous function + +function initSpritePreviewById(id) { + const token = tokens.find(t=>t.t.id == id); + let img = null; + + if (token && id) { + + img = document.createElement("img"); + img.src = token.t.sprite; + const hdnTokenId = $("token_preview_id"); + if (hdnTokenId) { + hdnTokenId.value = id; + } + + img.onload = () => { + scaleAltSpritePreview(); + } + } + + const previewClearBtn = $("token_preview_clear"); + if (previewClearBtn) { + previewClearBtn.style.display = (token && id) ? "block" : "none"; + } + const tokenPreview = $("token_preview"); + if (tokenPreview) { + tokenPreview.innerHTML = ""; + if (img) { + tokenPreview.appendChild(img); + } + } +} + +function dismissPreview() { + initSpritePreviewById(null); +} + +function scaleAltSpritePreview() { + const hdnTokenId = $("token_preview_id"); + const tokenPreview = $("token_preview"); + if (tokenPreview && hdnTokenId && mapImg && mapImg._image) { + const id = hdnTokenId.value; + const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; + const img = tokenPreview.children[0]; + const token = tokens.find(t=>t.t.id == id); + if (img && token) { + img.width = token.t.w * scaleFactor; + img.height = token.t.h * scaleFactor; + } + } +} + +function setMapImg(url) { + initializeMap(url); +} + +function isTableKeyValid(name, passcode) { + const r = /^[a-zA-Z0-9_]+$/; + return r.test(name) && r.test(passcode); +} + +function leave() { + conn.close(1000); + tableKey.name = ""; + tableKey.passcode = ""; +} +
M static/style.cssstatic/style.css

@@ -66,7 +66,7 @@ color: var(--bg_color);

background: var(--main_color); } -#errWrapper { +#err_wrapper { color: var(--err_color); background: var(--bg_color); border: solid 2px var(--err_color);

@@ -78,14 +78,14 @@ left: 50%;

transform: translateX(-50%); } -#closeErr { +#err_close { display: inline; border: none; color: var(--err_color); padding: 0 1ch; margin-right: 1ch; } -#errDiv { +#err_div { display: inline; }

@@ -130,15 +130,12 @@ max-height: 80vh;

overflow-y: auto; } -#auxMsgZone { +#aux_msg_input { width: 100%; padding:0.2em; height: 8em; } -#adminWrapper { -} - summary { cursor: pointer; }

@@ -205,11 +202,11 @@ .leaflet-container {

background: transparent; } -#tokenKeepAspect { +#token_aspect_lock { display: none; } -#tokenPreview_zone { +#token_admin_preview { position: relative; }
M static/util.jsstatic/util.js

@@ -1,26 +1,17 @@

-const errDiv = document.getElementById("errDiv"); -const errWrapper = document.getElementById("errWrapper"); - const defaultTheme = [ "#000000cc", "#ccccccff", "#1f9b92ff", "#002b36ff", "#DC143Cff" ]; -const bgCol_input = document.getElementById("bg_col_input"); -const fgCol_input = document.getElementById("fg_col_input"); -const mainCol_input = document.getElementById("main_col_input"); -const subCol_input = document.getElementById("sub_col_input"); -const errCol_input = document.getElementById("err_col_input"); - -const bgOp_input = document.getElementById("bg_col_opacity"); -const fgOp_input = document.getElementById("fg_col_opacity"); -const mainOp_input = document.getElementById("main_col_opacity"); -const subOp_input = document.getElementById("sub_col_opacity"); -const errOp_input = document.getElementById("err_col_opacity"); - const saveData = { username: "", theme: defaultTheme, } +function $(id) { + return document.getElementById(id); +} + function setErr(x) { + const errDiv = $("err_div"); + const errWrapper = $("err_wrapper"); if (errDiv) { errDiv.innerHTML = x; }

@@ -30,6 +21,7 @@ }

} function closeErr() { + const errWrapper = $("err_wrapper"); if (errWrapper) { errWrapper.style.display = "none"; }

@@ -39,14 +31,14 @@ function loadStorage() {

saveData.username = localStorage.getItem("username"); savedTheme = JSON.parse(localStorage.getItem("theme")); saveData.theme = savedTheme || defaultTheme; - const username = document.getElementById("name_entry"); + const username = $("name_entry"); if (username) { username.value = saveData.username; } } function saveName() { - const username = document.getElementById("name_entry"); + const username = $("name_entry"); if (username) { saveData.username = username.value; localStorage.setItem("username", saveData.username);

@@ -54,22 +46,26 @@ }

} function setupDiceAutoScroll() { - const diceWin = document.getElementById("dice_win"); + const diceWin = $("dice_win"); + if (diceWin) { diceWin.addEventListener("toggle", e => { if (diceWin.open) { - const diceLog = document.getElementById("dice_log"); - diceLog.children[diceLog.children.length - 1].scrollIntoView(); + const diceLog = $("dice_log"); + if (diceLog && diceLog.children.length > 0) { + diceLog.children[diceLog.children.length - 1].scrollIntoView(); + } } }); } } function hexToBytes(hex) { - let bytes = []; - for (let c = 0; c < hex.length; c += 2) - bytes.push(parseInt(hex.substr(c, 2), 16)); - return bytes; + let bytes = []; + for (let c = 0; c < hex.length; c += 2) { + bytes.push(parseInt(hex.substr(c, 2), 16)); + } + return bytes; } function toByte(v) {

@@ -97,31 +93,31 @@ sub_col = sub_col.substr(0,7);

let err_opacity = hexToBytes(err_col.substr(7)); err_col = err_col.substr(0,7); - bgCol_input.value = bg_col; - bgOp_input.value = bg_opacity; - fgCol_input.value = fg_col; - fgOp_input.value = fg_opacity; - mainCol_input.value = main_col; - mainOp_input.value = main_opacity; - subCol_input.value = sub_col; - subOp_input.value = sub_opacity; - errCol_input.value = err_col; - errOp_input.value = err_opacity; + $("bg_col_input").value = bg_col; + $("bg_col_opacity").value = bg_opacity; + $("fg_col_input").value = fg_col; + $("fg_col_opacity").value = fg_opacity; + $("main_col_input").value = main_col; + $("main_col_opacity").value = main_opacity; + $("sub_col_input").value = sub_col; + $("sub_col_opacity").value = sub_opacity; + $("err_col_input").value = err_col; + $("err_col_opacity").value = err_opacity; } catch {} } function setTheme() { - try{ - let bg_col = bgCol_input.value; - let bg_opacity = toByte(bgOp_input.value); - let fg_col = fgCol_input.value; - let fg_opacity =(toByte(fgOp_input.value)); - let main_col = mainCol_input.value; - let main_opacity = (toByte(mainOp_input.value)); - let sub_col = subCol_input.value; - let sub_opacity = (toByte(subOp_input.value)); - let err_col = errCol_input.value; - let err_opacity = (toByte(errOp_input.value)); + try { + let bg_col = $("bg_col_input").value + let bg_opacity = toByte($("bg_col_opacity").value); + let fg_col = $("fg_col_input").value; + let fg_opacity = (toByte($("fg_col_opacity").value)); + let main_col = $("main_col_input").value; + let main_opacity = (toByte($("main_col_opacity").value)); + let sub_col = $("sub_col_input").value; + let sub_opacity = (toByte($("sub_col_opacity").value)); + let err_col = $("err_col_input").value; + let err_opacity = (toByte($("err_col_opacity").value)); saveData.theme[0] = bg_col + bg_opacity; saveData.theme[1] = fg_col + fg_opacity;