From 191465ac6faa7802071dd49e8486fb4e35eef491 Mon Sep 17 00:00:00 2001 From: joshualitt Date: Mon, 10 Apr 2023 12:08:26 -0700 Subject: [PATCH] [web] Migrate the bulk of JS interop to JS types. (#123286) --- .../lib/src/foundation/_capabilities_web.dart | 3 +- .../lib/src/painting/_network_image_web.dart | 12 +- packages/flutter/lib/src/services/dom.dart | 208 ++++++++++++------ ...rm_selectable_region_context_menu_web.dart | 3 +- .../test/painting/_test_http_request.dart | 63 +++--- 5 files changed, 179 insertions(+), 110 deletions(-) diff --git a/packages/flutter/lib/src/foundation/_capabilities_web.dart b/packages/flutter/lib/src/foundation/_capabilities_web.dart index 5b5de845b1c..4da8d6223d1 100644 --- a/packages/flutter/lib/src/foundation/_capabilities_web.dart +++ b/packages/flutter/lib/src/foundation/_capabilities_web.dart @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:js_interop'; import 'package:js/js.dart'; // This value is set by the engine. It is used to determine if the application is // using canvaskit. @JS('window.flutterCanvasKit') -external Object? get _windowFlutterCanvasKit; +external JSAny? get _windowFlutterCanvasKit; /// The web implementation of [isCanvasKit] bool get isCanvasKit => _windowFlutterCanvasKit != null; diff --git a/packages/flutter/lib/src/painting/_network_image_web.dart b/packages/flutter/lib/src/painting/_network_image_web.dart index 40f3b8a655c..ceb012e65a8 100644 --- a/packages/flutter/lib/src/painting/_network_image_web.dart +++ b/packages/flutter/lib/src/painting/_network_image_web.dart @@ -3,11 +3,10 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:typed_data'; +import 'dart:js_interop'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; -import 'package:js/js.dart'; import '../services/dom.dart'; import 'image_provider.dart' as image_provider; @@ -18,7 +17,7 @@ typedef HttpRequestFactory = DomXMLHttpRequest Function(); /// Default HTTP client. DomXMLHttpRequest _httpClient() { - return createDomXMLHttpRequest(); + return DomXMLHttpRequest(); } /// Creates an overridable factory function. @@ -148,7 +147,7 @@ class NetworkImage }); } - request.addEventListener('load', allowInterop((DomEvent e) { + request.addEventListener('load', createDomEventListener((DomEvent e) { final int? status = request.status; final bool accepted = status! >= 200 && status < 300; final bool fileUri = status == 0; // file:// URIs have status of 0. @@ -166,13 +165,14 @@ class NetworkImage } })); - request.addEventListener('error', allowInterop(completer.completeError)); + request.addEventListener('error', + createDomEventListener(completer.completeError)); request.send(); await completer.future; - final Uint8List bytes = (request.response as ByteBuffer).asUint8List(); + final Uint8List bytes = (request.response! as JSArrayBuffer).toDart.asUint8List(); if (bytes.lengthInBytes == 0) { throw image_provider.NetworkImageLoadException( diff --git a/packages/flutter/lib/src/services/dom.dart b/packages/flutter/lib/src/services/dom.dart index 96a44a4a70c..6dad28b3d5c 100644 --- a/packages/flutter/lib/src/services/dom.dart +++ b/packages/flutter/lib/src/services/dom.dart @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:js_interop'; import 'package:js/js.dart'; -import 'package:js/js_util.dart' as js_util; /// This file includes static interop helpers for Flutter Web. // TODO(joshualitt): This file will eventually be removed, @@ -16,8 +16,11 @@ class DomWindow {} /// [DomWindow] required extension. extension DomWindowExtension on DomWindow { + @JS('matchMedia') + external DomMediaQueryList _matchMedia(JSString? query); + /// Returns a [DomMediaQueryList] of the media that matches [query]. - external DomMediaQueryList matchMedia(String? query); + DomMediaQueryList matchMedia(String? query) => _matchMedia(query?.toJS); /// Returns the [DomNavigator] associated with this window. external DomNavigator get navigator; @@ -37,8 +40,11 @@ class DomMediaQueryList {} /// [DomMediaQueryList] required extension. extension DomMediaQueryListExtension on DomMediaQueryList { + @JS('matches') + external JSBoolean get _matches; + /// Whether or not the query matched. - external bool get matches; + bool get matches => _matches.toDart; } /// [DomNavigator] interop object. @@ -48,8 +54,11 @@ class DomNavigator {} /// [DomNavigator] required extension. extension DomNavigatorExtension on DomNavigator { + @JS('platform') + external JSString? get _platform; + /// The underyling platform string. - external String? get platform; + String? get platform => _platform?.toDart; } /// A DOM event target. @@ -59,56 +68,93 @@ class DomEventTarget {} /// [DomEventTarget]'s required extension. extension DomEventTargetExtension on DomEventTarget { + @JS('addEventListener') + external JSVoid _addEventListener1(JSString type, DomEventListener? listener); + + @JS('addEventListener') + external JSVoid _addEventListener2( + JSString type, DomEventListener? listener, JSBoolean useCapture); + /// Adds an event listener to this event target. + @JS('addEventListener') void addEventListener(String type, DomEventListener? listener, [bool? useCapture]) { if (listener != null) { - js_util.callMethod(this, 'addEventListener', - [type, listener, if (useCapture != null) useCapture]); + if (useCapture == null) { + _addEventListener1(type.toJS, listener); + } else { + _addEventListener2(type.toJS, listener, useCapture.toJS); + } } } } /// [DomXMLHttpRequest] interop class. -@JS() +@JS('XMLHttpRequest') @staticInterop -class DomXMLHttpRequest extends DomEventTarget {} +class DomXMLHttpRequest extends DomEventTarget { + /// Constructor for [DomXMLHttpRequest]. + external factory DomXMLHttpRequest(); +} /// [DomXMLHttpRequest] extension. extension DomXMLHttpRequestExtension on DomXMLHttpRequest { /// Gets the response. - external dynamic get response; + external JSAny? get response; + + @JS('responseText') + external JSString? get _responseText; /// Gets the response text. - external String? get responseText; + String? get responseText => _responseText?.toDart; + + @JS('responseType') + external JSString get _responseType; /// Gets the response type. - external String get responseType; + String get responseType => _responseType.toDart; + + @JS('status') + external JSNumber? get _status; /// Gets the status. - external int? get status; + int? get status => _status?.toDart.toInt(); + + @JS('responseType') + external set _responseType(JSString value); /// Set the response type. - external set responseType(String value); + set responseType(String value) => _responseType = value.toJS; + + @JS('setRequestHeader') + external void _setRequestHeader(JSString header, JSString value); /// Set the request header. - external void setRequestHeader(String header, String value); + void setRequestHeader(String header, String value) => + _setRequestHeader(header.toJS, value.toJS); + + @JS('open') + external JSVoid _open(JSString method, JSString url, JSBoolean isAsync); /// Open the request. - void open(String method, String url, bool isAsync) => js_util.callMethod( - this, 'open', [method, url, isAsync]); + void open(String method, String url, bool isAsync) => + _open(method.toJS, url.toJS, isAsync.toJS); /// Send the request. - void send() => js_util.callMethod(this, 'send', []); + external JSVoid send(); } -/// Factory function for creating [DomXMLHttpRequest]. -DomXMLHttpRequest createDomXMLHttpRequest() => - domCallConstructorString('XMLHttpRequest', [])! - as DomXMLHttpRequest; - /// Type for event listener. -typedef DomEventListener = void Function(DomEvent event); +typedef DartDomEventListener = JSVoid Function(DomEvent event); + +/// The type of [JSFunction] expected as an `EventListener`. +@JS() +@staticInterop +class DomEventListener {} + +/// Creates a [DomEventListener] from a [DartDomEventListener]. +DomEventListener createDomEventListener(DartDomEventListener listener) => + listener.toJS as DomEventListener; /// [DomEvent] interop object. @JS() @@ -117,16 +163,15 @@ class DomEvent {} /// [DomEvent] required extension. extension DomEventExtension on DomEvent { + @JS('type') + external JSString get _type; + /// Get the event type. - external String get type; + String get type => _type.toDart; /// Initialize an event. - void initEvent(String type, [bool? bubbles, bool? cancelable]) => - js_util.callMethod(this, 'initEvent', [ - type, - if (bubbles != null) bubbles, - if (cancelable != null) cancelable - ]); + external JSVoid initEvent( + JSString type, JSBoolean bubbles, JSBoolean cancelable); } /// [DomProgressEvent] interop object. @@ -136,24 +181,17 @@ class DomProgressEvent extends DomEvent {} /// [DomProgressEvent] required extension. extension DomProgressEventExtension on DomProgressEvent { + @JS('loaded') + external JSNumber? get _loaded; + /// Amount of work done. - external int? get loaded; + int? get loaded => _loaded?.toDart.toInt(); + + @JS('total') + external JSNumber? get _total; /// Total amount of work. - external int? get total; -} - -/// Gets a constructor from a [String]. -Object? domGetConstructor(String constructorName) => - js_util.getProperty(domWindow, constructorName); - -/// Calls a constructor as a [String]. -Object? domCallConstructorString(String constructorName, List args) { - final Object? constructor = domGetConstructor(constructorName); - if (constructor == null) { - return null; - } - return js_util.callConstructor(constructor, args); + int? get total => _total?.toDart.toInt(); } /// The underlying DOM document. @@ -163,8 +201,11 @@ class DomDocument {} /// [DomDocument]'s required extension. extension DomDocumentExtension on DomDocument { + @JS('createEvent') + external DomEvent _createEvent(JSString eventType); + /// Creates an event. - external DomEvent createEvent(String eventType); + DomEvent createEvent(String eventType) => _createEvent(eventType.toJS); /// Creates a range. external DomRange createRange(); @@ -172,10 +213,9 @@ extension DomDocumentExtension on DomDocument { /// Gets the head element. external DomHTMLHeadElement? get head; - /// Creates a new element. - DomElement createElement(String name, [Object? options]) => - js_util.callMethod(this, 'createElement', - [name, if (options != null) options]) as DomElement; + /// Creates a [DomElement]. + @JS('createElement') + external DomElement createElement(JSString name); } /// Returns the top level document. @@ -185,14 +225,10 @@ external DomDocument get domDocument; /// Creates a new DOM event. DomEvent createDomEvent(String type, String name) { final DomEvent event = domDocument.createEvent(type); - event.initEvent(name, true, true); + event.initEvent(name.toJS, true.toJS, true.toJS); return event; } -/// Defines a new property on an Object. -@JS('Object.defineProperty') -external void objectDefineProperty(Object o, String symbol, dynamic desc); - /// A Range object. @JS() @staticInterop @@ -201,7 +237,7 @@ class DomRange {} /// [DomRange]'s required extension. extension DomRangeExtension on DomRange { /// Selects the provided node. - external void selectNode(DomNode node); + external JSVoid selectNode(DomNode node); } /// A node in the DOM. @@ -211,11 +247,14 @@ class DomNode extends DomEventTarget {} /// [DomNode]'s required extension. extension DomNodeExtension on DomNode { + @JS('innerText') + external set _innerText(JSString text); + /// Sets the innerText of this node. - external set innerText(String text); + set innerText(String text) => _innerText = text.toJS; /// Appends a node this node. - external void append(DomNode node); + external JSVoid append(DomNode node); } /// An element in the DOM. @@ -249,14 +288,23 @@ class DomMouseEvent extends DomUIEvent {} /// [DomMouseEvent]'s required extension. extension DomMouseEventExtension on DomMouseEvent { + @JS('offsetX') + external JSNumber get _offsetX; + /// Returns the current x offset. - external num get offsetX; + num get offsetX => _offsetX.toDart; + + @JS('offsetY') + external JSNumber get _offsetY; /// Returns the current y offset. - external num get offsetY; + num get offsetY => _offsetY.toDart; + + @JS('button') + external JSNumber get _button; /// Returns the current button. - external int get button; + int get button => _button.toDart.toInt(); } /// A DOM selection. @@ -267,10 +315,10 @@ class DomSelection {} /// [DomSelection]'s required extension. extension DomSelectionExtension on DomSelection { /// Removes all ranges from this selection. - external void removeAllRanges(); + external JSVoid removeAllRanges(); /// Adds a range to this selection. - external void addRange(DomRange range); + external JSVoid addRange(DomRange range); } /// A DOM html div element. @@ -280,7 +328,7 @@ class DomHTMLDivElement extends DomHTMLElement {} /// Factory constructor for [DomHTMLDivElement]. DomHTMLDivElement createDomHTMLDivElement() => - domDocument.createElement('div') as DomHTMLDivElement; + domDocument.createElement('div'.toJS) as DomHTMLDivElement; /// An html style element. @JS() @@ -295,7 +343,7 @@ extension DomHTMLStyleElementExtension on DomHTMLStyleElement { /// Factory constructor for [DomHTMLStyleElement]. DomHTMLStyleElement createDomHTMLStyleElement() => - domDocument.createElement('style') as DomHTMLStyleElement; + domDocument.createElement('style'.toJS) as DomHTMLStyleElement; /// CSS styles. @JS() @@ -310,11 +358,14 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { /// Sets the height. set height(String value) => setProperty('height', value); + @JS('setProperty') + external JSVoid _setProperty( + JSString propertyName, JSString value, JSString priority); + /// Sets a CSS property by name. void setProperty(String propertyName, String value, [String? priority]) { priority ??= ''; - js_util.callMethod( - this, 'setProperty', [propertyName, value, priority]); + _setProperty(propertyName.toJS, value.toJS, priority.toJS); } } @@ -335,12 +386,20 @@ class DomCSSStyleSheet extends DomStyleSheet {} /// [DomCSSStyleSheet]'s required extension. extension DomCSSStyleSheetExtension on DomCSSStyleSheet { + @JS('insertRule') + external JSNumber _insertRule1(JSString rule); + + @JS('insertRule') + external JSNumber _insertRule2(JSString rule, JSNumber index); + /// Inserts a rule into this style sheet. - int insertRule(String rule, [int? index]) => - js_util.callMethod(this, 'insertRule', [ - rule, - if (index != null) index.toDouble() - ]).toInt(); + int insertRule(String rule, [int? index]) { + if (index == null) { + return _insertRule1(rule.toJS).toDart.toInt(); + } else { + return _insertRule2(rule.toJS, index.toDouble().toJS).toDart.toInt(); + } + } } /// A list of token. @@ -350,6 +409,9 @@ class DomTokenList {} /// [DomTokenList]'s required extension. extension DomTokenListExtension on DomTokenList { + @JS('add') + external JSVoid _add(JSString value); + /// Adds a token to this token list. - external void add(String value); + void add(String value) => _add(value.toJS); } diff --git a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart index 3423e946434..2f8f08d40dc 100644 --- a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart +++ b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart @@ -5,7 +5,6 @@ import 'dart:ui' as ui; import 'package:flutter/rendering.dart'; -import 'package:js/js.dart'; import '../services/dom.dart'; import 'basic.dart'; @@ -115,7 +114,7 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { sheet.insertRule(_kClassRule, 0); sheet.insertRule(_kClassSelectionRule, 1); - htmlElement.addEventListener('mousedown', allowInterop((DomEvent event) { + htmlElement.addEventListener('mousedown', createDomEventListener((DomEvent event) { final DomMouseEvent mouseEvent = event as DomMouseEvent; if (mouseEvent.button != _kRightClickButton) { return; diff --git a/packages/flutter/test/painting/_test_http_request.dart b/packages/flutter/test/painting/_test_http_request.dart index 053484e72ef..debd39ee677 100644 --- a/packages/flutter/test/painting/_test_http_request.dart +++ b/packages/flutter/test/painting/_test_http_request.dart @@ -2,19 +2,25 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:js_interop'; + import 'package:flutter/src/services/dom.dart'; import 'package:js/js.dart'; import 'package:js/js_util.dart' as js_util; -void createGetter(Object mock, String key, T Function() get) { +/// Defines a new property on an Object. +@JS('Object.defineProperty') +external JSVoid objectDefineProperty(JSAny o, JSString symbol, JSAny desc); + +void createGetter(JSAny mock, String key, JSAny? Function() get) { objectDefineProperty( mock, - key, + key.toJS, js_util.jsify( - { - 'get': allowInterop(() => get()) + { + 'get': () { return get(); }.toJS } - )); + ) as JSAny); } @JS() @@ -22,46 +28,47 @@ void createGetter(Object mock, String key, T Function() get) { @anonymous class DomXMLHttpRequestMock { external factory DomXMLHttpRequestMock({ - void Function(String method, String url, bool async)? open, - String responseType = 'invalid', - int timeout = 10, - bool withCredentials = false, - void Function()? send, - void Function(String name, String value)? setRequestHeader, - void Function(String type, DomEventListener listener) addEventListener, + JSFunction? open, + JSString responseType, + JSNumber timeout, + JSBoolean withCredentials, + JSFunction? send, + JSFunction? setRequestHeader, + JSFunction addEventListener, }); } class TestHttpRequest { TestHttpRequest() { _mock = DomXMLHttpRequestMock( - open: allowInterop(open), - send: allowInterop(send), - setRequestHeader: allowInterop(setRequestHeader), - addEventListener: allowInterop(addEventListener), + open: open.toJS, + send: send.toJS, + setRequestHeader: setRequestHeader.toJS, + addEventListener: addEventListener.toJS, ); - createGetter(_mock, 'headers', () => headers); - createGetter(_mock, 'responseHeaders', () => responseHeaders); - createGetter(_mock, 'status', () => status); - createGetter(_mock, 'response', () => response); + createGetter(_mock, 'headers', () => js_util.jsify(headers) as JSAny); + createGetter(_mock, + 'responseHeaders', () => js_util.jsify(responseHeaders) as JSAny); + createGetter(_mock, 'status', () => status.toJS); + createGetter(_mock, 'response', () => js_util.jsify(response) as JSAny); } late DomXMLHttpRequestMock _mock; MockEvent? mockEvent; Map headers = {}; int status = -1; - dynamic response; + Object? response; Map get responseHeaders => headers; - void open(String method, String url, bool async) {} - void send() {} - void setRequestHeader(String name, String value) { - headers[name] = value; + JSVoid open(JSString method, JSString url, JSBoolean async) {} + JSVoid send() {} + JSVoid setRequestHeader(JSString name, JSString value) { + headers[name.toDart] = value.toDart; } - void addEventListener(String type, DomEventListener listener) { - if (type == mockEvent?.type) { - listener(mockEvent!.event); + JSVoid addEventListener(JSString type, DomEventListener listener) { + if (type.toDart == mockEvent?.type) { + (listener.toDart as DartDomEventListener)(mockEvent!.event); } }