react-native-advanced-text 0.1.17 → 0.1.19
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/android/src/main/java/com/advancedtext/AdvancedTextView.kt +269 -303
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +110 -95
- package/lib/module/AdvancedText.js +6 -1
- package/lib/module/AdvancedText.js.map +1 -1
- package/lib/module/AdvancedTextViewNativeComponent.ts +4 -4
- package/lib/typescript/src/AdvancedText.d.ts +13 -9
- package/lib/typescript/src/AdvancedText.d.ts.map +1 -1
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +2 -1
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +175 -175
- package/src/AdvancedText.tsx +24 -8
- package/src/AdvancedTextViewNativeComponent.ts +4 -4
|
@@ -1,303 +1,269 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import android.
|
|
5
|
-
import android.
|
|
6
|
-
import android.text.
|
|
7
|
-
import android.text.
|
|
8
|
-
import android.text.
|
|
9
|
-
import android.text.
|
|
10
|
-
import android.text.style.
|
|
11
|
-
import android.text.style.
|
|
12
|
-
import android.
|
|
13
|
-
import android.util.
|
|
14
|
-
import android.
|
|
15
|
-
import android.view.
|
|
16
|
-
import android.view.
|
|
17
|
-
import android.view.
|
|
18
|
-
import android.
|
|
19
|
-
import
|
|
20
|
-
import com.facebook.react.bridge.
|
|
21
|
-
import com.facebook.react.
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
private var
|
|
30
|
-
private var
|
|
31
|
-
private var
|
|
32
|
-
private var
|
|
33
|
-
private var customActionMode: ActionMode? = null
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
val
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
fun
|
|
111
|
-
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
val
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
val reactContext = context as? ReactContext ?: return
|
|
271
|
-
val event = Arguments.createMap().apply {
|
|
272
|
-
putString("word", word)
|
|
273
|
-
putInt("index", index)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
277
|
-
.receiveEvent(id, "onWordPress", event)
|
|
278
|
-
} catch (e: Exception) {
|
|
279
|
-
Log.e(TAG, "Error sending word press event", e)
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
fun clearSelection() {
|
|
284
|
-
Log.d(TAG, "clearSelection called")
|
|
285
|
-
val spannable = this.text as? android.text.Spannable ?: return
|
|
286
|
-
Selection.removeSelection(spannable)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
290
|
-
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
291
|
-
Log.d(TAG, "onMeasure: width=${measuredWidth}, height=${measuredHeight}")
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
295
|
-
super.onLayout(changed, left, top, right, bottom)
|
|
296
|
-
Log.d(TAG, "onLayout: changed=$changed, bounds=[$left,$top,$right,$bottom]")
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
data class HighlightedWord(
|
|
301
|
-
val index: Int,
|
|
302
|
-
val highlightColor: String
|
|
303
|
-
)
|
|
1
|
+
// File: AdvancedTextView.kt
|
|
2
|
+
package com.advancedtext
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.text.SpannableString
|
|
7
|
+
import android.text.Spanned
|
|
8
|
+
import android.text.TextPaint
|
|
9
|
+
import android.text.method.LinkMovementMethod
|
|
10
|
+
import android.text.style.ClickableSpan
|
|
11
|
+
import android.text.style.BackgroundColorSpan
|
|
12
|
+
import android.text.style.ForegroundColorSpan
|
|
13
|
+
import android.util.AttributeSet
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import android.view.ActionMode
|
|
16
|
+
import android.view.Menu
|
|
17
|
+
import android.view.MenuItem
|
|
18
|
+
import android.view.View
|
|
19
|
+
import android.widget.TextView
|
|
20
|
+
import com.facebook.react.bridge.Arguments
|
|
21
|
+
import com.facebook.react.bridge.ReactContext
|
|
22
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
23
|
+
import android.text.Selection
|
|
24
|
+
|
|
25
|
+
class AdvancedTextView : TextView {
|
|
26
|
+
|
|
27
|
+
private val TAG = "AdvancedTextView"
|
|
28
|
+
|
|
29
|
+
private var highlightedWords: List<HighlightedWord> = emptyList()
|
|
30
|
+
private var menuOptions: List<String> = emptyList()
|
|
31
|
+
private var indicatorWordIndex: Int = -1
|
|
32
|
+
private var lastSelectedText: String = ""
|
|
33
|
+
private var customActionMode: ActionMode? = null
|
|
34
|
+
private var currentText: String = ""
|
|
35
|
+
|
|
36
|
+
// Cache for word positions to avoid recalculating
|
|
37
|
+
private var wordPositions: List<WordPosition> = emptyList()
|
|
38
|
+
|
|
39
|
+
constructor(context: Context?) : super(context) { init() }
|
|
40
|
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
|
|
41
|
+
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
|
|
42
|
+
|
|
43
|
+
private fun init() {
|
|
44
|
+
Log.d(TAG, "AdvancedTextView initialized")
|
|
45
|
+
|
|
46
|
+
// Set default properties
|
|
47
|
+
textSize = 16f
|
|
48
|
+
setPadding(16, 16, 16, 16)
|
|
49
|
+
movementMethod = LinkMovementMethod.getInstance()
|
|
50
|
+
setTextIsSelectable(true)
|
|
51
|
+
|
|
52
|
+
customSelectionActionModeCallback = object : ActionMode.Callback {
|
|
53
|
+
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
54
|
+
customActionMode = mode
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
59
|
+
menu?.clear()
|
|
60
|
+
|
|
61
|
+
val selectionStart = selectionStart
|
|
62
|
+
val selectionEnd = selectionEnd
|
|
63
|
+
|
|
64
|
+
if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
|
|
65
|
+
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
66
|
+
|
|
67
|
+
menuOptions.forEachIndexed { index, option ->
|
|
68
|
+
menu?.add(0, index, index, option)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sendSelectionEvent(lastSelectedText, "selection")
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
|
78
|
+
item?.let {
|
|
79
|
+
val menuItemText = it.title.toString()
|
|
80
|
+
sendSelectionEvent(lastSelectedText, menuItemText)
|
|
81
|
+
mode?.finish()
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
88
|
+
customActionMode = null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun setAdvancedText(text: String) {
|
|
94
|
+
if (currentText == text) {
|
|
95
|
+
Log.d(TAG, "Text unchanged, skipping update")
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Log.d(TAG, "setAdvancedText: length=${text.length}")
|
|
100
|
+
currentText = text
|
|
101
|
+
calculateWordPositions(text)
|
|
102
|
+
updateTextWithHighlights()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fun setMenuOptions(menuOptions: List<String>) {
|
|
106
|
+
if (this.menuOptions == menuOptions) return
|
|
107
|
+
this.menuOptions = menuOptions
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
|
|
111
|
+
if (this.highlightedWords == highlightedWords) return
|
|
112
|
+
this.highlightedWords = highlightedWords
|
|
113
|
+
updateTextWithHighlights()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fun setIndicatorWordIndex(index: Int) {
|
|
117
|
+
if (this.indicatorWordIndex == index) return
|
|
118
|
+
this.indicatorWordIndex = index
|
|
119
|
+
updateTextWithHighlights()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun calculateWordPositions(text: String) {
|
|
123
|
+
if (text.isEmpty()) {
|
|
124
|
+
wordPositions = emptyList()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
val positions = mutableListOf<WordPosition>()
|
|
129
|
+
val regex = "\\S+".toRegex()
|
|
130
|
+
|
|
131
|
+
regex.findAll(text).forEachIndexed { index, match ->
|
|
132
|
+
positions.add(WordPosition(
|
|
133
|
+
index = index,
|
|
134
|
+
start = match.range.first,
|
|
135
|
+
end = match.range.last + 1,
|
|
136
|
+
word = match.value
|
|
137
|
+
))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
wordPositions = positions
|
|
141
|
+
Log.d(TAG, "Calculated ${wordPositions.size} word positions")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private fun updateTextWithHighlights() {
|
|
145
|
+
if (currentText.isEmpty()) {
|
|
146
|
+
Log.d(TAG, "No text available, skipping")
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
val spannableString = SpannableString(currentText)
|
|
151
|
+
|
|
152
|
+
// Apply spans efficiently
|
|
153
|
+
wordPositions.forEach { wordPos ->
|
|
154
|
+
// Apply highlights
|
|
155
|
+
highlightedWords.find { it.index == wordPos.index }?.let { highlightedWord ->
|
|
156
|
+
val color = parseColor(highlightedWord.highlightColor)
|
|
157
|
+
spannableString.setSpan(
|
|
158
|
+
BackgroundColorSpan(color),
|
|
159
|
+
wordPos.start,
|
|
160
|
+
wordPos.end,
|
|
161
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Apply indicator color
|
|
166
|
+
if (wordPos.index == indicatorWordIndex) {
|
|
167
|
+
spannableString.setSpan(
|
|
168
|
+
ForegroundColorSpan(Color.RED),
|
|
169
|
+
wordPos.start,
|
|
170
|
+
wordPos.end,
|
|
171
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Make words clickable
|
|
176
|
+
spannableString.setSpan(
|
|
177
|
+
WordClickableSpan(wordPos.index, wordPos.word),
|
|
178
|
+
wordPos.start,
|
|
179
|
+
wordPos.end,
|
|
180
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Use post to ensure UI thread and avoid layout issues
|
|
185
|
+
post {
|
|
186
|
+
setText(spannableString, BufferType.SPANNABLE)
|
|
187
|
+
Log.d(TAG, "Text updated with ${wordPositions.size} spans")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private fun parseColor(colorString: String): Int {
|
|
192
|
+
return try {
|
|
193
|
+
Color.parseColor(colorString)
|
|
194
|
+
} catch (e: IllegalArgumentException) {
|
|
195
|
+
Log.e(TAG, "Invalid color: $colorString, using yellow")
|
|
196
|
+
Color.YELLOW
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun sendSelectionEvent(selectedText: String, eventType: String) {
|
|
201
|
+
try {
|
|
202
|
+
val reactContext = context as? ReactContext ?: return
|
|
203
|
+
val event = Arguments.createMap().apply {
|
|
204
|
+
putString("selectedText", selectedText)
|
|
205
|
+
putString("event", eventType)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
209
|
+
.receiveEvent(id, "onSelection", event)
|
|
210
|
+
} catch (e: Exception) {
|
|
211
|
+
Log.e(TAG, "Error sending selection event", e)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private inner class WordClickableSpan(
|
|
216
|
+
private val wordIndex: Int,
|
|
217
|
+
private val word: String
|
|
218
|
+
) : ClickableSpan() {
|
|
219
|
+
|
|
220
|
+
override fun onClick(widget: View) {
|
|
221
|
+
Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
|
|
222
|
+
val spannable = widget as? TextView
|
|
223
|
+
spannable?.text?.let {
|
|
224
|
+
if (it is android.text.Spannable) {
|
|
225
|
+
Selection.removeSelection(it)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
sendWordPressEvent(word, wordIndex)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun updateDrawState(ds: TextPaint) {
|
|
232
|
+
super.updateDrawState(ds)
|
|
233
|
+
ds.isUnderlineText = false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun sendWordPressEvent(word: String, index: Int) {
|
|
238
|
+
try {
|
|
239
|
+
val reactContext = context as? ReactContext ?: return
|
|
240
|
+
val event = Arguments.createMap().apply {
|
|
241
|
+
putString("word", word)
|
|
242
|
+
putInt("index", index)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
246
|
+
.receiveEvent(id, "onWordPress", event)
|
|
247
|
+
} catch (e: Exception) {
|
|
248
|
+
Log.e(TAG, "Error sending word press event", e)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fun clearSelection() {
|
|
253
|
+
(text as? android.text.Spannable)?.let {
|
|
254
|
+
Selection.removeSelection(it)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
data class WordPosition(
|
|
259
|
+
val index: Int,
|
|
260
|
+
val start: Int,
|
|
261
|
+
val end: Int,
|
|
262
|
+
val word: String
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
data class HighlightedWord(
|
|
267
|
+
val index: Int,
|
|
268
|
+
val highlightColor: String
|
|
269
|
+
)
|