react-native-advanced-text 0.1.25 → 0.1.27

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.
@@ -1,316 +1,637 @@
1
- package com.advancedtext
2
-
3
- import android.content.Context
4
- import android.graphics.Color
5
- import android.text.SpannableString
6
- import android.text.Spanned
7
- import android.text.TextPaint
8
- import android.text.method.LinkMovementMethod
9
- import android.text.style.ClickableSpan
10
- import android.text.style.BackgroundColorSpan
11
- import android.text.style.ForegroundColorSpan
12
- import android.util.AttributeSet
13
- import android.util.Log
14
- import android.view.ActionMode
15
- import android.view.Menu
16
- import android.view.MenuItem
17
- import android.view.View
18
- import android.widget.TextView
19
- import com.facebook.react.bridge.Arguments
20
- import com.facebook.react.bridge.ReactContext
21
- import com.facebook.react.uimanager.events.RCTEventEmitter
22
- import android.text.Selection
23
-
24
- class AdvancedTextView : TextView {
25
-
26
- private val TAG = "AdvancedTextView"
27
-
28
- private var highlightedWords: List<HighlightedWord> = emptyList()
29
- private var menuOptions: List<String> = emptyList()
30
- private var indicatorWordIndex: Int = -1
31
- private var lastSelectedText: String = ""
32
- private var customActionMode: ActionMode? = null
33
- private var currentText: String = ""
34
- private var textColor: String = "#000000"
35
- private var fontSize: Float = 16f
36
- private var fontWeight: String = "normal"
37
- private var textAlign: String = "left"
38
- private var fontFamily: String = "sans-serif"
39
-
40
- private var wordPositions: List<WordPosition> = emptyList()
41
-
42
- constructor(context: Context?) : super(context) { init() }
43
- constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
44
- constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
45
-
46
- private fun init() {
47
- Log.d(TAG, "AdvancedTextView initialized")
48
-
49
- textSize = 16f
50
- setPadding(16, 16, 16, 16)
51
- movementMethod = LinkMovementMethod.getInstance()
52
- setTextIsSelectable(true)
53
-
54
-
55
- customSelectionActionModeCallback = object : ActionMode.Callback {
56
- override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
57
- customActionMode = mode
58
- return true
59
- }
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;
26
+
27
+ - (void)handleCustomMenuAction:(UIMenuItem *)sender;
28
+
29
+ @end
30
+
31
+
32
+ @interface CustomTextView : UITextView
33
+ @property (nonatomic, weak) AdvancedTextView *parentView;
34
+ @end
35
+
36
+ @implementation CustomTextView
37
+
38
+ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
39
+ {
40
+ NSLog(@"[CustomTextView] canPerformAction: %@", NSStringFromSelector(action));
41
+
42
+ if (action == @selector(handleCustomMenuAction:)) {
43
+ NSLog(@"[CustomTextView] Allowing custom action");
44
+ return YES;
45
+ }
46
+
47
+ NSLog(@"[CustomTextView] ❌ Blocking system action: %@", NSStringFromSelector(action));
48
+ return NO;
49
+ }
50
+
51
+ - (void)handleCustomMenuAction:(UIMenuItem *)sender
52
+ {
53
+ if (self.parentView) {
54
+ [self.parentView handleCustomMenuAction:sender];
55
+ }
56
+ }
57
+
58
+ @end
59
+
60
+
60
61
 
61
- override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
62
- menu?.clear()
62
+ @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
63
63
 
64
- val selectionStart = selectionStart
65
- val selectionEnd = selectionEnd
64
+ @property (nonatomic, strong) CustomTextView *textView;
66
65
 
67
- if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
68
- lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
66
+ @end
69
67
 
70
- menuOptions.forEachIndexed { index, option ->
71
- menu?.add(0, index, index, option)
72
- }
68
+ @implementation AdvancedTextView
69
+
70
+ + (ComponentDescriptorProvider)componentDescriptorProvider
71
+ {
72
+ NSLog(@"[AdvancedTextView] componentDescriptorProvider called");
73
+ return concreteComponentDescriptorProvider<AdvancedTextViewComponentDescriptor>();
74
+ }
75
+
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;
100
+ }
101
+ }
73
102
 
74
- sendSelectionEvent(lastSelectedText, "selection")
75
- return true
103
+ return self;
104
+ }
105
+
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;
126
+ }
127
+ }
128
+
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;
143
+ }
144
+ }
145
+
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
+ }
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
+ }
189
+
190
+ if (oldViewProps.text != newViewProps.text) {
191
+ textChanged = YES;
192
+ NSLog(@"[AdvancedTextView] Text changed");
193
+ }
194
+
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;
76
204
  }
