@thiagobueno/rn-selectable-text 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Robert Sherling
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ Disclaimer -
2
+
3
+ I tested this code in my own projects, but this code has been with heavy assistance from Claude Code. If you see a problem - submit a ticket!
4
+
5
+ # rn-selectable-text
6
+
7
+ A React Native library for custom text selection menus, redesigned from the ground up for React Native 0.81.1 with full support for the new architecture (Fabric).
8
+
9
+ The `SelectableTextView` component wraps your text content and provides custom menu options that appear when users select text. It supports nested text styling and cross-platform event handling.
10
+
11
+ ## Features
12
+
13
+ - Cross-platform support (iOS & Android)
14
+ - Support for nested text with different styles
15
+ - Custom menu options with callback handling
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ yarn add @thiagobueno/rn-selectable-text
21
+ ```
22
+
23
+ For iOS, run pod install:
24
+ ```sh
25
+ cd ios && pod install
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Basic Example
31
+
32
+ ```tsx
33
+ import React, { useState } from 'react';
34
+ import { View, Text, Alert } from 'react-native';
35
+ import { SelectableTextView } from '@thiagobueno/rn-selectable-text';
36
+
37
+ export default function App() {
38
+
39
+ const handleSelection = (event) => {
40
+ const { chosenOption, highlightedText } = event;
41
+ Alert.alert(
42
+ 'Selection Event',
43
+ `Option: ${chosenOption}\nSelected Text: ${highlightedText}`
44
+ );
45
+ };
46
+
47
+ return (
48
+ <View>
49
+
50
+ <SelectableTextView
51
+ menuOptions={['look up', 'copy', 'share']}
52
+ onSelection={handleSelection}
53
+ style={{ margin: 20 }}
54
+ >
55
+ <Text>This is simple selectable text</Text>
56
+ </SelectableTextView>
57
+ </View>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ### Advanced Example with Nested Text Styling
63
+
64
+ ```jsx
65
+ <SelectableTextView
66
+ menuOptions={['Action 1', 'Action 2', 'Custom Action']}
67
+ onSelection={handleSelection}
68
+ style={{ marginHorizontal: 20 }}
69
+ >
70
+ <Text style={{ color: 'black', fontSize: 16 }}>
71
+ This text is black{' '}
72
+ <Text style={{ textDecorationLine: 'underline', color: 'red' }}>
73
+ this part is underlined and red
74
+ </Text>{' '}
75
+ and this is black again. All of it is selectable with custom menu options!
76
+ </Text>
77
+ </SelectableTextView>
78
+ ```
79
+
80
+ ## API Reference
81
+
82
+ ### SelectableTextView Props
83
+
84
+ | Prop | Type | Required | Description |
85
+ | ------------- | --------------------------------- | -------- | ------------------------------------------- |
86
+ | `children` | `React.ReactNode` | Yes | Text components to make selectable |
87
+ | `menuOptions` | `string[]` | Yes | Array of menu option strings |
88
+ | `onSelection` | `(event: SelectionEvent) => void` | No | Callback fired when menu option is selected |
89
+ | `style` | `ViewStyle` | No | Style object for the container |
90
+
91
+ ### SelectionEvent
92
+
93
+ The `onSelection` callback receives an event object with:
94
+
95
+ ```typescript
96
+ interface SelectionEvent {
97
+ chosenOption: string; // The menu option that was selected
98
+ highlightedText: string; // The text that was highlighted by the user
99
+ }
100
+ ```
101
+
102
+ ## Requirements
103
+
104
+ - React Native 0.81.1+
105
+ - iOS 11.0+
106
+ - Android API level 21+
107
+ - React Native's new architecture (Fabric) enabled
108
+
109
+ ## Platform Differences
110
+
111
+ The library handles platform differences internally:
112
+ - **iOS**: Uses direct event handlers for optimal performance
113
+ - **Android**: Uses DeviceEventEmitter for reliable event delivery
114
+
115
+ Both platforms provide the same API and functionality.
116
+
117
+ ## License
118
+
119
+ MIT
120
+
121
+ ---
122
+
123
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,77 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['SelectableText_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+
22
+ apply plugin: "com.facebook.react"
23
+
24
+ def getExtOrIntegerDefault(name) {
25
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["SelectableText_" + name]).toInteger()
26
+ }
27
+
28
+ android {
29
+ namespace "com.selectabletext"
30
+
31
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
32
+
33
+ defaultConfig {
34
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
35
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
36
+ }
37
+
38
+ buildFeatures {
39
+ buildConfig true
40
+ }
41
+
42
+ buildTypes {
43
+ release {
44
+ minifyEnabled false
45
+ }
46
+ }
47
+
48
+ lintOptions {
49
+ disable "GradleCompatible"
50
+ }
51
+
52
+ compileOptions {
53
+ sourceCompatibility JavaVersion.VERSION_1_8
54
+ targetCompatibility JavaVersion.VERSION_1_8
55
+ }
56
+
57
+ sourceSets {
58
+ main {
59
+ java.srcDirs += [
60
+ "generated/java",
61
+ "generated/jni"
62
+ ]
63
+ }
64
+ }
65
+ }
66
+
67
+ repositories {
68
+ mavenCentral()
69
+ google()
70
+ }
71
+
72
+ def kotlin_version = getExtOrDefault("kotlinVersion")
73
+
74
+ dependencies {
75
+ implementation "com.facebook.react:react-android"
76
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
+ }
@@ -0,0 +1,5 @@
1
+ SelectableText_kotlinVersion=2.0.21
2
+ SelectableText_minSdkVersion=24
3
+ SelectableText_targetSdkVersion=34
4
+ SelectableText_compileSdkVersion=35
5
+ SelectableText_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,19 @@
1
+ package com.selectabletext
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+ import java.util.ArrayList
8
+
9
+ class SelectableTextViewPackage : ReactPackage {
10
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
11
+ val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
12
+ viewManagers.add(SelectableTextViewManager())
13
+ return viewManagers
14
+ }
15
+
16
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
17
+ return emptyList()
18
+ }
19
+ }
@@ -0,0 +1,97 @@
1
+ package com.selectabletext
2
+
3
+ import android.content.Context
4
+ import android.util.AttributeSet
5
+ import android.view.ActionMode
6
+ import android.view.Menu
7
+ import android.view.MenuItem
8
+ import android.widget.FrameLayout
9
+ import android.widget.TextView
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.facebook.react.bridge.ReactContext
12
+ import com.facebook.react.bridge.WritableMap
13
+ import com.facebook.react.modules.core.DeviceEventManagerModule
14
+
15
+ class SelectableTextView : FrameLayout {
16
+ private var menuOptions: Array<String> = emptyArray()
17
+ private var textView: TextView? = null
18
+
19
+ constructor(context: Context?) : super(context!!)
20
+ constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs)
21
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
22
+ context!!,
23
+ attrs,
24
+ defStyleAttr
25
+ )
26
+
27
+ fun setMenuOptions(options: Array<String>) {
28
+ this.menuOptions = options
29
+ setupTextView()
30
+ }
31
+
32
+ private fun setupTextView() {
33
+ // Find the first TextView child
34
+ for (i in 0 until childCount) {
35
+ val child = getChildAt(i)
36
+ if (child is TextView) {
37
+ textView = child
38
+ setupSelectionCallback(child)
39
+ break
40
+ }
41
+ }
42
+ }
43
+
44
+ private fun setupSelectionCallback(textView: TextView) {
45
+ textView.setTextIsSelectable(true)
46
+ textView.customSelectionActionModeCallback = object : ActionMode.Callback {
47
+ override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
48
+ return true
49
+ }
50
+
51
+ override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
52
+ menu?.clear()
53
+ menuOptions.forEachIndexed { index, option ->
54
+ menu?.add(0, index, 0, option)
55
+ }
56
+ return true
57
+ }
58
+
59
+ override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
60
+ val selectionStart = textView.selectionStart
61
+ val selectionEnd = textView.selectionEnd
62
+ val selectedText = textView.text.toString().substring(selectionStart, selectionEnd)
63
+ val chosenOption = menuOptions[item?.itemId ?: 0]
64
+
65
+ // Send event to React Native
66
+ onSelectionEvent(chosenOption, selectedText)
67
+
68
+ mode?.finish()
69
+ return true
70
+ }
71
+
72
+ override fun onDestroyActionMode(mode: ActionMode?) {
73
+ // Called when action mode is destroyed
74
+ }
75
+ }
76
+ }
77
+
78
+ private fun onSelectionEvent(chosenOption: String, highlightedText: String) {
79
+ val reactContext = context as ReactContext
80
+ val params = Arguments.createMap().apply {
81
+ putInt("viewTag", id)
82
+ putString("chosenOption", chosenOption)
83
+ putString("highlightedText", highlightedText)
84
+ }
85
+
86
+ reactContext
87
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
88
+ .emit("SelectableTextSelection", params)
89
+ }
90
+
91
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
92
+ super.onLayout(changed, left, top, right, bottom)
93
+ if (changed && textView == null) {
94
+ setupTextView()
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,53 @@
1
+ package com.selectabletext
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.common.MapBuilder
5
+ import com.facebook.react.module.annotations.ReactModule
6
+ import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import com.facebook.react.uimanager.ViewManagerDelegate
9
+ import com.facebook.react.uimanager.annotations.ReactProp
10
+ import com.facebook.react.viewmanagers.SelectableTextViewManagerInterface
11
+ import com.facebook.react.viewmanagers.SelectableTextViewManagerDelegate
12
+
13
+ @ReactModule(name = SelectableTextViewManager.NAME)
14
+ class SelectableTextViewManager : ViewGroupManager<SelectableTextView>(),
15
+ SelectableTextViewManagerInterface<SelectableTextView> {
16
+ private val mDelegate: ViewManagerDelegate<SelectableTextView>
17
+
18
+ init {
19
+ mDelegate = SelectableTextViewManagerDelegate(this)
20
+ }
21
+
22
+ override fun getDelegate(): ViewManagerDelegate<SelectableTextView>? {
23
+ return mDelegate
24
+ }
25
+
26
+ override fun getName(): String {
27
+ return NAME
28
+ }
29
+
30
+ public override fun createViewInstance(context: ThemedReactContext): SelectableTextView {
31
+ return SelectableTextView(context)
32
+ }
33
+
34
+ @ReactProp(name = "menuOptions")
35
+ override fun setMenuOptions(view: SelectableTextView, menuOptions: ReadableArray?) {
36
+ if (menuOptions != null) {
37
+ val options = Array(menuOptions.size()) { i ->
38
+ menuOptions.getString(i) ?: ""
39
+ }
40
+ view.setMenuOptions(options)
41
+ }
42
+ }
43
+
44
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
45
+ return MapBuilder.builder<String, Any>()
46
+ .put("topSelection", MapBuilder.of("registrationName", "onSelection"))
47
+ .build()
48
+ }
49
+
50
+ companion object {
51
+ const val NAME = "SelectableTextView"
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #ifndef SelectableTextViewNativeComponent_h
5
+ #define SelectableTextViewNativeComponent_h
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ @interface SelectableTextView : RCTViewComponentView <UITextViewDelegate>
10
+ @property (nonatomic, strong) UITextView *textView;
11
+ @property (nonatomic, strong) NSArray<NSString *> *menuOptions;
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
15
+
16
+ #endif /* SelectableTextViewNativeComponent_h */
@@ -0,0 +1,367 @@
1
+ #import "SelectableTextView.h"
2
+
3
+ #import <react/renderer/components/SelectableTextViewSpec/ComponentDescriptors.h>
4
+ #import <react/renderer/components/SelectableTextViewSpec/EventEmitters.h>
5
+ #import <react/renderer/components/SelectableTextViewSpec/Props.h>
6
+ #import <react/renderer/components/SelectableTextViewSpec/RCTComponentViewHelpers.h>
7
+
8
+ #import "RCTFabricComponentsPlugins.h"
9
+ #import <React/RCTConversions.h>
10
+
11
+ using namespace facebook::react;
12
+
13
+ @class SelectableTextView;
14
+
15
+ @interface SelectableUITextView : UITextView
16
+ @property (nonatomic, weak) SelectableTextView *parentSelectableTextView;
17
+ @end
18
+
19
+ @implementation SelectableUITextView
20
+
21
+ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
22
+ {
23
+ if (self.parentSelectableTextView) {
24
+ return [self.parentSelectableTextView canPerformAction:action withSender:sender];
25
+ }
26
+ return [super canPerformAction:action withSender:sender];
27
+ }
28
+
29
+ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
30
+ {
31
+ if (self.parentSelectableTextView) {
32
+ NSMethodSignature *signature = [self.parentSelectableTextView methodSignatureForSelector:aSelector];
33
+ if (signature) {
34
+ return signature;
35
+ }
36
+ }
37
+ return [super methodSignatureForSelector:aSelector];
38
+ }
39
+
40
+ - (void)forwardInvocation:(NSInvocation *)anInvocation
41
+ {
42
+ if (self.parentSelectableTextView) {
43
+ [self.parentSelectableTextView forwardInvocation:anInvocation];
44
+ } else {
45
+ [super forwardInvocation:anInvocation];
46
+ }
47
+ }
48
+
49
+ - (void)copy:(id)sender
50
+ {
51
+ // Bloqueado
52
+ }
53
+
54
+ @end
55
+
56
+ @interface SelectableTextView () <RCTSelectableTextViewViewProtocol, UITextViewDelegate>
57
+ - (void)unhideAllViews:(UIView *)view;
58
+ @end
59
+
60
+ @implementation SelectableTextView {
61
+ std::vector<std::string> _menuOptionsVector;
62
+ SelectableUITextView *_customTextView;
63
+ NSArray<NSString *> *_menuOptions;
64
+ }
65
+
66
+ + (ComponentDescriptorProvider)componentDescriptorProvider
67
+ {
68
+ return concreteComponentDescriptorProvider<SelectableTextViewComponentDescriptor>();
69
+ }
70
+
71
+ - (instancetype)initWithFrame:(CGRect)frame
72
+ {
73
+ if (self = [super initWithFrame:frame]) {
74
+ static const auto defaultProps = std::make_shared<const SelectableTextViewProps>();
75
+ _props = defaultProps;
76
+
77
+ _customTextView = [[SelectableUITextView alloc] init];
78
+ _customTextView.parentSelectableTextView = self;
79
+ _customTextView.delegate = self;
80
+ _customTextView.editable = NO;
81
+ _customTextView.selectable = YES;
82
+ _customTextView.scrollEnabled = NO;
83
+ _customTextView.backgroundColor = [UIColor clearColor];
84
+ _customTextView.textContainerInset = UIEdgeInsetsZero;
85
+ _customTextView.textContainer.lineFragmentPadding = 0;
86
+ _customTextView.userInteractionEnabled = YES;
87
+
88
+ _customTextView.allowsEditingTextAttributes = NO;
89
+ _customTextView.dataDetectorTypes = UIDataDetectorTypeNone;
90
+
91
+ _customTextView.text = @"";
92
+ _menuOptions = @[];
93
+
94
+ self.contentView = _customTextView;
95
+ self.userInteractionEnabled = YES;
96
+ }
97
+
98
+ return self;
99
+ }
100
+
101
+ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
102
+ oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
103
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
104
+
105
+ CGRect frame = RCTCGRectFromRect(layoutMetrics.getContentFrame());
106
+ _customTextView.frame = frame;
107
+ }
108
+
109
+ - (void)unhideAllViews:(UIView *)view {
110
+ if (view != _customTextView && view != self) {
111
+ view.hidden = NO;
112
+ }
113
+ for (UIView *subview in view.subviews) {
114
+ [self unhideAllViews:subview];
115
+ }
116
+ }
117
+
118
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
119
+ {
120
+ [self unhideAllViews:childComponentView];
121
+ [super unmountChildComponentView:childComponentView index:index];
122
+ }
123
+
124
+ - (void)prepareForRecycle {
125
+ [super prepareForRecycle];
126
+
127
+ [[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
128
+ [UIMenuController sharedMenuController].menuItems = nil;
129
+
130
+ _customTextView.text = nil;
131
+ _customTextView.selectedTextRange = nil;
132
+ _menuOptions = @[];
133
+ _menuOptionsVector.clear();
134
+
135
+ [self unhideAllViews:self];
136
+ }
137
+
138
+ - (void)dealloc {
139
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
140
+ [UIMenuController sharedMenuController].menuItems = nil;
141
+ }
142
+
143
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
144
+ {
145
+ const auto &oldViewProps = *std::static_pointer_cast<SelectableTextViewProps const>(_props);
146
+ const auto &newViewProps = *std::static_pointer_cast<SelectableTextViewProps const>(props);
147
+
148
+ if (oldViewProps.menuOptions != newViewProps.menuOptions) {
149
+ _menuOptionsVector = newViewProps.menuOptions;
150
+
151
+ NSMutableArray<NSString *> *options = [[NSMutableArray alloc] init];
152
+ for (const auto& option : _menuOptionsVector) {
153
+ [options addObject:[NSString stringWithUTF8String:option.c_str()]];
154
+ }
155
+ _menuOptions = options;
156
+ }
157
+
158
+ [super updateProps:props oldProps:oldProps];
159
+ }
160
+
161
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
162
+ {
163
+ [super mountChildComponentView:childComponentView index:index];
164
+ }
165
+
166
+ - (void)layoutSubviews
167
+ {
168
+ [super layoutSubviews];
169
+ [self updateTextViewContent];
170
+ }
171
+
172
+ - (void)updateTextViewContent
173
+ {
174
+ NSMutableAttributedString *combinedAttributedText = [[NSMutableAttributedString alloc] init];
175
+ [self extractStyledTextFromView:self intoAttributedString:combinedAttributedText hideViews:YES];
176
+ _customTextView.attributedText = combinedAttributedText;
177
+ }
178
+
179
+ - (void)extractStyledTextFromView:(UIView *)view intoAttributedString:(NSMutableAttributedString *)attributedString hideViews:(BOOL)hideViews
180
+ {
181
+ BOOL foundText = NO;
182
+
183
+ if ([view respondsToSelector:@selector(attributedText)]) {
184
+ NSAttributedString *attributedText = [view performSelector:@selector(attributedText)];
185
+ if (attributedText && attributedText.length > 0) {
186
+ [attributedString appendAttributedString:attributedText];
187
+ foundText = YES;
188
+ }
189
+ }
190
+ else if ([view isKindOfClass:[UILabel class]]) {
191
+ UILabel *label = (UILabel *)view;
192
+ if (label.attributedText && label.attributedText.length > 0) {
193
+ [attributedString appendAttributedString:label.attributedText];
194
+ foundText = YES;
195
+ } else if (label.text && label.text.length > 0) {
196
+ NSAttributedString *plainText = [[NSAttributedString alloc] initWithString:label.text];
197
+ [attributedString appendAttributedString:plainText];
198
+ foundText = YES;
199
+ }
200
+ }
201
+ else if ([view respondsToSelector:@selector(text)]) {
202
+ NSString *text = [view performSelector:@selector(text)];
203
+ if (text && text.length > 0) {
204
+ NSAttributedString *plainText = [[NSAttributedString alloc] initWithString:text];
205
+ [attributedString appendAttributedString:plainText];
206
+ foundText = YES;
207
+ }
208
+ }
209
+
210
+ if (foundText && hideViews && view != _customTextView && view != self) {
211
+ view.hidden = YES;
212
+ }
213
+
214
+ for (UIView *subview in view.subviews) {
215
+ if (subview != _customTextView && subview != self.contentView) {
216
+ [self extractStyledTextFromView:subview intoAttributedString:attributedString hideViews:hideViews];
217
+ }
218
+ }
219
+ }
220
+
221
+ #pragma mark - UITextViewDelegate
222
+
223
+ // ====================================================================
224
+ // A NOVA API DO IOS 16+ (Estável, Rápida e Imune a Crashes)
225
+ // ====================================================================
226
+ - (UIMenu *)textView:(UITextView *)textView editMenuForTextInRange:(NSRange)range suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0)) {
227
+ NSMutableArray<UIMenuElement *> *customActions = [[NSMutableArray alloc] init];
228
+
229
+ for (NSString *option in _menuOptions) {
230
+ // Mapeia o botão diretamente para nossa função sem precisar de hacks de string
231
+ UIAction *action = [UIAction actionWithTitle:option image:nil identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
232
+ [self handleMenuSelection:option];
233
+ }];
234
+ [customActions addObject:action];
235
+ }
236
+
237
+ // Retorna APENAS o nosso menu customizado, esmagando as opções padrões da Apple
238
+ return [UIMenu menuWithTitle:@"" children:customActions];
239
+ }
240
+
241
+ - (void)textViewDidChangeSelection:(UITextView *)textView
242
+ {
243
+ if (@available(iOS 16.0, *)) {
244
+ // No iOS 16+, a Apple cuida da exibição do menu automaticamente via delegate
245
+ return;
246
+ } else {
247
+ // Fallback legado para iPhones antigos (iOS 15 ou menor)
248
+ if (textView.selectedRange.length > 0 && _menuOptions.count > 0) {
249
+ dispatch_async(dispatch_get_main_queue(), ^{
250
+ [self showCustomMenu];
251
+ });
252
+ } else {
253
+ [[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
254
+ }
255
+ }
256
+ }
257
+
258
+ // Fallback apenas para iOS 15 e inferiores
259
+ - (void)showCustomMenu
260
+ {
261
+ if (![_customTextView canBecomeFirstResponder]) return;
262
+
263
+ [_customTextView becomeFirstResponder];
264
+
265
+ UIMenuController *menuController = [UIMenuController sharedMenuController];
266
+ menuController.menuItems = nil;
267
+
268
+ NSMutableArray<UIMenuItem *> *menuItems = [[NSMutableArray alloc] init];
269
+
270
+ for (NSString *option in _menuOptions) {
271
+ NSString *clean1 = [option stringByReplacingOccurrencesOfString:@" " withString:@"_"];
272
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-zA-Z0-9_]" options:0 error:nil];
273
+ NSString *selectorName = [regex stringByReplacingMatchesInString:clean1 options:0 range:NSMakeRange(0, clean1.length) withTemplate:@"_"];
274
+
275
+ SEL action = NSSelectorFromString([NSString stringWithFormat:@"customAction_%@:", selectorName]);
276
+ UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:option action:action];
277
+ [menuItems addObject:menuItem];
278
+ }
279
+
280
+ menuController.menuItems = menuItems;
281
+ [menuController update];
282
+
283
+ CGRect selectedRect = [_customTextView firstRectForRange:_customTextView.selectedTextRange];
284
+
285
+ if (!CGRectIsEmpty(selectedRect)) {
286
+ CGRect targetRect = [_customTextView convertRect:selectedRect toView:_customTextView];
287
+ [menuController showMenuFromView:_customTextView rect:targetRect];
288
+ }
289
+ }
290
+
291
+ - (BOOL)canBecomeFirstResponder
292
+ {
293
+ return YES;
294
+ }
295
+
296
+ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
297
+ {
298
+ NSString *selectorName = NSStringFromSelector(action);
299
+ if ([selectorName hasPrefix:@"customAction_"] && [selectorName hasSuffix:@":"]) {
300
+ return YES;
301
+ }
302
+ return NO;
303
+ }
304
+
305
+ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
306
+ {
307
+ NSString *selectorName = NSStringFromSelector(aSelector);
308
+ if ([selectorName hasPrefix:@"customAction_"] && [selectorName hasSuffix:@":"]) {
309
+ return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
310
+ }
311
+ return [super methodSignatureForSelector:aSelector];
312
+ }
313
+
314
+ - (void)forwardInvocation:(NSInvocation *)anInvocation
315
+ {
316
+ NSString *selectorName = NSStringFromSelector(anInvocation.selector);
317
+
318
+ if ([selectorName hasPrefix:@"customAction_"] && [selectorName hasSuffix:@":"]) {
319
+ NSString *cleanedOption = [selectorName substringWithRange:NSMakeRange(13, selectorName.length - 14)];
320
+
321
+ NSString *originalOption = nil;
322
+ for (NSString *option in _menuOptions) {
323
+ NSString *clean1 = [option stringByReplacingOccurrencesOfString:@" " withString:@"_"];
324
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-zA-Z0-9_]" options:0 error:nil];
325
+ NSString *testSelectorName = [regex stringByReplacingMatchesInString:clean1 options:0 range:NSMakeRange(0, clean1.length) withTemplate:@"_"];
326
+
327
+ if ([testSelectorName isEqualToString:cleanedOption]) {
328
+ originalOption = option;
329
+ break;
330
+ }
331
+ }
332
+
333
+ if (originalOption) {
334
+ [self handleMenuSelection:originalOption];
335
+ }
336
+ } else {
337
+ [super forwardInvocation:anInvocation];
338
+ }
339
+ }
340
+
341
+ - (void)handleMenuSelection:(NSString *)selectedOption
342
+ {
343
+ NSRange selectedRange = _customTextView.selectedRange;
344
+ NSString *selectedText = @"";
345
+
346
+ if (selectedRange.location != NSNotFound && selectedRange.length > 0) {
347
+ selectedText = [_customTextView.text substringWithRange:selectedRange];
348
+ }
349
+
350
+ _customTextView.selectedRange = NSMakeRange(0, 0);
351
+ [[UIMenuController sharedMenuController] hideMenuFromView:_customTextView];
352
+
353
+ if (auto eventEmitter = std::static_pointer_cast<const SelectableTextViewEventEmitter>(_eventEmitter)) {
354
+ SelectableTextViewEventEmitter::OnSelection selectionEvent = {
355
+ .chosenOption = std::string([selectedOption UTF8String]),
356
+ .highlightedText = std::string([selectedText UTF8String])
357
+ };
358
+ eventEmitter->onSelection(selectionEvent);
359
+ }
360
+ }
361
+
362
+ Class<RCTComponentViewProtocol> SelectableTextViewCls(void)
363
+ {
364
+ return SelectableTextView.class;
365
+ }
366
+
367
+ @end
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ import React, { useRef, useEffect } from 'react';
4
+ import { Platform, findNodeHandle, DeviceEventEmitter } from 'react-native';
5
+ import SelectableTextViewNativeComponent from './SelectableTextViewNativeComponent';
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ export const SelectableTextView = ({
8
+ children,
9
+ menuOptions,
10
+ onSelection,
11
+ style
12
+ }) => {
13
+ const viewRef = useRef(null);
14
+ useEffect(() => {
15
+ if (Platform.OS === 'android' && onSelection) {
16
+ const subscription = DeviceEventEmitter.addListener('SelectableTextSelection', eventData => {
17
+ const viewTag = findNodeHandle(viewRef.current);
18
+ if (viewTag === eventData.viewTag) {
19
+ onSelection({
20
+ chosenOption: eventData.chosenOption,
21
+ highlightedText: eventData.highlightedText
22
+ });
23
+ }
24
+ });
25
+ return () => subscription.remove();
26
+ }
27
+ return () => {};
28
+ }, [onSelection]);
29
+ const handleSelection = event => {
30
+ if (Platform.OS === 'ios' && onSelection) {
31
+ console.log('SelectableTextView - Direct event received:', event.nativeEvent);
32
+ onSelection(event.nativeEvent);
33
+ }
34
+ };
35
+ return /*#__PURE__*/_jsx(SelectableTextViewNativeComponent, {
36
+ ref: viewRef,
37
+ style: style,
38
+ menuOptions: menuOptions,
39
+ onSelection: Platform.OS === 'ios' ? handleSelection : undefined,
40
+ children: children
41
+ });
42
+ };
43
+ //# sourceMappingURL=SelectableTextView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","useRef","useEffect","Platform","findNodeHandle","DeviceEventEmitter","SelectableTextViewNativeComponent","jsx","_jsx","SelectableTextView","children","menuOptions","onSelection","style","viewRef","OS","subscription","addListener","eventData","viewTag","current","chosenOption","highlightedText","remove","handleSelection","event","console","log","nativeEvent","ref","undefined"],"sourceRoot":"../../src","sources":["SelectableTextView.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,MAAM,EAAEC,SAAS,QAAQ,OAAO;AAEhD,SAASC,QAAQ,EAAEC,cAAc,EAAEC,kBAAkB,QAAQ,cAAc;AAC3E,OAAOC,iCAAiC,MAEjC,qCAAqC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAS7C,OAAO,MAAMC,kBAAqD,GAAGA,CAAC;EAClEC,QAAQ;EACRC,WAAW;EACXC,WAAW;EACXC;AACJ,CAAC,KAAK;EACF,MAAMC,OAAO,GAAGb,MAAM,CAAM,IAAI,CAAC;EAEjCC,SAAS,CAAC,MAAM;IACZ,IAAIC,QAAQ,CAACY,EAAE,KAAK,SAAS,IAAIH,WAAW,EAAE;MAC1C,MAAMI,YAAY,GAAGX,kBAAkB,CAACY,WAAW,CAC/C,yBAAyB,EACxBC,SAIA,IAAK;QACF,MAAMC,OAAO,GAAGf,cAAc,CAACU,OAAO,CAACM,OAAO,CAAC;QAC/C,IAAID,OAAO,KAAKD,SAAS,CAACC,OAAO,EAAE;UAC/BP,WAAW,CAAC;YACRS,YAAY,EAAEH,SAAS,CAACG,YAAY;YACpCC,eAAe,EAAEJ,SAAS,CAACI;UAC/B,CAAC,CAAC;QACN;MACJ,CACJ,CAAC;MAED,OAAO,MAAMN,YAAY,CAACO,MAAM,CAAC,CAAC;IACtC;IACA,OAAO,MAAM,CAAE,CAAC;EACpB,CAAC,EAAE,CAACX,WAAW,CAAC,CAAC;EAEjB,MAAMY,eAAe,GAAIC,KAA2C,IAAK;IACrE,IAAItB,QAAQ,CAACY,EAAE,KAAK,KAAK,IAAIH,WAAW,EAAE;MACtCc,OAAO,CAACC,GAAG,CACP,6CAA6C,EAC7CF,KAAK,CAACG,WACV,CAAC;MACDhB,WAAW,CAACa,KAAK,CAACG,WAAW,CAAC;IAClC;EACJ,CAAC;EAED,oBACIpB,IAAA,CAACF,iCAAiC;IAC9BuB,GAAG,EAAEf,OAAQ;IACbD,KAAK,EAAEA,KAAM;IACbF,WAAW,EAAEA,WAAY;IACzBC,WAAW,EAAET,QAAQ,CAACY,EAAE,KAAK,KAAK,GAAGS,eAAe,GAAGM,SAAU;IAAApB,QAAA,EAEhEA;EAAQ,CACsB,CAAC;AAE5C,CAAC","ignoreList":[]}
@@ -0,0 +1,14 @@
1
+ import { codegenNativeComponent, type ViewProps } from 'react-native';
2
+ import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypesNamespace';
3
+
4
+ export interface SelectionEvent {
5
+ chosenOption: string;
6
+ highlightedText: string;
7
+ }
8
+
9
+ interface NativeProps extends ViewProps {
10
+ menuOptions: readonly string[];
11
+ onSelection?: DirectEventHandler<SelectionEvent>;
12
+ }
13
+
14
+ export default codegenNativeComponent<NativeProps>('SelectableTextView');
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export { SelectableTextView } from "./SelectableTextView.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["SelectableTextView"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,yBAAsB","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { type SelectionEvent } from './SelectableTextViewNativeComponent';
4
+ interface SelectableTextViewProps {
5
+ children: React.ReactNode;
6
+ menuOptions: string[];
7
+ onSelection?: (event: SelectionEvent) => void;
8
+ style?: ViewStyle;
9
+ }
10
+ export declare const SelectableTextView: React.FC<SelectableTextViewProps>;
11
+ export {};
12
+ //# sourceMappingURL=SelectableTextView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectableTextView.d.ts","sourceRoot":"","sources":["../../../src/SelectableTextView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAwB,MAAM,cAAc,CAAC;AAEpE,OAA0C,EACtC,KAAK,cAAc,EACtB,MAAM,qCAAqC,CAAC;AAE7C,UAAU,uBAAuB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC;CACrB;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAoDhE,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type ViewProps } from 'react-native';
2
+ import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypesNamespace';
3
+ export interface SelectionEvent {
4
+ chosenOption: string;
5
+ highlightedText: string;
6
+ }
7
+ interface NativeProps extends ViewProps {
8
+ menuOptions: readonly string[];
9
+ onSelection?: DirectEventHandler<SelectionEvent>;
10
+ }
11
+ declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
12
+ export default _default;
13
+ //# sourceMappingURL=SelectableTextViewNativeComponent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectableTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/SelectableTextViewNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AAE7F,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,UAAU,WAAY,SAAQ,SAAS;IACnC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;CACpD;;AAED,wBAAyE"}
@@ -0,0 +1,2 @@
1
+ export { SelectableTextView } from './SelectableTextView';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,170 @@
1
+ {
2
+ "name": "@thiagobueno/rn-selectable-text",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "A library for custom text selection menus",
6
+ "main": "./lib/module/index.js",
7
+ "types": "./lib/typescript/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "source": "./src/index.tsx",
11
+ "types": "./lib/typescript/src/index.d.ts",
12
+ "default": "./lib/module/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "lib",
19
+ "android",
20
+ "ios",
21
+ "cpp",
22
+ "*.podspec",
23
+ "react-native.config.js",
24
+ "!ios/build",
25
+ "!android/build",
26
+ "!android/gradle",
27
+ "!android/gradlew",
28
+ "!android/gradlew.bat",
29
+ "!android/local.properties",
30
+ "!**/__tests__",
31
+ "!**/__fixtures__",
32
+ "!**/__mocks__",
33
+ "!**/.*"
34
+ ],
35
+ "scripts": {
36
+ "example": "yarn workspace rn-selectable-text-example",
37
+ "test": "jest",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
41
+ "prepare": "bob build",
42
+ "release": "release-it --only-version"
43
+ },
44
+ "keywords": [
45
+ "react-native",
46
+ "ios",
47
+ "android"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/thiagobueno/rn-selectable-text.git"
52
+ },
53
+ "author": "Thiago Bueno (https://github.com/thiagobueno)",
54
+ "license": "MIT",
55
+ "bugs": {
56
+ "url": "https://github.com/thiagobueno/rn-selectable-text/issues"
57
+ },
58
+ "homepage": "https://github.com/thiagobueno/rn-selectable-text#readme",
59
+ "publishConfig": {
60
+ "registry": "https://registry.npmjs.org/"
61
+ },
62
+ "devDependencies": {
63
+ "@commitlint/config-conventional": "^19.8.1",
64
+ "@eslint/compat": "^1.3.2",
65
+ "@eslint/eslintrc": "^3.3.1",
66
+ "@eslint/js": "^9.35.0",
67
+ "@evilmartians/lefthook": "^1.12.3",
68
+ "@react-native-community/cli": "20.0.1",
69
+ "@react-native/babel-preset": "0.81.1",
70
+ "@react-native/eslint-config": "^0.81.1",
71
+ "@release-it/conventional-changelog": "^10.0.1",
72
+ "@types/jest": "^29.5.14",
73
+ "@types/react": "^19.1.0",
74
+ "commitlint": "^19.8.1",
75
+ "del-cli": "^6.0.0",
76
+ "eslint": "^9.35.0",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "eslint-plugin-prettier": "^5.5.4",
79
+ "jest": "^29.7.0",
80
+ "prettier": "^3.6.2",
81
+ "react": "19.1.0",
82
+ "react-native": "0.81.1",
83
+ "react-native-builder-bob": "^0.40.13",
84
+ "release-it": "^19.0.4",
85
+ "turbo": "^2.5.6",
86
+ "typescript": "^5.9.2"
87
+ },
88
+ "peerDependencies": {
89
+ "react": "*",
90
+ "react-native": "*"
91
+ },
92
+ "workspaces": [
93
+ "example"
94
+ ],
95
+ "packageManager": "yarn@3.6.1",
96
+ "jest": {
97
+ "preset": "react-native",
98
+ "modulePathIgnorePatterns": [
99
+ "<rootDir>/example/node_modules",
100
+ "<rootDir>/lib/"
101
+ ]
102
+ },
103
+ "commitlint": {
104
+ "extends": [
105
+ "@commitlint/config-conventional"
106
+ ]
107
+ },
108
+ "release-it": {
109
+ "git": {
110
+ "commitMessage": "chore: release ${version}",
111
+ "tagName": "v${version}"
112
+ },
113
+ "npm": {
114
+ "publish": true
115
+ },
116
+ "github": {
117
+ "release": true
118
+ },
119
+ "plugins": {
120
+ "@release-it/conventional-changelog": {
121
+ "preset": {
122
+ "name": "angular"
123
+ }
124
+ }
125
+ }
126
+ },
127
+ "prettier": {
128
+ "quoteProps": "consistent",
129
+ "singleQuote": true,
130
+ "tabWidth": 2,
131
+ "trailingComma": "es5",
132
+ "useTabs": false
133
+ },
134
+ "react-native-builder-bob": {
135
+ "source": "src",
136
+ "output": "lib",
137
+ "targets": [
138
+ [
139
+ "module",
140
+ {
141
+ "esm": true
142
+ }
143
+ ],
144
+ [
145
+ "typescript",
146
+ {
147
+ "project": "tsconfig.build.json"
148
+ }
149
+ ]
150
+ ]
151
+ },
152
+ "codegenConfig": {
153
+ "name": "SelectableTextViewSpec",
154
+ "type": "all",
155
+ "jsSrcsDir": "src",
156
+ "android": {
157
+ "javaPackageName": "com.selectabletext"
158
+ },
159
+ "ios": {
160
+ "componentProvider": {
161
+ "SelectableTextView": "SelectableTextView"
162
+ }
163
+ }
164
+ },
165
+ "create-react-native-library": {
166
+ "languages": "kotlin-objc",
167
+ "type": "fabric-view",
168
+ "version": "0.54.2"
169
+ }
170
+ }
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "rn-selectable-text"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/thiagobueno/rn-selectable-text.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+
20
+ install_modules_dependencies(s)
21
+ end
@@ -0,0 +1,67 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+ import type { ViewStyle, NativeSyntheticEvent } from 'react-native';
3
+ import { Platform, findNodeHandle, DeviceEventEmitter } from 'react-native';
4
+ import SelectableTextViewNativeComponent, {
5
+ type SelectionEvent,
6
+ } from './SelectableTextViewNativeComponent';
7
+
8
+ interface SelectableTextViewProps {
9
+ children: React.ReactNode;
10
+ menuOptions: string[];
11
+ onSelection?: (event: SelectionEvent) => void;
12
+ style?: ViewStyle;
13
+ }
14
+
15
+ export const SelectableTextView: React.FC<SelectableTextViewProps> = ({
16
+ children,
17
+ menuOptions,
18
+ onSelection,
19
+ style,
20
+ }) => {
21
+ const viewRef = useRef<any>(null);
22
+
23
+ useEffect(() => {
24
+ if (Platform.OS === 'android' && onSelection) {
25
+ const subscription = DeviceEventEmitter.addListener(
26
+ 'SelectableTextSelection',
27
+ (eventData: {
28
+ viewTag: number;
29
+ chosenOption: string;
30
+ highlightedText: string;
31
+ }) => {
32
+ const viewTag = findNodeHandle(viewRef.current);
33
+ if (viewTag === eventData.viewTag) {
34
+ onSelection({
35
+ chosenOption: eventData.chosenOption,
36
+ highlightedText: eventData.highlightedText,
37
+ });
38
+ }
39
+ }
40
+ );
41
+
42
+ return () => subscription.remove();
43
+ }
44
+ return () => { };
45
+ }, [onSelection]);
46
+
47
+ const handleSelection = (event: NativeSyntheticEvent<SelectionEvent>) => {
48
+ if (Platform.OS === 'ios' && onSelection) {
49
+ console.log(
50
+ 'SelectableTextView - Direct event received:',
51
+ event.nativeEvent
52
+ );
53
+ onSelection(event.nativeEvent);
54
+ }
55
+ };
56
+
57
+ return (
58
+ <SelectableTextViewNativeComponent
59
+ ref={viewRef}
60
+ style={style}
61
+ menuOptions={menuOptions}
62
+ onSelection={Platform.OS === 'ios' ? handleSelection : undefined}
63
+ >
64
+ {children}
65
+ </SelectableTextViewNativeComponent>
66
+ );
67
+ };
@@ -0,0 +1,14 @@
1
+ import { codegenNativeComponent, type ViewProps } from 'react-native';
2
+ import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypesNamespace';
3
+
4
+ export interface SelectionEvent {
5
+ chosenOption: string;
6
+ highlightedText: string;
7
+ }
8
+
9
+ interface NativeProps extends ViewProps {
10
+ menuOptions: readonly string[];
11
+ onSelection?: DirectEventHandler<SelectionEvent>;
12
+ }
13
+
14
+ export default codegenNativeComponent<NativeProps>('SelectableTextView');
package/src/index.tsx ADDED
@@ -0,0 +1 @@
1
+ export { SelectableTextView } from './SelectableTextView';