From 506c0530d9804c38750b144b1dd0e157c01278e6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 23 Jun 2026 09:48:13 -0400 Subject: [PATCH] Restore read-only chalkboard textarea --- distro-packages/Arch-Linux/skald-git/PKGBUILD | 2 +- skald.md | 13 +- static/skald.css | 29 +--- static/skald.html | 9 +- static/skald.js | 126 +++++------------- webserver/static_accessibility_test.go | 20 --- 6 files changed, 49 insertions(+), 150 deletions(-) diff --git a/distro-packages/Arch-Linux/skald-git/PKGBUILD b/distro-packages/Arch-Linux/skald-git/PKGBUILD index 6168669..04c119e 100644 --- a/distro-packages/Arch-Linux/skald-git/PKGBUILD +++ b/distro-packages/Arch-Linux/skald-git/PKGBUILD @@ -2,7 +2,7 @@ # shellcheck shell=bash disable=SC2034,SC2154 pkgname=skald-git -pkgver=0.0.0.r1545.g0d2bd1c +pkgver=0.0.0.r1540.g26c7808 pkgrel=1 pkgdesc='Audio-only hall-based conferencing server' arch=('x86_64' 'aarch64') diff --git a/skald.md b/skald.md index d64e465..16984e9 100644 --- a/skald.md +++ b/skald.md @@ -75,12 +75,13 @@ than navigating the user interface. Commands start with a slash character message to a given user. Type `/help` to display the list of available commands. -Below the chat input is a session-only virtual chalkboard suitable for -commands, code, and notes that should be visible to everyone in the hall. -Operators and administrators edit it as a multiline text area and may grant -or revoke chalkboard editing for connected users with `/chalkboard user` -and `/unchalkboard user`. Users without edit permission can read and focus -the chalkboard as preformatted text. +Below the chat input is a session-only virtual chalkboard. It is a +multiline text area suitable for commands, code, and notes that should be +visible to everyone in the hall. Operators and administrators may edit +the chalkboard and may grant or revoke chalkboard editing for connected +users with `/chalkboard user` and `/unchalkboard user`. Users without +edit permission can read and focus the chalkboard, but it is marked +read-only and does not trap the Tab key. ### Inviting users diff --git a/static/skald.css b/static/skald.css index fa666b8..71e055b 100644 --- a/static/skald.css +++ b/static/skald.css @@ -829,36 +829,14 @@ h1 { margin-top: 0; } -#chalkboard, -#chalkboard-view { +#chalkboard { box-sizing: border-box; display: block; width: 100%; min-height: 8rem; -} - -#chalkboard { resize: vertical; } -#chalkboard-view { - border: 1px solid #767676; - border-radius: 2px; - font-family: monospace; - overflow: auto; - padding: 2px; -} - -#chalkboard-view-text { - margin: 0; - white-space: pre-wrap; -} - -#chalkboard[hidden], -#chalkboard-view[hidden] { - display: none; -} - #chalkboard-copy { margin-top: 0.35rem; } @@ -867,12 +845,11 @@ h1 { opacity: 0.55; } -#chalkboard-view { +#chalkboard[readonly] { background: #f3f3f3; } -#chalkboard:focus, -#chalkboard-view:focus { +#chalkboard:focus { outline: 2px solid #0066cc; outline-offset: 2px; } diff --git a/static/skald.html b/static/skald.html index 65ff239..9c6c0a4 100644 --- a/static/skald.html +++ b/static/skald.html @@ -78,14 +78,7 @@

Virtual chalkboard

