chore(web): initial connection of attachment engine to ContextManager

This commit is contained in:
Joshua A. Horton 2023-04-25 14:24:27 +07:00
parent 69673bc291
commit fe16d0ce45
5 changed files with 82 additions and 49 deletions

View file

@ -2,6 +2,14 @@
"name": "keyman",
"description": "Facilitates text input in any language.",
"exports": {
"./engine/attachment": {
"types": "./build/engine/attachment/obj/index.d.ts",
"import": "./build/engine/attachment/obj/index.js"
},
"./engine/attachment/lib": {
"types": "./build/engine/attachment/obj/index.d.ts",
"import": "./build/engine/attachment/lib/index.mjs"
},
"./engine/paths": {
"types": "./build/engine/paths/obj/index.d.ts",
"import": "./build/engine/paths/obj/index.js"

View file

@ -2,6 +2,7 @@ import { type Keyboard, Mock } from '@keymanapp/keyboard-processor';
import { type KeyboardStub } from 'keyman/engine/package-cache';
import { CookieSerializer } from 'keyman/engine/dom-utils';
import { OutputTarget } from 'keyman/engine/element-wrappers';
import { PageContextAttachment } from 'keyman/engine/attachment';
import {
ContextManagerBase,
type KeyboardInterface
@ -18,6 +19,23 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
private config: BrowserConfiguration;
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
readonly focusAssistant = new FocusAssistant();
readonly page: PageContextAttachment;
constructor(engineConfig: BrowserConfiguration) {
super(engineConfig);
this.page = new PageContextAttachment(window.document, {
hostDevice: this.config.hostDevice,
isTopLevel: true
});
this.engineConfig.deferForInitialization.then(() => {
// TODO: set up attachment-listeners here that can add necessary event-hooks
// for focus management here!
this.page.install(this.engineConfig.attachType == 'manual');
});
}
initialize(): void {
this.on('keyboardasyncload', (stub, completion) => {

View file

@ -36,16 +36,22 @@ export default class DefaultBrowserRules extends DefaultRules {
const contextManager = this.contextManager;
let elem: HTMLElement;
switch(code) {
// This method will be transplanted to the specific `ContextManager` module stored above.
// This method will be handled between `ContextManager` and PageContextAttachment:
// pageContextAttachment.findNeighboringInput(contextManager.activeTarget.getElement(), <same flag>)
case Codes.keyCodes['K_TAB']:
domManager.moveToNext((Lkc.Lmodifiers & Codes.modifierCodes['SHIFT']) != 0);
const bBack = (Lkc.Lmodifiers & Codes.modifierCodes['SHIFT']) != 0;
elem = contextManager.page.findNeighboringInput(contextManager.activeTarget.getElement(), bBack);
elem.focus();
break;
case Codes.keyCodes['K_TABBACK']:
domManager.moveToNext(true);
elem = contextManager.page.findNeighboringInput(contextManager.activeTarget.getElement(), true);
elem.focus();
break;
case Codes.keyCodes['K_TABFWD']:
domManager.moveToNext(false);
elem = contextManager.page.findNeighboringInput(contextManager.activeTarget.getElement(), false);
elem.focus();
break;
}

View file

@ -136,11 +136,6 @@ export interface PageAttachmentOptions {
*/
hostDevice: DeviceSpec;
/**
* The KMW init() option, as set for the page.
*/
attachType: 'manual' | 'auto';
/**
* Should only be set to `true` for the top-level page. Should be `false` for
* any pages embedded in another page via iframe.
@ -199,6 +194,8 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
return this._sortedInputs;
}
private manualAttach: boolean;
/**
* Tracks the attachment MutationObserver.
*/
@ -239,7 +236,10 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
* Call this method **once**, when the page is fully loaded, to attach to all page elements
* eligible to serve as context for Keyman keyboard input.
*/
install() {
install(manualAttach: boolean) {
// Do before _SetupDocument!
this.manualAttach = manualAttach;
this._SetupDocument(document.documentElement);
// KMW 16.0 and before: these were only ever established for the top-level doc, and so for
@ -247,7 +247,7 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
//
// That said, for future consideration: enable it within iframe-internal documents too.
if(this.options.isTopLevel) {
this.initMutationObservers(this.document, this.options.attachType == 'manual');
this.initMutationObservers(this.document, manualAttach);
}
}
@ -558,7 +558,7 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
embeddedPageAttachment.on('enabled', (elem) => this.emit('enabled', elem));
embeddedPageAttachment.on('disabled', (elem) => this.emit('disabled', elem));
embeddedPageAttachment.install();
embeddedPageAttachment.install(this.manualAttach);
}
}
}
@ -737,6 +737,44 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
this._sortedInputs=tList;
}
/**
* Move focus to next (or previous) input or text area element on TAB
* Uses list of actual input elements
*
* Note that activeElement() on touch devices returns the DIV that overlays
* the input element, not the element itself.
*
* @param {number|boolean} bBack Direction to move (0 or 1)
*/
findNeighboringInput(activeBase: HTMLElement, bBack: number|boolean) {
var i,t=this.sortedInputs;
if(t.length == 0) {
return null;
}
// Identify the active element in the list of inputs ordered by position
for(i=0; i<t.length; i++) {
if(t[i] == activeBase) {
break;
}
}
// If the array is empty or does not hold the element, reverse by one so that
// either the last (bBack: true) or the first (bBack: false) element is selected.
if(i == t.length && !bBack) { // otherwise, ... "or the second".
i--;
}
// Find the next (or previous) element in the list
i = bBack ? i-1 : i+1;
// Treat the list as circular, wrapping the index if necessary.
i = i >= t.length ? i-t.length : i;
i = i < 0 ? i+t.length : i;
return t[i];
}
/**
* Function _GetDocumentEditables

View file

@ -453,43 +453,6 @@ namespace com.keyman.dom {
}
}
/**
* Move focus to next (or previous) input or text area element on TAB
* Uses list of actual input elements
*
* Note that activeElement() on touch devices returns the DIV that overlays
* the input element, not the element itself.
*
* @param {number|boolean} bBack Direction to move (0 or 1)
*/
moveToNext(bBack: number|boolean) {
var i,t=this.sortedInputs, activeBase = this.activeElement;
var touchable = this.keyman.util.device.touchable;
if(t.length == 0) {
return;
}
// For touchable devices, get the base element of the DIV
if(touchable) {
activeBase=activeBase.base;
}
// Identify the active element in the list of inputs ordered by position
for(i=0; i<t.length; i++) {
if(t[i] == activeBase) break;
}
// Find the next (or previous) element in the list
i = bBack ? i-1 : i+1;
// Treat the list as circular, wrapping the index if necessary.
i = i >= t.length ? i-t.length : i;
i = i < 0 ? i+t.length : i;
// Move to the selected element
t[i].focus();
}
/**
* Move focus to user-specified element
*