react-native-advanced-text 0.1.21 → 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() }
|
|
@@ -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,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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
56
|
+
self.contentView = _textView;
|
|
57
|
+
}
|
|
32
58
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
106
|
+
- (void)updateTextContent:(NSString *)text
|
|
53
107
|
{
|
|
54
|
-
|
|
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
|
-
-
|
|
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,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
|
-
}
|