Restore read-only chalkboard textarea

This commit is contained in:
Storm Dragon
2026-06-23 09:48:13 -04:00
parent 43d37b99be
commit 506c0530d9
6 changed files with 49 additions and 150 deletions
@@ -2,7 +2,7 @@
# shellcheck shell=bash disable=SC2034,SC2154 # shellcheck shell=bash disable=SC2034,SC2154
pkgname=skald-git pkgname=skald-git
pkgver=0.0.0.r1545.g0d2bd1c pkgver=0.0.0.r1540.g26c7808
pkgrel=1 pkgrel=1
pkgdesc='Audio-only hall-based conferencing server' pkgdesc='Audio-only hall-based conferencing server'
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+7 -6
View File
@@ -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 message to a given user. Type `/help` to display the list of available
commands. commands.
Below the chat input is a session-only virtual chalkboard suitable for Below the chat input is a session-only virtual chalkboard. It is a
commands, code, and notes that should be visible to everyone in the hall. multiline text area suitable for commands, code, and notes that should be
Operators and administrators edit it as a multiline text area and may grant visible to everyone in the hall. Operators and administrators may edit
or revoke chalkboard editing for connected users with `/chalkboard user` the chalkboard and may grant or revoke chalkboard editing for connected
and `/unchalkboard user`. Users without edit permission can read and focus users with `/chalkboard user` and `/unchalkboard user`. Users without
the chalkboard as preformatted text. edit permission can read and focus the chalkboard, but it is marked
read-only and does not trap the Tab key.
### Inviting users ### Inviting users
+3 -26
View File
@@ -829,36 +829,14 @@ h1 {
margin-top: 0; margin-top: 0;
} }
#chalkboard, #chalkboard {
#chalkboard-view {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
width: 100%; width: 100%;
min-height: 8rem; min-height: 8rem;
}
#chalkboard {
resize: vertical; 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 { #chalkboard-copy {
margin-top: 0.35rem; margin-top: 0.35rem;
} }
@@ -867,12 +845,11 @@ h1 {
opacity: 0.55; opacity: 0.55;
} }
#chalkboard-view { #chalkboard[readonly] {
background: #f3f3f3; background: #f3f3f3;
} }
#chalkboard:focus, #chalkboard:focus {
#chalkboard-view:focus {
outline: 2px solid #0066cc; outline: 2px solid #0066cc;
outline-offset: 2px; outline-offset: 2px;
} }
+1 -8
View File
@@ -78,14 +78,7 @@
</div> </div>
<div id="chalkboard-area"> <div id="chalkboard-area">
<h2 id="chalkboard-heading">Virtual chalkboard</h2> <h2 id="chalkboard-heading">Virtual chalkboard</h2>
<textarea id="chalkboard" aria-labelledby="chalkboard-heading" hidden></textarea> <textarea id="chalkboard" aria-labelledby="chalkboard-heading"></textarea>
<div id="chalkboard-view" tabindex="0" aria-labelledby="chalkboard-heading" hidden>
<br>
<br>
<pre id="chalkboard-view-text"></pre>
<br>
<br>
</div>
<button id="chalkboard-copy" type="button" class="btn btn-default" disabled>Copy to clipboard</button> <button id="chalkboard-copy" type="button" class="btn btn-default" disabled>Copy to clipboard</button>
</div> </div>
</div> </div>
+36 -88
View File
@@ -2650,7 +2650,6 @@ function flushLiveRegion(elementId) {
let text = state.queue.shift(); let text = state.queue.shift();
if(text === undefined) { if(text === undefined) {
announcement.replaceChildren();
state.active = false; state.active = false;
return; return;
} }
@@ -2679,41 +2678,6 @@ let chalkboardAnnouncementTimer = null;
let chalkboardHasPendingAnnouncement = false; let chalkboardHasPendingAnnouncement = false;
let chalkboardPendingHasText = false; let chalkboardPendingHasText = false;
let chalkboardLocalTextPending = null; 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() { function clearChalkboardAnnouncementTimer() {
if(chalkboardAnnouncementTimer) { if(chalkboardAnnouncementTimer) {
@@ -2731,29 +2695,28 @@ function canEditChalkboard() {
} }
function updateChalkboardAccess() { function updateChalkboardAccess() {
let editor = getChalkboardEditor(); let chalkboard = /** @type {HTMLTextAreaElement} */
let view = getChalkboardView(); (document.getElementById('chalkboard'));
if(!editor || !view) if(!chalkboard)
return; return;
let canEdit = canEditChalkboard(); let canEdit = canEditChalkboard();
let active = document.activeElement; chalkboard.readOnly = !canEdit;
editor.hidden = !canEdit; chalkboard.setAttribute('aria-readonly', canEdit ? 'false' : 'true');
view.hidden = canEdit; chalkboard.setAttribute(
editor.setAttribute('aria-label', 'Virtual chalkboard'); 'aria-label',
view.setAttribute('aria-label', 'Virtual chalkboard, read only'); canEdit ? 'Virtual chalkboard' : 'Virtual chalkboard, read only',
if(active === editor && !canEdit) );
view.focus();
else if(active === view && canEdit)
editor.focus();
updateChalkboardCopyButton(); updateChalkboardCopyButton();
} }
function updateChalkboardCopyButton() { function updateChalkboardCopyButton() {
let chalkboard = /** @type {HTMLTextAreaElement} */
(document.getElementById('chalkboard'));
let copyButton = /** @type {HTMLButtonElement} */ let copyButton = /** @type {HTMLButtonElement} */
(document.getElementById('chalkboard-copy')); (document.getElementById('chalkboard-copy'));
if(!copyButton) if(!chalkboard || !copyButton)
return; return;
copyButton.disabled = chalkboardText.length === 0; copyButton.disabled = chalkboard.value.length === 0;
} }
function resetChalkboard() { function resetChalkboard() {
@@ -2766,37 +2729,25 @@ function resetChalkboard() {
clearTimeout(chalkboardSendTimer); clearTimeout(chalkboardSendTimer);
chalkboardSendTimer = null; chalkboardSendTimer = null;
} }
setChalkboardText(''); let chalkboard = /** @type {HTMLTextAreaElement} */
(document.getElementById('chalkboard'));
if(chalkboard)
chalkboard.value = '';
updateChalkboardAccess(); 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() { async function copyChalkboardToClipboard() {
if(chalkboardText.length === 0) let chalkboard = /** @type {HTMLTextAreaElement} */
(document.getElementById('chalkboard'));
if(!chalkboard || chalkboard.value.length === 0)
return; return;
try { try {
if(navigator.clipboard && navigator.clipboard.writeText) { if(navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(chalkboardText); await navigator.clipboard.writeText(chalkboard.value);
} else if(!copyTextWithExecCommand(chalkboardText)) { } else {
chalkboard.focus();
chalkboard.select();
if(!document.execCommand('copy'))
throw new Error('copy command failed'); throw new Error('copy command failed');
} }
announceChat('Chalkboard copied to clipboard'); announceChat('Chalkboard copied to clipboard');
@@ -2820,12 +2771,14 @@ function announcePendingChalkboardUpdate() {
} }
function scheduleChalkboardUpdateAnnouncement(hasText) { function scheduleChalkboardUpdateAnnouncement(hasText) {
if(!getChalkboardEditor() || !getChalkboardView()) let chalkboard = /** @type {HTMLTextAreaElement} */
(document.getElementById('chalkboard'));
if(!chalkboard)
return; return;
chalkboardHasPendingAnnouncement = true; chalkboardHasPendingAnnouncement = true;
chalkboardPendingHasText = hasText; chalkboardPendingHasText = hasText;
if(chalkboardHasFocus()) if(document.activeElement === chalkboard)
return; return;
clearChalkboardAnnouncementTimer(); clearChalkboardAnnouncementTimer();
@@ -2849,14 +2802,16 @@ function applyChalkboardUpdate(message) {
return; return;
chalkboardRevision = revision; chalkboardRevision = revision;
if(!getChalkboardEditor() || !getChalkboardView()) let chalkboard = /** @type {HTMLTextAreaElement} */
(document.getElementById('chalkboard'));
if(!chalkboard)
return; return;
let isLocalEcho = let isLocalEcho =
chalkboardLocalTextPending !== null && chalkboardLocalTextPending !== null &&
text === chalkboardLocalTextPending; text === chalkboardLocalTextPending;
if(chalkboardText !== text) { if(chalkboard.value !== text) {
applyingChalkboardUpdate = true; applyingChalkboardUpdate = true;
setChalkboardText(text); chalkboard.value = text;
applyingChalkboardUpdate = false; applyingChalkboardUpdate = false;
if(isLocalEcho) { if(isLocalEcho) {
chalkboardLocalTextPending = null; chalkboardLocalTextPending = null;
@@ -2876,10 +2831,10 @@ function scheduleChalkboardSend() {
clearTimeout(chalkboardSendTimer); clearTimeout(chalkboardSendTimer);
chalkboardSendTimer = setTimeout(() => { chalkboardSendTimer = setTimeout(() => {
chalkboardSendTimer = null; chalkboardSendTimer = null;
let chalkboard = getChalkboardEditor(); let chalkboard = /** @type {HTMLTextAreaElement} */
(document.getElementById('chalkboard'));
if(!chalkboard || !serverConnection || !serverConnection.socket) if(!chalkboard || !serverConnection || !serverConnection.socket)
return; return;
chalkboardText = chalkboard.value;
chalkboardLocalTextPending = chalkboard.value; chalkboardLocalTextPending = chalkboard.value;
serverConnection.hallAction('chalkboard', { serverConnection.hallAction('chalkboard', {
text: chalkboard.value, text: chalkboard.value,
@@ -3871,9 +3826,6 @@ document.getElementById('input').onkeypress = function(e) {
document.getElementById('chalkboard').addEventListener('input', function(e) { document.getElementById('chalkboard').addEventListener('input', function(e) {
if(applyingChalkboardUpdate) if(applyingChalkboardUpdate)
return; return;
let chalkboard = getChalkboardEditor();
if(chalkboard)
setChalkboardText(chalkboard.value);
updateChalkboardCopyButton(); updateChalkboardCopyButton();
scheduleChalkboardSend(); scheduleChalkboardSend();
}); });
@@ -3887,10 +3839,6 @@ document.getElementById('chalkboard').addEventListener('focus', function(e) {
announcePendingChalkboardUpdate(); announcePendingChalkboardUpdate();
}); });
document.getElementById('chalkboard-view').addEventListener('focus', function(e) {
announcePendingChalkboardUpdate();
});
document.getElementById('chalkboard-copy').addEventListener('click', function(e) { document.getElementById('chalkboard-copy').addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
copyChalkboardToClipboard(); copyChalkboardToClipboard();
-20
View File
@@ -56,7 +56,6 @@ func TestStaticLiveRegionAnnouncementsStayWired(t *testing.T) {
requireFunctionContains(t, js, "announceLiveRegion", "state.queue.push") requireFunctionContains(t, js, "announceLiveRegion", "state.queue.push")
requireFunctionContains(t, js, "announceLiveRegion", "flushLiveRegion") requireFunctionContains(t, js, "announceLiveRegion", "flushLiveRegion")
requireFunctionContains(t, js, "flushLiveRegion", "appendChild") requireFunctionContains(t, js, "flushLiveRegion", "appendChild")
requireFunctionContains(t, js, "flushLiveRegion", "announcement.replaceChildren()")
requireFunctionContains(t, js, "addToChatbox", "announceChat") requireFunctionContains(t, js, "addToChatbox", "announceChat")
requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand raised`)") requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand raised`)")
requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand lowered`)") requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand lowered`)")
@@ -74,25 +73,6 @@ func TestHallHeadingIncludesCurrentStatus(t *testing.T) {
requireFunctionContains(t, js, "gotUserMessage", "hallRecording = false") 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, `<textarea id="chalkboard" aria-labelledby="chalkboard-heading" hidden>`, "chalkboard editor")
requireContains(t, html, `<div id="chalkboard-view" tabindex="0" aria-labelledby="chalkboard-heading" hidden>`, "chalkboard view")
requireContains(t, html, "<br>\n <br>\n <pre id=\"chalkboard-view-text\"></pre>", "chalkboard view leading breaks")
requireContains(t, html, "<pre id=\"chalkboard-view-text\"></pre>\n <br>\n <br>", "chalkboard view trailing breaks")
requireContains(t, html, `<pre id="chalkboard-view-text"></pre>`, "chalkboard view text")
requireContains(t, css, "white-space: pre-wrap", "chalkboard view")
requireContains(t, css, "#chalkboard[hidden]", "chalkboard hidden state")
requireFunctionContains(t, js, "getChalkboardEditor", "return /** @type {HTMLTextAreaElement} */ (")
requireFunctionContains(t, js, "setChalkboardText", "viewText.textContent = text")
requireFunctionContains(t, js, "updateChalkboardAccess", "editor.hidden = !canEdit")
requireFunctionContains(t, js, "updateChalkboardAccess", "view.hidden = canEdit")
requireFunctionContains(t, js, "applyChalkboardUpdate", "setChalkboardText(text)")
}
func TestGlobalShortcutAliasesStayDocumented(t *testing.T) { func TestGlobalShortcutAliasesStayDocumented(t *testing.T) {
js := readStaticFile(t, "skald.js") js := readStaticFile(t, "skald.js")