blob: 854bf9ec6f5e01307603e9d2335ac9ca8df0a711 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.6
part of engine;
// TODO(mdebbar): add other strategies.
// Some parts of this file were inspired/copied from the AngularDart router.
/// [LocationStrategy] is responsible for representing and reading route state
/// from the browser's URL.
///
/// At the moment, only one strategy is implemented: [HashLocationStrategy].
///
/// This is used by [BrowserHistory] to interact with browser history APIs.
abstract class LocationStrategy {
const LocationStrategy();
/// Subscribes to popstate events and returns a function that could be used to
/// unsubscribe from popstate events.
ui.VoidCallback onPopState(html.EventListener fn);
/// The active path in the browser history.
String get path;
/// Given a path that's internal to the app, create the external url that
/// will be used in the browser.
String prepareExternalUrl(String internalUrl);
/// Push a new history entry.
void pushState(dynamic state, String title, String url);
/// Replace the currently active history entry.
void replaceState(dynamic state, String title, String url);
/// Go to the previous history entry.
Future<void> back();
}
/// This is an implementation of [LocationStrategy] that uses the browser URL's
/// [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax)
/// to represent its state.
///
/// In order to use this [LocationStrategy] for an app, it needs to be set in
/// [ui.window.locationStrategy]:
///
/// ```dart
/// import 'package:flutter_web/material.dart';
/// import 'package:flutter_web/ui.dart' as ui;
///
/// void main() {
/// ui.window.locationStrategy = const ui.HashLocationStrategy();
/// runApp(MyApp());
/// }
/// ```
class HashLocationStrategy extends LocationStrategy {
final PlatformLocation _platformLocation;
const HashLocationStrategy(
[this._platformLocation = const BrowserPlatformLocation()]);
@override
ui.VoidCallback onPopState(html.EventListener fn) {
_platformLocation.onPopState(fn);
return () => _platformLocation.offPopState(fn);
}
@override
String get path {
// the hash value is always prefixed with a `#`
// and if it is empty then it will stay empty
String path = _platformLocation.hash ?? '';
assert(path.isEmpty || path.startsWith('#'));
// We don't want to return an empty string as a path. Instead we default to "/".
if (path.isEmpty || path == '#') {
return '/';
}
// At this point, we know [path] starts with "#" and isn't empty.
return path.substring(1);
}
@override
String prepareExternalUrl(String internalUrl) {
// It's convention that if the hash path is empty, we omit the `#`; however,
// if the empty URL is pushed it won't replace any existing fragment. So
// when the hash path is empty, we instead return the location's path and
// query.
return internalUrl.isEmpty
? '${_platformLocation.pathname}${_platformLocation.search}'
: '#$internalUrl';
}
@override
void pushState(dynamic state, String title, String url) {
_platformLocation.pushState(state, title, prepareExternalUrl(url));
}
@override
void replaceState(dynamic state, String title, String url) {
_platformLocation.replaceState(state, title, prepareExternalUrl(url));
}
@override
Future<void> back() {
_platformLocation.back();
return _waitForPopState();
}
/// Waits until the next popstate event is fired.
///
/// This is useful for example to wait until the browser has handled the
/// `history.back` transition.
Future<void> _waitForPopState() {
final Completer<void> completer = Completer<void>();
ui.VoidCallback unsubscribe;
unsubscribe = onPopState((_) {
unsubscribe();
completer.complete();
});
return completer.future;
}
}
/// [PlatformLocation] encapsulates all calls to DOM apis, which allows the
/// [LocationStrategy] classes to be platform agnostic and testable.
///
/// The [PlatformLocation] class is used directly by all implementations of
/// [LocationStrategy] when they need to interact with the DOM apis like
/// pushState, popState, etc...
abstract class PlatformLocation {
const PlatformLocation();
void onPopState(html.EventListener fn);
void offPopState(html.EventListener fn);
void onHashChange(html.EventListener fn);
void offHashChange(html.EventListener fn);
String get pathname;
String get search;
String get hash;
void pushState(dynamic state, String title, String url);
void replaceState(dynamic state, String title, String url);
void back();
}
/// An implementation of [PlatformLocation] for the browser.
class BrowserPlatformLocation extends PlatformLocation {
html.Location get _location => html.window.location;
html.History get _history => html.window.history;
const BrowserPlatformLocation();
@override
void onPopState(html.EventListener fn) {
html.window.addEventListener('popstate', fn);
}
@override
void offPopState(html.EventListener fn) {
html.window.removeEventListener('popstate', fn);
}
@override
void onHashChange(html.EventListener fn) {
html.window.addEventListener('hashchange', fn);
}
@override
void offHashChange(html.EventListener fn) {
html.window.removeEventListener('hashchange', fn);
}
@override
String get pathname => _location.pathname;
@override
String get search => _location.search;
@override
String get hash => _location.hash;
@override
void pushState(dynamic state, String title, String url) {
_history.pushState(state, title, url);
}
@override
void replaceState(dynamic state, String title, String url) {
_history.replaceState(state, title, url);
}
@override
void back() {
_history.back();
}
}