Restore read-only chalkboard textarea
This commit is contained in:
@@ -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')
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
+37
-89
@@ -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,38 +2729,26 @@ 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 {
|
||||||
throw new Error('copy command failed');
|
chalkboard.focus();
|
||||||
|
chalkboard.select();
|
||||||
|
if(!document.execCommand('copy'))
|
||||||
|
throw new Error('copy command failed');
|
||||||
}
|
}
|
||||||
announceChat('Chalkboard copied to clipboard');
|
announceChat('Chalkboard copied to clipboard');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user