Allow custom menu items for selection menu (#2101)
remove unused vars use gesture handler change callback name, pass selected text back do not use deprecate setTarget do not call super on methodSignatureForSelector twice add checks for custom menu items and callback make custom params optional add custom menu items outside of initWithFrame require key and label for menuItems fix typo
This commit is contained in:
parent
af418ecae4
commit
e1fc730eed
|
@ -70,6 +70,8 @@
|
||||||
@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential;
|
@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential;
|
||||||
@property (nonatomic, assign) BOOL pullToRefreshEnabled;
|
@property (nonatomic, assign) BOOL pullToRefreshEnabled;
|
||||||
@property (nonatomic, assign) BOOL enableApplePay;
|
@property (nonatomic, assign) BOOL enableApplePay;
|
||||||
|
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
|
||||||
#if !TARGET_OS_OSX
|
#if !TARGET_OS_OSX
|
||||||
@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl;
|
@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -23,6 +23,8 @@ static NSString *const MessageHandlerName = @"ReactNativeWebView";
|
||||||
static NSURLCredential* clientAuthenticationCredential;
|
static NSURLCredential* clientAuthenticationCredential;
|
||||||
static NSDictionary* customCertificatesForHost;
|
static NSDictionary* customCertificatesForHost;
|
||||||
|
|
||||||
|
NSString *const CUSTOM_SELECTOR = @"_CUSTOM_SELECTOR_";
|
||||||
|
|
||||||
#if !TARGET_OS_OSX
|
#if !TARGET_OS_OSX
|
||||||
// runtime trick to remove WKWebView keyboard default toolbar
|
// runtime trick to remove WKWebView keyboard default toolbar
|
||||||
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
|
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
|
||||||
|
@ -188,11 +190,114 @@ static NSDictionary* customCertificatesForHost;
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||||
|
// Only allow long press gesture
|
||||||
|
if ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
|
||||||
|
return YES;
|
||||||
|
}else{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener for long presses
|
||||||
|
- (void)startLongPress:(UILongPressGestureRecognizer *)pressSender
|
||||||
|
{
|
||||||
|
// When a long press ends, bring up our custom UIMenu
|
||||||
|
if(pressSender.state == UIGestureRecognizerStateEnded) {
|
||||||
|
if (!self.menuItems || self.menuItems.count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UIMenuController *menuController = [UIMenuController sharedMenuController];
|
||||||
|
NSMutableArray *menuControllerItems = [NSMutableArray arrayWithCapacity:self.menuItems.count];
|
||||||
|
|
||||||
|
for(NSString *menuItemName in self.menuItems) {
|
||||||
|
NSString *menuItemLabel = [RCTConvert NSString:menuItem[@"label"]];
|
||||||
|
NSString *menuItemKey = [RCTConvert NSString:menuItem[@"key"]];
|
||||||
|
NSString *sel = [NSString stringWithFormat:@"%@%@", CUSTOM_SELECTOR, menuItemKey];
|
||||||
|
UIMenuItem *item = [[UIMenuItem alloc] initWithTitle: menuItemLabel
|
||||||
|
action: NSSelectorFromString(sel)];
|
||||||
|
|
||||||
|
[menuControllerItems addObject: item];
|
||||||
|
}
|
||||||
|
|
||||||
|
menuController.menuItems = menuControllerItems;
|
||||||
|
[menuController setMenuVisible:YES animated:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)dealloc
|
- (void)dealloc
|
||||||
{
|
{
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)tappedMenuItem:(NSString *)eventType
|
||||||
|
{
|
||||||
|
// Get the selected text
|
||||||
|
// NOTE: selecting text in an iframe or shadow DOM will not work
|
||||||
|
[self.webView evaluateJavaScript: @"window.getSelection().toString()" completionHandler: ^(id result, NSError *error) {
|
||||||
|
if (error != nil) {
|
||||||
|
RCTLogWarn(@"%@", [NSString stringWithFormat:@"Error evaluating injectedJavaScript: This is possibly due to an unsupported return type. Try adding true to the end of your injectedJavaScript string. %@", error]);
|
||||||
|
} else {
|
||||||
|
if (self.onCustomMenuSelection) {
|
||||||
|
NSPredicate *filter = [NSPredicate predicateWithFormat:@"key contains[c] %@ ",eventType];
|
||||||
|
NSArray *filteredMenuItems = [self.menuItems filteredArrayUsingPredicate:filter];
|
||||||
|
NSDictionary *selectedMenuItem = filteredMenuItems[0];
|
||||||
|
NSString *label = [RCTConvert NSString:selectedMenuItem[@"label"]];
|
||||||
|
self.onCustomMenuSelection(@{
|
||||||
|
@"key": eventType,
|
||||||
|
@"label": label,
|
||||||
|
@"selectedText": result
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
RCTLogWarn(@"Error evaluating onCustomMenuSelection: You must implement an `onCustomMenuSelection` callback when using custom menu items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite method that interprets which action to call upon UIMenu Selection
|
||||||
|
// https://developer.apple.com/documentation/objectivec/nsobject/1571960-methodsignatureforselector
|
||||||
|
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
|
||||||
|
{
|
||||||
|
NSMethodSignature *existingSelector = [super methodSignatureForSelector:sel];
|
||||||
|
if (existingSelector) {
|
||||||
|
return existingSelector;
|
||||||
|
}
|
||||||
|
return [super methodSignatureForSelector:@selector(tappedMenuItem:)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needed to forward messages to other objects
|
||||||
|
// https://developer.apple.com/documentation/objectivec/nsobject/1571955-forwardinvocation
|
||||||
|
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||||
|
{
|
||||||
|
NSString *sel = NSStringFromSelector([invocation selector]);
|
||||||
|
NSRange match = [sel rangeOfString:CUSTOM_SELECTOR];
|
||||||
|
if (match.location == 0) {
|
||||||
|
[self tappedMenuItem:[sel substringFromIndex:17]];
|
||||||
|
} else {
|
||||||
|
[super forwardInvocation:invocation];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows the instance to respond to UIMenuController Actions
|
||||||
|
- (BOOL)canBecomeFirstResponder
|
||||||
|
{
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control which items show up on the UIMenuController
|
||||||
|
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
||||||
|
{
|
||||||
|
NSString *sel = NSStringFromSelector(action);
|
||||||
|
// Do any of them have our custom keys?
|
||||||
|
NSRange match = [sel rangeOfString:CUSTOM_SELECTOR];
|
||||||
|
|
||||||
|
if (match.location == 0) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://stackoverflow.com/questions/25713069/why-is-wkwebview-not-opening-links-with-target-blank/25853806#25853806 for details.
|
* See https://stackoverflow.com/questions/25713069/why-is-wkwebview-not-opening-links-with-target-blank/25853806#25853806 for details.
|
||||||
*/
|
*/
|
||||||
|
@ -328,6 +433,17 @@ static NSDictionary* customCertificatesForHost;
|
||||||
[self setKeyboardDisplayRequiresUserAction: _savedKeyboardDisplayRequiresUserAction];
|
[self setKeyboardDisplayRequiresUserAction: _savedKeyboardDisplayRequiresUserAction];
|
||||||
[self visitSource];
|
[self visitSource];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow this object to recognize gestures
|
||||||
|
if (self.menuItems != nil) {
|
||||||
|
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(startLongPress:)];
|
||||||
|
longPress.delegate = self;
|
||||||
|
|
||||||
|
longPress.minimumPressDuration = 0.4f;
|
||||||
|
longPress.numberOfTouchesRequired = 1;
|
||||||
|
longPress.cancelsTouchesInView = YES;
|
||||||
|
[self addGestureRecognizer:longPress];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update webview property when the component prop changes.
|
// Update webview property when the component prop changes.
|
||||||
|
|
|
@ -8,4 +8,6 @@
|
||||||
#import <React/RCTViewManager.h>
|
#import <React/RCTViewManager.h>
|
||||||
|
|
||||||
@interface RNCWebViewManager : RCTViewManager
|
@interface RNCWebViewManager : RCTViewManager
|
||||||
|
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -100,6 +100,8 @@ RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(enableApplePay, BOOL)
|
RCT_EXPORT_VIEW_PROPERTY(enableApplePay, BOOL)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(menuItems, NSArray);
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onCustomMenuSelection, RCTDirectEventBlock)
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
|
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1439,6 +1439,30 @@ Example:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
<WebView forceDarkOn={false} />
|
<WebView forceDarkOn={false} />
|
||||||
|
### `menuItems`
|
||||||
|
|
||||||
|
An array of custom menu item objects that will be appended to the UIMenu that appears when selecting text (will appear after 'Copy' and 'Share...'). Used in tandem with `onCustomMenuSelection`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<WebView menuItems={[{ label: 'Tweet', key: 'tweet' }, { label: 'Save for later', key: 'saveForLater' }]} />
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `onCustomMenuSelection`
|
||||||
|
|
||||||
|
Function called when a custom menu item is selected. It receives a Native event, which includes three custom keys: `label`, `key` and `selectedText`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<WebView
|
||||||
|
menuItems={[{ label: 'Tweet', key: 'tweet', { label: 'Save for later', key: 'saveForLater' }]}
|
||||||
|
onCustomMenuSelection={(webViewEvent) => {
|
||||||
|
const { label } = webViewEvent.nativeEvent; // The name of the menu item, i.e. 'Tweet'
|
||||||
|
const { key } = webViewEvent.nativeEvent; // The key of the menu item, i.e. 'tweet'
|
||||||
|
const { selectedText } = webViewEvent.nativeEvent; // Text highlighted
|
||||||
|
}}
|
||||||
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
### `basicAuthCredential`
|
### `basicAuthCredential`
|
||||||
|
|
|
@ -224,6 +224,18 @@ export interface WebViewSourceHtml {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebViewCustomMenuItems {
|
||||||
|
/**
|
||||||
|
* The unique key that will be added as a selector on the webview
|
||||||
|
* Returned by the `onCustomMenuSelection` callback
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* The label to appear on the UI Menu when selecting text
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
|
export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
|
||||||
|
|
||||||
export interface ViewManager {
|
export interface ViewManager {
|
||||||
|
@ -665,6 +677,20 @@ export interface IOSWebViewProps extends WebViewSharedProps {
|
||||||
* The default value is false.
|
* The default value is false.
|
||||||
*/
|
*/
|
||||||
enableApplePay?: boolean;
|
enableApplePay?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of objects which will be added to the UIMenu controller when selecting text.
|
||||||
|
* These will appear after a long press to select text.
|
||||||
|
*/
|
||||||
|
menuItems?: WebViewCustomMenuItems[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function fired when selecting a custom menu item created by `menuItems`.
|
||||||
|
* It passes a WebViewEvent with a `nativeEvent`, where custom keys are passed:
|
||||||
|
* `customMenuKey`: the string of the menu item
|
||||||
|
* `selectedText`: the text selected on the document
|
||||||
|
*/
|
||||||
|
onCustomMenuSelection?: (event: WebViewEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MacOSWebViewProps extends WebViewSharedProps {
|
export interface MacOSWebViewProps extends WebViewSharedProps {
|
||||||
|
|
Loading…
Reference in New Issue