@thiagobueno/rn-selectable-text 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -53
- package/ios/SelectableTextView.mm +94 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,29 +39,117 @@ import { SelectableTextView } from '@thiagobueno/rn-selectable-text';
|
|
|
39
39
|
|
|
40
40
|
export default function App() {
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const handleSelection = (event) => {
|
|
43
|
+
const { chosenOption, highlightedText } = event;
|
|
44
|
+
Alert.alert(
|
|
45
|
+
'Selection Event',
|
|
46
|
+
`Option: ${chosenOption}\nSelected Text: ${highlightedText}`
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
|
|
52
|
+
<SelectableTextView
|
|
53
|
+
menuOptions={['Save', 'Share', 'Copy']}
|
|
54
|
+
onSelection={handleSelection}
|
|
55
|
+
>
|
|
56
|
+
<Text style={{ fontSize: 18, color: '#333' }}>
|
|
57
|
+
Highlight this text to see the custom native menu!
|
|
58
|
+
</Text>
|
|
59
|
+
</SelectableTextView>
|
|
60
|
+
</View>
|
|
47
61
|
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
|
|
52
|
-
<SelectableTextView
|
|
53
|
-
menuOptions={['Save', 'Share', 'Copy']}
|
|
54
|
-
onSelection={handleSelection}
|
|
55
|
-
>
|
|
56
|
-
<Text style={{ fontSize: 18, color: '#333' }}>
|
|
57
|
-
Highlight this text to see the custom native menu!
|
|
58
|
-
</Text>
|
|
59
|
-
</SelectableTextView>
|
|
60
|
-
</View>
|
|
61
|
-
);
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### Invisible Custom Mode (ActionSheet & Bottom Sheet Integration)
|
|
66
|
+
|
|
67
|
+
If you want to use modern UIs like Apple's native `ActionSheetIOS` or a custom React Native Bottom Sheet, you can bypass the default horizontal bubble menu entirely.
|
|
68
|
+
|
|
69
|
+
By passing an **empty array (`[]`)** to `menuOptions`, the library enters **Invisible Mode**. It completely suppresses the native iOS/Android bubble menus while still tracking text selection in real-time and emitting the `CUSTOM_MODE` event.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import React, { useState } from 'react';
|
|
73
|
+
import { View, Text, Platform, ActionSheetIOS, Modal, Button, StyleSheet } from 'react-native';
|
|
74
|
+
import { SelectableTextView } from '@thiagobueno/rn-selectable-text';
|
|
75
|
+
|
|
76
|
+
export default function InvisibleModeApp() {
|
|
77
|
+
const [androidMenuVisible, setAndroidMenuVisible] = useState(false);
|
|
78
|
+
const [selectedText, setSelectedText] = useState('');
|
|
79
|
+
|
|
80
|
+
const handleSelection = ({ chosenOption, highlightedText }) => {
|
|
81
|
+
// 1. Hide menus if the user taps away and clears the selection
|
|
82
|
+
if (highlightedText.length === 0) {
|
|
83
|
+
setAndroidMenuVisible(false);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Invisible Mode triggers the "CUSTOM_MODE" option
|
|
88
|
+
if (chosenOption === 'CUSTOM_MODE') {
|
|
89
|
+
setSelectedText(highlightedText);
|
|
90
|
+
|
|
91
|
+
if (Platform.OS === 'ios') {
|
|
92
|
+
// Open Apple's Native Bottom Sheet
|
|
93
|
+
ActionSheetIOS.showActionSheetWithOptions(
|
|
94
|
+
{
|
|
95
|
+
title: 'Text Actions',
|
|
96
|
+
options: ['Cancel', 'Share to WhatsApp', 'Save'],
|
|
97
|
+
cancelButtonIndex: 0,
|
|
98
|
+
},
|
|
99
|
+
(buttonIndex) => {
|
|
100
|
+
if (buttonIndex === 1) console.log('WhatsApp:', highlightedText);
|
|
101
|
+
if (buttonIndex === 2) console.log('Save:', highlightedText);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
// Open Custom React Native Bottom Sheet for Android
|
|
106
|
+
setAndroidMenuVisible(true);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View style={{ flex: 1, padding: 20, justifyContent: 'center' }}>
|
|
113
|
+
<SelectableTextView
|
|
114
|
+
menuOptions={[]} // The empty array is the magic key for Invisible Mode
|
|
115
|
+
onSelection={handleSelection}
|
|
116
|
+
>
|
|
117
|
+
<Text style={{ fontSize: 18, color: '#333' }}>
|
|
118
|
+
Select this text. The native bubble menu is suppressed, allowing you
|
|
119
|
+
to render a beautiful Action Sheet or custom Modal instead!
|
|
120
|
+
</Text>
|
|
121
|
+
</SelectableTextView>
|
|
122
|
+
|
|
123
|
+
{Platform.OS === 'android' && (
|
|
124
|
+
<Modal visible={androidMenuVisible} transparent animationType="slide">
|
|
125
|
+
<View style={styles.modalOverlay}>
|
|
126
|
+
<View style={styles.bottomSheet}>
|
|
127
|
+
<Text style={{ marginBottom: 15 }}>Selected: {selectedText}</Text>
|
|
128
|
+
<Button title="Save" onPress={() => setAndroidMenuVisible(false)} />
|
|
129
|
+
<Button title="Close" color="red" onPress={() => setAndroidMenuVisible(false)} />
|
|
130
|
+
</View>
|
|
131
|
+
</View>
|
|
132
|
+
</Modal>
|
|
133
|
+
)}
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const styles = StyleSheet.create({
|
|
139
|
+
modalOverlay: {
|
|
140
|
+
flex: 1,
|
|
141
|
+
justifyContent: 'flex-end',
|
|
142
|
+
backgroundColor: 'rgba(0,0,0,0.5)'
|
|
143
|
+
},
|
|
144
|
+
bottomSheet: {
|
|
145
|
+
backgroundColor: 'white',
|
|
146
|
+
padding: 20,
|
|
147
|
+
borderTopLeftRadius: 20,
|
|
148
|
+
borderTopRightRadius: 20
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
65
153
|
### Advanced Example (Nested Text & Index Mapping)
|
|
66
154
|
|
|
67
155
|
When dealing with internationalization (i18n) or dynamic menus, it's highly recommended to map your selections by index rather than relying on the translated string.
|
|
@@ -75,38 +163,38 @@ const MENU_OPTIONS = ['Save Note', 'Edit Text', 'Highlight Content'];
|
|
|
75
163
|
|
|
76
164
|
export default function AdvancedApp() {
|
|
77
165
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
const handleSelection = ({ chosenOption, highlightedText }) => {
|
|
167
|
+
const actionIndex = MENU_OPTIONS.indexOf(chosenOption);
|
|
168
|
+
|
|
169
|
+
switch (actionIndex) {
|
|
170
|
+
case 0:
|
|
171
|
+
console.log('Action: Save Note - Text:', highlightedText);
|
|
172
|
+
break;
|
|
173
|
+
case 1:
|
|
174
|
+
console.log('Action: Edit Text - Text:', highlightedText);
|
|
175
|
+
break;
|
|
176
|
+
case 2:
|
|
177
|
+
console.log('Action: Highlight Content - Text:', highlightedText);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<View style={{ padding: 20 }}>
|
|
184
|
+
<SelectableTextView
|
|
185
|
+
menuOptions={MENU_OPTIONS}
|
|
186
|
+
onSelection={handleSelection}
|
|
187
|
+
>
|
|
188
|
+
<Text style={{ color: 'black', fontSize: 16 }}>
|
|
189
|
+
This text is black, but{' '}
|
|
190
|
+
<Text style={{ fontWeight: 'bold', color: 'blue' }}>
|
|
191
|
+
this part is bold and blue
|
|
192
|
+
</Text>{' '}
|
|
193
|
+
and this is black again. The entire block is selectable!
|
|
194
|
+
</Text>
|
|
195
|
+
</SelectableTextView>
|
|
196
|
+
</View>
|
|
197
|
+
);
|
|
110
198
|
}
|
|
111
199
|
```
|
|
112
200
|
|
|
@@ -127,8 +215,8 @@ The `onSelection` callback receives an event object with:
|
|
|
127
215
|
|
|
128
216
|
```typescript
|
|
129
217
|
interface SelectionEvent {
|
|
130
|
-
|
|
131
|
-
|
|
218
|
+
chosenOption: string; // The exact string of the menu option selected
|
|
219
|
+
highlightedText: string; // The specific text highlighted by the user
|
|
132
220
|
}
|
|
133
221
|
```
|
|
134
222
|
|
|
@@ -12,6 +12,15 @@ using namespace facebook::react;
|
|
|
12
12
|
|
|
13
13
|
@class SelectableTextView;
|
|
14
14
|
|
|
15
|
+
// ====================================================================
|
|
16
|
+
// CRASH PREVENTION: Safe C++ String Conversion
|
|
17
|
+
// ====================================================================
|
|
18
|
+
static std::string safeStdString(NSString *str) {
|
|
19
|
+
if (!str) return "";
|
|
20
|
+
const char *utf8 = [str UTF8String];
|
|
21
|
+
return utf8 ? std::string(utf8) : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
@interface SelectableUITextView : UITextView
|
|
16
25
|
@property (nonatomic, weak) SelectableTextView *parentSelectableTextView;
|
|
17
26
|
@end
|
|
@@ -20,8 +29,6 @@ using namespace facebook::react;
|
|
|
20
29
|
|
|
21
30
|
- (void)didMoveToWindow {
|
|
22
31
|
[super didMoveToWindow];
|
|
23
|
-
// FIX: iOS 16+ text selection bug after navigation.
|
|
24
|
-
// When returning to the screen, we force iOS to re-evaluate the interaction state.
|
|
25
32
|
if (self.window) {
|
|
26
33
|
BOOL wasSelectable = self.selectable;
|
|
27
34
|
self.selectable = NO;
|
|
@@ -31,12 +38,6 @@ using namespace facebook::react;
|
|
|
31
38
|
|
|
32
39
|
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
|
33
40
|
{
|
|
34
|
-
// FIX: We explicitly tell iOS that we can perform standard actions.
|
|
35
|
-
// This prevents the "[UIKitCore] The edit menu did not have performable commands" warning.
|
|
36
|
-
if (action == @selector(copy:) || action == @selector(selectAll:)) {
|
|
37
|
-
return YES;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
41
|
if (self.parentSelectableTextView) {
|
|
41
42
|
return [self.parentSelectableTextView canPerformAction:action withSender:sender];
|
|
42
43
|
}
|
|
@@ -63,7 +64,6 @@ using namespace facebook::react;
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
// Intercepts physical keyboard "Copy" and nullifies the action to keep it custom
|
|
67
67
|
- (void)copy:(id)sender
|
|
68
68
|
{
|
|
69
69
|
// Silently blocked
|
|
@@ -142,7 +142,6 @@ using namespace facebook::react;
|
|
|
142
142
|
- (void)prepareForRecycle {
|
|
143
143
|
[super prepareForRecycle];
|
|
144
144
|
|
|
145
|
-
// FIX: Force drop focus when leaving screen
|
|
146
145
|
[_customTextView resignFirstResponder];
|
|
147
146
|
|
|
148
147
|
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
@@ -151,7 +150,6 @@ using namespace facebook::react;
|
|
|
151
150
|
_customTextView.text = nil;
|
|
152
151
|
_customTextView.selectedTextRange = nil;
|
|
153
152
|
|
|
154
|
-
// CRITICAL FIX: Do NOT clear _menuOptions here. Fabric's prop diffing will handle updates.
|
|
155
153
|
[self unhideAllViews:self];
|
|
156
154
|
}
|
|
157
155
|
|
|
@@ -240,29 +238,76 @@ using namespace facebook::react;
|
|
|
240
238
|
|
|
241
239
|
#pragma mark - UITextViewDelegate
|
|
242
240
|
|
|
241
|
+
// ====================================================================
|
|
242
|
+
// REAL-TIME CUSTOM MODE TRACKING (With Crash Prevention bounds check)
|
|
243
|
+
// ====================================================================
|
|
244
|
+
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
245
|
+
{
|
|
246
|
+
if (textView.selectedRange.location != NSNotFound && textView.selectedRange.length > 0) {
|
|
247
|
+
|
|
248
|
+
// SAFE BOUNDS CHECK: Prevents iOS 15/17/18 out-of-bounds crash
|
|
249
|
+
NSString *selectedText = @"";
|
|
250
|
+
if (textView.selectedRange.location + textView.selectedRange.length <= textView.text.length) {
|
|
251
|
+
selectedText = [textView.text substringWithRange:textView.selectedRange];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// INVISIBLE MODE (ALL IOS VERSIONS)
|
|
255
|
+
if (_menuOptions.count == 0) {
|
|
256
|
+
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
257
|
+
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
258
|
+
.chosenOption = "CUSTOM_MODE",
|
|
259
|
+
.highlightedText = safeStdString(selectedText)
|
|
260
|
+
};
|
|
261
|
+
eventEmitter->onSelection(selectionEvent);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// STANDARD MODE
|
|
267
|
+
if (@available(iOS 16.0, *)) {
|
|
268
|
+
return;
|
|
269
|
+
} else {
|
|
270
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
271
|
+
[self showCustomMenu];
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
276
|
+
|
|
277
|
+
// IF SELECTION IS CLEARED, NOTIFY JS TO HIDE THE BOTTOM SHEET
|
|
278
|
+
if (_menuOptions.count == 0) {
|
|
279
|
+
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
280
|
+
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
281
|
+
.chosenOption = "CUSTOM_MODE",
|
|
282
|
+
.highlightedText = ""
|
|
283
|
+
};
|
|
284
|
+
eventEmitter->onSelection(selectionEvent);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
243
290
|
// ====================================================================
|
|
244
291
|
// THE NEW IOS 16+ API
|
|
245
292
|
// ====================================================================
|
|
246
293
|
- (UIMenu *)textView:(UITextView *)textView editMenuForTextInRange:(NSRange)range suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0)) {
|
|
247
294
|
|
|
248
|
-
// FORCING THE FOCUS: ensures the system doesn't dismiss our menu unexpectedly
|
|
249
|
-
[textView becomeFirstResponder];
|
|
250
|
-
|
|
251
|
-
// ====================================================================
|
|
252
|
-
// CUSTOM INVISIBLE MODE: Suppress native menu if array is empty
|
|
253
|
-
// ====================================================================
|
|
254
295
|
if (_menuOptions.count == 0) {
|
|
255
|
-
|
|
296
|
+
// SAFE BOUNDS CHECK
|
|
297
|
+
NSString *selectedText = @"";
|
|
298
|
+
if (range.location != NSNotFound && range.location + range.length <= textView.text.length) {
|
|
299
|
+
selectedText = [textView.text substringWithRange:range];
|
|
300
|
+
}
|
|
301
|
+
|
|
256
302
|
if (selectedText.length > 0) {
|
|
257
303
|
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
258
304
|
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
259
|
-
.chosenOption =
|
|
260
|
-
.highlightedText =
|
|
305
|
+
.chosenOption = "CUSTOM_MODE",
|
|
306
|
+
.highlightedText = safeStdString(selectedText)
|
|
261
307
|
};
|
|
262
308
|
eventEmitter->onSelection(selectionEvent);
|
|
263
309
|
}
|
|
264
310
|
}
|
|
265
|
-
// Returns an empty menu, effectively hiding the native UI
|
|
266
311
|
return [UIMenu menuWithTitle:@"" children:@[]];
|
|
267
312
|
}
|
|
268
313
|
|
|
@@ -278,36 +323,6 @@ using namespace facebook::react;
|
|
|
278
323
|
return [UIMenu menuWithTitle:@"" children:customActions];
|
|
279
324
|
}
|
|
280
325
|
|
|
281
|
-
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
282
|
-
{
|
|
283
|
-
if (@available(iOS 16.0, *)) {
|
|
284
|
-
return;
|
|
285
|
-
} else {
|
|
286
|
-
if (textView.selectedRange.length > 0) {
|
|
287
|
-
|
|
288
|
-
// CUSTOM INVISIBLE MODE FOR IOS < 16
|
|
289
|
-
if (_menuOptions.count == 0) {
|
|
290
|
-
NSString *selectedText = [textView.text substringWithRange:textView.selectedRange];
|
|
291
|
-
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
292
|
-
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
293
|
-
.chosenOption = std::string("CUSTOM_MODE"),
|
|
294
|
-
.highlightedText = std::string([selectedText UTF8String])
|
|
295
|
-
};
|
|
296
|
-
eventEmitter->onSelection(selectionEvent);
|
|
297
|
-
}
|
|
298
|
-
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
303
|
-
[self showCustomMenu];
|
|
304
|
-
});
|
|
305
|
-
} else {
|
|
306
|
-
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
326
|
- (void)showCustomMenu
|
|
312
327
|
{
|
|
313
328
|
if (![_customTextView canBecomeFirstResponder]) return;
|
|
@@ -334,7 +349,8 @@ using namespace facebook::react;
|
|
|
334
349
|
|
|
335
350
|
CGRect selectedRect = [_customTextView firstRectForRange:_customTextView.selectedTextRange];
|
|
336
351
|
|
|
337
|
-
|
|
352
|
+
// CRASH PREVENTION: Validate rect before showing menu (iOS 15 safety)
|
|
353
|
+
if (!CGRectIsEmpty(selectedRect) && !CGRectIsNull(selectedRect) && !CGRectIsInfinite(selectedRect)) {
|
|
338
354
|
CGRect targetRect = [_customTextView convertRect:selectedRect toView:_customTextView];
|
|
339
355
|
[menuController showMenuFromView:_customTextView rect:targetRect];
|
|
340
356
|
}
|
|
@@ -346,17 +362,19 @@ using namespace facebook::react;
|
|
|
346
362
|
}
|
|
347
363
|
|
|
348
364
|
// ====================================================================
|
|
349
|
-
//
|
|
365
|
+
// SUPPRESS SYSTEM MENU IN CUSTOM MODE
|
|
350
366
|
// ====================================================================
|
|
351
367
|
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
|
352
368
|
{
|
|
369
|
+
if (_menuOptions.count == 0) {
|
|
370
|
+
return NO;
|
|
371
|
+
}
|
|
372
|
+
|
|
353
373
|
NSString *selectorName = NSStringFromSelector(action);
|
|
354
374
|
if ([selectorName hasPrefix:@"customAction_"] && [selectorName hasSuffix:@":"]) {
|
|
355
375
|
return YES;
|
|
356
376
|
}
|
|
357
377
|
|
|
358
|
-
// We tell iOS that we can perform standard actions.
|
|
359
|
-
// This convinces the system not to abort the menu rendering.
|
|
360
378
|
if (action == @selector(copy:) || action == @selector(selectAll:)) {
|
|
361
379
|
return YES;
|
|
362
380
|
}
|
|
@@ -378,23 +396,26 @@ using namespace facebook::react;
|
|
|
378
396
|
NSString *selectorName = NSStringFromSelector(anInvocation.selector);
|
|
379
397
|
|
|
380
398
|
if ([selectorName hasPrefix:@"customAction_"] && [selectorName hasSuffix:@":"]) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
NSString *
|
|
386
|
-
|
|
387
|
-
|
|
399
|
+
// CRASH PREVENTION: Validate selector length before cutting string
|
|
400
|
+
if (selectorName.length > 14) {
|
|
401
|
+
NSString *cleanedOption = [selectorName substringWithRange:NSMakeRange(13, selectorName.length - 14)];
|
|
402
|
+
|
|
403
|
+
NSString *originalOption = nil;
|
|
404
|
+
for (NSString *option in _menuOptions) {
|
|
405
|
+
NSString *clean1 = [option stringByReplacingOccurrencesOfString:@" " withString:@"_"];
|
|
406
|
+
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-zA-Z0-9_]" options:0 error:nil];
|
|
407
|
+
NSString *testSelectorName = [regex stringByReplacingMatchesInString:clean1 options:0 range:NSMakeRange(0, clean1.length) withTemplate:@"_"];
|
|
408
|
+
|
|
409
|
+
if ([testSelectorName isEqualToString:cleanedOption]) {
|
|
410
|
+
originalOption = option;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
388
414
|
|
|
389
|
-
if (
|
|
390
|
-
originalOption
|
|
391
|
-
break;
|
|
415
|
+
if (originalOption) {
|
|
416
|
+
[self handleMenuSelection:originalOption];
|
|
392
417
|
}
|
|
393
418
|
}
|
|
394
|
-
|
|
395
|
-
if (originalOption) {
|
|
396
|
-
[self handleMenuSelection:originalOption];
|
|
397
|
-
}
|
|
398
419
|
} else {
|
|
399
420
|
[super forwardInvocation:anInvocation];
|
|
400
421
|
}
|
|
@@ -405,7 +426,8 @@ using namespace facebook::react;
|
|
|
405
426
|
NSRange selectedRange = _customTextView.selectedRange;
|
|
406
427
|
NSString *selectedText = @"";
|
|
407
428
|
|
|
408
|
-
|
|
429
|
+
// SAFE BOUNDS CHECK
|
|
430
|
+
if (selectedRange.location != NSNotFound && selectedRange.length > 0 && (selectedRange.location + selectedRange.length <= _customTextView.text.length)) {
|
|
409
431
|
selectedText = [_customTextView.text substringWithRange:selectedRange];
|
|
410
432
|
}
|
|
411
433
|
|
|
@@ -414,8 +436,8 @@ using namespace facebook::react;
|
|
|
414
436
|
|
|
415
437
|
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
416
438
|
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
417
|
-
.chosenOption =
|
|
418
|
-
.highlightedText =
|
|
439
|
+
.chosenOption = safeStdString(selectedOption),
|
|
440
|
+
.highlightedText = safeStdString(selectedText)
|
|
419
441
|
};
|
|
420
442
|
eventEmitter->onSelection(selectionEvent);
|
|
421
443
|
}
|