react-native-advanced-text 0.1.27 → 0.1.29

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/README.md CHANGED
@@ -1,37 +1,56 @@
1
- # react-native-advanced-text
2
-
3
- Advanced text component for React Native with custom select options.
4
-
5
- ## Installation
6
-
7
-
8
- ```sh
9
- npm install react-native-advanced-text
10
- ```
11
-
12
-
13
- ## Usage
14
-
15
-
16
- ```js
17
- import { AdvancedTextView } from "react-native-advanced-text";
18
-
19
- // ...
20
-
21
- <AdvancedTextView color="tomato" />
22
- ```
23
-
24
-
25
- ## Contributing
26
-
27
- - [Development workflow](CONTRIBUTING.md#development-workflow)
28
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
29
- - [Code of conduct](CODE_OF_CONDUCT.md)
30
-
31
- ## License
32
-
33
- MIT
34
-
35
- ---
36
-
37
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
1
+ # react-native-advanced-text
2
+
3
+ react-native-advanced-text is a powerful cross-platform text component for React Native that enables word-level interaction, dynamic highlighting, and custom selection actions.
4
+
5
+ ## Installation
6
+
7
+
8
+ ```sh
9
+ npm install react-native-advanced-text
10
+ ```
11
+
12
+
13
+ ## Usage
14
+
15
+
16
+ ```js
17
+ import { AdvancedTextView } from "react-native-advanced-text";
18
+
19
+ <AdvancedText
20
+ text={'This is an example of AdvancedText component. Tap on any word to see the event in action.'}
21
+ style={[styles.AdvancedText, { minHeight }]}
22
+ indicatorWordIndex={2}
23
+ onWordPress={(event) => {
24
+ console.log({event})
25
+ }}
26
+ menuOptions={['Highlight', 'Copy', 'Translate']}
27
+ onSelection={(event) => {
28
+ console.log({event})
29
+ }}
30
+ highlightedWords={[
31
+ {
32
+ index: 4,
33
+ highlightColor: '#6baeffb5',
34
+ },
35
+ ]}
36
+ fontSize={24}
37
+ color={'#FFFFFF'}
38
+ fontWeight="normal"
39
+ fontFamily={'monospace'}
40
+ />
41
+ ```
42
+
43
+
44
+ ## Contributing
45
+
46
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
47
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
48
+ - [Code of conduct](CODE_OF_CONDUCT.md)
49
+
50
+ ## License
51
+
52
+ MIT
53
+
54
+ ---
55
+
56
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -1,637 +1,367 @@
1
- #import "AdvancedTextView.h"
2
-
3
- #import <react/renderer/components/AdvancedTextViewSpec/ComponentDescriptors.h>
4
- #import <react/renderer/components/AdvancedTextViewSpec/EventEmitters.h>
5
- #import <react/renderer/components/AdvancedTextViewSpec/Props.h>
6
- #import <react/renderer/components/AdvancedTextViewSpec/RCTComponentViewHelpers.h>
7
-
8
- #import "RCTFabricComponentsPlugins.h"
9
-
10
- using namespace facebook::react;
11
-
12
-
13
- @class AdvancedTextView;
14
-
15
- @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
16
-
17
- @property (nonatomic, strong) NSMutableArray<NSDictionary *> *wordRanges;
18
- @property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *highlightColors;
19
- @property (nonatomic, strong) NSArray<NSString *> *menuOptions;
20
- @property (nonatomic, assign) NSInteger indicatorWordIndex;
21
- @property (nonatomic, assign) CGFloat fontSize;
22
- @property (nonatomic, strong) NSString *fontWeight;
23
- @property (nonatomic, strong) UIColor *textColor;
24
- @property (nonatomic, strong) NSString *textAlign;
25
- @property (nonatomic, strong) NSString *fontFamily;
1
+ package com.advancedtext
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.Point
6
+ import android.text.SpannableString
7
+ import android.text.Spannable
8
+ import android.text.Spanned
9
+ import android.text.TextPaint
10
+ import android.text.method.ArrowKeyMovementMethod
11
+ import android.text.style.ClickableSpan
12
+ import android.text.style.BackgroundColorSpan
13
+ import android.text.style.ForegroundColorSpan
14
+ import android.util.AttributeSet
15
+ import android.util.Log
16
+ import android.view.ActionMode
17
+ import android.view.Menu
18
+ import android.view.MenuItem
19
+ import android.view.View
20
+ import android.view.MotionEvent
21
+ import android.widget.TextView
22
+ import com.facebook.react.bridge.Arguments
23
+ import com.facebook.react.bridge.ReactContext
24
+ import com.facebook.react.uimanager.events.RCTEventEmitter
25
+ import android.text.Selection
26
+ import android.graphics.Typeface
27
+ import androidx.core.text.getSpans
28
+
29
+ class AdvancedTextView : TextView {
30
+
31
+ private val TAG = "AdvancedTextView"
32
+
33
+ private var highlightedWords: List<HighlightedWord> = emptyList()
34
+ private var menuOptions: List<String> = emptyList()
35
+ private var indicatorWordIndex: Int = -1
36
+ private var lastSelectedText: String = ""
37
+ private var customActionMode: ActionMode? = null
38
+ private var currentText: String = ""
39
+ private var textColor: String = "#000000"
40
+ private var fontSize: Float = 16f
41
+ private var fontWeight: String = "normal"
42
+ private var textAlign: String = "left"
43
+ private var fontFamily: String = "sans-serif"
44
+
45
+ private var wordPositions: List<WordPosition> = emptyList()
46
+
47
+ constructor(context: Context?) : super(context) { init() }
48
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
49
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
50
+
51
+ private fun init() {
52
+ Log.d(TAG, "AdvancedTextView initialized")
53
+
54
+ textSize = 16f
55
+ setPadding(16, 16, 16, 16)
56
+ setTextIsSelectable(true)
57
+
58
+ movementMethod = SmartMovementMethod
59
+
60
+ customSelectionActionModeCallback = object : ActionMode.Callback {
61
+ override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
62
+ customActionMode = mode
63
+ return true
64
+ }
26
65
 
27
- - (void)handleCustomMenuAction:(UIMenuItem *)sender;
66
+ override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
67
+ menu?.clear()
28
68
 
29
- @end
69
+ val selectionStart = selectionStart
70
+ val selectionEnd = selectionEnd
30
71
 
72
+ if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
73
+ lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
31
74
 
32
- @interface CustomTextView : UITextView
33
- @property (nonatomic, weak) AdvancedTextView *parentView;
34
- @end
75
+ menuOptions.forEachIndexed { index, option ->
76
+ menu?.add(0, index, index, option)
77
+ }
35
78
 
36
- @implementation CustomTextView
79
+ sendSelectionEvent(lastSelectedText, "selection")
80
+ return true
81
+ }
82
+ return false
83
+ }
37
84
 
38
- - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
39
- {
40
- NSLog(@"[CustomTextView] canPerformAction: %@", NSStringFromSelector(action));
85
+ override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
86
+ item?.let {
87
+ val menuItemText = it.title.toString()
88
+ sendSelectionEvent(lastSelectedText, menuItemText)
89
+ mode?.finish()
90
+ return true
91
+ }
92
+ return false
93
+ }
41
94
 
42
- if (action == @selector(handleCustomMenuAction:)) {
43
- NSLog(@"[CustomTextView] Allowing custom action");
44
- return YES;
95
+ override fun onDestroyActionMode(mode: ActionMode?) {
96
+ customActionMode = null
97
+ }
98
+ }
45
99
  }
