mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-15 10:27:31 +00:00
Merge pull request #2849 from keymanapp/refactor/web/engine/outputTarget-extensions
refactor(web/engine): extended abstraction with OutputTarget
This commit is contained in:
commit
2c4b39e316
9 changed files with 131 additions and 54 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue