react-native-advanced-text 0.1.26 → 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,318 +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
- import android.graphics.Typeface
24
-
25
- class AdvancedTextView : TextView {
26
-
27
- private val TAG = "AdvancedTextView"
28
-
29
- private var highlightedWords: List<HighlightedWord> = emptyList()
30
- private var menuOptions: List<String> = emptyList()
31
- private var indicatorWordIndex: Int = -1
32
- private var lastSelectedText: String = ""
33
- private var customActionMode: ActionMode? = null
34
- private var currentText: String = ""
35
- private var textColor: String = "#000000"
36
- private var fontSize: Float = 16f
37
- private var fontWeight: String = "normal"
38
- private var textAlign: String = "left"
39
- private var fontFamily: String = "sans-serif"
40
-
41
- private var wordPositions: List<WordPosition> = emptyList()
42
-
43
- constructor(context: Context?) : super(context) { init() }
44
- constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
45
- constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
46
-
47
- private fun init() {
48
- Log.d(TAG, "AdvancedTextView initialized")
49
-
50
- textSize = 16f
51
- setPadding(16, 16, 16, 16)
52
- movementMethod = LinkMovementMethod.getInstance()
53
- setTextIsSelectable(true)
54
-
55
-
56
- customSelectionActionModeCallback = object : ActionMode.Callback {
57
- override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
58
- customActionMode = mode
59
- return true
60
- }
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
+
61
61
 
62
- override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
63
- menu?.clear()
62
+ @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
64
63
 
65
- val selectionStart = selectionStart
66
- val selectionEnd = selectionEnd
64
+ @property (nonatomic, strong) CustomTextView *textView;
67
65
 
68
- if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
69
- lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
66
+ @end
70
67
 
71
- menuOptions.forEachIndexed { index, option ->
72
- menu?.add(0, index, index, option)
73
- }
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
+ }
74
102
 
75
- sendSelectionEvent(lastSelectedText, "selection")
76
- 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;
77
204
  }
78
- return false
79
205
  }
206
+ }
80
207
 
81
- override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
82
- item?.let {
83
- val menuItemText = it.title.toString()
84
- sendSelectionEvent(lastSelectedText, menuItemText)
85
- mode?.finish()
86
- 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;
87
215
  }
88
- return false
89
216
  }
217
+ }
90
218
 
91
- override fun onDestroyActionMode(mode: ActionMode?) {
92
- customActionMode = null
93
- }
219
+ if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
220
+ indicatorChanged = YES;
94
221
  }
95
- }
96
222
 
223
+ if (textChanged) {
224
+ NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
225
+ [self updateTextContent:text];
226
+ }
97
227
 
228
+ if (highlightsChanged) {
229
+ [self updateHighlightedWords:newViewProps.highlightedWords];
230
+ }
98
231
 
99
- fun setAdvancedText(text: String) {
100
- if (currentText == text) {
101
- Log.d(TAG, "Text unchanged, skipping update")
102
- return
232
+ if (menuChanged) {
233
+ [self updateMenuOptions:newViewProps.menuOptions];
103
234
  }
104
235
 
105
- Log.d(TAG, "setAdvancedText: length=${text.length}")
106
- currentText = text
107
- calculateWordPositions(text)
108
- updateTextWithHighlights()
109
- }
236
+ if (indicatorChanged) {
237
+ _indicatorWordIndex = newViewProps.indicatorWordIndex;
238
+ [self updateTextAppearance];
239
+ }
110
240
 
111
- fun setAdvancedTextColor(colorInt: Int) {
112
- textColor = String.format("#%06X", 0xFFFFFF and colorInt)
113
- 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;
114
251
  }
252
+ }
115
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
+ }
116
263
 
117
- fun setAdvancedTextSize(size: Float) {
118
- if (fontSize == size) return
119
- fontSize = size
120
- updateTextWithHighlights()
121
- }
264
+ _textView.text = text;
122
265
 
123
- fun setAdvancedFontWeight(weight: String) {
124
- if (fontWeight == weight) return
125
- fontWeight = weight
126
- updateTextWithHighlights()
127
- }
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
+ }
128
278
 
