diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index c8102994..af9efe8d 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -119,7 +119,7 @@ class _InAppWebViewExampleScreenState extends State { key: webViewKey, // contextMenu: contextMenu, initialUrlRequest: - URLRequest(url: Uri.parse("http://flutter.dev/")), + URLRequest(url: Uri.parse("https://flutter.dev")), // initialFile: "assets/index.html", initialUserScripts: UnmodifiableListView([]), initialSettings: settings, diff --git a/example/web/index.html b/example/web/index.html index f99fc531..a5e76af7 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -31,6 +31,9 @@ flutter_inappwebview_example + + + + + + + + + + + + flutter_inappwebview_example + + + +

Simple Page 2

+ Go to page 1 + + diff --git a/example/web/page.html b/example/web/page.html new file mode 100644 index 00000000..ac3f3139 --- /dev/null +++ b/example/web/page.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + flutter_inappwebview_example + + + +

Simple Page 1

+ Go to page 2 + + diff --git a/lib/assets/web/web_support.js b/lib/assets/web/web_support.js new file mode 100644 index 00000000..d4b48b12 --- /dev/null +++ b/lib/assets/web/web_support.js @@ -0,0 +1,63 @@ +window.flutter_inappwebview = { + viewId: null, + iframeId: null, + iframe: null, + prepare: function () { + var iframe = document.getElementById(window.flutter_inappwebview.iframeId); + if (iframe != null) { + window.flutter_inappwebview.iframe = iframe; + iframe.addEventListener('load', function (event) { + var url = iframe.src; + try { + url = iframe.contentWindow.location.href; + } catch (e) { + console.log(e); + } + window.flutter_inappwebview.nativeCommunication('iframeLoaded', window.flutter_inappwebview.viewId, [url]); + }); + } + }, + reload: function () { + var iframe = window.flutter_inappwebview.iframe; + if (iframe != null && iframe.contentWindow != null) { + try { + iframe.contentWindow.location.reload(); + } catch (e) { + console.log(e); + iframe.contentWindow.location.href = iframe.src; + } + } + }, + goBack: function () { + var iframe = window.flutter_inappwebview.iframe; + if (iframe != null) { + try { + iframe.contentWindow.history.back(); + } catch (e) { + console.log(e); + } + } + }, + goForward: function () { + var iframe = window.flutter_inappwebview.iframe; + if (iframe != null) { + try { + iframe.contentWindow.history.forward(); + } catch (e) { + console.log(e); + } + } + }, + evaluateJavascript: function (source) { + var iframe = window.flutter_inappwebview.iframe; + var result = null; + if (iframe != null) { + try { + result = iframe.contentWindow.eval(source); + } catch (e) { + console.log(e); + } + } + return result; + } +}; diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index 1d69ee2e..a30eacaf 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -1385,9 +1385,13 @@ class InAppWebViewController /// ///**NOTE for Android**: when loading an URL Request using "POST" method, headers are ignored. /// + ///**NOTE for Web**: if method is "GET" and headers are empty, it will change the `src` of the iframe. + ///For all other cases it will try to create an XMLHttpRequest and load the result inside the iframe. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.loadUrl](https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String))). If method is "POST", [Official API - WebView.postUrl](https://developer.android.com/reference/android/webkit/WebView#postUrl(java.lang.String,%20byte[])) ///- iOS ([Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1414954-load). If [allowingReadAccessTo] is used, [Official API - WKWebView.loadFileURL](https://developer.apple.com/documentation/webkit/wkwebview/1414973-loadfileurl)) + ///- Web Future loadUrl( {required URLRequest urlRequest, @Deprecated('Use allowingReadAccessTo instead') @@ -1432,7 +1436,9 @@ class InAppWebViewController /// ///- [mimeType] argument specifies the format of the data. The default value is `"text/html"`. ///- [encoding] argument specifies the encoding of the data. The default value is `"utf8"`. + ///**NOTE**: not used on Web. ///- [historyUrl] is an Android-specific argument that represents the URL to use as the history entry. The default value is `about:blank`. If non-null, this must be a valid URL. + ///**NOTE**: not used on Web. ///- [allowingReadAccessTo], used in combination with [baseUrl] (using the `file://` scheme), ///it represents the URL from which to read the web content. ///This [baseUrl] must be a file-based URL (using the `file://` scheme). @@ -1443,6 +1449,7 @@ class InAppWebViewController ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.loadDataWithBaseURL](https://developer.android.com/reference/android/webkit/WebView#loadDataWithBaseURL(java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String))) ///- iOS ([Official API - WKWebView.loadHTMLString](https://developer.apple.com/documentation/webkit/wkwebview/1415004-loadhtmlstring) or [Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1415011-load)) + ///- Web Future loadData( {required String data, String mimeType = "text/html", @@ -1511,6 +1518,7 @@ class InAppWebViewController ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.loadUrl](https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String))) ///- iOS ([Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1414954-load)) + ///- Web Future loadFile({required String assetFilePath}) async { assert(assetFilePath.isNotEmpty); Map args = {}; @@ -1520,9 +1528,12 @@ class InAppWebViewController ///Reloads the WebView. /// + ///**NOTE**: on Web, if `window.location.reload()` is not accessible inside the iframe, it will reload using the iframe `src` attribute. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.reload](https://developer.android.com/reference/android/webkit/WebView#reload())) ///- iOS ([Official API - WKWebView.reload](https://developer.apple.com/documentation/webkit/wkwebview/1414969-reload)) + ///- Web ([Official API - Location.reload](https://developer.mozilla.org/en-US/docs/Web/API/Location/reload)) Future reload() async { Map args = {}; await _channel.invokeMethod('reload', args); @@ -1533,6 +1544,7 @@ class InAppWebViewController ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.goBack](https://developer.android.com/reference/android/webkit/WebView#goBack())) ///- iOS ([Official API - WKWebView.goBack](https://developer.apple.com/documentation/webkit/wkwebview/1414952-goback)) + ///- Web ([Official API - History.back](https://developer.mozilla.org/en-US/docs/Web/API/History/back)) Future goBack() async { Map args = {}; await _channel.invokeMethod('goBack', args); @@ -1553,6 +1565,7 @@ class InAppWebViewController ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.goForward](https://developer.android.com/reference/android/webkit/WebView#goForward())) ///- iOS ([Official API - WKWebView.goForward](https://developer.apple.com/documentation/webkit/wkwebview/1414993-goforward)) + ///- Web ([Official API - History.forward](https://developer.mozilla.org/en-US/docs/Web/API/History/forward)) Future goForward() async { Map args = {}; await _channel.invokeMethod('goForward', args); @@ -1630,6 +1643,7 @@ class InAppWebViewController ///Those changes remain visible to all scripts, regardless of which content world you specify. ///For more information about content worlds, see [ContentWorld]. ///Available on iOS 14.0+. + ///**NOTE**: not used on Web. /// ///**NOTE**: This method shouldn't be called in the [WebView.onWebViewCreated] or [WebView.onLoadStart] events, ///because, in these events, the [WebView] is not ready to handle it yet. @@ -1639,6 +1653,7 @@ class InAppWebViewController ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.evaluateJavascript](https://developer.android.com/reference/android/webkit/WebView#evaluateJavascript(java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.String%3E))) ///- iOS ([Official API - WKWebView.evaluateJavascript](https://developer.apple.com/documentation/webkit/wkwebview/3656442-evaluatejavascript)) + ///- Web ([Official API - Window.eval](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval?retiredLocale=it)) Future evaluateJavascript( {required String source, ContentWorld? contentWorld}) async { Map args = {}; diff --git a/lib/src/in_app_webview/webview.dart b/lib/src/in_app_webview/webview.dart index e4f30d68..d1bd30b9 100644 --- a/lib/src/in_app_webview/webview.dart +++ b/lib/src/in_app_webview/webview.dart @@ -20,6 +20,7 @@ abstract class WebView { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- Web final void Function(InAppWebViewController controller)? onWebViewCreated; ///Event fired when the [WebView] starts to load an [url]. @@ -27,6 +28,7 @@ abstract class WebView { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onPageStarted](https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview)) + ///- Web final void Function(InAppWebViewController controller, Uri? url)? onLoadStart; ///Event fired when the [WebView] finishes loading an [url]. @@ -34,6 +36,7 @@ abstract class WebView { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onPageFinished](https://developer.android.com/reference/android/webkit/WebViewClient#onPageFinished(android.webkit.WebView,%20java.lang.String))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview)) + ///- Web ([Official API - Window.onload](https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event)) final void Function(InAppWebViewController controller, Uri? url)? onLoadStop; ///Event fired when the [WebView] encounters an error loading an [url]. diff --git a/lib/src/web/in_app_web_view_web_element.dart b/lib/src/web/in_app_web_view_web_element.dart index 44695f74..eda71b9d 100644 --- a/lib/src/web/in_app_web_view_web_element.dart +++ b/lib/src/web/in_app_web_view_web_element.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'dart:html'; +import 'dart:js' as js; import '../in_app_webview/in_app_webview_settings.dart'; import '../types.dart'; @@ -16,12 +17,16 @@ class InAppWebViewWebElement { String? initialFile; late InAppWebViewSettings settings; + late js.JsObject bridgeJsObject; + WebHistory webHistory = WebHistory(list: [], currentIndex: -1); InAppWebViewWebElement({required int viewId, required BinaryMessenger messenger}) { this._viewId = viewId; this._messenger = messenger; iframe = IFrameElement() ..id = 'flutter_inappwebview-$_viewId' + ..style.height = '100%' + ..style.width = '100%' ..style.border = 'none'; _channel = MethodChannel( @@ -32,19 +37,16 @@ class InAppWebViewWebElement { this._channel.setMethodCallHandler(handleMethodCall); - iframe.addEventListener('load', (event) async { - var obj = { - "url": iframe.src - }; - _channel.invokeMethod("onLoadStart", obj); - await Future.delayed(Duration(milliseconds: 100)); - _channel.invokeMethod("onLoadStop", obj); - }); + bridgeJsObject = js.JsObject.fromBrowserObject(js.context['flutter_inappwebview']); + bridgeJsObject['viewId'] = _viewId; + bridgeJsObject['iframeId'] = iframe.id; } /// Handles method calls over the MethodChannel of this plugin. Future handleMethodCall(MethodCall call) async { switch (call.method) { + case "getIFrameId": + return iframe.id; case "loadUrl": URLRequest urlRequest = URLRequest.fromMap(call.arguments["urlRequest"].cast())!; await _loadUrl(urlRequest: urlRequest); @@ -61,8 +63,15 @@ class InAppWebViewWebElement { case "reload": await _reload(); break; - case "getIFrameId": - return iframe.id; + case "goBack": + await _goBack(); + break; + case "goForward": + await _goForward(); + break; + case "evaluateJavascript": + String source = call.arguments["source"]; + return await _evaluateJavascript(source: source); default: throw PlatformException( code: 'Unimplemented', @@ -78,19 +87,13 @@ class InAppWebViewWebElement { if (settings.iframeSandox != null) { iframe.setAttribute("sandbox", settings.iframeSandox ?? ""); } - var width = settings.iframeWidth ?? iframe.width; - if (width == null || width.isEmpty) { - width = '100%'; - } - var height = settings.iframeHeight ?? iframe.height; - if (height == null || height.isEmpty) { - height = '100%'; - } - iframe.width = iframe.style.width = width; - iframe.height = iframe.style.height = height; + iframe.style.width = settings.iframeWidth ?? iframe.style.width; + iframe.style.height = settings.iframeHeight ?? iframe.style.height; iframe.referrerPolicy = settings.iframeReferrerPolicy ?? iframe.referrerPolicy; iframe.name = settings.iframeName ?? iframe.name; iframe.csp = settings.iframeCsp ?? iframe.csp; + + bridgeJsObject.callMethod("prepare"); } void makeInitialLoad() async { @@ -129,20 +132,48 @@ class InAppWebViewWebElement { } else { iframe.src = _convertHttpResponseToData(await _makeRequest(urlRequest)); } + var obj = { + "url": iframe.src + }; + _channel.invokeMethod("onLoadStart", obj); } Future _loadData({required String data, String mimeType = "text/html"}) async { iframe.src = 'data:$mimeType,' + Uri.encodeFull(data); + var obj = { + "url": iframe.src + }; + _channel.invokeMethod("onLoadStart", obj); } Future _loadFile({required String assetFilePath}) async { iframe.src = assetFilePath; + var obj = { + "url": iframe.src + }; + _channel.invokeMethod("onLoadStart", obj); } Future _reload() async { - var src = iframe.src; - if (src != null) { - iframe.contentWindow?.location.href = src; - } + bridgeJsObject.callMethod("reload"); + } + + Future _goBack() async { + bridgeJsObject.callMethod("goBack"); + } + + Future _goForward() async { + bridgeJsObject.callMethod("goForward"); + } + + Future _evaluateJavascript({required String source}) async { + return bridgeJsObject.callMethod("evaluateJavascript", [source]); + } + + onIFrameLoaded(String url) async { + var obj = { + "url": url + }; + _channel.invokeMethod("onLoadStop", obj); } } diff --git a/lib/src/web/web_platform.dart b/lib/src/web/web_platform.dart index 25b90e83..d40e850c 100644 --- a/lib/src/web/web_platform.dart +++ b/lib/src/web/web_platform.dart @@ -6,6 +6,8 @@ import 'shims/dart_ui.dart' as ui; import 'in_app_web_view_web_element.dart'; +import 'package:js/js.dart'; + /// Builds an iframe based WebView. /// /// This is used as the default implementation for [WebView] on web. @@ -24,6 +26,7 @@ class FlutterInAppWebViewWebPlatform { static void registerWith(Registrar registrar) { final pluginInstance = FlutterInAppWebViewWebPlatform(registrar); + _nativeCommunication = allowInterop(_dartNativeCommunication); } /// Handles method calls over the MethodChannel of this plugin. @@ -36,4 +39,24 @@ class FlutterInAppWebViewWebPlatform { ); } } +} + +/// Allows assigning a function to be callable from `window.flutter_inappwebview.nativeCommunication()` +@JS('flutter_inappwebview.nativeCommunication') +external set _nativeCommunication(void Function(String method, int viewId, [List? args]) f); + +/// Allows calling the assigned function from Dart as well. +@JS() +external void nativeCommunication(); + +void _dartNativeCommunication(String method, int viewId, [List? args]) { + if (WebPlatformManager.webViews.containsKey(viewId)) { + var webViewHtmlElement = WebPlatformManager.webViews[viewId] as InAppWebViewWebElement; + switch (method) { + case 'iframeLoaded': + String url = args![0] as String; + webViewHtmlElement.onIFrameLoaded(url); + break; + } + } } \ No newline at end of file diff --git a/lib/src/web/web_platform_manager.dart b/lib/src/web/web_platform_manager.dart index f59aa09a..acf8b3b3 100644 --- a/lib/src/web/web_platform_manager.dart +++ b/lib/src/web/web_platform_manager.dart @@ -1,3 +1,3 @@ -class WebPlatformManager { +abstract class WebPlatformManager { static final Map webViews = {}; } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 27639ddb..3574bbfa 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: flutter: sdk: flutter + js: ^0.6.3 dev_dependencies: flutter_test: @@ -38,6 +39,7 @@ flutter: assets: - packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html - packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css + - packages/flutter_inappwebview/assets/web/web_support.js # To add assets to your plugin package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg