| Index: third_party/pkg/route_hierarchical/lib/client.dart
|
| diff --git a/third_party/pkg/route_hierarchical/lib/client.dart b/third_party/pkg/route_hierarchical/lib/client.dart
|
| index 82428a01d396a8025cc616d478621a3fcd1a1d96..ec4ef4fc19f4154073d017bd1f14c62cc2204e57 100644
|
| --- a/third_party/pkg/route_hierarchical/lib/client.dart
|
| +++ b/third_party/pkg/route_hierarchical/lib/client.dart
|
| @@ -17,7 +17,9 @@ import 'url_template.dart';
|
|
|
| final _logger = new Logger('route');
|
|
|
| -typedef RouteEventHandler(RouteEvent path);
|
| +typedef RoutePreEnterEventHandler(RoutePreEnterEvent path);
|
| +typedef RouteEnterEventHandler(RouteEnterEvent path);
|
| +typedef RouteLeaveEventHandler(RouteLeaveEvent path);
|
|
|
| /**
|
| * A helper Router handle that scopes all route event subsriptions to it's
|
| @@ -25,29 +27,41 @@ typedef RouteEventHandler(RouteEvent path);
|
| */
|
| class RouteHandle implements Route {
|
| Route _route;
|
| - final StreamController<RouteEvent> _onRouteController;
|
| - final StreamController<RouteEvent> _onLeaveController;
|
| - Stream<RouteEvent> get onRoute => _onRouteController.stream;
|
| - Stream<RouteEvent> get onLeave => _onLeaveController.stream;
|
| - StreamSubscription _onRouteSubscription;
|
| + final StreamController<RoutePreEnterEvent> _onPreEnterController;
|
| + final StreamController<RouteEnterEvent> _onEnterController;
|
| + final StreamController<RouteLeaveEvent> _onLeaveController;
|
| +
|
| + @deprecated
|
| + Stream<RouteEnterEvent> get onRoute => onEnter;
|
| + Stream<RoutePreEnterEvent> get onPreEnter => _onPreEnterController.stream;
|
| + Stream<RouteEnterEvent> get onEnter => _onEnterController.stream;
|
| + Stream<RouteLeaveEvent> get onLeave => _onLeaveController.stream;
|
| +
|
| + StreamSubscription _onPreEnterSubscription;
|
| + StreamSubscription _onEnterSubscription;
|
| StreamSubscription _onLeaveSubscription;
|
| List<RouteHandle> _childHandles = <RouteHandle>[];
|
|
|
| RouteHandle._new(Route this._route)
|
| - : _onRouteController =
|
| - new StreamController<RouteEvent>.broadcast(sync: true),
|
| + : _onEnterController =
|
| + new StreamController<RouteEnterEvent>.broadcast(sync: true),
|
| + _onPreEnterController =
|
| + new StreamController<RoutePreEnterEvent>.broadcast(sync: true),
|
| _onLeaveController =
|
| - new StreamController<RouteEvent>.broadcast(sync: true) {
|
| - _onRouteSubscription = _route.onRoute.listen(_onRouteController.add);
|
| + new StreamController<RouteLeaveEvent>.broadcast(sync: true) {
|
| + _onEnterSubscription = _route.onEnter.listen(_onEnterController.add);
|
| + _onPreEnterSubscription =
|
| + _route.onPreEnter.listen(_onPreEnterController.add);
|
| _onLeaveSubscription = _route.onLeave.listen(_onLeaveController.add);
|
| }
|
|
|
| /// discards this handle.
|
| void discard() {
|
| _logger.finest('discarding handle for $_route');
|
| - _onRouteSubscription.cancel();
|
| + _onPreEnterSubscription.cancel();
|
| + _onEnterSubscription.cancel();
|
| _onLeaveSubscription.cancel();
|
| - _onRouteController.close();
|
| + _onEnterController.close();
|
| _onLeaveController.close();
|
| _childHandles.forEach((RouteHandle c) => c.discard());
|
| _childHandles.clear();
|
| @@ -56,7 +70,8 @@ class RouteHandle implements Route {
|
|
|
| /// Not supported. Overridden to throw an error.
|
| void addRoute({String name, Pattern path, bool defaultRoute: false,
|
| - RouteEventHandler enter, RouteEventHandler leave, mount}) =>
|
| + RouteEnterEventHandler enter, RoutePreEnterEventHandler preEnter,
|
| + RouteLeaveEventHandler leave, mount}) =>
|
| throw new UnsupportedError('addRoute is not supported in handle');
|
|
|
| /// See [Route.getRoute]
|
| @@ -117,6 +132,12 @@ class RouteHandle implements Route {
|
| Route get parent => _route.parent;
|
| }
|
|
|
| +childRoute({String name, Pattern path, bool defaultRoute: false,
|
| + RouteEnterEventHandler enter, RoutePreEnterEventHandler preEnter,
|
| + RouteLeaveEventHandler leave, mount}) => (Route route) =>
|
| + route.addRoute(name: name, path: path, defaultRoute: defaultRoute,
|
| + enter: enter, preEnter: preEnter, leave: leave, mount: leave);
|
| +
|
| /**
|
| * Route is a node in the tree of routes. The edge leading to the route is
|
| * defined by path.
|
| @@ -125,24 +146,32 @@ class Route {
|
| final String name;
|
| final Map<String, Route> _routes = new LinkedHashMap<String, Route>();
|
| final UrlMatcher path;
|
| - final StreamController<RouteEvent> _onRouteController;
|
| - final StreamController<RouteEvent> _onLeaveController;
|
| + final StreamController<RouteEnterEvent> _onEnterController;
|
| + final StreamController<RoutePreEnterEvent> _onPreEnterController;
|
| + final StreamController<RouteLeaveEvent> _onLeaveController;
|
| final Route parent;
|
| Route _defaultRoute;
|
| Route _currentRoute;
|
| RouteEvent _lastEvent;
|
|
|
| - Stream<RouteEvent> get onRoute => _onRouteController.stream;
|
| + @deprecated
|
| + Stream<RouteEvent> get onRoute => onEnter;
|
| +
|
| + Stream<RouteEvent> get onPreEnter => _onPreEnterController.stream;
|
| Stream<RouteEvent> get onLeave => _onLeaveController.stream;
|
| + Stream<RouteEvent> get onEnter => _onEnterController.stream;
|
|
|
| Route._new({this.name, this.path, this.parent})
|
| - : _onRouteController =
|
| - new StreamController<RouteEvent>.broadcast(sync: true),
|
| + : _onEnterController =
|
| + new StreamController<RouteEnterEvent>.broadcast(sync: true),
|
| + _onPreEnterController =
|
| + new StreamController<RoutePreEnterEvent>.broadcast(sync: true),
|
| _onLeaveController =
|
| - new StreamController<RouteEvent>.broadcast(sync: true);
|
| + new StreamController<RouteLeaveEvent>.broadcast(sync: true);
|
|
|
| void addRoute({String name, Pattern path, bool defaultRoute: false,
|
| - RouteEventHandler enter, RouteEventHandler leave, mount}) {
|
| + RouteEnterEventHandler enter, RoutePreEnterEventHandler preEnter,
|
| + RouteLeaveEventHandler leave, mount}) {
|
| if (name == null) {
|
| throw new ArgumentError('name is required for all routes');
|
| }
|
| @@ -158,8 +187,11 @@ class Route {
|
| }
|
| var route = new Route._new(name: name, path: matcher, parent: this);
|
|
|
| + if (preEnter != null) {
|
| + route.onPreEnter.listen(preEnter);
|
| + }
|
| if (enter != null) {
|
| - route.onRoute.listen(enter);
|
| + route.onEnter.listen(enter);
|
| }
|
| if (leave != null) {
|
| route.onLeave.listen(leave);
|
| @@ -293,23 +325,49 @@ class Route {
|
| /**
|
| * Route enter or leave event.
|
| */
|
| -class RouteEvent {
|
| +abstract class RouteEvent {
|
| final String path;
|
| final Map parameters;
|
| final Route route;
|
| - var _allowLeaveFutures = <Future<bool>>[];
|
|
|
| RouteEvent(this.path, this.parameters, this.route);
|
| +}
|
| +
|
| +class RoutePreEnterEvent extends RouteEvent {
|
| +
|
| + var _allowEnterFutures = <Future<bool>>[];
|
| +
|
| + RoutePreEnterEvent(path, parameters, route) : super(path, parameters, route);
|
|
|
| /**
|
| - * Can be called on leave with the future which will complete with a boolean
|
| + * Can be called on enter with the future which will complete with a boolean
|
| + * value allowing (true) or disallowing (false) the current navigation.
|
| + */
|
| + void allowEnter(Future<bool> allow) {
|
| + _allowEnterFutures.add(allow);
|
| + }
|
| +}
|
| +
|
| +class RouteEnterEvent extends RouteEvent {
|
| +
|
| + RouteEnterEvent(path, parameters, route) : super(path, parameters, route);
|
| +}
|
| +
|
| +class RouteLeaveEvent extends RouteEvent {
|
| +
|
| + var _allowLeaveFutures = <Future<bool>>[];
|
| +
|
| + RouteLeaveEvent(path, parameters, route) : super(path, parameters, route);
|
| +
|
| + /**
|
| + * Can be called on enter with the future which will complete with a boolean
|
| * value allowing (true) or disallowing (false) the current navigation.
|
| */
|
| void allowLeave(Future<bool> allow) {
|
| _allowLeaveFutures.add(allow);
|
| }
|
|
|
| - RouteEvent _clone() => new RouteEvent(path, parameters, route);
|
| + RouteLeaveEvent _clone() => new RouteLeaveEvent(path, parameters, route);
|
| }
|
|
|
| /**
|
| @@ -379,48 +437,106 @@ class Router {
|
| * window, such as [listen].
|
| */
|
| Future<bool> route(String path, {Route startingFrom}) {
|
| - var future = _route(path, startingFrom: startingFrom);
|
| + var future = _route(path, startingFrom);
|
| _onRouteStart.add(new RouteStartEvent._new(path, future));
|
| return future;
|
| }
|
|
|
| - Future<bool> _route(String path, {Route startingFrom}) {
|
| - var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom);
|
| + Future<bool> _route(String path, Route startingFrom) {
|
| + var baseRoute = startingFrom == null ? root : _dehandle(startingFrom);
|
| _logger.finest('route $path $baseRoute');
|
| - Route matchedRoute;
|
| - List matchingRoutes = baseRoute._routes.values.where(
|
| - (r) => r.path.match(path) != null).toList();
|
| - if (!matchingRoutes.isEmpty) {
|
| - if (matchingRoutes.length > 1) {
|
| - _logger.warning("More than one route matches $path $matchingRoutes");
|
| + var treePath = _matchingTreePath(path, baseRoute);
|
| + Route cmpBase = baseRoute;
|
| + var tail = path;
|
| + // Skip all routes that are unaffected by this path.
|
| + treePath = treePath.skipWhile((_Match matchedRoute) {
|
| + var skip = cmpBase._currentRoute == matchedRoute.route &&
|
| + !_paramsChanged(cmpBase, matchedRoute.urlMatch);
|
| + if (skip) {
|
| + cmpBase = matchedRoute.route;
|
| + tail = matchedRoute.urlMatch.tail;
|
| }
|
| - matchedRoute = matchingRoutes.first;
|
| - } else {
|
| - if (baseRoute._defaultRoute != null) {
|
| - matchedRoute = baseRoute._defaultRoute;
|
| + return skip;
|
| + });
|
| + // TODO(pavelgj): weird things happen without this line...
|
| + treePath = treePath.toList();
|
| + if (treePath.isEmpty) {
|
| + return new Future.value(true);
|
| + }
|
| + var preEnterFutures = _preEnter(tail, treePath);
|
| + return Future.wait(preEnterFutures).then((List<bool> results) {
|
| + if (results.fold(true, (a, b) => a && b)) {
|
| + return _processNewRoute(cmpBase, treePath, tail);
|
| + }
|
| + return false;
|
| + });
|
| + }
|
| +
|
| + List<Future<bool>> _preEnter(String tail, Iterable<_Match> treePath) {
|
| + List<Future<bool>> preEnterFutures = <Future<bool>>[];
|
| + treePath.forEach((_Match matchedRoute) {
|
| + tail = matchedRoute.urlMatch.tail;
|
| + var preEnterEvent = new RoutePreEnterEvent(tail, matchedRoute.urlMatch.parameters, matchedRoute.route);
|
| + matchedRoute.route._onPreEnterController.add(preEnterEvent);
|
| + preEnterFutures.addAll(preEnterEvent._allowEnterFutures);
|
| + });
|
| + return preEnterFutures;
|
| + }
|
| +
|
| + Future<bool> _processNewRoute(Route startingFrom, Iterable<_Match> treePath, String path) {
|
| + return _leaveOldRoutes(startingFrom, treePath).then((bool allowed) {
|
| + if (allowed) {
|
| + var base = startingFrom;
|
| + var tail = path;
|
| + treePath.forEach((_Match matchedRoute) {
|
| + tail = matchedRoute.urlMatch.tail;
|
| + var event = new RouteEnterEvent(matchedRoute.urlMatch.match,
|
| + matchedRoute.urlMatch.parameters, matchedRoute.route);
|
| + _unsetAllCurrentRoutes(base);
|
| + base._currentRoute = matchedRoute.route;
|
| + base._currentRoute._lastEvent = event;
|
| + matchedRoute.route._onEnterController.add(event);
|
| + base = matchedRoute.route;
|
| + });
|
| + return true;
|
| }
|
| + return false;
|
| + });
|
| + }
|
| +
|
| + Future<bool> _leaveOldRoutes(Route startingFrom, Iterable<_Match> treePath) {
|
| + if (treePath.isEmpty) {
|
| + return new Future.value(true);
|
| }
|
| - if (matchedRoute != null) {
|
| - var match = _getMatch(matchedRoute, path);
|
| - if (matchedRoute != baseRoute._currentRoute ||
|
| - _paramsChanged(baseRoute, match)) {
|
| - return _processNewRoute(baseRoute, path, match, matchedRoute);
|
| + var event = new RouteLeaveEvent('', {}, startingFrom);
|
| + return _leaveCurrentRoute(startingFrom, event);
|
| + }
|
| +
|
| + Iterable<_Match> _matchingTreePath(String path, Route baseRoute) {
|
| + List<_Match> treePath = <_Match>[];
|
| + Route matchedRoute;
|
| + do {
|
| + matchedRoute = null;
|
| + List matchingRoutes = baseRoute._routes.values.where(
|
| + (r) => r.path.match(path) != null).toList();
|
| + if (!matchingRoutes.isEmpty) {
|
| + if (matchingRoutes.length > 1) {
|
| + _logger.warning("More than one route matches $path $matchingRoutes");
|
| + }
|
| + matchedRoute = matchingRoutes.first;
|
| } else {
|
| - baseRoute._currentRoute._lastEvent =
|
| - new RouteEvent(match.match, match.parameters,
|
| - baseRoute._currentRoute);
|
| - return _route(match.tail, startingFrom: matchedRoute);
|
| - }
|
| - } else if (baseRoute._currentRoute != null) {
|
| - var event = new RouteEvent('', {}, baseRoute);
|
| - return _leaveCurrentRoute(baseRoute, event).then((success) {
|
| - if (success) {
|
| - baseRoute._currentRoute = null;
|
| + if (baseRoute._defaultRoute != null) {
|
| + matchedRoute = baseRoute._defaultRoute;
|
| }
|
| - return success;
|
| - });
|
| - }
|
| - return new Future.value(true);
|
| + }
|
| + if (matchedRoute != null) {
|
| + var match = _getMatch(matchedRoute, path);
|
| + treePath.add(new _Match(matchedRoute, match));
|
| + baseRoute = matchedRoute;
|
| + path = match.tail;
|
| + }
|
| + } while (matchedRoute != null);
|
| + return treePath;
|
| }
|
|
|
| bool _paramsChanged(Route baseRoute, UrlMatch match) {
|
| @@ -477,12 +593,7 @@ class Router {
|
| return '?$query';
|
| }
|
|
|
| - Route _dehandle(Route r) {
|
| - if (r is RouteHandle) {
|
| - return (r as RouteHandle)._getHost(r);
|
| - }
|
| - return r;
|
| - }
|
| + Route _dehandle(Route r) => r is RouteHandle ? r._getHost(r): r;
|
|
|
| UrlMatch _getMatch(Route route, String path) {
|
| var match = route.path.match(path);
|
| @@ -523,23 +634,6 @@ class Router {
|
| return [key, value];
|
| }
|
|
|
| - Future<bool> _processNewRoute(Route base, String path, UrlMatch match,
|
| - Route newRoute) {
|
| - _logger.finest('_processNewRoute $path');
|
| - var event = new RouteEvent(match.match, match.parameters, newRoute);
|
| - // before we make this a new current route, leave the old
|
| - return _leaveCurrentRoute(base, event).then((bool allowNavigation) {
|
| - if (allowNavigation) {
|
| - _unsetAllCurrentRoutes(base);
|
| - base._currentRoute = newRoute;
|
| - base._currentRoute._lastEvent = event;
|
| - newRoute._onRouteController.add(event);
|
| - return _route(match.tail, startingFrom: newRoute);
|
| - }
|
| - return false;
|
| - });
|
| - }
|
| -
|
| void _unsetAllCurrentRoutes(Route r) {
|
| if (r._currentRoute != null) {
|
| _unsetAllCurrentRoutes(r._currentRoute);
|
| @@ -547,11 +641,11 @@ class Router {
|
| }
|
| }
|
|
|
| - Future<bool> _leaveCurrentRoute(Route base, RouteEvent e) =>
|
| + Future<bool> _leaveCurrentRoute(Route base, RouteLeaveEvent e) =>
|
| Future.wait(_leaveCurrentRouteHelper(base, e))
|
| .then((values) => values.fold(true, (c, v) => c && v));
|
|
|
| - List<Future<bool>> _leaveCurrentRouteHelper(Route base, RouteEvent e) {
|
| + List<Future<bool>> _leaveCurrentRouteHelper(Route base, RouteLeaveEvent e) {
|
| var futures = [];
|
| if (base._currentRoute != null) {
|
| List<Future<bool>> pendingResponses = <Future<bool>>[];
|
| @@ -568,7 +662,7 @@ class Router {
|
| * Listens for window history events and invokes the router. On older
|
| * browsers the hashChange event is used instead.
|
| */
|
| - void listen({bool ignoreClick: false}) {
|
| + void listen({bool ignoreClick: false, Element appRoot}) {
|
| _logger.finest('listen ignoreClick=$ignoreClick');
|
| if (_listen) {
|
| throw new StateError('listen can only be called once');
|
| @@ -596,9 +690,12 @@ class Router {
|
| });
|
| }
|
| if (!ignoreClick) {
|
| + if (appRoot == null) {
|
| + appRoot = _window.document.documentElement;
|
| + }
|
| _logger.finest('listen on win');
|
| - _window.onClick.listen((Event e) {
|
| - if (e.target is AnchorElement) {
|
| + appRoot.onClick.listen((MouseEvent e) {
|
| + if (!e.ctrlKey && !e.metaKey && !e.shiftKey && e.target is AnchorElement) {
|
| AnchorElement anchor = e.target;
|
| if (anchor.host == _window.location.host) {
|
| _logger.finest('clicked ${anchor.pathname}${anchor.hash}');
|
| @@ -674,3 +771,10 @@ class Router {
|
| return res;
|
| }
|
| }
|
| +
|
| +class _Match {
|
| + final Route route;
|
| + final UrlMatch urlMatch;
|
| +
|
| + _Match(this.route, this.urlMatch);
|
| +}
|
|
|