1// Copyright 2013 The Flutter Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5part of engine; 6 7// TODO(mdebbar): add other strategies. 8 9// Some parts of this file were inspired/copied from the AngularDart router. 10 11/// Ensures that [str] is prefixed with [leading]. If [str] is already prefixed, 12/// it'll be returned unchanged. If it's not, this function will prefix it. 13/// 14/// The [applyWhenEmpty] flag controls whether this function should prefix [str] 15/// or not when it's an empty string. 16/// 17/// ```dart 18/// ensureLeading('/path', '/'); // "/path" 19/// ensureLeading('path', '/'); // "/path" 20/// ensureLeading('', '/'); // "/" 21/// ensureLeading('', '/', applyWhenEmpty: false); // "" 22/// ``` 23String ensureLeading(String str, String leading, {bool applyWhenEmpty = true}) { 24 if (str.isEmpty && !applyWhenEmpty) { 25 return str; 26 } 27 return str.startsWith(leading) ? str : '$leading$str'; 28} 29 30/// `LocationStrategy` is responsible for representing and reading route state 31/// from the browser's URL. At the moment, only one strategy is implemented: 32/// [HashLocationStrategy]. 33/// 34/// This is used by [BrowserHistory] to interact with browser history APIs. 35abstract class LocationStrategy { 36 const LocationStrategy(); 37 38 /// Subscribes to popstate events and returns a function that could be used to 39 /// unsubscribe from popstate events. 40 ui.VoidCallback onPopState(html.EventListener fn); 41 42 /// The active path in the browser history. 43 String get path; 44 45 /// Given a path that's internal to the app, create the external url that 46 /// will be used in the browser. 47 String prepareExternalUrl(String internalUrl); 48 49 /// Push a new history entry. 50 void pushState(dynamic state, String title, String url); 51 52 /// Replace the currently active history entry. 53 void replaceState(dynamic state, String title, String url); 54 55 /// Go to the previous history entry. 56 Future<void> back(); 57} 58 59/// This is an implementation of [LocationStrategy] that uses the browser URL's 60/// [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) 61/// to represent its state. 62/// 63/// In order to use this [LocationStrategy] for an app, it needs to be set in 64/// [ui.window.webOnlyLocationStrategy]: 65/// 66/// ```dart 67/// import 'package:flutter_web/material.dart'; 68/// import 'package:flutter_web/ui.dart' as ui; 69/// 70/// void main() { 71/// ui.window.webOnlyLocationStrategy = const ui.HashLocationStrategy(); 72/// runApp(MyApp()); 73/// } 74/// ``` 75class HashLocationStrategy extends LocationStrategy { 76 final PlatformLocation _platformLocation; 77 78 const HashLocationStrategy( 79 [this._platformLocation = const BrowserPlatformLocation()]); 80 81 @override 82 ui.VoidCallback onPopState(html.EventListener fn) { 83 _platformLocation.onPopState(fn); 84 return () => _platformLocation.offPopState(fn); 85 } 86 87 @override 88 String get path { 89 // the hash value is always prefixed with a `#` 90 // and if it is empty then it will stay empty 91 String path = _platformLocation.hash ?? ''; 92 // Dart will complain if a call to substring is 93 // executed with a position value that exceeds the 94 // length of string. 95 path = path.isEmpty ? path : path.substring(1); 96 // The path, by convention, should always contain a leading '/'. 97 return ensureLeading(path, '/'); 98 } 99 100 @override 101 String prepareExternalUrl(String internalUrl) { 102 // It's convention that if the hash path is empty, we omit the `#`; however, 103 // if the empty URL is pushed it won't replace any existing fragment. So 104 // when the hash path is empty, we instead return the location's path and 105 // query. 106 return internalUrl.isEmpty 107 ? '${_platformLocation.pathname}${_platformLocation.search}' 108 : '#$internalUrl'; 109 } 110 111 @override 112 void pushState(dynamic state, String title, String url) { 113 _platformLocation.pushState(state, title, prepareExternalUrl(url)); 114 } 115 116 @override 117 void replaceState(dynamic state, String title, String url) { 118 _platformLocation.replaceState(state, title, prepareExternalUrl(url)); 119 } 120 121 @override 122 Future<void> back() { 123 _platformLocation.back(); 124 return _waitForPopState(); 125 } 126 127 /// Waits until the next popstate event is fired. This is useful for example 128 /// to wait until the browser has handled the `history.back` transition. 129 Future<void> _waitForPopState() { 130 final Completer<void> completer = Completer<void>(); 131 ui.VoidCallback unsubscribe; 132 unsubscribe = onPopState((_) { 133 unsubscribe(); 134 completer.complete(); 135 }); 136 return completer.future; 137 } 138} 139 140/// `PlatformLocation` encapsulates all calls to DOM apis, which allows the 141/// [LocationStrategy] classes to be platform agnostic and testable. 142/// 143/// The `PlatformLocation` class is used directly by all implementations of 144/// [LocationStrategy] when they need to interact with the DOM apis like 145/// pushState, popState, etc... 146abstract class PlatformLocation { 147 const PlatformLocation(); 148 149 void onPopState(html.EventListener fn); 150 void offPopState(html.EventListener fn); 151 152 void onHashChange(html.EventListener fn); 153 void offHashChange(html.EventListener fn); 154 155 String get pathname; 156 String get search; 157 String get hash; 158 159 void pushState(dynamic state, String title, String url); 160 void replaceState(dynamic state, String title, String url); 161 void back(); 162} 163 164/// An implementation of [PlatformLocation] for the browser. 165class BrowserPlatformLocation extends PlatformLocation { 166 html.Location get _location => html.window.location; 167 html.History get _history => html.window.history; 168 169 const BrowserPlatformLocation(); 170 171 @override 172 void onPopState(html.EventListener fn) { 173 html.window.addEventListener('popstate', fn); 174 } 175 176 @override 177 void offPopState(html.EventListener fn) { 178 html.window.removeEventListener('popstate', fn); 179 } 180 181 @override 182 void onHashChange(html.EventListener fn) { 183 html.window.addEventListener('hashchange', fn); 184 } 185 186 @override 187 void offHashChange(html.EventListener fn) { 188 html.window.removeEventListener('hashchange', fn); 189 } 190 191 @override 192 String get pathname => _location.pathname; 193 194 @override 195 String get search => _location.search; 196 197 @override 198 String get hash => _location.hash; 199 200 @override 201 void pushState(dynamic state, String title, String url) { 202 _history.pushState(state, title, url); 203 } 204 205 @override 206 void replaceState(dynamic state, String title, String url) { 207 _history.replaceState(state, title, url); 208 } 209 210 @override 211 void back() { 212 _history.back(); 213 } 214} 215