feat(web): standalone osk tech demo / 'test' page

This commit is contained in:
Joshua A. Horton 2023-01-24 15:49:38 +07:00
parent 921d0da4b7
commit f94cd9f5dd
6 changed files with 687 additions and 0 deletions

View file

@ -2,6 +2,7 @@ export { DeviceSpec } from '@keymanapp/web-utils/build/obj/deviceSpec.js';
export { default as Keyboard } from '@keymanapp/keyboard-processor/build/obj/keyboards/keyboard.js';
export { default as Codes } from '@keymanapp/keyboard-processor/build/obj/text/codes.js';
export { default as KeyboardProperties } from './keyboardProperties.js';
export { default as FloatingOSKView } from './views/floatingOskView.js';
export { default as FloatingOSKViewCookie } from './views/floatingOskCookie.js';
export { default as AnchoredOSKView } from './views/anchoredOskView.js';
@ -9,6 +10,7 @@ export { default as InlinedOSKView } from './views/inlinedOskView.js';
export { BannerController } from './banner/bannerView.js';
export { default as VisualKeyboard } from './visualKeyboard.js';
export type { default as ViewConfiguration } from './config/viewConfiguration.js';
export type { default as SpacebarText } from './spacebarText.js';
export { default as Activator } from './views/activator.js';
export { default as SimpleActivator } from './views/simpleActivator.js';

View file

@ -0,0 +1,82 @@
import { Keyboard, KeyboardProperties, Codes } from '../../../../../build/engine/osk/lib/index.mjs';
// This script may or may not be temporary; the KMW "KeyboardManager" class may be spun off in a manner
// that could replace this.
export function loadKeyboardFromPath(path) {
const window_KeymanWeb = window['KeymanWeb'];
const window_keyman = window['keyman'];
const promise = new Promise((resolve, reject) => {
const loader = window['KeymanWeb'] = {
KR(kbdObj) {
this.keyboard = kbdObj;
},
keyboard: null,
};
// Needed for some debug-compiled keyboards. Also, the `keyman` top-level var itself is
// needed for any KMW 10+ keyboards.
window['keyman'] = {
osk: {
modifierCodes: Codes.modifierCodes,
keyCodes: Codes.keyCodes
}
};
const script = document.createElement('script');
document.head.appendChild(script);
script.onerror = reject;
script.onload = () => {
if(loader.keyboard) {
resolve(loader.keyboard);
} else {
reject();
}
// https://stackoverflow.com/a/37393041 - totally safe.
script.remove();
}
// Now that EVERYTHING ELSE is ready, establish the link to the keyboard's script.
script.src = path;
});
promise.finally(() => {
window['KeymanWeb'] = window_KeymanWeb;
window['keyman'] = window_keyman;
if(window['KeymanWeb'] === undefined) {
delete window['KeymanWeb'];
}
if(window['keyman'] === undefined) {
delete window['keyman'];
}
});
return promise;
}
export function loadKeyboardsFromStubs(apiStubs) {
let keyboards = {};
let priorPromise = Promise.resolve();
for(let stub of apiStubs) {
priorPromise = priorPromise.then(() => {
// Adds closure to capture 'id' for async completion use.
let overwriteLoader = (id, path) => {
let loader = loadKeyboardFromPath(path);
return loader.then(kbd => {
keyboards[stub.id].keyboard = new Keyboard(kbd)
});
}
keyboards[stub.id] = {
keyboard: overwriteLoader(stub.id, stub.filename),
metadata: new KeyboardProperties(stub)
};
return keyboards[stub.id].keyboard;
});
}
return priorPromise.then(() => keyboards);
}

View file

