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:
Scott Batson 2021-10-08 02:38:43 -04:00 committed by GitHub
parent af418ecae4
commit e1fc730eed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 0 deletions

View File

@ -70,6 +70,8 @@
@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential;
@property (nonatomic, assign) BOOL pullToRefreshEnabled;
@property (nonatomic, assign) BOOL enableApplePay;
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
#if !TARGET_OS_OSX
@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl;
#endif

View File

@ -23,6 +23,8 @@ static NSString *const MessageHandlerName = @"ReactNativeWebView";
static NSURLCredential* clientAuthenticationCredential;
static NSDictionary* customCertificatesForHost;
NSString *const CUSTOM_SELECTOR = @"_CUSTOM_SELECTOR_";
#if !TARGET_OS_OSX
// runtime trick to remove WKWebView keyboard default toolbar
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
@ -188,11 +190,114 @@ static NSDictionary* customCertificatesForHost;
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
{
[[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.
*/
@ -328,6 +433,17 @@ static NSDictionary* customCertificatesForHost;
[self setKeyboardDisplayRequiresUserAction: _savedKeyboardDisplayRequiresUserAction];
[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.

View File

@ -8,4 +8,6 @@
#import <React/RCTViewManager.h>
@interface RNCWebViewManager : RCTViewManager
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
@end

View File

@ -100,6 +100,8 @@ RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
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)
{

View File

@ -1439,6 +1439,30 @@ Example:
```javascript
<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`

View File

@ -224,6 +224,18 @@ export interface WebViewSourceHtml {
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 interface ViewManager {
@ -665,6 +677,20 @@ export interface IOSWebViewProps extends WebViewSharedProps {
* The default value is false.
*/
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 {