Skip to content

Commit

Permalink
feat: Add onOpenWindow event (#2640)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ldoppea committed Jul 24, 2023
1 parent 2060bd6 commit 933fe19
Show file tree
Hide file tree
Showing 18 changed files with 355 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;

import androidx.annotation.RequiresApi;
Expand All @@ -29,6 +30,7 @@
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.uimanager.UIManagerHelper;
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopOpenWindowEvent;

import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -77,6 +79,8 @@ public class RNCWebChromeClient extends WebChromeClient implements LifecycleEven
protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
protected boolean mAllowsProtectedMedia = false;

protected boolean mHasOnOpenWindowEvent = false;

public RNCWebChromeClient(RNCWebView webView) {
this.mWebView = webView;
}
Expand All @@ -85,6 +89,24 @@ public RNCWebChromeClient(RNCWebView webView) {
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {

final WebView newWebView = new WebView(view.getContext());

if(mHasOnOpenWindowEvent) {
newWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading (WebView subview, String url) {
WritableMap event = Arguments.createMap();
event.putString("targetUrl", url);

((RNCWebView) view).dispatchEvent(
view,
new TopOpenWindowEvent(view.getId(), event)
);

return true;
}
});
}

final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(newWebView);
resultMsg.sendToTarget();
Expand Down Expand Up @@ -343,4 +365,8 @@ public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) {
public void setAllowsProtectedMedia(boolean enabled) {
mAllowsProtectedMedia = enabled;
}

public void setHasOnOpenWindowEvent(boolean hasEvent) {
mHasOnOpenWindowEvent = hasEvent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class RNCWebViewManagerImpl {
private var mAllowsProtectedMedia = false
private var mDownloadingMessage: String? = null
private var mLackPermissionToDownloadMessage: String? = null
private var mHasOnOpenWindowEvent = false

private var mUserAgent: String? = null
private var mUserAgentWithApplicationName: String? = null
Expand Down Expand Up @@ -206,6 +207,7 @@ class RNCWebViewManagerImpl {
}
}
webChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia);
webChromeClient.setHasOnOpenWindowEvent(mHasOnOpenWindowEvent);
webView.webChromeClient = webChromeClient
} else {
var webChromeClient = webView.webChromeClient as RNCWebChromeClient?
Expand All @@ -216,6 +218,7 @@ class RNCWebViewManagerImpl {
}
}
webChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia);
webChromeClient.setHasOnOpenWindowEvent(mHasOnOpenWindowEvent);
webView.webChromeClient = webChromeClient
}
}
Expand Down Expand Up @@ -582,6 +585,11 @@ class RNCWebViewManagerImpl {
mLackPermissionToDownloadMessage = value
}

fun setHasOnOpenWindowEvent(view: RNCWebView, value: Boolean) {
mHasOnOpenWindowEvent = value
setupWebChromeClient(view)
}

