OLD | NEW |
| (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:html'; | |
9 | |
10 import 'package:logging/logging.dart'; | |
11 | |
12 import 'src/utils.dart'; | |
13 | |
14 import 'url_matcher.dart'; | |
15 export 'url_matcher.dart'; | |
16 import 'url_template.dart'; | |
17 | |
18 part 'route_handle.dart'; | |
19 | |
20 | |
21 final _logger = new Logger('route'); | |
22 const _PATH_SEPARATOR = '.'; | |
23 | |
24 typedef void RoutePreEnterEventHandler(RoutePreEnterEvent event); | |
25 typedef void RouteEnterEventHandler(RouteEnterEvent event); | |
26 typedef void RouteLeaveEventHandler(RouteLeaveEvent event); | |
27 | |
28 /** | |
29 * [Route] represents a node in the route tree. | |
30 */ | |
31 abstract class Route { | |
32 /** | |
33 * Name of the route. Used when querying routes. | |
34 */ | |
35 String get name; | |
36 | |
37 /** | |
38 * A path fragment [UrlMatcher] for this route. | |
39 */ | |
40 UrlMatcher get path; | |
41 | |
42 /** | |
43 * Parent route in the route tree. | |
44 */ | |
45 Route get parent; | |
46 | |
47 /** | |
48 * Indicates whether this route is currently active. Root route is always | |
49 * active. | |
50 */ | |
51 bool get isActive; | |
52 | |
53 /** | |
54 * Returns parameters for the currently active route. If the route is not | |
55 * active the getter returns null. | |
56 */ | |
57 Map get parameters; | |
58 | |
59 /** | |
60 * Whether to trigger the leave event when only the parameters change. | |
61 */ | |
62 bool get dontLeaveOnParamChanges; | |
63 | |
64 /** | |
65 * Returns a stream of [RouteEnterEvent] events. The [RouteEnterEvent] event | |
66 * is fired when route has already been made active, but before subroutes | |
67 * are entered. The event starts at the root and propagates from parent to | |
68 * child routes. | |
69 */ | |
70 @Deprecated("use [onEnter] instead.") | |
71 Stream<RouteEnterEvent> get onRoute; | |
72 | |
73 /** | |
74 * Returns a stream of [RoutePreEnterEvent] events. The [RoutePreEnterEvent] | |
75 * event is fired when the route is matched during the routing, but before | |
76 * any previous routes were left, or any new routes were entered. The event | |
77 * starts at the root and propagates from parent to child routes. | |
78 * | |
79 * At this stage it's possible to veto entering of the route by calling | |
80 * [RoutePreEnterEvent.allowEnter] with a [Future] returns a boolean value | |
81 * indicating whether enter is permitted (true) or not (false). | |
82 */ | |
83 Stream<RoutePreEnterEvent> get onPreEnter; | |
84 | |
85 /** | |
86 * Returns a stream of [RouteLeaveEvent] events. The [RouteLeaveEvent] | |
87 * event is fired when the route is being left. The event starts at the leaf | |
88 * route and propagates from child to parent routes. | |
89 * | |
90 * At this stage it's possible to veto leaving of the route by calling | |
91 * [RouteLeaveEvent.allowLeave] with a [Future] returns a boolean value | |
92 * indicating whether leave is permitted (true) or not (false). | |
93 * | |
94 * Note: that once child routes have been notified of the leave they will not | |
95 * be notified of the subsequent veto by any parent route. See: | |
96 * https://github.com/angular/route.dart/issues/28 | |
97 */ | |
98 Stream<RouteLeaveEvent> get onLeave; | |
99 | |
100 /** | |
101 * Returns a stream of [RouteEnterEvent] events. The [RouteEnterEvent] event | |
102 * is fired when route has already been made active, but before subroutes | |
103 * are entered. The event starts at the root and propagates from parent | |
104 * to child routes. | |
105 */ | |
106 Stream<RouteEnterEvent> get onEnter; | |
107 | |
108 void addRoute({String name, Pattern path, bool defaultRoute: false, | |
109 RouteEnterEventHandler enter, RoutePreEnterEventHandler preEnter, | |
110 RouteLeaveEventHandler leave, mount, dontLeaveOnParamChanges: false}); | |
111 | |
112 /** | |
113 * Queries sub-routes using the [routePath] and returns the matching [Route]. | |
114 * | |
115 * [routePath] is a dot-separated list of route names. Ex: foo.bar.baz, which | |
116 * means that current route should contain route named 'foo', the 'foo' route | |
117 * should contain route named 'bar', and so on. | |
118 * | |
119 * If no match is found then [:null:] is returned. | |
120 */ | |
121 @Deprecated("use [findRoute] instead.") | |
122 Route getRoute(String routePath); | |
123 | |
124 /** | |
125 * Queries sub-routes using the [routePath] and returns the matching [Route]. | |
126 * | |
127 * [routePath] is a dot-separated list of route names. Ex: foo.bar.baz, which | |
128 * means that current route should contain route named 'foo', the 'foo' route | |
129 * should contain route named 'bar', and so on. | |
130 * | |
131 * If no match is found then [:null:] is returned. | |
132 */ | |
133 Route findRoute(String routePath); | |
134 | |
135 /** | |
136 * Create an return a new [RouteHandle] for this route. | |
137 */ | |
138 RouteHandle newHandle(); | |
139 | |
140 String toString() => '[Route: $name]'; | |
141 } | |
142 | |
143 /** | |
144 * Route is a node in the tree of routes. The edge leading to the route is | |
145 * defined by path. | |
146 */ | |
147 class RouteImpl extends Route { | |
148 @override | |
149 final String name; | |
150 @override | |
151 final UrlMatcher path; | |
152 @override | |
153 final RouteImpl parent; | |
154 | |
155 final _routes = <String, RouteImpl>{}; | |
156 final StreamController<RouteEnterEvent> _onEnterController; | |
157 final StreamController<RoutePreEnterEvent> _onPreEnterController; | |
158 final StreamController<RouteLeaveEvent> _onLeaveController; | |
159 RouteImpl _defaultRoute; | |
160 RouteImpl _currentRoute; | |
161 RouteEvent _lastEvent; | |
162 @override | |
163 final bool dontLeaveOnParamChanges; | |
164 | |
165 @override | |
166 @Deprecated("use [onEnter] instead.") | |
167 Stream<RouteEvent> get onRoute => onEnter; | |
168 @override | |
169 Stream<RouteEvent> get onPreEnter => _onPreEnterController.stream; | |
170 @override | |
171 Stream<RouteEvent> get onLeave => _onLeaveController.stream; | |
172 @override | |
173 Stream<RouteEvent> get onEnter => _onEnterController.stream; | |
174 | |
175 RouteImpl._new({this.name, this.path, this.parent, | |
176 this.dontLeaveOnParamChanges: false}) | |
177 : _onEnterController = | |
178 new StreamController<RouteEnterEvent>.broadcast(sync: true), | |
179 _onPreEnterController = | |
180 new StreamController<RoutePreEnterEvent>.broadcast(sync: true), | |
181 _onLeaveController = | |
182 new StreamController<RouteLeaveEvent>.broadcast(sync: true); | |
183 | |
184 @override | |
185 void addRoute({String name, Pattern path, bool defaultRoute: false, | |
186 RouteEnterEventHandler enter, RoutePreEnterEventHandler preEnter, | |
187 RouteLeaveEventHandler leave, mount, dontLeaveOnParamChanges: false}) { | |
188 if (name == null) { | |
189 throw new ArgumentError('name is required for all routes'); | |
190 } | |
191 if (name.contains(_PATH_SEPARATOR)) { | |
192 throw new ArgumentError('name cannot contain dot.'); | |
193 } | |
194 if (_routes.containsKey(name)) { | |
195 throw new ArgumentError('Route $name already exists'); | |
196 } | |
197 | |
198 var matcher = path is UrlMatcher ? path : new UrlTemplate(path.toString()); | |
199 | |
200 var route = new RouteImpl._new(name: name, path: matcher, parent: this, | |
201 dontLeaveOnParamChanges: dontLeaveOnParamChanges); | |
202 | |
203 route..onPreEnter.listen(preEnter) | |
204 ..onEnter.listen(enter) | |
205 ..onLeave.listen(leave); | |
206 | |
207 if (mount != null) { | |
208 if (mount is Function) { | |
209 mount(route); | |
210 } else if (mount is Routable) { | |
211 mount.configureRoute(route); | |
212 } | |
213 } | |
214 | |
215 if (defaultRoute) { | |
216 if (_defaultRoute != null) { | |
217 throw new StateError('Only one default route can be added.'); | |
218 } | |
219 _defaultRoute = route; | |
220 } | |
221 _routes[name] = route; | |
222 } | |
223 | |
224 @override | |
225 Route getRoute(String routePath) => findRoute(routePath); | |
226 | |
227 @override | |
228 Route findRoute(String routePath) { | |
229 var routeName = routePath.split(_PATH_SEPARATOR).first; | |
230 if (!_routes.containsKey(routeName)) { | |
231 _logger.warning('Invalid route name: $routeName $_routes'); | |
232 return null; | |
233 } | |
234 var routeToGo = _routes[routeName]; | |
235 var childPath = routePath.substring(routeName.length); | |
236 return childPath.isEmpty ? routeToGo : | |
237 routeToGo.getRoute(childPath.substring(1)); | |
238 } | |
239 | |
240 String _getHead(String tail, Map queryParams) { | |
241 if (parent == null) return tail; | |
242 if (parent._currentRoute == null) { | |
243 throw new StateError('Router $parent has no current router.'); | |
244 } | |
245 _populateQueryParams(parent._currentRoute._lastEvent.parameters, | |
246 parent._currentRoute, queryParams); | |
247 return parent._getHead(parent._currentRoute._reverse(tail), queryParams); | |
248 } | |
249 | |
250 String _getTailUrl(String routePath, Map parameters, Map queryParams) { | |
251 var routeName = routePath.split('.').first; | |
252 if (!_routes.containsKey(routeName)) { | |
253 throw new StateError('Invalid route name: $routeName'); | |
254 } | |
255 var routeToGo = _routes[routeName]; | |
256 var tail = ''; | |
257 var childPath = routePath.substring(routeName.length); | |
258 if (childPath.isNotEmpty) { | |
259 tail = routeToGo._getTailUrl( | |
260 childPath.substring(1), parameters, queryParams); | |
261 } | |
262 _populateQueryParams(parameters, routeToGo, queryParams); | |
263 return routeToGo.path.reverse( | |
264 parameters: _joinParams(parameters, routeToGo._lastEvent), tail: tail); | |
265 } | |
266 | |
267 void _populateQueryParams(Map parameters, Route route, Map queryParams) { | |
268 parameters.keys.forEach((String prefixedKey) { | |
269 if (prefixedKey.startsWith('${route.name}.')) { | |
270 var key = prefixedKey.substring('${route.name}.'.length); | |
271 if (!route.path.urlParameterNames().contains(key)) { | |
272 queryParams[prefixedKey] = parameters[prefixedKey]; | |
273 } | |
274 } | |
275 }); | |
276 } | |
277 | |
278 Map _joinParams(Map parameters, RouteEvent lastEvent) => lastEvent == null | |
279 ? parameters | |
280 : new Map.from(lastEvent.parameters)..addAll(parameters); | |
281 | |
282 /** | |
283 * Returns a URL for this route. The tail (url generated by the child path) | |
284 * will be passes to the UrlMatcher to be properly appended in the | |
285 * right place. | |
286 */ | |
287 String _reverse(String tail) => | |
288 path.reverse(parameters: _lastEvent.parameters, tail: tail); | |
289 | |
290 /** | |
291 * Create an return a new [RouteHandle] for this route. | |
292 */ | |
293 @override | |
294 RouteHandle newHandle() { | |
295 _logger.finest('newHandle for $this'); | |
296 return new RouteHandle._new(this); | |
297 } | |
298 | |
299 /** | |
300 * Indicates whether this route is currently active. Root route is always | |
301 * active. | |
302 */ | |
303 @override | |
304 bool get isActive => | |
305 parent == null ? true : identical(parent._currentRoute, this); | |
306 | |
307 /** | |
308 * Returns parameters for the currently active route. If the route is not | |
309 * active the getter returns null. | |
310 */ | |
311 @override | |
312 Map get parameters { | |
313 if (isActive) { | |
314 return _lastEvent == null ? {} : new Map.from(_lastEvent.parameters); | |
315 } | |
316 return null; | |
317 } | |
318 } | |
319 | |
320 /** | |
321 * Route enter or leave event. | |
322 */ | |
323 abstract class RouteEvent { | |
324 final String path; | |
325 final Map parameters; | |
326 final Route route; | |
327 | |
328 RouteEvent(this.path, this.parameters, this.route); | |
329 } | |
330 | |
331 class RoutePreEnterEvent extends RouteEvent { | |
332 final _allowEnterFutures = <Future<bool>>[]; | |
333 | |
334 RoutePreEnterEvent(path, parameters, route) : super(path, parameters, route); | |
335 | |
336 RoutePreEnterEvent._fromMatch(_Match m) | |
337 : this(m.urlMatch.tail, m.urlMatch.parameters, m.route); | |
338 | |
339 /** | |
340 * Can be called on enter with the future which will complete with a boolean | |
341 * value allowing ([:true:]) or disallowing ([:false:]) the current | |
342 * navigation. | |
343 */ | |
344 void allowEnter(Future<bool> allow) { | |
345 _allowEnterFutures.add(allow); | |
346 } | |
347 } | |
348 | |
349 class RouteEnterEvent extends RouteEvent { | |
350 | |
351 RouteEnterEvent(path, parameters, route) : super(path, parameters, route); | |
352 | |
353 RouteEnterEvent._fromMatch(_Match m) | |
354 : this(m.urlMatch.match, m.urlMatch.parameters, m.route); | |
355 } | |
356 | |
357 class RouteLeaveEvent extends RouteEvent { | |
358 final _allowLeaveFutures = <Future<bool>>[]; | |
359 | |
360 RouteLeaveEvent(path, parameters, route) : super(path, parameters, route); | |
361 | |
362 /** | |
363 * Can be called on enter with the future which will complete with a boolean | |
364 * value allowing ([:true:]) or disallowing ([:false:]) the current | |
365 * navigation. | |
366 */ | |
367 void allowLeave(Future<bool> allow) { | |
368 _allowLeaveFutures.add(allow); | |
369 } | |
370 | |
371 RouteLeaveEvent _clone() => new RouteLeaveEvent(path, parameters, route); | |
372 } | |
373 | |
374 /** | |
375 * Event emitted when routing starts. | |
376 */ | |
377 class RouteStartEvent { | |
378 /** | |
379 * URI that was passed to [Router.route]. | |
380 */ | |
381 final String uri; | |
382 | |
383 /** | |
384 * Future that completes to a boolean value of whether the routing was | |
385 * successful. | |
386 */ | |
387 final Future<bool> completed; | |
388 | |
389 RouteStartEvent._new(this.uri, this.completed); | |
390 } | |
391 | |
392 abstract class Routable { | |
393 void configureRoute(Route router); | |
394 } | |
395 | |
396 /** | |
397 * Stores a set of [UrlPattern] to [Handler] associations and provides methods | |
398 * for calling a handler for a URL path, listening to [Window] history events, | |
399 * and creating HTML event handlers that navigate to a URL. | |
400 */ | |
401 class Router { | |
402 final bool _useFragment; | |
403 final Window _window; | |
404 final Route root; | |
405 final _onRouteStart = | |
406 new StreamController<RouteStartEvent>.broadcast(sync: true); | |
407 final bool sortRoutes; | |
408 bool _listen = false; | |
409 | |
410 /** | |
411 * [useFragment] determines whether this Router uses pure paths with | |
412 * [History.pushState] or paths + fragments and [Location.assign]. The default | |
413 * value is null which then determines the behavior based on | |
414 * [History.supportsState]. | |
415 */ | |
416 Router({bool useFragment, Window windowImpl, bool sortRoutes: true}) | |
417 : this._init(null, useFragment: useFragment, windowImpl: windowImpl, | |
418 sortRoutes: sortRoutes); | |
419 | |
420 | |
421 Router._init(Router parent, {bool useFragment, Window windowImpl, | |
422 this.sortRoutes}) | |
423 : _useFragment = (useFragment == null) | |
424 ? !History.supportsState | |
425 : useFragment, | |
426 _window = (windowImpl == null) ? window : windowImpl, | |
427 root = new RouteImpl._new(); | |
428 | |
429 /** | |
430 * A stream of route calls. | |
431 */ | |
432 Stream<RouteStartEvent> get onRouteStart => _onRouteStart.stream; | |
433 | |
434 /** | |
435 * Finds a matching [Route] added with [addRoute], parses the path | |
436 * and invokes the associated callback. | |
437 * | |
438 * This method does not perform any navigation, [go] should be used for that. | |
439 * This method is used to invoke a handler after some other code navigates the | |
440 * window, such as [listen]. | |
441 */ | |
442 Future<bool> route(String path, {Route startingFrom}) { | |
443 var future = _route(path, startingFrom); | |
444 _onRouteStart.add(new RouteStartEvent._new(path, future)); | |
445 return future; | |
446 } | |
447 | |
448 Future<bool> _route(String path, Route startingFrom) { | |
449 var baseRoute = startingFrom == null ? root : _dehandle(startingFrom); | |
450 _logger.finest('route $path $baseRoute'); | |
451 var treePath = _matchingTreePath(path, baseRoute); | |
452 var cmpBase = baseRoute; | |
453 var tail = path; | |
454 // Skip all routes that are unaffected by this path. | |
455 treePath = treePath.skipWhile((_Match matchedRoute) { | |
456 var skip = cmpBase._currentRoute == matchedRoute.route && | |
457 !_paramsChanged(cmpBase, matchedRoute.urlMatch); | |
458 if (skip) { | |
459 cmpBase = matchedRoute.route; | |
460 tail = matchedRoute.urlMatch.tail; | |
461 } | |
462 return skip; | |
463 }).toList(); | |
464 | |
465 if (treePath.isEmpty) return new Future.value(true); | |
466 | |
467 var preEnterFutures = _preEnter(tail, treePath); | |
468 | |
469 return Future.wait(preEnterFutures).then((List<bool> results) { | |
470 return results.any((v) => v == false) | |
471 ? false | |
472 : _processNewRoute(cmpBase, treePath, tail); | |
473 }); | |
474 } | |
475 | |
476 List<Future<bool>> _preEnter(String tail, List<_Match> treePath) { | |
477 var preEnterFutures = <Future<bool>>[]; | |
478 treePath.forEach((_Match matchedRoute) { | |
479 var preEnterEvent = new RoutePreEnterEvent._fromMatch(matchedRoute); | |
480 matchedRoute.route._onPreEnterController.add(preEnterEvent); | |
481 preEnterFutures.addAll(preEnterEvent._allowEnterFutures); | |
482 }); | |
483 return preEnterFutures; | |
484 } | |
485 | |
486 Future<bool> _processNewRoute(Route startingFrom, List<_Match> treePath, | |
487 String path) { | |
488 return _leaveOldRoutes(startingFrom, treePath).then((bool allowed) { | |
489 if (allowed) { | |
490 var base = startingFrom; | |
491 treePath.forEach((_Match matchedRoute) { | |
492 var event = new RouteEnterEvent._fromMatch(matchedRoute); | |
493 _unsetAllCurrentRoutes(base); | |
494 base._currentRoute = matchedRoute.route; | |
495 base._currentRoute._lastEvent = event; | |
496 matchedRoute.route._onEnterController.add(event); | |
497 base = matchedRoute.route; | |
498 }); | |
499 return true; | |
500 } | |
501 return false; | |
502 }); | |
503 } | |
504 | |
505 Future<bool> _leaveOldRoutes(RouteImpl startingFrom, List<_Match> treePath) { | |
506 if (treePath.isEmpty) return new Future.value(true); | |
507 | |
508 var currentRoute = startingFrom._currentRoute; | |
509 if (currentRoute != null && | |
510 currentRoute.dontLeaveOnParamChanges && | |
511 identical(currentRoute, treePath.last.route)) { | |
512 return new Future.value(true); | |
513 } | |
514 | |
515 var event = new RouteLeaveEvent('', {}, startingFrom); | |
516 return _leaveCurrentRoute(startingFrom, event); | |
517 } | |
518 | |
519 List _matchingRoutes(String path, RouteImpl baseRoute) { | |
520 var routes = baseRoute._routes.values.toList(); | |
521 if (sortRoutes) { | |
522 routes.sort((r1, r2) => r1.path.compareTo(r2.path)); | |
523 } | |
524 return routes.where((r) => r.path.match(path) != null).toList(); | |
525 } | |
526 | |
527 List<_Match> _matchingTreePath(String path, RouteImpl baseRoute) { | |
528 final treePath = <_Match>[]; | |
529 Route matchedRoute; | |
530 do { | |
531 matchedRoute = null; | |
532 List matchingRoutes = _matchingRoutes(path, baseRoute); | |
533 if (matchingRoutes.isNotEmpty) { | |
534 if (matchingRoutes.length > 1) { | |
535 _logger.warning("More than one route matches $path $matchingRoutes"); | |
536 } | |
537 matchedRoute = matchingRoutes.first; | |
538 } else { | |
539 if (baseRoute._defaultRoute != null) { | |
540 matchedRoute = baseRoute._defaultRoute; | |
541 } | |
542 } | |
543 if (matchedRoute != null) { | |
544 var match = _getMatch(matchedRoute, path); | |
545 treePath.add(new _Match(matchedRoute, match)); | |
546 baseRoute = matchedRoute; | |
547 path = match.tail; | |
548 } | |
549 } while (matchedRoute != null); | |
550 return treePath; | |
551 } | |
552 | |
553 bool _paramsChanged(RouteImpl baseRoute, UrlMatch match) { | |
554 var lastEvent = baseRoute._currentRoute._lastEvent; | |
555 return lastEvent.path != match.match || | |
556 !mapsShallowEqual(lastEvent.parameters, match.parameters); | |
557 } | |
558 | |
559 /// Navigates to a given relative route path, and parameters. | |
560 Future go(String routePath, Map parameters, | |
561 {Route startingFrom, bool replace: false}) { | |
562 var queryParams = {}; | |
563 var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom); | |
564 var newTail = baseRoute._getTailUrl(routePath, parameters, queryParams) + | |
565 _buildQuery(queryParams); | |
566 String newUrl = baseRoute._getHead(newTail, queryParams); | |
567 _logger.finest('go $newUrl'); | |
568 return route(newTail, startingFrom: baseRoute).then((success) { | |
569 if (success) _go(newUrl, null, replace); | |
570 return success; | |
571 }); | |
572 } | |
573 | |
574 /// Returns an absolute URL for a given relative route path and parameters. | |
575 String url(String routePath, {Route startingFrom, Map parameters}) { | |
576 var baseRoute = startingFrom == null ? this.root : _dehandle(startingFrom); | |
577 parameters = parameters == null ? {} : parameters; | |
578 var queryParams = {}; | |
579 var tail = baseRoute._getTailUrl(routePath, parameters, queryParams); | |
580 return (_useFragment ? '#' : '') + baseRoute._getHead(tail, queryParams) + | |
581 _buildQuery(queryParams); | |
582 } | |
583 | |
584 String _buildQuery(Map queryParams) { | |
585 if (queryParams.isEmpty) return ''; | |
586 var query = queryParams.keys.map((key) => | |
587 '$key=${Uri.encodeComponent(queryParams[key])}').join('&'); | |
588 return '?$query'; | |
589 } | |
590 | |
591 Route _dehandle(Route r) => r is RouteHandle ? r._getHost(r): r; | |
592 | |
593 UrlMatch _getMatch(Route route, String path) { | |
594 var match = route.path.match(path); | |
595 // default route | |
596 if (match == null) return new UrlMatch('', '', {}); | |
597 match.parameters.addAll(_parseQuery(route, path)); | |
598 return match; | |
599 } | |
600 | |
601 Map _parseQuery(Route route, String path) { | |
602 var params = {}; | |
603 if (path.indexOf('?') == -1) return params; | |
604 var queryStr = path.substring(path.indexOf('?') + 1); | |
605 queryStr.split('&').forEach((String keyValPair) { | |
606 List<String> keyVal = _parseKeyVal(keyValPair); | |
607 if (keyVal[0].startsWith('${route.name}.')) { | |
608 var key = keyVal[0].substring('${route.name}.'.length); | |
609 if (key.isNotEmpty) params[key] = Uri.decodeComponent(keyVal[1]); | |
610 } | |
611 }); | |
612 return params; | |
613 } | |
614 | |
615 List<String> _parseKeyVal(keyValPair) { | |
616 if (keyValPair.isEmpty) return const ['', '']; | |
617 var splitPoint = keyValPair.indexOf('=') == -1 ? | |
618 keyValPair.length : keyValPair.indexOf('=') + 1; | |
619 var key = keyValPair.substring(0, splitPoint + | |
620 (keyValPair.indexOf('=') == -1 ? 0 : -1)); | |
621 var value = keyValPair.substring(splitPoint); | |
622 return [key, value]; | |
623 } | |
624 | |
625 void _unsetAllCurrentRoutes(RouteImpl r) { | |
626 if (r._currentRoute != null) { | |
627 _unsetAllCurrentRoutes(r._currentRoute); | |
628 r._currentRoute = null; | |
629 } | |
630 } | |
631 | |
632 Future<bool> _leaveCurrentRoute(RouteImpl base, RouteLeaveEvent e) => | |
633 Future | |
634 .wait(_leaveCurrentRouteHelper(base, e)) | |
635 .then((values) => values.fold(true, (c, v) => c && v)); | |
636 | |
637 List<Future<bool>> _leaveCurrentRouteHelper(RouteImpl base, RouteLeaveEvent e)
{ | |
638 var futures = []; | |
639 if (base._currentRoute != null) { | |
640 List<Future<bool>> pendingResponses = <Future<bool>>[]; | |
641 // We create a copy of the route event | |
642 var event = e._clone(); | |
643 base._currentRoute._onLeaveController.add(event); | |
644 futures..addAll(event._allowLeaveFutures) | |
645 ..addAll(_leaveCurrentRouteHelper(base._currentRoute, event)); | |
646 } | |
647 return futures; | |
648 } | |
649 | |
650 /** | |
651 * Listens for window history events and invokes the router. On older | |
652 * browsers the hashChange event is used instead. | |
653 */ | |
654 void listen({bool ignoreClick: false, Element appRoot}) { | |
655 _logger.finest('listen ignoreClick=$ignoreClick'); | |
656 if (_listen) throw new StateError('listen can only be called once'); | |
657 _listen = true; | |
658 if (_useFragment) { | |
659 _window.onHashChange.listen((_) { | |
660 route(_normalizeHash(_window.location.hash)).then((allowed) { | |
661 // if not allowed, we need to restore the browser location | |
662 if (!allowed) _window.history.back(); | |
663 }); | |
664 }); | |
665 route(_normalizeHash(_window.location.hash)); | |
666 } else { | |
667 String getPath() => | |
668 '${_window.location.pathname}${_window.location.hash}'; | |
669 | |
670 _window.onPopState.listen((_) { | |
671 route(getPath()).then((allowed) { | |
672 // if not allowed, we need to restore the browser location | |
673 if (!allowed) _window.history.back(); | |
674 }); | |
675 }); | |
676 route(getPath()); | |
677 } | |
678 if (!ignoreClick) { | |
679 if (appRoot == null) appRoot = _window.document.documentElement; | |
680 _logger.finest('listen on win'); | |
681 appRoot.onClick | |
682 .where((MouseEvent e) => !(e.ctrlKey || e.metaKey || e.shiftKey)) | |
683 .where((MouseEvent e) => e.target is AnchorElement) | |
684 .listen((MouseEvent e) { | |
685 AnchorElement anchor = e.target; | |
686 if (anchor.host == _window.location.host) { | |
687 _logger.finest('clicked ${anchor.pathname}${anchor.hash}'); | |
688 e.preventDefault(); | |
689 var path = _useFragment | |
690 ? _normalizeHash(anchor.hash) | |
691 : '${anchor.pathname}'; | |
692 route(path).then((allowed) { | |
693 if (allowed) _go(path, null, false); | |
694 }); | |
695 } | |
696 }); | |
697 } | |
698 } | |
699 | |
700 String _normalizeHash(String hash) => hash.isEmpty ? '' : hash.substring(1); | |
701 | |
702 /** | |
703 * Navigates the browser to the path produced by [url] with [args] by calling | |
704 * [History.pushState], then invokes the handler associated with [url]. | |
705 * | |
706 * On older browsers [Location.assign] is used instead with the fragment | |
707 * version of the UrlPattern. | |
708 */ | |
709 Future<bool> gotoUrl(String url) => | |
710 route(url).then((success) { | |
711 if (success) _go(url, null, false); | |
712 }); | |
713 | |
714 void _go(String path, String title, bool replace) { | |
715 if (title == null) title = ''; | |
716 if (_useFragment) { | |
717 if (replace) { | |
718 _window.location.replace('#$path'); | |
719 } else { | |
720 _window.location.assign('#$path'); | |
721 } | |
722 (_window.document as HtmlDocument).title = title; | |
723 } else { | |
724 if (replace) { | |
725 _window.history.replaceState(null, title, path); | |
726 } else { | |
727 _window.history.pushState(null, title, path); | |
728 } | |
729 } | |
730 } | |
731 | |
732 /** | |
733 * Returns the current active route path in the route tree. | |
734 * Excludes the root path. | |
735 */ | |
736 List<Route> get activePath { | |
737 var res = <RouteImpl>[]; | |
738 var route = root; | |
739 while (route._currentRoute != null) { | |
740 route = route._currentRoute; | |
741 res.add(route); | |
742 } | |
743 return res; | |
744 } | |
745 | |
746 /** | |
747 * A shortcut for router.root.findRoute(). | |
748 */ | |
749 Route findRoute(String routePath) => root.findRoute(routePath); | |
750 } | |
751 | |
752 class _Match { | |
753 final RouteImpl route; | |
754 final UrlMatch urlMatch; | |
755 | |
756 _Match(this.route, this.urlMatch); | |
757 } | |
OLD | NEW |