@ -0,0 +1,265 @@
// Reference to the currently-active OSK.
var currentOSK = null;
var currentOSKMode = null;
// The currently-selected keyboard + metadata pairing.
var keyboard = null; // Cannot immediately initialized, as module-scripts use defer loading.
function log(str) {
document.getElementById('eventlog').textContent += str + '\n\n';
}
// NOT a module - so we can declare a few event listener methods.
function setOSK(mode) {
let modeList = ['floating', 'anchored', 'inline'];
if(currentOSK) {
if(mode) {
if(!modeList.includes(mode)) {
console.warn(`Invalid OSK mode requested: "${mode}"`);
}
}
// Close out the previous one.
currentOSK.shutdown();
if(currentOSK.element) {
try {
currentOSK.element.remove();
} catch(err) {
console.error(err);
}
}
}
showOSK(true); // Reset related CSS classes for UI elements.
modeList.forEach((panel) => {
let panelElement = document.getElementById(panel + "-mode");
if(panel == mode) {
panelElement.classList.remove('inactive');
panelElement.classList.add('active');
} else {
panelElement.classList.remove('active');
panelElement.classList.add('inactive');
}
});
let config = {
...BaseConfiguration,
device: window.targetDevice,
hostDevice: window.HOST_DEVICE
};
// Find the buttons & highlight accordingly.
const oskGroup = document.getElementById('osk-select');
const buttons = ['floating', 'anchored', 'inline'];
highlightGroupSelection(oskGroup, buttons, mode);
// Possible if there's no active OSK yet.
currentOSK = null;
currentOSKMode = mode;
if(!mode) {
return;
}
// Set up the new one.
switch(mode) {
case 'floating':
currentOSK = new modules.osk.FloatingOSKView(config);
setTarget('text');
break;
case 'anchored':
currentOSK = new modules.osk.AnchoredOSKView(config);
setTarget('text');
break;
case 'inline':
currentOSK = new modules.osk.InlinedOSKView(config);
document.getElementById('inline-container').appendChild(currentOSK.element);
// Default: 8px for most browsers.
const bodyMargin = parseInt(getComputedStyle(document.body).margin, 10);
const maxWidth = screen.width - 2 * bodyMargin;
// Check page width & constrain the OSK's width if necessary
let targetWidth = 800;
if(targetWidth > maxWidth) {
targetWidth = maxWidth;
}
// Finally, set the OSK's size.
currentOSK.setSize(targetWidth, targetWidth / 2);
break;
default:
}
if(!keyboard) {
keyboard = setKeyboard('us');
}
currentOSK.activeKeyboard = keyboard;
currentOSK.on('keyEvent', (event) => {
let eventText = JSON.stringify(event, (key, value) => {
switch(key) {
case 'srcKeyboard':
case 'source':
case 'device':
case 'keyDistribution':
return undefined;
}
return value;
});
log(eventText);
});
currentOSK.on('shouldShowLanguageMenu', () => {
log("(language menu requested)");
});
currentOSK.on('onshow', () => {
log("(osk shown)");
});
currentOSK.on('onhide', () => {
log("(osk hidden)");
})
}
function highlightGroupSelection(group, names, selected) {
for(let i = 0; i < names.length; i++) {
const button = group.children.item(i);
if(names[i] == selected) {
button.classList.add('selected');
} else {
button.classList.remove('selected');
}
}
}
function inlineSetSize() {
currentOSK.setSize(document.getElementById('set-width').value + 'px', document.getElementById('set-height').value + 'px');
}
function setTarget(type) {
let target = null;
const textarea = document.getElementById('ta1');
const input = document.getElementById('in1');
textarea.classList.remove('selected');
input.classList.remove('selected');
// Find the buttons & highlight accordingly.
const floatTarget = document.getElementById('float-target');
const anchorTarget = document.getElementById('anchor-target');
const buttons = ['text', 'input', 'none'];
highlightGroupSelection(floatTarget, buttons, type);
highlightGroupSelection(anchorTarget, buttons, type);
if(type == 'text') {
target = textarea;
} else if(type == 'input') {
target = input;
}
currentOSK.activationModel.activationTrigger = target;
if(target) {
target.classList.add('selected');
}
}
function showOSK(mode) {
// Find the buttons & highlight accordingly.
const floatShow = document.getElementById('float-show');
const anchorShow = document.getElementById('anchor-show');
const buttons = [true, false];
highlightGroupSelection(floatShow, buttons, mode);
highlightGroupSelection(anchorShow, buttons, mode);
if(!currentOSK) {
return;
}
const visibleAtStart = currentOSK.isVisible();
if(mode !== undefined) {
currentOSK.show(mode);
} else {
currentOSK.show();
highlightGroupSelection(floatShow, buttons, !visibleAtStart);
highlightGroupSelection(anchorShow, buttons, visibleAtStart);
}
}
function setKeyboard(kbdId) {
if(kbdId) {
const keyboardGroup = document.getElementById('keyboard-select');
const buttons = ['us', 'lao_2008_basic', 'obolo_chwerty_6351'];
highlightGroupSelection(keyboardGroup, buttons, kbdId);
keyboard = window.keyboards[kbdId];
} else if(!window.keyboard) {
setKeyboard('us');
return;
}
// Build the layer commands.
const layerButtonGroup = document.getElementById('layer-select');
const layout = keyboard.keyboard.layout(window.targetDevice.formFactor);
const layerNames = Object.keys(layout.layerMap);
layerButtonGroup.innerHTML = ''; // clear all children
for(let layer of layerNames) {
const button = document.createElement('button');
button.textContent = layer;
button.onclick = () => {
if(currentOSK && currentOSK.vkbd) {
currentOSK.vkbd.layerId = layer;
for(let i = 0; i < layerButtonGroup.children.length; i++) {
const btn = layerButtonGroup.children.item(i);
if(btn == button) {
btn.classList.add('selected');
} else {
btn.classList.remove('selected');
}
}
}
}
if(layer == 'default') {
button.classList.add('selected');
}
layerButtonGroup.appendChild(button);
}
if(!currentOSK) {
return keyboard;
}
// From here out, rely on the global var `keyboard`.
currentOSK.activeKeyboard = keyboard;
return keyboard;
}
function setTargetDevice(target) {
const devices = {
windows: new modules.osk.DeviceSpec('chrome', 'desktop', 'windows', false),
aPhone: new modules.osk.DeviceSpec('chrome', 'phone', 'android', true),
aPad: new modules.osk.DeviceSpec('chrome', 'tablet', 'android', true),
iPhone: new modules.osk.DeviceSpec('safari', 'phone', 'ios', true),
iPad: new modules.osk.DeviceSpec('safari', 'tablet', 'ios', true),
};
if(target) {
const deviceGroup = document.getElementById('device-select');
const buttons = ['windows', 'aPhone', 'aPad', 'iPhone', 'iPad'];
highlightGroupSelection(deviceGroup, buttons, target);
window.targetDevice = devices[target];
}
setOSK(currentOSKMode);
}

