blob: 0ef349d70981cd3ea739618b1f7dbe2e04abc685 [file] [log] [blame]
// Copyright 2014 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.
import 'dart:async';
import 'dart:html' as html;
import 'dart:ui' as ui;
import '../navigation_common/url_strategy.dart';
import 'js_url_strategy.dart';
import 'utils.dart';
/// Saves the current [UrlStrategy] to be accessed by [urlStrategy] or
/// [setUrlStrategy].
///
/// This is particularly required for web plugins relying on valid URL
/// encoding.
//
// Keep this in sync with the default url strategy in the web engine.
// Find it at:
// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/window.dart#L360
//
UrlStrategy? _urlStrategy = const HashUrlStrategy();
/// Returns the present [UrlStrategy] for handling the browser URL.
///
/// In case null is returned, the browser integration has been manually
/// disabled by [setUrlStrategy].
UrlStrategy? get urlStrategy => _urlStrategy;
/// Change the strategy to use for handling browser URL.
///
/// Setting this to null disables all integration with the browser history.
void setUrlStrategy(UrlStrategy? strategy) {
_urlStrategy = strategy;
JsUrlStrategy? jsUrlStrategy;
if (strategy != null) {
jsUrlStrategy = convertToJsUrlStrategy(strategy);
}
jsSetUrlStrategy(jsUrlStrategy);
}
/// Use the [PathUrlStrategy] to handle the browser URL.
void usePathUrlStrategy() {
setUrlStrategy(PathUrlStrategy());
}
/// Uses the browser URL's [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax)
/// to represent its state.
///
/// By default, this class is used as the URL strategy for the app. However,
/// this class is still useful for apps that want to extend it.
///
/// In order to use [HashUrlStrategy] for an app, it needs to be set like this:
///
/// ```dart
/// import 'package:flutter_web_plugins/flutter_web_plugins.dart';
///
/// // Somewhere before calling `runApp()` do:
/// setUrlStrategy(const HashUrlStrategy());
/// ```
class HashUrlStrategy extends UrlStrategy {
/// Creates an instance of [HashUrlStrategy].
///
/// The [PlatformLocation] parameter is useful for testing to mock out browser
/// interactions.
const HashUrlStrategy(
[this._platformLocation = const BrowserPlatformLocation()]);
final PlatformLocation _platformLocation;
@override
ui.VoidCallback addPopStateListener(EventListener fn) {
_platformLocation.addPopStateListener(fn);
return () => _platformLocation.removePopStateListener(fn);
}
@override
String getPath() {
// the hash value is always prefixed with a `#`
// and if it is empty then it will stay empty
final 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
Object? getState() => _platformLocation.state;
@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(Object? state, String title, String url) {
_platformLocation.pushState(state, title, prepareExternalUrl(url));
}
@override
void replaceState(Object? state, String title, String url) {
_platformLocation.replaceState(state, title, prepareExternalUrl(url));
}
@override
Future<void> go(int count) {
_platformLocation.go(count);
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>();
late ui.VoidCallback unsubscribe;
unsubscribe = addPopStateListener((_) {
unsubscribe();
completer.complete();
});
return completer.future;
}
}
/// Uses the browser URL's pathname to represent Flutter's route name.
///
/// In order to use [PathUrlStrategy] for an app, it needs to be set like this:
///
/// ```dart
/// import 'package:flutter_web_plugins/flutter_web_plugins.dart';
///
/// // Somewhere before calling `runApp()` do:
/// setUrlStrategy(PathUrlStrategy());
/// ```
class PathUrlStrategy extends HashUrlStrategy {
/// Creates an instance of [PathUrlStrategy].
///
/// The [PlatformLocation] parameter is useful for testing to mock out browser
/// interactions.
PathUrlStrategy([
super.platformLocation,
]) : _basePath = stripTrailingSlash(extractPathname(checkBaseHref(
platformLocation.getBaseHref(),
)));
final String _basePath;
@override
String getPath() {
final String path = _platformLocation.pathname + _platformLocation.search;
if (_basePath.isNotEmpty && path.startsWith(_basePath)) {
return ensureLeadingSlash(path.substring(_basePath.length));
}
return ensureLeadingSlash(path);
}
@override
String prepareExternalUrl(String internalUrl) {
if (internalUrl.isNotEmpty && !internalUrl.startsWith('/')) {
internalUrl = '/$internalUrl';
}
return '$_basePath$internalUrl';
}
}
/// Delegates to real browser APIs to provide platform location functionality.
class BrowserPlatformLocation extends PlatformLocation {
/// Default constructor for [BrowserPlatformLocation].
const BrowserPlatformLocation();
// Default value for [pathname] when it's not set in window.location.
// According to MDN this should be ''. Chrome seems to return '/'.
static const String _defaultPathname = '';
// Default value for [search] when it's not set in window.location.
// According to both chrome, and the MDN, this is ''.
static const String _defaultSearch = '';
html.Location get _location => html.window.location;
html.History get _history => html.window.history;
@override
void addPopStateListener(html.EventListener fn) {
html.window.addEventListener('popstate', fn);
}
@override
void removePopStateListener(html.EventListener fn) {
html.window.removeEventListener('popstate', fn);
}
@override
String get pathname => _location.pathname ?? _defaultPathname;
@override
String get search => _location.search ?? _defaultSearch;
@override
String get hash => _location.hash;
@override
Object? get state => _history.state;
@override
void pushState(Object? state, String title, String url) {
_history.pushState(state, title, url);
}
@override
void replaceState(Object? state, String title, String url) {
_history.replaceState(state, title, url);
}
@override
void go(int count) {
_history.go(count);
}
@override
String? getBaseHref() => getBaseElementHrefFromDom();
}