46
100
 
47
- NSLog(@"[CustomTextView] ❌ Blocking system action: %@", NSStringFromSelector(action));
48
- return NO;
49
- }
101
+ fun setAdvancedText(text: String) {
102
+ if (currentText == text) {
103
+ Log.d(TAG, "Text unchanged, skipping update")
104
+ return
105
+ }
50
106
 
51
- - (void)handleCustomMenuAction:(UIMenuItem *)sender
52
- {
53
- if (self.parentView) {
54
- [self.parentView handleCustomMenuAction:sender];
107
+ Log.d(TAG, "setAdvancedText: length=${text.length}")
108
+ currentText = text
109
+ calculateWordPositions(text)
110
+ updateTextWithHighlights()
55
111
  }
56
- }
57
-
58
- @end
59
-
60
112
 
113
+ fun setAdvancedTextColor(colorInt: Int) {
114
+ textColor = String.format("#%06X", 0xFFFFFF and colorInt)
115
+ updateTextWithHighlights()
116
+ }
61
117
 
62
- @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
63
-
64
- @property (nonatomic, strong) CustomTextView *textView;
65
-
66
- @end
67
-
68
- @implementation AdvancedTextView
118
+ fun setAdvancedTextSize(size: Float) {
119
+ if (fontSize == size) return
120
+ fontSize = size
121
+ updateTextWithHighlights()
122
+ }
69
123
 
70
- + (ComponentDescriptorProvider)componentDescriptorProvider
71
- {
72
- NSLog(@"[AdvancedTextView] componentDescriptorProvider called");
73
- return concreteComponentDescriptorProvider<AdvancedTextViewComponentDescriptor>();
74
- }
124
+ fun setAdvancedFontWeight(weight: String) {
125
+ if (fontWeight == weight) return
126
+ fontWeight = weight
127
+ updateTextWithHighlights()
128
+ }
75
129
 
76
- - (instancetype)initWithFrame:(CGRect)frame
77
- {
78
- NSLog(@"[AdvancedTextView] initWithFrame called");
79
- if (self = [super initWithFrame:frame]) {
80
- @try {
81
- static const auto defaultProps = std::make_shared<const AdvancedTextViewProps>();
82
- _props = defaultProps;
83
-
84
- _wordRanges = [NSMutableArray array];
85
- _highlightColors = [NSMutableDictionary dictionary];
86
- _indicatorWordIndex = -1;
87
- _fontSize = 16.0;
88
- _fontWeight = @"normal";
89
- _textColor = [UIColor labelColor];
90
- _textAlign = @"left";
91
- _fontFamily = @"System";
92
-
93
- [self setupTextView];
94
- [self setupGestureRecognizers];
95
-
96
- NSLog(@"[AdvancedTextView] Initialization successful");
97
- } @catch (NSException *exception) {
98
- NSLog(@"[AdvancedTextView] Exception in init: %@", exception);
99
- @throw;
130
+ fun setAdvancedTextAlign(align: String) {
131
+ if (textAlign == align) return
132
+ textAlign = align
133
+ when (align) {
134
+ "left" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
135
+ "center" -> textAlignment = View.TEXT_ALIGNMENT_CENTER
136
+ "right" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_END
137
+ else -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
100
138
  }
101
139
  }
102
140
 
103
- return self;
104
- }
141
+ fun setAdvancedFontFamily(family: String) {
142
+ if (fontFamily == family) return
143
+ fontFamily = family
144
+ typeface = Typeface.create(family, Typeface.NORMAL)
145
+ }
105
146
 
106
- - (void)setupTextView
107
- {
108
- NSLog(@"[AdvancedTextView] setupTextView called");
109
- @try {
110
- _textView = [[CustomTextView alloc] initWithFrame:self.bounds];
111
- _textView.parentView = self;
112
- _textView.editable = NO;
113
- _textView.selectable = YES;
114
- _textView.scrollEnabled = YES;
115
- _textView.backgroundColor = [UIColor clearColor];
116
- _textView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
117
- _textView.font = [UIFont systemFontOfSize:16];
118
- _textView.textColor = [UIColor labelColor];
119
- _textView.delegate = self;
120
-
121
- self.contentView = _textView;
122
- NSLog(@"[AdvancedTextView] TextView setup successful");
123
- } @catch (NSException *exception) {
124
- NSLog(@"[AdvancedTextView] Exception in setupTextView: %@", exception);
125
- @throw;
147
+ fun setMenuOptions(menuOptions: List<String>) {
148
+ if (this.menuOptions == menuOptions) return
149
+ this.menuOptions = menuOptions
126
150
  }
127
- }
128
151
 
129
- - (void)setupGestureRecognizers
130
- {
131
- NSLog(@"[AdvancedTextView] setupGestureRecognizers called");
132
- @try {
133
- UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
134
- initWithTarget:self
135
- action:@selector(handleTap:)];
136
- tapGesture.delegate = self;
137
- [_textView addGestureRecognizer:tapGesture];
138
-
139
- NSLog(@"[AdvancedTextView] Gesture recognizers setup successful");
140
- } @catch (NSException *exception) {
141
- NSLog(@"[AdvancedTextView] Exception in setupGestureRecognizers: %@", exception);
142
- @throw;
152
+ fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
153
+ if (this.highlightedWords == highlightedWords) return
154
+ this.highlightedWords = highlightedWords
155
+ updateTextWithHighlights()
143
156
  }
144
- }
145
157
 
146
- - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
147
- {
148
- NSLog(@"[AdvancedTextView] updateProps called");
149
- @try {
150
- const auto &oldViewProps = *std::static_pointer_cast<AdvancedTextViewProps const>(_props);
151
- const auto &newViewProps = *std::static_pointer_cast<AdvancedTextViewProps const>(props);
152
-
153
- BOOL textChanged = NO;
154
- BOOL highlightsChanged = NO;
155
- BOOL menuChanged = NO;
156
- BOOL indicatorChanged = NO;
157
- BOOL styleChanged = NO;
158
-
159
- if (oldViewProps.fontSize != newViewProps.fontSize && newViewProps.fontSize) {
160
- NSLog(@"[AdvancedTextView] Updating fontSize to: %f", newViewProps.fontSize);
161
- _fontSize = static_cast<CGFloat>(newViewProps.fontSize);
162
- styleChanged = YES;
163
- }
158
+ fun setIndicatorWordIndex(index: Int) {
159
+ if (this.indicatorWordIndex == index) return
160
+ this.indicatorWordIndex = index
161
+ updateTextWithHighlights()
162
+ }
164
163
 
165
- if (oldViewProps.textAlign != newViewProps.textAlign && !newViewProps.textAlign.empty()) {
166
- NSLog(@"[AdvancedTextView] Updating textAlign to: %s", newViewProps.textAlign.c_str());
167
- _textAlign = [NSString stringWithUTF8String:newViewProps.textAlign.c_str()];
168
- styleChanged = YES;
164
+ private fun calculateWordPositions(text: String) {
165
+ if (text.isEmpty()) {
166
+ wordPositions = emptyList()
167
+ return
169
168
  }
170
169
 
171
- if (oldViewProps.fontWeight != newViewProps.fontWeight && !newViewProps.fontWeight.empty()) {
172
- NSLog(@"[AdvancedTextView] Updating fontWeight to: %s", newViewProps.fontWeight.c_str());
173
- _fontWeight = [NSString stringWithUTF8String:newViewProps.fontWeight.c_str()];
174
- styleChanged = YES;
175
- }
170
+ val positions = mutableListOf<WordPosition>()
171
+ val regex = "\\S+".toRegex()
176
172
 
177
- if (oldViewProps.fontFamily != newViewProps.fontFamily && !newViewProps.fontFamily.empty()) {
178
- NSLog(@"[AdvancedTextView] Updating fontFamily to: %s", newViewProps.fontFamily.c_str());
179
- _fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
180
- styleChanged = YES;
173
+ regex.findAll(text).forEachIndexed { index, match ->
174
+ positions.add(WordPosition(
175
+ index = index,
176
+ start = match.range.first,
177
+ end = match.range.last + 1,
178
+ word = match.value
179
+ ))
181
180
  }
182
181
 
183
- if (oldViewProps.color != newViewProps.color && !newViewProps.color.empty()) {
184
- NSLog(@"[AdvancedTextView] Updating color to: %s", newViewProps.color.c_str());
185
- NSString *colorStr = [NSString stringWithUTF8String:newViewProps.color.c_str()];
186
- _textColor = [self hexStringToColor:colorStr];
187
- styleChanged = YES;
188
- }
182
+ wordPositions = positions
183
+ Log.d(TAG, "Calculated ${wordPositions.size} word positions")
184
+ }
189
185
 
190
- if (oldViewProps.text != newViewProps.text) {
191
- textChanged = YES;
192
- NSLog(@"[AdvancedTextView] Text changed");
186
+ private fun updateTextWithHighlights() {
187
+ if (currentText.isEmpty()) {
188
+ Log.d(TAG, "No text available, skipping")
189
+ return
193
190
  }
194
191
 
195
- if (oldViewProps.highlightedWords.size() != newViewProps.highlightedWords.size()) {
196
- highlightsChanged = YES;
197
- } else {
198
- for (size_t i = 0; i < oldViewProps.highlightedWords.size(); i++) {
199
- const auto &oldHW = oldViewProps.highlightedWords[i];
200
- const auto &newHW = newViewProps.highlightedWords[i];
201
- if (oldHW.index != newHW.index || oldHW.highlightColor != newHW.highlightColor) {
202
- highlightsChanged = YES;
203
- break;
204
- }
205
- }
206
- }
192
+ val spannableString = SpannableString(currentText)
207
193
 
208
- if (oldViewProps.menuOptions.size() != newViewProps.menuOptions.size()) {
209
- menuChanged = YES;
210
- } else {
211
- for (size_t i = 0; i < oldViewProps.menuOptions.size(); i++) {
212
- if (oldViewProps.menuOptions[i] != newViewProps.menuOptions[i]) {
213
- menuChanged = YES;
214
- break;
215
- }
194
+ wordPositions.forEach { wordPos ->
195
+ highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
196
+ val color = parseColor(highlightedWord.highlightColor)
197
+ spannableString.setSpan(
198
+ BackgroundColorSpan(color),
199
+ wordPos.start,
200
+ wordPos.end,
201
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
202
+ )
216
203
  }
217
- }
218
204
 
219
- if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
220
- indicatorChanged = YES;
221
- }
205
+ if (wordPos.index == indicatorWordIndex) {
206
+ spannableString.setSpan(
207
+ ForegroundColorSpan(Color.parseColor(textColor)),
208
+ wordPos.start,
209
+ wordPos.end,
210
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
211
+ )
212
+ }
222
213
 
223
- if (textChanged) {
224
- NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
225
- [self updateTextContent:text];
214
+ // Add clickable span for word clicks
215
+ spannableString.setSpan(
216
+ WordClickableSpan(wordPos.index, wordPos.word),
217
+ wordPos.start,
218
+ wordPos.end,
219
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
220
+ )
226
221
  }
227
222
 
228
- if (highlightsChanged) {
229
- [self updateHighlightedWords:newViewProps.highlightedWords];
223
+ textAlignment = when (textAlign) {
224
+ "left" -> View.TEXT_ALIGNMENT_TEXT_START
225
+ "center" -> View.TEXT_ALIGNMENT_CENTER
226
+ "right" -> View.TEXT_ALIGNMENT_TEXT_END
227
+ else -> View.TEXT_ALIGNMENT_TEXT_START
230
228
  }
231
229
 
232
- if (menuChanged) {
233
- [self updateMenuOptions:newViewProps.menuOptions];
234
- }
230
+ setTextSize(fontSize)
235
231
 
236
- if (indicatorChanged) {
237
- _indicatorWordIndex = newViewProps.indicatorWordIndex;
238
- [self updateTextAppearance];
232
+ typeface = when (fontWeight) {
233
+ "bold" -> Typeface.create(fontFamily, Typeface.BOLD)
234
+ "italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
235
+ else -> Typeface.create(fontFamily, Typeface.NORMAL)
239
236
  }
240
237
 
241
- if (styleChanged) {
242
- NSLog(@"[AdvancedTextView] Style properties changed, updating appearance");
243
- [self updateTextAppearance];
238
+ post {
239
+ setText(spannableString, BufferType.SPANNABLE)
240
+ Log.d(TAG, "Text updated with ${wordPositions.size} spans")
244
241
  }
245
-
246
- [super updateProps:props oldProps:oldProps];
247
- NSLog(@"[AdvancedTextView] updateProps completed successfully");
248
- } @catch (NSException *exception) {
249
- NSLog(@"[AdvancedTextView] Exception in updateProps: %@", exception.reason);
250
- @throw;
251
242
  }
252
- }
253
-
254
- - (void)updateTextContent:(NSString *)text
255
- {
256
- NSLog(@"[AdvancedTextView] updateTextContent called with text length: %lu",
257
- (unsigned long)text.length);
258
- @try {
259
- if (!text) {
260
- NSLog(@"[AdvancedTextView] Text is nil, skipping update");
261
- return;
262
- }
263
-
264
- _textView.text = text;
265
-
266
- [_wordRanges removeAllObjects];
267
-
268
- NSRange searchRange = NSMakeRange(0, text.length);
269
- NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
270
-
271
- NSInteger wordIndex = 0;
272
- while (searchRange.location < text.length) {
273
- while (searchRange.location < text.length &&
274
- [whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
275
- searchRange.location++;
276
- searchRange.length = text.length - searchRange.location;
277
- }
278
-
279
- if (searchRange.location >= text.length) break;
280
-
281
- NSUInteger wordStart = searchRange.location;
282
- while (searchRange.location < text.length &&
283
- ![whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
284
- searchRange.location++;
285
- }
286
-
287
- NSRange wordRange = NSMakeRange(wordStart, searchRange.location - wordStart);
288
- NSString *word = [text substringWithRange:wordRange];
289
-
290
- [_wordRanges addObject:@{
291
- @"word": word,
292
- @"range": [NSValue valueWithRange:wordRange],
293
- @"index": @(wordIndex)
294
- }];
295
243
 
296
- wordIndex++;
297
- searchRange.length = text.length - searchRange.location;
244
+ private fun parseColor(colorString: String): Int {
245
+ return try {
246
+ Color.parseColor(colorString)
247
+ } catch (e: IllegalArgumentException) {
248
+ Log.e(TAG, "Invalid color: $colorString, using yellow")
249
+ Color.YELLOW
298
250
  }
299
-
300
- NSLog(@"[AdvancedTextView] Parsed %ld words", (long)_wordRanges.count);
301
- [self updateTextAppearance];
302
- } @catch (NSException *exception) {
303
- NSLog(@"[AdvancedTextView] Exception in updateTextContent: %@", exception);
304
- @throw;
305
251
  }
306
- }
307
-
308
- - (void)updateHighlightedWords:(const std::vector<AdvancedTextViewHighlightedWordsStruct> &)highlightedWords
309
- {
310
- NSLog(@"[AdvancedTextView] updateHighlightedWords called with %zu highlights",
311
- highlightedWords.size());
312
- @try {
313
- [_highlightColors removeAllObjects];
314
252
 
315
- for (const auto &hw : highlightedWords) {
316
- NSInteger index = hw.index;
317
- NSString *colorString = [NSString stringWithUTF8String:hw.highlightColor.c_str()];
318
- UIColor *color = [self hexStringToColor:colorString];
319
-
320
- if (color) {
321
- _highlightColors[@(index)] = color;
253
+ private fun sendSelectionEvent(selectedText: String, eventType: String) {
254
+ try {
255
+ val reactContext = context as? ReactContext ?: return
256
+ val event = Arguments.createMap().apply {
257
+ putString("selectedText", selectedText)
258
+ putString("event", eventType)
322
259
  }
323
- }
324
260
 
325
- [self updateTextAppearance];
326
- } @catch (NSException *exception) {
327
- NSLog(@"[AdvancedTextView] Exception in updateHighlightedWords: %@", exception);
328
- @throw;
329
- }
330
- }
331
-
332
- - (void)updateMenuOptions:(const std::vector<std::string> &)options
333
- {
334
- NSLog(@"[AdvancedTextView] updateMenuOptions called with %zu options", options.size());
335
- @try {
336
- NSMutableArray *menuArray = [NSMutableArray array];
337
- for (const auto &option : options) {
338
- NSString *optionStr = [NSString stringWithUTF8String:option.c_str()];
339
- [menuArray addObject:optionStr];
340
- NSLog(@"[AdvancedTextView] Added menu option: %@", optionStr);
261
+ reactContext.getJSModule(RCTEventEmitter::class.java)
262
+ .receiveEvent(id, "onSelection", event)
263
+ } catch (e: Exception) {
264
+ Log.e(TAG, "Error sending selection event", e)
341
265
  }
342
- _menuOptions = [menuArray copy];
343
- } @catch (NSException *exception) {
344
- NSLog(@"[AdvancedTextView] Exception in updateMenuOptions: %@", exception);
345
- @throw;
346
266
  }
347
- }
348
-
349
- - (void)updateTextAppearance
350
- {
351
- @try {
352
- if (!_textView.text || _textView.text.length == 0) {
353
- return;
354
- }
355
-
356
- NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
357
- initWithString:_textView.text];
358
-
359
-
360
- UIFont *font = nil;
361
-
362
- if (_fontFamily && _fontFamily.length > 0) {
363
- font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
364
- }
365
-
366
- if (!font) {
367
- if (_fontWeight && [_fontWeight.lowercaseString isEqualToString:@"bold"]) {
368
- font = [UIFont boldSystemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
369
- } else if (_fontWeight && [_fontWeight.lowercaseString isEqualToString:@"italic"]) {
370
- font = [UIFont italicSystemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
371
- } else {
372
- font = [UIFont systemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
373
- }
374
- }
375
-
376
- [attributedString addAttribute:NSFontAttributeName
377
- value:font
378
- range:NSMakeRange(0, attributedString.length)];
379
-
380
-
381
- UIColor *color = _textColor ?: [UIColor labelColor];
382
- [attributedString addAttribute:NSForegroundColorAttributeName
383
- value:color
384
- range:NSMakeRange(0, attributedString.length)];
385
-
386
267
 
387
- for (NSDictionary *wordInfo in _wordRanges) {
388
- NSNumber *index = wordInfo[@"index"];
389
- NSValue *rangeValue = wordInfo[@"range"];
390
- NSRange range = [rangeValue rangeValue];
268
+ private inner class WordClickableSpan(
269
+ private val wordIndex: Int,
270
+ private val word: String
271
+ ) : ClickableSpan() {
391
272
 
392
- if (range.location + range.length > attributedString.length) {
393
- continue;
394
- }
395
-
396
- UIColor *highlightColor = _highlightColors[index];
397
- if (highlightColor) {
398
- [attributedString addAttribute:NSBackgroundColorAttributeName
399
- value:highlightColor
400
- range:range];
401
- }
402
-
403
- if (_indicatorWordIndex >= 0 && [index integerValue] == _indicatorWordIndex) {
404
- UIColor *indicatorColor = [[UIColor systemBlueColor] colorWithAlphaComponent:0.3];
405
- [attributedString addAttribute:NSBackgroundColorAttributeName
406
- value:indicatorColor
407
- range:range];
408
- }
273
+ override fun onClick(widget: View) {
274
+ Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
275
+ sendWordPressEvent(word, wordIndex)
409
276
  }
410
277
 
411
- _textView.attributedText = attributedString;
412
-
413
- if (_textAlign) {
414
- if ([_textAlign.lowercaseString isEqualToString:@"center"]) {
415
- _textView.textAlignment = NSTextAlignmentCenter;
416
- } else if ([_textAlign.lowercaseString isEqualToString:@"right"]) {
417
- _textView.textAlignment = NSTextAlignmentRight;
418
- } else {
419
- _textView.textAlignment = NSTextAlignmentLeft;
420
- }
421
- } else {
422
- _textView.textAlignment = NSTextAlignmentLeft;
278
+ override fun updateDrawState(ds: TextPaint) {
279
+ super.updateDrawState(ds)
280
+ ds.isUnderlineText = false
281
+ ds.color = Color.parseColor(textColor)
423
282
  }
424
-
425
- } @catch (NSException *exception) {
426
- NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
427
283
  }
428
- }
429
-
430
284
 
431
- - (void)handleTap:(UITapGestureRecognizer *)gesture
432
- {
433
- @try {
434
- if (gesture.state != UIGestureRecognizerStateEnded) return;
435
-
436
- CGPoint location = [gesture locationInView:_textView];
437
- NSInteger wordIndex = [self wordIndexAtPoint:location];
438
-
439
- _textView.selectedTextRange = nil;
440
-
441
- if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
442
- NSDictionary *wordInfo = _wordRanges[wordIndex];
443
- NSString *word = wordInfo[@"word"];
285
+ private fun sendWordPressEvent(word: String, index: Int) {
286
+ try {
287
+ val reactContext = context as? ReactContext ?: return
288
+ val event = Arguments.createMap().apply {
289
+ putString("word", word)
290
+ putInt("index", index)
291
+ }
444
292
 
445
- [self emitWordPressEvent:word index:wordIndex];
293
+ reactContext.getJSModule(RCTEventEmitter::class.java)
294
+ .receiveEvent(id, "onWordPress", event)
295
+ } catch (e: Exception) {
296
+ Log.e(TAG, "Error sending word press event", e)
446
297
  }
447
- } @catch (NSException *exception) {
448
- NSLog(@"[AdvancedTextView] Exception in handleTap: %@", exception);
449
298
  }
450
- }
451
-
452
- #pragma mark - UITextViewDelegate
453
-
454
- - (void)textViewDidChangeSelection:(UITextView *)textView
455
- {
456
- NSString *selectedText = [textView.text substringWithRange:textView.selectedRange];
457
299
 
458
- if (selectedText.length > 0) {
459
- NSLog(@"[AdvancedTextView] Selected text: %@", selectedText);
460
-
461
- if (_menuOptions && _menuOptions.count > 0) {
462
- [self setupCustomMenuItems];
300
+ fun clearSelection() {
301
+ (text as? android.text.Spannable)?.let {
302
+ Selection.removeSelection(it)
463
303
  }
464
304
  }
465
- }
466
-
467
- - (void)setupCustomMenuItems
468
- {
469
- NSLog(@"[AdvancedTextView] Setting up %lu custom menu items", (unsigned long)_menuOptions.count);
470
-
471
- UIMenuController *menuController = [UIMenuController sharedMenuController];
472
- NSMutableArray *customItems = [NSMutableArray array];
473
-
474
- for (NSString *option in _menuOptions) {
475
- UIMenuItem *item = [[UIMenuItem alloc] initWithTitle:option
476
- action:@selector(handleCustomMenuAction:)];
477
- [customItems addObject:item];
478
- NSLog(@"[AdvancedTextView] Created menu item: %@", option);
479
- }
480
-
481
- menuController.menuItems = customItems;
482
- }
483
-
484
-
485
- - (void)handleCustomMenuAction:(UIMenuItem *)sender
486
- {
487
- NSLog(@"[AdvancedTextView] Custom menu action: %@", sender.title);
488
-
489
- NSString *selectedText = [_textView.text substringWithRange:_textView.selectedRange];
490
-
491
- [self emitSelectionEvent:selectedText menuOption:sender.title];
492
-
493
- dispatch_async(dispatch_get_main_queue(), ^{
494
- self->_textView.selectedTextRange = nil;
495
- NSLog(@"[AdvancedTextView] Selection cleared");
496
- });
497
- }
498
-
499
- - (NSInteger)wordIndexAtPoint:(CGPoint)point
500
- {
501
- @try {
502
- if (!_textView.layoutManager || !_textView.textContainer) {
503
- return -1;
504
- }
505
305
 
506
- point.x -= _textView.textContainerInset.left;
507
- point.y -= _textView.textContainerInset.top;
508
306
 
509
- NSLayoutManager *layoutManager = _textView.layoutManager;
510
- NSTextContainer *textContainer = _textView.textContainer;
307
+ private object SmartMovementMethod : ArrowKeyMovementMethod() {
511
308
 
512
- NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
513
- inTextContainer:textContainer
514
- fractionOfDistanceBetweenInsertionPoints:nil];
515
-
516
- for (NSDictionary *wordInfo in _wordRanges) {
517
- NSValue *rangeValue = wordInfo[@"range"];
518
- NSRange range = [rangeValue rangeValue];
519
-
520
- if (NSLocationInRange(characterIndex, range)) {
521
- return [wordInfo[@"index"] integerValue];
309
+ override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
310
+ if (event != null && widget != null && buffer != null) {
311
+ if (handleMotion(event, widget, buffer)) {
312
+ return true
313
+ }
522
314
  }
315
+ return super.onTouchEvent(widget, buffer, event)
523
316
  }
524
317
 
525
- return -1;
526
- } @catch (NSException *exception) {
527
- return -1;
528
- }
529
- }
530
-
531
- - (void)setFontSize:(CGFloat)fontSize {
532
- _fontSize = fontSize;
533
- [self updateTextAppearance];
534
- }
318
+ private fun handleMotion(event: MotionEvent, widget: TextView, buffer: Spannable): Boolean {
319
+ var handled = false
535
320
 
536
- - (void)setFontWeight:(NSString *)fontWeight {
537
- _fontWeight = fontWeight;
538
- [self updateTextAppearance];
539
- }
540
-
541
- - (void)setTextColor:(UIColor *)textColor {
542
- _textColor = textColor;
543
- [self updateTextAppearance];
544
- }
545
-
546
- - (void)setTextAlign:(NSString *)textAlign {
547
- _textAlign = textAlign;
548
- [self updateTextAppearance];
549
- }
550
-
551
- - (void)setFontFamily:(NSString *)fontFamily {
552
- _fontFamily = fontFamily;
553
- [self updateTextAppearance];
554
- }
555
-
556
- - (void)emitWordPressEvent:(NSString *)word index:(NSInteger)index
557
- {
558
- NSLog(@"[AdvancedTextView] emitWordPressEvent: %@ at index: %ld", word, (long)index);
559
- @try {
560
- if (_eventEmitter) {
561
- auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
562
-
563
- AdvancedTextViewEventEmitter::OnWordPress event;
564
- event.word = [word UTF8String];
565
- event.index = static_cast<int>(index);
566
-
567
- emitter->onWordPress(event);
568
- }
569
- } @catch (NSException *exception) {
570
- NSLog(@"[AdvancedTextView] Exception in emitWordPressEvent: %@", exception);
571
- }
572
- }
573
-
574
- - (void)emitSelectionEvent:(NSString *)selectedText menuOption:(NSString *)option
575
- {
576
- NSLog(@"[AdvancedTextView] emitSelectionEvent: %@ with option: %@", selectedText, option);
577
- @try {
578
- if (_eventEmitter) {
579
- auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
321
+ if (event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
322
+ val target = Point().apply {
323
+ x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX
324
+ y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY
325
+ }
580
326
 
581
- AdvancedTextViewEventEmitter::OnSelection event;
582
- event.selectedText = [selectedText UTF8String];
583
- event.event = [option UTF8String];
327
+ val line = widget.layout.getLineForVertical(target.y)
328
+ val offset = widget.layout.getOffsetForHorizontal(line, target.x.toFloat())
584
329
 
585
- emitter->onSelection(event);
586
- }
587
- } @catch (NSException *exception) {
588
- NSLog(@"[AdvancedTextView] Exception in emitSelectionEvent: %@", exception);
589
- }
590
- }
330
+ if (event.action == MotionEvent.ACTION_DOWN) {
331
+ handled = handled || buffer.execute<ClickableSpan>(offset) {
332
+ Selection.setSelection(buffer, buffer.getSpanStart(it), buffer.getSpanEnd(it))
333
+ }
334
+ }
591
335
 
592
- - (void)layoutSubviews
593
- {
594
- @try {
595
- [super layoutSubviews];
596
- _textView.frame = self.bounds;
597
- } @catch (NSException *exception) {
598
- NSLog(@"[AdvancedTextView] Exception in layoutSubviews: %@", exception);
599
- }
600
- }
336
+ if (event.action == MotionEvent.ACTION_UP) {
337
+ handled = handled || buffer.execute<ClickableSpan>(offset) {
338
+ it.onClick(widget)
339
+ }
340
+ }
341
+ }
601
342
 
602
- - (UIColor *)hexStringToColor:(NSString *)stringToConvert
603
- {
604
- @try {
605
- if (!stringToConvert || [stringToConvert length] == 0) {
606
- return nil;
343
+ return handled
607
344
  }
608
345
 
609
- NSString *noHashString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""];
610
- NSScanner *stringScanner = [NSScanner scannerWithString:noHashString];
611
-
612
- unsigned hex;
613
- if (![stringScanner scanHexInt:&hex]) {
614
- return nil;
346
+ private inline fun <reified T : Any> Spannable.execute(offset: Int, fn: (T) -> Unit): Boolean {
347
+ val spans = this.getSpans<T>(offset, offset)
348
+ if (spans.isNotEmpty()) {
349
+ spans.forEach(fn)
350
+ return true
351
+ }
352
+ return false
615
353
  }
616
-
617
- int r = (hex >> 16) & 0xFF;
618
- int g = (hex >> 8) & 0xFF;
619
- int b = (hex) & 0xFF;
620
-
621
- return [UIColor colorWithRed:r / 255.0f green:g / 255.0f blue:b / 255.0f alpha:1.0f];
622
- } @catch (NSException *exception) {
623
- return nil;
624
354
  }
625
- }
626
-
627
- Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
628
- {
629
- return AdvancedTextView.class;
630
- }
631
355
 
632
- - (void)dealloc
633
- {
634
- NSLog(@"[AdvancedTextView] dealloc called");
356
+ data class WordPosition(
357
+ val index: Int,
358
+ val start: Int,
359
+ val end: Int,
360
+ val word: String
361
+ )
635
362
  }
636
363
 
637
- @end
364
+ data class HighlightedWord(
365
+ val index: Int,
366
+ val highlightColor: String
367
+ )
@@ -10,7 +10,6 @@
10
10
  using namespace facebook::react;
11
11
 
12
12
 
13
- // Forward declaration
14
13
  @class AdvancedTextView;
15
14
 
16
15
  @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
@@ -19,14 +18,17 @@ using namespace facebook::react;
19
18
  @property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *highlightColors;
20
19
  @property (nonatomic, strong) NSArray<NSString *> *menuOptions;
21
20
  @property (nonatomic, assign) NSInteger indicatorWordIndex;
21
+ @property (nonatomic, assign) CGFloat fontSize;
22
+ @property (nonatomic, strong) NSString *fontWeight;
23
+ @property (nonatomic, strong) UIColor *textColor;
24
+ @property (nonatomic, strong) NSString *textAlign;
25
+ @property (nonatomic, strong) NSString *fontFamily;
22
26
 
23
- // ✅ ADD THIS LINE
24
27
  - (void)handleCustomMenuAction:(UIMenuItem *)sender;
25
28
 
26
29
  @end
27
30
 
28
31
 
29
- // Custom UITextView subclass to override menu behavior
30
32
  @interface CustomTextView : UITextView
31
33
  @property (nonatomic, weak) AdvancedTextView *parentView;
32
34
  @end
@@ -37,22 +39,18 @@ using namespace facebook::react;
37
39
  {
38
40
  NSLog(@"[CustomTextView] canPerformAction: %@", NSStringFromSelector(action));
39
41
 
40
- // Only allow our custom menu actions
41
42
  if (action == @selector(handleCustomMenuAction:)) {
42
43
  NSLog(@"[CustomTextView] ✅ Allowing custom action");
43
44
  return YES;
44
45
  }
45
46
 
46
- // Block ALL system actions
47
47
  NSLog(@"[CustomTextView] ❌ Blocking system action: %@", NSStringFromSelector(action));
48
48
  return NO;
49
49
  }
50
50
 
51
51
  - (void)handleCustomMenuAction:(UIMenuItem *)sender
52
52
  {
53
- // Forward to parent view
54
53
  if (self.parentView) {
55
-
56
54
  [self.parentView handleCustomMenuAction:sender];
57
55
  }
58
56
  }
@@ -86,6 +84,11 @@ using namespace facebook::react;
86
84
  _wordRanges = [NSMutableArray array];
87
85
  _highlightColors = [NSMutableDictionary dictionary];
88
86
  _indicatorWordIndex = -1;
87
+ _fontSize = 16.0;
88
+ _fontWeight = @"normal";
89
+ _textColor = [UIColor labelColor];
90
+ _textAlign = @"left";
91
+ _fontFamily = @"System";
89
92
 
90
93
  [self setupTextView];
91
94
  [self setupGestureRecognizers];
@@ -127,7 +130,6 @@ using namespace facebook::react;
127
130
  {
128
131
  NSLog(@"[AdvancedTextView] setupGestureRecognizers called");
129
132
  @try {
130
- // Single tap for word selection
131
133
  UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
132
134
  initWithTarget:self
133
135
  action:@selector(handleTap:)];
@@ -152,14 +154,44 @@ using namespace facebook::react;
152
154
  BOOL highlightsChanged = NO;
153
155
  BOOL menuChanged = NO;
154
156
  BOOL indicatorChanged = NO;
157
+ BOOL styleChanged = NO;
158
+
159
+ if (oldViewProps.fontSize != newViewProps.fontSize && newViewProps.fontSize) {
160
+ NSLog(@"[AdvancedTextView] Updating fontSize to: %f", newViewProps.fontSize);
161
+ _fontSize = static_cast<CGFloat>(newViewProps.fontSize);
162
+ styleChanged = YES;
163
+ }
164
+
165
+ if (oldViewProps.textAlign != newViewProps.textAlign && !newViewProps.textAlign.empty()) {
166
+ NSLog(@"[AdvancedTextView] Updating textAlign to: %s", newViewProps.textAlign.c_str());
167
+ _textAlign = [NSString stringWithUTF8String:newViewProps.textAlign.c_str()];
168
+ styleChanged = YES;
169
+ }
170
+
171
+ if (oldViewProps.fontWeight != newViewProps.fontWeight && !newViewProps.fontWeight.empty()) {
172
+ NSLog(@"[AdvancedTextView] Updating fontWeight to: %s", newViewProps.fontWeight.c_str());
173
+ _fontWeight = [NSString stringWithUTF8String:newViewProps.fontWeight.c_str()];
174
+ styleChanged = YES;
175
+ }
176
+
177
+ if (oldViewProps.fontFamily != newViewProps.fontFamily && !newViewProps.fontFamily.empty()) {
178
+ NSLog(@"[AdvancedTextView] Updating fontFamily to: %s", newViewProps.fontFamily.c_str());
179
+ _fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
180
+ styleChanged = YES;
181
+ }
182
+
183
+ if (oldViewProps.color != newViewProps.color && !newViewProps.color.empty()) {
184
+ NSLog(@"[AdvancedTextView] Updating color to: %s", newViewProps.color.c_str());
185
+ NSString *colorStr = [NSString stringWithUTF8String:newViewProps.color.c_str()];
186
+ _textColor = [self hexStringToColor:colorStr];
187
+ styleChanged = YES;
188
+ }
155
189
 
156
- // Check text change
157
190
  if (oldViewProps.text != newViewProps.text) {
158
191
  textChanged = YES;
159
192
  NSLog(@"[AdvancedTextView] Text changed");
160
193
  }
161
194
 
162
- // Check highlighted words change
163
195
  if (oldViewProps.highlightedWords.size() != newViewProps.highlightedWords.size()) {
164
196
  highlightsChanged = YES;
165
197
  } else {
@@ -173,7 +205,6 @@ using namespace facebook::react;
173
205
  }
174
206
  }
175
207
 
176
- // Check menu options change
177
208
  if (oldViewProps.menuOptions.size() != newViewProps.menuOptions.size()) {
178
209
  menuChanged = YES;
179
210
  } else {
@@ -185,12 +216,10 @@ using namespace facebook::react;
185
216
  }
186
217
  }
187
218
 
188
- // Check indicator change
189
219
  if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
190
220
  indicatorChanged = YES;
191
221
  }
192
222
 
193
- // Apply updates
194
223
  if (textChanged) {
195
224
  NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
196
225
  [self updateTextContent:text];
@@ -209,6 +238,11 @@ using namespace facebook::react;
209
238
  [self updateTextAppearance];
210
239
  }
211
240
 
241
+ if (styleChanged) {
242
+ NSLog(@"[AdvancedTextView] Style properties changed, updating appearance");
243
+ [self updateTextAppearance];
244
+ }
245
+
212
246
  [super updateProps:props oldProps:oldProps];
213
247
  NSLog(@"[AdvancedTextView] updateProps completed successfully");
214
248
  } @catch (NSException *exception) {
@@ -229,7 +263,6 @@ using namespace facebook::react;
229
263
 
230
264
  _textView.text = text;
231
265
 
232
- // Parse text into words and their ranges
233
266
  [_wordRanges removeAllObjects];
234
267
 
235
268
  NSRange searchRange = NSMakeRange(0, text.length);
@@ -237,7 +270,6 @@ using namespace facebook::react;
237
270
 
238
271
  NSInteger wordIndex = 0;
239
272
  while (searchRange.location < text.length) {
240
- // Skip whitespace
241
273
  while (searchRange.location < text.length &&
242
274
  [whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
243
275
  searchRange.location++;
@@ -246,7 +278,6 @@ using namespace facebook::react;
246
278
 
247
279
  if (searchRange.location >= text.length) break;
248
280
 
249
- // Find word end
250
281
  NSUInteger wordStart = searchRange.location;
251
282
  while (searchRange.location < text.length &&
252
283
  ![whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
@@ -318,21 +349,41 @@ using namespace facebook::react;
318
349
  - (void)updateTextAppearance
319
350
  {
320
351
  @try {
321
- if (_wordRanges.count == 0 || !_textView.text || _textView.text.length == 0) {
352
+ if (!_textView.text || _textView.text.length == 0) {
322
353
  return;
323
354
  }
324
355
 
325
356
  NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
326
357
  initWithString:_textView.text];
327
358
 
359
+
360
+ UIFont *font = nil;
361
+
362
+ if (_fontFamily && _fontFamily.length > 0) {
363
+ font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
364
+ }
365
+
366
+ if (!font) {
367
+ if (_fontWeight && [_fontWeight.lowercaseString isEqualToString:@"bold"]) {
368
+ font = [UIFont boldSystemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
369
+ } else if (_fontWeight && [_fontWeight.lowercaseString isEqualToString:@"italic"]) {
370
+ font = [UIFont italicSystemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
371
+ } else {
372
+ font = [UIFont systemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
373
+ }
374
+ }
375
+
328
376
  [attributedString addAttribute:NSFontAttributeName
329
- value:[UIFont systemFontOfSize:16]
377
+ value:font
330
378
  range:NSMakeRange(0, attributedString.length)];
331
379
 
380
+
381
+ UIColor *color = _textColor ?: [UIColor labelColor];
332
382
  [attributedString addAttribute:NSForegroundColorAttributeName
333
- value:[UIColor labelColor]
383
+ value:color
334
384
  range:NSMakeRange(0, attributedString.length)];
335
385
 
386
+
336
387
  for (NSDictionary *wordInfo in _wordRanges) {
337
388
  NSNumber *index = wordInfo[@"index"];
338
389
  NSValue *rangeValue = wordInfo[@"range"];
@@ -358,11 +409,25 @@ using namespace facebook::react;
358
409
  }
359
410
 
360
411
  _textView.attributedText = attributedString;
412
+
413
+ if (_textAlign) {
414
+ if ([_textAlign.lowercaseString isEqualToString:@"center"]) {
415
+ _textView.textAlignment = NSTextAlignmentCenter;
416
+ } else if ([_textAlign.lowercaseString isEqualToString:@"right"]) {
417
+ _textView.textAlignment = NSTextAlignmentRight;
418
+ } else {
419
+ _textView.textAlignment = NSTextAlignmentLeft;
420
+ }
421
+ } else {
422
+ _textView.textAlignment = NSTextAlignmentLeft;
423
+ }
424
+
361
425
  } @catch (NSException *exception) {
362
426
  NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
363
427
  }
364
428
  }
365
429
 
430
+
366
431
  - (void)handleTap:(UITapGestureRecognizer *)gesture
367
432
  {
368
433
  @try {
@@ -371,7 +436,6 @@ using namespace facebook::react;
371
436
  CGPoint location = [gesture locationInView:_textView];
372
437
  NSInteger wordIndex = [self wordIndexAtPoint:location];
373
438
 
374
- // Dismiss any existing selection
375
439
  _textView.selectedTextRange = nil;
376
440
 
377
441
  if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
@@ -464,6 +528,31 @@ using namespace facebook::react;
464
528
  }
465
529
  }
466
530
 
531
+ - (void)setFontSize:(CGFloat)fontSize {
532
+ _fontSize = fontSize;
533
+ [self updateTextAppearance];
534
+ }
535
+
536
+ - (void)setFontWeight:(NSString *)fontWeight {
537
+ _fontWeight = fontWeight;
538
+ [self updateTextAppearance];
539
+ }
540
+
541
+ - (void)setTextColor:(UIColor *)textColor {
542
+ _textColor = textColor;
543
+ [self updateTextAppearance];
544
+ }
545
+
546
+ - (void)setTextAlign:(NSString *)textAlign {
547
+ _textAlign = textAlign;
548
+ [self updateTextAppearance];
549
+ }
550
+
551
+ - (void)setFontFamily:(NSString *)fontFamily {
552
+ _fontFamily = fontFamily;
553
+ [self updateTextAppearance];
554
+ }
555
+
467
556
  - (void)emitWordPressEvent:(NSString *)word index:(NSInteger)index
468
557
  {
469
558
  NSLog(@"[AdvancedTextView] emitWordPressEvent: %@ at index: %ld", word, (long)index);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": " Advanced text component for React Native with custom select options.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",