Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(309)

Unified Diff: third_party/pkg/route_hierarchical/lib/client.dart

Issue 124053002: Adding Angular and dependent packages for testing (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 12 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
new file mode 100644
index 0000000000000000000000000000000000000000..82428a01d396a8025cc616d478621a3fcd1a1d96
--- /dev/null
+++ b/third_party/pkg/route_hierarchical/lib/client.dart
@@ -0,0 +1,676 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library route.client;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:html';
+
+import 'package:logging/logging.dart';
+
+import 'url_matcher.dart';
+export 'url_matcher.dart';
+import 'url_template.dart';
+
+
+final _logger = new Logger('route');
+
+typedef RouteEventHandler(RouteEvent path);
+
+/**
+ * A helper Router handle that scopes all route event subsriptions to it's
+ * instance and provides an convinience [discard] method.
+ */
+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;
+ StreamSubscription _onLeaveSubscription;
+ List<RouteHandle> _childHandles = <RouteHandle>[];
+
+ RouteHandle._new(Route this._route)
+ : _onRouteController =
+ new StreamController<RouteEvent>.broadcast(sync: true),
+ _onLeaveController =
+ new StreamController<RouteEvent>.broadcast(sync: true) {
+ _onRouteSubscription = _route.onRoute.listen(_onRouteController.add);
+ _onLeaveSubscription = _route.onLeave.listen(_onLeaveController.add);
+ }
+
+ /// discards this handle.
+ void discard() {
+ _logger.finest('discarding handle for $_route');
+ _onRouteSubscription.cancel();
+ _onLeaveSubscription.cancel();
+ _onRouteController.close();
+ _onLeaveController.close();
+ _childHandles.forEach((RouteHandle c) => c.discard());
+ _childHandles.clear();
+ _route = null;
+ }
+
+ /// Not supported. Overridden to throw an error.
+ void addRoute({String name, Pattern path, bool defaultRoute: false,
+ RouteEventHandler enter, RouteEventHandler leave, mount}) =>
+ throw new UnsupportedError('addRoute is not supported in handle');
+
+ /// See [Route.getRoute]
+ Route getRoute(String routePath) {
+ Route r = _assertState(() => _getHost(_route).getRoute(routePath));
+ if (r == null) return null;
+ var handle = r.newHandle();
+ if (handle != null) {
+ _childHandles.add(handle);
+ }
+ return handle;
+ }
+
+ /**
+ * Create an return a new [RouteHandle] for this route.
+ */
+ RouteHandle newHandle() {
+ _logger.finest('newHandle for $this');
+ return new RouteHandle._new(_getHost(_route));
+ }
+
+ Route _getHost(Route r) {
+ _assertState();
+ if (r == null) {
+ throw new StateError('Oops?!');
+ }
+ if ((r is Route) && !(r is RouteHandle)) {
+ return r;
+ }
+ RouteHandle rh = r;
+ return rh._getHost(rh._route);
+ }
+
+ /// See [Route.reverse]
+ String reverse(String tail) =>
+ _assertState(() => _getHost(_route).reverse(tail));
+
+ _assertState([f()]) {
+ if (_route == null) {
+ throw new StateError('This route handle is already discated.');
+ }
+ if (f != null) return f();
+ }
+
+ /// See [Route.isActive]
+ bool get isActive => _route.isActive;
+
+ /// See [Route.parameters]
+ Map get parameters => _route.parameters;
+
+ /// See [Route.path]
+ UrlMatcher get path => _route.path;
+
+ /// See [Route.name]
+ String get name => _route.name;
+
+ /// See [Route.parent]
+ Route get parent => _route.parent;
+}
+
+/**
+ * Route is a node in the tree of routes. The edge leading to the route is
+ * defined by path.
+ */
+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 Route parent;
+ Route _defaultRoute;
+ Route _currentRoute;
+ RouteEvent _lastEvent;
+
+ Stream<RouteEvent> get onRoute => _onRouteController.stream;
+ Stream<RouteEvent> get onLeave => _onLeaveController.stream;
+
+ Route._new({this.name, this.path, this.parent})
+ : _onRouteController =
+ new StreamController<RouteEvent>.broadcast(sync: true),
+ _onLeaveController =
+ new StreamController<RouteEvent>.broadcast(sync: true);
+
+ void addRoute({String name, Pattern path, bool defaultRoute: false,
+ RouteEventHandler enter, RouteEventHandler leave, mount}) {
+ if (name == null) {
+ throw new ArgumentError('name is required for all routes');
+ }
+ if (_routes.containsKey(name)) {
+ throw new ArgumentError('Route $name already exists');
+ }
+
+ var matcher;
+ if (!(path is UrlMatcher)) {
+ matcher = new UrlTemplate(path.toString());
+ } else {
+ matcher = path;
+ }
+ var route = new Route._new(name: name, path: matcher, parent: this);
+
+ if (enter != null) {
+ route.onRoute.listen(enter);
+ }
+ if (leave != null) {
+ route.onLeave.listen(leave);
+ }
+
+ if (mount != null) {
+ if (mount is Function) {
+ mount(route);
+ } else if (mount is Routable) {
+ mount.configureRoute(route);
+ }
+ }
+
+ if (defaultRoute) {
+ if (_defaultRoute != null) {
+ throw new StateError('Only one default route can be added.');
+ }
+ _defaultRoute = route;
+ }
+ _routes[name] = route;
+ }
+
+ /**
+ * Returns a route node at the end of the given route path. Route path
+ * dot delimited string of route names.
+ */
+ Route getRoute(String routePath) {
+ var routeName = routePath.split('.').first;
+ if (!_routes.containsKey(routeName)) {
+ _logger.warning('Invalid route name: $routeName $_routes');
+ return null;
+ }
+ var routeToGo = _routes[routeName];
+ var childPath = routePath.substring(routeName.length);
+ if (!childPath.isEmpty) {
+ return routeToGo.getRoute(childPath.substring(1));
+ }
+ return routeToGo;
+ }
+
+ String _getHead(String tail, Map queryParams) {
+ if (parent == null) {
+ return tail;
+ }
+ if (parent._currentRoute == null) {
+ throw new StateError('Router $parent has no current router.');
+ }
+ _populateQueryParams(parent._currentRoute._lastEvent.parameters,
+ parent._currentRoute, queryParams);
+ return parent._getHead(parent._currentRoute.reverse(tail), queryParams);
+ }
+
+ String _getTailUrl(String routePath, Map parameters, Map queryParams) {
+ var routeName = routePath.split('.').first;
+ if (!_routes.containsKey(routeName)) {
+ throw new StateError('Invalid route name: $routeName');
+ }
+ var routeToGo = _routes[routeName];
+ var tail = '';
+ var childPath = routePath.substring(routeName.length);
+ if (childPath.length > 0) {
+ tail = routeToGo._getTailUrl(
+ childPath.substring(1), parameters, queryParams);
+ }
+ _populateQueryParams(parameters, routeToGo, queryParams);
+ return routeToGo.path.reverse(
+ parameters: _joinParams(parameters, routeToGo._lastEvent), tail: tail);
+ }
+
+ void _populateQueryParams(Map parameters, Route route, Map queryParams) {
+ parameters.keys.forEach((String prefixedKey) {
+ if (prefixedKey.startsWith('${route.name}.')) {
+ var key = prefixedKey.substring('${route.name}.'.length);
+ if (!route.path.urlParameterNames().contains(key)) {
+ queryParams[prefixedKey] = parameters[prefixedKey];
+ }
+ }
+ });
+ }
+
+ Map _joinParams(Map parameters, RouteEvent lastEvent) {
+ if (lastEvent == null) {
+ return parameters;
+ }
+ var joined = new Map.from(lastEvent.parameters);
+ parameters.forEach((k, v) { joined[k] = v; });
+ return joined;
+ }
+
+ String toString() {
+ return '[Route: $name]';
+ }
+
+ /**
+ * Returns a URL for this route. The tail (url generated by the child path)
+ * will be passes to the UrlMatcher to be properly appended in the
+ * right place.
+ */
+ String reverse(String tail) {
+ return path.reverse(parameters: _lastEvent.parameters, tail: tail);
+ }
+
+ /**
+ * Create an return a new [RouteHandle] for this route.
+ */
+ RouteHandle newHandle() {
+ _logger.finest('newHandle for $this');
+ return new RouteHandle._new(this);
+ }
+
+ /**
+ * Indicates whether this route is currently active. Root route is always
+ * active.
+ */
+ bool get isActive =>
+ parent == null ? true : identical(parent._currentRoute, this);
+
+ /**
+ * Returns parameters for the currently active route. If the route is not
+ * active the getter returns null.
+ */
+ Map get parameters {
+ if (isActive) {
+ if (_lastEvent == null) return {};
+ return new Map.from(_lastEvent.parameters);
+ }
+ return null;
+ }
+}
+
+/**
+ * Route enter or leave event.
+ */
+class RouteEvent {
+ final String path;
+ final Map parameters;
+ final Route route;
+ var _allowLeaveFutures = <Future<bool>>[];
+
+ RouteEvent(this.path, this.parameters, this.route);
+
+ /**
+ * Can be called on leave 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);
+}
+
+/**
+ * Event emitted when routing starts.
+ */
+class RouteStartEvent {
+
+ /**
+ * URI that was passed to [Router.route].
+ */
+ final String uri;
+
+ /**
+ * Future that completes to a boolean value of whether the routing was
+ * successful.
+ */
+ final Future<bool> completed;
+
+ RouteStartEvent._new(this.uri, this.completed);
+}
+
+abstract class Routable {
+ void configureRoute(Route router);
+}
+
+/**
+ * Stores a set of [UrlPattern] to [Handler] associations and provides methods
+ * for calling a handler for a URL path, listening to [Window] history events,
+ * and creating HTML event handlers that navigate to a URL.
+ */
+class Router {
+ final bool _useFragment;
+ final Window _window;
+ final Route root;
+ final StreamController<RouteStartEvent> _onRouteStart =
+ new StreamController<RouteStartEvent>.broadcast(sync: true);
+ bool _listen = false;
+
+ /**
+ * [useFragment] determines whether this Router uses pure paths with
+ * [History.pushState] or paths + fragments and [Location.assign]. The default
+ * value is null which then determines the behavior based on
+ * [History.supportsState].
+ */
+ Router({bool useFragment, Window windowImpl})
+ : this._init(null, useFragment: useFragment, windowImpl: windowImpl);
+
+
+ Router._init(Router parent, {bool useFragment, Window windowImpl})
+ : _useFragment = (useFragment == null)
+ ? !History.supportsState
+ : useFragment,
+ _window = (windowImpl == null) ? window : windowImpl,
+ root = new Route._new();
+
+ /**
+ * A stream of route calls.
+ */
+ Stream<RouteStartEvent> get onRouteStart => _onRouteStart.stream;
+
+ /**
+ * Finds a matching [Route] added with [addRoute], parses the path
+ * and invokes the associated callback.
+ *
+ * This method does not perform any navigation, [go] should be used for that.
+ * This method is used to invoke a handler after some other code navigates the
+ * window, such as [listen].
+ */
+ Future<bool> route(String path, {Route startingFrom}) {
+ var future = _route(path, startingFrom: 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);
+ _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");
+ }
+ matchedRoute = matchingRoutes.first;
+ } else {
+ if (baseRoute._defaultRoute != null) {
+ matchedRoute = baseRoute._defaultRoute;
+ }
+ }
+ if (matchedRoute != null) {
+ var match = _getMatch(matchedRoute, path);
+ if (matchedRoute != baseRoute._currentRoute ||
+ _paramsChanged(baseRoute, match)) {
+ return _processNewRoute(baseRoute, path, match, matchedRoute);
+ } 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;
+ }
+ return success;
+ });
+ }
+ return new Future.value(true);
+ }
+
+ bool _paramsChanged(Route baseRoute, UrlMatch match) {
+ return baseRoute._currentRoute._lastEvent.path != match.match ||
+ !_mapsEqual(baseRoute._currentRoute._lastEvent.parameters,
+ match.parameters);
+ }
+
+ bool _mapsEqual(Map a, Map b) {
+ if (a.keys.length != b.keys.length) {
+ return false;
+ }
+ for (var keyInA in a.keys) {
+ if (!b.containsKey(keyInA) || a[keyInA] != b[keyInA]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /// Navigates to a given relative route path, and parameters.
+ Future go(String routePath, Map parameters,
+ {Route startingFrom, bool replace: false}) {
+ Map queryParams = {};
+ var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom);
+ var newTail = baseRoute._getTailUrl(routePath, parameters, queryParams) +
+ _buildQuery(queryParams);
+ String newUrl = baseRoute._getHead(newTail, queryParams);
+ _logger.finest('go $newUrl');
+ return route(newTail, startingFrom: baseRoute).then((success) {
+ if (success) {
+ _go(newUrl, null, replace);
+ }
+ return success;
+ });
+ }
+
+ /// Returns an absolute URL for a given relative route path and parameters.
+ String url(String routePath, {Route startingFrom, Map parameters}) {
+ var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom);
+ parameters = parameters == null ? {} : parameters;
+ Map queryParams = {};
+ var tail = baseRoute._getTailUrl(routePath, parameters, queryParams);
+ return (_useFragment ? '#' : '') + baseRoute._getHead(tail, queryParams) +
+ _buildQuery(queryParams);
+ }
+
+ String _buildQuery(Map queryParams) {
+ var query = queryParams.keys.map((key) =>
+ '$key=${Uri.encodeComponent(queryParams[key])}').join('&');
+ if (query.isEmpty) {
+ return '';
+ }
+ return '?$query';
+ }
+
+ Route _dehandle(Route r) {
+ if (r is RouteHandle) {
+ return (r as RouteHandle)._getHost(r);
+ }
+ return r;
+ }
+
+ UrlMatch _getMatch(Route route, String path) {
+ var match = route.path.match(path);
+ if (match == null) { // default route
+ return new UrlMatch('', '', {});
+ }
+ _parseQuery(route, path).forEach((k, v) { match.parameters[k] = v; });
+ return match;
+ }
+
+ Map _parseQuery(Route route, String path) {
+ var params = {};
+ if (path.indexOf('?') == -1) {
+ return params;
+ }
+ String queryStr = path.substring(path.indexOf('?') + 1);
+ queryStr.split('&').forEach((String keyValPair) {
+ List<String> keyVal = _parseKeyVal(keyValPair);
+ if (keyVal[0].startsWith('${route.name}.')) {
+ var key = keyVal[0].substring('${route.name}.'.length);
+ if (!key.isEmpty) {
+ params[key] = Uri.decodeComponent(keyVal[1]);
+ }
+ }
+ });
+ return params;
+ }
+
+ List<String> _parseKeyVal(keyValPair) {
+ if (keyValPair.isEmpty) {
+ return ['', ''];
+ }
+ var splitPoint = keyValPair.indexOf('=') == -1 ?
+ keyValPair.length : keyValPair.indexOf('=') + 1;
+ var key = keyValPair.substring(0, splitPoint +
+ (keyValPair.indexOf('=') == -1 ? 0 : -1));
+ var value = keyValPair.substring(splitPoint);
+ 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);
+ r._currentRoute = null;
+ }
+ }
+
+ Future<bool> _leaveCurrentRoute(Route base, RouteEvent e) =>
+ Future.wait(_leaveCurrentRouteHelper(base, e))
+ .then((values) => values.fold(true, (c, v) => c && v));
+
+ List<Future<bool>> _leaveCurrentRouteHelper(Route base, RouteEvent e) {
+ var futures = [];
+ if (base._currentRoute != null) {
+ List<Future<bool>> pendingResponses = <Future<bool>>[];
+ // We create a copy of the route event
+ var event = e._clone();
+ base._currentRoute._onLeaveController.add(event);
+ futures.addAll(event._allowLeaveFutures);
+ futures.addAll(_leaveCurrentRouteHelper(base._currentRoute, event));
+ }
+ return futures;
+ }
+
+ /**
+ * Listens for window history events and invokes the router. On older
+ * browsers the hashChange event is used instead.
+ */
+ void listen({bool ignoreClick: false}) {
+ _logger.finest('listen ignoreClick=$ignoreClick');
+ if (_listen) {
+ throw new StateError('listen can only be called once');
+ }
+ _listen = true;
+ if (_useFragment) {
+ _window.onHashChange.listen((_) {
+ route(_normalizeHash(_window.location.hash)).then((allowed) {
+ // if not allowed, we need to restore the browser location
+ if (!allowed) {
+ _window.history.back();
+ }
+ });
+ });
+ route(_normalizeHash(_window.location.hash));
+ } else {
+ _window.onPopState.listen((_) {
+ var path = '${_window.location.pathname}${_window.location.hash}';
+ route(path).then((allowed) {
+ // if not allowed, we need to restore the browser location
+ if (!allowed) {
+ _window.history.back();
+ }
+ });
+ });
+ }
+ if (!ignoreClick) {
+ _logger.finest('listen on win');
+ _window.onClick.listen((Event e) {
+ if (e.target is AnchorElement) {
+ AnchorElement anchor = e.target;
+ if (anchor.host == _window.location.host) {
+ _logger.finest('clicked ${anchor.pathname}${anchor.hash}');
+ e.preventDefault();
+ var path;
+ if (_useFragment) {
+ path = _normalizeHash(anchor.hash);
+ } else {
+ path = '${anchor.pathname}';
+ }
+ route(path).then((allowed) {
+ if (allowed) {
+ _go(path, null, false);
+ }
+ });
+ }
+ }
+ });
+ }
+ }
+
+ String _normalizeHash(String hash) {
+ if (hash.isEmpty) {
+ return '';
+ }
+ return hash.substring(1);
+ }
+
+ /**
+ * Navigates the browser to the path produced by [url] with [args] by calling
+ * [History.pushState], then invokes the handler associated with [url].
+ *
+ * On older browsers [Location.assign] is used instead with the fragment
+ * version of the UrlPattern.
+ */
+ Future<bool> gotoUrl(String url) {
+ return route(url).then((success) {
+ if (success) {
+ _go(url, null, false);
+ }
+ });
+ }
+
+ void _go(String path, String title, bool replace) {
+ title = (title == null) ? '' : title;
+ if (_useFragment) {
+ if (replace) {
+ _window.location.replace('#$path');
+ } else {
+ _window.location.assign('#$path');
+ }
+ (_window.document as HtmlDocument).title = title;
+ } else {
+ if (replace) {
+ _window.history.replaceState(null, title, path);
+ } else {
+ _window.history.pushState(null, title, path);
+ }
+ }
+ }
+
+ /**
+ * Returns the current active route path in the route tree.
+ * Excludes the root path.
+ */
+ List<Route> get activePath {
+ var res = <Route>[];
+ var current = root;
+ while (current._currentRoute != null) {
+ current = current._currentRoute;
+ res.add(current);
+ }
+ return res;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698