View file

@ -0,0 +1,113 @@
body {font-family: Tahoma,helvetica;}
h3 {font-size: 1em;font-weight:normal;color: darkred; margin-bottom: 4px}
.test {font-size: 24px; width:80%; min-height:30px; border: 1px solid gray;}
#KeymanWebControl {width:50%;min-width:600px;}
.inactive {
display: none;
}
textarea.selected, input.selected {
border-width: 3px;
}
#keyboard-controller,
#osk-selector {
width: fit-content;
margin-bottom: 4px;
}
body.phone #keyboard-controller,
body.phone #osk-selector {
width: unset;
height: unset;
}
#keyboard-controller button,
#osk-selector button {
min-width: fit-content;
}
div.panel {
padding: 4px;
background-color: lightgray;
width: fit-content;
}
div.panel p.category {
display: inline-block;
font-weight: bold;
margin-top: 0.25em;
margin-bottom: 0.25em;
}
div.panel .group {
border: 1px darkgray solid;
padding: 4px;
}
div.panel div {
min-width: fit-content;
padding: 4px 0px;
}
body.phone div.panel {
width: calc(100% - 8px); /* 4px padding on div.panel, both sides */
}
div.panel .gap {
width: 4px;
display: inline-block;
}
div.panel div input {
float: right;
}
div.flex {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 4px;
flex-wrap: wrap;
}
div.flex.inactive {
display: none;
}
div.flex button {
flex-grow: 1;
}
div.flex .group {
flex-grow: 1;
}
#eventlog {
width: 100%;
}
body.touch .notouch {
display: none;
}
body .touch {
display: none;
}
body.touch div.touch {
display: block;
}
div.disabled {
font-size: 0.9em;
font-style:italic;
color:darkslategray"
}
button.selected {
font-weight: bold;
}

