summaryrefslogtreecommitdiff
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
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.
-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
+})