Merge branch 'master' into docs/nav-state-changes
This commit is contained in:
commit
06c1bb0657
|
@ -2,6 +2,10 @@
|
|||
|
||||
**React Native WebView** is a modern, well-supported, and cross-platform WebView for React Native. It is intended to be a replacement for the built-in WebView (which will be [removed from core](https://github.com/react-native-community/discussions-and-proposals/pull/3)).
|
||||
|
||||
> We just swapped out the React Native WebView in our app with the version from React Native Community. The swap took less than a day, required almost no code modifications, and is faster and CSS works better. Props to everyone in the community (including those at Infinite Red) that helped get that component split out.
|
||||
|
||||
_Garrett McCullough, mobile engineer at Virta Health_
|
||||
|
||||
## Platforms Supported
|
||||
|
||||
- [x] iOS (both UIWebView and WKWebView)
|
||||
|
|
|
@ -1,91 +1,122 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.2.71'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
ext.kotlin_version = '1.2.71'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
//noinspection DifferentKotlinGradleVersion
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
|
||||
def DEFAULT_TARGET_SDK_VERSION = 27
|
||||
def DEFAULT_COMPILE_SDK_VERSION = 27
|
||||
def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
|
||||
def DEFAULT_TARGET_SDK_VERSION = 27
|
||||
|
||||
def getExtOrDefault(name, defaultValue) {
|
||||
return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
|
||||
buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
productFlavors {
|
||||
}
|
||||
lintOptions {
|
||||
disable 'GradleCompatible'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
compileSdkVersion getExtOrDefault('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
|
||||
buildToolsVersion getExtOrDefault('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion getExtOrDefault('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
disable 'GradleCompatible'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
|
||||
def found = false
|
||||
def defaultDir = null
|
||||
def androidSourcesName = 'React Native sources'
|
||||
|
||||
if (rootProject.ext.has('reactNativeAndroidRoot')) {
|
||||
defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
|
||||
} else {
|
||||
defaultDir = new File(
|
||||
projectDir,
|
||||
'/../../../node_modules/react-native/android'
|
||||
)
|
||||
}
|
||||
|
||||
if (defaultDir.exists()) {
|
||||
maven {
|
||||
url 'https://maven.google.com/'
|
||||
name 'Google'
|
||||
url defaultDir.toString()
|
||||
name androidSourcesName
|
||||
}
|
||||
|
||||
// Stolen from react-native-firebase, thanks dudes!
|
||||
def found = false
|
||||
logger.quiet(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
|
||||
found = true
|
||||
} else {
|
||||
def parentDir = rootProject.projectDir
|
||||
def reactNativeAndroidName = 'React Native (Node Modules)'
|
||||
|
||||
1.upto(4, {
|
||||
if (found) return true
|
||||
parentDir = parentDir.parentFile
|
||||
def reactNativeAndroid = new File(
|
||||
parentDir,
|
||||
'node_modules/react-native/android'
|
||||
)
|
||||
1.upto(5, {
|
||||
if (found) return true
|
||||
parentDir = parentDir.parentFile
|
||||
|
||||
if (reactNativeAndroid.exists()) {
|
||||
maven {
|
||||
url reactNativeAndroid.toString()
|
||||
name reactNativeAndroidName
|
||||
}
|
||||
def androidSourcesDir = new File(
|
||||
parentDir,
|
||||
'node_modules/react-native'
|
||||
)
|
||||
|
||||
println "${project.name}: using React Native sources from ${reactNativeAndroid.toString()}"
|
||||
found = true
|
||||
def androidPrebuiltBinaryDir = new File(
|
||||
parentDir,
|
||||
'node_modules/react-native/android'
|
||||
)
|
||||
|
||||
if (androidPrebuiltBinaryDir.exists()) {
|
||||
maven {
|
||||
url androidPrebuiltBinaryDir.toString()
|
||||
name androidSourcesName
|
||||
}
|
||||
})
|
||||
|
||||
if (!found) {
|
||||
throw new GradleException(
|
||||
"${project.name}: unable to locate React Native Android sources, " +
|
||||
"ensure you have you installed React Native as a dependency and try again."
|
||||
)
|
||||
}
|
||||
logger.quiet(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
|
||||
found = true
|
||||
} else if (androidSourcesDir.exists()) {
|
||||
maven {
|
||||
url androidSourcesDir.toString()
|
||||
name androidSourcesName
|
||||
}
|
||||
|
||||
logger.quiet(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
|
||||
found = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
throw new GradleException(
|
||||
"${project.name}: unable to locate React Native android sources. " +
|
||||
"Ensure you have you installed React Native as a dependency in your project and try again."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.facebook.react:react-native:+'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
//noinspection GradleDynamicVersion
|
||||
api 'com.facebook.react:react-native:+'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
}
|
||||
|
|
|
@ -1,2 +1,13 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.reactnativecommunity.webview">
|
||||
</manifest>
|
||||
<application>
|
||||
<provider
|
||||
android:name=".RNCWebViewFileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.reactnativecommunity.webview;
|
||||
|
||||
import android.support.v4.content.FileProvider;
|
||||
|
||||
/**
|
||||
* Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
|
||||
*
|
||||
* See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
|
||||
*/
|
||||
public class RNCWebViewFileProvider extends FileProvider {
|
||||
|
||||
// This class intentionally left blank.
|
||||
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
package com.reactnativecommunity.webview;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -18,21 +23,25 @@ import java.util.Map;
|
|||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Picture;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.GeolocationPermissions;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.URLUtil;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
|
@ -51,11 +60,19 @@ import com.facebook.react.uimanager.annotations.ReactProp;
|
|||
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
|
||||
import com.facebook.react.uimanager.events.Event;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
|
||||
import com.reactnativecommunity.webview.events.TopMessageEvent;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
|
||||
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
@ -66,12 +83,14 @@ import org.json.JSONObject;
|
|||
* - GO_BACK
|
||||
* - GO_FORWARD
|
||||
* - RELOAD
|
||||
* - LOAD_URL
|
||||
*
|
||||
* {@link WebView} instances could emit following direct events:
|
||||
* - topLoadingFinish
|
||||
* - topLoadingStart
|
||||
* - topLoadingStart
|
||||
* - topLoadingProgress
|
||||
* - topShouldStartLoadWithRequest
|
||||
*
|
||||
* Each event will carry the following properties:
|
||||
* - target - view's react tag
|
||||
|
@ -85,6 +104,7 @@ import org.json.JSONObject;
|
|||
public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
||||
|
||||
protected static final String REACT_CLASS = "RNCWebView";
|
||||
private RNCWebViewPackage aPackage;
|
||||
|
||||
protected static final String HTML_ENCODING = "UTF-8";
|
||||
protected static final String HTML_MIME_TYPE = "text/html";
|
||||
|
@ -98,19 +118,18 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
public static final int COMMAND_STOP_LOADING = 4;
|
||||
public static final int COMMAND_POST_MESSAGE = 5;
|
||||
public static final int COMMAND_INJECT_JAVASCRIPT = 6;
|
||||
public static final int COMMAND_LOAD_URL = 7;
|
||||
|
||||
// Use `webView.loadUrl("about:blank")` to reliably reset the view
|
||||
// state and release page resources (including any running JavaScript).
|
||||
protected static final String BLANK_URL = "about:blank";
|
||||
|
||||
protected WebViewConfig mWebViewConfig;
|
||||
protected @Nullable WebView.PictureListener mPictureListener;
|
||||
|
||||
protected static class RNCWebViewClient extends WebViewClient {
|
||||
|
||||
protected boolean mLastLoadFailed = false;
|
||||
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
|
||||
protected @Nullable List<Pattern> mOriginWhitelist;
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView webView, String url) {
|
||||
|
@ -138,50 +157,16 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.equals(BLANK_URL)) return false;
|
||||
|
||||
// url blacklisting
|
||||
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
|
||||
ArrayList<Object> urlPrefixesForDefaultIntent =
|
||||
mUrlPrefixesForDefaultIntent.toArrayList();
|
||||
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
|
||||
if (url.startsWith((String) urlPrefix)) {
|
||||
launchIntent(view.getContext(), url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
launchIntent(view.getContext(), url);
|
||||
dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), url));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launchIntent(Context context, String url) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
String scheme = uri.getScheme() != null ? uri.getScheme() : "";
|
||||
String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
|
||||
String urlToCheck = scheme + "://" + authority;
|
||||
for (Pattern pattern : originWhitelist) {
|
||||
if (pattern.matcher(urlToCheck).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), request.getUrl().toString()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -230,10 +215,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
|
||||
mUrlPrefixesForDefaultIntent = specialUrls;
|
||||
}
|
||||
|
||||
public void setOriginWhitelist(List<Pattern> originWhitelist) {
|
||||
mOriginWhitelist = originWhitelist;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -244,6 +225,11 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
protected @Nullable String injectedJS;
|
||||
protected boolean messagingEnabled = false;
|
||||
protected @Nullable RNCWebViewClient mRNCWebViewClient;
|
||||
protected boolean sendContentSizeChangeEvents = false;
|
||||
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
|
||||
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
|
||||
}
|
||||
|
||||
|
||||
protected class RNCWebViewBridge {
|
||||
RNCWebView mContext;
|
||||
|
@ -284,6 +270,22 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
cleanupCallbacksAndDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int ow, int oh) {
|
||||
super.onSizeChanged(w, h, ow, oh);
|
||||
|
||||
if (sendContentSizeChangeEvents) {
|
||||
dispatchEvent(
|
||||
this,
|
||||
new ContentSizeChangeEvent(
|
||||
this.getId(),
|
||||
w,
|
||||
h
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWebViewClient(WebViewClient client) {
|
||||
super.setWebViewClient(client);
|
||||
|
@ -427,6 +429,25 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
|
||||
callback.invoke(origin, true, false);
|
||||
}
|
||||
|
||||
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
|
||||
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
|
||||
}
|
||||
protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
|
||||
getModule().startPhotoPickerIntent(filePathCallback, "");
|
||||
}
|
||||
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
|
||||
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
|
||||
String[] acceptTypes = fileChooserParams.getAcceptTypes();
|
||||
boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
|
||||
Intent intent = fileChooserParams.createIntent();
|
||||
return getModule().startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
|
||||
}
|
||||
});
|
||||
reactContext.addLifecycleEventListener(webView);
|
||||
mWebViewConfig.configWebView(webView);
|
||||
|
@ -453,6 +474,53 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
webView.setDownloadListener(new DownloadListener() {
|
||||
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
|
||||
RNCWebViewModule module = getModule();
|
||||
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
|
||||
|
||||
//Try to extract filename from contentDisposition, otherwise guess using URLUtil
|
||||
String fileName = "";
|
||||
try {
|
||||
fileName = contentDisposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1");
|
||||
fileName = URLDecoder.decode(fileName, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error extracting filename from contentDisposition: " + e);
|
||||
System.out.println("Falling back to URLUtil.guessFileName");
|
||||
fileName = URLUtil.guessFileName(url,contentDisposition,mimetype);
|
||||
}
|
||||
String downloadMessage = "Downloading " + fileName;
|
||||
|
||||
//Attempt to add cookie, if it exists
|
||||
URL urlObj = null;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
|
||||
String cookie = CookieManager.getInstance().getCookie(baseUrl);
|
||||
request.addRequestHeader("Cookie", cookie);
|
||||
System.out.println("Got cookie for DownloadManager: " + cookie);
|
||||
} catch (MalformedURLException e) {
|
||||
System.out.println("Error getting cookie for DownloadManager: " + e.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//Finish setting up request
|
||||
request.addRequestHeader("User-Agent", userAgent);
|
||||
request.setTitle(fileName);
|
||||
request.setDescription(downloadMessage);
|
||||
request.allowScanningByMediaScanner();
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
|
||||
|
||||
module.setDownloadRequest(request);
|
||||
|
||||
if (module.grantFileDownloaderPermissions()) {
|
||||
module.downloadFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return webView;
|
||||
}
|
||||
|
||||
|
@ -592,11 +660,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
|
||||
@ReactProp(name = "onContentSizeChange")
|
||||
public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
|
||||
if (sendContentSizeChangeEvents) {
|
||||
view.setPictureListener(getPictureListener());
|
||||
} else {
|
||||
view.setPictureListener(null);
|
||||
}
|
||||
((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents);
|
||||
}
|
||||
|
||||
@ReactProp(name = "mixedContentMode")
|
||||
|
@ -636,20 +700,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "originWhitelist")
|
||||
public void setOriginWhitelist(
|
||||
WebView view,
|
||||
@Nullable ReadableArray originWhitelist) {
|
||||
RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
|
||||
if (client != null && originWhitelist != null) {
|
||||
List<Pattern> whiteList = new LinkedList<>();
|
||||
for (int i = 0 ; i < originWhitelist.size() ; i++) {
|
||||
whiteList.add(Pattern.compile(originWhitelist.getString(i)));
|
||||
}
|
||||
client.setOriginWhitelist(whiteList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
|
||||
// Do not register default touch emitter and let WebView implementation handle touches
|
||||
|
@ -658,9 +708,13 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
|
||||
@Override
|
||||
public Map getExportedCustomDirectEventTypeConstants() {
|
||||
MapBuilder.Builder builder = MapBuilder.builder();
|
||||
builder.put("topLoadingProgress", MapBuilder.of("registrationName", "onLoadingProgress"));
|
||||
return builder.build();
|
||||
Map export = super.getExportedCustomDirectEventTypeConstants();
|
||||
if (export == null) {
|
||||
export = MapBuilder.newHashMap();
|
||||
}
|
||||
export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
|
||||
export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
|
||||
return export;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -671,7 +725,8 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
"reload", COMMAND_RELOAD,
|
||||
"stopLoading", COMMAND_STOP_LOADING,
|
||||
"postMessage", COMMAND_POST_MESSAGE,
|
||||
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT
|
||||
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
|
||||
"loadUrl", COMMAND_LOAD_URL
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -714,6 +769,12 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
RNCWebView reactWebView = (RNCWebView) root;
|
||||
reactWebView.evaluateJavascriptWithFallback(args.getString(0));
|
||||
break;
|
||||
case COMMAND_LOAD_URL:
|
||||
if (args == null) {
|
||||
throw new RuntimeException("Arguments for loading an url are null!");
|
||||
}
|
||||
root.loadUrl(args.getString(0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -724,27 +785,22 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
((RNCWebView) webView).cleanupCallbacksAndDestroy();
|
||||
}
|
||||
|
||||
protected WebView.PictureListener getPictureListener() {
|
||||
if (mPictureListener == null) {
|
||||
mPictureListener = new WebView.PictureListener() {
|
||||
@Override
|
||||
public void onNewPicture(WebView webView, Picture picture) {
|
||||
dispatchEvent(
|
||||
webView,
|
||||
new ContentSizeChangeEvent(
|
||||
webView.getId(),
|
||||
webView.getWidth(),
|
||||
webView.getContentHeight()));
|
||||
}
|
||||
};
|
||||
}
|
||||
return mPictureListener;
|
||||
}
|
||||
|
||||
protected static void dispatchEvent(WebView webView, Event event) {
|
||||
ReactContext reactContext = (ReactContext) webView.getContext();
|
||||
EventDispatcher eventDispatcher =
|
||||
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
|
||||
eventDispatcher.dispatchEvent(event);
|
||||
}
|
||||
|
||||
public RNCWebViewPackage getPackage() {
|
||||
return this.aPackage;
|
||||
}
|
||||
|
||||
public void setPackage(RNCWebViewPackage aPackage) {
|
||||
this.aPackage = aPackage;
|
||||
}
|
||||
|
||||
public RNCWebViewModule getModule() {
|
||||
return this.aPackage.getModule();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,381 @@
|
|||
|
||||
package com.reactnativecommunity.webview;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.FileProvider;
|
||||
import android.util.Log;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.modules.core.PermissionAwareActivity;
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
|
||||
public class RNCWebViewModule extends ReactContextBaseJavaModule {
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
|
||||
|
||||
private final ReactApplicationContext reactContext;
|
||||
private RNCWebViewPackage aPackage;
|
||||
|
||||
private static final int PICKER = 1;
|
||||
private static final int PICKER_LEGACY = 3;
|
||||
|
||||
private ValueCallback<Uri> filePathCallbackLegacy;
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
private Uri outputFileUri;
|
||||
|
||||
private DownloadManager.Request downloadRequest;
|
||||
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
|
||||
|
||||
final String DEFAULT_MIME_TYPES = "*/*";
|
||||
|
||||
public RNCWebViewModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
reactContext.addActivityEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RNCWebView";
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isFileUploadSupported(final Promise promise) {
|
||||
Boolean result = false;
|
||||
int current = Build.VERSION.SDK_INT;
|
||||
if (current >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
result = true;
|
||||
}
|
||||
if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
result = true;
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
|
||||
if (filePathCallback == null && filePathCallbackLegacy == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// based off of which button was pressed, we get an activity result and a file
|
||||
// the camera activity doesn't properly return the filename* (I think?) so we use
|
||||
// this filename instead
|
||||
switch (requestCode) {
|
||||
case PICKER:
|
||||
if (resultCode != RESULT_OK) {
|
||||
if (filePathCallback != null) {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
}
|
||||
} else {
|
||||
Uri result[] = this.getSelectedFiles(data, resultCode);
|
||||
if (result != null) {
|
||||
filePathCallback.onReceiveValue(result);
|
||||
} else {
|
||||
filePathCallback.onReceiveValue(new Uri[] { outputFileUri });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PICKER_LEGACY:
|
||||
Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
|
||||
filePathCallbackLegacy.onReceiveValue(result);
|
||||
break;
|
||||
|
||||
}
|
||||
filePathCallback = null;
|
||||
filePathCallbackLegacy= null;
|
||||
outputFileUri = null;
|
||||
}
|
||||
|
||||
public void onNewIntent(Intent intent) {
|
||||
}
|
||||
|
||||
private Uri[] getSelectedFiles(Intent data, int resultCode) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we have one file selected
|
||||
if (data.getData() != null) {
|
||||
if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// we have multiple files selected
|
||||
if (data.getClipData() != null) {
|
||||
final int numSelectedFiles = data.getClipData().getItemCount();
|
||||
Uri[] result = new Uri[numSelectedFiles];
|
||||
for (int i = 0; i < numSelectedFiles; i++) {
|
||||
result[i] = data.getClipData().getItemAt(i).getUri();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
|
||||
filePathCallbackLegacy = filePathCallback;
|
||||
|
||||
Intent fileChooserIntent = getFileChooserIntent(acceptType);
|
||||
Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
|
||||
|
||||
ArrayList<Parcelable> extraIntents = new ArrayList<>();
|
||||
if (acceptsImages(acceptType)) {
|
||||
extraIntents.add(getPhotoIntent());
|
||||
}
|
||||
if (acceptsVideo(acceptType)) {
|
||||
extraIntents.add(getVideoIntent());
|
||||
}
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
|
||||
|
||||
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
|
||||
getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
|
||||
} else {
|
||||
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
|
||||
filePathCallback = callback;
|
||||
|
||||
ArrayList<Parcelable> extraIntents = new ArrayList<>();
|
||||
if (acceptsImages(acceptTypes)) {
|
||||
extraIntents.add(getPhotoIntent());
|
||||
}
|
||||
if (acceptsVideo(acceptTypes)) {
|
||||
extraIntents.add(getVideoIntent());
|
||||
}
|
||||
|
||||
Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
|
||||
|
||||
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
|
||||
chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent);
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
|
||||
|
||||
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
|
||||
getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
|
||||
} else {
|
||||
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setDownloadRequest(DownloadManager.Request request) {
|
||||
this.downloadRequest = request;
|
||||
}
|
||||
|
||||
public void downloadFile() {
|
||||
DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
String downloadMessage = "Downloading";
|
||||
|
||||
dm.enqueue(this.downloadRequest);
|
||||
|
||||
Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public boolean grantFileDownloaderPermissions() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
PermissionAwareActivity activity = getPermissionAwareActivity();
|
||||
activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public RNCWebViewPackage getPackage() {
|
||||
return this.aPackage;
|
||||
}
|
||||
|
||||
public void setPackage(RNCWebViewPackage aPackage) {
|
||||
this.aPackage = aPackage;
|
||||
}
|
||||
|
||||
private Intent getPhotoIntent() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getVideoIntent() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
// @todo from experience, for Videos we get the data onActivityResult
|
||||
// so there's no need to store the Uri
|
||||
Uri outputVideoUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getFileChooserIntent(String acceptTypes) {
|
||||
String _acceptTypes = acceptTypes;
|
||||
if (acceptTypes.isEmpty()) {
|
||||
_acceptTypes = DEFAULT_MIME_TYPES;
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType(_acceptTypes);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Boolean acceptsImages(String types) {
|
||||
return types.isEmpty() || types.toLowerCase().contains("image");
|
||||
}
|
||||
private Boolean acceptsImages(String[] types) {
|
||||
return isArrayEmpty(types) || arrayContainsString(types, "image");
|
||||
}
|
||||
|
||||
private Boolean acceptsVideo(String types) {
|
||||
return types.isEmpty() || types.toLowerCase().contains("video");
|
||||
}
|
||||
private Boolean acceptsVideo(String[] types) {
|
||||
return isArrayEmpty(types) || arrayContainsString(types, "video");
|
||||
}
|
||||
|
||||
private Boolean arrayContainsString(String[] array, String pattern){
|
||||
for(String content : array){
|
||||
if(content.contains(pattern)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String[] getAcceptedMimeType(String[] types) {
|
||||
if (isArrayEmpty(types)) {
|
||||
return new String[]{DEFAULT_MIME_TYPES};
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
private Uri getOutputUri(String intentType) {
|
||||
File capturedFile = null;
|
||||
try {
|
||||
capturedFile = getCapturedFile(intentType);
|
||||
} catch (IOException e) {
|
||||
Log.e("CREATE FILE", "Error occurred while creating the File", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// for versions below 6.0 (23) we use the old File creation & permissions model
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return Uri.fromFile(capturedFile);
|
||||
}
|
||||
|
||||
// for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
|
||||
String packageName = getReactApplicationContext().getPackageName();
|
||||
return FileProvider.getUriForFile(getReactApplicationContext(), packageName+".fileprovider", capturedFile);
|
||||
}
|
||||
|
||||
private File getCapturedFile(String intentType) throws IOException {
|
||||
String prefix = "";
|
||||
String suffix = "";
|
||||
String dir = "";
|
||||
String filename = "";
|
||||
|
||||
if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
|
||||
prefix = "image-";
|
||||
suffix = ".jpg";
|
||||
dir = Environment.DIRECTORY_PICTURES;
|
||||
} else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
|
||||
prefix = "video-";
|
||||
suffix = ".mp4";
|
||||
dir = Environment.DIRECTORY_MOVIES;
|
||||
}
|
||||
|
||||
filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
|
||||
|
||||
// for versions below 6.0 (23) we use the old File creation & permissions model
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// only this Directory works on all tested Android versions
|
||||
// ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
|
||||
File storageDir = Environment.getExternalStoragePublicDirectory(dir);
|
||||
return new File(storageDir, filename);
|
||||
}
|
||||
|
||||
File storageDir = getReactApplicationContext().getExternalFilesDir(null);
|
||||
return File.createTempFile(filename, suffix, storageDir);
|
||||
}
|
||||
|
||||
private Boolean isArrayEmpty(String[] arr) {
|
||||
// when our array returned from getAcceptTypes() has no values set from the webview
|
||||
// i.e. <input type="file" />, without any "accept" attr
|
||||
// will be an array with one empty string element, afaik
|
||||
return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
|
||||
}
|
||||
|
||||
private PermissionAwareActivity getPermissionAwareActivity() {
|
||||
Activity activity = getCurrentActivity();
|
||||
if (activity == null) {
|
||||
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
|
||||
} else if (!(activity instanceof PermissionAwareActivity)) {
|
||||
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
|
||||
}
|
||||
return (PermissionAwareActivity) activity;
|
||||
}
|
||||
|
||||
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
|
||||
@Override
|
||||
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case FILE_DOWNLOAD_PERMISSION_REQUEST: {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (downloadRequest != null) {
|
||||
downloadFile();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getCurrentActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
package com.reactnativecommunity.webview;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -10,10 +11,19 @@ import com.facebook.react.bridge.NativeModule;
|
|||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
|
||||
public class RNCWebViewPackage implements ReactPackage {
|
||||
|
||||
private RNCWebViewManager manager;
|
||||
private RNCWebViewModule module;
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new RNCWebViewModule(reactContext));
|
||||
List<NativeModule> modulesList = new ArrayList<>();
|
||||
module = new RNCWebViewModule(reactContext);
|
||||
module.setPackage(this);
|
||||
modulesList.add(module);
|
||||
return modulesList;
|
||||
}
|
||||
|
||||
// Deprecated from RN 0.47
|
||||
|
@ -23,7 +33,12 @@ public class RNCWebViewPackage implements ReactPackage {
|
|||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
RNCWebViewManager viewManager = new RNCWebViewManager();
|
||||
return Arrays.<ViewManager>asList(viewManager);
|
||||
manager = new RNCWebViewManager();
|
||||
manager.setPackage(this);
|
||||
return Arrays.<ViewManager>asList(manager);
|
||||
}
|
||||
}
|
||||
|
||||
public RNCWebViewModule getModule() {
|
||||
return module;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.reactnativecommunity.webview.events;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.Event;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
|
||||
public class TopShouldStartLoadWithRequestEvent extends Event<TopMessageEvent> {
|
||||
public static final String EVENT_NAME = "topShouldStartLoadWithRequest";
|
||||
private final String mUrl;
|
||||
|
||||
public TopShouldStartLoadWithRequestEvent(int viewId, String url) {
|
||||
super(viewId);
|
||||
mUrl = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventName() {
|
||||
return EVENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCoalesce() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getCoalescingKey() {
|
||||
// All events for a given view can be coalesced.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatch(RCTEventEmitter rctEventEmitter) {
|
||||
WritableMap data = Arguments.createMap();
|
||||
data.putString("url", mUrl);
|
||||
data.putString("navigationType", "other");
|
||||
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="shared" path="." />
|
||||
</paths>
|
|
@ -110,3 +110,83 @@ class MyWeb extends Component {
|
|||
}
|
||||
```
|
||||
|
||||
### Add support for File Upload
|
||||
|
||||
##### iOS
|
||||
|
||||
For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
|
||||
|
||||
Photo capture:
|
||||
```
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Take pictures for certain activities</string>
|
||||
```
|
||||
|
||||
Gallery selection:
|
||||
```
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Select pictures for certain activities</string>
|
||||
```
|
||||
|
||||
Video recording:
|
||||
```
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Need microphone access for recording videos</string>
|
||||
```
|
||||
|
||||
##### Android
|
||||
|
||||
Add permission in AndroidManifest.xml:
|
||||
```xml
|
||||
<manifest ...>
|
||||
......
|
||||
|
||||
<!-- this is required only for Android 4.1-5.1 (api 16-22) -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
......
|
||||
</manifest>
|
||||
```
|
||||
|
||||
##### Check for File Upload support, with `static isFileUploadSupported()`
|
||||
|
||||
File Upload using `<input type="file" />` is not supported for Android 4.4 KitKat (see [details](https://github.com/delight-im/Android-AdvancedWebView/issues/4#issuecomment-70372146)):
|
||||
|
||||
```
|
||||
import { WebView } from "react-native-webview";
|
||||
|
||||
WebView.isFileUploadSupported().then(res => {
|
||||
if (res === true) {
|
||||
// file upload is supported
|
||||
} else {
|
||||
// not file upload support
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Add support for File Download
|
||||
|
||||
##### iOS
|
||||
|
||||
For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
|
||||
|
||||
Save to gallery:
|
||||
```
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Save pictures for certain activities.</string>
|
||||
```
|
||||
|
||||
##### Android
|
||||
|
||||
Add permission in AndroidManifest.xml:
|
||||
```xml
|
||||
<manifest ...>
|
||||
......
|
||||
|
||||
<!-- this is required to save files on Android -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
......
|
||||
</manifest>
|
||||
```
|
||||
|
|
|
@ -44,8 +44,11 @@ This document lays out the current public properties and methods for the React N
|
|||
- [`html`](Reference.md#html)
|
||||
- [`hideKeyboardAccessoryView`](Reference.md#hidekeyboardaccessoryview)
|
||||
- [`allowsBackForwardNavigationGestures`](Reference.md#allowsbackforwardnavigationgestures)
|
||||
- [`incognito`](Reference.md#incognito)
|
||||
- [`allowFileAccess`](Reference.md#allowFileAccess)
|
||||
- [`saveFormDataDisabled`](Reference.md#saveFormDataDisabled)
|
||||
- [`pagingEnabled`](Reference.md#pagingEnabled)
|
||||
- [`allowsLinkPreview`](Reference.md#allowsLinkPreview)
|
||||
|
||||
## Methods Index
|
||||
|
||||
|
@ -100,7 +103,7 @@ Controls whether to adjust the content inset for web views that are placed behin
|
|||
|
||||
### `injectedJavaScript`
|
||||
|
||||
Set this to provide JavaScript that will be injected into the web page when the view loads.
|
||||
Set this to provide JavaScript that will be injected into the web page when the view loads. Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
|
||||
|
||||
| Type | Required |
|
||||
| ------ | -------- |
|
||||
|
@ -273,6 +276,16 @@ Boolean value that forces the `WebView` to show the loading view on the first lo
|
|||
|
||||
---
|
||||
|
||||
### `style`
|
||||
|
||||
A style object that allow you to customize the `WebView` style. Please not that there are default styles (example: you need to add `flex: 0` to the style if you want to use `height` property).
|
||||
|
||||
| Type | Required |
|
||||
| ----- | -------- |
|
||||
| style | No |
|
||||
|
||||
---
|
||||
|
||||
### `decelerationRate`
|
||||
|
||||
A floating-point number that determines how quickly the scroll view decelerates after the user lifts their finger. You may also use the string shortcuts `"normal"` and `"fast"` which match the underlying iOS settings for `UIScrollViewDecelerationRateNormal` and `UIScrollViewDecelerationRateFast` respectively:
|
||||
|
@ -499,6 +512,16 @@ If true, this will be able horizontal swipe gestures when using the WKWebView. T
|
|||
|
||||
---
|
||||
|
||||
### `incognito`
|
||||
|
||||
Does not store any data within the lifetime of the WebView.
|
||||
|
||||
| Type | Required | Platform |
|
||||
| ------- | -------- | ------------- |
|
||||
| boolean | No | iOS WKWebView |
|
||||
|
||||
---
|
||||
|
||||
### `allowFileAccess`
|
||||
|
||||
If true, this will allow access to the file system via `file://` URI's. The default value is `false`.
|
||||
|
@ -517,6 +540,26 @@ Sets whether the WebView should disable saving form data. The default value is `
|
|||
| ------- | -------- | -------- |
|
||||
| boolean | No | Android |
|
||||
|
||||
---
|
||||
|
||||
### `pagingEnabled`
|
||||
|
||||
If the value of this property is true, the scroll view stops on multiples of the scroll view’s bounds when the user scrolls. The default value is false.
|
||||
|
||||
| Type | Required | Platform |
|
||||
| ------- | -------- | -------- |
|
||||
| boolean | No | iOS |
|
||||
|
||||
---
|
||||
|
||||
### `allowsLinkPreview`
|
||||
|
||||
A Boolean value that determines whether pressing on a link displays a preview of the destination for the link. In iOS this property is available on devices that support 3D Touch. In iOS 10 and later, the default value is true; before that, the default value is false.
|
||||
|
||||
| Type | Required | Platform |
|
||||
| ------- | -------- | -------- |
|
||||
| boolean | No | iOS |
|
||||
|
||||
## Methods
|
||||
|
||||
### `extraNativeComponentConfig()`
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
@interface RNCWKProcessPoolManager : NSObject
|
||||
|
||||
+ (instancetype) sharedManager;
|
||||
- (WKProcessPool *)sharedProcessPool;
|
||||
|
||||
@end
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "RNCWKProcessPoolManager.h"
|
||||
|
||||
@interface RNCWKProcessPoolManager() {
|
||||
WKProcessPool *_sharedProcessPool;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation RNCWKProcessPoolManager
|
||||
|
||||
+ (id) sharedManager {
|
||||
static RNCWKProcessPoolManager *_sharedManager = nil;
|
||||
@synchronized(self) {
|
||||
if(_sharedManager == nil) {
|
||||
_sharedManager = [[super alloc] init];
|
||||
}
|
||||
return _sharedManager;
|
||||
}
|
||||
}
|
||||
|
||||
- (WKProcessPool *)sharedProcessPool {
|
||||
if (!_sharedProcessPool) {
|
||||
_sharedProcessPool = [[WKProcessPool alloc] init];
|
||||
}
|
||||
return _sharedProcessPool;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
@protocol RNCWKWebViewDelegate <NSObject>
|
||||
|
||||
- (BOOL)webView:(RNCWKWebView *)webView
|
||||
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
|
||||
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
|
||||
withCallback:(RCTDirectEventBlock)callback;
|
||||
|
||||
@end
|
||||
|
@ -26,6 +26,7 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
|
|||
@property (nonatomic, assign) BOOL messagingEnabled;
|
||||
@property (nonatomic, copy) NSString *injectedJavaScript;
|
||||
@property (nonatomic, assign) BOOL scrollEnabled;
|
||||
@property (nonatomic, assign) BOOL pagingEnabled;
|
||||
@property (nonatomic, assign) CGFloat decelerationRate;
|
||||
@property (nonatomic, assign) BOOL allowsInlineMediaPlayback;
|
||||
@property (nonatomic, assign) BOOL bounces;
|
||||
|
@ -37,7 +38,10 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
|
|||
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
|
||||
@property (nonatomic, assign) BOOL hideKeyboardAccessoryView;
|
||||
@property (nonatomic, assign) BOOL allowsBackForwardNavigationGestures;
|
||||
@property (nonatomic, assign) BOOL incognito;
|
||||
@property (nonatomic, assign) BOOL useSharedProcessPool;
|
||||
@property (nonatomic, copy) NSString *userAgent;
|
||||
@property (nonatomic, assign) BOOL allowsLinkPreview;
|
||||
|
||||
- (void)postMessage:(NSString *)message;
|
||||
- (void)injectJavaScript:(NSString *)script;
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
#import "RNCWKWebView.h"
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTAutoInsetsProtocol.h>
|
||||
#import "RNCWKProcessPoolManager.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "objc/runtime.h"
|
||||
|
||||
static NSTimer *keyboardTimer;
|
||||
static NSString *const MessageHanderName = @"ReactNative";
|
||||
|
||||
// runtime trick to remove WKWebView keyboard default toolbar
|
||||
|
@ -40,12 +41,7 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
BOOL _savedHideKeyboardAccessoryView;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if(_webView){
|
||||
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
|
||||
}
|
||||
}
|
||||
- (void)dealloc{}
|
||||
|
||||
/**
|
||||
* See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html.
|
||||
|
@ -65,7 +61,6 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
return _webkitAvailable;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
|
@ -75,19 +70,6 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
_automaticallyAdjustContentInsets = YES;
|
||||
_contentInset = UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
// Workaround for a keyboard dismissal bug present in iOS 12
|
||||
// https://openradar.appspot.com/radar?id=5018321736957952
|
||||
if (@available(iOS 12.0, *)) {
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(keyboardWillHide)
|
||||
name:UIKeyboardWillHideNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(keyboardWillShow)
|
||||
name:UIKeyboardWillShowNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
@ -99,6 +81,12 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
};
|
||||
|
||||
WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
|
||||
if (_incognito) {
|
||||
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
|
||||
}
|
||||
if(self.useSharedProcessPool) {
|
||||
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
|
||||
}
|
||||
wkWebViewConfig.userContentController = [WKUserContentController new];
|
||||
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
|
||||
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
|
||||
|
@ -116,9 +104,12 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
_webView.UIDelegate = self;
|
||||
_webView.navigationDelegate = self;
|
||||
_webView.scrollView.scrollEnabled = _scrollEnabled;
|
||||
_webView.scrollView.pagingEnabled = _pagingEnabled;
|
||||
_webView.scrollView.bounces = _bounces;
|
||||
_webView.allowsLinkPreview = _allowsLinkPreview;
|
||||
[_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
|
||||
_webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
|
||||
|
||||
if (_userAgent) {
|
||||
_webView.customUserAgent = _userAgent;
|
||||
}
|
||||
|
@ -131,28 +122,19 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
[self addSubview:_webView];
|
||||
[self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView];
|
||||
[self visitSource];
|
||||
} else {
|
||||
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
|
||||
}
|
||||
}
|
||||
|
||||
-(void)keyboardWillHide
|
||||
- (void)removeFromSuperview
|
||||
{
|
||||
keyboardTimer = [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(keyboardDisplacementFix) userInfo:nil repeats:false];
|
||||
[[NSRunLoop mainRunLoop] addTimer:keyboardTimer forMode:NSRunLoopCommonModes];
|
||||
}
|
||||
-(void)keyboardWillShow
|
||||
{
|
||||
if (keyboardTimer != nil) {
|
||||
[keyboardTimer invalidate];
|
||||
if (_webView) {
|
||||
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
|
||||
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
|
||||
[_webView removeFromSuperview];
|
||||
_webView = nil;
|
||||
}
|
||||
}
|
||||
-(void)keyboardDisplacementFix
|
||||
{
|
||||
// https://stackoverflow.com/a/9637807/824966
|
||||
[UIView animateWithDuration:.25 animations:^{
|
||||
self.webView.scrollView.contentOffset = CGPointMake(0, 0);
|
||||
}];
|
||||
|
||||
[super removeFromSuperview];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
|
||||
|
@ -329,6 +311,88 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
|
||||
#pragma mark - WKNavigationDelegate methods
|
||||
|
||||
/**
|
||||
* alert
|
||||
*/
|
||||
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
|
||||
{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
completionHandler();
|
||||
}]];
|
||||
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* confirm
|
||||
*/
|
||||
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
completionHandler(YES);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
|
||||
completionHandler(NO);
|
||||
}]];
|
||||
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
|
||||
}
|
||||
|
||||
/**
|
||||
* prompt
|
||||
*/
|
||||
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.textColor = [UIColor lightGrayColor];
|
||||
textField.placeholder = defaultText;
|
||||
}];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
completionHandler([[alert.textFields lastObject] text]);
|
||||
}]];
|
||||
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
|
||||
}
|
||||
|
||||
/**
|
||||
* topViewController
|
||||
*/
|
||||
-(UIViewController *)topViewController{
|
||||
UIViewController *controller = [self topViewControllerWithRootViewController:[self getCurrentWindow].rootViewController];
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* topViewControllerWithRootViewController
|
||||
*/
|
||||
-(UIViewController *)topViewControllerWithRootViewController:(UIViewController *)viewController{
|
||||
if (viewController==nil) return nil;
|
||||
if (viewController.presentedViewController!=nil) {
|
||||
return [self topViewControllerWithRootViewController:viewController.presentedViewController];
|
||||
} else if ([viewController isKindOfClass:[UITabBarController class]]){
|
||||
return [self topViewControllerWithRootViewController:[(UITabBarController *)viewController selectedViewController]];
|
||||
} else if ([viewController isKindOfClass:[UINavigationController class]]){
|
||||
return [self topViewControllerWithRootViewController:[(UINavigationController *)viewController visibleViewController]];
|
||||
} else {
|
||||
return viewController;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* getCurrentWindow
|
||||
*/
|
||||
-(UIWindow *)getCurrentWindow{
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
if (window.windowLevel!=UIWindowLevelNormal) {
|
||||
for (UIWindow *wid in [UIApplication sharedApplication].windows) {
|
||||
if (window.windowLevel==UIWindowLevelNormal) {
|
||||
window = wid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decides whether to allow or cancel a navigation.
|
||||
* @see https://fburl.com/42r9fxob
|
||||
|
@ -402,6 +466,13 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
return;
|
||||
}
|
||||
|
||||
if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
|
||||
// Error code 102 "Frame load interrupted" is raised by the WKWebView
|
||||
// when the URL is from an http redirect. This is a common pattern when
|
||||
// implementing OAuth with a WebView.
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary:@{
|
||||
@"didFailProvisionalNavigation": @YES,
|
||||
|
@ -419,8 +490,12 @@ static NSString *const MessageHanderName = @"ReactNative";
|
|||
thenCall: (void (^)(NSString*)) callback
|
||||
{
|
||||
[self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
|
||||
if (error == nil && callback != nil) {
|
||||
callback([NSString stringWithFormat:@"%@", result]);
|
||||
if (error == nil) {
|
||||
if (callback != nil) {
|
||||
callback([NSString stringWithFormat:@"%@", result]);
|
||||
}
|
||||
} else {
|
||||
RCTLogError(@"Error evaluating injectedJavaScript: This is possibly due to an unsupported return type. Try adding true to the end of your injectedJavaScript string.");
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -45,7 +45,10 @@ RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
|||
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(allowsBackForwardNavigationGestures, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(incognito, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(pagingEnabled, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(userAgent, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL)
|
||||
|
||||
/**
|
||||
* Expose methods to enable messaging the webview.
|
||||
|
@ -69,6 +72,10 @@ RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RNCWKWebView) {
|
|||
view.bounces = json == nil ? true : [RCTConvert BOOL: json];
|
||||
}
|
||||
|
||||
RCT_CUSTOM_VIEW_PROPERTY(useSharedProcessPool, BOOL, RNCWKWebView) {
|
||||
view.useSharedProcessPool = json == nil ? true : [RCTConvert BOOL: json];
|
||||
}
|
||||
|
||||
RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWKWebView) {
|
||||
view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json];
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3515965E21A3C86000623BFA /* RNCWKProcessPoolManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */; };
|
||||
E914DBF6214474710071092B /* RNCUIWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E914DBF3214474710071092B /* RNCUIWebViewManager.m */; };
|
||||
E914DBF7214474710071092B /* RNCUIWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = E914DBF4214474710071092B /* RNCUIWebView.m */; };
|
||||
E91B351D21446E6C00F9801F /* RNCWKWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E91B351B21446E6C00F9801F /* RNCWKWebViewManager.m */; };
|
||||
|
@ -27,6 +28,8 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
134814201AA4EA6300B7C361 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCWebView.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNCWKProcessPoolManager.m; sourceTree = "<group>"; };
|
||||
3515965F21A3C87E00623BFA /* RNCWKProcessPoolManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNCWKProcessPoolManager.h; sourceTree = "<group>"; };
|
||||
E914DBF2214474710071092B /* RNCUIWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCUIWebView.h; sourceTree = "<group>"; };
|
||||
E914DBF3214474710071092B /* RNCUIWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCUIWebViewManager.m; sourceTree = "<group>"; };
|
||||
E914DBF4214474710071092B /* RNCUIWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCUIWebView.m; sourceTree = "<group>"; };
|
||||
|
@ -67,6 +70,8 @@
|
|||
E91B351C21446E6C00F9801F /* RNCWKWebView.m */,
|
||||
E91B351921446E6C00F9801F /* RNCWKWebViewManager.h */,
|
||||
E91B351B21446E6C00F9801F /* RNCWKWebViewManager.m */,
|
||||
3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */,
|
||||
3515965F21A3C87E00623BFA /* RNCWKProcessPoolManager.h */,
|
||||
134814211AA4EA7D00B7C361 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
|
@ -131,6 +136,7 @@
|
|||
E914DBF7214474710071092B /* RNCUIWebView.m in Sources */,
|
||||
E914DBF6214474710071092B /* RNCUIWebViewManager.m in Sources */,
|
||||
E91B351E21446E6C00F9801F /* RNCWKWebView.m in Sources */,
|
||||
3515965E21A3C86000623BFA /* RNCWKProcessPoolManager.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -8,34 +8,33 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ReactNative from 'react-native';
|
||||
import {
|
||||
import ReactNative, {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
requireNativeComponent,
|
||||
StyleSheet,
|
||||
UIManager,
|
||||
View,
|
||||
Image,
|
||||
requireNativeComponent
|
||||
NativeModules
|
||||
} from 'react-native';
|
||||
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import keyMirror from 'fbjs/lib/keyMirror';
|
||||
|
||||
import WebViewShared from './WebViewShared';
|
||||
import {
|
||||
defaultOriginWhitelist,
|
||||
createOnShouldStartLoadWithRequest,
|
||||
} from './WebViewShared';
|
||||
import type {
|
||||
WebViewEvent,
|
||||
WebViewError,
|
||||
WebViewErrorEvent,
|
||||
WebViewMessageEvent,
|
||||
WebViewNavigation,
|
||||
WebViewNavigationEvent,
|
||||
WebViewProgressEvent,
|
||||
WebViewSharedProps,
|
||||
WebViewSource,
|
||||
WebViewProgressEvent,
|
||||
} from './WebViewTypes';
|
||||
|
||||
const resolveAssetSource = Image.resolveAssetSource;
|
||||
|
@ -68,11 +67,18 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
scalesPageToFit: true,
|
||||
allowFileAccess: false,
|
||||
saveFormDataDisabled: false,
|
||||
originWhitelist: WebViewShared.defaultOriginWhitelist,
|
||||
originWhitelist: defaultOriginWhitelist,
|
||||
};
|
||||
|
||||
static isFileUploadSupported = async () => {
|
||||
// native implementation should return "true" only for Android 5+
|
||||
return NativeModules.RNCWebView.isFileUploadSupported();
|
||||
}
|
||||
|
||||
state = {
|
||||
viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE,
|
||||
viewState: this.props.startInLoadingState
|
||||
? WebViewState.LOADING
|
||||
: WebViewState.IDLE,
|
||||
lastErrorEvent: null,
|
||||
};
|
||||
|
||||
|
@ -125,12 +131,14 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
|
||||
const nativeConfig = this.props.nativeConfig || {};
|
||||
|
||||
const originWhitelist = (this.props.originWhitelist || []).map(
|
||||
WebViewShared.originWhitelistToRegex,
|
||||
);
|
||||
|
||||
let NativeWebView = nativeConfig.component || RNCWebView;
|
||||
|
||||
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||
this.onShouldStartLoadWithRequestCallback,
|
||||
this.props.originWhitelist,
|
||||
this.props.onShouldStartLoadWithRequest,
|
||||
);
|
||||
|
||||
const webView = (
|
||||
<NativeWebView
|
||||
ref={this.webViewRef}
|
||||
|
@ -151,6 +159,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
automaticallyAdjustContentInsets={
|
||||
this.props.automaticallyAdjustContentInsets
|
||||
}
|
||||
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
||||
onContentSizeChange={this.props.onContentSizeChange}
|
||||
onLoadingStart={this.onLoadingStart}
|
||||
onLoadingFinish={this.onLoadingFinish}
|
||||
|
@ -164,7 +173,6 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
allowUniversalAccessFromFileURLs={
|
||||
this.props.allowUniversalAccessFromFileURLs
|
||||
}
|
||||
originWhitelist={originWhitelist}
|
||||
mixedContentMode={this.props.mixedContentMode}
|
||||
saveFormDataDisabled={this.props.saveFormDataDisabled}
|
||||
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
|
||||
|
@ -284,11 +292,24 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
const { onMessage } = this.props;
|
||||
onMessage && onMessage(event);
|
||||
};
|
||||
|
||||
|
||||
onLoadingProgress = (event: WebViewProgressEvent) => {
|
||||
const { onLoadProgress} = this.props;
|
||||
const { onLoadProgress } = this.props;
|
||||
onLoadProgress && onLoadProgress(event);
|
||||
}
|
||||
};
|
||||
|
||||
onShouldStartLoadWithRequestCallback = (
|
||||
shouldStart: boolean,
|
||||
url: string,
|
||||
) => {
|
||||
if (shouldStart) {
|
||||
UIManager.dispatchViewManagerCommand(
|
||||
this.getWebViewHandle(),
|
||||
UIManager.RNCWebView.Commands.loadUrl,
|
||||
[String(url)],
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const RNCWebView = requireNativeComponent('RNCWebView');
|
||||
|
|
|
@ -25,7 +25,10 @@ import {
|
|||
import invariant from 'fbjs/lib/invariant';
|
||||
import keyMirror from 'fbjs/lib/keyMirror';
|
||||
|
||||
import WebViewShared from './WebViewShared';
|
||||
import {
|
||||
defaultOriginWhitelist,
|
||||
createOnShouldStartLoadWithRequest,
|
||||
} from './WebViewShared';
|
||||
import type {
|
||||
WebViewEvent,
|
||||
WebViewError,
|
||||
|
@ -130,9 +133,15 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
|
||||
static defaultProps = {
|
||||
useWebKit: true,
|
||||
originWhitelist: WebViewShared.defaultOriginWhitelist,
|
||||
originWhitelist: defaultOriginWhitelist,
|
||||
useSharedProcessPool: true,
|
||||
};
|
||||
|
||||
static isFileUploadSupported = async () => {
|
||||
// no native implementation for iOS, depends only on permissions
|
||||
return true;
|
||||
}
|
||||
|
||||
state = {
|
||||
viewState: this.props.startInLoadingState
|
||||
? WebViewState.LOADING
|
||||
|
@ -159,6 +168,15 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.props.useWebKit &&
|
||||
this.props.incognito
|
||||
) {
|
||||
console.warn(
|
||||
'The incognito property is not supported when useWebKit = false',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -199,40 +217,11 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
|
||||
const nativeConfig = this.props.nativeConfig || {};
|
||||
|
||||
let viewManager = nativeConfig.viewManager;
|
||||
|
||||
if (this.props.useWebKit) {
|
||||
viewManager = viewManager || RNCWKWebViewManager;
|
||||
} else {
|
||||
viewManager = viewManager || RNCUIWebViewManager;
|
||||
}
|
||||
|
||||
const compiledWhitelist = [
|
||||
'about:blank',
|
||||
...(this.props.originWhitelist || []),
|
||||
].map(WebViewShared.originWhitelistToRegex);
|
||||
const onShouldStartLoadWithRequest = event => {
|
||||
let shouldStart = true;
|
||||
const { url } = event.nativeEvent;
|
||||
const origin = WebViewShared.extractOrigin(url);
|
||||
const passesWhitelist = compiledWhitelist.some(x =>
|
||||
new RegExp(x).test(origin),
|
||||
);
|
||||
shouldStart = shouldStart && passesWhitelist;
|
||||
if (!passesWhitelist) {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
if (this.props.onShouldStartLoadWithRequest) {
|
||||
shouldStart =
|
||||
shouldStart &&
|
||||
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
|
||||
}
|
||||
invariant(viewManager != null, 'viewManager expected to be non-null');
|
||||
viewManager.startLoadWithResult(
|
||||
!!shouldStart,
|
||||
event.nativeEvent.lockIdentifier,
|
||||
);
|
||||
};
|
||||
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||
this.onShouldStartLoadWithRequestCallback,
|
||||
this.props.originWhitelist,
|
||||
this.props.onShouldStartLoadWithRequest,
|
||||
);
|
||||
|
||||
const decelerationRate = processDecelerationRate(
|
||||
this.props.decelerationRate,
|
||||
|
@ -264,6 +253,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
injectedJavaScript={this.props.injectedJavaScript}
|
||||
bounces={this.props.bounces}
|
||||
scrollEnabled={this.props.scrollEnabled}
|
||||
pagingEnabled={this.props.pagingEnabled}
|
||||
decelerationRate={decelerationRate}
|
||||
contentInset={this.props.contentInset}
|
||||
automaticallyAdjustContentInsets={
|
||||
|
@ -271,6 +261,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
}
|
||||
hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
|
||||
allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures}
|
||||
incognito={this.props.incognito}
|
||||
userAgent={this.props.userAgent}
|
||||
onLoadingStart={this._onLoadingStart}
|
||||
onLoadingFinish={this._onLoadingFinish}
|
||||
|
@ -285,6 +276,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
this.props.mediaPlaybackRequiresUserAction
|
||||
}
|
||||
dataDetectorTypes={this.props.dataDetectorTypes}
|
||||
useSharedProcessPool={this.props.useSharedProcessPool}
|
||||
allowsLinkPreview={this.props.allowsLinkPreview}
|
||||
{...nativeConfig.props}
|
||||
/>
|
||||
);
|
||||
|
@ -434,9 +427,25 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
};
|
||||
|
||||
_onLoadingProgress = (event: WebViewProgressEvent) => {
|
||||
const {onLoadProgress} = this.props;
|
||||
const { onLoadProgress } = this.props;
|
||||
onLoadProgress && onLoadProgress(event);
|
||||
}
|
||||
};
|
||||
|
||||
onShouldStartLoadWithRequestCallback = (
|
||||
shouldStart: boolean,
|
||||
url: string,
|
||||
lockIdentifier: number,
|
||||
) => {
|
||||
let viewManager = (this.props.nativeConfig || {}).viewManager;
|
||||
|
||||
if (this.props.useWebKit) {
|
||||
viewManager = viewManager || RNCWKWebViewManager;
|
||||
} else {
|
||||
viewManager = viewManager || RNCUIWebViewManager;
|
||||
}
|
||||
invariant(viewManager != null, 'viewManager expected to be non-null');
|
||||
viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: WebViewSharedProps) {
|
||||
if (!(prevProps.useWebKit && this.props.useWebKit)) {
|
||||
|
@ -444,6 +453,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
|
|||
}
|
||||
|
||||
this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
|
||||
this._showRedboxOnPropChanges(prevProps, 'incognito');
|
||||
this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
|
||||
this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
|
||||
|
||||
|
|
|
@ -8,19 +8,58 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
import { Linking } from 'react-native';
|
||||
import type {
|
||||
WebViewNavigationEvent,
|
||||
WebViewNavigation,
|
||||
OnShouldStartLoadWithRequest,
|
||||
} from './WebViewTypes';
|
||||
|
||||
const escapeStringRegexp = require('escape-string-regexp');
|
||||
const defaultOriginWhitelist = ['http://*', 'https://*'];
|
||||
|
||||
const WebViewShared = {
|
||||
defaultOriginWhitelist: ['http://*', 'https://*'],
|
||||
extractOrigin: (url: string): string => {
|
||||
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
|
||||
return result === null ? '' : result[0];
|
||||
},
|
||||
originWhitelistToRegex: (originWhitelist: string): string => {
|
||||
return escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
|
||||
},
|
||||
const extractOrigin = (url: string): string => {
|
||||
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
|
||||
return result === null ? '' : result[0];
|
||||
};
|
||||
|
||||
module.exports = WebViewShared;
|
||||
const originWhitelistToRegex = (originWhitelist: string): string =>
|
||||
escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
|
||||
|
||||
const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
|
||||
const origin = extractOrigin(url);
|
||||
return compiledWhitelist.some(x => new RegExp(x).test(origin));
|
||||
};
|
||||
|
||||
const compileWhitelist = (
|
||||
originWhitelist: ?$ReadOnlyArray<string>,
|
||||
): Array<string> =>
|
||||
['about:blank', ...(originWhitelist || [])].map(originWhitelistToRegex);
|
||||
|
||||
const createOnShouldStartLoadWithRequest = (
|
||||
loadRequest: (
|
||||
shouldStart: boolean,
|
||||
url: string,
|
||||
lockIdentifier: number,
|
||||
) => void,
|
||||
originWhitelist: ?$ReadOnlyArray<string>,
|
||||
onShouldStartLoadWithRequest: ?OnShouldStartLoadWithRequest,
|
||||
) => {
|
||||
return ({ nativeEvent }: WebViewNavigationEvent) => {
|
||||
let shouldStart = true;
|
||||
const { url, lockIdentifier } = nativeEvent;
|
||||
|
||||
if (!passesWhitelist(compileWhitelist(originWhitelist), url)) {
|
||||
Linking.openURL(url);
|
||||
shouldStart = false
|
||||
}
|
||||
|
||||
if (onShouldStartLoadWithRequest) {
|
||||
shouldStart = onShouldStartLoadWithRequest(nativeEvent);
|
||||
}
|
||||
|
||||
loadRequest(shouldStart, url, lockIdentifier);
|
||||
};
|
||||
};
|
||||
|
||||
export { defaultOriginWhitelist, createOnShouldStartLoadWithRequest };
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
import type {Node, Element, ComponentType} from 'react';
|
||||
import type { Node, Element, ComponentType } from 'react';
|
||||
|
||||
import type {SyntheticEvent} from 'CoreEventTypes';
|
||||
import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
|
||||
import type {ViewStyleProp} from 'StyleSheet';
|
||||
import type {ViewProps} from 'ViewPropTypes';
|
||||
import type { SyntheticEvent } from 'CoreEventTypes';
|
||||
import type { EdgeInsetsProp } from 'EdgeInsetsPropType';
|
||||
import type { ViewStyleProp } from 'StyleSheet';
|
||||
import type { ViewProps } from 'ViewPropTypes';
|
||||
|
||||
export type WebViewNativeEvent = $ReadOnly<{|
|
||||
url: string,
|
||||
|
@ -23,12 +23,13 @@ export type WebViewNativeEvent = $ReadOnly<{|
|
|||
title: string,
|
||||
canGoBack: boolean,
|
||||
canGoForward: boolean,
|
||||
lockIdentifier: number,
|
||||
|}>;
|
||||
|
||||
export type WebViewProgressEvent = $ReadOnly<{|
|
||||
...WebViewNativeEvent,
|
||||
progress: number,
|
||||
|}>
|
||||
...WebViewNativeEvent,
|
||||
progress: number,
|
||||
|}>;
|
||||
|
||||
export type WebViewNavigation = $ReadOnly<{|
|
||||
...WebViewNativeEvent,
|
||||
|
@ -118,22 +119,26 @@ export type WebViewSourceHtml = $ReadOnly<{|
|
|||
export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
|
||||
|
||||
export type WebViewNativeConfig = $ReadOnly<{|
|
||||
/*
|
||||
/**
|
||||
* The native component used to render the WebView.
|
||||
*/
|
||||
component?: ComponentType<WebViewSharedProps>,
|
||||
/*
|
||||
/**
|
||||
* Set props directly on the native component WebView. Enables custom props which the
|
||||
* original WebView doesn't pass through.
|
||||
*/
|
||||
props?: ?Object,
|
||||
/*
|
||||
/**
|
||||
* Set the ViewManager to use for communication with the native side.
|
||||
* @platform ios
|
||||
*/
|
||||
viewManager?: ?Object,
|
||||
|}>;
|
||||
|
||||
export type OnShouldStartLoadWithRequest = (
|
||||
event: WebViewNavigation,
|
||||
) => boolean;
|
||||
|
||||
export type IOSWebViewProps = $ReadOnly<{|
|
||||
/**
|
||||
* If true, use WKWebView instead of UIWebView.
|
||||
|
@ -168,6 +173,14 @@ export type IOSWebViewProps = $ReadOnly<{|
|
|||
*/
|
||||
scrollEnabled?: ?boolean,
|
||||
|
||||
/**
|
||||
* If the value of this property is true, the scroll view stops on multiples
|
||||
* of the scroll view’s bounds when the user scrolls.
|
||||
* The default value is false.
|
||||
* @platform ios
|
||||
*/
|
||||
pagingEnabled?: ?boolean,
|
||||
|
||||
/**
|
||||
* The amount by which the web view content is inset from the edges of
|
||||
* the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
|
||||
|
@ -197,17 +210,7 @@ export type IOSWebViewProps = $ReadOnly<{|
|
|||
*
|
||||
* @platform ios
|
||||
*/
|
||||
dataDetectorTypes?:
|
||||
| ?DataDetectorTypes
|
||||
| $ReadOnlyArray<DataDetectorTypes>,
|
||||
|
||||
/**
|
||||
* Function that allows custom handling of any web view requests. Return
|
||||
* `true` from the function to continue loading the request and `false`
|
||||
* to stop loading.
|
||||
* @platform ios
|
||||
*/
|
||||
onShouldStartLoadWithRequest?: (event: WebViewEvent) => mixed,
|
||||
dataDetectorTypes?: ?DataDetectorTypes | $ReadOnlyArray<DataDetectorTypes>,
|
||||
|
||||
/**
|
||||
* Boolean that determines whether HTML5 videos play inline or use the
|
||||
|
@ -229,10 +232,27 @@ export type IOSWebViewProps = $ReadOnly<{|
|
|||
* back-forward list navigations.
|
||||
*/
|
||||
allowsBackForwardNavigationGestures?: ?boolean,
|
||||
/**
|
||||
* A Boolean value indicating whether WebKit WebView should be created using a shared
|
||||
* process pool, enabling WebViews to share cookies and localStorage between each other.
|
||||
* Default is true but can be set to false for backwards compatibility.
|
||||
* @platform ios
|
||||
*/
|
||||
useSharedProcessPool?: ?boolean,
|
||||
/**
|
||||
* The custom user agent string.
|
||||
*/
|
||||
userAgent?: ?string,
|
||||
|
||||
/**
|
||||
* A Boolean value that determines whether pressing on a link
|
||||
* displays a preview of the destination for the link.
|
||||
*
|
||||
* This property is available on devices that support 3D Touch.
|
||||
* In iOS 10 and later, the default value is `true`; before that, the default value is `false`.
|
||||
* @platform ios
|
||||
*/
|
||||
allowsLinkPreview?: ?boolean,
|
||||
|}>;
|
||||
|
||||
export type AndroidWebViewProps = $ReadOnly<{|
|
||||
|
@ -277,7 +297,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
|
|||
*/
|
||||
saveFormDataDisabled?: ?boolean,
|
||||
|
||||
/*
|
||||
/**
|
||||
* Used on Android only, controls whether the given list of URL prefixes should
|
||||
* make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
|
||||
* default activity intent for those URL instead of loading it within the webview.
|
||||
|
@ -327,7 +347,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
|
|||
mixedContentMode?: ?('never' | 'always' | 'compatibility'),
|
||||
|}>;
|
||||
|
||||
export type WebViewSharedProps = $ReadOnly<{|
|
||||
export type WebViewSharedProps = $ReadOnly<{|
|
||||
...ViewProps,
|
||||
...IOSWebViewProps,
|
||||
...AndroidWebViewProps,
|
||||
|
@ -345,10 +365,19 @@ export type WebViewSharedProps = $ReadOnly<{|
|
|||
*/
|
||||
source?: ?WebViewSource,
|
||||
|
||||
/**
|
||||
* Does not store any data within the lifetime of the WebView.
|
||||
*/
|
||||
incognito?: ?boolean,
|
||||
|
||||
/**
|
||||
* Function that returns a view to show if there's an error.
|
||||
*/
|
||||
renderError: (errorDomain: ?string, errorCode: number, errorDesc: string) => Element<any>, // view to show if there's an error
|
||||
renderError: (
|
||||
errorDomain: ?string,
|
||||
errorCode: number,
|
||||
errorDesc: string,
|
||||
) => Element<any>, // view to show if there's an error
|
||||
|
||||
/**
|
||||
* Function that returns a loading indicator.
|
||||
|
@ -439,6 +468,13 @@ export type WebViewSharedProps = $ReadOnly<{|
|
|||
*/
|
||||
originWhitelist?: $ReadOnlyArray<string>,
|
||||
|
||||
/**
|
||||
* Function that allows custom handling of any web view requests. Return
|
||||
* `true` from the function to continue loading the request and `false`
|
||||
* to stop loading. The `navigationType` is always `other` on android.
|
||||
*/
|
||||
onShouldStartLoadWithRequest?: OnShouldStartLoadWithRequest,
|
||||
|
||||
/**
|
||||
* Override the native component used to render the WebView. Enables a custom native
|
||||
* WebView which uses the same JavaScript as the original WebView.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"Thibault Malbranche <malbranche.thibault@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"version": "2.8.0",
|
||||
"version": "3.1.1",
|
||||
"homepage": "https://github.com/react-native-community/react-native-webview#readme",
|
||||
"scripts": {
|
||||
"test:ios:flow": "flow check",
|
||||
|
|
|
@ -9,6 +9,12 @@ export interface WebViewNativeEvent {
|
|||
readonly canGoForward: boolean;
|
||||
}
|
||||
|
||||
export interface WebViewIOSLoadRequestEvent extends WebViewNativeEvent {
|
||||
target: number;
|
||||
lockIdentifier: number;
|
||||
navigationType: "click" | "formsubmit" | "backforward" | "reload" | "formresubmit" | "other";
|
||||
}
|
||||
|
||||
export interface WebViewProgressEvent extends WebViewNativeEvent {
|
||||
readonly progress: number;
|
||||
}
|
||||
|
@ -145,6 +151,14 @@ export interface IOSWebViewProps {
|
|||
*/
|
||||
scrollEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* If the value of this property is true, the scroll view stops on multiples
|
||||
* of the scroll view’s bounds when the user scrolls.
|
||||
* The default value is false.
|
||||
* @platform ios
|
||||
*/
|
||||
pagingEnabled?: boolean,
|
||||
|
||||
/**
|
||||
* The amount by which the web view content is inset from the edges of
|
||||
* the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
|
||||
|
@ -182,7 +196,7 @@ export interface IOSWebViewProps {
|
|||
* to stop loading.
|
||||
* @platform ios
|
||||
*/
|
||||
onShouldStartLoadWithRequest?: (event: WebViewNativeEvent) => any;
|
||||
onShouldStartLoadWithRequest?: (event: WebViewIOSLoadRequestEvent) => any;
|
||||
|
||||
/**
|
||||
* Boolean that determines whether HTML5 videos play inline or use the
|
||||
|
@ -199,6 +213,20 @@ export interface IOSWebViewProps {
|
|||
* backward compatible.
|
||||
*/
|
||||
hideKeyboardAccessoryView?: boolean;
|
||||
/**
|
||||
* If true, this will be able horizontal swipe gestures when using the WKWebView. The default value is `false`.
|
||||
*/
|
||||
allowsBackForwardNavigationGestures?: boolean;
|
||||
|
||||
/**
|
||||
* A Boolean value that determines whether pressing on a link
|
||||
* displays a preview of the destination for the link.
|
||||
*
|
||||
* This property is available on devices that support 3D Touch.
|
||||
* In iOS 10 and later, the default value is `true`; before that, the default value is `false`.
|
||||
* @platform ios
|
||||
*/
|
||||
allowsLinkPreview?: boolean;
|
||||
}
|
||||
|
||||
export interface AndroidWebViewProps {
|
||||
|
@ -370,7 +398,7 @@ export interface WebViewSharedProps extends ViewProps, IOSWebViewProps, AndroidW
|
|||
* Boolean value that forces the `WebView` to show the loading view
|
||||
* on the first load.
|
||||
*/
|
||||
startInLoadingState?: string;
|
||||
startInLoadingState?: boolean;
|
||||
|
||||
/**
|
||||
* Set this to provide JavaScript that will be injected into the web page
|
||||
|
@ -417,4 +445,6 @@ export class WebView extends Component<WebViewSharedProps> {
|
|||
public goBack: () => void;
|
||||
public reload: () => void;
|
||||
public stopLoading: () => void;
|
||||
public postMessage: (msg: string) => void;
|
||||
public injectJavaScript: (js: string) => void;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue