platform/packages/route/lib/browser.dart

214 lines
5.8 KiB
Dart
Raw Normal View History

2016-11-27 22:24:30 +00:00
import 'dart:async' show Stream, StreamController;
2017-03-20 19:46:54 +00:00
import 'dart:html';
2018-08-20 19:18:43 +00:00
2017-03-20 19:46:54 +00:00
import 'package:path/path.dart' as p;
2016-11-27 22:24:30 +00:00
2018-08-20 19:18:43 +00:00
import 'angel_route.dart';
2019-11-28 17:40:32 +00:00
final RegExp _hash = RegExp(r'^#/');
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
2016-11-27 22:24:30 +00:00
/// A variation of the [Router] support both hash routing and push state.
2018-08-20 19:18:43 +00:00
abstract class BrowserRouter<T> extends Router<T> {
2016-11-27 22:24:30 +00:00
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
2018-08-20 19:18:43 +00:00
Stream<RoutingResult<T>> get onResolve;
2016-11-27 22:24:30 +00:00
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
2018-08-20 19:18:43 +00:00
Stream<Route<T>> get onRoute;
2016-11-27 22:24:30 +00:00
/// Set `hash` to true to use hash routing instead of push state.
/// `listen` as `true` will call `listen` after initialization.
2019-04-11 15:51:38 +00:00
factory BrowserRouter({bool hash = false, bool listen = false}) {
2016-11-27 22:24:30 +00:00
return hash
2019-11-28 17:40:32 +00:00
? _HashRouter<T>(listen: listen)
: _PushStateRouter<T>(listen: listen);
2016-11-27 22:24:30 +00:00
}
BrowserRouter._() : super();
void _goTo(String path);
/// Navigates to the path generated by calling
/// [navigate] with the given [linkParams].
///
/// This always navigates to an absolute path.
void go(List linkParams);
2017-03-20 19:46:54 +00:00
// Handles a route path, manually.
// void handle(String path);
2016-11-27 22:24:30 +00:00
/// Begins listen for location changes.
void listen();
/// Identical to [all].
2018-08-20 19:18:43 +00:00
Route on(String path, T handler, {Iterable<T> middleware});
2016-11-27 22:24:30 +00:00
}
2018-08-20 19:18:43 +00:00
abstract class _BrowserRouterImpl<T> extends Router<T>
implements BrowserRouter<T> {
2017-11-18 05:29:08 +00:00
bool _listening = false;
2016-11-27 22:24:30 +00:00
Route _current;
2018-08-20 19:18:43 +00:00
StreamController<RoutingResult<T>> _onResolve =
2019-11-28 17:40:32 +00:00
StreamController<RoutingResult<T>>();
StreamController<Route<T>> _onRoute = StreamController<Route<T>>();
2018-08-20 19:18:43 +00:00
2016-11-27 22:24:30 +00:00
Route get currentRoute => _current;
@override
2018-08-20 19:18:43 +00:00
Stream<RoutingResult<T>> get onResolve => _onResolve.stream;
2016-11-27 22:24:30 +00:00
@override
2018-08-20 19:18:43 +00:00
Stream<Route<T>> get onRoute => _onRoute.stream;
2016-11-27 22:24:30 +00:00
_BrowserRouterImpl({bool listen}) : super() {
2016-12-19 04:43:55 +00:00
if (listen != false) this.listen();
2016-11-27 22:24:30 +00:00
prepareAnchors();
}
@override
2016-12-19 04:43:55 +00:00
void go(Iterable linkParams) => _goTo(navigate(linkParams));
2016-11-27 22:24:30 +00:00
2018-08-20 19:18:43 +00:00
Route on(String path, T handler, {Iterable<T> middleware}) =>
2017-08-03 16:14:21 +00:00
all(path, handler, middleware: middleware);
2016-11-27 22:24:30 +00:00
void prepareAnchors() {
2019-04-11 15:48:01 +00:00
final anchors = window.document
.querySelectorAll('a')
.cast<AnchorElement>(); //:not([dynamic])');
2016-11-27 22:24:30 +00:00
for (final AnchorElement $a in anchors) {
if ($a.attributes.containsKey('href') &&
!$a.attributes.containsKey('download') &&
!$a.attributes.containsKey('target') &&
$a.attributes['rel'] != 'external') {
$a.onClick.listen((e) {
e.preventDefault();
2018-08-20 19:18:43 +00:00
_goTo($a.attributes['href']);
//go($a.attributes['href'].split('/').where((str) => str.isNotEmpty));
2016-11-27 22:24:30 +00:00
});
}
$a.attributes['dynamic'] = 'true';
}
}
2017-11-18 05:29:08 +00:00
void _listen();
@override
void listen() {
2019-11-28 17:40:32 +00:00
if (_listening) {
2019-11-28 17:49:58 +00:00
throw StateError('The router is already listening for page changes.');
}
2017-11-18 05:29:08 +00:00
_listening = true;
_listen();
}
2016-11-27 22:24:30 +00:00
}
2018-08-20 19:18:43 +00:00
class _HashRouter<T> extends _BrowserRouterImpl<T> {
2016-11-27 22:24:30 +00:00
_HashRouter({bool listen}) : super(listen: listen) {
if (listen) this.listen();
}
@override
void _goTo(String uri) {
window.location.hash = '#$uri';
}
2017-03-20 19:46:54 +00:00
void handleHash([_]) {
final path = window.location.hash.replaceAll(_hash, '');
2018-08-20 19:18:43 +00:00
var allResolved = resolveAbsolute(path);
final resolved = allResolved.isEmpty ? null : allResolved.first;
2016-11-27 22:24:30 +00:00
2017-03-20 19:46:54 +00:00
if (resolved == null) {
_onResolve.add(null);
_onRoute.add(_current = null);
} else if (resolved != null && resolved.route != _current) {
_onResolve.add(resolved);
_onRoute.add(_current = resolved.route);
}
}
void handlePath(String path) {
2017-11-25 00:26:06 +00:00
final resolved = resolveAbsolute(path).first;
2017-03-20 19:46:54 +00:00
if (resolved == null) {
_onResolve.add(null);
_onRoute.add(_current = null);
} else if (resolved != null && resolved.route != _current) {
_onResolve.add(resolved);
_onRoute.add(_current = resolved.route);
2017-03-20 19:18:58 +00:00
}
2017-03-20 19:46:54 +00:00
}
2017-03-20 19:18:58 +00:00
2017-03-20 19:46:54 +00:00
@override
2017-11-18 05:29:08 +00:00
void _listen() {
2017-03-20 19:18:58 +00:00
window.onHashChange.listen(handleHash);
handleHash();
2016-11-27 22:24:30 +00:00
}
}
2018-08-20 19:18:43 +00:00
class _PushStateRouter<T> extends _BrowserRouterImpl<T> {
2017-03-20 19:46:54 +00:00
String _basePath;
2016-11-27 22:24:30 +00:00
_PushStateRouter({bool listen, Route root}) : super(listen: listen) {
2017-03-20 19:46:54 +00:00
var $base = window.document.querySelector('base[href]') as BaseElement;
2019-11-28 17:40:32 +00:00
if ($base?.href?.isNotEmpty != true) {
throw StateError(
2017-03-20 19:46:54 +00:00
'You must have a <base href="<base-url-here>"> element present in your document to run the push state router.');
2019-11-28 17:49:58 +00:00
}
_basePath = $base.href.replaceAll(_straySlashes, '');
2016-11-27 22:24:30 +00:00
if (listen) this.listen();
}
@override
void _goTo(String uri) {
2017-11-25 00:26:06 +00:00
final resolved = resolveAbsolute(uri).first;
2017-03-20 19:46:54 +00:00
var relativeUri = uri;
if (_basePath?.isNotEmpty == true) {
relativeUri = p.join(_basePath, uri.replaceAll(_straySlashes, ''));
}
2016-11-27 22:24:30 +00:00
if (resolved == null) {
_onResolve.add(null);
_onRoute.add(_current = null);
} else {
final route = resolved.route;
2018-08-20 19:18:43 +00:00
window.history.pushState({'path': route.path, 'params': {}},
route.name ?? route.path, relativeUri);
2016-11-27 22:24:30 +00:00
_onResolve.add(resolved);
_onRoute.add(_current = route);
}
}
2017-03-20 19:46:54 +00:00
void handleState(state) {
if (state is Map && state.containsKey('path')) {
2018-08-20 18:59:51 +00:00
var path = state['path'].toString();
2017-11-25 00:26:06 +00:00
final resolved = resolveAbsolute(path).first;
2017-03-20 19:46:54 +00:00
if (resolved != null && resolved.route != _current) {
2017-08-03 16:14:21 +00:00
//properties.addAll(state['properties'] ?? {});
2017-03-20 19:46:54 +00:00
_onResolve.add(resolved);
2017-11-25 00:26:06 +00:00
_onRoute.add(_current = resolved.route);
2016-11-27 22:24:30 +00:00
} else {
_onResolve.add(null);
_onRoute.add(_current = null);
}
2017-03-20 19:46:54 +00:00
} else {
_onResolve.add(null);
_onRoute.add(_current = null);
2017-03-20 19:18:58 +00:00
}
2017-03-20 19:46:54 +00:00
}
2017-03-20 19:18:58 +00:00
2017-03-20 19:46:54 +00:00
@override
2017-11-18 05:29:08 +00:00
void _listen() {
2017-03-20 19:18:58 +00:00
window.onPopState.listen((e) {
handleState(e.state);
2016-11-27 22:24:30 +00:00
});
2017-03-20 19:18:58 +00:00
handleState(window.history.state);
2016-11-27 22:24:30 +00:00
}
}