dedup code for modals in popup

This commit is contained in:
tophf 2020-11-23 12:09:30 +03:00
parent 4d198f56e2
commit 5bb1b5ef35
4 changed files with 84 additions and 90 deletions

View File

@ -387,9 +387,10 @@ function focusAccessibility() {
}
/**
* Switches to the next/previous keyboard-focusable element
* Switches to the next/previous keyboard-focusable element.
* Doesn't check `visibility` or `display` via getComputedStyle for simplicity.
* @param {HTMLElement} rootElement
* @param {Number} step - for exmaple 1 or -1
* @param {Number} step - for exmaple 1 or -1 (or 0 to focus the first focusable el in the box)
* @returns {HTMLElement|false|undefined} -
* HTMLElement: focus changed,
* false: focus unchanged,
@ -397,16 +398,15 @@ function focusAccessibility() {
*/
function moveFocus(rootElement, step) {
const elements = [...rootElement.getElementsByTagName('*')];
const activeIndex = Math.max(0, elements.indexOf(document.activeElement));
const activeEl = document.activeElement;
const activeIndex = step ? Math.max(step < 0 ? 0 : -1, elements.indexOf(activeEl)) : -1;
const num = elements.length;
const {activeElement} = document;
if (!step) step = 1;
for (let i = 1; i < num; i++) {
const elementIndex = (activeIndex + i * step + num) % num;
// we don't use positive tabindex so we stop at any valid value
const el = elements[elementIndex];
const el = elements[(activeIndex + i * step + num) % num];
if (!el.disabled && el.tabIndex >= 0) {
el.focus();
return activeElement !== el && el;
return activeEl !== el && el;
}
}
}
@ -462,3 +462,18 @@ function setupLivePrefs(
}
}
}
/* exported getEventKeyName */
/**
* @param {KeyboardEvent} e
* @param {boolean} [letterAsCode] - use locale-independent KeyA..KeyZ for single-letter chars
*/
function getEventKeyName(e, letterAsCode) {
const mods =
(e.shiftKey ? 'Shift-' : '') +
(e.ctrlKey ? 'Ctrl-' : '') +
(e.altKey ? 'Alt-' : '') +
(e.metaKey ? 'Meta-' : '');
return (mods === e.key + '-' ? '' : mods) +
(e.key.length === 1 && letterAsCode ? e.code : e.key);
}

View File

@ -25,22 +25,22 @@
</label>
</div>
<div class="actions">
<a href="#" class="configure" i18n-title="configureStyle" tabindex="0">
<a href="#" class="configure" i18n-title="configureStyle">
<svg class="svg-icon config"><use xlink:href="#svg-icon-config"></use></svg>
</a>
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel" tabindex="0">
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel">
<svg class="svg-icon edit" viewBox="0 0 14 16">
<path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"/>
</svg>
</a>
<a href="#" class="menu-button" i18n-title="popupMenuButtonTooltip" tabindex="0">
<a href="#" class="menu-button" i18n-title="popupMenuButtonTooltip">
<svg class="svg-icon menu-button-icon" viewBox="0 0 3 16">
<path fill-rule="evenodd" d="M0 2.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zm0 5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zM1.5 14a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</a>
</div>
</div>
<div class="menu" tabindex="-1">
<div class="menu">
<div class="menu-items-wrapper">
<b class="menu-title"></b>
<label class="menu-item exclude-by-domain button">

View File

@ -729,11 +729,6 @@ body.blocked .actions > .main-controls {
display: flex;
}
#confirm[data-display=true] + #installed .menu[data-display=true] {
opacity: 0;
pointer-events: none;
}
#confirm > div {
width: 80%;
max-height: 80%;

View File

