From e1fc730eedfa32b2eef69d924812ee96a5f3d8cb Mon Sep 17 00:00:00 2001 From: Scott Batson <881981+sbatson5@users.noreply.github.com> Date: Fri, 8 Oct 2021 02:38:43 -0400 Subject: [PATCH] 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 --- apple/RNCWebView.h | 2 + apple/RNCWebView.m | 116 ++++++++++++++++++++++++++++++++++++++ apple/RNCWebViewManager.h | 2 + apple/RNCWebViewManager.m | 2 + docs/Reference.md | 24 ++++++++ src/WebViewTypes.ts | 26 +++++++++ 6 files changed, 172 insertions(+) diff --git a/apple/RNCWebView.h b/apple/RNCWebView.h index de11e29..21008d7 100644 --- a/apple/RNCWebView.h +++ b/apple/RNCWebView.h @@ -70,6 +70,8 @@ @property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential; @property (nonatomic, assign) BOOL pullToRefreshEnabled; @property (nonatomic, assign) BOOL enableApplePay; +@property (nonatomic, copy) NSArray * _Nullable menuItems; +@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; #if !TARGET_OS_OSX @property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl; #endif diff --git a/apple/RNCWebView.m b/apple/RNCWebView.m index ff9ff9e..17ee2d8 100644 --- a/apple/RNCWebView.m +++ b/apple/RNCWebView.m @@ -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. diff --git a/apple/RNCWebViewManager.h b/apple/RNCWebViewManager.h index 383a921..c7252e5 100644 --- a/apple/RNCWebViewManager.h +++ b/apple/RNCWebViewManager.h @@ -8,4 +8,6 @@ #import @interface RNCWebViewManager : RCTViewManager + @property (nonatomic, copy) NSArray * _Nullable menuItems; + @property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; @end diff --git a/apple/RNCWebViewManager.m b/apple/RNCWebViewManager.m index a75585e..ccdee58 100644 --- a/apple/RNCWebViewManager.m +++ b/apple/RNCWebViewManager.m @@ -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) { diff --git a/docs/Reference.md b/docs/Reference.md index 92d1275..cdb2c63 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -1439,6 +1439,30 @@ Example: ```javascript +### `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 + + +``` + +### `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 + { + 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` diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 7f532e4..99f2c25 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -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 {