diff --git a/web/src/engine/osk/src/index.ts b/web/src/engine/osk/src/index.ts index cad97bde0e..be6de79618 100644 --- a/web/src/engine/osk/src/index.ts +++ b/web/src/engine/osk/src/index.ts @@ -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'; diff --git a/web/src/test/manual/web/osk/kbdLoader.mjs b/web/src/test/manual/web/osk/kbdLoader.mjs new file mode 100644 index 0000000000..47baaf3666 --- /dev/null +++ b/web/src/test/manual/web/osk/kbdLoader.mjs @@ -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); +} \ No newline at end of file diff --git a/web/src/test/manual/web/osk/scratchspace/commands.js b/web/src/test/manual/web/osk/scratchspace/commands.js new file mode 100644 index 0000000000..5f9e538ce8 --- /dev/null +++ b/web/src/test/manual/web/osk/scratchspace/commands.js @@ -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); +} \ No newline at end of file diff --git a/web/src/test/manual/web/osk/scratchspace/index.css b/web/src/test/manual/web/osk/scratchspace/index.css new file mode 100644 index 0000000000..f8484a81c4 --- /dev/null +++ b/web/src/test/manual/web/osk/scratchspace/index.css @@ -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; +} \ No newline at end of file diff --git a/web/src/test/manual/web/osk/scratchspace/index.html b/web/src/test/manual/web/osk/scratchspace/index.html new file mode 100644 index 0000000000..70b3f8fa79 --- /dev/null +++ b/web/src/test/manual/web/osk/scratchspace/index.html @@ -0,0 +1,179 @@ + + +
+ + + + + + + + + + + ++ 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. +
+ +Keyboard
+Layer
+OSK Mode
+Target Device
+Active Element
+Visibility
+Active Element
+Visibility
+OSK Sizing
+Textbox target:
+ + +Input target:
+ + +--End of Document--
+ + + + + \ No newline at end of file diff --git a/web/src/test/manual/web/osk/scratchspace/setup.mjs b/web/src/test/manual/web/osk/scratchspace/setup.mjs new file mode 100644 index 0000000000..26fdc98818 --- /dev/null +++ b/web/src/test/manual/web/osk/scratchspace/setup.mjs @@ -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; \ No newline at end of file