[web] Support custom url strategies (#59797)
This commit is contained in:
parent
4f2fcca6a9
commit
ffcf419136
@ -43,6 +43,7 @@ dependencies:
|
|||||||
http: 0.12.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
http: 0.12.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
intl: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
intl: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
@ -105,7 +106,6 @@ dev_dependencies:
|
|||||||
html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
|
||||||
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
mime: 0.9.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
mime: 0.9.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
@ -28,6 +28,7 @@ dependencies:
|
|||||||
connectivity_macos: 0.1.0+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
connectivity_macos: 0.1.0+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
connectivity_platform_interface: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
connectivity_platform_interface: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
device_info_platform_interface: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
device_info_platform_interface: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
path: 1.8.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
path: 1.8.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
plugin_platform_interface: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
plugin_platform_interface: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
@ -73,7 +74,6 @@ dev_dependencies:
|
|||||||
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
|
||||||
json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
@ -17,5 +17,6 @@
|
|||||||
/// describing how the `url_launcher` package was created using [flutter_web_plugins].
|
/// describing how the `url_launcher` package was created using [flutter_web_plugins].
|
||||||
library flutter_web_plugins;
|
library flutter_web_plugins;
|
||||||
|
|
||||||
|
export 'src/navigation/url_strategy.dart';
|
||||||
export 'src/plugin_event_channel.dart';
|
export 'src/plugin_event_channel.dart';
|
||||||
export 'src/plugin_registry.dart';
|
export 'src/plugin_registry.dart';
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// @dart = 2.8
|
||||||
|
|
||||||
|
@JS()
|
||||||
|
library js_location_strategy;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:js/js.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'url_strategy.dart';
|
||||||
|
|
||||||
|
typedef _JsSetUrlStrategy = void Function(JsUrlStrategy);
|
||||||
|
|
||||||
|
/// A JavaScript hook to customize the URL strategy of a Flutter app.
|
||||||
|
//
|
||||||
|
// Keep this in sync with the JS name in the web engine. Find it at:
|
||||||
|
// https://github.com/flutter/engine/blob/custom_location_strategy/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart
|
||||||
|
//
|
||||||
|
// TODO(mdebbar): Add integration test https://github.com/flutter/flutter/issues/66852
|
||||||
|
@JS('_flutter_web_set_location_strategy')
|
||||||
|
external _JsSetUrlStrategy get jsSetUrlStrategy;
|
||||||
|
|
||||||
|
typedef _PathGetter = String Function();
|
||||||
|
|
||||||
|
typedef _StateGetter = Object Function();
|
||||||
|
|
||||||
|
typedef _AddPopStateListener = ui.VoidCallback Function(html.EventListener);
|
||||||
|
|
||||||
|
typedef _StringToString = String Function(String);
|
||||||
|
|
||||||
|
typedef _StateOperation = void Function(
|
||||||
|
Object state, String title, String url);
|
||||||
|
|
||||||
|
typedef _HistoryMove = Future<void> Function(int count);
|
||||||
|
|
||||||
|
/// Given a Dart implementation of URL strategy, converts it to a JavaScript
|
||||||
|
/// URL strategy to be passed through JS interop.
|
||||||
|
JsUrlStrategy convertToJsUrlStrategy(UrlStrategy strategy) {
|
||||||
|
if (strategy == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsUrlStrategy(
|
||||||
|
getPath: allowInterop(strategy.getPath),
|
||||||
|
getState: allowInterop(strategy.getState),
|
||||||
|
addPopStateListener: allowInterop(strategy.addPopStateListener),
|
||||||
|
prepareExternalUrl: allowInterop(strategy.prepareExternalUrl),
|
||||||
|
pushState: allowInterop(strategy.pushState),
|
||||||
|
replaceState: allowInterop(strategy.replaceState),
|
||||||
|
go: allowInterop(strategy.go),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JavaScript representation of a URL strategy.
|
||||||
|
///
|
||||||
|
/// This is used to pass URL strategy implementations across a JS-interop
|
||||||
|
/// bridge from the app to the engine.
|
||||||
|
@JS()
|
||||||
|
@anonymous
|
||||||
|
abstract class JsUrlStrategy {
|
||||||
|
/// Creates an instance of [JsUrlStrategy] from a bag of URL strategy
|
||||||
|
/// functions.
|
||||||
|
external factory JsUrlStrategy({
|
||||||
|
@required _PathGetter getPath,
|
||||||
|
@required _StateGetter getState,
|
||||||
|
@required _AddPopStateListener addPopStateListener,
|
||||||
|
@required _StringToString prepareExternalUrl,
|
||||||
|
@required _StateOperation pushState,
|
||||||
|
@required _StateOperation replaceState,
|
||||||
|
@required _HistoryMove go,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Adds a listener to the `popstate` event and returns a function that
|
||||||
|
/// removes the listener.
|
||||||
|
external ui.VoidCallback addPopStateListener(html.EventListener fn);
|
||||||
|
|
||||||
|
/// Returns the active path in the browser.
|
||||||
|
external String getPath();
|
||||||
|
|
||||||
|
/// Returns the history state in the browser.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
|
||||||
|
external Object getState();
|
||||||
|
|
||||||
|
/// Given a path that's internal to the app, create the external url that
|
||||||
|
/// will be used in the browser.
|
||||||
|
external String prepareExternalUrl(String internalUrl);
|
||||||
|
|
||||||
|
/// Push a new history entry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
|
||||||
|
external void pushState(Object state, String title, String url);
|
||||||
|
|
||||||
|
/// Replace the currently active history entry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
|
||||||
|
external void replaceState(Object state, String title, String url);
|
||||||
|
|
||||||
|
/// Moves forwards or backwards through the history stack.
|
||||||
|
///
|
||||||
|
/// A negative [count] value causes a backward move in the history stack. And
|
||||||
|
/// a positive [count] value causs a forward move.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// * `go(-2)` moves back 2 steps in history.
|
||||||
|
/// * `go(3)` moves forward 3 steps in hisotry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
|
||||||
|
external Future<void> go(int count);
|
||||||
|
}
|
@ -0,0 +1,320 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// @dart = 2.8
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'js_url_strategy.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
/// Change the strategy to use for handling browser URL.
|
||||||
|
///
|
||||||
|
/// Setting this to null disables all integration with the browser history.
|
||||||
|
void setUrlStrategy(UrlStrategy strategy) {
|
||||||
|
jsSetUrlStrategy(convertToJsUrlStrategy(strategy));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents and reads route state from the browser's URL.
|
||||||
|
///
|
||||||
|
/// By default, the [HashUrlStrategy] subclass is used if the app doesn't
|
||||||
|
/// specify one.
|
||||||
|
abstract class UrlStrategy {
|
||||||
|
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||||
|
/// const constructors so that they can be used in const expressions.
|
||||||
|
const UrlStrategy();
|
||||||
|
|
||||||
|
/// Adds a listener to the `popstate` event and returns a function that, when
|
||||||
|
/// invoked, removes the listener.
|
||||||
|
ui.VoidCallback addPopStateListener(html.EventListener fn);
|
||||||
|
|
||||||
|
/// Returns the active path in the browser.
|
||||||
|
String getPath();
|
||||||
|
|
||||||
|
/// The state of the current browser history entry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
|
||||||
|
Object getState();
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
|
||||||
|
void pushState(Object state, String title, String url);
|
||||||
|
|
||||||
|
/// Replace the currently active history entry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
|
||||||
|
void replaceState(Object state, String title, String url);
|
||||||
|
|
||||||
|
/// Moves forwards or backwards through the history stack.
|
||||||
|
///
|
||||||
|
/// A negative [count] value causes a backward move in the history stack. And
|
||||||
|
/// a positive [count] value causs a forward move.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// * `go(-2)` moves back 2 steps in history.
|
||||||
|
/// * `go(3)` moves forward 3 steps in hisotry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
|
||||||
|
Future<void> go(int count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// interations.
|
||||||
|
const HashUrlStrategy(
|
||||||
|
[this._platformLocation = const BrowserPlatformLocation()]);
|
||||||
|
|
||||||
|
final PlatformLocation _platformLocation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ui.VoidCallback addPopStateListener(html.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>();
|
||||||
|
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
|
||||||
|
/// interations.
|
||||||
|
PathUrlStrategy([
|
||||||
|
PlatformLocation _platformLocation = const BrowserPlatformLocation(),
|
||||||
|
]) : _basePath = stripTrailingSlash(extractPathname(checkBaseHref(
|
||||||
|
_platformLocation.getBaseHref(),
|
||||||
|
))),
|
||||||
|
super(_platformLocation);
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encapsulates all calls to DOM apis, which allows the [UrlStrategy] classes
|
||||||
|
/// to be platform agnostic and testable.
|
||||||
|
///
|
||||||
|
/// For convenience, the [PlatformLocation] class can be used by implementations
|
||||||
|
/// of [UrlStrategy] to interact with DOM apis like pushState, popState, etc.
|
||||||
|
abstract class PlatformLocation {
|
||||||
|
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||||
|
/// const constructors so that they can be used in const expressions.
|
||||||
|
const PlatformLocation();
|
||||||
|
|
||||||
|
/// Registers an event listener for the `popstate` event.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
|
||||||
|
void addPopStateListener(html.EventListener fn);
|
||||||
|
|
||||||
|
/// Unregisters the given listener (added by [addPopStateListener]) from the
|
||||||
|
/// `popstate` event.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
|
||||||
|
void removePopStateListener(html.EventListener fn);
|
||||||
|
|
||||||
|
/// The `pathname` part of the URL in the browser address bar.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname
|
||||||
|
String get pathname;
|
||||||
|
|
||||||
|
/// The `query` part of the URL in the browser address bar.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/search
|
||||||
|
String get search;
|
||||||
|
|
||||||
|
/// The `hash` part of the URL in the browser address bar.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/hash
|
||||||
|
String get hash;
|
||||||
|
|
||||||
|
/// The `state` in the current history entry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
|
||||||
|
Object get state;
|
||||||
|
|
||||||
|
/// Adds a new entry to the browser history stack.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
|
||||||
|
void pushState(Object state, String title, String url);
|
||||||
|
|
||||||
|
/// Replaces the current entry in the browser history stack.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
|
||||||
|
void replaceState(Object state, String title, String url);
|
||||||
|
|
||||||
|
/// Moves forwards or backwards through the history stack.
|
||||||
|
///
|
||||||
|
/// A negative [count] value causes a backward move in the history stack. And
|
||||||
|
/// a positive [count] value causs a forward move.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// * `go(-2)` moves back 2 steps in history.
|
||||||
|
/// * `go(3)` moves forward 3 steps in hisotry.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
|
||||||
|
void go(int count);
|
||||||
|
|
||||||
|
/// The base href where the Flutter app is being served.
|
||||||
|
///
|
||||||
|
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
String getBaseHref();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegates to real browser APIs to provide platform location functionality.
|
||||||
|
class BrowserPlatformLocation extends PlatformLocation {
|
||||||
|
/// Default constructor for [BrowserPlatformLocation].
|
||||||
|
const BrowserPlatformLocation();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get search => _location.search;
|
||||||
|
|
||||||
|
@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();
|
||||||
|
// String getBaseHref() => html.document.baseUri;
|
||||||
|
}
|
67
packages/flutter_web_plugins/lib/src/navigation/utils.dart
Normal file
67
packages/flutter_web_plugins/lib/src/navigation/utils.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// @dart = 2.8
|
||||||
|
|
||||||
|
import 'dart:html';
|
||||||
|
|
||||||
|
AnchorElement _urlParsingNode;
|
||||||
|
|
||||||
|
/// Extracts the pathname part of a full [url].
|
||||||
|
///
|
||||||
|
/// Example: for the url `http://example.com/foo`, the extracted pathname will
|
||||||
|
/// be `/foo`.
|
||||||
|
String extractPathname(String url) {
|
||||||
|
// TODO(mdebbar): Use the `URI` class instead?
|
||||||
|
_urlParsingNode ??= AnchorElement();
|
||||||
|
_urlParsingNode.href = url;
|
||||||
|
final String pathname = _urlParsingNode.pathname;
|
||||||
|
return (pathname.isEmpty || pathname[0] == '/') ? pathname : '/$pathname';
|
||||||
|
}
|
||||||
|
|
||||||
|
Element _baseElement;
|
||||||
|
|
||||||
|
/// Finds the <base> element in the document and returns its `href` attribute.
|
||||||
|
///
|
||||||
|
/// Returns null if the element isn't found.
|
||||||
|
String getBaseElementHrefFromDom() {
|
||||||
|
if (_baseElement == null) {
|
||||||
|
_baseElement = document.querySelector('base');
|
||||||
|
if (_baseElement == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _baseElement.getAttribute('href');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that [baseHref] is set.
|
||||||
|
///
|
||||||
|
/// Throws an exception otherwise.
|
||||||
|
String checkBaseHref(String baseHref) {
|
||||||
|
if (baseHref == null) {
|
||||||
|
throw Exception('Please add a <base> element to your index.html');
|
||||||
|
}
|
||||||
|
if (!baseHref.endsWith('/')) {
|
||||||
|
throw Exception('The base href has to end with a "/" to work correctly');
|
||||||
|
}
|
||||||
|
return baseHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepends a forward slash to [path] if it doesn't start with one already.
|
||||||
|
///
|
||||||
|
/// Returns [path] unchanged if it already starts with a forward slash.
|
||||||
|
String ensureLeadingSlash(String path) {
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
return '/$path';
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the trailing forward slash from [path] if any.
|
||||||
|
String stripTrailingSlash(String path) {
|
||||||
|
if (path.endsWith('/')) {
|
||||||
|
return path.substring(0, path.length - 1);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
@ -10,6 +10,8 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
js: 0.6.3-nullsafety.1
|
||||||
|
|
||||||
characters: 1.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
characters: 1.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
collection: 1.15.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
collection: 1.15.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
@ -34,4 +36,4 @@ dev_dependencies:
|
|||||||
term_glyph: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
term_glyph: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
test_api: 0.2.19-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
test_api: 0.2.19-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
|
||||||
# PUBSPEC CHECKSUM: 417a
|
# PUBSPEC CHECKSUM: 2180
|
||||||
|
@ -0,0 +1,188 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// @dart = 2.8
|
||||||
|
|
||||||
|
import 'dart:html';
|
||||||
|
|
||||||
|
@TestOn('chrome') // Uses web-only Flutter SDK
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('$HashUrlStrategy', () {
|
||||||
|
TestPlatformLocation location;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
location = TestPlatformLocation();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
location = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leading slash is optional', () {
|
||||||
|
final HashUrlStrategy strategy = HashUrlStrategy(location);
|
||||||
|
|
||||||
|
location.hash = '#/';
|
||||||
|
expect(strategy.getPath(), '/');
|
||||||
|
|
||||||
|
location.hash = '#/foo';
|
||||||
|
expect(strategy.getPath(), '/foo');
|
||||||
|
|
||||||
|
location.hash = '#foo';
|
||||||
|
expect(strategy.getPath(), 'foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path should not be empty', () {
|
||||||
|
final HashUrlStrategy strategy = HashUrlStrategy(location);
|
||||||
|
|
||||||
|
location.hash = '';
|
||||||
|
expect(strategy.getPath(), '/');
|
||||||
|
|
||||||
|
location.hash = '#';
|
||||||
|
expect(strategy.getPath(), '/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('$PathUrlStrategy', () {
|
||||||
|
TestPlatformLocation location;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
location = TestPlatformLocation();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
location = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates base href', () {
|
||||||
|
location.baseHref = '/';
|
||||||
|
expect(
|
||||||
|
() => PathUrlStrategy(location),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
|
||||||
|
location.baseHref = '/foo/';
|
||||||
|
expect(
|
||||||
|
() => PathUrlStrategy(location),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
|
||||||
|
location.baseHref = '';
|
||||||
|
expect(
|
||||||
|
() => PathUrlStrategy(location),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
|
||||||
|
location.baseHref = 'foo';
|
||||||
|
expect(
|
||||||
|
() => PathUrlStrategy(location),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
|
||||||
|
location.baseHref = '/foo';
|
||||||
|
expect(
|
||||||
|
() => PathUrlStrategy(location),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leading slash is always prepended', () {
|
||||||
|
location.baseHref = '/';
|
||||||
|
final PathUrlStrategy strategy = PathUrlStrategy(location);
|
||||||
|
|
||||||
|
location.pathname = '';
|
||||||
|
expect(strategy.getPath(), '/');
|
||||||
|
|
||||||
|
location.pathname = 'foo';
|
||||||
|
expect(strategy.getPath(), '/foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets path correctly in the presence of basePath', () {
|
||||||
|
location.baseHref = 'https://example.com/foo/';
|
||||||
|
final PathUrlStrategy strategy = PathUrlStrategy(location);
|
||||||
|
|
||||||
|
location.pathname = '/foo/';
|
||||||
|
expect(strategy.getPath(), '/');
|
||||||
|
|
||||||
|
location.pathname = '/foo';
|
||||||
|
expect(strategy.getPath(), '/');
|
||||||
|
|
||||||
|
location.pathname = '/foo/bar';
|
||||||
|
expect(strategy.getPath(), '/bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets path correctly in the presence of query params', () {
|
||||||
|
location.baseHref = 'https://example.com/foo/';
|
||||||
|
location.pathname = '/foo/bar';
|
||||||
|
final PathUrlStrategy strategy = PathUrlStrategy(location);
|
||||||
|
|
||||||
|
|
||||||
|
location.search = '?q=1';
|
||||||
|
expect(strategy.getPath(), '/bar?q=1');
|
||||||
|
|
||||||
|
location.search = '?q=1&t=r';
|
||||||
|
expect(strategy.getPath(), '/bar?q=1&t=r');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generates external path correctly in the presence of basePath', () {
|
||||||
|
location.baseHref = 'https://example.com/foo/';
|
||||||
|
final PathUrlStrategy strategy = PathUrlStrategy(location);
|
||||||
|
|
||||||
|
expect(strategy.prepareExternalUrl(''), '/foo');
|
||||||
|
expect(strategy.prepareExternalUrl('/'), '/foo/');
|
||||||
|
expect(strategy.prepareExternalUrl('bar'), '/foo/bar');
|
||||||
|
expect(strategy.prepareExternalUrl('/bar'), '/foo/bar');
|
||||||
|
expect(strategy.prepareExternalUrl('/bar/'), '/foo/bar/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mock implementation of [PlatformLocation] that doesn't access the browser.
|
||||||
|
class TestPlatformLocation extends PlatformLocation {
|
||||||
|
@override
|
||||||
|
String pathname = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String search = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String hash = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic state;
|
||||||
|
|
||||||
|
/// Mocks the base href of the document.
|
||||||
|
String baseHref = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addPopStateListener(EventListener fn) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removePopStateListener(EventListener fn) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pushState(dynamic state, String title, String url) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void replaceState(dynamic state, String title, String url) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void go(int count) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getBaseHref() => baseHref;
|
||||||
|
}
|
38
packages/flutter_web_plugins/test/navigation/utils_test.dart
Normal file
38
packages/flutter_web_plugins/test/navigation/utils_test.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// @dart = 2.8
|
||||||
|
|
||||||
|
@TestOn('browser') // Uses web-only Flutter SDK
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_web_plugins/src/navigation/utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('checks base href', () {
|
||||||
|
expect(() => checkBaseHref(null), throwsException);
|
||||||
|
expect(() => checkBaseHref('foo'), throwsException);
|
||||||
|
expect(() => checkBaseHref('/foo'), throwsException);
|
||||||
|
expect(() => checkBaseHref('foo/bar'), throwsException);
|
||||||
|
expect(() => checkBaseHref('/foo/bar'), throwsException);
|
||||||
|
|
||||||
|
expect(() => checkBaseHref('/'), returnsNormally);
|
||||||
|
expect(() => checkBaseHref('/foo/'), returnsNormally);
|
||||||
|
expect(() => checkBaseHref('/foo/bar/'), returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extracts pathname from URL', () {
|
||||||
|
expect(extractPathname('/'), '/');
|
||||||
|
expect(extractPathname('/foo'), '/foo');
|
||||||
|
expect(extractPathname('/foo/'), '/foo/');
|
||||||
|
expect(extractPathname('/foo/bar'), '/foo/bar');
|
||||||
|
expect(extractPathname('/foo/bar/'), '/foo/bar/');
|
||||||
|
|
||||||
|
expect(extractPathname('https://example.com'), '/');
|
||||||
|
expect(extractPathname('https://example.com/'), '/');
|
||||||
|
expect(extractPathname('https://example.com/foo'), '/foo');
|
||||||
|
expect(extractPathname('https://example.com/foo#bar'), '/foo');
|
||||||
|
expect(extractPathname('https://example.com/foo/#bar'), '/foo/');
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user