fun setMinimumFontSize(view: RNCWebView, value: Int) {
view.settings.minimumFontSize = value
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.reactnativecommunity.webview.events

import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

/**
* Event emitted when the WebView opens a new Window (i.e: target=_blank)
*/
class TopOpenWindowEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopOpenWindowEvent>(viewId) {
companion object {
const val EVENT_NAME = "topOpenWindow"
}

override fun getEventName(): String = EVENT_NAME

override fun canCoalesce(): Boolean = false

override fun getCoalescingKey(): Short = 0

override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import com.reactnativecommunity.webview.events.TopOpenWindowEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;

import android.webkit.WebChromeClient;

import org.json.JSONException;
import org.json.JSONObject;

Expand Down Expand Up @@ -199,6 +202,12 @@ public void setLackPermissionToDownloadMessage(RNCWebView view, @Nullable String
mRNCWebViewManagerImpl.setLackPermissionToDownloadMessage(value);
}

@Override
@ReactProp(name = "hasOnOpenWindowEvent")
public void setHasOnOpenWindowEvent(RNCWebView view, boolean hasEvent) {
mRNCWebViewManagerImpl.setHasOnOpenWindowEvent(view, hasEvent);
}

@Override
@ReactProp(name = "mediaPlaybackRequiresUserAction")
public void setMediaPlaybackRequiresUserAction(RNCWebView view, boolean value) {
Expand Down Expand Up @@ -502,6 +511,7 @@ public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone"));
export.put(TopCustomMenuSelectionEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomMenuSelection"));
export.put(TopOpenWindowEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOpenWindow"));
return export;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import com.reactnativecommunity.webview.events.TopOpenWindowEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;

import android.graphics.Color;
import android.webkit.WebChromeClient;

import org.json.JSONException;
import org.json.JSONObject;
Expand Down Expand Up @@ -170,6 +172,11 @@ public void setLackPermissionToDownloadMessage(RNCWebView view, @Nullable String
mRNCWebViewManagerImpl.setLackPermissionToDownloadMessage(value);
}

@ReactProp(name = "hasOnOpenWindowEvent")
public void setHasOnOpenWindowEvent(RNCWebView view, boolean hasEvent) {
mRNCWebViewManagerImpl.setHasOnOpenWindowEvent(view, hasEvent);
}

@ReactProp(name = "mediaPlaybackRequiresUserAction")
public void setMediaPlaybackRequiresUserAction(RNCWebView view, boolean value) {
mRNCWebViewManagerImpl.setMediaPlaybackRequiresUserAction(view, value);
Expand Down Expand Up @@ -295,6 +302,7 @@ public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone"));
export.put(TopCustomMenuSelectionEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomMenuSelection"));
export.put(TopOpenWindowEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOpenWindow"));
return export;
}

Expand Down
15 changes: 15 additions & 0 deletions apple/RNCWebView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
_view.onFileDownload = nil;
}
}
if (oldViewProps.hasOnOpenWindowEvent != newViewProps.hasOnOpenWindowEvent) {
if (newViewProps.hasOnOpenWindowEvent) {
_view.onOpenWindow = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
auto webViewEventEmitter = std::static_pointer_cast<RNCWebViewEventEmitter const>(_eventEmitter);
facebook::react::RNCWebViewEventEmitter::OnOpenWindow data = {
.targetUrl = std::string([[dictionary valueForKey:@"targetUrl"] UTF8String])
};
webViewEventEmitter->onOpenWindow(data);
}
};
} else {
_view.onOpenWindow = nil;
}
}
//
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */
if (oldViewProps.contentMode != newViewProps.contentMode) {
Expand Down
1 change: 1 addition & 0 deletions apple/RNCWebViewImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
@property (nonatomic, copy) RCTDirectEventBlock onScroll;
@property (nonatomic, copy) RCTDirectEventBlock onContentProcessDidTerminate;
@property (nonatomic, copy) RCTDirectEventBlock onOpenWindow;


@property (nonatomic, weak) id<RNCWebViewDelegate> _Nullable delegate;
Expand Down
26 changes: 25 additions & 1 deletion apple/RNCWebViewImpl.m
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,15 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
NSURL *url = navigationAction.request.URL;

if (_onOpenWindow) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{@"targetUrl": url.absoluteString}];
_onOpenWindow(event);
} else {
[webView loadRequest:navigationAction.request];
}
}
return nil;
}
Expand Down Expand Up @@ -1208,6 +1216,21 @@ - (void) webView:(WKWebView *)webView
WKNavigationType navigationType = navigationAction.navigationType;
NSURLRequest *request = navigationAction.request;
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
BOOL hasTargetFrame = navigationAction.targetFrame != nil;

if (_onOpenWindow && !hasTargetFrame) {
// When OnOpenWindow should be called, we want to prevent the navigation
// If not prevented, the `decisionHandler` is called first and after that `createWebViewWithConfiguration` is called
// In that order the WebView's ref would be updated with the target URL even if `createWebViewWithConfiguration` does not call `loadRequest`
// So the WebView's document stays on the current URL, but the WebView's ref is replaced by the target URL
// By preventing the navigation here, we also prevent the WebView's ref mutation
// The counterpart is that we have to manually call `_onOpenWindow` here, because no navigation means no call to `createWebViewWithConfiguration`
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{@"targetUrl": request.URL.absoluteString}];
decisionHandler(WKNavigationActionPolicyCancel);
_onOpenWindow(event);
return;
}

