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="menu">
<div class="panel" id="panel-print" data-default> <div class="panel" id="panel-print" data-default>
<label for="device-options" data-i18n="device-">Device:</label> <label for="device-options" data-i18n="device-">Device:</label>
<select id="device-options"> <select id="device-options" data-key>
</select> </select>
<button id="device-refresh" data-i18n="refresh">Refresh</button> <button id="device-refresh" data-i18n="refresh" data-key>Refresh</button>
<hr /> <hr />
<label data-i18n="mode-">Mode:</label> <label data-i18n="mode-">Mode:</label>
<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> <span data-i18n="canvas">Canvas</span>
</label> </label>
<!-- <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> <span data-i18n="document">Document</span>
</label><br /> --> </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 /> <br />
<label data-i18n="process-as-">Process as:</label> <label data-i18n="process-as-">Process as:</label>
<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> <span data-i18n="text">Text</span>
</label> </label>
<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> <span data-i18n="picture">Picture</span>
</label> </label>
<!-- <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> <span data-i18n="pattern">Pattern</span>
</label> --><br /> </label> --><br />
<label for="threshold" data-i18n="threshold-">Threshold:</label> <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 /> <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> <label for="transparent-as-white" data-i18n="transparent-as-white">Transparent as White</label>
</div> </div>
<div class="panel active" id="panel-help"> <div class="panel active" id="panel-help">
<div> <div>
<p data-i18n="coming-soon">Coming Soon...</p> <p data-i18n="coming-soon">Coming Soon...</p>
<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 --> <!-- LibreJS doesn't work with dynamically inserted script tag. Going to complain -->
<a href="jslicense.html" data-jslicense="1" target="frame" <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> </p>
</div> </div>
</div> </div>
<div class="panel" id="panel-settings"> <div class="panel" id="panel-settings">
<label for="scan-time" data-i18n="scan-time-">Scan Time:</label> <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> <span data-i18n="-seconds">seconds</span>
<br /> <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> <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> <label for="flip-v" data-i18n="flip-vertically">Flip Vertically</label>
<hr /> <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> <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> <label for="dump" data-i18n="dump-traffic">Dump Traffic</label>
<br /> <br />
<button id="set-accessibility"> <button id="set-accessibility" data-key>
<span>🌎</span> <span>🌎</span>
<span data-i18n="accessibility">Accessibility</span> <span data-i18n="accessibility">Accessibility</span>
</button> </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"> <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> </div>
<div class="panel" id="panel-error"> <div class="panel" id="panel-error">
@ -91,9 +91,9 @@
</div> </div>
</div> </div>
<div class="compact-menu"> <div class="compact-menu">
<div class="compact-button" data-panel="panel-print" data-i18n="print">Print</div> <button class="compact-button" data-panel="panel-print" data-i18n="print" data-key="8">Print</button>
<div class="compact-button active" data-panel="panel-help" data-i18n="help">Help</div> <button class="compact-button active" data-panel="panel-help" data-i18n="help" data-key="9">Help</button>
<div class="compact-button" data-panel="panel-settings" data-i18n="settings">Settings</div> <button class="compact-button" data-panel="panel-settings" data-i18n="settings" data-key="0">Settings</button>
</div> </div>
<div class="center"> <div class="center">
<!-- --> <!-- -->
@ -107,7 +107,7 @@
<!-- <button id="canvas-expand" data-i18n="expand">Expand</button> <!-- <button id="canvas-expand" data-i18n="expand">Expand</button>
<button id="canvas-crop" data-i18n="crop">Crop</button> --> <button id="canvas-crop" data-i18n="crop">Crop</button> -->
<!-- <button id="button-preview" data-i18n="preview">Preview</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>
<div class="blank"></div> <div class="blank"></div>
</div> </div>
@ -117,29 +117,33 @@
<input type="file" id="file" /> <input type="file" id="file" />
<div id="accessibility"> <div id="accessibility">
<div> <div>
<h2 data-i18n="language">Language</h2> <h2>
<div id="select-language"> <span>🌎</span>
</div> <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>
<div> <div>
<h2 data-i18n="layout">Layout</h2> <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> <label for="dark-theme" data-i18n="dark-theme">Dark Theme</label>
<br /> <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> <label for="high-contrast" data-i18n="high-contrast">High Contrast</label>
<br /> <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> <label for="no-animation" data-i18n="disable-animation">Disable Animation</label>
<br /> <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> <label for="force-rtl" data-i18n="right-to-left-text-order">Right-to-left text order</label>
<br /> <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> <label for="large-font" data-i18n="large-font">Large Font</label>
</div> </div>
</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>
<div id="dialog" class="hidden"> <div id="dialog" class="hidden">
<div class="shade"></div> <div class="shade"></div>
@ -148,7 +152,7 @@
<!-- Dialog content --> <!-- Dialog content -->
</div> </div>
<div id="dialog-choices"> <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 /> <hr />
</div> </div>
</div> </div>
@ -164,10 +168,11 @@
<p class="noscript"> <p class="noscript">
<span>Please enable JavaScript!</span><br /> <span>Please enable JavaScript!</span><br />
<a href="jslicense.html" data-i18n="javascript-license-information" <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> </p>
</noscript> </noscript>
</div> </div>
<div id="keyboard-shortcuts-layer"></div>
<script src="loader.js"></script> <script src="loader.js"></script>
</body> </body>
</html> </html>

