Accessibility: keyboard mode by Tab

This commit is contained in:
NaitLee 2022-04-26 17:24:46 +08:00
parent bf81524083
commit ab9ad406cb
5 changed files with 195 additions and 42 deletions

View File

@ -16,73 +16,73 @@
<div class="menu">
<div class="panel" id="panel-print" data-default>
<label for="device-options" data-i18n="device-">Device:</label>
<select id="device-options">
<select id="device-options" data-key>
</select>
<button id="device-refresh" data-i18n="refresh">Refresh</button>
<button id="device-refresh" data-i18n="refresh" data-key>Refresh</button>
<hr />
<label data-i18n="mode-">Mode:</label>
<label>
<input type="radio" name="mode" value="mode-canvas" checked />
<input type="radio" name="mode" value="mode-canvas" data-key checked />
<span data-i18n="canvas">Canvas</span>
</label>
<!-- <label>
<input type="radio" name="mode" value="mode-document" />
<input type="radio" name="mode" value="mode-document" data-key />
<span data-i18n="document">Document</span>
</label><br /> -->
<button id="insert-picture" data-i18n="insert-picture">Insert Picture</button>
<button id="insert-picture" data-i18n="insert-picture" data-key="Enter">Insert Picture</button>
<br />
<label data-i18n="process-as-">Process as:</label>
<label>
<input type="radio" name="algo" value="algo-direct" />
<input type="radio" name="algo" value="algo-direct" data-key />
<span data-i18n="text">Text</span>
</label>
<label>
<input type="radio" name="algo" value="algo-steinberg" checked />
<input type="radio" name="algo" value="algo-steinberg" data-key checked />
<span data-i18n="picture">Picture</span>
</label>
<!-- <label>
<input type="radio" name="algo" value="algo-halftone" />
<input type="radio" name="algo" value="algo-halftone" data-key />
<span data-i18n="pattern">Pattern</span>
</label> --><br />
<label for="threshold" data-i18n="threshold-">Threshold:</label>
<input type="range" min="0" max="256" value="128" step="8" id="threshold" data-default />
<input type="range" min="0" max="256" value="128" step="8" id="threshold" data-key data-default />
<br />
<input type="checkbox" name="transparent-as-white" id="transparent-as-white" checked />
<input type="checkbox" name="transparent-as-white" id="transparent-as-white" data-key checked />
<label for="transparent-as-white" data-i18n="transparent-as-white">Transparent as White</label>
</div>
<div class="panel active" id="panel-help">
<div>
<p data-i18n="coming-soon">Coming Soon...</p>
<p>
<a href="about.html" target="frame" data-i18n="about">About</a>
<a href="about.html" target="frame" data-i18n="about" data-key>About</a>
<!-- LibreJS doesn't work with dynamically inserted script tag. Going to complain -->
<a href="jslicense.html" data-jslicense="1" target="frame"
data-i18n="javascript-license-information">JavaScript License Information</a>
data-i18n="javascript-license-information" data-key>JavaScript License Information</a>
</p>
</div>
</div>
<div class="panel" id="panel-settings">
<label for="scan-time" data-i18n="scan-time-">Scan Time:</label>
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" />
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" data-key />
<span data-i18n="-seconds">seconds</span>
<br />
<input type="checkbox" name="flip-h" id="flip-h" />
<input type="checkbox" name="flip-h" id="flip-h" data-key />
<label for="flip-h" data-i18n="flip-horizontally">Flip Horizontally</label>
<input type="checkbox" name="flip-v" id="flip-v" />
<input type="checkbox" name="flip-v" id="flip-v" data-key />
<label for="flip-v" data-i18n="flip-vertically">Flip Vertically</label>
<hr />
<input type="checkbox" name="dry-run" id="dry-run" />
<input type="checkbox" name="dry-run" id="dry-run" data-key />
<label for="dry-run" data-i18n="dry-run">Dry Run</label>
<input type="checkbox" name="dump" id="dump" />
<input type="checkbox" name="dump" id="dump" data-key />
<label for="dump" data-i18n="dump-traffic">Dump Traffic</label>
<br />
<button id="set-accessibility">
<button id="set-accessibility" data-key>
<span>🌎</span>
<span data-i18n="accessibility">Accessibility</span>
</button>
<button class="hidden" data-panel="panel-error" data-i18n="error-message">Error Message</button>
<button class="hidden" data-panel="panel-error" data-i18n="error-message" data-key>Error Message</button>
<div class="center">
<button id="button-exit" data-i18n="exit">Exit</button>
<button id="button-exit" data-i18n="exit" data-key>Exit</button>
</div>
</div>
<div class="panel" id="panel-error">
@ -91,9 +91,9 @@
</div>
</div>
<div class="compact-menu">
<div class="compact-button" data-panel="panel-print" data-i18n="print">Print</div>
<div class="compact-button active" data-panel="panel-help" data-i18n="help">Help</div>
<div class="compact-button" data-panel="panel-settings" data-i18n="settings">Settings</div>
<button class="compact-button" data-panel="panel-print" data-i18n="print" data-key="8">Print</button>
<button class="compact-button active" data-panel="panel-help" data-i18n="help" data-key="9">Help</button>
<button class="compact-button" data-panel="panel-settings" data-i18n="settings" data-key="0">Settings</button>
</div>
<div class="center">
<!-- -->
@ -107,7 +107,7 @@
<!-- <button id="canvas-expand" data-i18n="expand">Expand</button>
<button id="canvas-crop" data-i18n="crop">Crop</button> -->
<!-- <button id="button-preview" data-i18n="preview">Preview</button> -->
<button id="button-print" data-i18n="print">Print</button>
<button id="button-print" data-i18n="print" data-key=" ">Print</button>
</div>
<div class="blank"></div>
</div>
@ -117,29 +117,33 @@
<input type="file" id="file" />
<div id="accessibility">
<div>
<h2 data-i18n="language">Language</h2>
<div id="select-language">
</div>
<h2>
<span>🌎</span>
<span data-i18n="language">Language</span>
</h2>
<select multiple id="select-language" data-key="5">
</select>
<p data-i18n="press-tab-to-control-with-keyboard">Press Tab to control with keyboard</p>
</div>
<div>
<h2 data-i18n="layout">Layout</h2>
<input type="checkbox" name="dark-theme" id="dark-theme" />
<input type="checkbox" name="dark-theme" id="dark-theme" data-key />
<label for="dark-theme" data-i18n="dark-theme">Dark Theme</label>
<br />
<input type="checkbox" name="high-contrast" id="high-contrast" />
<input type="checkbox" name="high-contrast" id="high-contrast" data-key />
<label for="high-contrast" data-i18n="high-contrast">High Contrast</label>
<br />
<input type="checkbox" name="no-animation" id="no-animation" />
<input type="checkbox" name="no-animation" id="no-animation" data-key />
<label for="no-animation" data-i18n="disable-animation">Disable Animation</label>
<br />
<input type="checkbox" name="force-rtl" id="force-rtl" />
<input type="checkbox" name="force-rtl" id="force-rtl" data-key />
<label for="force-rtl" data-i18n="right-to-left-text-order">Right-to-left text order</label>
<br />
<input type="checkbox" name="large-font" id="large-font" />
<input type="checkbox" name="large-font" id="large-font" data-key />
<label for="large-font" data-i18n="large-font">Large Font</label>
</div>
</div>
<iframe id="frame" src="about:blank" name="frame" title="frame"></iframe>
<iframe id="frame" src="about:blank" name="frame" title="frame" data-key="6"></iframe>
</div>
<div id="dialog" class="hidden">
<div class="shade"></div>
@ -148,7 +152,7 @@
<!-- Dialog content -->
</div>
<div id="dialog-choices">
<input id="dialog-input" type="text" id="dialog-input" placeholder="">
<input id="dialog-input" type="text" id="dialog-input" placeholder="" data-key="4">
<hr />
</div>
</div>
@ -164,10 +168,11 @@
<p class="noscript">
<span>Please enable JavaScript!</span><br />
<a href="jslicense.html" data-i18n="javascript-license-information"
data-jslicense="1" >JavaScript License Information</a>
data-jslicense="1" tabindex="1" >JavaScript License Information</a>
</p>
</noscript>
</div>
<div id="keyboard-shortcuts-layer"></div>
<script src="loader.js"></script>
</body>
</html>