- - +
diff --git a/static/skald.js b/static/skald.js index d9cb80e..cf21a78 100644 --- a/static/skald.js +++ b/static/skald.js @@ -2650,7 +2650,6 @@ function flushLiveRegion(elementId) { let text = state.queue.shift(); if(text === undefined) { - announcement.replaceChildren(); state.active = false; return; } @@ -2679,41 +2678,6 @@ let chalkboardAnnouncementTimer = null; let chalkboardHasPendingAnnouncement = false; let chalkboardPendingHasText = false; let chalkboardLocalTextPending = null; -let chalkboardText = ''; - -function getChalkboardEditor() { - return /** @type {HTMLTextAreaElement} */ ( - document.getElementById('chalkboard') - ); -} - -function getChalkboardView() { - return document.getElementById('chalkboard-view'); -} - -function getChalkboardViewText() { - return document.getElementById('chalkboard-view-text'); -} - -/** - * @param {string} text - */ -function setChalkboardText(text) { - chalkboardText = text; - - let editor = getChalkboardEditor(); - if(editor && editor.value !== text) - editor.value = text; - - let viewText = getChalkboardViewText(); - if(viewText && viewText.textContent !== text) - viewText.textContent = text; -} - -function chalkboardHasFocus() { - let active = document.activeElement; - return active === getChalkboardEditor() || active === getChalkboardView(); -} function clearChalkboardAnnouncementTimer() { if(chalkboardAnnouncementTimer) { @@ -2731,29 +2695,28 @@ function canEditChalkboard() { } function updateChalkboardAccess() { - let editor = getChalkboardEditor(); - let view = getChalkboardView(); - if(!editor || !view) + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); + if(!chalkboard) return; let canEdit = canEditChalkboard(); - let active = document.activeElement; - editor.hidden = !canEdit; - view.hidden = canEdit; - editor.setAttribute('aria-label', 'Virtual chalkboard'); - view.setAttribute('aria-label', 'Virtual chalkboard, read only'); - if(active === editor && !canEdit) - view.focus(); - else if(active === view && canEdit) - editor.focus(); + chalkboard.readOnly = !canEdit; + chalkboard.setAttribute('aria-readonly', canEdit ? 'false' : 'true'); + chalkboard.setAttribute( + 'aria-label', + canEdit ? 'Virtual chalkboard' : 'Virtual chalkboard, read only', + ); updateChalkboardCopyButton(); } function updateChalkboardCopyButton() { + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); let copyButton = /** @type {HTMLButtonElement} */ (document.getElementById('chalkboard-copy')); - if(!copyButton) + if(!chalkboard || !copyButton) return; - copyButton.disabled = chalkboardText.length === 0; + copyButton.disabled = chalkboard.value.length === 0; } function resetChalkboard() { @@ -2766,38 +2729,26 @@ function resetChalkboard() { clearTimeout(chalkboardSendTimer); chalkboardSendTimer = null; } - setChalkboardText(''); + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); + if(chalkboard) + chalkboard.value = ''; updateChalkboardAccess(); } -/** - * @param {string} text - */ -function copyTextWithExecCommand(text) { - let fallback = document.createElement('textarea'); - fallback.value = text; - fallback.readOnly = true; - fallback.style.position = 'fixed'; - fallback.style.left = '-9999px'; - fallback.style.top = '0'; - document.body.appendChild(fallback); - try { - fallback.focus(); - fallback.select(); - return document.execCommand('copy'); - } finally { - document.body.removeChild(fallback); - } -} - async function copyChalkboardToClipboard() { - if(chalkboardText.length === 0) + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); + if(!chalkboard || chalkboard.value.length === 0) return; try { if(navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(chalkboardText); - } else if(!copyTextWithExecCommand(chalkboardText)) { - throw new Error('copy command failed'); + await navigator.clipboard.writeText(chalkboard.value); + } else { + chalkboard.focus(); + chalkboard.select(); + if(!document.execCommand('copy')) + throw new Error('copy command failed'); } announceChat('Chalkboard copied to clipboard'); } catch(e) { @@ -2820,12 +2771,14 @@ function announcePendingChalkboardUpdate() { } function scheduleChalkboardUpdateAnnouncement(hasText) { - if(!getChalkboardEditor() || !getChalkboardView()) + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); + if(!chalkboard) return; chalkboardHasPendingAnnouncement = true; chalkboardPendingHasText = hasText; - if(chalkboardHasFocus()) + if(document.activeElement === chalkboard) return; clearChalkboardAnnouncementTimer(); @@ -2849,14 +2802,16 @@ function applyChalkboardUpdate(message) { return; chalkboardRevision = revision; - if(!getChalkboardEditor() || !getChalkboardView()) + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); + if(!chalkboard) return; let isLocalEcho = chalkboardLocalTextPending !== null && text === chalkboardLocalTextPending; - if(chalkboardText !== text) { + if(chalkboard.value !== text) { applyingChalkboardUpdate = true; - setChalkboardText(text); + chalkboard.value = text; applyingChalkboardUpdate = false; if(isLocalEcho) { chalkboardLocalTextPending = null; @@ -2876,10 +2831,10 @@ function scheduleChalkboardSend() { clearTimeout(chalkboardSendTimer); chalkboardSendTimer = setTimeout(() => { chalkboardSendTimer = null; - let chalkboard = getChalkboardEditor(); + let chalkboard = /** @type {HTMLTextAreaElement} */ + (document.getElementById('chalkboard')); if(!chalkboard || !serverConnection || !serverConnection.socket) return; - chalkboardText = chalkboard.value; chalkboardLocalTextPending = chalkboard.value; serverConnection.hallAction('chalkboard', { text: chalkboard.value, @@ -3871,9 +3826,6 @@ document.getElementById('input').onkeypress = function(e) { document.getElementById('chalkboard').addEventListener('input', function(e) { if(applyingChalkboardUpdate) return; - let chalkboard = getChalkboardEditor(); - if(chalkboard) - setChalkboardText(chalkboard.value); updateChalkboardCopyButton(); scheduleChalkboardSend(); }); @@ -3887,10 +3839,6 @@ document.getElementById('chalkboard').addEventListener('focus', function(e) { announcePendingChalkboardUpdate(); }); -document.getElementById('chalkboard-view').addEventListener('focus', function(e) { - announcePendingChalkboardUpdate(); -}); - document.getElementById('chalkboard-copy').addEventListener('click', function(e) { e.preventDefault(); copyChalkboardToClipboard(); diff --git a/webserver/static_accessibility_test.go b/webserver/static_accessibility_test.go index 2d34e12..202d0db 100644 --- a/webserver/static_accessibility_test.go +++ b/webserver/static_accessibility_test.go @@ -56,7 +56,6 @@ func TestStaticLiveRegionAnnouncementsStayWired(t *testing.T) { requireFunctionContains(t, js, "announceLiveRegion", "state.queue.push") requireFunctionContains(t, js, "announceLiveRegion", "flushLiveRegion") requireFunctionContains(t, js, "flushLiveRegion", "appendChild") - requireFunctionContains(t, js, "flushLiveRegion", "announcement.replaceChildren()") requireFunctionContains(t, js, "addToChatbox", "announceChat") requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand raised`)") requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand lowered`)") @@ -74,25 +73,6 @@ func TestHallHeadingIncludesCurrentStatus(t *testing.T) { requireFunctionContains(t, js, "gotUserMessage", "hallRecording = false") } -func TestChalkboardReadOnlyViewIsNotAnEditBox(t *testing.T) { - html := readStaticFile(t, "skald.html") - css := readStaticFile(t, "skald.css") - js := readStaticFile(t, "skald.js") - - requireContains(t, html, `