View file

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<!-- Set the viewport width to match phone and tablet device widths -->
<meta name="viewport" content="width=device-width,user-scalable=no" />
<!-- Allow KeymanWeb to be saved to the iPhone home screen -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Enable IE9 Standards mode -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Testing Page - standalone OSK</title>
<!-- Your page CSS -->
<link rel="stylesheet" type="text/css" href="index.css"/>
<!-- Initialization: set paths to keyboards, resources and fonts as required -->
<script type="module" src="setup.mjs"></script>
<script src="commands.js"></script>
</head>
<!-- Sample page HTML -->
<body>
<h2>KeymanWeb OSK Module Testing</h2>
<p>
Note: if hosted via an IIS setup, you may need to add the 'mjs' extension under
"MIME Types" for it to properly serve related module-based JS files.
</p>
<hr/>
<div id="controllers">
<div id="keyboard-controller" class="flex panel">
<div class="group">
<p class="category">Keyboard</p>
<div id="keyboard-select" class="flex">
<button onclick="setKeyboard('us');">US English</button>
<button onclick="setKeyboard('lao_2008_basic');">Lao</button>
<button onclick="setKeyboard('obolo_chwerty_6351');">Obolo Chwerty</button>
</div>
</div>
<!-- select keyboard to use -->
<!-- select active layer -->
<div class="group">
<p class="category">Layer</p>
<div id="layer-select" class="flex">
<button>default</button>
<button>shift</button>
</div>
</div>
</div>
<div id="osk-selector" class="flex panel" style="flex-direction:column">
<div class="group">
<p class="category">OSK Mode</p>
<div id="osk-select" class="flex">
<button onclick="setOSK('floating');">Floating (desktop) OSK</button>
<button onclick="setOSK('anchored');">Anchored (touch) OSK</button>
<button onclick="setOSK('inline');">Inline OSK</button>
</div>
</div>
<div class="group">
<p class="category">Target Device</p>
<div id="device-select" class="flex">
<button onclick="setTargetDevice('windows');">Windows desktop</button>
<button onclick="setTargetDevice('aPhone');">Android phone</button>
<button onclick="setTargetDevice('aPad');">Android tablet</button>
<button onclick="setTargetDevice('iPhone');" class="selected">iPhone</button>
<button onclick="setTargetDevice('iPad');">iPad</button>
</div>
</div>
</div>
<div id="floating-mode" class="flex panel inactive">
<div class="group">
<!-- OutputTarget selector (text vs input) -->
<p class="category">Active Element</p>
<div id="float-target" class="flex">
<button onclick="setTarget('text')">Textarea</button>
<button onclick="setTarget('input')">Input</button>
<button onclick="setTarget('none')">None</button>
</div>
</div>
<div class="group">
<!-- show / hide -->
<p class="category">Visibility</p>
<div id="float-show" class="notouch flex">
<button onclick="showOSK(true)">Show</button>
<button onclick="showOSK(false)">Hide</button>
<button onclick="showOSK()">Toggle</button>
</div>
<div class="touch disabled">
Disabled (touch-based)
</div>
</div>
</div>
<div id="anchored-mode" class="flex panel inactive">
<div class="group">
<!-- OutputTarget selector (text vs input) -->
<p class="category">Active Element</p>
<div id="anchor-target" class="flex">
<button onclick="setTarget('text')">Textarea</button>
<button onclick="setTarget('input')">Input</button>
<button onclick="setTarget('none')">None</button>
</div>
</div>
<div class="group">
<!-- show / hide -->
<p class="category">Visibility</p>
<div id="anchor-show" class="flex notouch">
<button onclick="showOSK(true)">Show</button>
<button onclick="showOSK(false)">Hide</button>
<button onclick="showOSK()">Toggle</button>
</div>
<div class="touch disabled">
Disabled (touch-based)
</div>
</div>
</div>
<div id="inline-mode" class="flex panel inactive">
<div class="group">
<p class="category">OSK Sizing</p>
<div>Width: <span class='gap'></span><input id="set-width" value='800'/></div>
<div>Height: <span class='gap'></span><input id="set-height" value='400'/></div>
<button onclick="inlineSetSize()" style="width:100%; margin-top: 4px;">Apply</button>
</div>
</div>
</div>
<hr/>
<div id="inline-container" style="width: 80%; height: 40%"></div>
<p>Textbox target:</p>
<textarea id='ta1' class='test'></textarea>
<p>Input target:</p>
<input id='in1' class='test' value=''/>
<hr/>
<h3>Event log:</h3>
<textarea id="eventlog" rows="10" readonly></textarea>
<button onclick="document.getElementById('eventlog').textContent = '';">Clear</button>
<hr/>
<h3><a href="../..">Return to testing home page</a></h3>
<!-- include a blank div to enable scrolling -->
<div style="height:1000px"></div>
<p>--End of Document--</p>
</body>
<!--
*** DEVELOPER NOTE -- FIREFOX CONFIGURATION FOR TESTING ***
*
* If the URL bar starts with <b>file://</b>, Firefox may not load the font used
* to display the special characters used in the On-Screen Keyboard.
*
* To work around this Firefox bug, navigate to <b>about:config</b>
* and set <b>security.fileuri.strict_origin_policy</b> to <b>false</b>
* while testing.
*
* Firefox resolves website-based CSS URI references correctly without needing
* any configuration change, so this change should only be made for file-based testing.
*
***
-->
</html>