77
- return false
78
205
  }
206
+ }
79
207
 
80
- override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
81
- item?.let {
82
- val menuItemText = it.title.toString()
83
- sendSelectionEvent(lastSelectedText, menuItemText)
84
- mode?.finish()
85
- return true
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;
86
215
  }
87
- return false
88
216
  }
217
+ }
89
218
 
90
- override fun onDestroyActionMode(mode: ActionMode?) {
91
- customActionMode = null
92
- }
219
+ if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
220
+ indicatorChanged = YES;
93
221
  }
94
- }
95
222
 
223
+ if (textChanged) {
224
+ NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
225
+ [self updateTextContent:text];
226
+ }
96
227
 
228
+ if (highlightsChanged) {
229
+ [self updateHighlightedWords:newViewProps.highlightedWords];
230
+ }
97
231
 
98
- fun setAdvancedText(text: String) {
99
- if (currentText == text) {
100
- Log.d(TAG, "Text unchanged, skipping update")
101
- return
232
+ if (menuChanged) {
233
+ [self updateMenuOptions:newViewProps.menuOptions];
102
234
  }
103
235
 
104
- Log.d(TAG, "setAdvancedText: length=${text.length}")
105
- currentText = text
106
- calculateWordPositions(text)
107
- updateTextWithHighlights()
108
- }
236
+ if (indicatorChanged) {
237
+ _indicatorWordIndex = newViewProps.indicatorWordIndex;
238
+ [self updateTextAppearance];
239
+ }
109
240
 
110
- fun setAdvancedTextColor(colorInt: Int) {
111
- textColor = String.format("#%06X", 0xFFFFFF and colorInt)
112
- updateTextWithHighlights()
241
+ if (styleChanged) {
242
+ NSLog(@"[AdvancedTextView] Style properties changed, updating appearance");
243
+ [self updateTextAppearance];
244
+ }
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;
113
251
  }
252
+ }
114
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
+ }
115
263
 
116
- fun setAdvancedTextSize(size: Float) {
117
- if (fontSize == size) return
118
- fontSize = size
119
- updateTextWithHighlights() // ensures size change is applied with highlights
120
- }
264
+ _textView.text = text;
121
265
 
122
- fun setAdvancedFontWeight(weight: String) {
123
- if (fontWeight == weight) return
124
- fontWeight = weight
125
- }
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
+ }
126
278
 
127
- fun setAdvancedTextAlign(align: String) {
128
- if (textAlign == align) return
129
- textAlign = align
130
- when (align) {
131
- "left" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
132
- "center" -> textAlignment = View.TEXT_ALIGNMENT_CENTER
133
- "right" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_END
134
- else -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
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
+
296
+ wordIndex++;
297
+ searchRange.length = text.length - searchRange.location;
135
298
  }
136
- }
137
299
 
138
- fun setAdvancedFontFamily(family: String) {
139
- if (fontFamily == family) return
140
- fontFamily = family
141
- typeface = Typeface.create(family, Typeface.NORMAL)
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;
142
305
  }
306
+ }
143
307
 
144
- fun setMenuOptions(menuOptions: List<String>) {
145
- if (this.menuOptions == menuOptions) return
146
- this.menuOptions = menuOptions
147
- }
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
+
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];
148
319
 
149
- fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
150
- if (this.highlightedWords == highlightedWords) return
151
- this.highlightedWords = highlightedWords
152
- updateTextWithHighlights()
320
+ if (color) {
321
+ _highlightColors[@(index)] = color;
322
+ }
323
+ }
324
+
325
+ [self updateTextAppearance];
326
+ } @catch (NSException *exception) {
327
+ NSLog(@"[AdvancedTextView] Exception in updateHighlightedWords: %@", exception);
328
+ @throw;
153
329
  }
330
+ }
154
331
 
155
- fun setIndicatorWordIndex(index: Int) {
156
- if (this.indicatorWordIndex == index) return
157
- this.indicatorWordIndex = index
158
- updateTextWithHighlights()
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);
341
+ }
342
+ _menuOptions = [menuArray copy];
343
+ } @catch (NSException *exception) {
344
+ NSLog(@"[AdvancedTextView] Exception in updateMenuOptions: %@", exception);
345
+ @throw;
159
346
  }
347
+ }
160
348
 
