diff --git a/web/history.md b/web/history.md index ccc0866ff7..8e4fddff2a 100644 --- a/web/history.md +++ b/web/history.md @@ -26,6 +26,7 @@ * Added automated testing for KeymanWeb builds. (#350) * Fixed bugs in the handling of deadkeys. (#281) * Change from ISO 639-3 language codes to BCP-47 language codes +* Now generates 'change' and 'input' events from keyboard and OSK input (#42) (#571) ## 2017-07-10 2.0.473 stable * 2.0 stable release build. diff --git a/web/source/build_recorder.sh b/web/source/build_recorder.sh old mode 100644 new mode 100755 diff --git a/web/source/kmwcallback.ts b/web/source/kmwcallback.ts index c2097c13ee..914b773180 100644 --- a/web/source/kmwcallback.ts +++ b/web/source/kmwcallback.ts @@ -824,6 +824,11 @@ namespace com.keyman { if(dn >= 0) { this._DeadkeyAdjustPos(this._SelPos(Pelem), -dn + s._kmwLength()); // I3318,I3319 } + + if((dn >= 0 || s) && Pelem == DOMEventHandlers.states.activeElement) { + // Record that we've made an edit. + DOMEventHandlers.states.changed = true; + } return; } @@ -933,7 +938,13 @@ namespace com.keyman { if(typeof(this.keymanweb.refreshElementContent) == 'function') { this.keymanweb.refreshElementContent(Pelem); } + + if((dn >= 0 || s) && Pelem == DOMEventHandlers.states.activeElement) { + // Record that we've made an edit. + DOMEventHandlers.states.changed = true; + } } + /** * Function deadkeyOutput KDO @@ -1186,6 +1197,32 @@ namespace com.keyman { } // I3318 - deadkey changes END + doInputEvent(_target: HTMLElement|Document) { + var event: Event; + // TypeScript doesn't yet recognize InputEvent as a type! + if(typeof window['InputEvent'] == 'function') { + event = new window['InputEvent']('input', {"bubbles": true, "cancelable": false}); + } // No else - there is no supported version in some browsers. + + // Ensure that touch-aliased elements fire as if from the aliased element. + if(_target['base'] && _target['base']['kmw_ip']) { + _target = _target['base']; + } + + if(_target && event) { + _target.dispatchEvent(event); + } + } + + defaultBackspace(Pelem?: HTMLElement|Document) { + if(!Pelem) { + Pelem = this.keymanweb.domManager.getLastActiveElement(); + } + + this.output(1, Pelem, ""); + this.doInputEvent(Pelem); + } + /** * Function processKeystroke * Scope Private @@ -1206,7 +1243,13 @@ namespace com.keyman { this.keymanweb.util.activeDevice = device; // Calls the start-group of the active keyboard. - return this.keymanweb.keyboardManager.activeKeyboard['gs'](element, keystroke); + var matched = this.keymanweb.keyboardManager.activeKeyboard['gs'](element, keystroke); + + if(matched) { + this.doInputEvent(element); + } + + return matched; } /** diff --git a/web/source/kmwdom.ts b/web/source/kmwdom.ts index 3e76f5c06f..88b2085fd2 100644 --- a/web/source/kmwdom.ts +++ b/web/source/kmwdom.ts @@ -60,9 +60,13 @@ namespace com.keyman { if(this.enablementObserver) { this.enablementObserver.disconnect(); } - if(this.attachmentObserver.disconnect) { + if(this.attachmentObserver) { this.attachmentObserver.disconnect(); } + + for(let input of this.inputList) { + this.disableInputElement(input); + } } /** diff --git a/web/source/kmwdomevents.ts b/web/source/kmwdomevents.ts index 31a8f156e8..d9410cd1a1 100644 --- a/web/source/kmwdomevents.ts +++ b/web/source/kmwdomevents.ts @@ -21,6 +21,8 @@ namespace com.keyman { focusing: boolean; focusTimer: number; + changed: boolean; // Tracks if the element has been edited since gaining focus. + /* ----------------------- Static event-related methods ------------------------ */ setFocusTimer(): void { @@ -324,6 +326,8 @@ namespace com.keyman { this.keyman.osk._Hide(false); } + this.doChangeEvent(Ltarg); + return true; }.bind(this); @@ -700,14 +704,14 @@ namespace com.keyman { // Support backspace in simulated input DIV from physical keyboard where not matched in rule I3363 (Build 301) if(Levent.Lcode == 8 && !LeventMatched && Levent.Ltarg.className != null && Levent.Ltarg.className.indexOf('keymanweb-input') >= 0) { - kbdInterface.output(1, DOMEventHandlers.states.lastActiveElement, ""); + this.keyman.interface.defaultBackspace(); } } else { // Mnemonic layout if(Levent.Lcode == 8) { // I1595 - Backspace for mnemonic DOMEventHandlers.states._KeyPressToSwallow = 1; if(!kbdInterface.processKeystroke(util.physicalDevice,Levent.Ltarg,Levent)) { - kbdInterface.output(1, DOMEventHandlers.states.lastActiveElement, ""); // I3363 (Build 301) + this.keyman.interface.defaultBackspace(); // I3363 (Build 301) } return false; //added 16/3/13 to fix double backspace on mnemonic layouts on desktop } @@ -758,9 +762,30 @@ namespace com.keyman { return false; } } + return true; }.bind(this); + doChangeEvent(_target: HTMLElement|Document) { + if(DOMEventHandlers.states.changed) { + var event: Event; + if(typeof Event == 'function') { + event = new Event('change', {"bubbles": true, "cancelable": false}); + } else { // IE path + event = document.createEvent("HTMLEvents"); + event.initEvent('change', true, false); + } + + // Ensure that touch-aliased elements fire as if from the aliased element. + if(_target['base'] && _target['base']['kmw_ip']) { + _target = _target['base']; + } + _target.dispatchEvent(event); + } + + DOMEventHandlers.states.changed = false; + } + /** * Function _KeyPress * Scope Private @@ -1356,8 +1381,10 @@ namespace com.keyman { // This works OK for iOS, but may need something else for other platforms if(('relatedTarget' in e) && e.relatedTarget) { var elem: HTMLElement = e.relatedTarget as HTMLElement; + this.doChangeEvent(elem); if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) { - this.cancelInput(); return; + this.cancelInput(); + return; } } diff --git a/web/source/kmwkeyboards.ts b/web/source/kmwkeyboards.ts index 533e78379a..660a74843c 100644 --- a/web/source/kmwkeyboards.ts +++ b/web/source/kmwkeyboards.ts @@ -233,10 +233,12 @@ namespace com.keyman { **/ mergeStub(kp: any, lp: any, options) { var sp: KeyboardStub = this.findStub(kp['id'], lp['id']); + var isNew: boolean = false; if(sp == null) { sp= new KeyboardStub(kp['id'], lp['id']); this.keyboardStubs.push(sp); + isNew = true; } // Accept region as number (from Cloud server), code, or name @@ -316,6 +318,12 @@ namespace com.keyman { // Update the UI this.doKeyboardRegistered(sp['KI'],sp['KL'],sp['KN'],sp['KLC'],sp['KP']); + + // If we have no activeStub because there were no stubs, set the new keyboard as active. + // Do not trigger on merges. + if(!this.activeStub && isNew && this.keyboardStubs.length == 1) { + this.setActiveKeyboard(sp['KI'], sp['KLC']); + } } /** @@ -356,7 +364,12 @@ namespace com.keyman { setActiveKeyboard(PInternalName: string, PLgCode: string) { //TODO: This does not make sense: the callbacks should be in _SetActiveKeyboard, not here, // since this is always called FROM the UI, which should not need notification. - // If UI callbacks are needed at all, they should be within _SetActiveKeyboard + // If UI callbacks are needed at all, they should be within _SetActiveKeyboard + + if(PInternalName && PInternalName.indexOf("Keyboard_") != 0) { + PInternalName = "Keyboard_" + PInternalName; + } + this.doBeforeKeyboardChange(PInternalName,PLgCode); this._SetActiveKeyboard(PInternalName,PLgCode,true); if(this.keymanweb.domManager.getLastActiveElement() != null) { @@ -369,7 +382,7 @@ namespace com.keyman { // } this.doKeyboardChange(PInternalName, PLgCode); } - + /** * Change active keyboard to keyboard selected by (internal) name and language code * @@ -628,14 +641,15 @@ namespace com.keyman { // Prepare and show the OSK for this keyboard osk._Load(); - - // Remove the wait message, if defined - if(!manager.keymanweb.isEmbedded) { - util.wait(false); - } - } // A handler portion for cases where the new