summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-10-17 19:50:49 +0200
committerTor Andersson <tor@ccxvii.net>2023-10-21 19:41:47 +0200
commitf518dfdfd16375aebe5b4c5e8fc063850acd6feb (patch)
tree9cfb60f573551cde3b6aa7fba130eed0fbfa0ab7 /public
parent77b20af11c6492616ece6cc7100b9d1f10722c3b (diff)
downloadserver-f518dfdfd16375aebe5b4c5e8fc063850acd6feb.tar.gz
Improvements to mobile layout code.
Fixes for Chrome. Fixes for Safari. Hide log at start if window is small. Toolbar menu and icon buttons using details, menu, and button elements.
Diffstat (limited to 'public')
-rw-r--r--public/common/client.css323
-rw-r--r--public/common/client.js193
2 files changed, 296 insertions, 220 deletions
diff --git a/public/common/client.css b/public/common/client.css
index d943146..e24ef6d 100644
--- a/public/common/client.css
+++ b/public/common/client.css
@@ -1,20 +1,27 @@
/* COMMON GRID LAYOUT */
+:root {
+ --font-normal: "Source Serif", "Georgia", "Noto Emoji", "Dingbats", serif;
+ --font-small: "Source Serif SmText", "Georgia", "Noto Emoji", "Dingbats", serif;
+ --font-widget: "Source Sans", "Verdana", "Noto Emoji", "Dingbats", sans-serif;
+}
+
html {
image-rendering: -webkit-optimize-contrast; /* try to fix chromium's terrible image rescaling */
+ -webkit-tap-highlight-color: transparent; /* disable blue flashes when tapping on chrome mobile */
}
html, button, input, select, textarea {
- font-family: "Source Sans", "Circled Numbers", "Dingbats", "Noto Emoji", "Verdana", sans-serif;
+ font-family: var(--font-widget);
font-size: 16px;
}
#chat_text, #chat_input, #notepad_input {
- font-family: "Source Serif", "Circled Numbers", "Dingbats", "Noto Emoji", "Georgia", serif;
+ font-family: var(--font-normal);
}
#log, #turn_info {
- font-family: "Source Serif SmText", "Circled Numbers", "Dingbats", "Noto Emoji", "Georgia", serif;
+ font-family: var(--font-small);
}
.hide {
@@ -44,6 +51,7 @@ button {
border: 2px solid;
outline: 1px solid black;
white-space: nowrap;
+ color: black;
}
button:disabled {
@@ -63,6 +71,20 @@ button:enabled:active:hover {
/* MAIN GRID */
+html {
+ box-sizing: border-box;
+ background-color: black;
+ padding:
+ env(safe-area-inset-top, 0px)
+ env(safe-area-inset-right, 0px)
+ env(safe-area-inset-bottom, 0px)
+ env(safe-area-inset-left, 0px);
+ width: 100vw;
+ width: 100dvw;
+ height: 100vh;
+ height: 100dvh;
+}
+
body {
margin: 0;
padding: 0;
@@ -70,13 +92,8 @@ body {
overflow: clip;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto minmax(0, 1fr) auto;
- width: 100dvw;
- height: 100dvh;
-}
-
-header {
- grid-column: 1/3;
- grid-row: 1;
+ width: 100%;
+ height: 100%;
}
main {
@@ -138,135 +155,179 @@ footer {
padding: 0 8px;
}
-/* MENU */
+/* HEADER */
-.menu {
- user-select: none;
+header {
+ grid-column: 1/3;
+ grid-row: 1;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: right;
+ align-items: center;
+ gap: 0 8px;
+ border-bottom: 1px solid black;
+ background-color: gainsboro;
}
-.menu_item img {
- vertical-align: top;
- height: 20px;
+
+header .viewpoint_button.selected {
+ background-color: hsl(51,100%,60%);
+ border-color: hsl(51,100%,80%) hsl(51,100%,40%) hsl(51,100%,40%) hsl(51,100%,80%);
}
-.menu_title img {
- display: block;
- height: 36px;
- padding: 4px;
+
+header .viewpoint_button.selected:active:hover {
+ border-color: hsl(51,100%,40%) hsl(51,100%,80%) hsl(51,100%,80%) hsl(51,100%,40%);
}
-.menu:hover .menu_title {
- background-color: black;
- color: white;
+
+header.disconnected {
+ background-color: red !important;
}
-.menu:hover .menu_title img {
- filter: invert(100%);
+
+header.your_turn {
+ background-color: orange;
}
-.menu_popup {
- display: none;
- position: absolute;
- min-width: 160px;
+
+header.replay {
+ background-image: repeating-linear-gradient(45deg, gainsboro, gainsboro 40px, silver 40px, silver 80px);
+}
+
+#prompt {
+ flex: 1 1 300px;
+ padding-left: 4px;
+ font-size: 18px;
+ line-height: 18px;
+ padding: 4px 0 4px 4px;
+ text-overflow: ellipsis;
white-space: nowrap;
- border: 1px solid black;
- background-color: white;
- z-index: 501;
+ overflow: hidden;
}
-.menu:hover .menu_popup {
- display: block;
+
+#prompt:hover {
+ white-space: normal;
}
-.menu_separator {
- border-top: 1px solid black;
+
+#actions, #viewpoint_panel {
+ display: flex;
+ justify-content: end;
+ align-items: center;
}
-.menu_item {
+
+#actions {
+ flex-wrap: wrap;
+ align-content: space-between;
padding: 4px 8px;
- cursor: pointer;
+ gap: 8px;
}
-.menu_item:hover {
- background-color: black;
- color: white;
+
+/* TOOLBAR & MENUS */
+
+#toolbar {
+ display: flex;
+ user-select: none;
}
-.menu_item:hover img {
- filter: invert(100%);
+
+details summary::-webkit-details-marker {
+ display: none;
}
-a.menu_item {
+
+details menu {
display: block;
- text-decoration: none;
- color: black;
-}
-.menu_item.disabled {
- color: gray;
+ min-width: 140px;
}
-/* TOOL BAR */
-
-.icon_button {
- user-select: none;
+menu li a {
+ display: block;
+ text-decoration: none;
+ color: inherit;
}
-.icon_button img {
+
+summary img, #toolbar button img {
+ pointer-events: none;
display: block;
height: 36px;
padding: 4px;
}
-.icon_button:hover {
- background-color: black;
- color: white;
+
+#toolbar button {
+ background-color: transparent;
+ border: none;
+ outline: none;
+ height: auto;
+ padding: 0;
+ margin: 0;
}
-.icon_button:hover img {
- filter: invert(100%);
+
+details[open] > summary { background-color: #0004; }
+@media (hover: hover) {
+ summary:hover, #toolbar button:hover { background-color: #0004; }
}
+summary:active, #toolbar button:active { background-color: #0008; }
-header {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0 8px;
- border-bottom: 1px solid black;
- background-color: gainsboro;
+summary {
+ cursor: pointer;
+ list-style: none;
}
-#toolbar {
- display: flex;
+/* POPUP MENUS */
+
+menu {
+ overflow-y: auto;
+ max-height: calc(100% - 44px);
+ display: none;
+ position: absolute;
+ user-select: none;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ border: 1px solid black;
+ background-color: white;
+ z-index: 500;
}
-header.disconnected {
- background-color: red !important;
+menu li {
+ padding: 4px 8px;
+ cursor: pointer;
}
-header.your_turn {
- background-color: orange;
+menu li:hover {
+ background-color: black;
+ color: white;
}
-header.replay {
- background-image: repeating-linear-gradient(45deg, gainsboro, gainsboro 40px, silver 40px, silver 80px);
+menu li.title {
+ cursor: default;
+ color: inherit;
+ background-color: gainsboro;
}
-#actions, #viewpoint_panel {
- display: flex;
- justify-content: end;
- align-items: center;
+menu li.separator {
+ cursor: default;
+ padding: 0;
+ border-top: 1px solid black;
}
-#actions {
- flex-wrap: wrap;
- padding: 4px 8px;
- gap: 8px;
+menu li.disabled {
+ cursor: default;
+ color: gray;
+ background-color: inherit;
}
-header .viewpoint_button.selected {
- background-color: hsl(51,100%,60%);
- border-color: hsl(51,100%,80%) hsl(51,100%,40%) hsl(51,100%,40%) hsl(51,100%,80%);
+menu li img {
+ vertical-align: top;
+ height: 20px;
}
-header .viewpoint_button.selected:active:hover {
- border-color: hsl(51,100%,40%) hsl(51,100%,80%) hsl(51,100%,80%) hsl(51,100%,40%);
+menu li:hover img {
+ filter: invert(100%);
}
-#prompt {
- padding-left: 12px;
- font-size: 18px;
- flex: 1 1 200px;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
+@media (pointer: coarse) {
+ menu li:not(.separator) {
+ padding: 8px;
+ }
}
+/* REPLAY CONTROLS */
+
#replay_panel {
grid-column: 2;
grid-row: 3;
@@ -286,11 +347,13 @@ header .viewpoint_button.selected:active:hover {
opacity: 60%;
}
-.replay_button:hover {
- background-color: #fffc;
+@media (hover: hover) {
+ .replay_button:hover {
+ background-color: #fffc;
+ }
}
-.replay_button:hover:active {
+.replay_button:active {
background-color: #fff8;
}
@@ -359,8 +422,8 @@ header .viewpoint_button.selected:active:hover {
/* CHAT WINDOW */
-#chat_button.new {
- filter: invert(100%);
+#chat_button.new img {
+ filter: invert(100%) drop-shadow(0px 0px 2px black);
}
#chat_window {
@@ -373,7 +436,7 @@ header .viewpoint_button.selected:active:hover {
#notepad_window {
left: 60px;
top: 200px;
- height: auto;
+ width: 520px;
z-index: 498;
}
@@ -471,11 +534,18 @@ header .viewpoint_button.selected:active:hover {
display: none;
}
-@media (pointer: coarse) and (max-width: 400px) {
+@media (min-width: 1900px) {
+ #log_button { display: none; }
+}
+
+@media (pointer: coarse) and ( (max-width: 400px) or (max-height: 400px) ) {
#fullscreen_button { display: block; }
#zoom_button { display: none; }
}
+@media (max-width: 800px) { footer { bottom: 25px } }
+@media (pointer: coarse) and (max-width: 800px) { footer { bottom: 37px } }
+
@media (pointer: coarse) {
#replay_panel, .replay_button {
height: 36px;
@@ -483,15 +553,15 @@ header .viewpoint_button.selected:active:hover {
}
@media (max-height: 600px) {
- .role_name:not(:hover) .role_user {
+ .role_name:not(:hover) div.role_user {
display: none;
}
}
@media (max-width: 800px) {
body {
- grid-template-columns: 1fr min-content;
- grid-template-rows: min-content 1fr min-content;
+ grid-template-columns: minmax(0, 1fr) min-content;
+ grid-template-rows: min-content minmax(0, 1fr) min-content;
}
header {
grid-column: 1/3;
@@ -514,52 +584,38 @@ header .viewpoint_button.selected:active:hover {
border-left: none;
z-index: 496;
}
- header {
- display: grid;
- grid-template-columns: min-content auto;
- }
- #toolbar {
- grid-row: 1;
- grid-column: 1;
- }
- #prompt {
- grid-row: 2;
- grid-column: 1/3;
- padding: 0 8px 3px 8px;
- }
- #prompt:hover {
- white-space: normal;
- }
- #actions {
- grid-row: 1;
- grid-column: 2;
- }
-
#chat_window, #notepad_window {
position: static;
grid-column: 1;
- grid-row: 2;
+ grid-row: 2/4;
display: none;
width: auto;
box-shadow: none;
border: none;
}
+ #chat_text, #notepad_input {
+ min-height: 48px;
+ }
#chat_window.show, #notepad_window.show {
display: grid;
}
}
-@media (max-width: 800px) { footer { bottom: 25px } }
-@media (pointer: coarse) and (max-width: 800px) { footer { bottom: 37px } }
+@media (max-width: 600px) {
+ header {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: min-content minmax(36px, auto) auto;
+ }
+ #toolbar { grid-column: 1; grid-row: 1 }
+ #actions { grid-column: 1; grid-row: 2 }
+ #prompt { grid-column: 1; grid-row: 3 }
+}
@media (max-width: 400px) {
body {
grid-template-columns: 1fr;
}
- header {
- grid-template-columns: 1fr;
- grid-template-rows: auto auto auto;
- }
aside {
grid-column: 1;
grid-row: 2/3;
@@ -567,7 +623,4 @@ header .viewpoint_button.selected:active:hover {
width: 100vw;
border: none;
}
- #toolbar { grid-row: 1; grid-column: 1; }
- #actions { grid-row: 2; grid-column: 1; }
- #prompt { grid-row: 3; grid-column: 1; }
}
diff --git a/public/common/client.js b/public/common/client.js
index 8aa1f65..1c0892d 100644
--- a/public/common/client.js
+++ b/public/common/client.js
@@ -1,5 +1,7 @@
"use strict"
+// TODO: hide more functions and globals in anonymous function scope
+
let params = {
mode: "play",
title_id: window.location.pathname.split("/")[1],
@@ -272,7 +274,7 @@ function init_notepad() {
notepad_window.innerHTML = `
<div id="notepad_header">Notepad: ${player}</div>
<div id="notepad_x" onclick="toggle_notepad()">\u274c</div>
- <textarea id="notepad_input" cols="55" rows="10" maxlength="16000" oninput="dirty_notepad()"></textarea>
+ <textarea id="notepad_input" maxlength="16000" oninput="dirty_notepad()"></textarea>
<div id="notepad_footer"><button id="notepad_save" onclick="save_notepad()" disabled>Save</button></div>
`
document.querySelector("body").appendChild(notepad_window)
@@ -350,19 +352,17 @@ window.addEventListener("keyup", (evt) => {
/* REMATCH BUTTON */
-function add_icon_button(where, id, img, title, fn) {
+function add_icon_button(where, id, img, fn) {
let button = document.getElementById(id)
if (!button) {
- button = document.createElement("div")
+ button = document.createElement("button")
button.id = id
- button.title = title
- button.className = "icon_button"
button.innerHTML = '<img src="/images/' + img + '.svg">'
button.addEventListener("click", fn)
if (where)
- document.getElementById("toolbar").appendChild(button)
+ document.querySelector("#toolbar").appendChild(button)
else
- document.querySelector(".menu").after(button)
+ document.querySelector("#toolbar details").after(button)
}
return button
}
@@ -383,9 +383,9 @@ function goto_replay() {
}
function on_game_over() {
- add_icon_button(1, "replay_button", "sherlock-holmes-mirror", "Watch replay", goto_replay)
+ add_icon_button(1, "replay_button", "sherlock-holmes-mirror", goto_replay)
if (player !== "Observer")
- add_icon_button(1, "rematch_button", "cycle", "Propose a rematch!", goto_rematch)
+ add_icon_button(1, "rematch_button", "cycle", goto_rematch)
remove_resign_menu()
}
@@ -713,54 +713,41 @@ function init_replay() {
document.body.appendChild(script)
}
-window.addEventListener("load", function () {
- zoom_map()
- if (params.mode === "debug")
- init_replay()
- else if (params.mode === "replay")
- init_replay()
- else if (params.mode === "play")
- connect_play()
- else
- document.getElementById("prompt").textContent = "Invalid mode: " + params.mode
-})
-
/* MAIN MENU */
-add_icon_button(0, "chat_button", "chat-bubble", "Open chat", toggle_chat).classList.add("hide")
-add_icon_button(0, "zoom_button", "magnifying-glass", "Zoom", () => toggle_zoom())
-add_icon_button(0, "log_button", "scroll-quill", "Hide log", toggle_log)
-add_icon_button(0, "fullscreen_button", "expand", "Fullscreen", toggle_fullscreen)
+add_icon_button(0, "chat_button", "chat-bubble", toggle_chat).classList.add("hide")
+add_icon_button(0, "zoom_button", "magnifying-glass", () => toggle_zoom())
+add_icon_button(0, "log_button", "scroll-quill", toggle_log)
+add_icon_button(0, "fullscreen_button", "expand", toggle_fullscreen)
-function init_main_menu() {
- let popup = document.querySelector(".menu_popup")
- let sep = document.createElement("div")
- sep.className = "menu_separator"
- sep.id = "main_menu_separator"
+function add_main_menu_separator() {
+ let popup = document.querySelector("#toolbar details menu")
+ let sep = document.createElement("li")
+ sep.className = "separator"
popup.insertBefore(sep, popup.firstChild)
}
function add_main_menu_item(text, onclick) {
- let popup = document.querySelector(".menu_popup")
- let sep = document.getElementById("main_menu_separator")
- let item = document.createElement("div")
- item.className = "menu_item"
+ let popup = document.querySelector("#toolbar details menu")
+ let sep = popup.querySelector(".separator")
+ let item = document.createElement("li")
item.onclick = onclick
item.textContent = text
popup.insertBefore(item, sep)
}
function add_main_menu_item_link(text, url) {
- let popup = document.querySelector(".menu_popup")
- let sep = document.getElementById("main_menu_separator")
- let item = document.createElement("a")
- item.className = "menu_item"
- item.href = url
- item.textContent = text
+ let popup = document.querySelector("#toolbar details menu")
+ let sep = popup.querySelector(".separator")
+ let item = document.createElement("li")
+ let a = document.createElement("a")
+ a.href = url
+ a.textContent = text
+ item.appendChild(a)
popup.insertBefore(item, sep)
}
-init_main_menu()
+add_main_menu_separator()
if (params.mode === "play" && params.role !== "Observer") {
add_main_menu_item_link("Go home", "/games/active")
add_main_menu_item_link("Go to next game", "/games/next")
@@ -768,6 +755,20 @@ if (params.mode === "play" && params.role !== "Observer") {
add_main_menu_item_link("Go home", "/")
}
+function close_menus(self) {
+ for (let node of document.querySelectorAll("#toolbar > details"))
+ if (node !== self)
+ node.removeAttribute("open")
+}
+
+for (let node of document.querySelectorAll("#toolbar > details")) {
+ node.onclick = function () { close_menus(node) }
+ node.onmouseleave = function () { node.removeAttribute("open") }
+}
+for (let node of document.querySelectorAll("#toolbar > details > menu")) {
+ node.onclick = function () { close_menus(null) }
+}
+
function toggle_fullscreen() {
if (document.fullscreenElement)
document.exitFullscreen()
@@ -857,45 +858,21 @@ function on_snap_stop() {
function toggle_log() {
document.querySelector("aside").classList.toggle("hide")
- zoom_map()
+ update_layout()
}
var toggle_zoom = function () {}
-
-function zoom_map() {
- let mapwrap = document.getElementById("mapwrap")
- if (mapwrap) {
- let main = document.querySelector("main")
- let map = document.getElementById("map")
- map.style.transform = null
- mapwrap.style.width = null
- mapwrap.style.height = null
- if (mapwrap.classList.contains("fit")) {
- let { width: gw, height: gh } = main.getBoundingClientRect()
- let { width: ww, height: wh } = mapwrap.getBoundingClientRect()
- let { width: cw, height: ch } = map.getBoundingClientRect()
- let scale = Math.min(ww / cw, gh / ch)
- if (scale < 1) {
- map.style.transform = "scale(" + scale + ")"
- mapwrap.style.width = (cw * scale) + "px"
- mapwrap.style.height = (ch * scale) + "px"
- }
- }
- }
-}
-
-window.addEventListener("resize", zoom_map)
+var update_layout = function () {}
/* PAN & ZOOM GAME BOARD */
;(function panzoom_init() {
- const MIN_ZOOM = 0.5
- const MAX_ZOOM = 1.5
+ var MIN_ZOOM = Number(document.querySelector("main").dataset.minZoom) || 0.5
+ var MAX_ZOOM = Number(document.querySelector("main").dataset.maxZoom) || 1.5
+
const THRESHOLD = 0.0625
const DECELERATION = 125
- console.log("DPX", window.devicePixelRatio)
-
const e_scroll = document.querySelector("main")
e_scroll.style.touchAction = "none"
@@ -914,11 +891,11 @@ window.addEventListener("resize", zoom_map)
e_scroll.appendChild(e_outer)
const mapwrap = document.getElementById("mapwrap")
- const map = document.getElementById("map") || e_inner.firstChild
+ const map = document.getElementById("map") || e_inner.querySelector("div")
const map_w = mapwrap ? mapwrap.clientWidth : map.clientWidth
const map_h = mapwrap ? mapwrap.clientHeight : map.clientHeight
- console.log("MAP", map_w, map_h)
+ console.log("INIT MAP", map, map_w, map_h, window.devicePixelRatio)
var transform0 = { x: 0, y: 0, scale: 1 }
var transform1 = { x: 0, y: 0, scale: 1 }
@@ -940,14 +917,9 @@ window.addEventListener("resize", zoom_map)
var mom_vx = 0
var mom_vy = 0
- try {
- new ResizeObserver(update_transform_resize).observe(document.getElementById("log"))
- } catch (err) {
- window.addEventListener("resize", function (evt) {
- old_scale = 0
- update_transform()
- })
- }
+ // set globals to our scoped functions
+ toggle_zoom = toggle_zoom_imp
+ update_layout = update_layout_imp
function clamp_scale(scale) {
let win_w = e_scroll.clientWidth
@@ -981,10 +953,10 @@ window.addEventListener("resize", zoom_map)
function toggle_zoom_imp() {
if (transform1.scale === 1) {
- if (window.innerWidth >= 800) {
+ if (window.innerWidth > 800) {
if (mapwrap) {
mapwrap.classList.toggle("fit")
- zoom_map()
+ update_map_fit()
return
}
}
@@ -998,10 +970,38 @@ window.addEventListener("resize", zoom_map)
return false
}
+ function update_layout_imp() {
+ update_map_fit()
+ update_transform_resize()
+ scroll_log_to_end()
+ }
+
+ function update_map_fit() {
+ let mapwrap = document.getElementById("mapwrap")
+ if (mapwrap) {
+ let main = document.querySelector("main")
+ let map = document.getElementById("map")
+ map.style.transform = null
+ mapwrap.style.width = null
+ mapwrap.style.height = null
+ if (mapwrap.classList.contains("fit")) {
+ let { width: gw, height: gh } = main.getBoundingClientRect()
+ let { width: ww, height: wh } = mapwrap.getBoundingClientRect()
+ let { width: cw, height: ch } = map.getBoundingClientRect()
+ let scale = Math.min(ww / cw, gh / ch)
+ if (scale < 1) {
+ map.style.transform = "scale(" + scale + ")"
+ mapwrap.style.width = (cw * scale) + "px"
+ mapwrap.style.height = (ch * scale) + "px"
+ }
+ }
+ }
+ }
+
function disable_map_fit() {
if (mapwrap && mapwrap.classList.contains("fit")) {
mapwrap.classList.remove("fit")
- zoom_map()
+ update_map_fit()
}
}
@@ -1048,6 +1048,12 @@ window.addEventListener("resize", zoom_map)
}
}
+ function update_transform_resize() {
+ old_scale = 0
+ anchor_transform()
+ update_transform()
+ }
+
function start_measure(time) {
mom_last_t = [ time, time, time ]
mom_last_x = [ transform1.x, transform1.x, transform1.x ]
@@ -1213,6 +1219,23 @@ window.addEventListener("resize", zoom_map)
},
{ passive: false }
)
-
- toggle_zoom = toggle_zoom_imp
})()
+
+/* INITIALIZE */
+
+window.addEventListener("resize", () => update_layout())
+
+window.addEventListener("load", function () {
+ if (window.innerWidth <= 800)
+ document.querySelector("aside").classList.add("hide")
+ update_layout()
+
+ if (params.mode === "debug")
+ init_replay()
+ else if (params.mode === "replay")
+ init_replay()
+ else if (params.mode === "play")
+ connect_play()
+ else
+ document.getElementById("prompt").textContent = "Invalid mode: " + params.mode
+})