161
- private fun calculateWordPositions(text: String) {
162
- if (text.isEmpty()) {
163
- wordPositions = emptyList()
164
- return
349
+ - (void)updateTextAppearance
350
+ {
351
+ @try {
352
+ if (!_textView.text || _textView.text.length == 0) {
353
+ return;
165
354
  }
166
355
 
167
- val positions = mutableListOf<WordPosition>()
168
- val regex = "\\S+".toRegex()
356
+ NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
357
+ initWithString:_textView.text];
169
358
 
170
- regex.findAll(text).forEachIndexed { index, match ->
171
- positions.add(WordPosition(
172
- index = index,
173
- start = match.range.first,
174
- end = match.range.last + 1,
175
- word = match.value
176
- ))
177
- }
178
359
 
179
- wordPositions = positions
180
- Log.d(TAG, "Calculated ${wordPositions.size} word positions")
181
- }
360
+ UIFont *font = nil;
182
361
 
183
- private fun updateTextWithHighlights() {
184
- if (currentText.isEmpty()) {
185
- Log.d(TAG, "No text available, skipping")
186
- return
362
+ if (_fontFamily && _fontFamily.length > 0) {
363
+ font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
187
364
  }
188
365
 
189
- val spannableString = SpannableString(currentText)
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
+ }
190
375
 
191
- wordPositions.forEach { wordPos ->
192
- highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
193
- val color = parseColor(highlightedWord.highlightColor)
194
- spannableString.setSpan(
195
- BackgroundColorSpan(color),
196
- wordPos.start,
197
- wordPos.end,
198
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
199
- )
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
+
387
+ for (NSDictionary *wordInfo in _wordRanges) {
388
+ NSNumber *index = wordInfo[@"index"];
389
+ NSValue *rangeValue = wordInfo[@"range"];
390
+ NSRange range = [rangeValue rangeValue];
391
+
392
+ if (range.location + range.length > attributedString.length) {
393
+ continue;
200
394
  }
201
395
 
202
- if (wordPos.index == indicatorWordIndex) {
203
- spannableString.setSpan(
204
- ForegroundColorSpan(Color.parseColor(textColor)),
205
- wordPos.start,
206
- wordPos.end,
207
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
208
- )
396
+ UIColor *highlightColor = _highlightColors[index];
397
+ if (highlightColor) {
398
+ [attributedString addAttribute:NSBackgroundColorAttributeName
399
+ value:highlightColor
400
+ range:range];
209
401
  }
210
402
 
211
- spannableString.setSpan(
212
- WordClickableSpan(wordPos.index, wordPos.word),
213
- wordPos.start,
214
- wordPos.end,
215
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
216
- )
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
+ }
217
409
  }
218
410
 
219
- textAlignment = when (textAlign) {
220
- "left" -> View.TEXT_ALIGNMENT_TEXT_START
221
- "center" -> View.TEXT_ALIGNMENT_CENTER
222
- "right" -> View.TEXT_ALIGNMENT_TEXT_END
223
- else -> View.TEXT_ALIGNMENT_TEXT_START
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;
224
423
  }
225
424
 
226
- setTextSize(fontSize)
425
+ } @catch (NSException *exception) {
426
+ NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
427
+ }
428
+ }
429
+
227
430
 
228
- typeface = when (fontWeight) {
229
- "bold" -> Typeface.create(fontFamily, Typeface.BOLD)
230
- "italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
231
- else -> Typeface.create(fontFamily, Typeface.NORMAL)
232
- }
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;
233
440
 
234
- post {
235
- setText(spannableString, BufferType.SPANNABLE)
236
- Log.d(TAG, "Text updated with ${wordPositions.size} spans")
441
+ if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
442
+ NSDictionary *wordInfo = _wordRanges[wordIndex];
443
+ NSString *word = wordInfo[@"word"];
444
+
445
+ [self emitWordPressEvent:word index:wordIndex];
237
446
  }
447
+ } @catch (NSException *exception) {
448
+ NSLog(@"[AdvancedTextView] Exception in handleTap: %@", exception);
238
449
  }
450
+ }
451
+
452
+ #pragma mark - UITextViewDelegate
453
+
454
+ - (void)textViewDidChangeSelection:(UITextView *)textView
455
+ {
456
+ NSString *selectedText = [textView.text substringWithRange:textView.selectedRange];
457
+
458
+ if (selectedText.length > 0) {
459
+ NSLog(@"[AdvancedTextView] Selected text: %@", selectedText);
239
460
 
240
- private fun parseColor(colorString: String): Int {
241
- return try {
242
- Color.parseColor(colorString)
243
- } catch (e: IllegalArgumentException) {
244
- Log.e(TAG, "Invalid color: $colorString, using yellow")
245
- Color.YELLOW
461
+ if (_menuOptions && _menuOptions.count > 0) {
462
+ [self setupCustomMenuItems];
246
463
  }
247
464
  }
465
+ }
248
466
 
249
- private fun sendSelectionEvent(selectedText: String, eventType: String) {
250
- try {
251
- val reactContext = context as? ReactContext ?: return
252
- val event = Arguments.createMap().apply {
253
- putString("selectedText", selectedText)
254
- putString("event", eventType)
255
- }
467
+ - (void)setupCustomMenuItems
468
+ {
469
+ NSLog(@"[AdvancedTextView] Setting up %lu custom menu items", (unsigned long)_menuOptions.count);
256
470
 
257
- reactContext.getJSModule(RCTEventEmitter::class.java)
258
- .receiveEvent(id, "onSelection", event)
259
- } catch (e: Exception) {
260
- Log.e(TAG, "Error sending selection event", e)
261
- }
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);
262
479
  }
263
480
 
264
- private inner class WordClickableSpan(
265
- private val wordIndex: Int,
266
- private val word: String
267
- ) : ClickableSpan() {
481
+ menuController.menuItems = customItems;
482
+ }
268
483
 
269
- override fun onClick(widget: View) {
270
- Log.d(TAG, "WordClickableSpan onClick triggered: '$word' (index=$wordIndex)")
271
484
 
272
- widget.post {
273
- sendWordPressEvent(word, wordIndex)
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
+
506
+ point.x -= _textView.textContainerInset.left;
507
+ point.y -= _textView.textContainerInset.top;
508
+
509
+ NSLayoutManager *layoutManager = _textView.layoutManager;
510
+ NSTextContainer *textContainer = _textView.textContainer;
511
+
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];
274
522
  }
275
523
  }
276
524
 
277
- override fun updateDrawState(ds: TextPaint) {
278
- super.updateDrawState(ds)
279
- ds.isUnderlineText = false
280
- ds.color = Color.parseColor(textColor)
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
+ }
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
+
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);
281
568
  }
