Skip to content

Commit e1fc730

Browse files
authored
Allow custom menu items for selection menu (react-native-webview#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
1 parent af418ec commit e1fc730

File tree

6 files changed

+172
-0
lines changed

6 files changed

+172
-0
lines changed

apple/RNCWebView.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential;
7171
@property (nonatomic, assign) BOOL pullToRefreshEnabled;
7272
@property (nonatomic, assign) BOOL enableApplePay;
73+
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
74+
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
7375
#if !TARGET_OS_OSX
7476
@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl;
7577
#endif

apple/RNCWebView.m

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
static NSURLCredential* clientAuthenticationCredential;
2424
static NSDictionary* customCertificatesForHost;
2525

26+
NSString *const CUSTOM_SELECTOR = @"_CUSTOM_SELECTOR_";
27+
2628
#if !TARGET_OS_OSX
2729
// runtime trick to remove WKWebView keyboard default toolbar
2830
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
@@ -188,11 +190,114 @@ - (instancetype)initWithFrame:(CGRect)frame
188190
return self;
189191
}
190192

193+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
194+
// Only allow long press gesture
195+
if ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
196+
return YES;
197+
}else{
198+
return NO;
199+
}
200+
}
201+
202+
// Listener for long presses
203+
- (void)startLongPress:(UILongPressGestureRecognizer *)pressSender
204+
{
205+
// When a long press ends, bring up our custom UIMenu
206+
if(pressSender.state == UIGestureRecognizerStateEnded) {
207+
if (!self.menuItems || self.menuItems.count == 0) {
208+
return;
209+
}
210+
UIMenuController *menuController = [UIMenuController sharedMenuController];
211+
NSMutableArray *menuControllerItems = [NSMutableArray arrayWithCapacity:self.menuItems.count];
212+
213+
for(NSString *menuItemName in self.menuItems) {
214+
NSString *menuItemLabel = [RCTConvert NSString:menuItem[@"label"]];
215+
NSString *menuItemKey = [RCTConvert NSString:menuItem[@"key"]];
216+
NSString *sel = [NSString stringWithFormat:@"%@%@", CUSTOM_SELECTOR, menuItemKey];
217+
UIMenuItem *item = [[UIMenuItem alloc] initWithTitle: menuItemLabel
218+
action: NSSelectorFromString(sel)];
219+
220+
[menuControllerItems addObject: item];
221+
}
222+
223+
menuController.menuItems = menuControllerItems;
224+
[menuController setMenuVisible:YES animated:YES];
225+
}
226+
}
227+
191228
- (void)dealloc
192229
{
193230
[[NSNotificationCenter defaultCenter] removeObserver:self];
194231
}
195232

233+
- (void)tappedMenuItem:(NSString *)eventType
234+
{
235+
// Get the selected text
236+
// NOTE: selecting text in an iframe or shadow DOM will not work
237+
[self.webView evaluateJavaScript: @"window.getSelection().toString()" completionHandler: ^(id result, NSError *error) {
238+
if (error != nil) {
239+
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]);
240+
} else {
241+
if (self.onCustomMenuSelection) {
242+
NSPredicate *filter = [NSPredicate predicateWithFormat:@"key contains[c] %@ ",eventType];
243+
NSArray *filteredMenuItems = [self.menuItems filteredArrayUsingPredicate:filter];
244+
NSDictionary *selectedMenuItem = filteredMenuItems[0];
245+
NSString *label = [RCTConvert NSString:selectedMenuItem[@"label"]];
246+
self.onCustomMenuSelection(@{
247+
@"key": eventType,
248+
@"label": label,
249+
@"selectedText": result
250+
});
251+
} else {
252+
RCTLogWarn(@"Error evaluating onCustomMenuSelection: You must implement an `onCustomMenuSelection` callback when using custom menu items");
253+
}
254+
}
255+
}];
256+
}
257+
258+
// Overwrite method that interprets which action to call upon UIMenu Selection
259+
// https://developer.apple.com/documentation/objectivec/nsobject/1571960-methodsignatureforselector
260+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
261+
{
262+
NSMethodSignature *existingSelector = [super methodSignatureForSelector:sel];
263+
if (existingSelector) {
264+
return existingSelector;
265+
}
266+
return [super methodSignatureForSelector:@selector(tappedMenuItem:)];
267+
}
268+
269+
// Needed to forward messages to other objects
270+
// https://developer.apple.com/documentation/objectivec/nsobject/1571955-forwardinvocation
271+
- (void)forwardInvocation:(NSInvocation *)invocation
272+
{
273+
NSString *sel = NSStringFromSelector([invocation selector]);
274+
NSRange match = [sel rangeOfString:CUSTOM_SELECTOR];
275+
if (match.location == 0) {
276+
[self tappedMenuItem:[sel substringFromIndex:17]];
277+
} else {
278+
[super forwardInvocation:invocation];
279+
}
280+
}
281+
282+
// Allows the instance to respond to UIMenuController Actions
283+
- (BOOL)canBecomeFirstResponder
284+
{
285+
return YES;
286+
}
287+
288+
// Control which items show up on the UIMenuController
289+
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
290+
{
291+
NSString *sel = NSStringFromSelector(action);
292+
// Do any of them have our custom keys?
293+
NSRange match = [sel rangeOfString:CUSTOM_SELECTOR];
294+
295+
if (match.location == 0) {
296+
return YES;
297+
}
298+
return NO;
299+
}
300+
196301
/**
197302
* See https://stackoverflow.com/questions/25713069/why-is-wkwebview-not-opening-links-with-target-blank/25853806#25853806 for details.
198303
*/
@@ -328,6 +433,17 @@ - (void)didMoveToWindow
328433
[self setKeyboardDisplayRequiresUserAction: _savedKeyboardDisplayRequiresUserAction];
329434
[self visitSource];
330435
}
436+
437+
// Allow this object to recognize gestures
438+
if (self.menuItems != nil) {
439+
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(startLongPress:)];
440+
longPress.delegate = self;
441+
442+
longPress.minimumPressDuration = 0.4f;
443+
longPress.numberOfTouchesRequired = 1;
444+
longPress.cancelsTouchesInView = YES;
445+
[self addGestureRecognizer:longPress];
446+
}
331447
}
332448

