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
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')
+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
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
+3 -26
View File
@@ -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;
}
+1 -8
View File
@@ -78,14 +78,7 @@
</div>
<div id="chalkboard-area">
<h2 id="chalkboard-heading">Virtual chalkboard</h2>
<textarea id="chalkboard" aria-labelledby="chalkboard-heading" hidden></textarea>
<div id="chalkboard-view" tabindex="0" aria-labelledby="chalkboard-heading" hidden>
<br>
<br>
<pre id="chalkboard-view-text"></pre>
<br>
<br>
</div>
<textarea id="chalkboard" aria-labelledby="chalkboard-heading"></textarea>
<button id="chalkboard-copy" type="button" class="btn btn-default" disabled>Copy to clipboard</button>
</div>
</div>
+36 -88
View File
@@ -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,37 +2729,25 @@ 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)) {
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');
@@ -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();
-20
View File
@@ -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, `<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) {
js := readStaticFile(t, "skald.js")