569
+ } @catch (NSException *exception) {
570
+ NSLog(@"[AdvancedTextView] Exception in emitWordPressEvent: %@", exception);
282
571
  }
572
+ }
283
573
 
284
- private fun sendWordPressEvent(word: String, index: Int) {
285
- try {
286
- val reactContext = context as? ReactContext ?: return
287
- val event = Arguments.createMap().apply {
288
- putString("word", word)
289
- putInt("index", index)
290
- }
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);
291
580
 
292
- reactContext.getJSModule(RCTEventEmitter::class.java)
293
- .receiveEvent(id, "onWordPress", event)
294
- } catch (e: Exception) {
295
- Log.e(TAG, "Error sending word press event", e)
581
+ AdvancedTextViewEventEmitter::OnSelection event;
582
+ event.selectedText = [selectedText UTF8String];
583
+ event.event = [option UTF8String];
584
+
585
+ emitter->onSelection(event);
296
586
  }
587
+ } @catch (NSException *exception) {
588
+ NSLog(@"[AdvancedTextView] Exception in emitSelectionEvent: %@", exception);
589
+ }
590
+ }
591
+
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);
297
599
  }
600
+ }
298
601
 
299
- fun clearSelection() {
300
- (text as? android.text.Spannable)?.let {
301
- Selection.removeSelection(it)
602
+ - (UIColor *)hexStringToColor:(NSString *)stringToConvert
603
+ {
604
+ @try {
605
+ if (!stringToConvert || [stringToConvert length] == 0) {
606
+ return nil;
302
607
  }
608
+
609
+ NSString *noHashString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""];
610
+ NSScanner *stringScanner = [NSScanner scannerWithString:noHashString];
611
+
612
+ unsigned hex;
613
+ if (![stringScanner scanHexInt:&hex]) {
614
+ return nil;
615
+ }
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;
303
624
  }
625
+ }
626
+
627
+ Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
628
+ {
629
+ return AdvancedTextView.class;
630
+ }
304
631
 
305
- data class WordPosition(
306
- val index: Int,
307
- val start: Int,
308
- val end: Int,
309
- val word: String
310
- )
632
+ - (void)dealloc
633
+ {
634
+ NSLog(@"[AdvancedTextView] dealloc called");
311
635
  }
312
636
 
313
- data class HighlightedWord(
314
- val index: Int,
315
- val highlightColor: String
316
- )
637
+ @end
@@ -83,11 +83,11 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
83
83
  view?.setIndicatorWordIndex(if (index >= 0) index else -1)
84
84
  }
