react-native-advanced-text 0.1.33 → 0.1.40
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 +7 -2
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +111 -32
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +20 -8
- package/ios/AdvancedTextView.mm +140 -46
- package/lib/module/AdvancedText.js +1 -3
- package/lib/module/AdvancedText.js.map +1 -1
- package/lib/module/AdvancedTextViewNativeComponent.ts +3 -1
- package/lib/typescript/src/AdvancedText.d.ts.map +1 -1
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +3 -1
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AdvancedText.tsx +0 -2
- package/src/AdvancedTextViewNativeComponent.ts +3 -1
package/README.md
CHANGED
|
@@ -14,12 +14,11 @@ npm install react-native-advanced-text
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
```js
|
|
17
|
-
import {
|
|
17
|
+
import { AdvancedText } from "react-native-advanced-text";
|
|
18
18
|
|
|
19
19
|
<AdvancedText
|
|
20
20
|
text={'This is an example of AdvancedText component. Tap on any word to see the event in action.'}
|
|
21
21
|
style={[styles.AdvancedText, { minHeight }]}
|
|
22
|
-
indicatorWordIndex={2}
|
|
23
22
|
onWordPress={(event) => {
|
|
24
23
|
console.log({event})
|
|
25
24
|
}}
|
|
@@ -28,9 +27,15 @@ import { AdvancedTextView } from "react-native-advanced-text";
|
|
|
28
27
|
console.log({event})
|
|
29
28
|
}}
|
|
30
29
|
highlightedWords={[
|
|
30
|
+
{
|
|
31
|
+
index: 2,
|
|
32
|
+
highlightColor: '#FFD60A',
|
|
33
|
+
borderRadius: 10,
|
|
34
|
+
},
|
|
31
35
|
{
|
|
32
36
|
index: 4,
|
|
33
37
|
highlightColor: '#6baeffb5',
|
|
38
|
+
borderRadius: 6,
|
|
34
39
|
},
|
|
35
40
|
]}
|
|
36
41
|
fontSize={24}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
package com.advancedtext
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
4
5
|
import android.graphics.Color
|
|
5
6
|
import android.graphics.Point
|
|
7
|
+
import android.graphics.RectF
|
|
6
8
|
import android.text.SpannableString
|
|
7
9
|
import android.text.Spannable
|
|
8
10
|
import android.text.Spanned
|
|
9
11
|
import android.text.TextPaint
|
|
10
12
|
import android.text.method.ArrowKeyMovementMethod
|
|
11
13
|
import android.text.style.ClickableSpan
|
|
12
|
-
import android.text.style.BackgroundColorSpan
|
|
13
|
-
import android.text.style.ForegroundColorSpan
|
|
14
14
|
import android.util.AttributeSet
|
|
15
15
|
import android.util.Log
|
|
16
16
|
import android.view.ActionMode
|
|
@@ -32,7 +32,6 @@ class AdvancedTextView : TextView {
|
|
|
32
32
|
|
|
33
33
|
private var highlightedWords: List<HighlightedWord> = emptyList()
|
|
34
34
|
private var menuOptions: List<String> = emptyList()
|
|
35
|
-
private var indicatorWordIndex: Int = -1
|
|
36
35
|
private var lastSelectedText: String = ""
|
|
37
36
|
private var customActionMode: ActionMode? = null
|
|
38
37
|
private var currentText: String = ""
|
|
@@ -43,6 +42,9 @@ class AdvancedTextView : TextView {
|
|
|
43
42
|
private var fontFamily: String = "sans-serif"
|
|
44
43
|
private var lineHeightMultiplier: Float = 1.0f
|
|
45
44
|
|
|
45
|
+
private var indicatorWordIndex: Int = -1
|
|
46
|
+
private var indicatorColor: String = ""
|
|
47
|
+
|
|
46
48
|
private var wordPositions: List<WordPosition> = emptyList()
|
|
47
49
|
|
|
48
50
|
constructor(context: Context?) : super(context) { init() }
|
|
@@ -157,18 +159,24 @@ class AdvancedTextView : TextView {
|
|
|
157
159
|
this.menuOptions = menuOptions
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
fun setIndicatorWordIndex(index: Int) {
|
|
163
|
+
if (indicatorWordIndex == index) return
|
|
164
|
+
indicatorWordIndex = index
|
|
165
|
+
invalidate()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fun setIndicatorColor(color: String) {
|
|
169
|
+
if (indicatorColor == color) return
|
|
170
|
+
indicatorColor = color
|
|
171
|
+
invalidate()
|
|
172
|
+
}
|
|
173
|
+
|
|
160
174
|
fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
|
|
161
175
|
if (this.highlightedWords == highlightedWords) return
|
|
162
176
|
this.highlightedWords = highlightedWords
|
|
163
177
|
updateTextWithHighlights()
|
|
164
178
|
}
|
|
165
179
|
|
|
166
|
-
fun setIndicatorWordIndex(index: Int) {
|
|
167
|
-
if (this.indicatorWordIndex == index) return
|
|
168
|
-
this.indicatorWordIndex = index
|
|
169
|
-
updateTextWithHighlights()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
180
|
private fun calculateWordPositions(text: String) {
|
|
173
181
|
if (text.isEmpty()) {
|
|
174
182
|
wordPositions = emptyList()
|
|
@@ -206,27 +214,12 @@ class AdvancedTextView : TextView {
|
|
|
206
214
|
val spannableString = SpannableString(currentText)
|
|
207
215
|
|
|
208
216
|
wordPositions.forEach { wordPos ->
|
|
209
|
-
highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
|
|
210
|
-
val color = parseColor(highlightedWord.highlightColor)
|
|
211
|
-
spannableString.setSpan(
|
|
212
|
-
BackgroundColorSpan(color),
|
|
213
|
-
wordPos.start,
|
|
214
|
-
wordPos.extendedEnd,
|
|
215
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (wordPos.index == indicatorWordIndex) {
|
|
220
|
-
spannableString.setSpan(
|
|
221
|
-
ForegroundColorSpan(Color.parseColor(textColor)),
|
|
222
|
-
wordPos.start,
|
|
223
|
-
wordPos.end,
|
|
224
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
217
|
spannableString.setSpan(
|
|
229
|
-
WordClickableSpan(
|
|
218
|
+
WordClickableSpan(
|
|
219
|
+
wordIndex = wordPos.index,
|
|
220
|
+
word = wordPos.word,
|
|
221
|
+
wordColor = parseColor(textColor)
|
|
222
|
+
),
|
|
230
223
|
wordPos.start,
|
|
231
224
|
wordPos.end,
|
|
232
225
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
@@ -258,6 +251,90 @@ class AdvancedTextView : TextView {
|
|
|
258
251
|
}
|
|
259
252
|
}
|
|
260
253
|
|
|
254
|
+
override fun onDraw(canvas: Canvas) {
|
|
255
|
+
drawHighlightedWordBackgrounds(canvas)
|
|
256
|
+
super.onDraw(canvas)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private fun drawHighlightedWordBackgrounds(canvas: Canvas) {
|
|
260
|
+
val currentLayout = layout ?: return
|
|
261
|
+
val density = resources.displayMetrics.density
|
|
262
|
+
val horizontalPadding = 6f * density
|
|
263
|
+
val verticalPadding = 2f * density
|
|
264
|
+
val originalColor = paint.color
|
|
265
|
+
|
|
266
|
+
highlightedWords.forEach { highlightedWord ->
|
|
267
|
+
val wordPos = wordPositions.find { it.index == highlightedWord.index } ?: return@forEach
|
|
268
|
+
val start = wordPos.start
|
|
269
|
+
val end = wordPos.extendedEnd.coerceAtMost(currentText.length)
|
|
270
|
+
|
|
271
|
+
if (start >= end) return@forEach
|
|
272
|
+
|
|
273
|
+
val startLine = currentLayout.getLineForOffset(start)
|
|
274
|
+
val safeEndOffset = (end - 1).coerceAtLeast(start)
|
|
275
|
+
val endLine = currentLayout.getLineForOffset(safeEndOffset)
|
|
276
|
+
|
|
277
|
+
paint.color = parseColor(highlightedWord.highlightColor)
|
|
278
|
+
|
|
279
|
+
for (line in startLine..endLine) {
|
|
280
|
+
val lineStartOffset = maxOf(start, currentLayout.getLineStart(line))
|
|
281
|
+
val lineEndOffset = minOf(end, currentLayout.getLineEnd(line))
|
|
282
|
+
if (lineStartOffset >= lineEndOffset) continue
|
|
283
|
+
|
|
284
|
+
val left = currentLayout.getPrimaryHorizontal(lineStartOffset)
|
|
285
|
+
val right = currentLayout.getPrimaryHorizontal(lineEndOffset)
|
|
286
|
+
val top = currentLayout.getLineTop(line).toFloat()
|
|
287
|
+
val bottom = currentLayout.getLineBottom(line).toFloat()
|
|
288
|
+
val rect = RectF(
|
|
289
|
+
totalPaddingLeft + left - horizontalPadding,
|
|
290
|
+
totalPaddingTop + top + verticalPadding,
|
|
291
|
+
totalPaddingLeft + right + horizontalPadding,
|
|
292
|
+
totalPaddingTop + bottom - verticalPadding
|
|
293
|
+
)
|
|
294
|
+
val radius = highlightedWord.borderRadius * density
|
|
295
|
+
canvas.drawRoundRect(rect, radius, radius, paint)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Draw indicator for indicatorWordIndex
|
|
300
|
+
if (indicatorWordIndex >= 0 && indicatorColor.isNotEmpty()) {
|
|
301
|
+
val wordPos = wordPositions.find { it.index == indicatorWordIndex }
|
|
302
|
+
if (wordPos != null) {
|
|
303
|
+
val start = wordPos.start
|
|
304
|
+
val end = wordPos.extendedEnd.coerceAtMost(currentText.length)
|
|
305
|
+
|
|
306
|
+
if (start < end) {
|
|
307
|
+
val startLine = currentLayout.getLineForOffset(start)
|
|
308
|
+
val safeEndOffset = (end - 1).coerceAtLeast(start)
|
|
309
|
+
val endLine = currentLayout.getLineForOffset(safeEndOffset)
|
|
310
|
+
|
|
311
|
+
paint.color = parseColor(indicatorColor)
|
|
312
|
+
|
|
313
|
+
for (line in startLine..endLine) {
|
|
314
|
+
val lineStartOffset = maxOf(start, currentLayout.getLineStart(line))
|
|
315
|
+
val lineEndOffset = minOf(end, currentLayout.getLineEnd(line))
|
|
316
|
+
if (lineStartOffset >= lineEndOffset) continue
|
|
317
|
+
|
|
318
|
+
val left = currentLayout.getPrimaryHorizontal(lineStartOffset)
|
|
319
|
+
val right = currentLayout.getPrimaryHorizontal(lineEndOffset)
|
|
320
|
+
val top = currentLayout.getLineTop(line).toFloat()
|
|
321
|
+
val bottom = currentLayout.getLineBottom(line).toFloat()
|
|
322
|
+
val rect = RectF(
|
|
323
|
+
totalPaddingLeft + left - horizontalPadding,
|
|
324
|
+
totalPaddingTop + top + verticalPadding,
|
|
325
|
+
totalPaddingLeft + right + horizontalPadding,
|
|
326
|
+
totalPaddingTop + bottom - verticalPadding
|
|
327
|
+
)
|
|
328
|
+
val radius = 4f * density
|
|
329
|
+
canvas.drawRoundRect(rect, radius, radius, paint)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
paint.color = originalColor
|
|
336
|
+
}
|
|
337
|
+
|
|
261
338
|
private fun parseColor(colorString: String): Int {
|
|
262
339
|
return try {
|
|
263
340
|
Color.parseColor(colorString)
|
|
@@ -284,7 +361,8 @@ class AdvancedTextView : TextView {
|
|
|
284
361
|
|
|
285
362
|
private inner class WordClickableSpan(
|
|
286
363
|
private val wordIndex: Int,
|
|
287
|
-
private val word: String
|
|
364
|
+
private val word: String,
|
|
365
|
+
private val wordColor: Int
|
|
288
366
|
) : ClickableSpan() {
|
|
289
367
|
|
|
290
368
|
override fun onClick(widget: View) {
|
|
@@ -295,7 +373,7 @@ class AdvancedTextView : TextView {
|
|
|
295
373
|
override fun updateDrawState(ds: TextPaint) {
|
|
296
374
|
super.updateDrawState(ds)
|
|
297
375
|
ds.isUnderlineText = false
|
|
298
|
-
ds.color =
|
|
376
|
+
ds.color = wordColor
|
|
299
377
|
}
|
|
300
378
|
}
|
|
301
379
|
|
|
@@ -381,5 +459,6 @@ class AdvancedTextView : TextView {
|
|
|
381
459
|
|
|
382
460
|
data class HighlightedWord(
|
|
383
461
|
val index: Int,
|
|
384
|
-
val highlightColor: String
|
|
462
|
+
val highlightColor: String,
|
|
463
|
+
val borderRadius: Float = 0f
|
|
385
464
|
)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
package com.advancedtext
|
|
3
3
|
|
|
4
4
|
import android.graphics.Color
|
|
5
|
-
import android.util.TypedValue
|
|
6
5
|
import android.view.ViewGroup
|
|
7
6
|
import com.facebook.react.bridge.ReadableArray
|
|
8
7
|
import com.facebook.react.module.annotations.ReactModule
|
|
@@ -50,7 +49,12 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
50
49
|
words.add(
|
|
51
50
|
HighlightedWord(
|
|
52
51
|
index = wordMap.getInt("index"),
|
|
53
|
-
highlightColor = wordMap.getString("highlightColor") ?: "#FFFF00"
|
|
52
|
+
highlightColor = wordMap.getString("highlightColor") ?: "#FFFF00",
|
|
53
|
+
borderRadius = if (wordMap.hasKey("borderRadius")) {
|
|
54
|
+
wordMap.getDouble("borderRadius").toFloat()
|
|
55
|
+
} else {
|
|
56
|
+
0f
|
|
57
|
+
}
|
|
54
58
|
)
|
|
55
59
|
)
|
|
56
60
|
}
|
|
@@ -77,12 +81,6 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
77
81
|
view?.setMenuOptions(options)
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
@ReactProp(name = "indicatorWordIndex")
|
|
81
|
-
fun setIndicatorWordIndex(view: AdvancedTextView?, index: Int) {
|
|
82
|
-
android.util.Log.d(NAME, "setIndicatorWordIndex: $index")
|
|
83
|
-
view?.setIndicatorWordIndex(if (index >= 0) index else -1)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
84
|
@ReactProp(name = "color")
|
|
87
85
|
fun setColor(view: AdvancedTextView?, color: String?) {
|
|
88
86
|
android.util.Log.d(NAME, "setColor called with: $color")
|
|
@@ -124,6 +122,20 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
|
|
125
|
+
@ReactProp(name = "indicatorWordIndex")
|
|
126
|
+
fun setIndicatorWordIndex(view: AdvancedTextView?, indicatorWordIndex: Int) {
|
|
127
|
+
android.util.Log.d(NAME, "setIndicatorWordIndex called with: $indicatorWordIndex")
|
|
128
|
+
view?.setIndicatorWordIndex(indicatorWordIndex)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@ReactProp(name = "indicatorColor")
|
|
132
|
+
fun setIndicatorColor(view: AdvancedTextView?, indicatorColor: String?) {
|
|
133
|
+
android.util.Log.d(NAME, "setIndicatorColor called with: $indicatorColor")
|
|
134
|
+
if (indicatorColor != null) {
|
|
135
|
+
view?.setIndicatorColor(indicatorColor)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
@ReactProp(name = "lineHeight")
|
|
128
140
|
fun setLineHeight(view: AdvancedTextView?, lineHeight: Float) {
|
|
129
141
|
android.util.Log.d(NAME, "setLineHeight called with: $lineHeight")
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -15,17 +15,19 @@ using namespace facebook::react;
|
|
|
15
15
|
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
|
|
16
16
|
|
|
17
17
|
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *wordRanges;
|
|
18
|
-
@property (nonatomic, strong) NSMutableDictionary<NSNumber *,
|
|
18
|
+
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSDictionary *> *highlightStyles;
|
|
19
19
|
@property (nonatomic, strong) NSArray<NSString *> *menuOptions;
|
|
20
|
-
@property (nonatomic, assign) NSInteger indicatorWordIndex;
|
|
21
20
|
@property (nonatomic, assign) CGFloat fontSize;
|
|
22
21
|
@property (nonatomic, strong) NSString *fontWeight;
|
|
23
22
|
@property (nonatomic, strong) UIColor *textColor;
|
|
24
23
|
@property (nonatomic, strong) NSString *textAlign;
|
|
25
24
|
@property (nonatomic, strong) NSString *fontFamily;
|
|
26
25
|
@property (nonatomic, assign) CGFloat lineHeight;
|
|
26
|
+
@property (nonatomic, assign) NSInteger indicatorWordIndex;
|
|
27
|
+
@property (nonatomic, strong) UIColor *indicatorColor;
|
|
27
28
|
|
|
28
29
|
- (void)handleCustomMenuAction:(UIMenuItem *)sender;
|
|
30
|
+
- (void)drawHighlightedBackgroundsInTextView:(UITextView *)textView;
|
|
29
31
|
|
|
30
32
|
@end
|
|
31
33
|
|
|
@@ -56,6 +58,12 @@ using namespace facebook::react;
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
- (void)drawRect:(CGRect)rect
|
|
62
|
+
{
|
|
63
|
+
[self.parentView drawHighlightedBackgroundsInTextView:self];
|
|
64
|
+
[super drawRect:rect];
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
@end
|
|
60
68
|
|
|
61
69
|
|
|
@@ -83,14 +91,15 @@ using namespace facebook::react;
|
|
|
83
91
|
_props = defaultProps;
|
|
84
92
|
|
|
85
93
|
_wordRanges = [NSMutableArray array];
|
|
86
|
-
|
|
87
|
-
_indicatorWordIndex = -1;
|
|
94
|
+
_highlightStyles = [NSMutableDictionary dictionary];
|
|
88
95
|
_fontSize = 16.0;
|
|
89
96
|
_fontWeight = @"normal";
|
|
90
97
|
_textColor = [UIColor labelColor];
|
|
91
98
|
_textAlign = @"left";
|
|
92
99
|
_fontFamily = @"System";
|
|
93
100
|
_lineHeight = 0.0;
|
|
101
|
+
_indicatorWordIndex = -1;
|
|
102
|
+
_indicatorColor = nil;
|
|
94
103
|
|
|
95
104
|
[self setupTextView];
|
|
96
105
|
[self setupGestureRecognizers];
|
|
@@ -155,7 +164,6 @@ using namespace facebook::react;
|
|
|
155
164
|
BOOL textChanged = NO;
|
|
156
165
|
BOOL highlightsChanged = NO;
|
|
157
166
|
BOOL menuChanged = NO;
|
|
158
|
-
BOOL indicatorChanged = NO;
|
|
159
167
|
BOOL styleChanged = NO;
|
|
160
168
|
|
|
161
169
|
if (oldViewProps.fontSize != newViewProps.fontSize && newViewProps.fontSize) {
|
|
@@ -200,7 +208,9 @@ using namespace facebook::react;
|
|
|
200
208
|
for (size_t i = 0; i < oldViewProps.highlightedWords.size(); i++) {
|
|
201
209
|
const auto &oldHW = oldViewProps.highlightedWords[i];
|
|
202
210
|
const auto &newHW = newViewProps.highlightedWords[i];
|
|
203
|
-
if (oldHW.index != newHW.index ||
|
|
211
|
+
if (oldHW.index != newHW.index ||
|
|
212
|
+
oldHW.highlightColor != newHW.highlightColor ||
|
|
213
|
+
oldHW.borderRadius != newHW.borderRadius) {
|
|
204
214
|
highlightsChanged = YES;
|
|
205
215
|
break;
|
|
206
216
|
}
|
|
@@ -218,16 +228,29 @@ using namespace facebook::react;
|
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
230
|
|
|
221
|
-
if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
|
|
222
|
-
indicatorChanged = YES;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
231
|
if (oldViewProps.lineHeight != newViewProps.lineHeight) {
|
|
226
232
|
NSLog(@"[AdvancedTextView] Updating lineHeight to: %f", newViewProps.lineHeight);
|
|
227
233
|
_lineHeight = static_cast<CGFloat>(newViewProps.lineHeight);
|
|
228
234
|
styleChanged = YES;
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
|
|
238
|
+
NSLog(@"[AdvancedTextView] Updating indicatorWordIndex to: %d", newViewProps.indicatorWordIndex);
|
|
239
|
+
_indicatorWordIndex = static_cast<NSInteger>(newViewProps.indicatorWordIndex);
|
|
240
|
+
[_textView setNeedsDisplay];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (oldViewProps.indicatorColor != newViewProps.indicatorColor) {
|
|
244
|
+
NSLog(@"[AdvancedTextView] Updating indicatorColor");
|
|
245
|
+
if (!newViewProps.indicatorColor.empty()) {
|
|
246
|
+
NSString *colorStr = [NSString stringWithUTF8String:newViewProps.indicatorColor.c_str()];
|
|
247
|
+
_indicatorColor = [self hexStringToColor:colorStr];
|
|
248
|
+
} else {
|
|
249
|
+
_indicatorColor = nil;
|
|
250
|
+
}
|
|
251
|
+
[_textView setNeedsDisplay];
|
|
252
|
+
}
|
|
253
|
+
|
|
231
254
|
if (textChanged) {
|
|
232
255
|
NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
|
|
233
256
|
[self updateTextContent:text];
|
|
@@ -241,11 +264,6 @@ using namespace facebook::react;
|
|
|
241
264
|
[self updateMenuOptions:newViewProps.menuOptions];
|
|
242
265
|
}
|
|
243
266
|
|
|
244
|
-
if (indicatorChanged) {
|
|
245
|
-
_indicatorWordIndex = newViewProps.indicatorWordIndex;
|
|
246
|
-
[self updateTextAppearance];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
267
|
if (styleChanged) {
|
|
250
268
|
NSLog(@"[AdvancedTextView] Style properties changed, updating appearance");
|
|
251
269
|
[self updateTextAppearance];
|
|
@@ -277,9 +295,11 @@ using namespace facebook::react;
|
|
|
277
295
|
NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
278
296
|
|
|
279
297
|
NSInteger wordIndex = 0;
|
|
298
|
+
NSMutableArray *allMatches = [NSMutableArray array];
|
|
299
|
+
|
|
280
300
|
while (searchRange.location < text.length) {
|
|
281
301
|
while (searchRange.location < text.length &&
|
|
282
|
-
|
|
302
|
+
[whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
|
|
283
303
|
searchRange.location++;
|
|
284
304
|
searchRange.length = text.length - searchRange.location;
|
|
285
305
|
}
|
|
@@ -288,14 +308,14 @@ using namespace facebook::react;
|
|
|
288
308
|
|
|
289
309
|
NSUInteger wordStart = searchRange.location;
|
|
290
310
|
while (searchRange.location < text.length &&
|
|
291
|
-
|
|
311
|
+
![whitespaceSet characterIsMember:[text characterAtIndex:searchRange.location]]) {
|
|
292
312
|
searchRange.location++;
|
|
293
313
|
}
|
|
294
314
|
|
|
295
315
|
NSRange wordRange = NSMakeRange(wordStart, searchRange.location - wordStart);
|
|
296
316
|
NSString *word = [text substringWithRange:wordRange];
|
|
297
317
|
|
|
298
|
-
[
|
|
318
|
+
[allMatches addObject:@{
|
|
299
319
|
@"word": word,
|
|
300
320
|
@"range": [NSValue valueWithRange:wordRange],
|
|
301
321
|
@"index": @(wordIndex)
|
|
@@ -305,6 +325,25 @@ using namespace facebook::react;
|
|
|
305
325
|
searchRange.length = text.length - searchRange.location;
|
|
306
326
|
}
|
|
307
327
|
|
|
328
|
+
for (NSInteger i = 0; i < allMatches.count; i++) {
|
|
329
|
+
NSMutableDictionary *wordInfo = [allMatches[i] mutableCopy];
|
|
330
|
+
NSRange wordRange = [wordInfo[@"range"] rangeValue];
|
|
331
|
+
|
|
332
|
+
NSUInteger extendedEnd;
|
|
333
|
+
if (i + 1 < allMatches.count) {
|
|
334
|
+
NSRange nextRange = [allMatches[i + 1][@"range"] rangeValue];
|
|
335
|
+
extendedEnd = nextRange.location;
|
|
336
|
+
} else {
|
|
337
|
+
extendedEnd = text.length;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
NSRange extendedRange = NSMakeRange(wordRange.location, extendedEnd - wordRange.location);
|
|
341
|
+
wordInfo[@"extendedRange"] = [NSValue valueWithRange:extendedRange];
|
|
342
|
+
|
|
343
|
+
[_wordRanges addObject:[wordInfo copy]];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
308
347
|
NSLog(@"[AdvancedTextView] Parsed %ld words", (long)_wordRanges.count);
|
|
309
348
|
[self updateTextAppearance];
|
|
310
349
|
} @catch (NSException *exception) {
|
|
@@ -318,15 +357,19 @@ using namespace facebook::react;
|
|
|
318
357
|
NSLog(@"[AdvancedTextView] updateHighlightedWords called with %zu highlights",
|
|
319
358
|
highlightedWords.size());
|
|
320
359
|
@try {
|
|
321
|
-
[
|
|
360
|
+
[_highlightStyles removeAllObjects];
|
|
322
361
|
|
|
323
362
|
for (const auto &hw : highlightedWords) {
|
|
324
363
|
NSInteger index = hw.index;
|
|
325
364
|
NSString *colorString = [NSString stringWithUTF8String:hw.highlightColor.c_str()];
|
|
326
365
|
UIColor *color = [self hexStringToColor:colorString];
|
|
366
|
+
CGFloat borderRadius = static_cast<CGFloat>(hw.borderRadius);
|
|
327
367
|
|
|
328
368
|
if (color) {
|
|
329
|
-
|
|
369
|
+
_highlightStyles[@(index)] = @{
|
|
370
|
+
@"color": color,
|
|
371
|
+
@"borderRadius": @(MAX(borderRadius, 0.0))
|
|
372
|
+
};
|
|
330
373
|
}
|
|
331
374
|
}
|
|
332
375
|
|
|
@@ -382,7 +425,7 @@ using namespace facebook::react;
|
|
|
382
425
|
value:paragraphStyle
|
|
383
426
|
range:NSMakeRange(0, attributedString.length)];
|
|
384
427
|
}
|
|
385
|
-
|
|
428
|
+
|
|
386
429
|
UIFont *font = nil;
|
|
387
430
|
|
|
388
431
|
if (_fontFamily && _fontFamily.length > 0) {
|
|
@@ -409,32 +452,8 @@ using namespace facebook::react;
|
|
|
409
452
|
value:color
|
|
410
453
|
range:NSMakeRange(0, attributedString.length)];
|
|
411
454
|
|
|
412
|
-
|
|
413
|
-
for (NSDictionary *wordInfo in _wordRanges) {
|
|
414
|
-
NSNumber *index = wordInfo[@"index"];
|
|
415
|
-
NSValue *rangeValue = wordInfo[@"range"];
|
|
416
|
-
NSRange range = [rangeValue rangeValue];
|
|
417
|
-
|
|
418
|
-
if (range.location + range.length > attributedString.length) {
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
UIColor *highlightColor = _highlightColors[index];
|
|
423
|
-
if (highlightColor) {
|
|
424
|
-
[attributedString addAttribute:NSBackgroundColorAttributeName
|
|
425
|
-
value:highlightColor
|
|
426
|
-
range:range];
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (_indicatorWordIndex >= 0 && [index integerValue] == _indicatorWordIndex) {
|
|
430
|
-
UIColor *indicatorColor = [[UIColor systemBlueColor] colorWithAlphaComponent:0.3];
|
|
431
|
-
[attributedString addAttribute:NSBackgroundColorAttributeName
|
|
432
|
-
value:indicatorColor
|
|
433
|
-
range:range];
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
455
|
_textView.attributedText = attributedString;
|
|
456
|
+
[_textView setNeedsDisplay];
|
|
438
457
|
|
|
439
458
|
if (_textAlign) {
|
|
440
459
|
if ([_textAlign.lowercaseString isEqualToString:@"center"]) {
|
|
@@ -453,6 +472,81 @@ using namespace facebook::react;
|
|
|
453
472
|
}
|
|
454
473
|
}
|
|
455
474
|
|
|
475
|
+
- (void)drawHighlightedBackgroundsInTextView:(UITextView *)textView
|
|
476
|
+
{
|
|
477
|
+
if (!textView.layoutManager || !textView.textContainer) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
NSLayoutManager *layoutManager = textView.layoutManager;
|
|
482
|
+
NSTextContainer *textContainer = textView.textContainer;
|
|
483
|
+
|
|
484
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
485
|
+
NSNumber *index = wordInfo[@"index"];
|
|
486
|
+
NSDictionary *highlightStyle = _highlightStyles[index];
|
|
487
|
+
if (!highlightStyle) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
NSValue *extendedRangeValue = wordInfo[@"extendedRange"];
|
|
492
|
+
NSRange characterRange = extendedRangeValue ? [extendedRangeValue rangeValue] : [wordInfo[@"range"] rangeValue];
|
|
493
|
+
if (characterRange.location == NSNotFound || characterRange.length == 0) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
UIColor *highlightColor = highlightStyle[@"color"];
|
|
498
|
+
CGFloat radius = [highlightStyle[@"borderRadius"] doubleValue];
|
|
499
|
+
NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
|
|
500
|
+
|
|
501
|
+
[highlightColor setFill];
|
|
502
|
+
[layoutManager enumerateEnclosingRectsForGlyphRange:glyphRange
|
|
503
|
+
withinSelectedGlyphRange:NSMakeRange(NSNotFound, 0)
|
|
504
|
+
inTextContainer:textContainer
|
|
505
|
+
usingBlock:^(CGRect enclosingRect, BOOL *stop) {
|
|
506
|
+
CGRect adjustedRect = CGRectInset(enclosingRect, -6.0, -2.0);
|
|
507
|
+
adjustedRect.origin.x += textView.textContainerInset.left;
|
|
508
|
+
adjustedRect.origin.y += textView.textContainerInset.top;
|
|
509
|
+
|
|
510
|
+
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:adjustedRect
|
|
511
|
+
cornerRadius:MAX(radius, 0.0)];
|
|
512
|
+
[path fill];
|
|
513
|
+
}];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Draw indicator for indicatorWordIndex
|
|
517
|
+
if (_indicatorWordIndex >= 0 && _indicatorColor) {
|
|
518
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
519
|
+
NSNumber *index = wordInfo[@"index"];
|
|
520
|
+
if ([index integerValue] != _indicatorWordIndex) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
NSValue *extendedRangeValue = wordInfo[@"extendedRange"];
|
|
525
|
+
NSRange characterRange = extendedRangeValue ? [extendedRangeValue rangeValue] : [wordInfo[@"range"] rangeValue];
|
|
526
|
+
if (characterRange.location == NSNotFound || characterRange.length == 0) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
|
|
531
|
+
|
|
532
|
+
[_indicatorColor setFill];
|
|
533
|
+
[layoutManager enumerateEnclosingRectsForGlyphRange:glyphRange
|
|
534
|
+
withinSelectedGlyphRange:NSMakeRange(NSNotFound, 0)
|
|
535
|
+
inTextContainer:textContainer
|
|
536
|
+
usingBlock:^(CGRect enclosingRect, BOOL *stop) {
|
|
537
|
+
CGRect adjustedRect = CGRectInset(enclosingRect, -6.0, -2.0);
|
|
538
|
+
adjustedRect.origin.x += textView.textContainerInset.left;
|
|
539
|
+
adjustedRect.origin.y += textView.textContainerInset.top;
|
|
540
|
+
|
|
541
|
+
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:adjustedRect
|
|
542
|
+
cornerRadius:4.0];
|
|
543
|
+
[path fill];
|
|
544
|
+
}];
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
456
550
|
|
|
457
551
|
- (void)handleTap:(UITapGestureRecognizer *)gesture
|
|
458
552
|
{
|
|
@@ -10,7 +10,6 @@ export const AdvancedText = ({
|
|
|
10
10
|
menuOptions,
|
|
11
11
|
onWordPress,
|
|
12
12
|
onSelection,
|
|
13
|
-
indicatorWordIndex,
|
|
14
13
|
...restProps
|
|
15
14
|
}) => {
|
|
16
15
|
return /*#__PURE__*/_jsx(AdvancedTextViewNativeComponent, {
|
|
@@ -20,8 +19,7 @@ export const AdvancedText = ({
|
|
|
20
19
|
highlightedWords: highlightedWords,
|
|
21
20
|
menuOptions: menuOptions,
|
|
22
21
|
onWordPress: onWordPress,
|
|
23
|
-
onSelection: onSelection
|
|
24
|
-
indicatorWordIndex: indicatorWordIndex
|
|
22
|
+
onSelection: onSelection
|
|
25
23
|
});
|
|
26
24
|
};
|
|
27
25
|
//# sourceMappingURL=AdvancedText.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","AdvancedTextViewNativeComponent","jsx","_jsx","AdvancedText","text","style","highlightedWords","menuOptions","onWordPress","onSelection","
|
|
1
|
+
{"version":3,"names":["React","AdvancedTextViewNativeComponent","jsx","_jsx","AdvancedText","text","style","highlightedWords","menuOptions","onWordPress","onSelection","restProps"],"sourceRoot":"../../src","sources":["AdvancedText.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,+BAA+B,MAE/B,mCAAmC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE3C,OAAO,MAAMC,YAAmC,GAAGA,CAAC;EAClDC,IAAI;EACJC,KAAK;EACLC,gBAAgB;EAChBC,WAAW;EACXC,WAAW;EACXC,WAAW;EACX,GAAGC;AACL,CAAC,KAAK;EACJ,oBACER,IAAA,CAACF,+BAA+B;IAAA,GAC1BU,SAAS;IACbL,KAAK,EAAEA,KAAM;IACbD,IAAI,EAAEA,IAAK;IACXE,gBAAgB,EAAEA,gBAAiB;IACnCC,WAAW,EAAEA,WAAY;IACzBC,WAAW,EAAEA,WAAY;IACzBC,WAAW,EAAEA;EAAY,CAC1B,CAAC;AAEN,CAAC","ignoreList":[]}
|
|
@@ -7,6 +7,7 @@ import type { DirectEventHandler, Int32 , Float } from 'react-native/Libraries/
|
|
|
7
7
|
interface HighlightedWord {
|
|
8
8
|
index: Int32;
|
|
9
9
|
highlightColor: string;
|
|
10
|
+
borderRadius?: Float;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface NativeProps extends ViewProps {
|
|
@@ -15,13 +16,14 @@ export interface NativeProps extends ViewProps {
|
|
|
15
16
|
menuOptions?: ReadonlyArray<string>;
|
|
16
17
|
onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
17
18
|
onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
|
|
18
|
-
indicatorWordIndex?: Int32;
|
|
19
19
|
fontSize?: Int32;
|
|
20
20
|
fontWeight?: string;
|
|
21
21
|
color?: string;
|
|
22
22
|
textAlign?: string;
|
|
23
23
|
fontFamily?: string;
|
|
24
24
|
lineHeight?: Float;
|
|
25
|
+
indicatorWordIndex?: Int32;
|
|
26
|
+
indicatorColor?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export default codegenNativeComponent<NativeProps>('AdvancedTextView');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdvancedText.d.ts","sourceRoot":"","sources":["../../../src/AdvancedText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAwC,EACtC,KAAK,WAAW,EACjB,MAAM,mCAAmC,CAAC;AAE3C,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,
|
|
1
|
+
{"version":3,"file":"AdvancedText.d.ts","sourceRoot":"","sources":["../../../src/AdvancedText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAwC,EACtC,KAAK,WAAW,EACjB,MAAM,mCAAmC,CAAC;AAE3C,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAoB9C,CAAC"}
|
|
@@ -3,6 +3,7 @@ import type { DirectEventHandler, Int32, Float } from 'react-native/Libraries/Ty
|
|
|
3
3
|
interface HighlightedWord {
|
|
4
4
|
index: Int32;
|
|
5
5
|
highlightColor: string;
|
|
6
|
+
borderRadius?: Float;
|
|
6
7
|
}
|
|
7
8
|
export interface NativeProps extends ViewProps {
|
|
8
9
|
text: string;
|
|
@@ -16,13 +17,14 @@ export interface NativeProps extends ViewProps {
|
|
|
16
17
|
selectedText: string;
|
|
17
18
|
event: string;
|
|
18
19
|
}>;
|
|
19
|
-
indicatorWordIndex?: Int32;
|
|
20
20
|
fontSize?: Int32;
|
|
21
21
|
fontWeight?: string;
|
|
22
22
|
color?: string;
|
|
23
23
|
textAlign?: string;
|
|
24
24
|
fontFamily?: string;
|
|
25
25
|
lineHeight?: Float;
|
|
26
|
+
indicatorWordIndex?: Int32;
|
|
27
|
+
indicatorColor?: string;
|
|
26
28
|
}
|
|
27
29
|
declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
|
|
28
30
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;;AAED,wBAAuE"}
|
package/package.json
CHANGED
package/src/AdvancedText.tsx
CHANGED
|
@@ -10,7 +10,6 @@ export const AdvancedText: React.FC<NativeProps> = ({
|
|
|
10
10
|
menuOptions,
|
|
11
11
|
onWordPress,
|
|
12
12
|
onSelection,
|
|
13
|
-
indicatorWordIndex,
|
|
14
13
|
...restProps
|
|
15
14
|
}) => {
|
|
16
15
|
return (
|
|
@@ -22,7 +21,6 @@ export const AdvancedText: React.FC<NativeProps> = ({
|
|
|
22
21
|
menuOptions={menuOptions}
|
|
23
22
|
onWordPress={onWordPress}
|
|
24
23
|
onSelection={onSelection}
|
|
25
|
-
indicatorWordIndex={indicatorWordIndex}
|
|
26
24
|
/>
|
|
27
25
|
);
|
|
28
26
|
};
|
|
@@ -7,6 +7,7 @@ import type { DirectEventHandler, Int32 , Float } from 'react-native/Libraries/
|
|
|
7
7
|
interface HighlightedWord {
|
|
8
8
|
index: Int32;
|
|
9
9
|
highlightColor: string;
|
|
10
|
+
borderRadius?: Float;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface NativeProps extends ViewProps {
|
|
@@ -15,13 +16,14 @@ export interface NativeProps extends ViewProps {
|
|
|
15
16
|
menuOptions?: ReadonlyArray<string>;
|
|
16
17
|
onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
17
18
|
onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
|
|
18
|
-
indicatorWordIndex?: Int32;
|
|
19
19
|
fontSize?: Int32;
|
|
20
20
|
fontWeight?: string;
|
|
21
21
|
color?: string;
|
|
22
22
|
textAlign?: string;
|
|
23
23
|
fontFamily?: string;
|
|
24
24
|
lineHeight?: Float;
|
|
25
|
+
indicatorWordIndex?: Int32;
|
|
26
|
+
indicatorColor?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export default codegenNativeComponent<NativeProps>('AdvancedTextView');
|