From e73eb21c45c9f67198346695671a3ad303e9d887 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 23 Jun 2026 00:45:20 -0400 Subject: [PATCH] Change chalkboard to pre tag for people who do not have write access because some screen readers seem to have problems with read only edit boxes. --- distro-packages/Arch-Linux/skald-git/PKGBUILD | 2 +- skald.md | 13 +- static/skald.css | 26 +++- static/skald.html | 1 + static/skald.js | 120 ++++++++++++------ 5 files changed, 114 insertions(+), 48 deletions(-) diff --git a/distro-packages/Arch-Linux/skald-git/PKGBUILD b/distro-packages/Arch-Linux/skald-git/PKGBUILD index 04c119e..4cab742 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.r1540.g26c7808 +pkgver=0.0.0.r1541.gd8817f6 pkgrel=1 pkgdesc='Audio-only hall-based conferencing server' arch=('x86_64' 'aarch64') diff --git a/skald.md b/skald.md index 16984e9..d64e465 100644 --- a/skald.md +++ b/skald.md @@ -75,13 +75,12 @@ 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. 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. +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. ### Inviting users diff --git a/static/skald.css b/static/skald.css index 71e055b..7d45daf 100644 --- a/static/skald.css +++ b/static/skald.css @@ -829,14 +829,33 @@ h1 { margin-top: 0; } -#chalkboard { +#chalkboard, +#chalkboard-view { 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; + margin: 0; + overflow: auto; + padding: 2px; + white-space: pre-wrap; +} + +#chalkboard[hidden], +#chalkboard-view[hidden] { + display: none; +} + #chalkboard-copy { margin-top: 0.35rem; } @@ -845,11 +864,12 @@ h1 { opacity: 0.55; } -#chalkboard[readonly] { +#chalkboard-view { background: #f3f3f3; } -#chalkboard:focus { +#chalkboard:focus, +#chalkboard-view:focus { outline: 2px solid #0066cc; outline-offset: 2px; } diff --git a/static/skald.html b/static/skald.html index 9c6c0a4..f85ff26 100644 --- a/static/skald.html +++ b/static/skald.html @@ -79,6 +79,7 @@

Virtual chalkboard

+
diff --git a/static/skald.js b/static/skald.js index cf21a78..48f0e72 100644 --- a/static/skald.js +++ b/static/skald.js @@ -2678,6 +2678,36 @@ 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'); +} + +/** + * @param {string} text + */ +function setChalkboardText(text) { + chalkboardText = text; + + let editor = getChalkboardEditor(); + if(editor && editor.value !== text) + editor.value = text; + + let view = getChalkboardView(); + if(view && view.textContent !== text) + view.textContent = text; +} + +function chalkboardHasFocus() { + let active = document.activeElement; + return active === getChalkboardEditor() || active === getChalkboardView(); +} function clearChalkboardAnnouncementTimer() { if(chalkboardAnnouncementTimer) { @@ -2695,28 +2725,29 @@ function canEditChalkboard() { } function updateChalkboardAccess() { - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); - if(!chalkboard) + let editor = getChalkboardEditor(); + let view = getChalkboardView(); + if(!editor || !view) return; let canEdit = canEditChalkboard(); - chalkboard.readOnly = !canEdit; - chalkboard.setAttribute('aria-readonly', canEdit ? 'false' : 'true'); - chalkboard.setAttribute( - 'aria-label', - canEdit ? 'Virtual chalkboard' : 'Virtual chalkboard, read only', - ); + 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(); updateChalkboardCopyButton(); } function updateChalkboardCopyButton() { - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); let copyButton = /** @type {HTMLButtonElement} */ (document.getElementById('chalkboard-copy')); - if(!chalkboard || !copyButton) + if(!copyButton) return; - copyButton.disabled = chalkboard.value.length === 0; + copyButton.disabled = chalkboardText.length === 0; } function resetChalkboard() { @@ -2729,26 +2760,38 @@ function resetChalkboard() { clearTimeout(chalkboardSendTimer); chalkboardSendTimer = null; } - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); - if(chalkboard) - chalkboard.value = ''; + setChalkboardText(''); 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() { - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); - if(!chalkboard || chalkboard.value.length === 0) + if(chalkboardText.length === 0) return; try { if(navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(chalkboard.value); - } else { - chalkboard.focus(); - chalkboard.select(); - if(!document.execCommand('copy')) - throw new Error('copy command failed'); + await navigator.clipboard.writeText(chalkboardText); + } else if(!copyTextWithExecCommand(chalkboardText)) { + throw new Error('copy command failed'); } announceChat('Chalkboard copied to clipboard'); } catch(e) { @@ -2771,14 +2814,12 @@ function announcePendingChalkboardUpdate() { } function scheduleChalkboardUpdateAnnouncement(hasText) { - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); - if(!chalkboard) + if(!getChalkboardEditor() || !getChalkboardView()) return; chalkboardHasPendingAnnouncement = true; chalkboardPendingHasText = hasText; - if(document.activeElement === chalkboard) + if(chalkboardHasFocus()) return; clearChalkboardAnnouncementTimer(); @@ -2802,16 +2843,14 @@ function applyChalkboardUpdate(message) { return; chalkboardRevision = revision; - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); - if(!chalkboard) + if(!getChalkboardEditor() || !getChalkboardView()) return; let isLocalEcho = chalkboardLocalTextPending !== null && text === chalkboardLocalTextPending; - if(chalkboard.value !== text) { + if(chalkboardText !== text) { applyingChalkboardUpdate = true; - chalkboard.value = text; + setChalkboardText(text); applyingChalkboardUpdate = false; if(isLocalEcho) { chalkboardLocalTextPending = null; @@ -2831,10 +2870,10 @@ function scheduleChalkboardSend() { clearTimeout(chalkboardSendTimer); chalkboardSendTimer = setTimeout(() => { chalkboardSendTimer = null; - let chalkboard = /** @type {HTMLTextAreaElement} */ - (document.getElementById('chalkboard')); + let chalkboard = getChalkboardEditor(); if(!chalkboard || !serverConnection || !serverConnection.socket) return; + chalkboardText = chalkboard.value; chalkboardLocalTextPending = chalkboard.value; serverConnection.hallAction('chalkboard', { text: chalkboard.value, @@ -3826,6 +3865,9 @@ 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(); }); @@ -3839,6 +3881,10 @@ 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();