From 038c344a4f31d2cac10dbdba9a23fc1f100126e0 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 2 Mar 2018 12:04:13 +0700 Subject: [PATCH 1/9] KMW now generates onChange events. Still need onInput. --- web/source/kmwcallback.ts | 10 +++ web/source/kmwdom.ts | 4 + web/source/kmwdomevents.ts | 28 +++++- web/unit_tests/cases/events.js | 91 ++++++++++++++++++++ web/unit_tests/fixtures/eventTestConfig.html | 3 + 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 web/unit_tests/cases/events.js create mode 100644 web/unit_tests/fixtures/eventTestConfig.html diff --git a/web/source/kmwcallback.ts b/web/source/kmwcallback.ts index 167fe21441..c1e89eddfc 100644 --- a/web/source/kmwcallback.ts +++ b/web/source/kmwcallback.ts @@ -823,6 +823,11 @@ class KeyboardInterface { 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; } @@ -932,6 +937,11 @@ class KeyboardInterface { 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; + } } /** diff --git a/web/source/kmwdom.ts b/web/source/kmwdom.ts index 8695aa07b4..0921e2c586 100644 --- a/web/source/kmwdom.ts +++ b/web/source/kmwdom.ts @@ -62,6 +62,10 @@ class DOMManager { if(this.attachmentObserver.disconnect) { 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 09f6f6b1d7..130cbb8a60 100644 --- a/web/source/kmwdomevents.ts +++ b/web/source/kmwdomevents.ts @@ -20,6 +20,8 @@ focusing: boolean; focusTimer: number; + changed: boolean; // Tracks if the element has been edited since gaining focus. + /* ----------------------- Static event-related methods ------------------------ */ setFocusTimer(): void { @@ -323,9 +325,31 @@ class DOMEventHandlers { this.keyman.osk._Hide(false); } + this.doChangeEvent(Ltarg); + 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 doControlBlurred * Scope Private @@ -1369,8 +1393,10 @@ class DOMTouchHandlers extends DOMEventHandlers { // 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/unit_tests/cases/events.js b/web/unit_tests/cases/events.js new file mode 100644 index 0000000000..1d26321a67 --- /dev/null +++ b/web/unit_tests/cases/events.js @@ -0,0 +1,91 @@ +var assert = chai.assert; + +describe('Event Management', function() { + + before(function(done) { + this.timeout(20000); + fixture.setBase('unit_tests/fixtures'); + fixture.load("eventTestConfig.html"); + + setupKMW(null, function() { + // We use this keyboard since we only need minimal input functionality for these tests. + // Smaller is better when dealing with net latency. + loadKeyboardFromJSON("/keyboards/test_simple_deadkeys.json", done, 10000); + }, 10000); + + }); + + after(function() { + teardownKMW(); + }); + + it('Keystroke-based onChange event generation', function(done) { + var simple_A = {"type":"key","key":"a","code":"KeyA","keyCode":65,"modifierSet":0,"location":0}; + var event = new KMWRecorder.PhysicalInputEvent(simple_A); + + var ele = document.getElementById("input"); + var aliasing = false; + + ele.onchange = function() { + done(); + } + + if(ele['kmw_ip']) { + ele = ele['kmw_ip']; + aliasing = true; + } + + // A bit of a force-hack to ensure the element is seen as active for the tests. + window['DOMEventHandlers'].states.lastActiveElement = ele; + window['DOMEventHandlers'].states.activeElement = ele; + + event.simulateEventOn(ele); + + var focusEvent; + + if(typeof FocusEvent == 'function') { + focusEvent = new FocusEvent('blur', {relatedTarget: ele}); + } else { + focusEvent = document.createEvent("FocusEvent"); + focusEvent.initFocusEvent("blur", true, false, ele.ownerDocument.defaultView, 0, ele); + } + + if(focusEvent) + ele.dispatchEvent(focusEvent); + }); + + it('OSK-based onChange event generation', function(done) { + var simple_A = {"type":"osk","keyID":"default-K_A"}; + var event = new KMWRecorder.OSKInputEvent(simple_A); + + var ele = document.getElementById("input"); + var aliasing = false; + + ele.onchange = function() { + done(); + } + + if(ele['kmw_ip']) { + ele = ele['kmw_ip']; + aliasing = true; + } + + // A bit of a force-hack to ensure the element is seen as active for the tests. + window['DOMEventHandlers'].states.lastActiveElement = ele; + window['DOMEventHandlers'].states.activeElement = ele; + + event.simulateEventOn(ele); + + var focusEvent; + + if(typeof FocusEvent == 'function') { + focusEvent = new FocusEvent('blur', {relatedTarget: ele}); + } else { + focusEvent = document.createEvent("FocusEvent"); + focusEvent.initFocusEvent("blur", true, false, ele.ownerDocument.defaultView, 0, ele); + } + + if(focusEvent) + ele.dispatchEvent(focusEvent); + }); +}); \ No newline at end of file diff --git a/web/unit_tests/fixtures/eventTestConfig.html b/web/unit_tests/fixtures/eventTestConfig.html new file mode 100644 index 0000000000..e528a36866 --- /dev/null +++ b/web/unit_tests/fixtures/eventTestConfig.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 8d74c8cc9531c9a472bf080f9bf852d0500a137c Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 2 Mar 2018 13:47:03 +0700 Subject: [PATCH 2/9] Implements basic support + testing for the 'input' event. --- web/source/kmwcallback.ts | 34 +++++++++++++++- web/source/kmwdomevents.ts | 8 ++-- web/source/kmwosk.ts | 4 +- web/unit_tests/cases/events.js | 72 ++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/web/source/kmwcallback.ts b/web/source/kmwcallback.ts index c1e89eddfc..774ae15c6d 100644 --- a/web/source/kmwcallback.ts +++ b/web/source/kmwcallback.ts @@ -1195,6 +1195,32 @@ class KeyboardInterface { } // 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, this.keymanweb.domManager.getLastActiveElement(), ""); + this.doInputEvent(Pelem); + } + /** * Function processKeystroke * Scope Private @@ -1215,7 +1241,13 @@ class KeyboardInterface { 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/kmwdomevents.ts b/web/source/kmwdomevents.ts index 130cbb8a60..ee0642061a 100644 --- a/web/source/kmwdomevents.ts +++ b/web/source/kmwdomevents.ts @@ -334,10 +334,10 @@ class DOMEventHandlers { if(DOMEventHandlers.states.changed) { var event: Event; if(typeof Event == 'function') { - event = new Event("change", {"bubbles": true, "cancelable": false}); + event = new Event('change', {"bubbles": true, "cancelable": false}); } else { // IE path event = document.createEvent("HTMLEvents"); - event.initEvent("change", true, false); + event.initEvent('change', true, false); } // Ensure that touch-aliased elements fire as if from the aliased element. @@ -723,14 +723,14 @@ class DOMEventHandlers { // 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(); } return false; //added 16/3/13 to fix double backspace on mnemonic layouts on desktop } diff --git a/web/source/kmwosk.ts b/web/source/kmwosk.ts index c37c44d71e..feb35eacd4 100644 --- a/web/source/kmwosk.ts +++ b/web/source/kmwosk.ts @@ -915,7 +915,7 @@ if(!window['keyman']['initialized']) { switch(code) { case osk.keyCodes['K_BKSP']: //Only desktop UI, not touch devices. TODO: add repeat while mouse down for desktop UI - kbdInterface.output(1, keymanweb.domManager.getLastActiveElement(), ""); + this.keyman.interface.defaultBackspace(); break; case osk.keyCodes['K_TAB']: keymanweb.domManager.moveToNext(keyShiftState); @@ -973,7 +973,7 @@ if(!window['keyman']['initialized']) { // // Failed to move right - there's nothing to delete. // break; // } - // kbdInterface.output(1, keymanweb.domManager.getLastActiveElement(), ""); + // this.keyman.interface.defaultBackspace(); // } } } diff --git a/web/unit_tests/cases/events.js b/web/unit_tests/cases/events.js index 1d26321a67..5c28680101 100644 --- a/web/unit_tests/cases/events.js +++ b/web/unit_tests/cases/events.js @@ -27,6 +27,7 @@ describe('Event Management', function() { var aliasing = false; ele.onchange = function() { + ele.onchange = null; done(); } @@ -62,6 +63,7 @@ describe('Event Management', function() { var aliasing = false; ele.onchange = function() { + ele.onchange = null; done(); } @@ -88,4 +90,74 @@ describe('Event Management', function() { if(focusEvent) ele.dispatchEvent(focusEvent); }); + + it('Keystroke-based onInput event generation', function(done) { + // Not all browsers support InputEvent. Bypass the test for these. + if(typeof InputEvent != 'function') { + console.log("InputEvent not supported."); + done(); + } + + var simple_A = {"type":"key","key":"a","code":"KeyA","keyCode":65,"modifierSet":0,"location":0}; + var event = new KMWRecorder.PhysicalInputEvent(simple_A); + + var ele = document.getElementById("input"); + var aliasing = false; + + var counterObj = {i:0}; + var fin = 3; + + if(typeof InputEvent == 'function') { + ele.addEventListener("input", function() { + counterObj.i++; + if(counterObj.i == fin) { + done(); + } + }); + } + + if(ele['kmw_ip']) { + ele = ele['kmw_ip']; + aliasing = true; + } + + event.simulateEventOn(ele); + event.simulateEventOn(ele); + event.simulateEventOn(ele); + }); + + it('OSK-based onInput event generation', function(done) { + // Not all browsers support InputEvent. Bypass the test for these. + if(typeof InputEvent != 'function') { + console.log("InputEvent not supported."); + done(); + } + + var simple_A = {"type":"osk","keyID":"default-K_A"}; + var event = new KMWRecorder.OSKInputEvent(simple_A); + + var ele = document.getElementById("input"); + var aliasing = false; + + var counterObj = {i:0}; + var fin = 3; + + if(typeof InputEvent == 'function') { + ele.addEventListener("input", function() { + counterObj.i++; + if(counterObj.i == fin) { + done(); + } + }); + } + + if(ele['kmw_ip']) { + ele = ele['kmw_ip']; + aliasing = true; + } + + event.simulateEventOn(ele); + event.simulateEventOn(ele); + event.simulateEventOn(ele); + }); }); \ No newline at end of file From 98b092351fa1196006baccd29fcd465800488baf Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 26 Feb 2018 15:34:54 +0700 Subject: [PATCH 3/9] Prevents odd Safari test bug. --- web/source/build_recorder.sh | 0 web/unit_tests/cases/events.js | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) mode change 100644 => 100755 web/source/build_recorder.sh diff --git a/web/source/build_recorder.sh b/web/source/build_recorder.sh old mode 100644 new mode 100755 diff --git a/web/unit_tests/cases/events.js b/web/unit_tests/cases/events.js index 5c28680101..86a611bd4c 100644 --- a/web/unit_tests/cases/events.js +++ b/web/unit_tests/cases/events.js @@ -10,7 +10,12 @@ describe('Event Management', function() { setupKMW(null, function() { // We use this keyboard since we only need minimal input functionality for these tests. // Smaller is better when dealing with net latency. - loadKeyboardFromJSON("/keyboards/test_simple_deadkeys.json", done, 10000); + loadKeyboardFromJSON("/keyboards/test_simple_deadkeys.json", function() { + // Interestingly, when auto-testing there's a Safari bug that prevents + // this from being preserved after the first forced blur command below. + keyman.globalKeyboard = "Keyboard_test_simple_deadkeys"; + done(); + }, 10000); }, 10000); }); From 89de17cb7d3514ae53760aca9f66eda8926d3f49 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 1 Mar 2018 11:48:25 +0700 Subject: [PATCH 4/9] Reimplements the auto-selection of the first keyboard stub. --- web/source/kmwkeyboards.ts | 25 ++++++++++++++++++++----- web/unit_tests/cases/engine.js | 5 +++-- web/unit_tests/test_utils.js | 6 ++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/web/source/kmwkeyboards.ts b/web/source/kmwkeyboards.ts index f4f528a358..20c5b0657c 100644 --- a/web/source/kmwkeyboards.ts +++ b/web/source/kmwkeyboards.ts @@ -232,10 +232,12 @@ class KeyboardManager { **/ 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 @@ -315,6 +317,12 @@ class KeyboardManager { // 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']); + } } /** @@ -627,12 +635,13 @@ class KeyboardManager { // 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