More ui accessibility improvements.
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.r1527.g47d1308
|
pkgver=0.0.0.r1528.gaac197e
|
||||||
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
-21
@@ -1,6 +1,6 @@
|
|||||||
/* Accessibility: Visible focus indicators for keyboard navigation */
|
/* Accessibility: Visible focus indicators for keyboard navigation */
|
||||||
button:focus,
|
button:focus,
|
||||||
[role="button"]:focus,
|
[role="separator"]:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
@@ -1003,38 +1003,50 @@ header .collapse:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#users .user-p {
|
#users .user-p {
|
||||||
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-top: 0;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-sidebar.active #users > div {
|
#left-sidebar.active #users > .user-p {
|
||||||
padding: 10px 5px !important;
|
padding: 10px 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#users > div:hover {
|
#users > .user-p:hover,
|
||||||
|
#users > .user-p:focus {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#users > div::before {
|
#users .user-status-icon {
|
||||||
content: "\f111";
|
|
||||||
font-family: 'Font Awesome 6 Free';
|
|
||||||
color: #20b91e;
|
color: #20b91e;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
#users > div.user-status-raisehand::before {
|
#users .user-status-icon::before {
|
||||||
|
content: "\f111";
|
||||||
|
}
|
||||||
|
|
||||||
|
#users .user-status-raisehand .user-status-icon::before {
|
||||||
content: "\f256";
|
content: "\f256";
|
||||||
}
|
}
|
||||||
|
|
||||||
#users > div::after {
|
#users .user-microphone-icon {
|
||||||
font-family: 'Font Awesome 6 Free';
|
font-family: 'Font Awesome 6 Free';
|
||||||
color: #808080;
|
color: #808080;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
@@ -1042,7 +1054,7 @@ header .collapse:hover {
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#users > div.user-status-microphone::after {
|
#users .user-microphone-icon::before {
|
||||||
content: "\f130";
|
content: "\f130";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1187,21 +1199,63 @@ header .collapse:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--contextualMenuBg: #eee;
|
--skald-menu-bg: #eee;
|
||||||
--contextualMenuShadow: 1px 1px 1px #444; */
|
--skald-menu-shadow: 1px 1px 1px #444;
|
||||||
--contextualMenuRadius: 0px; */
|
--skald-menu-radius: 0;
|
||||||
--contextualMenuText: black;
|
--skald-menu-text: black;
|
||||||
|
--skald-menu-hover: #ddd;
|
||||||
--contextualSubMenuBg: #eee;
|
--skald-menu-separator: #999;
|
||||||
|
|
||||||
--contextualHover: #ddd;
|
|
||||||
|
|
||||||
--contextualOverflowIcon: #999;
|
|
||||||
--contextualSeperator: #999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextualMenu{
|
.skald-action-menu {
|
||||||
|
position: absolute;
|
||||||
z-index: 2999;
|
z-index: 2999;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: 14em;
|
||||||
|
list-style: none;
|
||||||
|
background: var(--skald-menu-bg);
|
||||||
|
box-shadow: var(--skald-menu-shadow);
|
||||||
|
border-radius: var(--skald-menu-radius);
|
||||||
|
color: var(--skald-menu-text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skald-action-menu li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skald-action-menu-item {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skald-action-menu-item:focus,
|
||||||
|
.skald-action-menu-item:hover {
|
||||||
|
background: var(--skald-menu-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skald-action-menu-separator {
|
||||||
|
border-top: 1px solid var(--skald-menu-separator);
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #404064;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-progress {
|
.file-progress {
|
||||||
|
|||||||
+1
-3
@@ -12,7 +12,6 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="/third-party/fontawesome/css/solid.min.css"/>
|
<link rel="stylesheet" type="text/css" href="/third-party/fontawesome/css/solid.min.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/third-party/fontawesome/css/regular.min.css"/>
|
<link rel="stylesheet" type="text/css" href="/third-party/fontawesome/css/regular.min.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/third-party/toastify/toastify.css"/>
|
<link rel="stylesheet" type="text/css" href="/third-party/toastify/toastify.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/third-party/contextual/contextual.css"/>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -82,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="resizer" class="connected-only invisible" aria-hidden="true" tabindex="0" role="separator" aria-label="Chat resize handle" aria-orientation="vertical"></div>
|
<div id="resizer" class="connected-only invisible" aria-hidden="true" tabindex="0" role="separator" aria-label="Chat resize handle" aria-orientation="vertical" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" aria-valuetext="Chat width 50 percent"></div>
|
||||||
<div class="coln-right" id="right">
|
<div class="coln-right" id="right">
|
||||||
<button class="chat-btn show-chat invisible" id="show-chat" aria-label="Show chat">
|
<button class="chat-btn show-chat invisible" id="show-chat" aria-label="Show chat">
|
||||||
<i class="far fa-comment-alt icon-chat" aria-hidden="true"></i>
|
<i class="far fa-comment-alt icon-chat" aria-hidden="true"></i>
|
||||||
@@ -217,7 +216,6 @@
|
|||||||
|
|
||||||
<script src="/protocol.js" defer></script>
|
<script src="/protocol.js" defer></script>
|
||||||
<script src="/third-party/toastify/toastify.js" defer></script>
|
<script src="/third-party/toastify/toastify.js" defer></script>
|
||||||
<script src="/third-party/contextual/contextual.js" defer></script>
|
|
||||||
<script src="/skald.js" defer></script>
|
<script src="/skald.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+178
-127
@@ -449,6 +449,7 @@ function setConnected(connected) {
|
|||||||
connectionbox.classList.add('invisible');
|
connectionbox.classList.add('invisible');
|
||||||
closeNav();
|
closeNav();
|
||||||
setLeftPanelExpanded(true);
|
setLeftPanelExpanded(true);
|
||||||
|
updateChatResizerValueFromLayout();
|
||||||
displayUsername();
|
displayUsername();
|
||||||
window.onresize = function(e) {
|
window.onresize = function(e) {
|
||||||
scheduleReconsiderDownRate();
|
scheduleReconsiderDownRate();
|
||||||
@@ -1632,55 +1633,115 @@ document.getElementById('invite-dialog').onclose = function(e) {
|
|||||||
makeToken(template);
|
makeToken(template);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let currentActionMenu = null;
|
||||||
|
let currentActionMenuTrigger = null;
|
||||||
|
|
||||||
|
function closeActionMenu(restoreFocus) {
|
||||||
|
document.removeEventListener('click', closeActionMenuOnOutside);
|
||||||
|
if(currentActionMenu)
|
||||||
|
currentActionMenu.remove();
|
||||||
|
if(currentActionMenuTrigger)
|
||||||
|
currentActionMenuTrigger.setAttribute('aria-expanded', 'false');
|
||||||
|
if(restoreFocus && currentActionMenuTrigger)
|
||||||
|
currentActionMenuTrigger.focus();
|
||||||
|
currentActionMenu = null;
|
||||||
|
currentActionMenuTrigger = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canModerateChat() {
|
||||||
|
return !!(serverConnection && serverConnection.permissions &&
|
||||||
|
serverConnection.permissions.indexOf('op') >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds keyboard navigation to Contextual menu
|
* @param {{label?: string, type?: string, onClick?: function(): void}[]} items
|
||||||
* @param {HTMLElement} menuElement
|
* @param {HTMLElement} trigger
|
||||||
|
* @param {string} label
|
||||||
*/
|
*/
|
||||||
function makeContextualMenuAccessible(menuElement) {
|
function openActionMenu(items, trigger, label) {
|
||||||
// Find all menu items
|
closeActionMenu(false);
|
||||||
let menuItems = menuElement.querySelectorAll('.contextualMenuItem:not(.disabled)');
|
|
||||||
if(menuItems.length === 0) return;
|
|
||||||
|
|
||||||
let currentIndex = 0;
|
let menu = document.createElement('ul');
|
||||||
|
menu.classList.add('skald-action-menu');
|
||||||
|
menu.setAttribute('role', 'menu');
|
||||||
|
menu.setAttribute('aria-label', label);
|
||||||
|
|
||||||
// Make menu items focusable and add ARIA
|
let buttons = [];
|
||||||
menuItems.forEach((item, index) => {
|
items.forEach(item => {
|
||||||
item.setAttribute('role', 'menuitem');
|
let li = document.createElement('li');
|
||||||
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
if(item.type === 'seperator') {
|
||||||
});
|
li.classList.add('skald-action-menu-separator');
|
||||||
|
li.setAttribute('role', 'separator');
|
||||||
// Focus first item
|
menu.appendChild(li);
|
||||||
menuItems[0].focus();
|
|
||||||
|
|
||||||
// Add keyboard navigation
|
|
||||||
menuElement.addEventListener('keydown', function(e) {
|
|
||||||
if(e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
contextualCore.CloseMenu();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(e.key === 'ArrowDown') {
|
let button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.classList.add('skald-action-menu-item');
|
||||||
|
button.setAttribute('role', 'menuitem');
|
||||||
|
button.textContent = item.label || 'Action';
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
closeActionMenu(true);
|
||||||
|
if(item.onClick)
|
||||||
|
item.onClick();
|
||||||
|
});
|
||||||
|
li.appendChild(button);
|
||||||
|
menu.appendChild(li);
|
||||||
|
buttons.push(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(buttons.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
menu.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
menu.addEventListener('keydown', function(e) {
|
||||||
|
let index = buttons.indexOf(document.activeElement);
|
||||||
|
if(e.key === 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
menuItems[currentIndex].setAttribute('tabindex', '-1');
|
closeActionMenu(true);
|
||||||
currentIndex = (currentIndex + 1) % menuItems.length;
|
} else if(e.key === 'ArrowDown') {
|
||||||
menuItems[currentIndex].setAttribute('tabindex', '0');
|
e.preventDefault();
|
||||||
menuItems[currentIndex].focus();
|
buttons[(index + 1 + buttons.length) % buttons.length].focus();
|
||||||
} else if(e.key === 'ArrowUp') {
|
} else if(e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
menuItems[currentIndex].setAttribute('tabindex', '-1');
|
buttons[(index - 1 + buttons.length) % buttons.length].focus();
|
||||||
currentIndex = currentIndex === 0 ? menuItems.length - 1 : currentIndex - 1;
|
} else if(e.key === 'Home') {
|
||||||
menuItems[currentIndex].setAttribute('tabindex', '0');
|
|
||||||
menuItems[currentIndex].focus();
|
|
||||||
} else if(e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Click the parent li element which has the actual click handler
|
buttons[0].focus();
|
||||||
let parentLi = menuItems[currentIndex].parentElement;
|
} else if(e.key === 'End') {
|
||||||
if(parentLi) {
|
e.preventDefault();
|
||||||
parentLi.click();
|
buttons[buttons.length - 1].focus();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
currentActionMenu = menu;
|
||||||
|
currentActionMenuTrigger = trigger;
|
||||||
|
trigger.setAttribute('aria-haspopup', 'menu');
|
||||||
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
|
||||||
|
let rect = trigger.getBoundingClientRect();
|
||||||
|
let left = rect.left;
|
||||||
|
let top = rect.bottom;
|
||||||
|
if(left + menu.offsetWidth > window.innerWidth)
|
||||||
|
left = Math.max(0, window.innerWidth - menu.offsetWidth);
|
||||||
|
if(top + menu.offsetHeight > window.innerHeight)
|
||||||
|
top = Math.max(0, rect.top - menu.offsetHeight);
|
||||||
|
menu.style.left = `${left}px`;
|
||||||
|
menu.style.top = `${top}px`;
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
document.addEventListener('click', closeActionMenuOnOutside);
|
||||||
|
}, 0);
|
||||||
|
buttons[0].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeActionMenuOnOutside() {
|
||||||
|
closeActionMenu(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1740,19 +1801,7 @@ function userMenu(elt) {
|
|||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** @ts-ignore */
|
openActionMenu(items, elt, `Actions for ${user.username || 'participant'}`);
|
||||||
new Contextual({
|
|
||||||
items: items,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add keyboard navigation after menu is created
|
|
||||||
setTimeout(() => {
|
|
||||||
let menu = document.querySelector('.contextualMenu');
|
|
||||||
if(menu) {
|
|
||||||
menu.setAttribute('role', 'menu');
|
|
||||||
makeContextualMenuAccessible(menu);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1761,27 +1810,32 @@ function userMenu(elt) {
|
|||||||
*/
|
*/
|
||||||
function addUser(id, userinfo) {
|
function addUser(id, userinfo) {
|
||||||
let div = document.getElementById('users');
|
let div = document.getElementById('users');
|
||||||
let user = document.createElement('div');
|
let user = document.createElement('button');
|
||||||
user.id = 'user-' + id;
|
user.id = 'user-' + id;
|
||||||
|
user.type = 'button';
|
||||||
user.classList.add("user-p");
|
user.classList.add("user-p");
|
||||||
user.setAttribute('role', 'button');
|
user.setAttribute('aria-haspopup', 'menu');
|
||||||
user.setAttribute('tabindex', '0');
|
user.setAttribute('aria-expanded', 'false');
|
||||||
|
let statusIcon = document.createElement('span');
|
||||||
|
statusIcon.classList.add('user-status-icon', 'fas');
|
||||||
|
statusIcon.setAttribute('aria-hidden', 'true');
|
||||||
|
user.appendChild(statusIcon);
|
||||||
|
let name = document.createElement('span');
|
||||||
|
name.classList.add('user-name');
|
||||||
|
user.appendChild(name);
|
||||||
|
let microphoneIcon = document.createElement('span');
|
||||||
|
microphoneIcon.classList.add('user-microphone-icon', 'fas');
|
||||||
|
microphoneIcon.setAttribute('aria-hidden', 'true');
|
||||||
|
microphoneIcon.hidden = true;
|
||||||
|
user.appendChild(microphoneIcon);
|
||||||
setUserStatus(id, user, userinfo, false);
|
setUserStatus(id, user, userinfo, false);
|
||||||
user.addEventListener('click', function(e) {
|
user.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
let elt = e.currentTarget;
|
let elt = e.currentTarget;
|
||||||
if(!elt || !(elt instanceof HTMLElement))
|
if(!elt || !(elt instanceof HTMLElement))
|
||||||
throw new Error("Couldn't find user div");
|
throw new Error("Couldn't find user button");
|
||||||
userMenu(elt);
|
userMenu(elt);
|
||||||
});
|
});
|
||||||
user.addEventListener('keydown', function(e) {
|
|
||||||
if(e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
let elt = e.target;
|
|
||||||
if(!elt || !(elt instanceof HTMLElement))
|
|
||||||
throw new Error("Couldn't find user div");
|
|
||||||
userMenu(elt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let us = div.children;
|
let us = div.children;
|
||||||
|
|
||||||
@@ -1832,19 +1886,21 @@ function changeUser(id, userinfo) {
|
|||||||
*/
|
*/
|
||||||
function setUserStatus(id, elt, userinfo, notify) {
|
function setUserStatus(id, elt, userinfo, notify) {
|
||||||
let username = userinfo.username ? userinfo.username : '(anon)';
|
let username = userinfo.username ? userinfo.username : '(anon)';
|
||||||
elt.textContent = username;
|
let name = elt.querySelector('.user-name');
|
||||||
|
if(!name)
|
||||||
|
throw new Error("Couldn't find user name span");
|
||||||
|
if(name.textContent !== username)
|
||||||
|
name.textContent = username;
|
||||||
|
|
||||||
let wasRaised = elt.classList.contains('user-status-raisehand');
|
let wasRaised = elt.classList.contains('user-status-raisehand');
|
||||||
if(userinfo.data.raisehand) {
|
if(userinfo.data.raisehand) {
|
||||||
elt.classList.add('user-status-raisehand');
|
elt.classList.add('user-status-raisehand');
|
||||||
elt.setAttribute('aria-label', `${username} (hand raised)`);
|
|
||||||
if(notify && !wasRaised) {
|
if(notify && !wasRaised) {
|
||||||
announceUrgent(`${username}: hand raised`);
|
announceUrgent(`${username}: hand raised`);
|
||||||
playNotificationSound('hand-raised');
|
playNotificationSound('hand-raised');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elt.classList.remove('user-status-raisehand');
|
elt.classList.remove('user-status-raisehand');
|
||||||
elt.setAttribute('aria-label', username);
|
|
||||||
if(notify && wasRaised) {
|
if(notify && wasRaised) {
|
||||||
announceUrgent(`${username}: hand lowered`);
|
announceUrgent(`${username}: hand lowered`);
|
||||||
playNotificationSound('hand-lowered');
|
playNotificationSound('hand-lowered');
|
||||||
@@ -1866,6 +1922,11 @@ function setUserStatus(id, elt, userinfo, notify) {
|
|||||||
|
|
||||||
if(microphone)
|
if(microphone)
|
||||||
statusParts.push('microphone on');
|
statusParts.push('microphone on');
|
||||||
|
elt.classList.toggle('user-status-microphone', microphone);
|
||||||
|
let microphoneIcon = elt.querySelector('.user-microphone-icon');
|
||||||
|
if(!microphoneIcon)
|
||||||
|
throw new Error("Couldn't find user microphone icon span");
|
||||||
|
microphoneIcon.hidden = !microphone;
|
||||||
elt.setAttribute('aria-label', statusParts.join(', '));
|
elt.setAttribute('aria-label', statusParts.join(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2559,14 +2620,6 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m
|
|||||||
if(peerId) {
|
if(peerId) {
|
||||||
container.dataset.peerId = peerId;
|
container.dataset.peerId = peerId;
|
||||||
container.dataset.username = nick;
|
container.dataset.username = nick;
|
||||||
container.addEventListener('click', function(e) {
|
|
||||||
if(e.detail !== 2)
|
|
||||||
return;
|
|
||||||
let elt = e.currentTarget;
|
|
||||||
if(!elt || !(elt instanceof HTMLElement))
|
|
||||||
throw new Error("Couldn't find chat message div");
|
|
||||||
chatMessageMenu(elt);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type{HTMLElement} */
|
/** @type{HTMLElement} */
|
||||||
@@ -2634,6 +2687,20 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m
|
|||||||
lastMessage = {};
|
lastMessage = {};
|
||||||
}
|
}
|
||||||
container.appendChild(footer);
|
container.appendChild(footer);
|
||||||
|
if(peerId && canModerateChat()) {
|
||||||
|
let actions = document.createElement('button');
|
||||||
|
actions.type = 'button';
|
||||||
|
actions.classList.add('message-actions');
|
||||||
|
actions.setAttribute('aria-haspopup', 'menu');
|
||||||
|
actions.setAttribute('aria-expanded', 'false');
|
||||||
|
actions.setAttribute('aria-label', `Actions for message from ${nick || '(anon)'}`);
|
||||||
|
actions.textContent = 'Actions';
|
||||||
|
actions.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
chatMessageMenu(container, actions);
|
||||||
|
});
|
||||||
|
footer.appendChild(actions);
|
||||||
|
}
|
||||||
|
|
||||||
let box = document.getElementById('box');
|
let box = document.getElementById('box');
|
||||||
box.appendChild(row);
|
box.appendChild(row);
|
||||||
@@ -2657,10 +2724,10 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} elt
|
* @param {HTMLElement} elt
|
||||||
|
* @param {HTMLElement} trigger
|
||||||
*/
|
*/
|
||||||
function chatMessageMenu(elt) {
|
function chatMessageMenu(elt, trigger) {
|
||||||
if(!(serverConnection && serverConnection.permissions &&
|
if(!canModerateChat())
|
||||||
serverConnection.permissions.indexOf('op') >= 0))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let messageId = elt.dataset.id;
|
let messageId = elt.dataset.id;
|
||||||
@@ -2691,19 +2758,7 @@ function chatMessageMenu(elt) {
|
|||||||
serverConnection.userAction('kick', peerId);
|
serverConnection.userAction('kick', peerId);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
/** @ts-ignore */
|
openActionMenu(items, trigger, `Actions for message from ${u}`);
|
||||||
new Contextual({
|
|
||||||
items: items,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add keyboard navigation after menu is created
|
|
||||||
setTimeout(() => {
|
|
||||||
let menu = document.querySelector('.contextualMenu');
|
|
||||||
if(menu) {
|
|
||||||
menu.setAttribute('role', 'menu');
|
|
||||||
makeContextualMenuAccessible(menu);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3480,24 +3535,49 @@ document.getElementById('input').onkeypress = function(e) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateChatResizerValue(leftPercent) {
|
||||||
|
let resizer = document.getElementById('resizer');
|
||||||
|
if(!resizer)
|
||||||
|
return;
|
||||||
|
let rounded = Math.round(leftPercent);
|
||||||
|
resizer.setAttribute('aria-valuenow', rounded.toString());
|
||||||
|
resizer.setAttribute('aria-valuetext', `Chat width ${rounded} percent`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChatResizerValueFromLayout() {
|
||||||
|
let mainrow = document.getElementById("mainrow");
|
||||||
|
let left = document.getElementById("left");
|
||||||
|
if(!mainrow || !left || mainrow.offsetWidth === 0)
|
||||||
|
return;
|
||||||
|
updateChatResizerValue(left.offsetWidth * 100 / mainrow.offsetWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setChatWidth(leftPercent) {
|
||||||
|
let full_width = document.getElementById("mainrow").offsetWidth;
|
||||||
|
let left = document.getElementById("left");
|
||||||
|
let right = document.getElementById("right");
|
||||||
|
|
||||||
|
let min_left_width = 300 * 100 / full_width;
|
||||||
|
if(leftPercent < min_left_width)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
left.style.flex = leftPercent.toString();
|
||||||
|
right.style.flex = (100 - leftPercent).toString();
|
||||||
|
updateChatResizerValue(leftPercent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function chatResizer(e) {
|
function chatResizer(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let full_width = document.getElementById("mainrow").offsetWidth;
|
let full_width = document.getElementById("mainrow").offsetWidth;
|
||||||
let left = document.getElementById("left");
|
let left = document.getElementById("left");
|
||||||
let right = document.getElementById("right");
|
|
||||||
|
|
||||||
let start_x = e.clientX;
|
let start_x = e.clientX;
|
||||||
let start_width = left.offsetWidth;
|
let start_width = left.offsetWidth;
|
||||||
|
|
||||||
function start_drag(e) {
|
function start_drag(e) {
|
||||||
let left_width = (start_width + e.clientX - start_x) * 100 / full_width;
|
let left_width = (start_width + e.clientX - start_x) * 100 / full_width;
|
||||||
// set min chat width to 300px
|
setChatWidth(left_width);
|
||||||
let min_left_width = 300 * 100 / full_width;
|
|
||||||
if (left_width < min_left_width) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
left.style.flex = left_width.toString();
|
|
||||||
right.style.flex = (100 - left_width).toString();
|
|
||||||
}
|
}
|
||||||
function stop_drag(e) {
|
function stop_drag(e) {
|
||||||
document.documentElement.removeEventListener(
|
document.documentElement.removeEventListener(
|
||||||
@@ -3524,21 +3604,12 @@ document.getElementById('resizer').addEventListener('keydown', function(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let full_width = document.getElementById("mainrow").offsetWidth;
|
let full_width = document.getElementById("mainrow").offsetWidth;
|
||||||
let left = document.getElementById("left");
|
let left = document.getElementById("left");
|
||||||
let right = document.getElementById("right");
|
|
||||||
let current_width = left.offsetWidth;
|
let current_width = left.offsetWidth;
|
||||||
let step = 20; // pixels per keypress
|
let step = 20; // pixels per keypress
|
||||||
|
|
||||||
let new_width = e.key === 'ArrowLeft' ? current_width - step : current_width + step;
|
let new_width = e.key === 'ArrowLeft' ? current_width - step : current_width + step;
|
||||||
let left_percent = new_width * 100 / full_width;
|
let left_percent = new_width * 100 / full_width;
|
||||||
|
setChatWidth(left_percent);
|
||||||
// set min chat width to 300px
|
|
||||||
let min_left_width = 300 * 100 / full_width;
|
|
||||||
if (left_percent < min_left_width) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
left.style.flex = left_percent.toString();
|
|
||||||
right.style.flex = (100 - left_percent).toString();
|
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
@@ -3697,16 +3768,6 @@ document.getElementById('close-chat').onclick = function(e) {
|
|||||||
document.getElementById('show-chat').focus();
|
document.getElementById('show-chat').focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('close-chat').onkeydown = function(e) {
|
|
||||||
if(e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
setVisibility('left', false);
|
|
||||||
setVisibility('show-chat', true);
|
|
||||||
resizePeers();
|
|
||||||
document.getElementById('show-chat').focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('show-chat').onclick = function(e) {
|
document.getElementById('show-chat').onclick = function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setVisibility('left', true);
|
setVisibility('left', true);
|
||||||
@@ -3715,16 +3776,6 @@ document.getElementById('show-chat').onclick = function(e) {
|
|||||||
document.getElementById('close-chat').focus();
|
document.getElementById('close-chat').focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('show-chat').onkeydown = function(e) {
|
|
||||||
if(e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
setVisibility('left', true);
|
|
||||||
setVisibility('show-chat', false);
|
|
||||||
resizePeers();
|
|
||||||
document.getElementById('close-chat').focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function serverConnect() {
|
async function serverConnect() {
|
||||||
if(serverConnection && serverConnection.socket)
|
if(serverConnection && serverConnection.socket)
|
||||||
serverConnection.close();
|
serverConnection.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user