@ -10,9 +10,11 @@
configDialog
FIREFOX
getActiveTab
getEventKeyName
getStyleDataMerged
hotkeys
initializing
moveFocus
msg
onDOMready
prefs
@ -30,6 +32,7 @@ let installed;
const handleEvent = {};
const ENTRY_ID_PREFIX_RAW = 'style-';
const MODAL_SHOWN = 'data-display'; // attribute name
$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
@ -135,6 +138,19 @@ async function initPopup(frames) {
$('#popup-wiki-button').onclick = handleEvent.openURLandHide;
$('#confirm').onclick = function (e) {
const {id} = this.dataset;
switch (e.target.dataset.cmd) {
case 'ok':
hideModal(this, {animate: true});
API.deleteStyle(Number(id));
break;
case 'cancel':
showModal($('.menu', $.entry(id)), '.menu-close');
break;
}
};
if (!prefs.get('popup.stylesFirst')) {
document.body.insertBefore(
$('body > .actions'),
@ -468,88 +484,20 @@ Object.assign(handleEvent, {
toggleMenu(event) {
const entry = handleEvent.getClickedStyleElement(event);
const menu = $('.menu', entry);
const menuActive = $('.menu[data-display=true]');
if (menuActive) {
// fade-out style menu
animateElement(menu, 'lights-on')
.then(() => (menu.dataset.display = false));
window.onkeydown = null;
if (menu.hasAttribute(MODAL_SHOWN)) {
hideModal(menu, {animate: true});
} else {
$('.menu-title', entry).textContent = $('.style-name', entry).textContent;
menu.dataset.display = true;
menu.style.cssText = '';
window.onkeydown = event => {
const close = $('.menu-close', entry);
const checkbox = $('.exclude-by-domain-checkbox', entry);
if (document.activeElement === close && (event.key === 'Tab') && !event.shiftKey) {
event.preventDefault();
checkbox.focus();
}
if (document.activeElement === checkbox && (event.key === 'Tab') && event.shiftKey) {
event.preventDefault();
close.focus();
}
if (event.key === 'Escape') {
event.preventDefault();
close.click();
}
};
showModal(menu, '.menu-close');
}
event.preventDefault();
},
delete(event) {
const entry = handleEvent.getClickedStyleElement(event);
const id = entry.styleId;
const box = $('#confirm');
const menu = $('.menu', entry);
const cancel = $('[data-cmd="cancel"]', box);
const affirm = $('[data-cmd="ok"]', box);
box.dataset.display = true;
box.style.cssText = '';
box.dataset.id = entry.styleId;
$('b', box).textContent = $('.style-name', entry).textContent;
affirm.focus();
affirm.onclick = () => confirm(true);
cancel.onclick = () => confirm(false);
window.onkeydown = event => {
const close = $('.menu-close', entry);
const checkbox = $('.exclude-by-domain-checkbox', entry);
const confirmActive = $('#confirm[data-display="true"]');
const {key} = event;
if (document.activeElement === cancel && (key === 'Tab')) {
event.preventDefault();
affirm.focus();
}
if (document.activeElement === close && (key === 'Tab') && !event.shiftKey) {
event.preventDefault();
checkbox.focus();
}
if (document.activeElement === checkbox && (key === 'Tab') && event.shiftKey) {
event.preventDefault();
close.focus();
}
if (key === 'Escape') {
event.preventDefault();
if (confirmActive) {
box.dataset.display = false;
menu.focus();
} else {
close.click();
}
}
};
function confirm(ok) {
if (ok) {
// fade-out deletion confirmation dialog
animateElement(box, 'lights-on')
.then(() => (box.dataset.display = false));
window.onkeydown = null;
API.deleteStyle(id);
} else {
box.dataset.display = false;
menu.focus();
}
}
showModal(box, '[data-cmd=cancel]');
},
configure(event) {
@ -672,3 +620,39 @@ function blockPopup(isBlocked = true) {
t.template.noStyles.remove();
}
}
function showModal(box, cancelButtonSelector) {
const oldBox = $(`[${MODAL_SHOWN}]`);
if (oldBox) box.style.animationName = 'none';
// '' would be fine but 'true' is backward-compatible with the existing userstyles
box.setAttribute(MODAL_SHOWN, 'true');
box._onkeydown = e => {
const key = getEventKeyName(e);
switch (key) {
case 'Tab':
case 'Shift-Tab':
e.preventDefault();
moveFocus(box, e.shiftKey ? -1 : 1);
break;
case 'Escape': {
e.preventDefault();
window.onkeydown = null;
$(cancelButtonSelector, box).click();
break;
}
}
};
window.on('keydown', box._onkeydown);
moveFocus(box, 0);
hideModal(oldBox);
}
async function hideModal(box, {animate} = {}) {
window.off('keydown', box._onkeydown);
box._onkeydown = null;
if (animate) {
box.style.animationName = '';
await animateElement(box, 'lights-on');
}
box.removeAttribute(MODAL_SHOWN);
}