View File

@ -101,5 +101,10 @@
"high-contrast": "High Contrast", "high-contrast": "High Contrast",
"welcome": "Welcome!", "welcome": "Welcome!",
"copyright-and-license": "Copyright and License", "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": "高对比度", "high-contrast": "高对比度",
"welcome": "欢迎!", "welcome": "欢迎!",
"copyright-and-license": "版权与许可", "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-note: rgba(0, 255, 0, 0.2);
--notice-warn: rgba(255, 128, 0, 0.2); --notice-warn: rgba(255, 128, 0, 0.2);
--notice-error: rgba(255, 0, 0, 0.2); --notice-error: rgba(255, 0, 0, 0.2);
--shade: rgba(238, 238, 238, 0.5);
} }
body { body {
@ -141,7 +142,7 @@ main>.menu-side {
position: sticky; position: sticky;
top: 0; top: 0;
height: 100%; height: 100%;
overflow: auto; /* overflow: auto; */
margin: var(--span); margin: var(--span);
min-width: 12em; min-width: 12em;
} }
@ -163,8 +164,15 @@ main>.menu-side>.menu {
line-height: 2em; line-height: 2em;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
border: none;
border-top: var(--border) solid var(--fore-color); border-top: var(--border) solid var(--fore-color);
border-bottom: var(--border) solid transparent; border-bottom: var(--border) solid transparent;
padding: 0;
margin: 0;
}
.compact-button:hover {
padding: 0;
margin: 0;
} }
.compact-button.active { .compact-button.active {
border: var(--border) solid var(--fore-color); border: var(--border) solid var(--fore-color);
@ -339,7 +347,7 @@ hr {
#accessibility>*:nth-child(1) { #accessibility>*:nth-child(1) {
flex-grow: 1; flex-grow: 1;
} }
#select-language { #select-language[multiple] {
width: calc(100% - var(--span-double)); width: calc(100% - var(--span-double));
height: 8em; height: 8em;
border: var(--border) solid var(--fore-color); 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(2) { animation-delay: 0.3s; }
#loading-screen>.dots>span:nth-child(3) { animation-delay: 0.6s; } #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 { a {
transition: all var(--anim-time) ease-out; transition: all var(--anim-time) ease-out;
} }
@ -485,7 +520,7 @@ a {
} }
@media (prefers-color-scheme: dark) { @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; } body, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
a:link, a:visited { color: #66f; } a:link, a:visited { color: #66f; }
a:hover, a:active { color: #77f; } a:hover, a:active { color: #77f; }
@ -493,7 +528,7 @@ a {
#control-document, .logo { filter: brightness(0.6); } #control-document, .logo { filter: brightness(0.6); }
} }
/* so silly... */ /* 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, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
body.dark a:link, body.dark a:visited { color: #66f; } body.dark a:link, body.dark a:visited { color: #66f; }
body.dark a:hover, body.dark a:active { color: #77f; } body.dark a:hover, body.dark a:active { color: #77f; }
@ -541,6 +576,7 @@ body.high-contrast {
--notice-note: transparent; --notice-note: transparent;
--notice-warn: transparent; --notice-warn: transparent;
--notice-error: transparent; --notice-error: transparent;
--shade: rgba(0, 0, 0, 0.8);
transition-duration: 0s; transition-duration: 0s;
} }
body.high-contrast .shade { transition-duration: 0s; opacity: 1; } body.high-contrast .shade { transition-duration: 0s; opacity: 1; }

View File

@ -83,9 +83,11 @@ const Dialog = (function() {
last_choices = []; last_choices = [];
dialog_input.value = ''; dialog_input.value = '';
dialog_input.style.display = have_input ? 'unset' : 'none'; dialog_input.style.display = have_input ? 'unset' : 'none';
let index = 1;
for (let choice of choices) { for (let choice of choices) {
let button = document.createElement('button'); let button = document.createElement('button');
button.setAttribute('data-i18n', choice); button.setAttribute('data-i18n', choice);
button.setAttribute('data-key', index++);
button.innerText = i18n(choice); button.innerText = i18n(choice);
if (!have_input) if (!have_input)
button.addEventListener('click', () => dialog_input.value = choice); button.addEventListener('click', () => dialog_input.value = choice);
@ -403,7 +405,7 @@ function applyI18nToDom(doc) {
} }
async function initI18n() { async function initI18n() {
if (typeof i18n === 'undefined') return; if (typeof i18n === 'undefined') return;
/** @type {HTMLOptionElement} */ /** @type {HTMLSelectElement} */
let language_options = document.getElementById('select-language'); let language_options = document.getElementById('select-language');
/** @type {{ [code: string]: string }} */ /** @type {{ [code: string]: string }} */
let list = await fetch('/lang/list.json').then(r => r.json()); 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); i18n.add(value, await fetch(`/lang/${value}.json`).then(r => r.json()), true);
applyI18nToDom(); applyI18nToDom();
} }
language_options.addEventListener('change', () => {
language_options.selectedOptions.item(0).click();
});
for (let code in list) { for (let code in list) {
let option = document.createElement('option'); let option = document.createElement('option');
option.value = code; option.value = code;
option.innerText = list[code]; option.innerText = list[code];
option.addEventListener('click', (event) => { option.addEventListener('click', (event) => {
/** @type {HTMLOptionElement} */
let option = event.currentTarget; let option = event.currentTarget;
let value = option.value; let value = option.value;
language_options.selectedIndex = option.index;
use_language(value); use_language(value);
Notice.note('welcome');
}); });
language_options.appendChild(option); language_options.appendChild(option);
} }
@ -455,6 +463,97 @@ async function testI18n(lang) {
, true); , 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 { class Main {
promise; promise;
/** @type {CanvasController} */ /** @type {CanvasController} */
@ -525,6 +624,9 @@ class Main {
this.attachSetter('#flip-v', 'change', 'flip_v'); this.attachSetter('#flip-v', 'change', 'flip_v');
this.attachSetter('#dump', 'change', 'dump'); this.attachSetter('#dump', 'change', 'dump');
await this.loadConfig(); await this.loadConfig();
if (this.settings['is_android'])
document.getElementById('select-language').multiple = false;
initKeyboardShortcuts();
this.searchDevices(); this.searchDevices();
document.querySelector('main').classList.remove('hard-hidden'); document.querySelector('main').classList.remove('hard-hidden');
document.getElementById('loading-screen').classList.add('hidden'); document.getElementById('loading-screen').classList.add('hidden');