From 65e48da91c35eedcc82ac3c0548b3254befe1dad Mon Sep 17 00:00:00 2001 From: joshualitt Date: Thu, 9 Jun 2022 10:46:46 -0700 Subject: [PATCH] [web] Migrate Flutter Web DOM usage to JS static interop - 24. (flutter/engine#33352) --- .../lib/web_ui/lib/src/engine/dom.dart | 80 +++++++++++++++---- .../lib/src/engine/navigation/history.dart | 15 ++-- .../engine/navigation/js_url_strategy.dart | 8 +- .../src/engine/navigation/url_strategy.dart | 34 ++++---- .../web_ui/lib/src/engine/test_embedding.dart | 12 +-- .../lib/web_ui/test/engine/history_test.dart | 7 +- .../flutter/lib/web_ui/test/window_test.dart | 2 +- 7 files changed, 103 insertions(+), 55 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart index 041efd03820..5d3b9b7a78d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart @@ -26,8 +26,10 @@ extension DomWindowExtension on DomWindow { external DomConsole get console; external num get devicePixelRatio; external DomDocument get document; + external DomHistory get history; external int? get innerHeight; external int? get innerWidth; + external DomLocation get location; external DomNavigator get navigator; external DomVisualViewport? get visualViewport; external DomPerformance get performance; @@ -61,7 +63,7 @@ extension DomNavigatorExtension on DomNavigator { @JS() @staticInterop -class DomDocument {} +class DomDocument extends DomNode {} extension DomDocumentExtension on DomDocument { external DomElement? get documentElement; @@ -142,6 +144,7 @@ extension DomProgressEventExtension on DomProgressEvent { class DomNode extends DomEventTarget {} extension DomNodeExtension on DomNode { + external String? get baseUri; external DomNode? get firstChild; external String get innerText; external DomNode? get lastChild; @@ -691,6 +694,67 @@ extension DomKeyboardEventExtension on DomKeyboardEvent { external bool getModifierState(String keyArg); } +@JS() +@staticInterop +class DomHistory {} + +extension DomHistoryExtension on DomHistory { + dynamic get state => js_util.dartify(js_util.getProperty(this, 'state')); + external void go([int? delta]); + void pushState(dynamic data, String title, String? url) => + js_util.callMethod(this, 'pushState', [ + if (data is Map || data is Iterable) js_util.jsify(data) else data, + title, + url + ]); + void replaceState(dynamic data, String title, String? url) => + js_util.callMethod(this, 'replaceState', [ + if (data is Map || data is Iterable) js_util.jsify(data) else data, + title, + url + ]); +} + +@JS() +@staticInterop +class DomLocation {} + +extension DomLocationExtension on DomLocation { + external String? get pathname; + external String? get search; + // We have to change the name here because 'hash' is inherited from [Object]. + String get locationHash => js_util.getProperty(this, 'hash'); +} + +@JS() +@staticInterop +class DomPopStateEvent extends DomEvent {} + +DomPopStateEvent createDomPopStateEvent( + String type, Map? eventInitDict) => + domCallConstructorString('PopStateEvent', [ + type, + if (eventInitDict != null) js_util.jsify(eventInitDict) + ])! as DomPopStateEvent; + +extension DomPopStateEventExtension on DomPopStateEvent { + dynamic get state => js_util.dartify(js_util.getProperty(this, 'state')); +} + +Object? domGetConstructor(String constructorName) => + js_util.getProperty(domWindow, constructorName); + +Object? domCallConstructorString(String constructorName, List args) { + final Object? constructor = domGetConstructor(constructorName); + if (constructor == null) { + return null; + } + return js_util.callConstructor(constructor, args); +} + +bool domInstanceOfString(Object? element, String objectType) => + js_util.instanceof(element, domGetConstructor(objectType)!); + /// [_DomElementList] is the shared interface for APIs that return either /// `NodeList` or `HTMLCollection`. Do *not* add any API to this class that /// isn't support by both JS objects. Furthermore, this is an internal class and @@ -741,17 +805,3 @@ class _DomElementListWrapper extends Iterable { @override int get length => elementList.length; } - -Object? domGetConstructor(String constructorName) => - js_util.getProperty(domWindow, constructorName); - -Object? domCallConstructorString(String constructorName, List args) { - final Object? constructor = domGetConstructor(constructorName); - if (constructor == null) { - return null; - } - return js_util.callConstructor(constructor, args); -} - -bool domInstanceOfString(Object? element, String objectType) => - js_util.instanceof(element, domGetConstructor(objectType)!); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart index f472c8a3406..f16ec764f29 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; +import '../dom.dart'; import '../platform_dispatcher.dart'; import '../services/message_codec.dart'; import '../services/message_codecs.dart'; @@ -52,9 +51,7 @@ abstract class BrowserHistory { bool _isDisposed = false; void _setupStrategy(UrlStrategy strategy) { - _unsubscribe = strategy.addPopStateListener( - onPopState as html.EventListener, - ); + _unsubscribe = strategy.addPopStateListener(onPopState as DomEventListener); } /// Release any resources held by this [BrowserHistory] instance. @@ -103,7 +100,7 @@ abstract class BrowserHistory { /// /// Subclasses should send appropriate system messages to update the flutter /// applications accordingly. - void onPopState(covariant html.PopStateEvent event); + void onPopState(covariant DomPopStateEvent event); /// Restore any modifications to the html browser history during the lifetime /// of this class. @@ -186,7 +183,7 @@ class MultiEntriesBrowserHistory extends BrowserHistory { } @override - void onPopState(covariant html.PopStateEvent event) { + void onPopState(covariant DomPopStateEvent event) { assert(urlStrategy != null); // May be a result of direct url access while the flutter application is // already running. @@ -263,7 +260,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { _setupStrategy(strategy); final String path = currentPath; - if (!_isFlutterEntry(html.window.history.state)) { + if (!_isFlutterEntry(domWindow.history.state)) { // An entry may not have come from Flutter, for example, when the user // refreshes the page. They land directly on the "flutter" entry, so // there's no need to set up the "origin" and "flutter" entries, we can @@ -314,7 +311,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { String? _userProvidedRouteName; @override - void onPopState(covariant html.PopStateEvent event) { + void onPopState(covariant DomPopStateEvent event) { if (_isOriginEntry(event.state)) { _setupFlutterEntry(urlStrategy!); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart index 9014e517e25..9fbf52ed103 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart @@ -5,16 +5,16 @@ @JS() library js_url_strategy; -import 'dart:html' as html; - import 'package:js/js.dart'; import 'package:ui/ui.dart' as ui; +import '../dom.dart'; + typedef _PathGetter = String Function(); typedef _StateGetter = Object? Function(); -typedef _AddPopStateListener = ui.VoidCallback Function(html.EventListener); +typedef _AddPopStateListener = ui.VoidCallback Function(DomEventListener); typedef _StringToString = String Function(String); @@ -47,7 +47,7 @@ abstract class JsUrlStrategy { extension JsUrlStrategyExtension on JsUrlStrategy { /// Adds a listener to the `popstate` event and returns a function that, when /// invoked, removes the listener. - external ui.VoidCallback addPopStateListener(html.EventListener fn); + external ui.VoidCallback addPopStateListener(DomEventListener fn); /// Returns the active path in the browser. external String getPath(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart index 7d277c151e6..780c5a1c6a0 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart @@ -3,11 +3,12 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; import 'package:js/js.dart' as js; import 'package:ui/ui.dart' as ui; +import '../dom.dart'; +import '../safe_browser_api.dart'; import 'js_url_strategy.dart'; /// Represents and reads route state from the browser's URL. @@ -21,7 +22,7 @@ abstract class UrlStrategy { /// Adds a listener to the `popstate` event and returns a function that, when /// invoked, removes the listener. - ui.VoidCallback addPopStateListener(html.EventListener fn); + ui.VoidCallback addPopStateListener(DomEventListener fn); /// Returns the active path in the browser. String getPath(); @@ -82,9 +83,10 @@ class HashUrlStrategy extends UrlStrategy { final PlatformLocation _platformLocation; @override - ui.VoidCallback addPopStateListener(html.EventListener fn) { - _platformLocation.addPopStateListener(fn); - return () => _platformLocation.removePopStateListener(fn); + ui.VoidCallback addPopStateListener(DomEventListener fn) { + final DomEventListener wrappedFn = allowInterop(fn); + _platformLocation.addPopStateListener(wrappedFn); + return () => _platformLocation.removePopStateListener(wrappedFn); } @override @@ -156,7 +158,7 @@ class CustomUrlStrategy extends UrlStrategy { final JsUrlStrategy delegate; @override - ui.VoidCallback addPopStateListener(html.EventListener fn) => + ui.VoidCallback addPopStateListener(DomEventListener fn) => delegate.addPopStateListener(js.allowInterop(fn)); @override @@ -194,13 +196,13 @@ abstract class 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); + void addPopStateListener(DomEventListener 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); + void removePopStateListener(DomEventListener fn); /// The `pathname` part of the URL in the browser address bar. /// @@ -256,17 +258,17 @@ class BrowserPlatformLocation extends PlatformLocation { /// Default constructor for [BrowserPlatformLocation]. const BrowserPlatformLocation(); - html.Location get _location => html.window.location; - html.History get _history => html.window.history; + DomLocation get _location => domWindow.location; + DomHistory get _history => domWindow.history; @override - void addPopStateListener(html.EventListener fn) { - html.window.addEventListener('popstate', fn); + void addPopStateListener(DomEventListener fn) { + domWindow.addEventListener('popstate', fn); } @override - void removePopStateListener(html.EventListener fn) { - html.window.removeEventListener('popstate', fn); + void removePopStateListener(DomEventListener fn) { + domWindow.removeEventListener('popstate', fn); } @override @@ -276,7 +278,7 @@ class BrowserPlatformLocation extends PlatformLocation { String get search => _location.search!; @override - String get hash => _location.hash; + String get hash => _location.locationHash; @override Object? get state => _history.state; @@ -297,5 +299,5 @@ class BrowserPlatformLocation extends PlatformLocation { } @override - String? getBaseHref() => html.document.baseUri; + String? getBaseHref() => domDocument.baseUri; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart index 8303ed997fa..5d86744a008 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart @@ -6,7 +6,6 @@ // https://github.com/flutter/flutter/issues/100394 import 'dart:async'; -import 'dart:html' as html; import 'package:ui/ui.dart' as ui; @@ -149,15 +148,16 @@ class TestUrlStrategy extends UrlStrategy { }); } - final List listeners = []; + final List listeners = []; @override - ui.VoidCallback addPopStateListener(html.EventListener fn) { - listeners.add(fn); + ui.VoidCallback addPopStateListener(DomEventListener fn) { + final DomEventListener wrappedFn = allowInterop(fn); + listeners.add(wrappedFn); return () { // Schedule a micro task here to avoid removing the listener during // iteration in [_firePopStateEvent]. - scheduleMicrotask(() => listeners.remove(fn)); + scheduleMicrotask(() => listeners.remove(wrappedFn)); }; } @@ -172,7 +172,7 @@ class TestUrlStrategy extends UrlStrategy { /// like a real browser. void _firePopStateEvent() { assert(withinAppHistory); - final html.PopStateEvent event = html.PopStateEvent( + final DomPopStateEvent event = createDomPopStateEvent( 'popstate', {'state': currentEntry.state}, ); diff --git a/engine/src/flutter/lib/web_ui/test/engine/history_test.dart b/engine/src/flutter/lib/web_ui/test/engine/history_test.dart index 2593b42f5e5..fabb4db483e 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/history_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/history_test.dart @@ -6,12 +6,11 @@ // TODO(mdebbar): https://github.com/flutter/flutter/issues/51169 import 'dart:async'; -import 'dart:html' as html; import 'package:quiver/testing/async.dart'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; -import 'package:ui/src/engine.dart' show window; +import 'package:ui/src/engine.dart' show window, DomEventListener; import 'package:ui/src/engine/browser_detection.dart'; import 'package:ui/src/engine/navigation.dart'; import 'package:ui/src/engine/services.dart'; @@ -723,12 +722,12 @@ class TestPlatformLocation extends PlatformLocation { String get search => throw UnimplementedError(); @override - void addPopStateListener(html.EventListener fn) { + void addPopStateListener(DomEventListener fn) { throw UnimplementedError(); } @override - void removePopStateListener(html.EventListener fn) { + void removePopStateListener(DomEventListener fn) { throw UnimplementedError(); } diff --git a/engine/src/flutter/lib/web_ui/test/window_test.dart b/engine/src/flutter/lib/web_ui/test/window_test.dart index 1aa52522882..a44eed7e7d9 100644 --- a/engine/src/flutter/lib/web_ui/test/window_test.dart +++ b/engine/src/flutter/lib/web_ui/test/window_test.dart @@ -47,7 +47,7 @@ void testMain() { final JsUrlStrategy jsUrlStrategy = JsUrlStrategy( getPath: allowInterop(() => '/initial'), getState: allowInterop(() => state), - addPopStateListener: allowInterop((html.EventListener listener) => () {}), + addPopStateListener: allowInterop((DomEventListener listener) => () {}), prepareExternalUrl: allowInterop((String value) => ''), pushState: allowInterop((Object? newState, String title, String url) { expect(newState is Map, true);