@thiagobueno/rn-selectable-text 1.0.4 → 1.0.6
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.
|
@@ -5,6 +5,8 @@ import android.util.AttributeSet
|
|
|
5
5
|
import android.view.ActionMode
|
|
6
6
|
import android.view.Menu
|
|
7
7
|
import android.view.MenuItem
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.view.ViewGroup
|
|
8
10
|
import android.widget.FrameLayout
|
|
9
11
|
import android.widget.TextView
|
|
10
12
|
import com.facebook.react.bridge.Arguments
|
|
@@ -16,7 +18,7 @@ class SelectableTextView : FrameLayout {
|
|
|
16
18
|
private var menuOptions: Array<String> = emptyArray()
|
|
17
19
|
private var textView: TextView? = null
|
|
18
20
|
|
|
19
|
-
//
|
|
21
|
+
// Holds the reference to the native Android ActionMode (the text selection menu)
|
|
20
22
|
private var currentActionMode: ActionMode? = null
|
|
21
23
|
|
|
22
24
|
constructor(context: Context?) : super(context!!)
|
|
@@ -48,13 +50,33 @@ class SelectableTextView : FrameLayout {
|
|
|
48
50
|
textView.setTextIsSelectable(true)
|
|
49
51
|
textView.customSelectionActionModeCallback = object : ActionMode.Callback {
|
|
50
52
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
51
|
-
//
|
|
53
|
+
// Save the reference of the native Android bar as soon as it is created
|
|
52
54
|
currentActionMode = mode
|
|
53
55
|
return true
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
57
59
|
menu?.clear()
|
|
60
|
+
|
|
61
|
+
// ====================================================================
|
|
62
|
+
// CUSTOM INVISIBLE MODE: Suppress native menu if array is empty
|
|
63
|
+
// ====================================================================
|
|
64
|
+
if (menuOptions.isEmpty()) {
|
|
65
|
+
// Trick to hide the native action bar: assign a 1x1 pixel empty view
|
|
66
|
+
val view = View(context)
|
|
67
|
+
view.layoutParams = ViewGroup.LayoutParams(1, 1)
|
|
68
|
+
mode?.customView = view
|
|
69
|
+
|
|
70
|
+
val selectionStart = textView.selectionStart
|
|
71
|
+
val selectionEnd = textView.selectionEnd
|
|
72
|
+
if (selectionEnd > selectionStart) {
|
|
73
|
+
val selectedText = textView.text.toString().substring(selectionStart, selectionEnd)
|
|
74
|
+
onSelectionEvent("CUSTOM_MODE", selectedText)
|
|
75
|
+
}
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Standard behavior: populate native menu
|
|
58
80
|
menuOptions.forEachIndexed { index, option ->
|
|
59
81
|
menu?.add(0, index, 0, option)
|
|
60
82
|
}
|
|
@@ -75,7 +97,7 @@ class SelectableTextView : FrameLayout {
|
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
78
|
-
//
|
|
100
|
+
// Clear the reference when the user closes the menu by tapping outside
|
|
79
101
|
currentActionMode = null
|
|
80
102
|
}
|
|
81
103
|
}
|
|
@@ -102,13 +124,13 @@ class SelectableTextView : FrameLayout {
|
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
// ====================================================================
|
|
105
|
-
//
|
|
127
|
+
// SAFETY FIX (Preventing the disappearing views bug)
|
|
106
128
|
// ====================================================================
|
|
107
129
|
override fun onDetachedFromWindow() {
|
|
108
130
|
super.onDetachedFromWindow()
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
131
|
+
// If React Native decides to remove this View from the screen (scroll or navigation)
|
|
132
|
+
// and the native bar is still open, we force it to close.
|
|
133
|
+
// This frees up memory and releases the Android UI Thread.
|
|
112
134
|
currentActionMode?.finish()
|
|
113
135
|
currentActionMode = null
|
|
114
136
|
}
|
|
@@ -20,8 +20,6 @@ using namespace facebook::react;
|
|
|
20
20
|
|
|
21
21
|
- (void)didMoveToWindow {
|
|
22
22
|
[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
23
|
if (self.window) {
|
|
26
24
|
BOOL wasSelectable = self.selectable;
|
|
27
25
|
self.selectable = NO;
|
|
@@ -31,12 +29,8 @@ using namespace facebook::react;
|
|
|
31
29
|
|
|
32
30
|
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
|
33
31
|
{
|
|
34
|
-
// FIX:
|
|
35
|
-
//
|
|
36
|
-
if (action == @selector(copy:) || action == @selector(selectAll:)) {
|
|
37
|
-
return YES;
|
|
38
|
-
}
|
|
39
|
-
|
|
32
|
+
// FIX: Remove hardcoded YES. We delegate entirely to the parent logic
|
|
33
|
+
// to allow complete suppression of the menu in Custom Mode.
|
|
40
34
|
if (self.parentSelectableTextView) {
|
|
41
35
|
return [self.parentSelectableTextView canPerformAction:action withSender:sender];
|
|
42
36
|
}
|
|
@@ -63,7 +57,6 @@ using namespace facebook::react;
|
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
// Intercepts physical keyboard "Copy" and nullifies the action to keep it custom
|
|
67
60
|
- (void)copy:(id)sender
|
|
68
61
|
{
|
|
69
62
|
// Silently blocked
|
|
@@ -142,7 +135,6 @@ using namespace facebook::react;
|
|
|
142
135
|
- (void)prepareForRecycle {
|
|
143
136
|
[super prepareForRecycle];
|
|
144
137
|
|
|
145
|
-
// FIX: Force drop focus when leaving screen
|
|
146
138
|
[_customTextView resignFirstResponder];
|
|
147
139
|
|
|
148
140
|
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
@@ -151,11 +143,6 @@ using namespace facebook::react;
|
|
|
151
143
|
_customTextView.text = nil;
|
|
152
144
|
_customTextView.selectedTextRange = nil;
|
|
153
145
|
|
|
154
|
-
// CRITICAL FIX: Do NOT clear _menuOptions here. Fabric's prop diffing will handle updates.
|
|
155
|
-
// If we clear it here, navigating away and back will result in an empty menu and crash iOS.
|
|
156
|
-
// _menuOptions = @[];
|
|
157
|
-
// _menuOptionsVector.clear();
|
|
158
|
-
|
|
159
146
|
[self unhideAllViews:self];
|
|
160
147
|
}
|
|
161
148
|
|
|
@@ -244,18 +231,59 @@ using namespace facebook::react;
|
|
|
244
231
|
|
|
245
232
|
#pragma mark - UITextViewDelegate
|
|
246
233
|
|
|
234
|
+
// ====================================================================
|
|
235
|
+
// REAL-TIME CUSTOM MODE TRACKING
|
|
236
|
+
// ====================================================================
|
|
237
|
+
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
238
|
+
{
|
|
239
|
+
if (textView.selectedRange.length > 0) {
|
|
240
|
+
// INVISIBLE MODE (ALL IOS VERSIONS)
|
|
241
|
+
if (_menuOptions.count == 0) {
|
|
242
|
+
NSString *selectedText = [textView.text substringWithRange:textView.selectedRange];
|
|
243
|
+
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
244
|
+
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
245
|
+
.chosenOption = std::string("CUSTOM_MODE"),
|
|
246
|
+
.highlightedText = std::string([selectedText UTF8String])
|
|
247
|
+
};
|
|
248
|
+
eventEmitter->onSelection(selectionEvent);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// STANDARD MODE
|
|
254
|
+
if (@available(iOS 16.0, *)) {
|
|
255
|
+
return;
|
|
256
|
+
} else {
|
|
257
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
258
|
+
[self showCustomMenu];
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
263
|
+
|
|
264
|
+
// IF SELECTION IS CLEARED, NOTIFY JS TO HIDE THE BOTTOM SHEET
|
|
265
|
+
if (_menuOptions.count == 0) {
|
|
266
|
+
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
267
|
+
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
268
|
+
.chosenOption = std::string("CUSTOM_MODE"),
|
|
269
|
+
.highlightedText = std::string("")
|
|
270
|
+
};
|
|
271
|
+
eventEmitter->onSelection(selectionEvent);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
247
277
|
// ====================================================================
|
|
248
278
|
// THE NEW IOS 16+ API
|
|
249
279
|
// ====================================================================
|
|
250
280
|
- (UIMenu *)textView:(UITextView *)textView editMenuForTextInRange:(NSRange)range suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0)) {
|
|
251
281
|
|
|
252
|
-
// FORCING THE FOCUS: ensures the system doesn't dismiss our menu unexpectedly
|
|
253
|
-
[textView becomeFirstResponder];
|
|
254
|
-
|
|
255
|
-
// SAFETY NET: If options are empty (due to async loading), return standard copy to avoid crash
|
|
256
282
|
if (_menuOptions.count == 0) {
|
|
257
|
-
return [UIMenu menuWithTitle:@"" children
|
|
283
|
+
return [UIMenu menuWithTitle:@"" children:@[]];
|
|
258
284
|
}
|
|
285
|
+
|
|
286
|
+
[textView becomeFirstResponder];
|
|
259
287
|
|
|
260
288
|
NSMutableArray<UIMenuElement *> *customActions = [[NSMutableArray alloc] init];
|
|
261
289
|
|
|
@@ -266,25 +294,9 @@ using namespace facebook::react;
|
|
|
266
294
|
[customActions addObject:action];
|
|
267
295
|
}
|
|
268
296
|
|
|
269
|
-
// Returns only our custom menu. The native "Copy" action required by the system is swallowed and doesn't appear.
|
|
270
297
|
return [UIMenu menuWithTitle:@"" children:customActions];
|
|
271
298
|
}
|
|
272
299
|
|
|
273
|
-
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
274
|
-
{
|
|
275
|
-
if (@available(iOS 16.0, *)) {
|
|
276
|
-
return;
|
|
277
|
-
} else {
|
|
278
|
-
if (textView.selectedRange.length > 0 && _menuOptions.count > 0) {
|
|
279
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
280
|
-
[self showCustomMenu];
|
|
281
|
-
});
|
|
282
|
-
} else {
|
|
283
|
-
[[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
300
|
- (void)showCustomMenu
|
|
289
301
|
{
|
|
290
302
|
if (![_customTextView canBecomeFirstResponder]) return;
|
|
@@ -323,17 +335,20 @@ using namespace facebook::react;
|
|
|
323
335
|
}
|
|
324
336
|
|
|
325
337
|
// ====================================================================
|
|
326
|
-
//
|
|
338
|
+
// SUPPRESS SYSTEM MENU IN CUSTOM MODE
|
|
327
339
|
// ====================================================================
|
|
328
340
|
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
|
329
341
|
{
|
|
342
|
+
// 100% INVISIBLE MODE: iOS is forbidden to render any bubble menu
|
|
343
|
+
if (_menuOptions.count == 0) {
|
|
344
|
+
return NO;
|
|
345
|
+
}
|
|
346
|
+
|
|
330
347
|
NSString *selectorName = NSStringFromSelector(action);
|
|
331
348
|
if ([selectorName hasPrefix:@"customAction_"] && [selectorName hasSuffix:@":"]) {
|
|
332
349
|
return YES;
|
|
333
350
|
}
|
|
334
351
|
|
|
335
|
-
// We tell iOS that we can perform standard actions.
|
|
336
|
-
// This convinces the system not to abort the menu rendering.
|
|
337
352
|
if (action == @selector(copy:) || action == @selector(selectAll:)) {
|
|
338
353
|
return YES;
|
|
339
354
|
}
|