| 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: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 } |
| OLD | NEW |