react-native-advanced-text 0.1.21 → 0.1.23
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() }
|
|
@@ -42,51 +40,52 @@ class AdvancedTextView : TextView {
|
|
|
42
40
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
|
|
43
41
|
|
|
44
42
|
private fun init() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
43
|
+
Log.d(TAG, "AdvancedTextView initialized")
|
|
44
|
+
|
|
45
|
+
textSize = 16f
|
|
46
|
+
setPadding(16, 16, 16, 16)
|
|
47
|
+
movementMethod = LinkMovementMethod.getInstance()
|
|
48
|
+
setTextIsSelectable(true)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
customSelectionActionModeCallback = object : ActionMode.Callback {
|
|
52
|
+
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
53
|
+
customActionMode = mode
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
58
|
+
menu?.clear()
|
|
59
|
+
|
|
60
|
+
val selectionStart = selectionStart
|
|
61
|
+
val selectionEnd = selectionEnd
|
|
62
|
+
|
|
63
|
+
if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
|
|
64
|
+
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
65
|
+
|
|
66
|
+
menuOptions.forEachIndexed { index, option ->
|
|
67
|
+
menu?.add(0, index, index, option)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sendSelectionEvent(lastSelectedText, "selection")
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
|
77
|
+
item?.let {
|
|
78
|
+
val menuItemText = it.title.toString()
|
|
79
|
+
sendSelectionEvent(lastSelectedText, menuItemText)
|
|
80
|
+
mode?.finish()
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
87
|
+
customActionMode = null
|
|
88
|
+
}
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -155,9 +154,7 @@ class AdvancedTextView : TextView {
|
|
|
155
154
|
|
|
156
155
|
val spannableString = SpannableString(currentText)
|
|
157
156
|
|
|
158
|
-
// Apply spans efficiently
|
|
159
157
|
wordPositions.forEach { wordPos ->
|
|
160
|
-
// Apply highlights
|
|
161
158
|
highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
|
|
162
159
|
val color = parseColor(highlightedWord.highlightColor)
|
|
163
160
|
spannableString.setSpan(
|
|
@@ -168,7 +165,6 @@ class AdvancedTextView : TextView {
|
|
|
168
165
|
)
|
|
169
166
|
}
|
|
170
167
|
|
|
171
|
-
// Apply indicator color
|
|
172
168
|
if (wordPos.index == indicatorWordIndex) {
|
|
173
169
|
spannableString.setSpan(
|
|
174
170
|
ForegroundColorSpan(Color.parseColor(textColor)),
|
|
@@ -178,7 +174,6 @@ class AdvancedTextView : TextView {
|
|
|
178
174
|
)
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
// Make words clickable
|
|
182
177
|
spannableString.setSpan(
|
|
183
178
|
WordClickableSpan(wordPos.index, wordPos.word),
|
|
184
179
|
wordPos.start,
|
|
@@ -187,7 +182,6 @@ class AdvancedTextView : TextView {
|
|
|
187
182
|
)
|
|
188
183
|
}
|
|
189
184
|
|
|
190
|
-
// Use post to ensure UI thread and avoid layout issues
|
|
191
185
|
post {
|
|
192
186
|
setText(spannableString, BufferType.SPANNABLE)
|
|
193
187
|
Log.d(TAG, "Text updated with ${wordPositions.size} spans")
|
|
@@ -224,14 +218,11 @@ class AdvancedTextView : TextView {
|
|
|
224
218
|
) : ClickableSpan() {
|
|
225
219
|
|
|
226
220
|
override fun onClick(widget: View) {
|
|
227
|
-
Log.d(TAG, "
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Selection.removeSelection(it)
|
|
232
|
-
}
|
|
221
|
+
Log.d(TAG, "WordClickableSpan onClick triggered: '$word' (index=$wordIndex)")
|
|
222
|
+
|
|
223
|
+
widget.post {
|
|
224
|
+
sendWordPressEvent(word, wordIndex)
|
|
233
225
|
}
|
|
234
|
-
sendWordPressEvent(word, wordIndex)
|
|
235
226
|
}
|
|
236
227
|
|
|
237
228
|
override fun updateDrawState(ds: TextPaint) {
|
package/ios/AdvancedTextView.h
CHANGED
|
@@ -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 /*
|
|
15
|
+
#endif /* AdvancedTextView_h */
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -9,63 +9,583 @@
|
|
|
9
9
|
|
|
10
10
|
using namespace facebook::react;
|
|
11
11
|
|
|
12
|
-
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol>
|
|
12
|
+
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
|
|
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
|
{
|
|
26
|
+
NSLog(@"[AdvancedTextView] componentDescriptorProvider called");
|
|
22
27
|
return concreteComponentDescriptorProvider<AdvancedTextViewComponentDescriptor>();
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
26
31
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
NSLog(@"[AdvancedTextView] initWithFrame called");
|
|
33
|
+
if (self = [super initWithFrame:frame]) {
|
|
34
|
+
@try {
|
|
35
|
+
static const auto defaultProps = std::make_shared<const AdvancedTextViewProps>();
|
|
36
|
+
_props = defaultProps;
|
|
37
|
+
|
|
38
|
+
_wordRanges = [NSMutableArray array];
|
|
39
|
+
_highlightColors = [NSMutableDictionary dictionary];
|
|
40
|
+
_indicatorWordIndex = -1;
|
|
41
|
+
|
|
42
|
+
[self setupTextView];
|
|
43
|
+
[self setupGestureRecognizers];
|
|
44
|
+
|
|
45
|
+
NSLog(@"[AdvancedTextView] Initialization successful");
|
|
46
|
+
} @catch (NSException *exception) {
|
|
47
|
+
NSLog(@"[AdvancedTextView] Exception in init: %@", exception);
|
|
48
|
+
@throw;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
return self;
|
|
53
|
+
}
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
- (void)setupTextView
|
|
56
|
+
{
|
|
57
|
+
NSLog(@"[AdvancedTextView] setupTextView called");
|
|
58
|
+
@try {
|
|
59
|
+
_textView = [[UITextView alloc] initWithFrame:self.bounds];
|
|
60
|
+
_textView.editable = NO;
|
|
61
|
+
_textView.selectable = YES; // Allow text selection
|
|
62
|
+
_textView.scrollEnabled = YES;
|
|
63
|
+
_textView.backgroundColor = [UIColor clearColor];
|
|
64
|
+
_textView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
|
|
65
|
+
_textView.font = [UIFont systemFontOfSize:16];
|
|
66
|
+
_textView.textColor = [UIColor labelColor];
|
|
67
|
+
_textView.delegate = self;
|
|
35
68
|
|
|
36
|
-
|
|
69
|
+
self.contentView = _textView;
|
|
70
|
+
NSLog(@"[AdvancedTextView] TextView setup successful");
|
|
71
|
+
} @catch (NSException *exception) {
|
|
72
|
+
NSLog(@"[AdvancedTextView] Exception in setupTextView: %@", exception);
|
|
73
|
+
@throw;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
- (void)setupGestureRecognizers
|
|
78
|
+
{
|
|
79
|
+
NSLog(@"[AdvancedTextView] setupGestureRecognizers called");
|
|
80
|
+
@try {
|
|
81
|
+
// Single tap for word selection
|
|
82
|
+
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
|
|
83
|
+
initWithTarget:self
|
|
84
|
+
action:@selector(handleTap:)];
|
|
85
|
+
tapGesture.delegate = self;
|
|
86
|
+
[_textView addGestureRecognizer:tapGesture];
|
|
87
|
+
|
|
88
|
+
NSLog(@"[AdvancedTextView] Gesture recognizers setup successful");
|
|
89
|
+
} @catch (NSException *exception) {
|
|
90
|
+
NSLog(@"[AdvancedTextView] Exception in setupGestureRecognizers: %@", exception);
|
|
91
|
+
@throw;
|
|
92
|
+
}
|
|
37
93
|
}
|
|
38
94
|
|
|
39
95
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
40
96
|
{
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
NSLog(@"[AdvancedTextView] updateProps called");
|
|
98
|
+
@try {
|
|
99
|
+
const auto &oldViewProps = *std::static_pointer_cast<AdvancedTextViewProps const>(_props);
|
|
100
|
+
const auto &newViewProps = *std::static_pointer_cast<AdvancedTextViewProps const>(props);
|
|
101
|
+
|
|
102
|
+
BOOL textChanged = NO;
|
|
103
|
+
BOOL highlightsChanged = NO;
|
|
104
|
+
BOOL menuChanged = NO;
|
|
105
|
+
BOOL indicatorChanged = NO;
|
|
106
|
+
|
|
107
|
+
// Check text change
|
|
108
|
+
if (oldViewProps.text != newViewProps.text) {
|
|
109
|
+
textChanged = YES;
|
|
110
|
+
NSLog(@"[AdvancedTextView] Text changed: %s", newViewProps.text.c_str());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check highlighted words change
|
|
114
|
+
if (oldViewProps.highlightedWords.size() != newViewProps.highlightedWords.size()) {
|
|
115
|
+
highlightsChanged = YES;
|
|
116
|
+
NSLog(@"[AdvancedTextView] Highlights size changed: %zu -> %zu",
|
|
117
|
+
oldViewProps.highlightedWords.size(), newViewProps.highlightedWords.size());
|
|
118
|
+
} else {
|
|
119
|
+
for (size_t i = 0; i < oldViewProps.highlightedWords.size(); i++) {
|
|
120
|
+
const auto &oldHW = oldViewProps.highlightedWords[i];
|
|
121
|
+
const auto &newHW = newViewProps.highlightedWords[i];
|
|
122
|
+
if (oldHW.index != newHW.index || oldHW.highlightColor != newHW.highlightColor) {
|
|
123
|
+
highlightsChanged = YES;
|
|
124
|
+
NSLog(@"[AdvancedTextView] Highlight changed at index %zu", i);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check menu options change
|
|
131
|
+
if (oldViewProps.menuOptions.size() != newViewProps.menuOptions.size()) {
|
|
132
|
+
menuChanged = YES;
|
|
133
|
+
NSLog(@"[AdvancedTextView] Menu options size changed: %zu -> %zu",
|
|
134
|
+
oldViewProps.menuOptions.size(), newViewProps.menuOptions.size());
|
|
135
|
+
} else {
|
|
136
|
+
for (size_t i = 0; i < oldViewProps.menuOptions.size(); i++) {
|
|
137
|
+
if (oldViewProps.menuOptions[i] != newViewProps.menuOptions[i]) {
|
|
138
|
+
menuChanged = YES;
|
|
139
|
+
NSLog(@"[AdvancedTextView] Menu option changed at index %zu", i);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check indicator change
|
|
146
|
+
if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
|
|
147
|
+
indicatorChanged = YES;
|
|
148
|
+
NSLog(@"[AdvancedTextView] Indicator changed: %d -> %d",
|
|
149
|
+
oldViewProps.indicatorWordIndex, newViewProps.indicatorWordIndex);
|
|
150
|
+
}
|
|
43
151
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
152
|
+
// Apply updates
|
|
153
|
+
if (textChanged) {
|
|
154
|
+
NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
|
|
155
|
+
[self updateTextContent:text];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (highlightsChanged) {
|
|
159
|
+
[self updateHighlightedWords:newViewProps.highlightedWords];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (menuChanged) {
|
|
163
|
+
[self updateMenuOptions:newViewProps.menuOptions];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (indicatorChanged) {
|
|
167
|
+
_indicatorWordIndex = newViewProps.indicatorWordIndex;
|
|
168
|
+
[self updateTextAppearance];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
[super updateProps:props oldProps:oldProps];
|
|
172
|
+
NSLog(@"[AdvancedTextView] updateProps completed successfully");
|
|
173
|
+
} @catch (NSException *exception) {
|
|
174
|
+
NSLog(@"[AdvancedTextView] Exception in updateProps: %@, reason: %@",
|
|
175
|
+
exception.name, exception.reason);
|
|
176
|
+
@throw;
|
|
47
177
|
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
- (void)updateTextContent:(NSString *)text
|
|
181
|
+
{
|
|
182
|
+
NSLog(@"[AdvancedTextView] updateTextContent called with text length: %lu",
|
|
183
|
+
(unsigned long)text.length);
|
|
184
|
+
@try {
|
|
185
|
+
if (!text) {
|
|
186
|
+
NSLog(@"[AdvancedTextView] Text is nil, skipping update");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// IMPORTANT: Set the text first!
|
|
191
|
+
_textView.text = text;
|
|
192
|
+
NSLog(@"[AdvancedTextView] Set textView.text with length: %lu", (unsigned long)_textView.text.length);
|
|
193
|
+
|
|
194
|
+
// Parse text into words and their ranges
|
|
195
|
+
[_wordRanges removeAllObjects];
|
|
48
196
|
|
|
49
|
-
|
|
197
|
+
NSRange searchRange = NSMakeRange(0, text.length);
|
|
198
|
+
NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
199
|
+
|
|
200
|
+
NSInteger wordIndex = 0;
|
|
201
|
+
while (searchRange.location < text.length) {
|
|
202
|
+
// Skip whitespace
|
|
203
|
+
while (searchRange.location < text.length &&
|
|
204
|
+
[whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
|
|
205
|
+
searchRange.location++;
|
|
206
|
+
searchRange.length = text.length - searchRange.location;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (searchRange.location >= text.length) break;
|
|
210
|
+
|
|
211
|
+
// Find word end
|
|
212
|
+
NSUInteger wordStart = searchRange.location;
|
|
213
|
+
while (searchRange.location < text.length &&
|
|
214
|
+
![whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
|
|
215
|
+
searchRange.location++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
NSRange wordRange = NSMakeRange(wordStart, searchRange.location - wordStart);
|
|
219
|
+
NSString *word = [text substringWithRange:wordRange];
|
|
220
|
+
|
|
221
|
+
[_wordRanges addObject:@{
|
|
222
|
+
@"word": word,
|
|
223
|
+
@"range": [NSValue valueWithRange:wordRange],
|
|
224
|
+
@"index": @(wordIndex)
|
|
225
|
+
}];
|
|
226
|
+
|
|
227
|
+
wordIndex++;
|
|
228
|
+
searchRange.length = text.length - searchRange.location;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
NSLog(@"[AdvancedTextView] Parsed %ld words", (long)_wordRanges.count);
|
|
232
|
+
[self updateTextAppearance];
|
|
233
|
+
} @catch (NSException *exception) {
|
|
234
|
+
NSLog(@"[AdvancedTextView] Exception in updateTextContent: %@", exception);
|
|
235
|
+
@throw;
|
|
236
|
+
}
|
|
50
237
|
}
|
|
51
238
|
|
|
52
|
-
|
|
239
|
+
- (void)updateHighlightedWords:(const std::vector<AdvancedTextViewHighlightedWordsStruct> &)highlightedWords
|
|
53
240
|
{
|
|
54
|
-
|
|
241
|
+
NSLog(@"[AdvancedTextView] updateHighlightedWords called with %zu highlights",
|
|
242
|
+
highlightedWords.size());
|
|
243
|
+
@try {
|
|
244
|
+
[_highlightColors removeAllObjects];
|
|
245
|
+
|
|
246
|
+
for (const auto &hw : highlightedWords) {
|
|
247
|
+
NSInteger index = hw.index;
|
|
248
|
+
NSString *colorString = [NSString stringWithUTF8String:hw.highlightColor.c_str()];
|
|
249
|
+
UIColor *color = [self hexStringToColor:colorString];
|
|
250
|
+
|
|
251
|
+
if (color) {
|
|
252
|
+
_highlightColors[@(index)] = color;
|
|
253
|
+
NSLog(@"[AdvancedTextView] Added highlight for word %ld with color %@",
|
|
254
|
+
(long)index, colorString);
|
|
255
|
+
} else {
|
|
256
|
+
NSLog(@"[AdvancedTextView] Failed to parse color: %@", colorString);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
[self updateTextAppearance];
|
|
261
|
+
} @catch (NSException *exception) {
|
|
262
|
+
NSLog(@"[AdvancedTextView] Exception in updateHighlightedWords: %@", exception);
|
|
263
|
+
@throw;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
- (void)updateMenuOptions:(const std::vector<std::string> &)options
|
|
268
|
+
{
|
|
269
|
+
NSLog(@"[AdvancedTextView] updateMenuOptions called with %zu options", options.size());
|
|
270
|
+
@try {
|
|
271
|
+
NSMutableArray *menuArray = [NSMutableArray array];
|
|
272
|
+
for (const auto &option : options) {
|
|
273
|
+
NSString *optionStr = [NSString stringWithUTF8String:option.c_str()];
|
|
274
|
+
[menuArray addObject:optionStr];
|
|
275
|
+
NSLog(@"[AdvancedTextView] Added menu option: %@", optionStr);
|
|
276
|
+
}
|
|
277
|
+
_menuOptions = [menuArray copy];
|
|
278
|
+
} @catch (NSException *exception) {
|
|
279
|
+
NSLog(@"[AdvancedTextView] Exception in updateMenuOptions: %@", exception);
|
|
280
|
+
@throw;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
- (void)updateTextAppearance
|
|
285
|
+
{
|
|
286
|
+
NSLog(@"[AdvancedTextView] updateTextAppearance called");
|
|
287
|
+
@try {
|
|
288
|
+
if (_wordRanges.count == 0) {
|
|
289
|
+
NSLog(@"[AdvancedTextView] No word ranges, skipping appearance update");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!_textView.text || _textView.text.length == 0) {
|
|
294
|
+
NSLog(@"[AdvancedTextView] TextView has no text");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
|
|
299
|
+
initWithString:_textView.text];
|
|
300
|
+
|
|
301
|
+
// Apply default attributes
|
|
302
|
+
[attributedString addAttribute:NSFontAttributeName
|
|
303
|
+
value:[UIFont systemFontOfSize:16]
|
|
304
|
+
range:NSMakeRange(0, attributedString.length)];
|
|
305
|
+
|
|
306
|
+
[attributedString addAttribute:NSForegroundColorAttributeName
|
|
307
|
+
value:[UIColor labelColor]
|
|
308
|
+
range:NSMakeRange(0, attributedString.length)];
|
|
309
|
+
|
|
310
|
+
// Apply highlights
|
|
311
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
312
|
+
NSNumber *index = wordInfo[@"index"];
|
|
313
|
+
NSValue *rangeValue = wordInfo[@"range"];
|
|
314
|
+
NSRange range = [rangeValue rangeValue];
|
|
315
|
+
|
|
316
|
+
// Validate range
|
|
317
|
+
if (range.location + range.length > attributedString.length) {
|
|
318
|
+
NSLog(@"[AdvancedTextView] Invalid range at index %@: {%lu, %lu} for string length %lu",
|
|
319
|
+
index, (unsigned long)range.location, (unsigned long)range.length,
|
|
320
|
+
(unsigned long)attributedString.length);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
UIColor *highlightColor = _highlightColors[index];
|
|
325
|
+
if (highlightColor) {
|
|
326
|
+
[attributedString addAttribute:NSBackgroundColorAttributeName
|
|
327
|
+
value:highlightColor
|
|
328
|
+
range:range];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Add background color indicator for indicated word
|
|
332
|
+
if (_indicatorWordIndex >= 0 && [index integerValue] == _indicatorWordIndex) {
|
|
333
|
+
// Use a semi-transparent blue background for the current word
|
|
334
|
+
UIColor *indicatorColor = [[UIColor systemBlueColor] colorWithAlphaComponent:0.3];
|
|
335
|
+
[attributedString addAttribute:NSBackgroundColorAttributeName
|
|
336
|
+
value:indicatorColor
|
|
337
|
+
range:range];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
_textView.attributedText = attributedString;
|
|
342
|
+
NSLog(@"[AdvancedTextView] Text appearance updated successfully");
|
|
343
|
+
} @catch (NSException *exception) {
|
|
344
|
+
NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@, reason: %@",
|
|
345
|
+
exception.name, exception.reason);
|
|
346
|
+
@throw;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
- (void)handleTap:(UITapGestureRecognizer *)gesture
|
|
351
|
+
{
|
|
352
|
+
NSLog(@"[AdvancedTextView] handleTap called");
|
|
353
|
+
@try {
|
|
354
|
+
if (gesture.state != UIGestureRecognizerStateEnded) return;
|
|
355
|
+
|
|
356
|
+
CGPoint location = [gesture locationInView:_textView];
|
|
357
|
+
NSInteger wordIndex = [self wordIndexAtPoint:location];
|
|
358
|
+
|
|
359
|
+
NSLog(@"[AdvancedTextView] Tap at point: {%.2f, %.2f}, word index: %ld",
|
|
360
|
+
location.x, location.y, (long)wordIndex);
|
|
361
|
+
|
|
362
|
+
// Dismiss any existing selection
|
|
363
|
+
_textView.selectedTextRange = nil;
|
|
364
|
+
|
|
365
|
+
if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
|
|
366
|
+
NSDictionary *wordInfo = _wordRanges[wordIndex];
|
|
367
|
+
NSString *word = wordInfo[@"word"];
|
|
368
|
+
|
|
369
|
+
NSLog(@"[AdvancedTextView] Word pressed: %@", word);
|
|
370
|
+
[self emitWordPressEvent:word index:wordIndex];
|
|
371
|
+
}
|
|
372
|
+
} @catch (NSException *exception) {
|
|
373
|
+
NSLog(@"[AdvancedTextView] Exception in handleTap: %@", exception);
|
|
374
|
+
}
|
|
55
375
|
}
|
|
56
376
|
|
|
57
|
-
-
|
|
377
|
+
#pragma mark - UITextViewDelegate
|
|
378
|
+
|
|
379
|
+
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange interaction:(UITextItemInteraction)interaction
|
|
58
380
|
{
|
|
59
|
-
|
|
60
|
-
|
|
381
|
+
return YES;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
385
|
+
{
|
|
386
|
+
NSLog(@"[AdvancedTextView] Selection changed");
|
|
387
|
+
|
|
388
|
+
// Get selected text
|
|
389
|
+
NSString *selectedText = [textView.text substringWithRange:textView.selectedRange];
|
|
61
390
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
int r = (hex >> 16) & 0xFF;
|
|
65
|
-
int g = (hex >> 8) & 0xFF;
|
|
66
|
-
int b = (hex) & 0xFF;
|
|
391
|
+
if (selectedText.length > 0) {
|
|
392
|
+
NSLog(@"[AdvancedTextView] Selected text: %@", selectedText);
|
|
67
393
|
|
|
68
|
-
|
|
394
|
+
// Add custom menu items
|
|
395
|
+
if (_menuOptions && _menuOptions.count > 0) {
|
|
396
|
+
[self setupCustomMenuItems];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
- (void)setupCustomMenuItems
|
|
402
|
+
{
|
|
403
|
+
NSLog(@"[AdvancedTextView] Setting up custom menu items");
|
|
404
|
+
|
|
405
|
+
UIMenuController *menuController = [UIMenuController sharedMenuController];
|
|
406
|
+
NSMutableArray *customItems = [NSMutableArray array];
|
|
407
|
+
|
|
408
|
+
for (NSString *option in _menuOptions) {
|
|
409
|
+
UIMenuItem *item = [[UIMenuItem alloc] initWithTitle:option
|
|
410
|
+
action:@selector(handleCustomMenuAction:)];
|
|
411
|
+
[customItems addObject:item];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
menuController.menuItems = customItems;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
- (void)handleCustomMenuAction:(UIMenuItem *)sender
|
|
418
|
+
{
|
|
419
|
+
NSLog(@"[AdvancedTextView] Custom menu action: %@", sender.title);
|
|
420
|
+
|
|
421
|
+
NSString *selectedText = [_textView.text substringWithRange:_textView.selectedRange];
|
|
422
|
+
[self emitSelectionEvent:selectedText menuOption:sender.title];
|
|
423
|
+
|
|
424
|
+
// Clear selection after action
|
|
425
|
+
_textView.selectedTextRange = nil;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
|
|
429
|
+
{
|
|
430
|
+
// Allow custom menu items
|
|
431
|
+
if (action == @selector(handleCustomMenuAction:)) {
|
|
432
|
+
return YES;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Allow default actions (copy, etc.)
|
|
436
|
+
if (action == @selector(copy:) ||
|
|
437
|
+
action == @selector(selectAll:) ||
|
|
438
|
+
action == @selector(select:)) {
|
|
439
|
+
return [super canPerformAction:action withSender:sender];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return NO;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
|
|
446
|
+
{
|
|
447
|
+
// Removed - using native iOS text selection menu instead
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
- (NSInteger)wordIndexAtPoint:(CGPoint)point
|
|
451
|
+
{
|
|
452
|
+
@try {
|
|
453
|
+
if (!_textView.layoutManager || !_textView.textContainer) {
|
|
454
|
+
NSLog(@"[AdvancedTextView] Layout manager or text container is nil");
|
|
455
|
+
return -1;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Adjust point for text container insets
|
|
459
|
+
point.x -= _textView.textContainerInset.left;
|
|
460
|
+
point.y -= _textView.textContainerInset.top;
|
|
461
|
+
|
|
462
|
+
NSLayoutManager *layoutManager = _textView.layoutManager;
|
|
463
|
+
NSTextContainer *textContainer = _textView.textContainer;
|
|
464
|
+
|
|
465
|
+
NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
|
|
466
|
+
inTextContainer:textContainer
|
|
467
|
+
fractionOfDistanceBetweenInsertionPoints:nil];
|
|
468
|
+
|
|
469
|
+
NSLog(@"[AdvancedTextView] Character index at point: %lu", (unsigned long)characterIndex);
|
|
470
|
+
|
|
471
|
+
// Find which word this character belongs to
|
|
472
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
473
|
+
NSValue *rangeValue = wordInfo[@"range"];
|
|
474
|
+
NSRange range = [rangeValue rangeValue];
|
|
475
|
+
|
|
476
|
+
if (NSLocationInRange(characterIndex, range)) {
|
|
477
|
+
return [wordInfo[@"index"] integerValue];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return -1;
|
|
482
|
+
} @catch (NSException *exception) {
|
|
483
|
+
NSLog(@"[AdvancedTextView] Exception in wordIndexAtPoint: %@", exception);
|
|
484
|
+
return -1;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
- (void)showContextMenuForWord:(NSString *)word atIndex:(NSInteger)index location:(CGPoint)location
|
|
489
|
+
{
|
|
490
|
+
// Removed - using native iOS text selection menu instead
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
- (UIViewController *)findViewController
|
|
494
|
+
{
|
|
495
|
+
// Removed - no longer needed
|
|
496
|
+
return nil;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
- (void)emitWordPressEvent:(NSString *)word index:(NSInteger)index
|
|
500
|
+
{
|
|
501
|
+
NSLog(@"[AdvancedTextView] emitWordPressEvent: %@ at index: %ld", word, (long)index);
|
|
502
|
+
@try {
|
|
503
|
+
if (_eventEmitter) {
|
|
504
|
+
auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
|
|
505
|
+
|
|
506
|
+
AdvancedTextViewEventEmitter::OnWordPress event;
|
|
507
|
+
event.word = [word UTF8String];
|
|
508
|
+
event.index = static_cast<int>(index);
|
|
509
|
+
|
|
510
|
+
emitter->onWordPress(event);
|
|
511
|
+
NSLog(@"[AdvancedTextView] Word press event emitted successfully");
|
|
512
|
+
} else {
|
|
513
|
+
NSLog(@"[AdvancedTextView] Event emitter is null");
|
|
514
|
+
}
|
|
515
|
+
} @catch (NSException *exception) {
|
|
516
|
+
NSLog(@"[AdvancedTextView] Exception in emitWordPressEvent: %@", exception);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
- (void)emitSelectionEvent:(NSString *)selectedText menuOption:(NSString *)option
|
|
521
|
+
{
|
|
522
|
+
NSLog(@"[AdvancedTextView] emitSelectionEvent: %@ with option: %@", selectedText, option);
|
|
523
|
+
@try {
|
|
524
|
+
if (_eventEmitter) {
|
|
525
|
+
auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
|
|
526
|
+
|
|
527
|
+
AdvancedTextViewEventEmitter::OnSelection event;
|
|
528
|
+
event.selectedText = [selectedText UTF8String];
|
|
529
|
+
event.event = [option UTF8String];
|
|
530
|
+
|
|
531
|
+
emitter->onSelection(event);
|
|
532
|
+
NSLog(@"[AdvancedTextView] Selection event emitted successfully");
|
|
533
|
+
} else {
|
|
534
|
+
NSLog(@"[AdvancedTextView] Event emitter is null");
|
|
535
|
+
}
|
|
536
|
+
} @catch (NSException *exception) {
|
|
537
|
+
NSLog(@"[AdvancedTextView] Exception in emitSelectionEvent: %@", exception);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
- (void)layoutSubviews
|
|
542
|
+
{
|
|
543
|
+
NSLog(@"[AdvancedTextView] layoutSubviews called");
|
|
544
|
+
@try {
|
|
545
|
+
[super layoutSubviews];
|
|
546
|
+
_textView.frame = self.bounds;
|
|
547
|
+
} @catch (NSException *exception) {
|
|
548
|
+
NSLog(@"[AdvancedTextView] Exception in layoutSubviews: %@", exception);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
- (UIColor *)hexStringToColor:(NSString *)stringToConvert
|
|
553
|
+
{
|
|
554
|
+
@try {
|
|
555
|
+
if (!stringToConvert || [stringToConvert length] == 0) {
|
|
556
|
+
NSLog(@"[AdvancedTextView] Empty color string");
|
|
557
|
+
return nil;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
NSString *noHashString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""];
|
|
561
|
+
NSScanner *stringScanner = [NSScanner scannerWithString:noHashString];
|
|
562
|
+
|
|
563
|
+
unsigned hex;
|
|
564
|
+
if (![stringScanner scanHexInt:&hex]) {
|
|
565
|
+
NSLog(@"[AdvancedTextView] Failed to parse hex color: %@", stringToConvert);
|
|
566
|
+
return nil;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
int r = (hex >> 16) & 0xFF;
|
|
570
|
+
int g = (hex >> 8) & 0xFF;
|
|
571
|
+
int b = (hex) & 0xFF;
|
|
572
|
+
|
|
573
|
+
return [UIColor colorWithRed:r / 255.0f green:g / 255.0f blue:b / 255.0f alpha:1.0f];
|
|
574
|
+
} @catch (NSException *exception) {
|
|
575
|
+
NSLog(@"[AdvancedTextView] Exception in hexStringToColor: %@", exception);
|
|
576
|
+
return nil;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
|
|
581
|
+
{
|
|
582
|
+
NSLog(@"[AdvancedTextView] AdvancedTextViewCls called");
|
|
583
|
+
return AdvancedTextView.class;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
- (void)dealloc
|
|
587
|
+
{
|
|
588
|
+
NSLog(@"[AdvancedTextView] dealloc called");
|
|
69
589
|
}
|
|
70
590
|
|
|
71
591
|
@end
|
package/package.json
CHANGED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
package com.advancedtext
|
|
2
|
-
|
|
3
|
-
import android.text.Layout
|
|
4
|
-
import android.text.Selection
|
|
5
|
-
import android.text.Spannable
|
|
6
|
-
import android.text.method.BaseMovementMethod
|
|
7
|
-
import android.text.style.ClickableSpan
|
|
8
|
-
import android.view.MotionEvent
|
|
9
|
-
import android.widget.TextView
|
|
10
|
-
|
|
11
|
-
class ClickableMovementMethod : BaseMovementMethod() {
|
|
12
|
-
|
|
13
|
-
companion object {
|
|
14
|
-
private var sInstance: ClickableMovementMethod? = null
|
|
15
|
-
|
|
16
|
-
fun getInstance(): ClickableMovementMethod {
|
|
17
|
-
if (sInstance == null) {
|
|
18
|
-
sInstance = ClickableMovementMethod()
|
|
19
|
-
}
|
|
20
|
-
return sInstance!!
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
override fun canSelectArbitrarily(): Boolean = false
|
|
25
|
-
|
|
26
|
-
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
|
|
27
|
-
val action = event.actionMasked
|
|
28
|
-
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
|
29
|
-
|
|
30
|
-
var x = (event.x - widget.totalPaddingLeft + widget.scrollX).toInt()
|
|
31
|
-
var y = (event.y - widget.totalPaddingTop + widget.scrollY).toInt()
|
|
32
|
-
|
|
33
|
-
val layout: Layout = widget.layout
|
|
34
|
-
val links: Array<ClickableSpan>? = if (y < 0 || y > layout.height) {
|
|
35
|
-
null
|
|
36
|
-
} else {
|
|
37
|
-
val line = layout.getLineForVertical(y)
|
|
38
|
-
if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) null
|
|
39
|
-
else {
|
|
40
|
-
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
|
|
41
|
-
buffer.getSpans(offset, offset, ClickableSpan::class.java)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!links.isNullOrEmpty()) {
|
|
46
|
-
if (action == MotionEvent.ACTION_UP) links[0].onClick(widget)
|
|
47
|
-
else Selection.setSelection(buffer, buffer.getSpanStart(links[0]), buffer.getSpanEnd(links[0]))
|
|
48
|
-
return true
|
|
49
|
-
} else {
|
|
50
|
-
Selection.removeSelection(buffer)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
override fun initialize(widget: TextView, text: Spannable) {
|
|
57
|
-
Selection.removeSelection(text)
|
|
58
|
-
}
|
|
59
|
-
}
|