diff --git a/web/source/dom/contentEditable.ts b/web/source/dom/contentEditable.ts index 3eaa53af17..825a7bfddc 100644 --- a/web/source/dom/contentEditable.ts +++ b/web/source/dom/contentEditable.ts @@ -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; diff --git a/web/source/dom/designIFrame.ts b/web/source/dom/designIFrame.ts index 4f2c0219fe..ec57de989c 100644 --- a/web/source/dom/designIFrame.ts +++ b/web/source/dom/designIFrame.ts @@ -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; diff --git a/web/source/dom/input.ts b/web/source/dom/input.ts index e10c6719d5..92189d527b 100644 --- a/web/source/dom/input.ts +++ b/web/source/dom/input.ts @@ -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); + } + } + } } } \ No newline at end of file diff --git a/web/source/dom/textarea.ts b/web/source/dom/textarea.ts index db0d21cd8b..c9fb742c52 100644 --- a/web/source/dom/textarea.ts +++ b/web/source/dom/textarea.ts @@ -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'); + } } } \ No newline at end of file diff --git a/web/source/dom/touchAlias.ts b/web/source/dom/touchAlias.ts index 954c521055..837acbed6f 100644 --- a/web/source/dom/touchAlias.ts +++ b/web/source/dom/touchAlias.ts @@ -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()); } diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 190d601b0f..8c36238bbb 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -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], diff --git a/web/source/text/outputTarget.ts b/web/source/text/outputTarget.ts index f3bfa73490..9f66ac1413 100644 --- a/web/source/text/outputTarget.ts +++ b/web/source/text/outputTarget.ts @@ -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; } diff --git a/web/source/text/processor.ts b/web/source/text/processor.ts index fd7faa645d..51ff669060 100644 --- a/web/source/text/processor.ts +++ b/web/source/text/processor.ts @@ -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 = Lelem; - } else if(typeof(Lelem.base) != 'undefined' && dom.Utils.instanceof(Lelem.base, "HTMLInputElement")) { - inputEle = 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. diff --git a/web/testing/prediction-ui/index.html b/web/testing/prediction-ui/index.html index a0b40d3ea0..7bc242f02d 100644 --- a/web/testing/prediction-ui/index.html +++ b/web/testing/prediction-ui/index.html @@ -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)