if (_onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
Expand Down Expand Up @@ -1243,6 +1266,7 @@ - (void) webView:(WKWebView *)webView
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)],
@"isTopFrame": @(isTopFrame),
@"hasTargetFrame": @(hasTargetFrame),
@"lockIdentifier": @(lockIdentifier)
}];
_onShouldStartLoadWithRequest(event);
Expand Down
4 changes: 4 additions & 0 deletions apple/RNCWebViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ - (RNCView *)view
RCT_EXPORT_VIEW_PROPERTY(onHttpError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onContentProcessDidTerminate, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onOpenWindow, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoaded, NSString)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL)
Expand Down Expand Up @@ -120,8 +121,11 @@ - (RNCView *)view
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(enableApplePay, BOOL)
RCT_EXPORT_VIEW_PROPERTY(menuItems, NSArray);

// New arch only
RCT_CUSTOM_VIEW_PROPERTY(hasOnFileDownload, BOOL, RNCWebViewImpl) {}
RCT_CUSTOM_VIEW_PROPERTY(hasOnOpenWindowEvent, BOOL, RNCWebViewImpl) {}

RCT_EXPORT_VIEW_PROPERTY(onCustomMenuSelection, RCTDirectEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(pullToRefreshEnabled, BOOL, RNCWebViewImpl) {
view.pullToRefreshEnabled = json == nil ? false : [RCTConvert BOOL: json];
Expand Down
2 changes: 1 addition & 1 deletion docs/Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The Android example app will built, the Metro bundler will launch, and the examp
#### For iOS:

```sh
pod install --project-directory=ios
pod install --project-directory=example/ios
yarn ios
```

Expand Down
35 changes: 35 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This document lays out the current public properties and methods for the React N
- [`onHttpError`](Reference.md#onhttperror)
- [`onMessage`](Reference.md#onmessage)
- [`onNavigationStateChange`](Reference.md#onnavigationstatechange)
- [`onOpenWindow`](Reference.md#onopenwindow)
- [`onContentProcessDidTerminate`](Reference.md#oncontentprocessdidterminate)
- [`onScroll`](Reference.md#onscroll)
- [`originWhitelist`](Reference.md#originwhitelist)
Expand Down Expand Up @@ -577,6 +578,37 @@ url

---

### `onOpenWindow`[](#props-index)<!-- Link generated with jump2header -->

Function that is invoked when the `WebView` should open a new window.

This happens when the JS calls `window.open('http://someurl', '_blank')` or when the user clicks on a `<a href="http://someurl" target="_blank">` link.

| Type | Required |
| -------- | -------- |
| function | No |

Example:

```jsx
<WebView
source={{ uri: 'https://reactnative.dev' }}
onOpenWindow={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
const { targetUrl } = nativeEvent
console.log('Intercepted OpenWindow for', targetUrl)
}}
/>
```

Function passed to onOpenWindow is called with a SyntheticEvent wrapping a nativeEvent with these properties:

```
targetUrl
```

---

### `onContentProcessDidTerminate`[](#props-index)

Function that is invoked when the `WebView` content process is terminated.
Expand Down Expand Up @@ -753,8 +785,11 @@ lockIdentifier
mainDocumentURL (iOS only)
navigationType (iOS only)
isTopFrame (iOS only)
hasTargetFrame (iOS only)
```

The `hasTargetFrame` prop is a boolean that is `false` when the navigation targets a new window or tab, otherwise it should be `true` ([more info](https://developer.apple.com/documentation/webkit/wknavigationaction/1401918-targetframe)). Note that this prop should always be `true` when `onOpenWindow` event is registered on the WebView because the `false` case is intercepted by this event.

---

### `startInLoadingState`[](#props-index)
Expand Down
14 changes: 14 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Messaging from './examples/Messaging';
import NativeWebpage from './examples/NativeWebpage';
import ApplePay from './examples/ApplePay';
import CustomMenu from './examples/CustomMenu';
import OpenWindow from './examples/OpenWindow';

const TESTS = {
Messaging: {
Expand Down Expand Up @@ -110,6 +111,14 @@ const TESTS = {
render() {
return <CustomMenu />;
},
},
OpenWindow: {
title: 'Open Window',
testId: 'OpenWindow',
description: 'Test to intercept new window events',
render() {
return <OpenWindow />;
},
}
};

Expand Down Expand Up @@ -208,6 +217,11 @@ export default class App extends Component<Props, State> {
title="CustomMenu"
onPress={() => this._changeTest('CustomMenu')}
/>
<Button
testID="testType_openwindow"
title="OpenWindow"
onPress={() => this._changeTest('OpenWindow')}
/>
</View>

{restarting ? null : (
Expand Down

0 comments on commit 933fe19

Please sign in to comment.