Revised Keyboard mode

This commit is contained in:
NaitLee 2022-04-27 01:59:18 +08:00
parent ab9ad406cb
commit a8454cbf51
11 changed files with 151 additions and 110 deletions

View File

@ -1,4 +1,4 @@
#!/bin/sh
cd www
npx tsc --allowJs --outFile main.comp.js polyfill.js i18n-ext.js i18n.js image.js main.js
npx tsc --allowJs --outFile main.comp.js polyfill.js i18n-ext.js i18n.js image.js accessibility.js main.js
cd ..

View File

@ -28,6 +28,7 @@ Currently:
- Friendly!
- Language support! You can participate in translation!
- Good user interface, adaptive to PC/mobile and light/dark theme!
- Accessibility features, everyone is considered!
- Feature-rich!
- Web UI, for most people!

View File

@ -15,12 +15,37 @@ In simple cases, you just make a copy of already-there language file, and modify
You should know what's your locale "code", for example "English (US)" is `en-US`. You can look at your browser locale configuration, or gather from the Web.
After that, add an entry in `list.json`.
## Naming
Plain:
```json
"there-is-an-apple": "There is an apple"
```
With parameter(s):
```json
"there-are-0-apples-in-1-baskets": "There are {0} apples in {1} baskets"
```
With Conditions: *(language dependent)*
```json
"0-apples": {
"single": "{0} apple",
"multiple": "{0} apples"
}
```
Special Key:
```json
"KeyboardLayout": "1234567890qwertzuiopasdfghjklyxcvbnm"
```
## It seems can't satisfy another grammar
That's what is going to be fun:
This i18n implementation didn't do built-in basics.
Instead the grammar details is all done by code.
Instead the grammar details are all done by code.
That doesn't mean difficulty:

View File

@ -24,6 +24,7 @@
- 友好!
- 语言支持!您可参与翻译!
- 良好的用户界面,可适应桌面/移动端/明暗主题!
- 无障碍功能,考虑到每一个人!
- 功能丰富!
- 网页界面,所有人都可以用!

99
www/accessibility.js Normal file
View File

@ -0,0 +1,99 @@
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 toLocaleKey(key) {
const qwerty = '1234567890qwertyuiopasdfghjklzxcvbnm';
let keys, index;
if (
typeof i18n === 'undefined' ||
key.length !== 1 ||
(keys = i18n('KeyboardLayout')) === 'KeyboardLayout' ||
(index = qwerty.indexOf(key)) === -1
) return key;
return keys[index];
}
function initKeyboardShortcuts() {
const layer = document.getElementById('keyboard-shortcuts-layer');
const dialog = document.getElementById('dialog');
let key, keys = 'qwertyuiopasdfghjklzxcvbnm';
let focus, focusing = 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 = {};
if (focusing) shortcuts = { ESC: focus };
else
for (let input of inputs) {
if (isHidden(input)) continue;
let key = toLocaleKey(input.getAttribute('data-key') || keys[key_index++]);
shortcuts[key] = input;
}
// Array.from(layer.children).forEach(e => e.remove());
for (let i = layer.children.length; i <= inputs.length; i++) {
let span = document.createElement('span');
layer.appendChild(span);
}
let index = 0;
for (let key in shortcuts) {
let span = layer.children[index++];
let input = shortcuts[key];
let position = input.getBoundingClientRect();
let text = key.toUpperCase().replace(' ', 'SPACE');
if (span.innerText !== text) span.innerText = text;
span.style.top = position.y + 'px';
span.style.left = position.x + 'px';
span.style.display = '';
}
for (let i = index; i < layer.children.length; i++) {
layer.children[i].style.display = 'none';
}
}
const start = () => setInterval(mark_keys, 1000);
const types_to_click = ['submit', 'file', 'checkbox', 'radio', 'A'];
document.body.addEventListener('keyup', (event) => {
key = event.key;
if (!started) {
if (key !== 'Tab') return;
mark_keys();
start();
started = true;
}
document.body.addEventListener('keyup', () =>
requestAnimationFrame(mark_keys)
, { once: true });
let input = shortcuts[key];
if (input) {
if (types_to_click.includes(input.type || input.tagName))
input.click();
else {
input.focus();
focusing = true;
}
focus = input;
} else if (key === 'Escape' && focus) {
focus.blur();
focusing = !focusing;
}
});
}

View File

