Two days ago, I found that a known bug in Dart prevented me from using the
KeyEvent class. The KeyEvent class is supposed to normalize all kinds of browser craziness surrounding keyboard handling, so I was bummed that I was unable to use it.But nothing is preventing me from creating a subclass of
KeyEvent that does not suffer from the problem. So I do just that:
class KeyEventX extends KeyEvent {
  KeyboardEvent _parent;
  KeyEventX(KeyboardEvent parent): super(parent) {
    _parent = parent;
  }
  // Avoid bug in KeyEvent
  // https://code.google.com/p/dart/issues/detail?id=11139
  String get $dom_keyIdentifier => _parent.$dom_keyIdentifier;
  String get keyIdentifier => _parent.$dom_keyIdentifier;
  // ...
}The two getter methods for key identifier should avoid the “Unsupported operation: keyIdentifier is unsupported” error from the other day. Instead of accessing the keyIdentifier property of the soon-to-be-awesome-but-currently-buggy KeyEvent class, I obtain it from the plain-old KeyboardEvent that DOM programmers know and love.To make use of my new
KeyEventX class, I need a stream that produces instances of it. I rather like the simplicity of the KeyboardEventStream class, so I extend its functionality into a new KeyboardEventStreamX class:class KeyboardEventStreamX extends KeyboardEventStream {
  // ...
  static Stream<KeyEventX> onKeyDown(EventTarget target) {
    return Element.
      keyDownEvent.
      forTarget(target).
      map((e)=> new KeyEventX(e));
  }
}Given an event target (i.e. a DOM Element), I return a stream that maps regular KeyboardEvent instances into KeyEventX instances. It is pretty awesome that streams are iterable—meaning that I transform my event without even the minor complexity of a stream transformer.With my
KeyboardEventStreamX class that produces a stream of my non-buggy KeyEventX objects, I can now rewrite some of the keyboard controls in the ICE Code Editor as:    KeyboardEventStreamX.onKeyDown(document).listen((e) {
      if (!e.ctrlKey) return;
      switch(e.keyIdentifier) {
        case 'U+004E': // N
          new NewProjectDialog(this).open();
          e.preventDefault();
          break;
        case 'U+004F': // O
          new OpenDialog(this).open();
          e.preventDefault();
          break;
        // ...
      }
    });And that works! The dartanalyzer tool is happy with the type information, the copious tests that I have written are all still passing and everything seems OK when I smoke test the sample application.Now, that is nice and all, but since I am writing this to support keyboard shortcuts, I do not want to be checking for control keys, then checking for the character key. I want a predicate method on my
KeyEventX that tells me if it is a Ctrl+N or Ctrl+O.Thanks to last night's
keyIdentifierFor() helper method, I can do just that:class KeyEventX extends KeyEvent {
  // ...
  bool isCtrl(String char) {
    if (!ctrlKey) return false;
    if (keyIdentifier == keyIdentifierFor(char)) return true;
    return false;
  }
  String keyIdentifierFor(char) {
    if (char.codeUnits.length != 1) throw "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();
  }
}With isCtrl() in hand, I can rewrite my keyboard shortcut code as:    KeyboardEventStreamX.onKeyDown(document).listen((e) {
      if (e.isCtrl('N')) {
        new NewProjectDialog(this).open();
        e.preventDefault();
      }
      if (e.isCtrl('O')) {
        new OpenDialog(this).open();
        e.preventDefault();
      }
    });That is very nice.To be sure, there is some code duplication that I can clean-up in there. But I have made my keyboard handling code much cleaner and easier to read. Gone are the unicode strings. In are the the easy-to-follow
isCtrl('N') booleans. I like.And, unless I am wrong (though I am often wrong), I do believe that I am dangerously close to a legitimately testable approach to keyboard events in Dart. Last night, I was able to generate keyboard events in test that registered identically to those produced in Chrome. Tonight, I have significantly improved handling those events. All that is lacking is the ability to normalize those events across browsers. Dart will get there eventually. Until that happens, I have the perfect place get started: in
KeyEventX. I will pick up with that tomorrow.Day #838
No comments:
Post a Comment