View File

@ -101,5 +101,10 @@
"high-contrast": "High Contrast",
"welcome": "Welcome!",
"copyright-and-license": "Copyright and License",
"some-rights-reserved": "Some rights reserved."
"some-rights-reserved": "Some rights reserved.",
"ENTER": "Enter",
"SPACE": "Space",
"ESCAPE": "Esc",
"TAB": "Tab",
"press-tab-to-control-with-keyboard": "Press Tab to control with keyboard"
}

View File

@ -96,5 +96,10 @@
"high-contrast": "高对比度",
"welcome": "欢迎!",
"copyright-and-license": "版权与许可",
"some-rights-reserved": "保留一些权利。"
"some-rights-reserved": "保留一些权利。",
"ENTER": "回车",
"SPACE": "空格",
"ESCAPE": "ESC",
"TAB": "Tab",
"press-tab-to-control-with-keyboard": "按下 Tab 以使用键盘操作"
}

View File

@ -20,6 +20,7 @@
--notice-note: rgba(0, 255, 0, 0.2);
--notice-warn: rgba(255, 128, 0, 0.2);
--notice-error: rgba(255, 0, 0, 0.2);
--shade: rgba(238, 238, 238, 0.5);
}
body {
@ -141,7 +142,7 @@ main>.menu-side {
position: sticky;
top: 0;
height: 100%;
overflow: auto;
/* overflow: auto; */
margin: var(--span);
min-width: 12em;
}
@ -163,8 +164,15 @@ main>.menu-side>.menu {
line-height: 2em;
text-align: center;
cursor: pointer;
border: none;
border-top: var(--border) solid var(--fore-color);
border-bottom: var(--border) solid transparent;
padding: 0;
margin: 0;
}
.compact-button:hover {
padding: 0;
margin: 0;
}
.compact-button.active {
border: var(--border) solid var(--fore-color);
@ -339,7 +347,7 @@ hr {
#accessibility>*:nth-child(1) {
flex-grow: 1;
}
#select-language {
#select-language[multiple] {
width: calc(100% - var(--span-double));
height: 8em;
border: var(--border) solid var(--fore-color);
@ -408,6 +416,33 @@ hr {
#loading-screen>.dots>span:nth-child(2) { animation-delay: 0.3s; }
#loading-screen>.dots>span:nth-child(3) { animation-delay: 0.6s; }
#keyboard-shortcuts-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
overflow: visible;
pointer-events: all;
z-index: 2;
}
#keyboard-shortcuts-layer span {
display: inline-block;
position: absolute;
/* border: var(--border) dotted var(--fore-color); */
background-color: var(--shade);
padding: var(--span-half) var(--span);
white-space: pre;
line-height: 1em;
font-family: 'DejaVu Sans Mono', 'Consolas', monospace;
transform: translateY(calc(var(--font-size) * -1));
}
body.force-rtl #keyboard-shortcuts-layer span {
transform: translate(
calc(var(--font-size) * 2),
calc(var(--font-size) * -1)
);
}
a {
transition: all var(--anim-time) ease-out;
}
@ -485,7 +520,7 @@ a {
}
@media (prefers-color-scheme: dark) {
:root { --fore-color: #eee; --back-color: #333; }
:root { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); }
body, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
a:link, a:visited { color: #66f; }
a:hover, a:active { color: #77f; }
@ -493,7 +528,7 @@ a {
#control-document, .logo { filter: brightness(0.6); }
}
/* so silly... */
body.dark { --fore-color: #eee; --back-color: #333; }
body.dark { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); }
body.dark, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
body.dark a:link, body.dark a:visited { color: #66f; }
body.dark a:hover, body.dark a:active { color: #77f; }
@ -541,6 +576,7 @@ body.high-contrast {
--notice-note: transparent;
--notice-warn: transparent;
--notice-error: transparent;
--shade: rgba(0, 0, 0, 0.8);
transition-duration: 0s;
}
body.high-contrast .shade { transition-duration: 0s; opacity: 1; }

View File

@ -83,9 +83,11 @@ const Dialog = (function() {
last_choices = [];
dialog_input.value = '';
dialog_input.style.display = have_input ? 'unset' : 'none';
let index = 1;
for (let choice of choices) {
let button = document.createElement('button');
button.setAttribute('data-i18n', choice);
button.setAttribute('data-key', index++);
button.innerText = i18n(choice);
if (!have_input)
button.addEventListener('click', () => dialog_input.value = choice);
@ -403,7 +405,7 @@ function applyI18nToDom(doc) {
}
async function initI18n() {
if (typeof i18n === 'undefined') return;
/** @type {HTMLOptionElement} */
/** @type {HTMLSelectElement} */
let language_options = document.getElementById('select-language');
/** @type {{ [code: string]: string }} */
let list = await fetch('/lang/list.json').then(r => r.json());
@ -412,14 +414,20 @@ async function initI18n() {
i18n.add(value, await fetch(`/lang/${value}.json`).then(r => r.json()), true);
applyI18nToDom();
}
language_options.addEventListener('change', () => {
language_options.selectedOptions.item(0).click();
});
for (let code in list) {
let option = document.createElement('option');
option.value = code;
option.innerText = list[code];
option.addEventListener('click', (event) => {
/** @type {HTMLOptionElement} */
let option = event.currentTarget;
let value = option.value;
language_options.selectedIndex = option.index;
use_language(value);
Notice.note('welcome');
});
language_options.appendChild(option);
}
@ -455,6 +463,97 @@ async function testI18n(lang) {
, true);
}
function isHidden(element) {
let parents = [element];
while (parents[0].parentElement)
parents.unshift(parents[0].parentElement);
return parents.some(e => {
let rect = e.getBoundingClientRect();
return (
e.classList.contains('hidden') ||
e.classList.contains('hard-hidden') ||
e.style.display == 'none' ||
rect.width == 0 || rect.height == 0 ||
rect.x < 0 || rect.y < 0 ||
e.style.visibility == 'none' ||
e.style.opacity == '0'
);
});
}
function initKeyboardShortcuts() {
const layer = document.getElementById('keyboard-shortcuts-layer');
const dialog = document.getElementById('dialog');
let key, keys = 'qwertyuiopasdfghjklzxcvbnm';
let focused, onbreak = false, started = false;
let inputs, shortcuts = {};
const mark_keys = () => {
if (dialog.classList.contains('hidden'))
inputs = Array.from(document.querySelectorAll('*[data-key]'));
else inputs = Array.from(document.querySelectorAll('#dialog *[data-key]'));
/** @type {{ [key: string]: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement }} */
let key_index = 0;
shortcuts = {};
for (let input of inputs) {
if (isHidden(input)) continue;
let key = input.getAttribute('data-key') || keys[key_index++];
shortcuts[key] = input;
}
Array.from(layer.children).forEach(e => e.remove());
for (let key in shortcuts) {
if (onbreak) {
let rect = focused.getBoundingClientRect();
let span = document.createElement('span');
span.innerText = i18n('ESCAPE');
span.style.top = rect.y + 'px';
span.style.left = rect.x + 'px';
layer.appendChild(span);
break;
}
let input = shortcuts[key];
let position = input.getBoundingClientRect();
let span = document.createElement('span');
span.innerText = i18n(key.toUpperCase().replace(' ', 'SPACE'));
span.style.top = position.y + 'px';
span.style.right = (window.innerWidth - position.x) + 'px';
layer.appendChild(span);
}
}
const start = () => setInterval(mark_keys, 1000);
document.body.addEventListener('keyup', (event) => {
document.body.addEventListener('keyup', () => requestAnimationFrame(mark_keys), { once: true });
key = event.key;
if (key === 'Tab') {
if (!started) { start(); started = true; }
mark_keys();
return;
}
let input = shortcuts[key];
if (input) {
switch (input.type || input.tagName) {
case 'range':
case 'text':
case 'number':
case 'tel':
case 'date':
case 'select-one':
case 'select-multiple':
case 'DIV':
case 'TEXTAREA':
input.focus();
onbreak = true;
break;
default:
input.click();
}
focused = input;
} else if (key === 'Escape' && focused) {
focused.blur();
onbreak = !onbreak;
}
});
}
class Main {
promise;
/** @type {CanvasController} */
@ -525,6 +624,9 @@ class Main {
this.attachSetter('#flip-v', 'change', 'flip_v');
this.attachSetter('#dump', 'change', 'dump');
await this.loadConfig();
if (this.settings['is_android'])
document.getElementById('select-language').multiple = false;
initKeyboardShortcuts();
this.searchDevices();
document.querySelector('main').classList.remove('hard-hidden');
document.getElementById('loading-screen').classList.add('hidden');