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
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
menu?.clear()
|
|
62
|
+
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
val selectionEnd = selectionEnd
|
|
64
|
+
@property (nonatomic, strong) CustomTextView *textView;
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
66
|
+
@end
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
Log.d(TAG, "Text unchanged, skipping update")
|
|
102
|
-
return
|
|
232
|
+
if (menuChanged) {
|
|
233
|
+
[self updateMenuOptions:newViewProps.menuOptions];
|
|
103
234
|
}
|
|
104
235
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
236
|
+
if (indicatorChanged) {
|
|
237
|
+
_indicatorWordIndex = newViewProps.indicatorWordIndex;
|
|
238
|
+
[self updateTextAppearance];
|
|
239
|
+
}
|
|
110
240
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
if (fontSize == size) return
|
|
119
|
-
fontSize = size
|
|
120
|
-
updateTextWithHighlights()
|
|
121
|
-
}
|
|
264
|
+
_textView.text = text;
|
|
122
265
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
349
|
+
- (void)updateTextAppearance
|
|
350
|
+
{
|
|
351
|
+
@try {
|
|
352
|
+
if (!_textView.text || _textView.text.length == 0) {
|
|
353
|
+
return;
|
|
167
354
|
}
|
|
168
355
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
182
|
-
Log.d(TAG, "Calculated ${wordPositions.size} word positions")
|
|
183
|
-
}
|
|
360
|
+
UIFont *font = nil;
|
|
184
361
|
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
|
|
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
|
-
|
|
425
|
+
} @catch (NSException *exception) {
|
|
426
|
+
NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
229
430
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
val end: Int,
|
|
311
|
-
val word: String
|
|
312
|
-
)
|
|
632
|
+
- (void)dealloc
|
|
633
|
+
{
|
|
634
|
+
NSLog(@"[AdvancedTextView] dealloc called");
|
|
313
635
|
}
|
|
314
636
|
|
|
315
|
-
|
|
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,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');
|