diff --git a/0-transpile.sh b/0-transpile.sh index deb6bd8..4af87b6 100755 --- a/0-transpile.sh +++ b/0-transpile.sh @@ -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 .. diff --git a/README.md b/README.md index 810a45b..51fd9ff 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/i18n.i18n/i18n.md b/i18n.i18n/i18n.md index b074fa3..1392a9c 100644 --- a/i18n.i18n/i18n.md +++ b/i18n.i18n/i18n.md @@ -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: diff --git a/readme.i18n/README.zh_CN.md b/readme.i18n/README.zh_CN.md index 4bf6abf..9256748 100644 --- a/readme.i18n/README.zh_CN.md +++ b/readme.i18n/README.zh_CN.md @@ -24,6 +24,7 @@ - 友好! - 语言支持!您可参与翻译! - 良好的用户界面,可适应桌面/移动端/明暗主题! + - 无障碍功能,考虑到每一个人! - 功能丰富! - 网页界面,所有人都可以用! diff --git a/www/accessibility.js b/www/accessibility.js new file mode 100644 index 0000000..01d1263 --- /dev/null +++ b/www/accessibility.js @@ -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; + } + }); +} diff --git a/www/index.html b/www/index.html index 94e95de..ab89c90 100644 --- a/www/index.html +++ b/www/index.html @@ -91,9 +91,9 @@
- - - + + +
@@ -121,7 +121,7 @@ 🌎 Language -

Press Tab to control with keyboard

@@ -143,7 +143,7 @@ - +
- +
diff --git a/www/jslicense.html b/www/jslicense.html index be5541c..bece627 100644 --- a/www/jslicense.html +++ b/www/jslicense.html @@ -49,6 +49,12 @@ i18n-ext.js I18n "extensions" + + accessibility.js + CC0-1.0-only + accessibility.js + Accessibility features + main.js GNU-GPL-3.0-or-later diff --git a/www/lang/de-DE.json b/www/lang/de-DE.json index 38d8817..a0dba02 100644 --- a/www/lang/de-DE.json +++ b/www/lang/de-DE.json @@ -1,5 +1,6 @@ { "$language": "Deutsch", + "KeyboardLayout": "1234567890qwertzuiopasdfghjklyxcvbnm", "cat-printer": "Cat Printer", "printer": "Drucker", "device-": "Gerät:", diff --git a/www/loader.js b/www/loader.js index a37204e..eb6e641 100644 --- a/www/loader.js +++ b/www/loader.js @@ -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' ]; diff --git a/www/main.css b/www/main.css index ab7c830..8ae510c 100644 --- a/www/main.css +++ b/www/main.css @@ -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; } diff --git a/www/main.js b/www/main.js index 20cc467..dd89edf 100644 --- a/www/main.js +++ b/www/main.js @@ -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');