diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md index 8ab70f9a78d3..befd7e6422c2 100644 --- a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.1 + +* Fixes unreliable encoding of HTML to the iframe element. +* Adds JavascriptChannels and running Javascript support only for loading Html as a string. + ## 0.1.0+3 * Minor fixes for new analysis options. diff --git a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart index 232ecdd302b7..20e4429038b6 100644 --- a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_web_example/web_view.dart'; void main() { @@ -69,4 +70,39 @@ void main() { expect(element, isNotNull); expect(element!.src, secondaryUrl); }); + + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadHtmlString('
'); + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + + await controller.runJavascript('Echo.postMessage("hello");'); + expect(messagesReceived, equals(['hello'])); + }); } diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart index ffd3367d33f4..73b6c082b83e 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart @@ -30,6 +30,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.javascriptChannels, }) : super(key: key); /// The WebView platform that's used by this WebView. @@ -45,6 +46,9 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + final Set? javascriptChannels; + @override State createState() => _WebViewState(); } @@ -90,7 +94,7 @@ class _WebViewState extends State { webSettings: _webSettingsFromWidget(widget), ), javascriptChannelRegistry: - JavascriptChannelRegistry({}), + JavascriptChannelRegistry(widget.javascriptChannels), ); } } @@ -159,6 +163,11 @@ class WebViewController { return _webViewPlatformController.loadRequest(request); } + /// Loads an Html document. + Future loadHtmlString(String html) async { + return _webViewPlatformController.loadHtmlString(html); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. diff --git a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart index 637c24926275..c803639f06ed 100644 --- a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart +++ b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart @@ -4,10 +4,13 @@ import 'dart:async'; import 'dart:html'; +import 'dart:js' as js; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'shims/dart_ui.dart' as ui; @@ -41,15 +44,29 @@ class WebWebViewPlatform implements WebViewPlatform { if (onWebViewPlatformCreated == null) { return; } + final IFrameElement element = document.getElementById('webview-$viewId')! as IFrameElement; + + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + element, viewId, javascriptChannelRegistry); + + js.context['webview${viewId}_getWindow'] = (js.JsObject window) { + controller.window = window; + }; + + js.context['webview${viewId}_channel'] = (String name, String message) { + javascriptChannelRegistry?.channels.values + .firstWhere((JavascriptChannel channel) => channel.name == name) + .onMessageReceived(JavascriptMessage(message)); + }; + if (creationParams.initialUrl != null) { // ignore: unsafe_html element.src = creationParams.initialUrl; } - onWebViewPlatformCreated(WebWebViewPlatformController( - element, - )); + onWebViewPlatformCreated(controller); }, ); } @@ -64,8 +81,14 @@ class WebWebViewPlatform implements WebViewPlatform { /// Implementation of [WebViewPlatformController] for web. class WebWebViewPlatformController implements WebViewPlatformController { /// Constructs a [WebWebViewPlatformController]. - WebWebViewPlatformController(this._element); + WebWebViewPlatformController(this._element, + [this._viewId = 1, this._javascriptChannelRegistry]); + + /// The IFrame's Window object. + js.JsObject? window; + final JavascriptChannelRegistry? _javascriptChannelRegistry; + final int _viewId; final IFrameElement _element; HttpRequestFactory _httpRequestFactory = HttpRequestFactory(); @@ -103,7 +126,7 @@ class WebWebViewPlatformController implements WebViewPlatformController { @override Future evaluateJavascript(String javascript) { - throw UnimplementedError(); + return runJavascriptReturningResult(javascript); } @override @@ -149,12 +172,27 @@ class WebWebViewPlatformController implements WebViewPlatformController { @override Future runJavascript(String javascript) { - throw UnimplementedError(); + if (window == null) { + throw UnsupportedError( + 'Running Javascript is available only by loading the Html as a string', + ); + } + + return Future.value( + window!.callMethod('eval', [javascript])); } @override Future runJavascriptReturningResult(String javascript) { - throw UnimplementedError(); + if (window == null) { + throw UnsupportedError( + 'Running Javascript is available only by loading the Html as a string', + ); + } + + return Future.value( + window?.callMethod('eval', [javascript])) + .then((dynamic value) => value.toString()); } @override @@ -183,7 +221,10 @@ class WebWebViewPlatformController implements WebViewPlatformController { String? baseUrl, }) async { // ignore: unsafe_html - _element.src = 'data:text/html,${Uri.encodeFull(html)}'; + _element.srcdoc = preprocessHtml(html); + final Completer loaded = Completer(); + _element.addEventListener('load', (Event event) => loaded.complete()); + return loaded.future; } @override @@ -207,6 +248,35 @@ class WebWebViewPlatformController implements WebViewPlatformController { Future loadFlutterAsset(String key) { throw UnimplementedError(); } + + /// Change the Html before passing it to the iframe. + String preprocessHtml(String html) { + final dom.Document document = parse(html); + + final dom.Element scriptElement = document.createElement('script'); + final StringBuffer scriptContent = StringBuffer(); + + scriptContent.writeln('parent.webview${_viewId}_getWindow(window);'); + + _javascriptChannelRegistry?.channels + .forEach((String _, JavascriptChannel channel) { + final String funcName = 'parent.webview${_viewId}_channel'; + + scriptContent.writeln( + 'window.${channel.name} = { postMessage: (message) => $funcName("${channel.name}", message) };', + ); + }); + + scriptElement.text = scriptContent.toString(); + document.head?.insertBefore(scriptElement, document.head!.firstChild); + + String outputHtml = document.outerHtml; + if (!outputHtml.trim().startsWith('')) { + outputHtml = '$outputHtml'; + } + + return outputHtml; + } } /// Factory class for creating [HttpRequest] instances. diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml index a834c9b77d51..b21a8c9dd0ed 100644 --- a/packages/webview_flutter/webview_flutter_web/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_web description: A Flutter plugin that provides a WebView widget on web. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 0.1.0+3 +version: 0.1.1 environment: sdk: ">=2.14.0 <3.0.0" @@ -21,6 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter + html: ^0.15.0 webview_flutter_platform_interface: ^1.8.0 dev_dependencies: diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart index 6058dcf07272..72337ec3cb26 100644 --- a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart @@ -64,7 +64,8 @@ void main() { // Run controller.loadHtmlString('test html'); // Verify - verify(mockElement.src = 'data:text/html,${Uri.encodeFull('test html')}'); + verify(mockElement.srcdoc = + 'test html'); }); group('loadRequest', () {