129
- fun setAdvancedTextAlign(align: String) {
130
- if (textAlign == align) return
131
- textAlign = align
132
- when (align) {
133
- "left" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
134
- "center" -> textAlignment = View.TEXT_ALIGNMENT_CENTER
135
- "right" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_END
136
- 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;
137
298
  }
138
- }
139
299
 
140
- fun setAdvancedFontFamily(family: String) {
141
- if (fontFamily == family) return
142
- fontFamily = family
143
- 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;
144
305
  }
306
+ }
145
307
 
146
- fun setMenuOptions(menuOptions: List<String>) {
147
- if (this.menuOptions == menuOptions) return
148
- this.menuOptions = menuOptions
149
- }
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];
150
319
 
151
- fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
152
- if (this.highlightedWords == highlightedWords) return
153
- this.highlightedWords = highlightedWords
154
- 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;
155
329
  }
330
+ }
156
331
 
157
- fun setIndicatorWordIndex(index: Int) {
158
- if (this.indicatorWordIndex == index) return
159
- this.indicatorWordIndex = index
160
- 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;
161
346
  }
347
+ }
162
348
 
163
- private fun calculateWordPositions(text: String) {
164
- if (text.isEmpty()) {
165
- wordPositions = emptyList()
166
- return
349
+ - (void)updateTextAppearance
350
+ {
351
+ @try {
352
+ if (!_textView.text || _textView.text.length == 0) {
353
+ return;
167
354
  }
168
355
 
169
- val positions = mutableListOf<WordPosition>()
170
- val regex = "\\S+".toRegex()
356
+ NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
357
+ initWithString:_textView.text];
171
358
 
172
- regex.findAll(text).forEachIndexed { index, match ->
173
- positions.add(WordPosition(
174
- index = index,
175
- start = match.range.first,
176
- end = match.range.last + 1,
177
- word = match.value
178
- ))
179
- }
180
359
 
181
- wordPositions = positions
182
- Log.d(TAG, "Calculated ${wordPositions.size} word positions")
183
- }
360
+ UIFont *font = nil;
184
361
 
185
- private fun updateTextWithHighlights() {
186
- if (currentText.isEmpty()) {
187
- Log.d(TAG, "No text available, skipping")
188
- return
362
+ if (_fontFamily && _fontFamily.length > 0) {
363
+ font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
189
364
  }
190
365
 
191
- 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
+ }
192
375
 
193
- wordPositions.forEach { wordPos ->
194
- highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
195
- val color = parseColor(highlightedWord.highlightColor)
196
- spannableString.setSpan(
197
- BackgroundColorSpan(color),
198
- wordPos.start,
199
- wordPos.end,
200
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
201
- )
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;
202
394
  }
203
395
 
204
- if (wordPos.index == indicatorWordIndex) {
205
- spannableString.setSpan(
206
- ForegroundColorSpan(Color.parseColor(textColor)),
207
- wordPos.start,
208
- wordPos.end,
209
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
210
- )
396
+ UIColor *highlightColor = _highlightColors[index];
397
+ if (highlightColor) {
398
+ [attributedString addAttribute:NSBackgroundColorAttributeName
399
+ value:highlightColor
400
+ range:range];
211
401
  }
212
402
 
213
- spannableString.setSpan(
214
- WordClickableSpan(wordPos.index, wordPos.word),
215
- wordPos.start,
216
- wordPos.end,
217
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
218
- )
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
+ }
219
409
  }
220
410
 
221
- textAlignment = when (textAlign) {
222
- "left" -> View.TEXT_ALIGNMENT_TEXT_START
223
- "center" -> View.TEXT_ALIGNMENT_CENTER
224
- "right" -> View.TEXT_ALIGNMENT_TEXT_END
225
- 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;
226
423
  }
227
424
 
228
- setTextSize(fontSize)
425
+ } @catch (NSException *exception) {
426
+ NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
427
+ }
428
+ }
429
+
229
430
 