View file

@ -0,0 +1,46 @@
import * as KeymanOSK from '../../../../../../build/engine/osk/lib/index.mjs';
import Device from '../../../../../../build/engine/device-detect/lib/index.mjs';
import { loadKeyboardFromPath, loadKeyboardsFromStubs } from '../kbdLoader.mjs';
// NOTE: we must actually set these in this manner to have accessibility outside of
// this script tag!
window.modules = {
osk: KeymanOSK,
device: Device
};
let prefix = '../../';
const stubs = [
{id:'us',name:'English',languages:{id:'en',name:'English'}, filename:(prefix + 'us-1.0.js')},
{id:'obolo_chwerty_6351',name:'obolo_chwerty_6351',languages:{id:'en',name:'English'},
filename:(prefix + 'obolo_chwerty_6351.js')},
{id:'lao_2008_basic',name:'Lao Basic', languages: {id:'lo',name:'Lao',region:'Asia'},
filename:(prefix + 'lao_2008_basic-1.2.js')}
]
window.keyboards = loadKeyboardsFromStubs(stubs).then((keyboards) => {
window.keyboards = keyboards;
});
let device = new Device();
device.detect();
if(device.formFactor == 'phone') {
document.body.classList.add('phone');
}
if(device.touchable) {
document.body.classList.add('touch');
}
window.HOST_DEVICE = device.coreSpec;
window.targetDevice = new modules.osk.DeviceSpec('safari', 'phone', 'ios', true);
window.BaseConfiguration = {
isEmbedded: false,
fontRootPath: '',
// Relative path to the resources' original versions.
commonStyleSheetRefs: ['../../../../../resources/osk/kmwosk.css', '../../../../../resources/osk/globe-hint.css']
};
window.keyboardLoader = loadKeyboardFromPath;