@thiagobueno/rn-selectable-text 1.0.3 → 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
- // 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
  }
@@ -18,10 +18,21 @@ using namespace facebook::react;
18
18
 
19
19
  @implementation SelectableUITextView
20
20
 
21
+ - (void)didMoveToWindow {
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
+ if (self.window) {
26
+ BOOL wasSelectable = self.selectable;
27
+ self.selectable = NO;
28
+ self.selectable = wasSelectable;
29
+ }
30
+ }
31
+
21
32
  - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
22
33
  {
23
34
  // FIX: We explicitly tell iOS that we can perform standard actions.
24
- // This prevents the "[UIKitCore] The edit menu did not have performable commands" warning and crash.
35
+ // This prevents the "[UIKitCore] The edit menu did not have performable commands" warning.
25
36
  if (action == @selector(copy:) || action == @selector(selectAll:)) {
26
37
  return YES;
27
38
  }
@@ -131,14 +142,16 @@ using namespace facebook::react;
131
142
  - (void)prepareForRecycle {
132
143
  [super prepareForRecycle];
133
144
 
145
+ // FIX: Force drop focus when leaving screen
146
+ [_customTextView resignFirstResponder];
147
+
134
148
  [[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
135
149
  [UIMenuController sharedMenuController].menuItems = nil;
136
150
 
137
151
  _customTextView.text = nil;
138
152
  _customTextView.selectedTextRange = nil;
139
- _menuOptions = @[];
140
- _menuOptionsVector.clear();
141
153
 
154
+ // CRITICAL FIX: Do NOT clear _menuOptions here. Fabric's prop diffing will handle updates.
142
155
  [self unhideAllViews:self];
143
156
  }
144
157
 
@@ -235,6 +248,24 @@ using namespace facebook::react;
235
248
  // FORCING THE FOCUS: ensures the system doesn't dismiss our menu unexpectedly
236
249
  [textView becomeFirstResponder];
237
250
 
251
+ // ====================================================================
252
+ // CUSTOM INVISIBLE MODE: Suppress native menu if array is empty
253
+ // ====================================================================
254
+ if (_menuOptions.count == 0) {
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:@[]];
267
+ }
268
+
238
269
  NSMutableArray<UIMenuElement *> *customActions = [[NSMutableArray alloc] init];
239
270
 
240
271
  for (NSString *option in _menuOptions) {
@@ -244,7 +275,6 @@ using namespace facebook::react;
244
275
  [customActions addObject:action];
245
276
  }
246
277
 
247
- // Returns only our custom menu. The native "Copy" action required by the system is swallowed and doesn't appear.
248
278
  return [UIMenu menuWithTitle:@"" children:customActions];
249
279
  }
250
280
 
@@ -253,7 +283,22 @@ using namespace facebook::react;
253
283
  if (@available(iOS 16.0, *)) {
254
284
  return;
255
285
  } else {
256
- if (textView.selectedRange.length > 0 && _menuOptions.count > 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
+
257
302
  dispatch_async(dispatch_get_main_queue(), ^{
258
303
  [self showCustomMenu];
259
304
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thiagobueno/rn-selectable-text",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "A library for custom text selection menus",
6
6
  "main": "./lib/module/index.js",
@@ -164,4 +164,4 @@
164
164
  "type": "fabric-view",
165
165
  "version": "0.54.2"
166
166
  }
167
- }
167
+ }