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 {