Tuesday, August 13, 2013

KeyIdentifier and Other Arcane Keyboard Event Properties


It's a good thing I don't mind making a complete fool of myself.

Two nights ago, I extracted code out into a library and patted myself on the back for being able to do so without breaking a single thing in the source application. Except I did break something. In the new ctrl_alt_foo package, I made one teeny weeny little change just before I committed. Naturally, I broke a whole bunch of stuff. It took some digging, but I eventually found a way to fix it, but... I am unsure if it works cross browser.

Part of the promise of Dart is that it will generate cross-browser JavaScript code that will work the same in all browsers. The eventual goal is to support at least the last two revisions of all major browsers. In the still early days of Dart, it is more like Internet Explorer 10 and several revisions of Firefox and Chrome.

Except for keyboard events. For keyboard events, developers are pretty much on our own, with minimal ability to generate keyboard events and little support beyond native properties for handling events. This will change in the near future, and all is not hopeless today. This is where I hope the ctrl_alt_foo package fits in.

At least I hope so.

I am not quite sure if the changes that I made last night, which support generating and handling non-ASCII keyboard events (like Enter, Escape, arrow keys) will work when compiled to JavaScript. There is, as they say, one way to find out... I compile the source application, the ICE Code Editor, into JavaScript and try it out.

It does not work.

In fact, it is broken worse in Chrome than it is in Firefox. In Chrome, it seems that ctrl_alt_foo package is trying to parse the string values representing the key named ("Control", "Up", "Down") instead of the key code that contains the bytes in the Unicode version of the key. In Firefox, everything seems to be working—except for detection of Enter keyboard events (oddly, Escape seems to work).

Since I can reproduce the Chrome error in Dartium, I am doing something wrong that I ought to be able to test. The relevant portion of the stack trace that I am seeing is:
Uncaught Error: FormatException: Down
Stack Trace:
#0      int._native_parse (dart:core-patch/integers_patch.dart:54:3)
#1      int.parse (dart:core-patch/integers_patch.dart:59:59)
#2      KeyEventX.keyCode (package:ctrl_alt_foo/key_event_x.dart:60:44)
#3      KeyEventX.key (package:ctrl_alt_foo/key_event_x.dart:52:9)
#4      KeyEventX.isDown (package:ctrl_alt_foo/key_event_x.dart:40:22)
#5      OpenDialog._handleArrowKeys. (package:ice_code_editor/full/open_dialog.dart:113:13)
When ctrl_alt_foo tries to parse the key code, it is seeing the string “Down” instead of the keycode.

This turns out to be yet another fascinating use of the keyIdentifier property. In the getter mehtod for the wrapper that I am using to smooth out the kinks of keyboard handling, I add a print statement to see what Chrome/Dartium is using for this property:
class KeyEventX extends KeyEvent {
  // ...
  String get keyIdentifier {
    print('[keyIdentifier] ${$dom_keyIdentifier}');
    return $dom_keyIdentifier;
  }
  // ...
}
What I see when I press “regular” keys like “A” is a string representing the Unicode value:
[keyIdentifier] U+0041
What I see when I press non-printable characters it the “key name”:
[keyIdentifier] Down
[keyIdentifier] Up
[keyIdentifier] Enter
Yay. Yet another condition case to handle.

The in-browser fix is fairly easy: I return null unless the $dom_keyIdentifier starts with the weird Unicode value:
class KeyEventX extends KeyEvent {
  // ...
  String get keyIdentifier {
    print('[keyIdentifier] ${$dom_keyIdentifier}');
    if ($dom_keyIdentifier.startsWith('U+')) return $dom_keyIdentifier;
    return null;
  }
  // ...
}
Without that, ctrl_alt_foo will fallback to using the keyCode value which is available in Chrome, Dartium, Firefox, and even Internet Explorer. So the fallback is a nice cross browser solution. Unfortunately for me, it is not a testable one.

To generate legitimate keyboard events in my unit test, I need to send those key names directly. This is not too hard—I change the return value of the lookup to return the key name instead of the Unicode. That still leaves with the need to handle this in my keyIdentifier. I cannot stick with falling back to keyCode since Dart gives me no way (at present) to populate that value programatically. My workaround is to lookup the Unicode string:
class KeyEventX extends KeyEvent {
  // ...
  String get keyIdentifier {
    if ($dom_keyIdentifier.startsWith('U+')) return $dom_keyIdentifier;
    if (KeyIdentifier.map.containsKey($dom_keyIdentifier)) return KeyIdentifier.map[$dom_keyIdentifier];
    return null;
  }
  // ...
}
I am not thrilled with that since I am now breaking keyIdentifier. Well, I am breaking it if there is a specification for it somewhere, which I do not believe there is. Still, it feels wrong-ish. Even so, it has the advantage of working with the rest of the code that is capable of parsing and handling those Unicode strings.

Hopefully, this is just a brief stopgap until Dart better supports keyboard events. I may add a TODO or something urging me to revisit if I am still using this same code 6 months from now.

Anyhow, that solves all of my cross-browser problems except for handling the Enter key. Mercifully, that turns out to be slightly less arcane. In Chrome, that key is being generated as newline character (0x0A). In Firefox, it is generated as a carriage return (0x0D). Thanks browsers!

The fix is to add a second condition to the isEnter predicate method:
bool get isEnter =>
    key == KeyIdentifier.keyFor('Enter') ||
    keyCode == 13;
With that, I have all of my keyboard event tests passing again, but still creating events in as close to a realistic way as possible—the addition tonight was sending the key name instead of the Unicode string. And everything seems to be working cross browser.

Except for Internet Explorer, of course. I will pick back up with that tomorrow.

I begin to understand why the Dart folks have not been in a rush to solve this problem. Also, I have a greater appreciation of what the jQuery folks have to go through. Browser coding: ick.


Day #842

No comments:

Post a Comment