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

Side by Side 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, 11 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library route.client;
6
7 import 'dart:async';
8 import 'dart:collection';
9 import 'dart:html';
10
11 import 'package:logging/logging.dart';
12
13 import 'url_matcher.dart';
14 export 'url_matcher.dart';
15 import 'url_template.dart';
16
17
18 final _logger = new Logger('route');
19
20 typedef RouteEventHandler(RouteEvent path);
21
22 /**
23 * A helper Router handle that scopes all route event subsriptions to it's
24 * instance and provides an convinience [discard] method.
25 */
26 class RouteHandle implements Route {
27 Route _route;
28 final StreamController<RouteEvent> _onRouteController;
29 final StreamController<RouteEvent> _onLeaveController;
30 Stream<RouteEvent> get onRoute => _onRouteController.stream;
31 Stream<RouteEvent> get onLeave => _onLeaveController.stream;
32 StreamSubscription _onRouteSubscription;
33 StreamSubscription _onLeaveSubscription;
34 List<RouteHandle> _childHandles = <RouteHandle>[];
35
36 RouteHandle._new(Route this._route)
37 : _onRouteController =
38 new StreamController<RouteEvent>.broadcast(sync: true),
39 _onLeaveController =
40 new StreamController<RouteEvent>.broadcast(sync: true) {
41 _onRouteSubscription = _route.onRoute.listen(_onRouteController.add);
42 _onLeaveSubscription = _route.onLeave.listen(_onLeaveController.add);
43 }
44
45 /// discards this handle.
46 void discard() {
47 _logger.finest('discarding handle for $_route');
48 _onRouteSubscription.cancel();
49 _onLeaveSubscription.cancel();
50 _onRouteController.close();
51 _onLeaveController.close();
52 _childHandles.forEach((RouteHandle c) => c.discard());
53 _childHandles.clear();
54 _route = null;
55 }
56
57 /// Not supported. Overridden to throw an error.
58 void addRoute({String name, Pattern path, bool defaultRoute: false,
59 RouteEventHandler enter, RouteEventHandler leave, mount}) =>
60 throw new UnsupportedError('addRoute is not supported in handle');
61
62 /// See [Route.getRoute]
63 Route getRoute(String routePath) {
64 Route r = _assertState(() => _getHost(_route).getRoute(routePath));
65 if (r == null) return null;
66 var handle = r.newHandle();
67 if (handle != null) {
68 _childHandles.add(handle);
69 }
70 return handle;
71 }
72
73 /**
74 * Create an return a new [RouteHandle] for this route.
75 */
76 RouteHandle newHandle() {
77 _logger.finest('newHandle for $this');
78 return new RouteHandle._new(_getHost(_route));
79 }
80
81 Route _getHost(Route r) {
82 _assertState();
83 if (r == null) {
84 throw new StateError('Oops?!');
85 }
86 if ((r is Route) && !(r is RouteHandle)) {
87 return r;
88 }
89 RouteHandle rh = r;
90 return rh._getHost(rh._route);
91 }
92
93 /// See [Route.reverse]
94 String reverse(String tail) =>
95 _assertState(() => _getHost(_route).reverse(tail));
96
97 _assertState([f()]) {
98 if (_route == null) {
99 throw new StateError('This route handle is already discated.');
100 }
101 if (f != null) return f();
102 }
103
104 /// See [Route.isActive]
105 bool get isActive => _route.isActive;
106
107 /// See [Route.parameters]
108 Map get parameters => _route.parameters;
109
110 /// See [Route.path]
111 UrlMatcher get path => _route.path;
112
113 /// See [Route.name]
114 String get name => _route.name;
115
116 /// See [Route.parent]
117 Route get parent => _route.parent;
118 }
119
120 /**
121 * Route is a node in the tree of routes. The edge leading to the route is
122 * defined by path.
123 */
124 class Route {
125 final String name;
126 final Map<String, Route> _routes = new LinkedHashMap<String, Route>();
127 final UrlMatcher path;
128 final StreamController<RouteEvent> _onRouteController;
129 final StreamController<RouteEvent> _onLeaveController;
130 final Route parent;
131 Route _defaultRoute;
132 Route _currentRoute;
133 RouteEvent _lastEvent;
134
135 Stream<RouteEvent> get onRoute => _onRouteController.stream;
136 Stream<RouteEvent> get onLeave => _onLeaveController.stream;
137
138 Route._new({this.name, this.path, this.parent})
139 : _onRouteController =
140 new StreamController<RouteEvent>.broadcast(sync: true),
141 _onLeaveController =
142 new StreamController<RouteEvent>.broadcast(sync: true);
143
144 void addRoute({String name, Pattern path, bool defaultRoute: false,
145 RouteEventHandler enter, RouteEventHandler leave, mount}) {
146 if (name == null) {
147 throw new ArgumentError('name is required for all routes');
148 }
149 if (_routes.containsKey(name)) {
150 throw new ArgumentError('Route $name already exists');
151 }
152
153 var matcher;
154 if (!(path is UrlMatcher)) {
155 matcher = new UrlTemplate(path.toString());
156 } else {
157 matcher = path;
158 }
159 var route = new Route._new(name: name, path: matcher, parent: this);
160
161 if (enter != null) {
162 route.onRoute.listen(enter);
163 }
164 if (leave != null) {
165 route.onLeave.listen(leave);
166 }
167
168 if (mount != null) {
169 if (mount is Function) {
170 mount(route);
171 } else if (mount is Routable) {
172 mount.configureRoute(route);
173 }
174 }
175
176 if (defaultRoute) {
177 if (_defaultRoute != null) {
178 throw new StateError('Only one default route can be added.');
179 }
180 _defaultRoute = route;
181 }
182 _routes[name] = route;
183 }
184
185 /**
186 * Returns a route node at the end of the given route path. Route path
187 * dot delimited string of route names.
188 */
189 Route getRoute(String routePath) {
190 var routeName = routePath.split('.').first;
191 if (!_routes.containsKey(routeName)) {
192 _logger.warning('Invalid route name: $routeName $_routes');
193 return null;
194 }
195 var routeToGo = _routes[routeName];
196 var childPath = routePath.substring(routeName.length);
197 if (!childPath.isEmpty) {
198 return routeToGo.getRoute(childPath.substring(1));
199 }
200 return routeToGo;
201 }
202
203 String _getHead(String tail, Map queryParams) {
204 if (parent == null) {
205 return tail;
206 }
207 if (parent._currentRoute == null) {
208 throw new StateError('Router $parent has no current router.');
209 }
210 _populateQueryParams(parent._currentRoute._lastEvent.parameters,
211 parent._currentRoute, queryParams);
212 return parent._getHead(parent._currentRoute.reverse(tail), queryParams);
213 }
214
215 String _getTailUrl(String routePath, Map parameters, Map queryParams) {
216 var routeName = routePath.split('.').first;
217 if (!_routes.containsKey(routeName)) {
218 throw new StateError('Invalid route name: $routeName');
219 }
220 var routeToGo = _routes[routeName];
221 var tail = '';
222 var childPath = routePath.substring(routeName.length);
223 if (childPath.length > 0) {
224 tail = routeToGo._getTailUrl(
225 childPath.substring(1), parameters, queryParams);
226 }
227 _populateQueryParams(parameters, routeToGo, queryParams);
228 return routeToGo.path.reverse(
229 parameters: _joinParams(parameters, routeToGo._lastEvent), tail: tail);
230 }
231
232 void _populateQueryParams(Map parameters, Route route, Map queryParams) {
233 parameters.keys.forEach((String prefixedKey) {
234 if (prefixedKey.startsWith('${route.name}.')) {
235 var key = prefixedKey.substring('${route.name}.'.length);
236 if (!route.path.urlParameterNames().contains(key)) {
237 queryParams[prefixedKey] = parameters[prefixedKey];
238 }
239 }
240 });
241 }
242
243 Map _joinParams(Map parameters, RouteEvent lastEvent) {
244 if (lastEvent == null) {
245 return parameters;
246 }
247 var joined = new Map.from(lastEvent.parameters);
248 parameters.forEach((k, v) { joined[k] = v; });
249 return joined;
250 }
251
252 String toString() {
253 return '[Route: $name]';
254 }
255
256 /**
257 * Returns a URL for this route. The tail (url generated by the child path)
258 * will be passes to the UrlMatcher to be properly appended in the
259 * right place.
260 */
261 String reverse(String tail) {
262 return path.reverse(parameters: _lastEvent.parameters, tail: tail);
263 }
264
265 /**
266 * Create an return a new [RouteHandle] for this route.
267 */
268 RouteHandle newHandle() {
269 _logger.finest('newHandle for $this');
270 return new RouteHandle._new(this);
271 }
272
273 /**
274 * Indicates whether this route is currently active. Root route is always
275 * active.
276 */
277 bool get isActive =>
278 parent == null ? true : identical(parent._currentRoute, this);
279
280 /**
281 * Returns parameters for the currently active route. If the route is not
282 * active the getter returns null.
283 */
284 Map get parameters {
285 if (isActive) {
286 if (_lastEvent == null) return {};
287 return new Map.from(_lastEvent.parameters);
288 }
289 return null;
290 }
291 }
292
293 /**
294 * Route enter or leave event.
295 */
296 class RouteEvent {
297 final String path;
298 final Map parameters;
299 final Route route;
300 var _allowLeaveFutures = <Future<bool>>[];
301
302 RouteEvent(this.path, this.parameters, this.route);
303
304 /**
305 * Can be called on leave with the future which will complete with a boolean
306 * value allowing (true) or disallowing (false) the current navigation.
307 */
308 void allowLeave(Future<bool> allow) {
309 _allowLeaveFutures.add(allow);
310 }
311
312 RouteEvent _clone() => new RouteEvent(path, parameters, route);
313 }
314
315 /**
316 * Event emitted when routing starts.
317 */
318 class RouteStartEvent {
319
320 /**
321 * URI that was passed to [Router.route].
322 */
323 final String uri;
324
325 /**
326 * Future that completes to a boolean value of whether the routing was
327 * successful.
328 */
329 final Future<bool> completed;
330
331 RouteStartEvent._new(this.uri, this.completed);
332 }
333
334 abstract class Routable {
335 void configureRoute(Route router);
336 }
337
338 /**
339 * Stores a set of [UrlPattern] to [Handler] associations and provides methods
340 * for calling a handler for a URL path, listening to [Window] history events,
341 * and creating HTML event handlers that navigate to a URL.
342 */
343 class Router {
344 final bool _useFragment;
345 final Window _window;
346 final Route root;
347 final StreamController<RouteStartEvent> _onRouteStart =
348 new StreamController<RouteStartEvent>.broadcast(sync: true);
349 bool _listen = false;
350
351 /**
352 * [useFragment] determines whether this Router uses pure paths with
353 * [History.pushState] or paths + fragments and [Location.assign]. The default
354 * value is null which then determines the behavior based on
355 * [History.supportsState].
356 */
357 Router({bool useFragment, Window windowImpl})
358 : this._init(null, useFragment: useFragment, windowImpl: windowImpl);
359
360
361 Router._init(Router parent, {bool useFragment, Window windowImpl})
362 : _useFragment = (useFragment == null)
363 ? !History.supportsState
364 : useFragment,
365 _window = (windowImpl == null) ? window : windowImpl,
366 root = new Route._new();
367
368 /**
369 * A stream of route calls.
370 */
371 Stream<RouteStartEvent> get onRouteStart => _onRouteStart.stream;
372
373 /**
374 * Finds a matching [Route] added with [addRoute], parses the path
375 * and invokes the associated callback.
376 *
377 * This method does not perform any navigation, [go] should be used for that.
378 * This method is used to invoke a handler after some other code navigates the
379 * window, such as [listen].
380 */
381 Future<bool> route(String path, {Route startingFrom}) {
382 var future = _route(path, startingFrom: startingFrom);
383 _onRouteStart.add(new RouteStartEvent._new(path, future));
384 return future;
385 }
386
387 Future<bool> _route(String path, {Route startingFrom}) {
388 var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom);
389 _logger.finest('route $path $baseRoute');
390 Route matchedRoute;
391 List matchingRoutes = baseRoute._routes.values.where(
392 (r) => r.path.match(path) != null).toList();
393 if (!matchingRoutes.isEmpty) {
394 if (matchingRoutes.length > 1) {
395 _logger.warning("More than one route matches $path $matchingRoutes");
396 }
397 matchedRoute = matchingRoutes.first;
398 } else {
399 if (baseRoute._defaultRoute != null) {
400 matchedRoute = baseRoute._defaultRoute;
401 }
402 }
403 if (matchedRoute != null) {
404 var match = _getMatch(matchedRoute, path);
405 if (matchedRoute != baseRoute._currentRoute ||
406 _paramsChanged(baseRoute, match)) {
407 return _processNewRoute(baseRoute, path, match, matchedRoute);
408 } else {
409 baseRoute._currentRoute._lastEvent =
410 new RouteEvent(match.match, match.parameters,
411 baseRoute._currentRoute);
412 return _route(match.tail, startingFrom: matchedRoute);
413 }
414 } else if (baseRoute._currentRoute != null) {
415 var event = new RouteEvent('', {}, baseRoute);
416 return _leaveCurrentRoute(baseRoute, event).then((success) {
417 if (success) {
418 baseRoute._currentRoute = null;
419 }
420 return success;
421 });
422 }
423 return new Future.value(true);
424 }
425
426 bool _paramsChanged(Route baseRoute, UrlMatch match) {
427 return baseRoute._currentRoute._lastEvent.path != match.match ||
428 !_mapsEqual(baseRoute._currentRoute._lastEvent.parameters,
429 match.parameters);
430 }
431
432 bool _mapsEqual(Map a, Map b) {
433 if (a.keys.length != b.keys.length) {
434 return false;
435 }
436 for (var keyInA in a.keys) {
437 if (!b.containsKey(keyInA) || a[keyInA] != b[keyInA]) {
438 return false;
439 }
440 }
441 return true;
442 }
443
444 /// Navigates to a given relative route path, and parameters.
445 Future go(String routePath, Map parameters,
446 {Route startingFrom, bool replace: false}) {
447 Map queryParams = {};
448 var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom);
449 var newTail = baseRoute._getTailUrl(routePath, parameters, queryParams) +
450 _buildQuery(queryParams);
451 String newUrl = baseRoute._getHead(newTail, queryParams);
452 _logger.finest('go $newUrl');
453 return route(newTail, startingFrom: baseRoute).then((success) {
454 if (success) {
455 _go(newUrl, null, replace);
456 }
457 return success;
458 });
459 }
460
461 /// Returns an absolute URL for a given relative route path and parameters.
462 String url(String routePath, {Route startingFrom, Map parameters}) {
463 var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom);
464 parameters = parameters == null ? {} : parameters;
465 Map queryParams = {};
466 var tail = baseRoute._getTailUrl(routePath, parameters, queryParams);
467 return (_useFragment ? '#' : '') + baseRoute._getHead(tail, queryParams) +
468 _buildQuery(queryParams);
469 }
470
471 String _buildQuery(Map queryParams) {
472 var query = queryParams.keys.map((key) =>
473 '$key=${Uri.encodeComponent(queryParams[key])}').join('&');
474 if (query.isEmpty) {
475 return '';
476 }
477 return '?$query';
478 }
479
480 Route _dehandle(Route r) {
481 if (r is RouteHandle) {
482 return (r as RouteHandle)._getHost(r);
483 }
484 return r;
485 }
486
487 UrlMatch _getMatch(Route route, String path) {
488 var match = route.path.match(path);
489 if (match == null) { // default route
490 return new UrlMatch('', '', {});
491 }
492 _parseQuery(route, path).forEach((k, v) { match.parameters[k] = v; });
493 return match;
494 }
495
496 Map _parseQuery(Route route, String path) {
497 var params = {};
498 if (path.indexOf('?') == -1) {
499 return params;
500 }
501 String queryStr = path.substring(path.indexOf('?') + 1);
502 queryStr.split('&').forEach((String keyValPair) {
503 List<String> keyVal = _parseKeyVal(keyValPair);
504 if (keyVal[0].startsWith('${route.name}.')) {
505 var key = keyVal[0].substring('${route.name}.'.length);
506 if (!key.isEmpty) {
507 params[key] = Uri.decodeComponent(keyVal[1]);
508 }
509 }
510 });
511 return params;
512 }
513
514 List<String> _parseKeyVal(keyValPair) {
515 if (keyValPair.isEmpty) {
516 return ['', ''];
517 }
518 var splitPoint = keyValPair.indexOf('=') == -1 ?
519 keyValPair.length : keyValPair.indexOf('=') + 1;
520 var key = keyValPair.substring(0, splitPoint +
521 (keyValPair.indexOf('=') == -1 ? 0 : -1));
522 var value = keyValPair.substring(splitPoint);
523 return [key, value];
524 }
525
526 Future<bool> _processNewRoute(Route base, String path, UrlMatch match,
527 Route newRoute) {
528 _logger.finest('_processNewRoute $path');
529 var event = new RouteEvent(match.match, match.parameters, newRoute);
530 // before we make this a new current route, leave the old
531 return _leaveCurrentRoute(base, event).then((bool allowNavigation) {
532 if (allowNavigation) {
533 _unsetAllCurrentRoutes(base);
534 base._currentRoute = newRoute;
535 base._currentRoute._lastEvent = event;
536 newRoute._onRouteController.add(event);
537 return _route(match.tail, startingFrom: newRoute);
538 }
539 return false;
540 });
541 }
542
543 void _unsetAllCurrentRoutes(Route r) {
544 if (r._currentRoute != null) {
545 _unsetAllCurrentRoutes(r._currentRoute);
546 r._currentRoute = null;
547 }
548 }
549
550 Future<bool> _leaveCurrentRoute(Route base, RouteEvent e) =>
551 Future.wait(_leaveCurrentRouteHelper(base, e))
552 .then((values) => values.fold(true, (c, v) => c && v));
553
554 List<Future<bool>> _leaveCurrentRouteHelper(Route base, RouteEvent e) {
555 var futures = [];
556 if (base._currentRoute != null) {
557 List<Future<bool>> pendingResponses = <Future<bool>>[];
558 // We create a copy of the route event
559 var event = e._clone();
560 base._currentRoute._onLeaveController.add(event);
561 futures.addAll(event._allowLeaveFutures);
562 futures.addAll(_leaveCurrentRouteHelper(base._currentRoute, event));
563 }
564 return futures;
565 }
566
567 /**
568 * Listens for window history events and invokes the router. On older
569 * browsers the hashChange event is used instead.
570 */
571 void listen({bool ignoreClick: false}) {
572 _logger.finest('listen ignoreClick=$ignoreClick');
573 if (_listen) {
574 throw new StateError('listen can only be called once');
575 }
576 _listen = true;
577 if (_useFragment) {
578 _window.onHashChange.listen((_) {
579 route(_normalizeHash(_window.location.hash)).then((allowed) {
580 // if not allowed, we need to restore the browser location
581 if (!allowed) {
582 _window.history.back();
583 }
584 });
585 });
586 route(_normalizeHash(_window.location.hash));
587 } else {
588 _window.onPopState.listen((_) {
589 var path = '${_window.location.pathname}${_window.location.hash}';
590 route(path).then((allowed) {
591 // if not allowed, we need to restore the browser location
592 if (!allowed) {
593 _window.history.back();
594 }
595 });
596 });
597 }
598 if (!ignoreClick) {
599 _logger.finest('listen on win');
600 _window.onClick.listen((Event e) {
601 if (e.target is AnchorElement) {
602 AnchorElement anchor = e.target;
603 if (anchor.host == _window.location.host) {
604 _logger.finest('clicked ${anchor.pathname}${anchor.hash}');
605 e.preventDefault();
606 var path;
607 if (_useFragment) {
608 path = _normalizeHash(anchor.hash);
609 } else {
610 path = '${anchor.pathname}';
611 }
612 route(path).then((allowed) {
613 if (allowed) {
614 _go(path, null, false);
615 }
616 });
617 }
618 }
619 });
620 }
621 }
622
623 String _normalizeHash(String hash) {
624 if (hash.isEmpty) {
625 return '';
626 }
627 return hash.substring(1);
628 }
629
630 /**
631 * Navigates the browser to the path produced by [url] with [args] by calling
632 * [History.pushState], then invokes the handler associated with [url].
633 *
634 * On older browsers [Location.assign] is used instead with the fragment
635 * version of the UrlPattern.
636 */
637 Future<bool> gotoUrl(String url) {
638 return route(url).then((success) {
639 if (success) {
640 _go(url, null, false);
641 }
642 });
643 }
644
645 void _go(String path, String title, bool replace) {
646 title = (title == null) ? '' : title;
647 if (_useFragment) {
648 if (replace) {
649 _window.location.replace('#$path');
650 } else {
651 _window.location.assign('#$path');
652 }
653 (_window.document as HtmlDocument).title = title;
654 } else {
655 if (replace) {
656 _window.history.replaceState(null, title, path);
657 } else {
658 _window.history.pushState(null, title, path);
659 }
660 }
661 }
662
663 /**
664 * Returns the current active route path in the route tree.
665 * Excludes the root path.
666 */
667 List<Route> get activePath {
668 var res = <Route>[];
669 var current = root;
670 while (current._currentRoute != null) {
671 current = current._currentRoute;
672 res.add(current);
673 }
674 return res;
675 }
676 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698