url_strategy.dart 7.04 KB
Newer Older
1 2 3 4 5 6 7 8
// 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;

9
import '../navigation_common/url_strategy.dart';
10 11 12
import 'js_url_strategy.dart';
import 'utils.dart';

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
/// 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;

31 32 33
/// Change the strategy to use for handling browser URL.
///
/// Setting this to null disables all integration with the browser history.
34
void setUrlStrategy(UrlStrategy? strategy) {
35 36
  _urlStrategy = strategy;

37 38 39 40 41
  JsUrlStrategy? jsUrlStrategy;
  if (strategy != null) {
    jsUrlStrategy = convertToJsUrlStrategy(strategy);
  }
  jsSetUrlStrategy(jsUrlStrategy);
42 43
}

44 45 46
/// Use the [PathUrlStrategy] to handle the browser URL.
void usePathUrlStrategy() {
  setUrlStrategy(PathUrlStrategy());
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
}

/// 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
67
  /// interactions.
68 69 70 71 72 73
  const HashUrlStrategy(
      [this._platformLocation = const BrowserPlatformLocation()]);

  final PlatformLocation _platformLocation;

  @override
74
  ui.VoidCallback addPopStateListener(EventListener fn) {
75 76 77 78 79 80 81 82
    _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
83
    final String path = _platformLocation.hash;
84 85 86 87 88 89 90 91 92 93 94
    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
95
  Object? getState() => _platformLocation.state;
96 97 98 99 100 101 102 103 104 105 106 107 108

  @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
109
  void pushState(Object? state, String title, String url) {
110 111 112 113
    _platformLocation.pushState(state, title, prepareExternalUrl(url));
  }

  @override
114
  void replaceState(Object? state, String title, String url) {
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    _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>();
130
    late ui.VoidCallback unsubscribe;
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    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
153
  /// interactions.
154
  PathUrlStrategy([
155
    super.platformLocation,
156
  ])  : _basePath = stripTrailingSlash(extractPathname(checkBaseHref(
157
          platformLocation.getBaseHref(),
158
        )));
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

  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();

185 186 187 188 189 190 191 192
  // 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 = '';

193
  html.Location get _location => html.window.location;
194

195 196 197 198 199 200 201 202 203 204 205 206 207
  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
208
  String get pathname => _location.pathname ?? _defaultPathname;
209 210

  @override
211
  String get search => _location.search ?? _defaultSearch;
212 213 214 215 216

  @override
  String get hash => _location.hash;

  @override
217
  Object? get state => _history.state;
218 219

  @override
220
  void pushState(Object? state, String title, String url) {
221 222 223 224
    _history.pushState(state, title, url);
  }

  @override
225
  void replaceState(Object? state, String title, String url) {
226 227 228 229 230 231 232 233 234
    _history.replaceState(state, title, url);
  }

  @override
  void go(int count) {
    _history.go(count);
  }

  @override
235
  String? getBaseHref() => getBaseElementHrefFromDom();
236
}