react-native-advanced-text 0.1.20 → 0.1.22

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,4 +1,3 @@
1
- // File: AdvancedTextView.kt
2
1
  package com.advancedtext
3
2
 
4
3
  import android.content.Context
@@ -34,7 +33,6 @@ class AdvancedTextView : TextView {
34
33
  private var currentText: String = ""
35
34
  private var textColor: String = "#000000"
36
35
 
37
- // Cache for word positions to avoid recalculating
38
36
  private var wordPositions: List<WordPosition> = emptyList()
39
37
 
40
38
  constructor(context: Context?) : super(context) { init() }
@@ -44,11 +42,11 @@ class AdvancedTextView : TextView {
44
42
  private fun init() {
45
43
  Log.d(TAG, "AdvancedTextView initialized")
46
44
 
47
- // Set default properties
48
45
  textSize = 16f
49
46
  setPadding(16, 16, 16, 16)
50
47
  movementMethod = LinkMovementMethod.getInstance()
51
48
  setTextIsSelectable(true)
49
+
52
50
 
53
51
  customSelectionActionModeCallback = object : ActionMode.Callback {
54
52
  override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
@@ -156,9 +154,7 @@ class AdvancedTextView : TextView {
156
154
 
157
155
  val spannableString = SpannableString(currentText)
158
156
 
159
- // Apply spans efficiently
160
157
  wordPositions.forEach { wordPos ->
161
- // Apply highlights
162
158
  highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
163
159
  val color = parseColor(highlightedWord.highlightColor)
164
160
  spannableString.setSpan(
@@ -169,7 +165,6 @@ class AdvancedTextView : TextView {
169
165
  )
170
166
  }
171
167
 
172
- // Apply indicator color
173
168
  if (wordPos.index == indicatorWordIndex) {
174
169
  spannableString.setSpan(
175
170
  ForegroundColorSpan(Color.parseColor(textColor)),
@@ -179,7 +174,6 @@ class AdvancedTextView : TextView {
179
174
  )
180
175
  }
181
176
 
182
- // Make words clickable
183
177
  spannableString.setSpan(
184
178
  WordClickableSpan(wordPos.index, wordPos.word),
185
179
  wordPos.start,
@@ -188,7 +182,6 @@ class AdvancedTextView : TextView {
188
182
  )
189
183
  }
190
184
 
191
- // Use post to ensure UI thread and avoid layout issues
192
185
  post {
193
186
  setText(spannableString, BufferType.SPANNABLE)
194
187
  Log.d(TAG, "Text updated with ${wordPositions.size} spans")
@@ -225,14 +218,11 @@ class AdvancedTextView : TextView {
225
218
  ) : ClickableSpan() {
226
219
 
227
220
  override fun onClick(widget: View) {
228
- Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
229
- val spannable = widget as? TextView
230
- spannable?.text?.let {
231
- if (it is android.text.Spannable) {
232
- Selection.removeSelection(it)
233
- }
221
+ Log.d(TAG, "WordClickableSpan onClick triggered: '$word' (index=$wordIndex)")
222
+
223
+ widget.post {
224
+ sendWordPressEvent(word, wordIndex)
234
225
  }
235
- sendWordPressEvent(word, wordIndex)
236
226
  }
237
227
 
238
228
  override fun updateDrawState(ds: TextPaint) {
@@ -1,14 +1,15 @@
1
+ #ifndef AdvancedTextView_h
2
+ #define AdvancedTextView_h
3
+
1
4
  #import <React/RCTViewComponentView.h>
2
5
  #import <UIKit/UIKit.h>
3
6
 
4
- #ifndef AdvancedTextViewNativeComponent_h
5
- #define AdvancedTextViewNativeComponent_h
6
-
7
7
  NS_ASSUME_NONNULL_BEGIN
8
8
 
9
9
  @interface AdvancedTextView : RCTViewComponentView
10
+
10
11
  @end
11
12
 
12
13
  NS_ASSUME_NONNULL_END
13
14
 
14
- #endif /* AdvancedTextViewNativeComponent_h */
15
+ #endif /* AdvancedTextView_h */
@@ -9,13 +9,17 @@
9
9
 
10
10
  using namespace facebook::react;
11
11
 
12
- @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol>
12
+ @interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate>
13
+
14
+ @property (nonatomic, strong) UITextView *textView;
15
+ @property (nonatomic, strong) NSMutableArray<NSDictionary *> *wordRanges;
16
+ @property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *highlightColors;
17
+ @property (nonatomic, strong) NSArray<NSString *> *menuOptions;
18
+ @property (nonatomic, assign) NSInteger indicatorWordIndex;
13
19
 
14
20
  @end
15
21
 
16
- @implementation AdvancedTextView {
17
- UIView * _view;
18
- }
22
+ @implementation AdvancedTextView
19
23
 
20
24
  + (ComponentDescriptorProvider)componentDescriptorProvider
21
25
  {
@@ -24,16 +28,49 @@ using namespace facebook::react;
24
28
 
25
29
  - (instancetype)initWithFrame:(CGRect)frame
26
30
  {
27
- if (self = [super initWithFrame:frame]) {
28
- static const auto defaultProps = std::make_shared<const AdvancedTextViewProps>();
29
- _props = defaultProps;
31
+ if (self = [super initWithFrame:frame]) {
32
+ static const auto defaultProps = std::make_shared<const AdvancedTextViewProps>();
33
+ _props = defaultProps;
34
+
35
+ _wordRanges = [NSMutableArray array];
36
+ _highlightColors = [NSMutableDictionary dictionary];
37
+ _indicatorWordIndex = -1;
38
+
39
+ [self setupTextView];
40
+ [self setupGestureRecognizers];
41
+ }
42
+
43
+ return self;
44
+ }
45
+
46
+ - (void)setupTextView
47
+ {
48
+ _textView = [[UITextView alloc] initWithFrame:self.bounds];
49
+ _textView.editable = NO;
50
+ _textView.scrollEnabled = YES;
51
+ _textView.backgroundColor = [UIColor clearColor];
52
+ _textView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
53
+ _textView.font = [UIFont systemFontOfSize:16];
54
+ _textView.textColor = [UIColor labelColor];
30
55
 
31
- _view = [[UIView alloc] init];
56
+ self.contentView = _textView;
57
+ }
32
58
 
33
- self.contentView = _view;
34
- }
59
+ - (void)setupGestureRecognizers
60
+ {
61
+ // Single tap for word selection
62
+ UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
63
+ initWithTarget:self
64
+ action:@selector(handleTap:)];
65
+ tapGesture.delegate = self;
66
+ [_textView addGestureRecognizer:tapGesture];
35
67
 
36
- return self;
68
+ // Long press for context menu
69
+ UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc]
70
+ initWithTarget:self
71
+ action:@selector(handleLongPress:)];
72
+ longPressGesture.delegate = self;
73
+ [_textView addGestureRecognizer:longPressGesture];
37
74
  }
38
75
 
39
76
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
@@ -41,26 +78,287 @@ using namespace facebook::react;
41
78
  const auto &oldViewProps = *std::static_pointer_cast<AdvancedTextViewProps const>(_props);
42
79
  const auto &newViewProps = *std::static_pointer_cast<AdvancedTextViewProps const>(props);
43
80
 
44
- if (oldViewProps.color != newViewProps.color) {
45
- NSString * colorToConvert = [[NSString alloc] initWithUTF8String: newViewProps.color.c_str()];
46
- [_view setBackgroundColor:[self hexStringToColor:colorToConvert]];
81
+ // Update text
82
+ if (oldViewProps.text != newViewProps.text) {
83
+ NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
84
+ [self updateTextContent:text];
85
+ }
86
+
87
+ // Update highlighted words
88
+ if (oldViewProps.highlightedWords != newViewProps.highlightedWords) {
89
+ [self updateHighlightedWords:newViewProps.highlightedWords];
90
+ }
91
+
92
+ // Update menu options
93
+ if (oldViewProps.menuOptions != newViewProps.menuOptions) {
94
+ [self updateMenuOptions:newViewProps.menuOptions];
95
+ }
96
+
97
+ // Update indicator word index
98
+ if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
99
+ _indicatorWordIndex = newViewProps.indicatorWordIndex;
100
+ [self updateTextAppearance];
47
101
  }
48
102
 
49
103
  [super updateProps:props oldProps:oldProps];
50
104
  }
51
105
 
52
- Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
106
+ - (void)updateTextContent:(NSString *)text
53
107
  {
54
- return AdvancedTextView.class;
108
+ // Parse text into words and their ranges
109
+ [_wordRanges removeAllObjects];
110
+
111
+ NSRange searchRange = NSMakeRange(0, text.length);
112
+ NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
113
+
114
+ NSInteger wordIndex = 0;
115
+ while (searchRange.location < text.length) {
116
+ // Skip whitespace
117
+ while (searchRange.location < text.length &&
118
+ [whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
119
+ searchRange.location++;
120
+ searchRange.length = text.length - searchRange.location;
121
+ }
122
+
123
+ if (searchRange.location >= text.length) break;
124
+
125
+ // Find word end
126
+ NSUInteger wordStart = searchRange.location;
127
+ while (searchRange.location < text.length &&
128
+ ![whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
129
+ searchRange.location++;
130
+ }
131
+
132
+ NSRange wordRange = NSMakeRange(wordStart, searchRange.location - wordStart);
133
+ NSString *word = [text substringWithRange:wordRange];
134
+
135
+ [_wordRanges addObject:@{
136
+ @"word": word,
137
+ @"range": [NSValue valueWithRange:wordRange],
138
+ @"index": @(wordIndex)
139
+ }];
140
+
141
+ wordIndex++;
142
+ searchRange.length = text.length - searchRange.location;
143
+ }
144
+
145
+ [self updateTextAppearance];
55
146
  }
56
147
 
57
- - hexStringToColor:(NSString *)stringToConvert
148
+ - (void)updateHighlightedWords:(const std::vector<AdvancedTextViewHighlightedWordsStruct> &)highlightedWords
58
149
  {
150
+ [_highlightColors removeAllObjects];
151
+
152
+ for (const auto &hw : highlightedWords) {
153
+ NSInteger index = hw.index;
154
+ NSString *colorString = [NSString stringWithUTF8String:hw.highlightColor.c_str()];
155
+ UIColor *color = [self hexStringToColor:colorString];
156
+
157
+ if (color) {
158
+ _highlightColors[@(index)] = color;
159
+ }
160
+ }
161
+
162
+ [self updateTextAppearance];
163
+ }
164
+
165
+ - (void)updateMenuOptions:(const std::vector<std::string> &)options
166
+ {
167
+ NSMutableArray *menuArray = [NSMutableArray array];
168
+ for (const auto &option : options) {
169
+ [menuArray addObject:[NSString stringWithUTF8String:option.c_str()]];
170
+ }
171
+ _menuOptions = [menuArray copy];
172
+ }
173
+
174
+ - (void)updateTextAppearance
175
+ {
176
+ if (_wordRanges.count == 0) return;
177
+
178
+ NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
179
+ initWithString:_textView.text];
180
+
181
+ // Apply default attributes
182
+ [attributedString addAttribute:NSFontAttributeName
183
+ value:[UIFont systemFontOfSize:16]
184
+ range:NSMakeRange(0, attributedString.length)];
185
+
186
+ [attributedString addAttribute:NSForegroundColorAttributeName
187
+ value:[UIColor labelColor]
188
+ range:NSMakeRange(0, attributedString.length)];
189
+
190
+ // Apply highlights
191
+ for (NSDictionary *wordInfo in _wordRanges) {
192
+ NSNumber *index = wordInfo[@"index"];
193
+ NSValue *rangeValue = wordInfo[@"range"];
194
+ NSRange range = [rangeValue rangeValue];
195
+
196
+ UIColor *highlightColor = _highlightColors[index];
197
+ if (highlightColor) {
198
+ [attributedString addAttribute:NSBackgroundColorAttributeName
199
+ value:highlightColor
200
+ range:range];
201
+ }
202
+
203
+ // Add indicator (underline or special formatting) for indicated word
204
+ if (_indicatorWordIndex >= 0 && [index integerValue] == _indicatorWordIndex) {
205
+ [attributedString addAttribute:NSUnderlineStyleAttributeName
206
+ value:@(NSUnderlineStyleSingle)
207
+ range:range];
208
+ [attributedString addAttribute:NSUnderlineColorAttributeName
209
+ value:[UIColor systemBlueColor]
210
+ range:range];
211
+ }
212
+ }
213
+
214
+ _textView.attributedText = attributedString;
215
+ }
216
+
217
+ - (void)handleTap:(UITapGestureRecognizer *)gesture
218
+ {
219
+ if (gesture.state != UIGestureRecognizerStateEnded) return;
220
+
221
+ CGPoint location = [gesture locationInView:_textView];
222
+ NSInteger wordIndex = [self wordIndexAtPoint:location];
223
+
224
+ if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
225
+ NSDictionary *wordInfo = _wordRanges[wordIndex];
226
+ NSString *word = wordInfo[@"word"];
227
+
228
+ [self emitWordPressEvent:word index:wordIndex];
229
+ }
230
+ }
231
+
232
+ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
233
+ {
234
+ if (gesture.state != UIGestureRecognizerStateBegan) return;
235
+
236
+ CGPoint location = [gesture locationInView:_textView];
237
+ NSInteger wordIndex = [self wordIndexAtPoint:location];
238
+
239
+ if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
240
+ NSDictionary *wordInfo = _wordRanges[wordIndex];
241
+ NSString *word = wordInfo[@"word"];
242
+
243
+ [self showContextMenuForWord:word atIndex:wordIndex location:location];
244
+ }
245
+ }
246
+
247
+ - (NSInteger)wordIndexAtPoint:(CGPoint)point
248
+ {
249
+ // Adjust point for text container insets
250
+ point.x -= _textView.textContainerInset.left;
251
+ point.y -= _textView.textContainerInset.top;
252
+
253
+ NSLayoutManager *layoutManager = _textView.layoutManager;
254
+ NSTextContainer *textContainer = _textView.textContainer;
255
+
256
+ NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
257
+ inTextContainer:textContainer
258
+ fractionOfDistanceBetweenInsertionPoints:nil];
259
+
260
+ // Find which word this character belongs to
261
+ for (NSDictionary *wordInfo in _wordRanges) {
262
+ NSValue *rangeValue = wordInfo[@"range"];
263
+ NSRange range = [rangeValue rangeValue];
264
+
265
+ if (NSLocationInRange(characterIndex, range)) {
266
+ return [wordInfo[@"index"] integerValue];
267
+ }
268
+ }
269
+
270
+ return -1;
271
+ }
272
+
273
+ - (void)showContextMenuForWord:(NSString *)word atIndex:(NSInteger)index location:(CGPoint)location
274
+ {
275
+ if (!_menuOptions || _menuOptions.count == 0) return;
276
+
277
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:word
278
+ message:nil
279
+ preferredStyle:UIAlertControllerStyleActionSheet];
280
+
281
+ for (NSString *option in _menuOptions) {
282
+ UIAlertAction *action = [UIAlertAction actionWithTitle:option
283
+ style:UIAlertActionStyleDefault
284
+ handler:^(UIAlertAction * _Nonnull action) {
285
+ [self emitSelectionEvent:word menuOption:option];
286
+ }];
287
+ [alert addAction:action];
288
+ }
289
+
290
+ UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"
291
+ style:UIAlertActionStyleCancel
292
+ handler:nil];
293
+ [alert addAction:cancelAction];
294
+
295
+ // Present from the root view controller
296
+ UIViewController *rootVC = [self findViewController];
297
+ if (rootVC) {
298
+ // For iPad, set up popover presentation
299
+ if (alert.popoverPresentationController) {
300
+ alert.popoverPresentationController.sourceView = _textView;
301
+ alert.popoverPresentationController.sourceRect = CGRectMake(location.x, location.y, 1, 1);
302
+ }
303
+
304
+ [rootVC presentViewController:alert animated:YES completion:nil];
305
+ }
306
+ }
307
+
308
+ - (UIViewController *)findViewController
309
+ {
310
+ UIResponder *responder = self;
311
+ while (responder) {
312
+ if ([responder isKindOfClass:[UIViewController class]]) {
313
+ return (UIViewController *)responder;
314
+ }
315
+ responder = [responder nextResponder];
316
+ }
317
+ return nil;
318
+ }
319
+
320
+ - (void)emitWordPressEvent:(NSString *)word index:(NSInteger)index
321
+ {
322
+ if (_eventEmitter) {
323
+ auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
324
+
325
+ AdvancedTextViewEventEmitter::OnWordPress event;
326
+ event.word = [word UTF8String];
327
+ event.index = static_cast<int>(index);
328
+
329
+ emitter->onWordPress(event);
330
+ }
331
+ }
332
+
333
+ - (void)emitSelectionEvent:(NSString *)selectedText menuOption:(NSString *)option
334
+ {
335
+ if (_eventEmitter) {
336
+ auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
337
+
338
+ AdvancedTextViewEventEmitter::OnSelection event;
339
+ event.selectedText = [selectedText UTF8String];
340
+ event.event = [option UTF8String];
341
+
342
+ emitter->onSelection(event);
343
+ }
344
+ }
345
+
346
+ - (void)layoutSubviews
347
+ {
348
+ [super layoutSubviews];
349
+ _textView.frame = self.bounds;
350
+ }
351
+
352
+ - (UIColor *)hexStringToColor:(NSString *)stringToConvert
353
+ {
354
+ if (!stringToConvert || [stringToConvert length] == 0) return nil;
355
+
59
356
  NSString *noHashString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""];
60
357
  NSScanner *stringScanner = [NSScanner scannerWithString:noHashString];
61
358
 
62
359
  unsigned hex;
63
360
  if (![stringScanner scanHexInt:&hex]) return nil;
361
+
64
362
  int r = (hex >> 16) & 0xFF;
65
363
  int g = (hex >> 8) & 0xFF;
66
364
  int b = (hex) & 0xFF;
@@ -68,4 +366,9 @@ Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
68
366
  return [UIColor colorWithRed:r / 255.0f green:g / 255.0f blue:b / 255.0f alpha:1.0f];
69
367
  }
70
368
 
369
+ Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
370
+ {
371
+ return AdvancedTextView.class;
372
+ }
373
+
71
374
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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",