333449
// Update webview property when the component prop changes.

apple/RNCWebViewManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
#import <React/RCTViewManager.h>
99

1010
@interface RNCWebViewManager : RCTViewManager
11+
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
12+
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
1113
@end

apple/RNCWebViewManager.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ - (RCTUIView *)view
100100
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
101101
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
102102
RCT_EXPORT_VIEW_PROPERTY(enableApplePay, BOOL)
103+
RCT_EXPORT_VIEW_PROPERTY(menuItems, NSArray);
104+
RCT_EXPORT_VIEW_PROPERTY(onCustomMenuSelection, RCTDirectEventBlock)
103105

104106
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
105107
{

docs/Reference.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,30 @@ Example:
14391439

14401440
```javascript
14411441
<WebView forceDarkOn={false} />
1442+
### `menuItems`
1443+
1444+
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`
1445+
1446+
Example:
1447+
1448+
```javascript
1449+
<WebView menuItems={[{ label: 'Tweet', key: 'tweet' }, { label: 'Save for later', key: 'saveForLater' }]} />
1450+
1451+
```
1452+
1453+
### `onCustomMenuSelection`
1454+
1455+
Function called when a custom menu item is selected. It receives a Native event, which includes three custom keys: `label`, `key` and `selectedText`.
1456+
1457+
```javascript
1458+
<WebView
1459+
menuItems={[{ label: 'Tweet', key: 'tweet', { label: 'Save for later', key: 'saveForLater' }]}
1460+
onCustomMenuSelection={(webViewEvent) => {
1461+
const { label } = webViewEvent.nativeEvent; // The name of the menu item, i.e. 'Tweet'
1462+
const { key } = webViewEvent.nativeEvent; // The key of the menu item, i.e. 'tweet'
1463+
const { selectedText } = webViewEvent.nativeEvent; // Text highlighted
1464+
}}
1465+
/>
14421466
```
14431467

14441468
### `basicAuthCredential`

src/WebViewTypes.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,18 @@ export interface WebViewSourceHtml {
224224
baseUrl?: string;
225225
}
226226

227+
export interface WebViewCustomMenuItems {
228+
/**
229+
* The unique key that will be added as a selector on the webview
230+
* Returned by the `onCustomMenuSelection` callback
231+
*/
232+
key: string;
233+
/**
234+
* The label to appear on the UI Menu when selecting text
235+
*/
236+
label: string;
237+
}
238+
227239
export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
228240

229241
export interface ViewManager {
@@ -665,6 +677,20 @@ export interface IOSWebViewProps extends WebViewSharedProps {
665677
* The default value is false.
666678
*/
667679
enableApplePay?: boolean;
680+
681+
/**
682+
* An array of objects which will be added to the UIMenu controller when selecting text.
683+
* These will appear after a long press to select text.
684+
*/
685+
menuItems?: WebViewCustomMenuItems[];
686+
687+
/**
688+
* The function fired when selecting a custom menu item created by `menuItems`.
689+
* It passes a WebViewEvent with a `nativeEvent`, where custom keys are passed:
690+
* `customMenuKey`: the string of the menu item
691+
* `selectedText`: the text selected on the document
692+
*/
693+
onCustomMenuSelection?: (event: WebViewEvent) => void;
668694
}
669695

670696
export interface MacOSWebViewProps extends WebViewSharedProps {

0 commit comments

Comments
 (0)