• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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