From f518dfdfd16375aebe5b4c5e8fc063850acd6feb Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Tue, 17 Oct 2023 19:50:49 +0200 Subject: 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. --- public/common/client.css | 323 +++++++++++++++++++++++++++-------------------- public/common/client.js | 193 +++++++++++++++------------- 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 = `
Notepad: ${player}
\u274c
- + ` 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 = '' 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 +}) -- cgit v1.2.3