Saturday, August 10, 2013

Hacking Cross Browser Support of Keyboard Events in Dart


Last night I was able to significantly improve the keyboard shortcut handling in the Dart version of the ICE Code Editor. Instead of checking for the control key (and possibly other modifier keys), I now have nice predicate methods:
    KeyboardEventStreamX.onKeyDown(document).listen((e) {
      if (e.isCtrl('N')) {
        new NewProjectDialog(this).open();
        e.preventDefault();
      }
      if (e.isCtrlShift('H')) {
        toggleCode();
        e.preventDefault();
      }
    });
This masks some heartache in Dart's current keyboard event generating and handling. The only way to generate a keyboard event in Dart is to dispatch a KeyboardEvent object with the keyIdentifier property set:
typeCtrl(char) {
  document.activeElement.dispatchEvent(
    new KeyboardEvent(
      'keydown',
      keyIdentifier: keyIdentifierFor(char),
      ctrlKey: true
    )
  );
}
The keyIdentifier property is a strange beast. It is a string containing the unicode encoded bytes of the key pressed. Not the unicode bytes, it is a string: 'U+004E'. It is not an easy thing to generate, so the keyIdentifier() function is doing it for me:
String keyIdentifierFor(char) {
  if (char.codeUnits.length != 1) fail("Don't know how to type “$char”");
  // Keys are uppercase (see Event.keyCode)
  var key = char.toUpperCase();
  return 'U+00' + key.codeUnits.first.toRadixString(16).toUpperCase();
}
Ick.

With the above code and the KeyboardEventStreamX class from last night, I am able to generate keyboard events in test that exactly mimic those generated in Chrome. I even have some of those nice predicate methods for use with the keyboard short cuts.

What I don't have is a solution that will work in Firefox or Internet Explorer. All of this keyboard handling relies on that keyidentifier property which does not exist in Firefox or Internet Explorer—even when my Dart code is compiled to JavaScript with dart2js. No doubt this will be resolved in the near future. But until that time, I would very much like to find a way to handle keyboard events in Dart in a cross-browser, testable way.

For now, testing is going to have to rely on the keyIdentifier property since there is simply no other way to generate keyboard events in Dart at this time. The handling, in KeyboardEventStreamX, will have to mostly rely on the same keyIdentifier property, since that is the only testable approach that I can take. But nothing prevents me from falling back to properties that exist in other browsers, like keyCode.

In the KeyboardEventX, which is produced by KeyboardEventStreamX stream and mimics the built-in (but buggy) KeyboardEvent class, I override the keyCode getter. In there, it first checks keyIdentifier and falls back to the keyCode of the raw event:
class KeyEventX extends KeyEvent {
  // ...
  int get keyCode {
    if (keyIdentifier != null) return int.parse(keyIdentifier.replaceFirst('U+', '0x'));
    if (_parent.keyCode != 0) return _parent.keyCode;
    return null;
  }
}
The first line works with Chrome and Dart-generated events. The second line works with all other browsers. And that is all that I really need.

With a reliable keyCode, I can define a reliable key getter:
class KeyEventX extends KeyEvent {
  // ...
  String get key {
    if (keyCode == null) return 'Unidentified';
    return new String.fromCharCode(keyCode);
  }

  int get keyCode {
    if (keyIdentifier != null) return int.parse(keyIdentifier.replaceFirst('U+', '0x'));
    if (_parent.keyCode != 0) return _parent.keyCode;
    return null;
  }
}
Boom. That's the golden egg promised (but not implemented yet) in JavaScript's KeyboardEvent. Even in jQuery, we have to get the integer value for the key pressed. Best of all, I think that will even work with pressing “special” keys like Escape, Enter, etc.

I end by replacing last night's isCtrl(char) getter with a simpler one based on my new key getter:
class KeyEventX extends KeyEvent {
  // ...
  bool isCtrl(String char) => ctrlKey && isKey(char);
  bool isKey(String char) => char == key;

  String get key {
    if (keyCode == null) return 'Unidentified';
    return new String.fromCharCode(keyCode);
  }
  int get keyCode {
    if (keyIdentifier != null) return int.parse(keyIdentifier.replaceFirst('U+', '0x'));
    if (_parent.keyCode != 0) return _parent.keyCode;
    return null;
  }
}
It has been a long, strange journey with keyboard input in Dart for me. It is only going to get better in core Dart and, in all honesty, it is already in pretty decent shape. My hang up has always been the ability to test it reliably. Between last night's work and tonight, I believe that I have eliminated my objections.

Tomorrow, I am going to test the non-ascii key events and get started on extracting this out into a library. Yay!


Day #839

No comments:

Post a Comment