@thiagobueno/rn-selectable-text 1.0.4 → 1.0.5
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
|
}
|
|
@@ -152,10 +152,6 @@ using namespace facebook::react;
|
|
|
152
152
|
_customTextView.selectedTextRange = nil;
|
|
153
153
|
|
|
154
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
155
|
[self unhideAllViews:self];
|
|
160
156
|
}
|
|
161
157
|
|
|
@@ -252,9 +248,22 @@ using namespace facebook::react;
|
|
|
252
248
|
// FORCING THE FOCUS: ensures the system doesn't dismiss our menu unexpectedly
|
|
253
249
|
[textView becomeFirstResponder];
|
|
254
250
|
|
|
255
|
-
//
|
|
251
|
+
// ====================================================================
|
|
252
|
+
// CUSTOM INVISIBLE MODE: Suppress native menu if array is empty
|
|
253
|
+
// ====================================================================
|
|
256
254
|
if (_menuOptions.count == 0) {
|
|
257
|
-
|
|
255
|
+
NSString *selectedText = [textView.text substringWithRange:range];
|
|
256
|
+
if (selectedText.length > 0) {
|
|
257
|
+
if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
|
|
258
|
+
SelectableTextViewEventEmitter::OnSelection selectionEvent = {
|
|
259
|
+
.chosenOption = std::string("CUSTOM_MODE"),
|
|
260
|
+
.highlightedText = std::string([selectedText UTF8String])
|
|
261
|
+
};
|
|
262
|
+
eventEmitter->onSelection(selectionEvent);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Returns an empty menu, effectively hiding the native UI
|
|
266
|
+
return [UIMenu menuWithTitle:@"" children:@[]];
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
NSMutableArray<UIMenuElement *> *customActions = [[NSMutableArray alloc] init];
|
|
@@ -266,7 +275,6 @@ using namespace facebook::react;
|
|
|
266
275
|
[customActions addObject:action];
|
|
267
276
|
}
|
|
268
277
|
|
|
269
|
-
// Returns only our custom menu. The native "Copy" action required by the system is swallowed and doesn't appear.
|
|
270
278
|
return [UIMenu menuWithTitle:@"" children:customActions];
|
|
271
279
|
}
|
|
272
280
|
|
|
@@ -275,7 +283,22 @@ using namespace facebook::react;
|
|
|
275
283
|
if (@available(iOS 16.0, *)) {
|
|
276
284
|
return;
|
|
277
285
|
} else {
|
|
278
|
-
if (textView.selectedRange.length > 0
|
|
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
|
+
|
|
279
302
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
280
303
|
[self showCustomMenu];
|
|
281
304
|
});
|