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 167fe21441..1f22249161 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;
+ }
}
/**
@@ -1185,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, Pelem, "");
+ this.doInputEvent(Pelem);
+ }
+
/**
* Function processKeystroke
* Scope Private
@@ -1205,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/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 00b9fab6a9..a59e6111dc 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
@@ -699,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
}
@@ -1353,8 +1377,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/source/kmwosk.ts b/web/source/kmwosk.ts
index c37c44d71e..d51ef9cbfa 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(), "");
+ kbdInterface.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(), "");
+ // kbdInterface.defaultBackspace();
// }
}
}
diff --git a/web/unit_tests/cases/events.js b/web/unit_tests/cases/events.js
new file mode 100644
index 0000000000..b92edf40d9
--- /dev/null
+++ b/web/unit_tests/cases/events.js
@@ -0,0 +1,166 @@
+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", 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);
+
+ });
+
+ 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() {
+ ele.onchange = null;
+ 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() {
+ ele.onchange = null;
+ 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('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();
+ return;
+ }
+
+ 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;
+
+ 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();
+ return;
+ }
+
+ 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;
+
+ 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
diff --git a/web/unit_tests/fixtures/eventTestConfig.html b/web/unit_tests/fixtures/eventTestConfig.html
new file mode 100644
index 0000000000..b3a77bac09
--- /dev/null
+++ b/web/unit_tests/fixtures/eventTestConfig.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file