react-native-advanced-text 0.1.27 → 0.1.29
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.
- package/README.md +56 -37
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +283 -553
- package/ios/AdvancedTextView.mm +109 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,37 +1,56 @@
|
|
|
1
|
-
# react-native-advanced-text
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
```sh
|
|
9
|
-
npm install react-native-advanced-text
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```js
|
|
17
|
-
import { AdvancedTextView } from "react-native-advanced-text";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
# react-native-advanced-text
|
|
2
|
+
|
|
3
|
+
react-native-advanced-text is a powerful cross-platform text component for React Native that enables word-level interaction, dynamic highlighting, and custom selection actions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install react-native-advanced-text
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { AdvancedTextView } from "react-native-advanced-text";
|
|
18
|
+
|
|
19
|
+
<AdvancedText
|
|
20
|
+
text={'This is an example of AdvancedText component. Tap on any word to see the event in action.'}
|
|
21
|
+
style={[styles.AdvancedText, { minHeight }]}
|
|
22
|
+
indicatorWordIndex={2}
|
|
23
|
+
onWordPress={(event) => {
|
|
24
|
+
console.log({event})
|
|
25
|
+
}}
|
|
26
|
+
menuOptions={['Highlight', 'Copy', 'Translate']}
|
|
27
|
+
onSelection={(event) => {
|
|
28
|
+
console.log({event})
|
|
29
|
+
}}
|
|
30
|
+
highlightedWords={[
|
|
31
|
+
{
|
|
32
|
+
index: 4,
|
|
33
|
+
highlightColor: '#6baeffb5',
|
|
34
|
+
},
|
|
35
|
+
]}
|
|
36
|
+
fontSize={24}
|
|
37
|
+
color={'#FFFFFF'}
|
|
38
|
+
fontWeight="normal"
|
|
39
|
+
fontFamily={'monospace'}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Contributing
|
|
45
|
+
|
|
46
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
47
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
48
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -1,637 +1,367 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
package com.advancedtext
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.Point
|
|
6
|
+
import android.text.SpannableString
|
|
7
|
+
import android.text.Spannable
|
|
8
|
+
import android.text.Spanned
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.text.method.ArrowKeyMovementMethod
|
|
11
|
+
import android.text.style.ClickableSpan
|
|
12
|
+
import android.text.style.BackgroundColorSpan
|
|
13
|
+
import android.text.style.ForegroundColorSpan
|
|
14
|
+
import android.util.AttributeSet
|
|
15
|
+
import android.util.Log
|
|
16
|
+
import android.view.ActionMode
|
|
17
|
+
import android.view.Menu
|
|
18
|
+
import android.view.MenuItem
|
|
19
|
+
import android.view.View
|
|
20
|
+
import android.view.MotionEvent
|
|
21
|
+
import android.widget.TextView
|
|
22
|
+
import com.facebook.react.bridge.Arguments
|
|
23
|
+
import com.facebook.react.bridge.ReactContext
|
|
24
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
25
|
+
import android.text.Selection
|
|
26
|
+
import android.graphics.Typeface
|
|
27
|
+
import androidx.core.text.getSpans
|
|
28
|
+
|
|
29
|
+
class AdvancedTextView : TextView {
|
|
30
|
+
|
|
31
|
+
private val TAG = "AdvancedTextView"
|
|
32
|
+
|
|
33
|
+
private var highlightedWords: List<HighlightedWord> = emptyList()
|
|
34
|
+
private var menuOptions: List<String> = emptyList()
|
|
35
|
+
private var indicatorWordIndex: Int = -1
|
|
36
|
+
private var lastSelectedText: String = ""
|
|
37
|
+
private var customActionMode: ActionMode? = null
|
|
38
|
+
private var currentText: String = ""
|
|
39
|
+
private var textColor: String = "#000000"
|
|
40
|
+
private var fontSize: Float = 16f
|
|
41
|
+
private var fontWeight: String = "normal"
|
|
42
|
+
private var textAlign: String = "left"
|
|
43
|
+
private var fontFamily: String = "sans-serif"
|
|
44
|
+
|
|
45
|
+
private var wordPositions: List<WordPosition> = emptyList()
|
|
46
|
+
|
|
47
|
+
constructor(context: Context?) : super(context) { init() }
|
|
48
|
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
|
|
49
|
+
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
|
|
50
|
+
|
|
51
|
+
private fun init() {
|
|
52
|
+
Log.d(TAG, "AdvancedTextView initialized")
|
|
53
|
+
|
|
54
|
+
textSize = 16f
|
|
55
|
+
setPadding(16, 16, 16, 16)
|
|
56
|
+
setTextIsSelectable(true)
|
|
57
|
+
|
|
58
|
+
movementMethod = SmartMovementMethod
|
|
59
|
+
|
|
60
|
+
customSelectionActionModeCallback = object : ActionMode.Callback {
|
|
61
|
+
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
62
|
+
customActionMode = mode
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
26
65
|
|
|
27
|
-
|
|
66
|
+
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
67
|
+
menu?.clear()
|
|
28
68
|
|
|
29
|
-
|
|
69
|
+
val selectionStart = selectionStart
|
|
70
|
+
val selectionEnd = selectionEnd
|
|
30
71
|
|
|
72
|
+
if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
|
|
73
|
+
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
31
74
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
menuOptions.forEachIndexed { index, option ->
|
|
76
|
+
menu?.add(0, index, index, option)
|
|
77
|
+
}
|
|
35
78
|
|
|
36
|
-
|
|
79
|
+
sendSelectionEvent(lastSelectedText, "selection")
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
37
84
|
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
85
|
+
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
|
86
|
+
item?.let {
|
|
87
|
+
val menuItemText = it.title.toString()
|
|
88
|
+
sendSelectionEvent(lastSelectedText, menuItemText)
|
|
89
|
+
mode?.finish()
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
41
94
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
96
|
+
customActionMode = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
45
99
|
}
|
|
46
100
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
101
|
+
fun setAdvancedText(text: String) {
|
|
102
|
+
if (currentText == text) {
|
|
103
|
+
Log.d(TAG, "Text unchanged, skipping update")
|
|
104
|
+
return
|
|
105
|
+
}
|
|
50
106
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
107
|
+
Log.d(TAG, "setAdvancedText: length=${text.length}")
|
|
108
|
+
currentText = text
|
|
109
|
+
calculateWordPositions(text)
|
|
110
|
+
updateTextWithHighlights()
|
|
55
111
|
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@end
|
|
59
|
-
|
|
60
112
|
|
|
113
|
+
fun setAdvancedTextColor(colorInt: Int) {
|
|
114
|
+
textColor = String.format("#%06X", 0xFFFFFF and colorInt)
|
|
115
|
+
updateTextWithHighlights()
|
|
116
|
+
}
|
|
61
117
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@implementation AdvancedTextView
|
|
118
|
+
fun setAdvancedTextSize(size: Float) {
|
|
119
|
+
if (fontSize == size) return
|
|
120
|
+
fontSize = size
|
|
121
|
+
updateTextWithHighlights()
|
|
122
|
+
}
|
|
69
123
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
124
|
+
fun setAdvancedFontWeight(weight: String) {
|
|
125
|
+
if (fontWeight == weight) return
|
|
126
|
+
fontWeight = weight
|
|
127
|
+
updateTextWithHighlights()
|
|
128
|
+
}
|
|
75
129
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
130
|
+
fun setAdvancedTextAlign(align: String) {
|
|
131
|
+
if (textAlign == align) return
|
|
132
|
+
textAlign = align
|
|
133
|
+
when (align) {
|
|
134
|
+
"left" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
|
|
135
|
+
"center" -> textAlignment = View.TEXT_ALIGNMENT_CENTER
|
|
136
|
+
"right" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_END
|
|
137
|
+
else -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
|
|
100
138
|
}
|
|
101
139
|
}
|
|
102
140
|
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
fun setAdvancedFontFamily(family: String) {
|
|
142
|
+
if (fontFamily == family) return
|
|
143
|
+
fontFamily = family
|
|
144
|
+
typeface = Typeface.create(family, Typeface.NORMAL)
|
|
145
|
+
}
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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;
|
|
147
|
+
fun setMenuOptions(menuOptions: List<String>) {
|
|
148
|
+
if (this.menuOptions == menuOptions) return
|
|
149
|
+
this.menuOptions = menuOptions
|
|
126
150
|
}
|
|
127
|
-
}
|
|
128
151
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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;
|
|
152
|
+
fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
|
|
153
|
+
if (this.highlightedWords == highlightedWords) return
|
|
154
|
+
this.highlightedWords = highlightedWords
|
|
155
|
+
updateTextWithHighlights()
|
|
143
156
|
}
|
|
144
|
-
}
|
|
145
157
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
}
|
|
158
|
+
fun setIndicatorWordIndex(index: Int) {
|
|
159
|
+
if (this.indicatorWordIndex == index) return
|
|
160
|
+
this.indicatorWordIndex = index
|
|
161
|
+
updateTextWithHighlights()
|
|
162
|
+
}
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
private fun calculateWordPositions(text: String) {
|
|
165
|
+
if (text.isEmpty()) {
|
|
166
|
+
wordPositions = emptyList()
|
|
167
|
+
return
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
_fontWeight = [NSString stringWithUTF8String:newViewProps.fontWeight.c_str()];
|
|
174
|
-
styleChanged = YES;
|
|
175
|
-
}
|
|
170
|
+
val positions = mutableListOf<WordPosition>()
|
|
171
|
+
val regex = "\\S+".toRegex()
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
regex.findAll(text).forEachIndexed { index, match ->
|
|
174
|
+
positions.add(WordPosition(
|
|
175
|
+
index = index,
|
|
176
|
+
start = match.range.first,
|
|
177
|
+
end = match.range.last + 1,
|
|
178
|
+
word = match.value
|
|
179
|
+
))
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
_textColor = [self hexStringToColor:colorStr];
|
|
187
|
-
styleChanged = YES;
|
|
188
|
-
}
|
|
182
|
+
wordPositions = positions
|
|
183
|
+
Log.d(TAG, "Calculated ${wordPositions.size} word positions")
|
|
184
|
+
}
|
|
189
185
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
private fun updateTextWithHighlights() {
|
|
187
|
+
if (currentText.isEmpty()) {
|
|
188
|
+
Log.d(TAG, "No text available, skipping")
|
|
189
|
+
return
|
|
193
190
|
}
|
|
194
191
|
|
|
195
|
-
|
|
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;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
192
|
+
val spannableString = SpannableString(currentText)
|
|
207
193
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
194
|
+
wordPositions.forEach { wordPos ->
|
|
195
|
+
highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
|
|
196
|
+
val color = parseColor(highlightedWord.highlightColor)
|
|
197
|
+
spannableString.setSpan(
|
|
198
|
+
BackgroundColorSpan(color),
|
|
199
|
+
wordPos.start,
|
|
200
|
+
wordPos.end,
|
|
201
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
202
|
+
)
|
|
216
203
|
}
|
|
217
|
-
}
|
|
218
204
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
205
|
+
if (wordPos.index == indicatorWordIndex) {
|
|
206
|
+
spannableString.setSpan(
|
|
207
|
+
ForegroundColorSpan(Color.parseColor(textColor)),
|
|
208
|
+
wordPos.start,
|
|
209
|
+
wordPos.end,
|
|
210
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
211
|
+
)
|
|
212
|
+
}
|
|
222
213
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
214
|
+
// Add clickable span for word clicks
|
|
215
|
+
spannableString.setSpan(
|
|
216
|
+
WordClickableSpan(wordPos.index, wordPos.word),
|
|
217
|
+
wordPos.start,
|
|
218
|
+
wordPos.end,
|
|
219
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
220
|
+
)
|
|
226
221
|
}
|
|
227
222
|
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
textAlignment = when (textAlign) {
|
|
224
|
+
"left" -> View.TEXT_ALIGNMENT_TEXT_START
|
|
225
|
+
"center" -> View.TEXT_ALIGNMENT_CENTER
|
|
226
|
+
"right" -> View.TEXT_ALIGNMENT_TEXT_END
|
|
227
|
+
else -> View.TEXT_ALIGNMENT_TEXT_START
|
|
230
228
|
}
|
|
231
229
|
|
|
232
|
-
|
|
233
|
-
[self updateMenuOptions:newViewProps.menuOptions];
|
|
234
|
-
}
|
|
230
|
+
setTextSize(fontSize)
|
|
235
231
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
232
|
+
typeface = when (fontWeight) {
|
|
233
|
+
"bold" -> Typeface.create(fontFamily, Typeface.BOLD)
|
|
234
|
+
"italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
|
|
235
|
+
else -> Typeface.create(fontFamily, Typeface.NORMAL)
|
|
239
236
|
}
|
|
240
237
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
post {
|
|
239
|
+
setText(spannableString, BufferType.SPANNABLE)
|
|
240
|
+
Log.d(TAG, "Text updated with ${wordPositions.size} spans")
|
|
244
241
|
}
|
|
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;
|
|
251
242
|
}
|
|
252
|
-
}
|
|
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
|
-
}
|
|
263
|
-
|
|
264
|
-
_textView.text = text;
|
|
265
|
-
|
|
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
|
-
}
|
|
278
|
-
|
|
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
243
|
|
|
296
|
-
|
|
297
|
-
|
|
244
|
+
private fun parseColor(colorString: String): Int {
|
|
245
|
+
return try {
|
|
246
|
+
Color.parseColor(colorString)
|
|
247
|
+
} catch (e: IllegalArgumentException) {
|
|
248
|
+
Log.e(TAG, "Invalid color: $colorString, using yellow")
|
|
249
|
+
Color.YELLOW
|
|
298
250
|
}
|
|
299
|
-
|
|
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;
|
|
305
251
|
}
|
|
306
|
-
}
|
|
307
|
-
|
|
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
252
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
_highlightColors[@(index)] = color;
|
|
253
|
+
private fun sendSelectionEvent(selectedText: String, eventType: String) {
|
|
254
|
+
try {
|
|
255
|
+
val reactContext = context as? ReactContext ?: return
|
|
256
|
+
val event = Arguments.createMap().apply {
|
|
257
|
+
putString("selectedText", selectedText)
|
|
258
|
+
putString("event", eventType)
|
|
322
259
|
}
|
|
323
|
-
}
|
|
324
260
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
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);
|
|
261
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
262
|
+
.receiveEvent(id, "onSelection", event)
|
|
263
|
+
} catch (e: Exception) {
|
|
264
|
+
Log.e(TAG, "Error sending selection event", e)
|
|
341
265
|
}
|
|
342
|
-
_menuOptions = [menuArray copy];
|
|
343
|
-
} @catch (NSException *exception) {
|
|
344
|
-
NSLog(@"[AdvancedTextView] Exception in updateMenuOptions: %@", exception);
|
|
345
|
-
@throw;
|
|
346
266
|
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
- (void)updateTextAppearance
|
|
350
|
-
{
|
|
351
|
-
@try {
|
|
352
|
-
if (!_textView.text || _textView.text.length == 0) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
|
|
357
|
-
initWithString:_textView.text];
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
UIFont *font = nil;
|
|
361
|
-
|
|
362
|
-
if (_fontFamily && _fontFamily.length > 0) {
|
|
363
|
-
font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
|
|
364
|
-
}
|
|
365
|
-
|
|
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
|
-
}
|
|
375
|
-
|
|
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
267
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
268
|
+
private inner class WordClickableSpan(
|
|
269
|
+
private val wordIndex: Int,
|
|
270
|
+
private val word: String
|
|
271
|
+
) : ClickableSpan() {
|
|
391
272
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
UIColor *highlightColor = _highlightColors[index];
|
|
397
|
-
if (highlightColor) {
|
|
398
|
-
[attributedString addAttribute:NSBackgroundColorAttributeName
|
|
399
|
-
value:highlightColor
|
|
400
|
-
range:range];
|
|
401
|
-
}
|
|
402
|
-
|
|
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
|
-
}
|
|
273
|
+
override fun onClick(widget: View) {
|
|
274
|
+
Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
|
|
275
|
+
sendWordPressEvent(word, wordIndex)
|
|
409
276
|
}
|
|
410
277
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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;
|
|
278
|
+
override fun updateDrawState(ds: TextPaint) {
|
|
279
|
+
super.updateDrawState(ds)
|
|
280
|
+
ds.isUnderlineText = false
|
|
281
|
+
ds.color = Color.parseColor(textColor)
|
|
423
282
|
}
|
|
424
|
-
|
|
425
|
-
} @catch (NSException *exception) {
|
|
426
|
-
NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
|
|
427
283
|
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
284
|
|
|
431
|
-
|
|
432
|
-
{
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
_textView.selectedTextRange = nil;
|
|
440
|
-
|
|
441
|
-
if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
|
|
442
|
-
NSDictionary *wordInfo = _wordRanges[wordIndex];
|
|
443
|
-
NSString *word = wordInfo[@"word"];
|
|
285
|
+
private fun sendWordPressEvent(word: String, index: Int) {
|
|
286
|
+
try {
|
|
287
|
+
val reactContext = context as? ReactContext ?: return
|
|
288
|
+
val event = Arguments.createMap().apply {
|
|
289
|
+
putString("word", word)
|
|
290
|
+
putInt("index", index)
|
|
291
|
+
}
|
|
444
292
|
|
|
445
|
-
|
|
293
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
294
|
+
.receiveEvent(id, "onWordPress", event)
|
|
295
|
+
} catch (e: Exception) {
|
|
296
|
+
Log.e(TAG, "Error sending word press event", e)
|
|
446
297
|
}
|
|
447
|
-
} @catch (NSException *exception) {
|
|
448
|
-
NSLog(@"[AdvancedTextView] Exception in handleTap: %@", exception);
|
|
449
298
|
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
#pragma mark - UITextViewDelegate
|
|
453
|
-
|
|
454
|
-
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
455
|
-
{
|
|
456
|
-
NSString *selectedText = [textView.text substringWithRange:textView.selectedRange];
|
|
457
299
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (_menuOptions && _menuOptions.count > 0) {
|
|
462
|
-
[self setupCustomMenuItems];
|
|
300
|
+
fun clearSelection() {
|
|
301
|
+
(text as? android.text.Spannable)?.let {
|
|
302
|
+
Selection.removeSelection(it)
|
|
463
303
|
}
|
|
464
304
|
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
- (void)setupCustomMenuItems
|
|
468
|
-
{
|
|
469
|
-
NSLog(@"[AdvancedTextView] Setting up %lu custom menu items", (unsigned long)_menuOptions.count);
|
|
470
|
-
|
|
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);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
menuController.menuItems = customItems;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
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
305
|
|
|
506
|
-
point.x -= _textView.textContainerInset.left;
|
|
507
|
-
point.y -= _textView.textContainerInset.top;
|
|
508
306
|
|
|
509
|
-
|
|
510
|
-
NSTextContainer *textContainer = _textView.textContainer;
|
|
307
|
+
private object SmartMovementMethod : ArrowKeyMovementMethod() {
|
|
511
308
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
NSValue *rangeValue = wordInfo[@"range"];
|
|
518
|
-
NSRange range = [rangeValue rangeValue];
|
|
519
|
-
|
|
520
|
-
if (NSLocationInRange(characterIndex, range)) {
|
|
521
|
-
return [wordInfo[@"index"] integerValue];
|
|
309
|
+
override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
|
|
310
|
+
if (event != null && widget != null && buffer != null) {
|
|
311
|
+
if (handleMotion(event, widget, buffer)) {
|
|
312
|
+
return true
|
|
313
|
+
}
|
|
522
314
|
}
|
|
315
|
+
return super.onTouchEvent(widget, buffer, event)
|
|
523
316
|
}
|
|
524
317
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return -1;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
- (void)setFontSize:(CGFloat)fontSize {
|
|
532
|
-
_fontSize = fontSize;
|
|
533
|
-
[self updateTextAppearance];
|
|
534
|
-
}
|
|
318
|
+
private fun handleMotion(event: MotionEvent, widget: TextView, buffer: Spannable): Boolean {
|
|
319
|
+
var handled = false
|
|
535
320
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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);
|
|
568
|
-
}
|
|
569
|
-
} @catch (NSException *exception) {
|
|
570
|
-
NSLog(@"[AdvancedTextView] Exception in emitWordPressEvent: %@", exception);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
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);
|
|
321
|
+
if (event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
|
|
322
|
+
val target = Point().apply {
|
|
323
|
+
x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX
|
|
324
|
+
y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY
|
|
325
|
+
}
|
|
580
326
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
event.event = [option UTF8String];
|
|
327
|
+
val line = widget.layout.getLineForVertical(target.y)
|
|
328
|
+
val offset = widget.layout.getOffsetForHorizontal(line, target.x.toFloat())
|
|
584
329
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
330
|
+
if (event.action == MotionEvent.ACTION_DOWN) {
|
|
331
|
+
handled = handled || buffer.execute<ClickableSpan>(offset) {
|
|
332
|
+
Selection.setSelection(buffer, buffer.getSpanStart(it), buffer.getSpanEnd(it))
|
|
333
|
+
}
|
|
334
|
+
}
|
|
591
335
|
|
|
592
|
-
|
|
593
|
-
{
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
NSLog(@"[AdvancedTextView] Exception in layoutSubviews: %@", exception);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
336
|
+
if (event.action == MotionEvent.ACTION_UP) {
|
|
337
|
+
handled = handled || buffer.execute<ClickableSpan>(offset) {
|
|
338
|
+
it.onClick(widget)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
601
342
|
|
|
602
|
-
|
|
603
|
-
{
|
|
604
|
-
@try {
|
|
605
|
-
if (!stringToConvert || [stringToConvert length] == 0) {
|
|
606
|
-
return nil;
|
|
343
|
+
return handled
|
|
607
344
|
}
|
|
608
345
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
346
|
+
private inline fun <reified T : Any> Spannable.execute(offset: Int, fn: (T) -> Unit): Boolean {
|
|
347
|
+
val spans = this.getSpans<T>(offset, offset)
|
|
348
|
+
if (spans.isNotEmpty()) {
|
|
349
|
+
spans.forEach(fn)
|
|
350
|
+
return true
|
|
351
|
+
}
|
|
352
|
+
return false
|
|
615
353
|
}
|
|
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;
|
|
624
354
|
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
Class<RCTComponentViewProtocol> AdvancedTextViewCls(void)
|
|
628
|
-
{
|
|
629
|
-
return AdvancedTextView.class;
|
|
630
|
-
}
|
|
631
355
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
356
|
+
data class WordPosition(
|
|
357
|
+
val index: Int,
|
|
358
|
+
val start: Int,
|
|
359
|
+
val end: Int,
|
|
360
|
+
val word: String
|
|
361
|
+
)
|
|
635
362
|
}
|
|
636
363
|
|
|
637
|
-
|
|
364
|
+
data class HighlightedWord(
|
|
365
|
+
val index: Int,
|
|
366
|
+
val highlightColor: String
|
|
367
|
+
)
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
using namespace facebook::react;
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
// Forward declaration
|
|
14
13
|
@class AdvancedTextView;
|
|
15
14
|
|
|
16
15
|
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
|
|
@@ -19,14 +18,17 @@ using namespace facebook::react;
|
|
|
19
18
|
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *highlightColors;
|
|
20
19
|
@property (nonatomic, strong) NSArray<NSString *> *menuOptions;
|
|
21
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;
|
|
22
26
|
|
|
23
|
-
// ✅ ADD THIS LINE
|
|
24
27
|
- (void)handleCustomMenuAction:(UIMenuItem *)sender;
|
|
25
28
|
|
|
26
29
|
@end
|
|
27
30
|
|
|
28
31
|
|
|
29
|
-
// Custom UITextView subclass to override menu behavior
|
|
30
32
|
@interface CustomTextView : UITextView
|
|
31
33
|
@property (nonatomic, weak) AdvancedTextView *parentView;
|
|
32
34
|
@end
|
|
@@ -37,22 +39,18 @@ using namespace facebook::react;
|
|
|
37
39
|
{
|
|
38
40
|
NSLog(@"[CustomTextView] canPerformAction: %@", NSStringFromSelector(action));
|
|
39
41
|
|
|
40
|
-
// Only allow our custom menu actions
|
|
41
42
|
if (action == @selector(handleCustomMenuAction:)) {
|
|
42
43
|
NSLog(@"[CustomTextView] ✅ Allowing custom action");
|
|
43
44
|
return YES;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
// Block ALL system actions
|
|
47
47
|
NSLog(@"[CustomTextView] ❌ Blocking system action: %@", NSStringFromSelector(action));
|
|
48
48
|
return NO;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
- (void)handleCustomMenuAction:(UIMenuItem *)sender
|
|
52
52
|
{
|
|
53
|
-
// Forward to parent view
|
|
54
53
|
if (self.parentView) {
|
|
55
|
-
|
|
56
54
|
[self.parentView handleCustomMenuAction:sender];
|
|
57
55
|
}
|
|
58
56
|
}
|
|
@@ -86,6 +84,11 @@ using namespace facebook::react;
|
|
|
86
84
|
_wordRanges = [NSMutableArray array];
|
|
87
85
|
_highlightColors = [NSMutableDictionary dictionary];
|
|
88
86
|
_indicatorWordIndex = -1;
|
|
87
|
+
_fontSize = 16.0;
|
|
88
|
+
_fontWeight = @"normal";
|
|
89
|
+
_textColor = [UIColor labelColor];
|
|
90
|
+
_textAlign = @"left";
|
|
91
|
+
_fontFamily = @"System";
|
|
89
92
|
|
|
90
93
|
[self setupTextView];
|
|
91
94
|
[self setupGestureRecognizers];
|
|
@@ -127,7 +130,6 @@ using namespace facebook::react;
|
|
|
127
130
|
{
|
|
128
131
|
NSLog(@"[AdvancedTextView] setupGestureRecognizers called");
|
|
129
132
|
@try {
|
|
130
|
-
// Single tap for word selection
|
|
131
133
|
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
|
|
132
134
|
initWithTarget:self
|
|
133
135
|
action:@selector(handleTap:)];
|
|
@@ -152,14 +154,44 @@ using namespace facebook::react;
|
|
|
152
154
|
BOOL highlightsChanged = NO;
|
|
153
155
|
BOOL menuChanged = NO;
|
|
154
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
|
+
}
|
|
155
189
|
|
|
156
|
-
// Check text change
|
|
157
190
|
if (oldViewProps.text != newViewProps.text) {
|
|
158
191
|
textChanged = YES;
|
|
159
192
|
NSLog(@"[AdvancedTextView] Text changed");
|
|
160
193
|
}
|
|
161
194
|
|
|
162
|
-
// Check highlighted words change
|
|
163
195
|
if (oldViewProps.highlightedWords.size() != newViewProps.highlightedWords.size()) {
|
|
164
196
|
highlightsChanged = YES;
|
|
165
197
|
} else {
|
|
@@ -173,7 +205,6 @@ using namespace facebook::react;
|
|
|
173
205
|
}
|
|
174
206
|
}
|
|
175
207
|
|
|
176
|
-
// Check menu options change
|
|
177
208
|
if (oldViewProps.menuOptions.size() != newViewProps.menuOptions.size()) {
|
|
178
209
|
menuChanged = YES;
|
|
179
210
|
} else {
|
|
@@ -185,12 +216,10 @@ using namespace facebook::react;
|
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
|
|
188
|
-
// Check indicator change
|
|
189
219
|
if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
|
|
190
220
|
indicatorChanged = YES;
|
|
191
221
|
}
|
|
192
222
|
|
|
193
|
-
// Apply updates
|
|
194
223
|
if (textChanged) {
|
|
195
224
|
NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
|
|
196
225
|
[self updateTextContent:text];
|
|
@@ -209,6 +238,11 @@ using namespace facebook::react;
|
|
|
209
238
|
[self updateTextAppearance];
|
|
210
239
|
}
|
|
211
240
|
|
|
241
|
+
if (styleChanged) {
|
|
242
|
+
NSLog(@"[AdvancedTextView] Style properties changed, updating appearance");
|
|
243
|
+
[self updateTextAppearance];
|
|
244
|
+
}
|
|
245
|
+
|
|
212
246
|
[super updateProps:props oldProps:oldProps];
|
|
213
247
|
NSLog(@"[AdvancedTextView] updateProps completed successfully");
|
|
214
248
|
} @catch (NSException *exception) {
|
|
@@ -229,7 +263,6 @@ using namespace facebook::react;
|
|
|
229
263
|
|
|
230
264
|
_textView.text = text;
|
|
231
265
|
|
|
232
|
-
// Parse text into words and their ranges
|
|
233
266
|
[_wordRanges removeAllObjects];
|
|
234
267
|
|
|
235
268
|
NSRange searchRange = NSMakeRange(0, text.length);
|
|
@@ -237,7 +270,6 @@ using namespace facebook::react;
|
|
|
237
270
|
|
|
238
271
|
NSInteger wordIndex = 0;
|
|
239
272
|
while (searchRange.location < text.length) {
|
|
240
|
-
// Skip whitespace
|
|
241
273
|
while (searchRange.location < text.length &&
|
|
242
274
|
[whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
|
|
243
275
|
searchRange.location++;
|
|
@@ -246,7 +278,6 @@ using namespace facebook::react;
|
|
|
246
278
|
|
|
247
279
|
if (searchRange.location >= text.length) break;
|
|
248
280
|
|
|
249
|
-
// Find word end
|
|
250
281
|
NSUInteger wordStart = searchRange.location;
|
|
251
282
|
while (searchRange.location < text.length &&
|
|
252
283
|
![whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
|
|
@@ -318,21 +349,41 @@ using namespace facebook::react;
|
|
|
318
349
|
- (void)updateTextAppearance
|
|
319
350
|
{
|
|
320
351
|
@try {
|
|
321
|
-
if (
|
|
352
|
+
if (!_textView.text || _textView.text.length == 0) {
|
|
322
353
|
return;
|
|
323
354
|
}
|
|
324
355
|
|
|
325
356
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
|
|
326
357
|
initWithString:_textView.text];
|
|
327
358
|
|
|
359
|
+
|
|
360
|
+
UIFont *font = nil;
|
|
361
|
+
|
|
362
|
+
if (_fontFamily && _fontFamily.length > 0) {
|
|
363
|
+
font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
|
|
364
|
+
}
|
|
365
|
+
|
|
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
|
+
}
|
|
375
|
+
|
|
328
376
|
[attributedString addAttribute:NSFontAttributeName
|
|
329
|
-
value:
|
|
377
|
+
value:font
|
|
330
378
|
range:NSMakeRange(0, attributedString.length)];
|
|
331
379
|
|
|
380
|
+
|
|
381
|
+
UIColor *color = _textColor ?: [UIColor labelColor];
|
|
332
382
|
[attributedString addAttribute:NSForegroundColorAttributeName
|
|
333
|
-
value:
|
|
383
|
+
value:color
|
|
334
384
|
range:NSMakeRange(0, attributedString.length)];
|
|
335
385
|
|
|
386
|
+
|
|
336
387
|
for (NSDictionary *wordInfo in _wordRanges) {
|
|
337
388
|
NSNumber *index = wordInfo[@"index"];
|
|
338
389
|
NSValue *rangeValue = wordInfo[@"range"];
|
|
@@ -358,11 +409,25 @@ using namespace facebook::react;
|
|
|
358
409
|
}
|
|
359
410
|
|
|
360
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;
|
|
423
|
+
}
|
|
424
|
+
|
|
361
425
|
} @catch (NSException *exception) {
|
|
362
426
|
NSLog(@"[AdvancedTextView] Exception in updateTextAppearance: %@", exception.reason);
|
|
363
427
|
}
|
|
364
428
|
}
|
|
365
429
|
|
|
430
|
+
|
|
366
431
|
- (void)handleTap:(UITapGestureRecognizer *)gesture
|
|
367
432
|
{
|
|
368
433
|
@try {
|
|
@@ -371,7 +436,6 @@ using namespace facebook::react;
|
|
|
371
436
|
CGPoint location = [gesture locationInView:_textView];
|
|
372
437
|
NSInteger wordIndex = [self wordIndexAtPoint:location];
|
|
373
438
|
|
|
374
|
-
// Dismiss any existing selection
|
|
375
439
|
_textView.selectedTextRange = nil;
|
|
376
440
|
|
|
377
441
|
if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
|
|
@@ -464,6 +528,31 @@ using namespace facebook::react;
|
|
|
464
528
|
}
|
|
465
529
|
}
|
|
466
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
|
+
|
|
467
556
|
- (void)emitWordPressEvent:(NSString *)word index:(NSInteger)index
|
|
468
557
|
{
|
|
469
558
|
NSLog(@"[AdvancedTextView] emitWordPressEvent: %@ at index: %ld", word, (long)index);
|
package/package.json
CHANGED