230
- typeface = when (fontWeight) {
231
- "bold" -> Typeface.create(fontFamily, Typeface.BOLD)
232
- "italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
233
- else -> Typeface.create(fontFamily, Typeface.NORMAL)
234
- }
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;
235
440
 
236
- post {
237
- setText(spannableString, BufferType.SPANNABLE)
238
- 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];
239
446
  }
447
+ } @catch (NSException *exception) {
448
+ NSLog(@"[AdvancedTextView] Exception in handleTap: %@", exception);
240
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);
241
460
 
242
- private fun parseColor(colorString: String): Int {
243
- return try {
244
- Color.parseColor(colorString)
245
- } catch (e: IllegalArgumentException) {
246
- Log.e(TAG, "Invalid color: $colorString, using yellow")
247
- Color.YELLOW
461
+ if (_menuOptions && _menuOptions.count > 0) {
462
+ [self setupCustomMenuItems];
248
463
  }
249
464
  }
465
+ }
250
466
 
251
- private fun sendSelectionEvent(selectedText: String, eventType: String) {
252
- try {
253
- val reactContext = context as? ReactContext ?: return
254
- val event = Arguments.createMap().apply {
255
- putString("selectedText", selectedText)
256
- putString("event", eventType)
257
- }
467
+ - (void)setupCustomMenuItems
468
+ {
469
+ NSLog(@"[AdvancedTextView] Setting up %lu custom menu items", (unsigned long)_menuOptions.count);
258
470
 
259
- reactContext.getJSModule(RCTEventEmitter::class.java)
260
- .receiveEvent(id, "onSelection", event)
261
- } catch (e: Exception) {
262
- Log.e(TAG, "Error sending selection event", e)
263
- }
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);
264
479
  }
265
480
 
266
- private inner class WordClickableSpan(
267
- private val wordIndex: Int,
268
- private val word: String
269
- ) : ClickableSpan() {
481
+ menuController.menuItems = customItems;
482
+ }
270
483
 
271
- override fun onClick(widget: View) {
272
- Log.d(TAG, "WordClickableSpan onClick triggered: '$word' (index=$wordIndex)")
273
484
 
274
- widget.post {
275
- 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];
276
522
  }
277
523
  }
278
524
 
279
- override fun updateDrawState(ds: TextPaint) {
280
- super.updateDrawState(ds)
281
- ds.isUnderlineText = false
282
- 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);
283
568
  }
569
+ } @catch (NSException *exception) {
570
+ NSLog(@"[AdvancedTextView] Exception in emitWordPressEvent: %@", exception);
284
571
  }
572
+ }
285
573
 
286
- private fun sendWordPressEvent(word: String, index: Int) {
287
- try {
288
- val reactContext = context as? ReactContext ?: return
289
- val event = Arguments.createMap().apply {
290
- putString("word", word)
291
- putInt("index", index)
292
- }
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);
293
580
 
294
- reactContext.getJSModule(RCTEventEmitter::class.java)
295
- .receiveEvent(id, "onWordPress", event)
296
- } catch (e: Exception) {
297
- 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);
298
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);
299
599
  }
600
+ }
300
601
 
301
- fun clearSelection() {
302
- (text as? android.text.Spannable)?.let {
303
- Selection.removeSelection(it)
602
+ - (UIColor *)hexStringToColor:(NSString *)stringToConvert
603
+ {
604
+ @try {
605
+ if (!stringToConvert || [stringToConvert length] == 0) {
606
+ return nil;
304
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;
305
624
  }
625
+ }
626
+
627
+ Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
628
+ {
629
+ return AdvancedTextView.class;
630
+ }
306
631
 
307
- data class WordPosition(
308
- val index: Int,
309
- val start: Int,
310
- val end: Int,
311
- val word: String
312
- )
632
+ - (void)dealloc
633
+ {
634
+ NSLog(@"[AdvancedTextView] dealloc called");
313
635
  }
314
636
 
315
- data class HighlightedWord(
316
- val index: Int,
317
- val highlightColor: String
318
- )
637
+ @end
@@ -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.26",
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');