// 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 'dart:ui_web' as ui_web; import '../navigation_common/platform_location.dart'; import 'utils.dart'; export 'dart:ui_web' show UrlStrategy; /// 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 // ui_web.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]. ui_web.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(ui_web.UrlStrategy? strategy) { _urlStrategy = strategy; ui_web.urlStrategy = strategy; } /// 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 ui_web.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(ui_web.PopStateListener fn) { void wrappedFn(Object event) { // `fn` expects `event.state`, not a `html.Event`. fn((event as html.PopStateEvent).state); } _platformLocation.addPopStateListener(wrappedFn); return () => _platformLocation.removePopStateListener(wrappedFn); } @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 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 _waitForPopState() { final Completer completer = Completer(); 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(); }