2024-09-23 01:46:14 +00:00
|
|
|
import 'dart:async' show Stream, StreamController;
|
|
|
|
import 'dart:html';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
|
2024-09-25 04:04:57 +00:00
|
|
|
import 'route.dart';
|
2024-09-23 01:46:14 +00:00
|
|
|
|
|
|
|
final RegExp _hash = RegExp(r'^#/');
|
|
|
|
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
|
|
|
|
|
|
|
/// A variation of the [Router] support both hash routing and push state.
|
|
|
|
abstract class BrowserRouter<T> extends Router<T> {
|
|
|
|
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
|
|
|
Stream<RoutingResult<T?>> get onResolve;
|
|
|
|
|
|
|
|
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
|
|
|
Stream<Route<T?>> get onRoute;
|
|
|
|
|
|
|
|
/// Set `hash` to true to use hash routing instead of push state.
|
|
|
|
/// `listen` as `true` will call `listen` after initialization.
|
|
|
|
factory BrowserRouter({bool hash = false, bool listen = false}) {
|
|
|
|
return hash
|
|
|
|
? _HashRouter<T>(listen: listen)
|
|
|
|
: _PushStateRouter<T>(listen: listen);
|
|
|
|
}
|
|
|
|
|
|
|
|
//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);
|
|
|
|
|
|
|
|
// Handles a route path, manually.
|
|
|
|
// void handle(String path);
|
|
|
|
|
|
|
|
/// Begins listen for location changes.
|
|
|
|
void listen();
|
|
|
|
|
|
|
|
/// Identical to [all].
|
|
|
|
Route on(String path, T handler, {Iterable<T> middleware});
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class _BrowserRouterImpl<T> extends Router<T>
|
|
|
|
implements BrowserRouter<T> {
|
|
|
|
bool _listening = false;
|
|
|
|
Route? _current;
|
|
|
|
final StreamController<RoutingResult<T?>> _onResolve =
|
|
|
|
StreamController<RoutingResult<T?>>();
|
|
|
|
final StreamController<Route<T?>> _onRoute = StreamController<Route<T?>>();
|
|
|
|
|
|
|
|
Route? get currentRoute => _current;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<RoutingResult<T?>> get onResolve => _onResolve.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<Route<T?>> get onRoute => _onRoute.stream;
|
|
|
|
|
|
|
|
_BrowserRouterImpl({bool listen = false}) : super() {
|
|
|
|
if (listen != false) this.listen();
|
|
|
|
prepareAnchors();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void go(Iterable linkParams) => _goTo(navigate(linkParams));
|
|
|
|
|
|
|
|
@override
|
|
|
|
Route on(String path, T handler, {Iterable<T> middleware = const []}) =>
|
|
|
|
all(path, handler, middleware: middleware);
|
|
|
|
|
|
|
|
void prepareAnchors() {
|
|
|
|
final anchors = window.document
|
|
|
|
.querySelectorAll('a')
|
|
|
|
.cast<AnchorElement>(); //:not([dynamic])');
|
|
|
|
|
|
|
|
for (final $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();
|
|
|
|
_goTo($a.attributes['href']!);
|
|
|
|
//go($a.attributes['href'].split('/').where((str) => str.isNotEmpty));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
$a.attributes['dynamic'] = 'true';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _listen();
|
|
|
|
|
|
|
|
@override
|
|
|
|
void listen() {
|
|
|
|
if (_listening) {
|
|
|
|
throw StateError('The router is already listening for page changes.');
|
|
|
|
}
|
|
|
|
_listening = true;
|
|
|
|
_listen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _HashRouter<T> extends _BrowserRouterImpl<T> {
|
|
|
|
_HashRouter({required bool listen}) : super(listen: listen) {
|
|
|
|
if (listen) {
|
|
|
|
this.listen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void _goTo(String uri) {
|
|
|
|
window.location.hash = '#$uri';
|
|
|
|
}
|
|
|
|
|
|
|
|
void handleHash([_]) {
|
|
|
|
final path = window.location.hash.replaceAll(_hash, '');
|
|
|
|
var allResolved = resolveAbsolute(path);
|
|
|
|
|
|
|
|
if (allResolved.isEmpty) {
|
|
|
|
// TODO: Need fixing
|
|
|
|
//_onResolve.add(null);
|
|
|
|
//_onRoute.add(_current = null);
|
|
|
|
_current = null;
|
|
|
|
} else {
|
|
|
|
var resolved = allResolved.first;
|
|
|
|
if (resolved.route != _current) {
|
|
|
|
_onResolve.add(resolved);
|
|
|
|
_onRoute.add(_current = resolved.route);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void handlePath(String path) {
|
|
|
|
final resolved = resolveAbsolute(path).first;
|
|
|
|
|
|
|
|
//if (resolved == null) {
|
|
|
|
// _onResolve.add(null);
|
|
|
|
// _onRoute.add(_current = null);
|
|
|
|
//} else
|
|
|
|
if (resolved.route != _current) {
|
|
|
|
_onResolve.add(resolved);
|
|
|
|
_onRoute.add(_current = resolved.route);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void _listen() {
|
|
|
|
window.onHashChange.listen(handleHash);
|
|
|
|
handleHash();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _PushStateRouter<T> extends _BrowserRouterImpl<T> {
|
|
|
|
late String _basePath;
|
|
|
|
|
|
|
|
_PushStateRouter({required bool listen}) : super(listen: listen) {
|
|
|
|
var $base = window.document.querySelector('base[href]') as BaseElement;
|
|
|
|
|
|
|
|
if ($base.href.isNotEmpty != true) {
|
|
|
|
throw StateError(
|
|
|
|
'You must have a <base href="<base-url-here>"> element present in your document to run the push state router.');
|
|
|
|
}
|
|
|
|
_basePath = $base.href.replaceAll(_straySlashes, '');
|
|
|
|
if (listen) this.listen();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void _goTo(String uri) {
|
|
|
|
final resolved = resolveAbsolute(uri).first;
|
|
|
|
var relativeUri = uri;
|
|
|
|
|
|
|
|
if (_basePath.isNotEmpty) {
|
|
|
|
relativeUri = p.join(_basePath, uri.replaceAll(_straySlashes, ''));
|
|
|
|
}
|
|
|
|
|
|
|
|
//if (resolved == null) {
|
|
|
|
// _onResolve.add(null);
|
|
|
|
// _onRoute.add(_current = null);
|
|
|
|
//} else {
|
|
|
|
final route = resolved.route;
|
|
|
|
var thisPath = route.name ?? '';
|
|
|
|
if (thisPath.isEmpty) {
|
|
|
|
thisPath = route.path;
|
|
|
|
}
|
|
|
|
window.history
|
|
|
|
.pushState({'path': route.path, 'params': {}}, thisPath, relativeUri);
|
|
|
|
_onResolve.add(resolved);
|
|
|
|
_onRoute.add(_current = route);
|
|
|
|
//}
|
|
|
|
}
|
|
|
|
|
|
|
|
void handleState(state) {
|
|
|
|
if (state is Map && state.containsKey('path')) {
|
|
|
|
var path = state['path'].toString();
|
|
|
|
final resolved = resolveAbsolute(path).first;
|
|
|
|
|
|
|
|
if (resolved.route != _current) {
|
|
|
|
//properties.addAll(state['properties'] ?? {});
|
|
|
|
_onResolve.add(resolved);
|
|
|
|
_onRoute.add(_current = resolved.route);
|
|
|
|
} else {
|
|
|
|
//_onResolve.add(null);
|
|
|
|
//_onRoute.add(_current = null);
|
|
|
|
_current = null;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
//_onResolve.add(null);
|
|
|
|
//_onRoute.add(_current = null);
|
|
|
|
_current = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void _listen() {
|
|
|
|
window.onPopState.listen((e) {
|
|
|
|
handleState(e.state);
|
|
|
|
});
|
|
|
|
|
|
|
|
handleState(window.history.state);
|
|
|
|
}
|
|
|
|
}
|