85
85
 
86
- @ReactProp(name = "color", customType = "Color")
87
- fun setColor(view: AdvancedTextView?, color: Int?) {
86
+ @ReactProp(name = "color")
87
+ fun setColor(view: AdvancedTextView?, color: String?) {
88
88
  android.util.Log.d(NAME, "setColor called with: $color")
89
89
  if (color != null) {
90
- view?.setAdvancedTextColor(color)
90
+ view?.setAdvancedTextColor(Color.parseColor(color))
91
91
  }
92
92
  }
93
93
 
@@ -1,26 +1,26 @@
1
- /* eslint-disable prettier/prettier */
2
- import type { ViewProps } from 'react-native';
3
- import { codegenNativeComponent } from 'react-native';
4
- // @ts-ignore
5
- import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
-
7
- interface HighlightedWord {
8
- index: Int32;
9
- highlightColor: string;
10
- }
11
-
12
- export interface NativeProps extends ViewProps {
13
- text: string;
14
- highlightedWords?: ReadonlyArray<HighlightedWord>;
15
- menuOptions?: ReadonlyArray<string>;
16
- onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
- onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
- indicatorWordIndex?: Int32;
19
- fontSize?: Int32;
20
- fontWeight?: string;
21
- color?: string;
22
- textAlign?: string;
23
- fontFamily?: string;
24
- }
25
-
26
- export default codegenNativeComponent<NativeProps>('AdvancedTextView');
1
+ /* eslint-disable prettier/prettier */
2
+ import type { ViewProps } from 'react-native';
3
+ import { codegenNativeComponent } from 'react-native';
4
+ // @ts-ignore
5
+ import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
+
7
+ interface HighlightedWord {
8
+ index: Int32;
9
+ highlightColor: string;
10
+ }
11
+
12
+ export interface NativeProps extends ViewProps {
13
+ text: string;
14
+ highlightedWords?: ReadonlyArray<HighlightedWord>;
15
+ menuOptions?: ReadonlyArray<string>;
16
+ onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
+ onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
+ indicatorWordIndex?: Int32;
19
+ fontSize?: Int32;
20
+ fontWeight?: string;
21
+ color?: string;
22
+ textAlign?: string;
23
+ fontFamily?: string;
24
+ }
25
+
26
+ export default codegenNativeComponent<NativeProps>('AdvancedTextView');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
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",
@@ -1,26 +1,26 @@
1
- /* eslint-disable prettier/prettier */
2
- import type { ViewProps } from 'react-native';
3
- import { codegenNativeComponent } from 'react-native';
4
- // @ts-ignore
5
- import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
-
7
- interface HighlightedWord {
8
- index: Int32;
9
- highlightColor: string;
10
- }
11
-
12
- export interface NativeProps extends ViewProps {
13
- text: string;
14
- highlightedWords?: ReadonlyArray<HighlightedWord>;
15
- menuOptions?: ReadonlyArray<string>;
16
- onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
- onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
- indicatorWordIndex?: Int32;
19
- fontSize?: Int32;
20
- fontWeight?: string;
21
- color?: string;
22
- textAlign?: string;
23
- fontFamily?: string;
24
- }
25
-
26
- export default codegenNativeComponent<NativeProps>('AdvancedTextView');
1
+ /* eslint-disable prettier/prettier */
2
+ import type { ViewProps } from 'react-native';
3
+ import { codegenNativeComponent } from 'react-native';
4
+ // @ts-ignore
5
+ import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
+
7
+ interface HighlightedWord {
8
+ index: Int32;
9
+ highlightColor: string;
10
+ }
11
+
12
+ export interface NativeProps extends ViewProps {
13
+ text: string;
14
+ highlightedWords?: ReadonlyArray<HighlightedWord>;
15
+ menuOptions?: ReadonlyArray<string>;
16
+ onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
+ onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
+ indicatorWordIndex?: Int32;
19
+ fontSize?: Int32;
20
+ fontWeight?: string;
21
+ color?: string;
22
+ textAlign?: string;
23
+ fontFamily?: string;
24
+ }
25
+
26
+ export default codegenNativeComponent<NativeProps>('AdvancedTextView');