Merge pull request #2849 from keymanapp/refactor/web/engine/outputTarget-extensions

refactor(web/engine): extended abstraction with OutputTarget
This commit is contained in:
Joshua Horton 2020-03-23 12:47:34 +07:00 • committed by GitHub
commit 2c4b39e316
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 131 additions and 54 deletions

View file

@ -31,6 +31,10 @@ namespace com.keyman.dom {
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLElement {
return this.root;
}
@ -214,6 +218,20 @@ namespace com.keyman.dom {
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;

View file

@ -49,6 +49,10 @@ namespace com.keyman.dom {
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLIFrameElement {
return this.root;
}
@ -220,6 +224,20 @@ namespace com.keyman.dom {
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;

View file

@ -29,6 +29,10 @@ namespace com.keyman.dom {
this._cachedSelectionStart = -1;
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLInputElement {
return this.root;
}
@ -138,5 +142,25 @@ namespace com.keyman.dom {
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
Input.newlineHandler(this.root);
}
static newlineHandler(inputEle: HTMLInputElement) {
// Can't occur for Mocks - just Input and TouchAlias types.
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
// Allows compiling this separately from the main body of KMW.
// TODO: rework class to accept a class-static 'callback' from the DOM module that this can call.
// Would eliminate the need for this 'static' reference.
// Only strongly matters once we better modularize KMW, with web-dom vs web-dom-targets vs web-core, etc.
if(com.keyman["singleton"]) {
com.keyman["singleton"].domManager.moveToNext(false);
}
}
}
}
}

View file

@ -36,6 +36,10 @@ namespace com.keyman.dom {
this._cachedSelectionStart = -1;
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLTextAreaElement {
return this.root;
}
@ -145,5 +149,9 @@ namespace com.keyman.dom {
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
}
}

View file

@ -63,6 +63,20 @@ namespace com.keyman.dom {
this.root.setTextBeforeCaret(this.root.getTextBeforeCaret() + s);
}
handleNewlineAtCaret(): void {
// Insert new line in text area fields
if(this.root.base.nodeName == 'TEXTAREA') {
// As the TouchAliasElement was implemented long before OutputTargets, it already has
// built-in newline handling.
this.insertTextBeforeCaret('\n');
} else if(dom.Utils.instanceof(this.root.base, "HTMLInputElement")) {
// HTMLInputElements do not permit newlines; they instead have DOM-specific behaviors.
Input.newlineHandler(this.root.base as HTMLInputElement);
} else {
console.warn("TouchAlias OutputTarget cannot output newlines for unexpected base element types!");
}
}
protected setTextAfterCaret(s: string) {
this.root.setText(this.getTextBeforeCaret() + s, this.getTextBeforeCaret()._kmwLength());
}

View file

@ -127,7 +127,7 @@ namespace com.keyman.text {
let nativeDefaultKeyOutput = Processor.prototype.defaultKeyOutput;
// Overrides the 'native'-mode implementation with in-app friendly defaults prioritized over 'native' defaults.
Processor.prototype.defaultKeyOutput = function(this: Processor, Lkc: KeyEvent, keyShiftState: number, usingOSK: boolean, disableDOM: boolean): string {
Processor.prototype.defaultKeyOutput = function(this: Processor, Lkc: KeyEvent, keyShiftState: number, usingOSK: boolean): string {
let keyman = com.keyman.singleton;
// Note: this assumes Lelem is properly attached and has an element interface.
@ -140,7 +140,7 @@ namespace com.keyman.text {
if (code == Codes.keyCodes.K_SPACE) {
return ' ';
} else if (code == Codes.keyCodes.K_BKSP) {
if(!disableDOM) {
if(!(Lkc.Ltarg instanceof Mock) ) {
keyman['interface'].defaultBackspace();
return '';
} else {
@ -433,7 +433,7 @@ namespace com.keyman.text {
// Check the virtual key
Lkc = {
Ltarg: Lelem,
Ltarg: com.keyman.text.Processor.getOutputTarget(Lelem),
Lmodifiers: keyShiftState,
Lstates: 0,
Lcode: Codes.keyCodes[keyName],

View file

@ -59,6 +59,14 @@ namespace com.keyman.text {
this._dks = new text.DeadkeyTracker();
}
/**
* Signifies that this OutputTarget has no default key processing behaviors. This should be false
* for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement.
*/
get isSynthetic(): boolean {
return true;
}
deadkeys(): text.DeadkeyTracker {
return this._dks;
}
@ -243,6 +251,13 @@ namespace com.keyman.text {
*/
abstract insertTextBeforeCaret(s: string): void;
/**
* Allows element-specific handling for ENTER key inputs. Conceptually, this should usually
* correspond to `insertTextBeforeCaret('\n'), but actual implementation will vary greatly among
* elements.
*/
abstract handleNewlineAtCaret(): void;
/**
* Saves element-specific state properties prone to mutation, enabling restoration after
* text-output operations.
@ -342,6 +357,10 @@ namespace com.keyman.text {
this.caretIndex += s.kmwLength();
}
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
protected setTextAfterCaret(s: string): void {
this.text = this.getTextBeforeCaret() + s;
}

View file

@ -38,22 +38,18 @@ namespace com.keyman.text {
* @param {boolean} usingOSK
* @return {string}
*/
defaultKeyOutput(Lkc: KeyEvent, keyShiftState: number, usingOSK: boolean, disableDOM: boolean): string {
var Lkc: KeyEvent;
defaultKeyOutput(Lkc: KeyEvent, keyShiftState: number, usingOSK: boolean): string {
let keyName = Lkc.kName;
let n = Lkc.Lcode;
let outputTarget = Lkc.Ltarg;
let Lelem = outputTarget.getElement();
let keyman = com.keyman.singleton;
let domManager = keyman.domManager;
let activeKeyboard = keyman.keyboardManager.activeKeyboard;
let quiet = keyman.interface.activeTargetOutput instanceof Mock;
let quiet = outputTarget instanceof Mock;
var ch = '', checkCodes = false;
var touchAlias = (Lelem && typeof(Lelem.base) != 'undefined');
// check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs.
if(keyShiftState == 0) {
checkCodes = true;
@ -66,8 +62,9 @@ namespace com.keyman.text {
}
}
// If this was triggered by the OSK -or- if it was triggered within a touch-aliased DIV element.
if(touchAlias || usingOSK) {
// If this was triggered by the OSK -or- if it was triggered by a 'synthetic' OutputTarget (TouchAlias, Mock)
// that lacks default key processing behavior.
if(usingOSK || outputTarget.isSynthetic) {
var code = Codes.keyCodes[keyName];
if(!code) {
code = n;
@ -75,51 +72,36 @@ namespace com.keyman.text {
switch(code) {
case Codes.keyCodes['K_BKSP']: //Only desktop UI, not touch devices. TODO: add repeat while mouse down for desktop UI
if(disableDOM) {
if(quiet) {
// TODO: Remove need for this clause via refactoring. It's currently needed for predictive text Mocks.
return '\b'; // the escape sequence for backspace.
} else {
keyman.interface.defaultBackspace(outputTarget);
}
return '';
case Codes.keyCodes['K_TAB']:
if(!disableDOM) {
if(!quiet) {
domManager.moveToNext(keyShiftState);
}
break;
case Codes.keyCodes['K_TABBACK']:
if(!disableDOM) {
if(!quiet) {
domManager.moveToNext(true);
}
break;
case Codes.keyCodes['K_TABFWD']:
if(!disableDOM) {
if(!quiet) {
domManager.moveToNext(false);
}
break;
case Codes.keyCodes['K_ENTER']:
// Insert new line in text area fields
if(Lelem.nodeName == 'TEXTAREA' || (typeof Lelem.base != 'undefined' && Lelem.base.nodeName == 'TEXTAREA')) {
return '\n';
// Or move to next field from TEXT fields
} else if(usingOSK) {
var inputEle: HTMLInputElement;
if(keyman.isEmbedded) { // In embedded mode, the OutputTarget may not meet other conditions.
return '\n';
} else if(dom.Utils.instanceof(Lelem, "HTMLInputElement")) {
inputEle = <HTMLInputElement> Lelem;
} else if(typeof(Lelem.base) != 'undefined' && dom.Utils.instanceof(Lelem.base, "HTMLInputElement")) {
inputEle = <HTMLInputElement> Lelem.base;
}
if(!disableDOM) {
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
domManager.moveToNext(false);
}
}
// Ensure the newline is forwarded when in an embedded context! It doesn't yet have its own specialized OutputTarget type.
if(keyman.isEmbedded && !quiet) { // Don't pass it along for Mocks, though. They're (currently) only used in predictive text.
keyman['oninserttext'](0, '\n');
}
break;
outputTarget.handleNewlineAtCaret();
return '\n'; // We still return this, as it ensures we generate a rule-match.
case Codes.keyCodes['K_SPACE']:
return ' ';
// break;
@ -163,16 +145,11 @@ namespace com.keyman.text {
// Not strictly if `Lkc.vkCode` is properly maintained, but it's good to have an
// extra safety; this would have blocked the backspace bug as well.
} else if(Lkc.Lcode == 8) {
if(disableDOM) {
return '\b'; // the escape sequence for backspace.
} else {
keyman.interface.defaultBackspace();
return '';
}
keyman.interface.defaultBackspace();
return '';
}
// Translate numpad keystrokes into their non-numpad equivalents
// TODO: Make part of 'defaultKeyOutput'.
if(Lkc.Lcode >= Codes.keyCodes["K_NP0"] && Lkc.Lcode <= Codes.keyCodes["K_NPSLASH"] && activeKeyboard && !activeKeyboard['KM']) {
// Number pad, numlock on
if(Lkc.Lcode < 106) {
@ -334,7 +311,7 @@ namespace com.keyman.text {
}
}
processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget, fromOSK: boolean, disableDOM: boolean): RuleBehavior {
processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget, fromOSK: boolean): RuleBehavior {
let keyman = com.keyman.singleton;
var activeKeyboard = keyman.keyboardManager.activeKeyboard;
@ -367,15 +344,13 @@ namespace com.keyman.text {
kbdInterface.activeTargetOutput = outputTarget;
let preInput = Mock.from(outputTarget);
// TODO: That third parameter should be `fromOSK`, not always `true`. Changing this breaks Mocks a bit, though,
// and will likely have interactions with the '\b' check below.
var ch = this.defaultKeyOutput(keyEvent, keyEvent.Lmodifiers, true, disableDOM);
var ch = this.defaultKeyOutput(keyEvent, keyEvent.Lmodifiers, fromOSK);
kbdInterface.activeTargetOutput = null;
if(ch) {
if(ch == '\b') { // Is only returned when disableDOM is true, which prevents automatic default backspace application.
// defaultKeyOutput can't always find the outputTarget if we're working with alternates!
kbdInterface.defaultBackspace(outputTarget);
} else {
} else if(ch != '\n') { // \n is handled automatically now.
kbdInterface.output(0, outputTarget, ch);
}
matchBehavior = new RuleBehavior();
@ -480,7 +455,7 @@ namespace com.keyman.text {
let preInputMock = Mock.from(outputTarget);
let ruleBehavior = this.processKeystroke(keyEvent, outputTarget, fromOSK, false);
let ruleBehavior = this.processKeystroke(keyEvent, outputTarget, fromOSK);
// Swap layer as appropriate.
if(keyEvent.kNextLayer) {
@ -506,7 +481,8 @@ namespace com.keyman.text {
}
let altEvent = this._GetClickEventProperties(altKey, keyEvent.Ltarg.getElement());
let alternateBehavior = this.processKeystroke(altEvent, mock, fromOSK, true);
altEvent.Ltarg = mock;
let alternateBehavior = this.processKeystroke(altEvent, mock, fromOSK);
if(alternateBehavior) {
// TODO: if alternateBehavior.beep == true, set 'p' to 0. It's a disallowed key sequence,
// so a user should never have intended to type it. Should probably renormalize
@ -650,8 +626,8 @@ namespace com.keyman.text {
// To facilitate storing relevant commands, we should probably reverse-lookup
// the actual keyname instead.
mappingEvent.kName = 'K_xxxx';
mappingEvent.Ltarg = null;
var mappedChar: string = this.defaultKeyOutput(mappingEvent, (shifted ? 0x10 : 0), false, true);
mappingEvent.Ltarg = new Mock(); // helps prevent breakage for mnemonics.
var mappedChar: string = this.defaultKeyOutput(mappingEvent, (shifted ? 0x10 : 0), false);
/* First, save a backup of the original code. This one won't needlessly trigger keyboard
* rules, but allows us to replicate/emulate commands after rule processing if needed.

View file

@ -41,7 +41,7 @@
attachType:'auto'
}).then(function() {
kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'}, filename:'../us-1.0.js'});
kmw.addKeyboards({id:'lao_2008_basic',name:'Lao',languages:{id:'lo',name:'Lao'}, filename:'../lao_2008_basic.js'});
kmw.addKeyboards({id:'lao_2008_basic',name:'Lao',languages:{id:'lo',name:'Lao'}, filename:'../lao_2008_basic-1.2.js'});
var pageRef = (window.location.protocol == 'file:')
? window.location.href.substr(0, window.location.href.lastIndexOf('/')+1)