Sky Event Model

SKY MODULE
<!-- part of sky:core -->

<script>
abstract class Event<ReturnType> {
  Event() { init(); }
  void init() { }

  bool get bubbles;

  EventTarget _target;
  EventTarget get target => _target;

  EventTarget _currentTarget;
  EventTarget get currentTarget => _currentTarget;

  bool handled; // precise semantics depend on the event type, but in general, set this when you set result
  ReturnType result;

  bool resultIsCompatible(dynamic candidate) => candidate is ReturnType;

  // TODO(ianh): abstract API for doing things at shadow tree boundaries 
  // TODO(ianh): do events get blocked at scope boundaries, e.g. focus events when both sides are in the scope?
  // TODO(ianh): do events get retargetted, e.g. focus when leaving a custom element?
}

class EventTarget {
  EventTarget() : _eventsController = new DispatcherController<@nonnull Event>();

  Dispatcher get events => _eventsController.dispatcher;
  EventTarget parentNode;

  List<@nonnull EventTarget> getEventDispatchChain() {
    if (this.parentNode == null) {
      return [this];
    } else {
      var result = this.parentNode.getEventDispatchChain();
      result.insert(0, this);
      return result;
    }
  }

  final DispatcherController _eventsController;

  dynamic dispatchEvent(@nonnull Event event, { dynamic defaultResult: null }) { // O(N*M) where N is the length of the chain and M is the average number of listeners per link in the chain
    // note: this will throw an ExceptionListException<ExceptionListException> if any of the listeners threw
    assert(event != null); // event must be non-null
    event.handled = false;
    assert(event.resultIsCompatible(defaultResult));
    event.result = defaultResult;
    event._target = this;
    var chain;
    if (event.bubbles)
      chain = this.getEventDispatchChain();
    else
      chain = [this];
    var exceptions = new ExceptionListException<ExceptionListException>();
    for (var link in chain) {
      try {
        link._dispatchEventLocally(event);
      } on ExceptionListException catch (e) {
        exceptions.add(e);
      }
    }
    if (exceptions.length > 0)
      throw exceptions;
    return event.result;
  }

  void _dispatchEventLocally(@nonnull Event event) {
    event._currentTarget = this;
    _eventsController.add(event);
  }
}
</script>