@ -91,9 +91,9 @@
</div>
</div>
<div class="compact-menu">
<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>
<button class="compact-button" data-panel="panel-print" data-i18n="print" data-key="z">Print</button>
<button class="compact-button active" data-panel="panel-help" data-i18n="help" data-key="x">Help</button>
<button class="compact-button" data-panel="panel-settings" data-i18n="settings" data-key="c">Settings</button>
</div>
<div class="center">
<!-- -->
@ -121,7 +121,7 @@
<span>🌎</span>
<span data-i18n="language">Language</span>
</h2>
<select multiple id="select-language" data-key="5">
<select multiple id="select-language" data-key="a">
</select>
<p data-i18n="press-tab-to-control-with-keyboard">Press Tab to control with keyboard</p>
</div>
@ -143,7 +143,7 @@
<label for="large-font" data-i18n="large-font">Large Font</label>
</div>
</div>
<iframe id="frame" src="about:blank" name="frame" title="frame" data-key="6"></iframe>
<iframe id="frame" src="about:blank" name="frame" title="frame" data-key="v"></iframe>
</div>
<div id="dialog" class="hidden">
<div class="shade"></div>
@ -152,7 +152,7 @@
<!-- Dialog content -->
</div>
<div id="dialog-choices">
<input id="dialog-input" type="text" id="dialog-input" placeholder="" data-key="4">
<input id="dialog-input" type="text" id="dialog-input" placeholder="" data-key="m">
<hr />
</div>
</div>

View File

@ -49,6 +49,12 @@
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td>I18n "extensions"</td>
</tr>
<tr>
<td><a href="accessibility.js">accessibility.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
<td><a href="accessibility.js">accessibility.js</a></td>
<td>Accessibility features</td>
</tr>
<tr>
<td><a href="main.js">main.js</a></td>
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU-GPL-3.0-or-later</a></td>

View File

@ -1,5 +1,6 @@
{
"$language": "Deutsch",
"KeyboardLayout": "1234567890qwertzuiopasdfghjklyxcvbnm",
"cat-printer": "Cat Printer",
"printer": "Drucker",
"device-": "Gerät:",

View File

@ -6,7 +6,7 @@
var fallbacks = [
// main scripts, which we will directly modify
'i18n-ext.js', 'i18n.js', 'image.js', 'main.js',
'i18n-ext.js', 'i18n.js', 'image.js', 'accessibility.js', 'main.js',
// "compatibility" script, produced with eg. typescript tsc
'main.comp.js'
];

View File

@ -434,13 +434,7 @@ hr {
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)
);
transform: translate(-1em, calc(var(--font-size) * -1));
}
a {
@ -580,7 +574,7 @@ body.high-contrast {
transition-duration: 0s;
}
body.high-contrast .shade { transition-duration: 0s; opacity: 1; }
body.high-contrast * { background-color: var(--back-color); }
/* body.high-contrast * { background-color: var(--back-color); } */
body.high-contrast .logo, canvas { filter: unset !important; }
body.high-contrast #notice * { border: var(--border) dashed var(--fore-color); }
body.high-contrast a:any-link { color: #00f; }

View File

@ -83,11 +83,12 @@ const Dialog = (function() {
last_choices = [];
dialog_input.value = '';
dialog_input.style.display = have_input ? 'unset' : 'none';
const keys = 'bn,.';
let index = 1;
for (let choice of choices) {
let button = document.createElement('button');
button.setAttribute('data-i18n', choice);
button.setAttribute('data-key', index++);
button.setAttribute('data-key', keys[index++]);
button.innerText = i18n(choice);
if (!have_input)
button.addEventListener('click', () => dialog_input.value = choice);
@ -463,97 +464,6 @@ 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} */
@ -580,6 +490,10 @@ class Main {
let iframe = document.getElementById('frame');
iframe.addEventListener('load', () => {
iframe.contentDocument.body.classList.value = document.body.classList.value;
iframe.contentDocument.body.addEventListener('keyup', (event) => {
if (event.key === 'Escape')
document.body.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
});
applyI18nToDom(iframe.contentDocument);
});
function apply_class(class_name, value) {
@ -626,7 +540,7 @@ class Main {
await this.loadConfig();
if (this.settings['is_android'])
document.getElementById('select-language').multiple = false;
initKeyboardShortcuts();
if (typeof initKeyboardShortcuts === 'function') initKeyboardShortcuts();
this.searchDevices();
document.querySelector('main').classList.remove('hard-hidden');
document.getElementById('loading-screen').classList.add('hidden');