Sky Event Model
import 'dart:collection';
import 'dart:async';
class ExceptionAndStackTrace<T> {
const ExceptionAndStackTrace(this.exception, this.stackTrace);
final T exception;
final StackTrace stackTrace;
}
class ExceptionListException<T> extends IterableMixin<ExceptionAndStackTrace<T>> implements Exception {
List<ExceptionAndStackTrace<T>> _exceptions;
void add(T exception, [StackTrace stackTrace = null]) {
if (_exceptions == null)
_exceptions = new List<ExceptionAndStackTrace<T>>();
_exceptions.add(new ExceptionAndStackTrace<T>(exception, stackTrace));
}
int get length => _exceptions == null ? 0 : _exceptions.length;
Iterator<ExceptionAndStackTrace<T>> get iterator => _exceptions.iterator;
}
typedef bool Filter<T>(T t);
typedef void Handler<T>(T t);
class DispatcherController<T> {
DispatcherController() : dispatcher = new Dispatcher<T>();
final Dispatcher<T> dispatcher;
void add(T data) => dispatcher._add(data);
}
class Dispatcher<T> {
List<Pair<Handler, ZoneUnaryCallback>> _listeners;
void listen(Handler<T> handler) {
// you should not throw out of this handler
if (_listeners == null)
_listeners = new List<Pair<Handler, ZoneUnaryCallback>>();
_listeners.add(new Pair<Handler, ZoneUnaryCallback>(handler, Zone.current.bindUnaryCallback(handler)));
}
bool unlisten(Handler<T> handler) {
if (_listeners == null)
return false;
var target = _listeners.lastWhere((v) => v.a == handler, orElse: () => null);
if (target == null)
return false;
_listeners.removeAt(_listeners.lastIndexOf(target));
return true;
}
void _add(T data) {
if (_listeners == null)
return;
ExceptionListException exceptions = new ExceptionListException();
// we make a copy of the list here so that the listeners can
// mutate our list without worry
_listeners.toList().forEach((Pair<Handler, ZoneUnaryCallback> item) {
try {
item.b(data);
} catch (exception, stackTrace) {
exceptions.add(exception, stackTrace);
}
});
if (exceptions.length > 0)
throw exceptions;
}
Dispatcher<T> where(Filter<T> filter) => new WhereDispatcher<T>(this, filter);
Dispatcher<T> until(Filter<T> filter) {
var subdispatcher = new Dispatcher<T>();
Handler handler;
handler = (T data) {
if (filter(data))
unlisten(handler);
else
subdispatcher._add(data);
};
listen(handler);
return subdispatcher;
}
Future<T> firstWhere(Filter<T> filter) {
Completer completer = new Completer();
Handler handler;
handler = (T data) {
if (filter(data)) {
completer.complete(data);
unlisten(handler);
}
};
listen(handler);
return completer.future;
}
}
class WhereDispatcher<T> extends Dispatcher {
WhereDispatcher(this.parent, this.filter) : super();
Dispatcher parent;
Filter filter;
void listen(Handler<T> handler) {
if (_listeners == null || _listeners.length == 0)
parent.listen(_handler);
super.listen(handler);
}
bool unlisten(Handler<T> handler) {
var result = super.unlisten(handler);
if (result && _listeners.length == 0)
parent.unlisten(_handler);
return result;
}
void _handler(T data) {
if (filter(data))
_add(data);
}
}
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?
// e.g. sent from inside a shadow tree, when exiting the shadow tree, focus event should:
// - disappear if we're moving from one to another element
// - be targetted if it's going to another node in a different scope
}
class EventTarget {
EventTarget() : _eventsController = new DispatcherController<Event>();
Dispatcher get events => _eventsController.dispatcher;
EventTarget get parentNode;
List<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(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(Event event) {
event._currentTarget = this;
_eventsController.add(event);
}
}