@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
- // A MÁGICA: Variável para segurar a referência do menu nativo do Android
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
- // Salva a referência da barra nativa do Android assim que ela nasce
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
- // Limpa a referência quando o próprio usuário fecha o menu tocando fora
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
- // A CIRURGIA DE SEGURANÇA (Prevenção do Bug das Views que Somem)
127
+ // SAFETY FIX (Preventing the disappearing views bug)
106
128
  // ====================================================================
107
129
  override fun onDetachedFromWindow() {
108
130
  super.onDetachedFromWindow()
109
- // Se o React Native decidir remover essa View da tela (scroll ou navegação)
110
- // e a barra nativa ainda estiver aberta, nós a fechamos à força.
111
- // Isso devolve a memória e libera a UI Thread do Android.
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: 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
-
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:suggestedActions];
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
- // THE TRICK TO FORCE THE MENU TO OPEN AND PREVENT UIKIT WARNINGS
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thiagobueno/rn-selectable-text",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "A library for custom text selection menus",
6
6
  "main": "./lib/module/index.js",