react-native-advanced-text 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,296 +1,303 @@
1
- package com.advancedtext
2
-
3
- import android.content.Context
4
- import android.graphics.Color
5
- import android.text.SpannableString
6
- import android.text.Spanned
7
- import android.text.TextPaint
8
- import android.text.method.LinkMovementMethod
9
- import android.text.style.ClickableSpan
10
- import android.text.style.BackgroundColorSpan
11
- import android.util.AttributeSet
12
- import android.util.Log
13
- import android.util.TypedValue
14
- import android.view.ActionMode
15
- import android.view.Menu
16
- import android.view.MenuItem
17
- import android.view.View
18
- import android.widget.TextView
19
- import com.facebook.react.bridge.Arguments
20
- import com.facebook.react.bridge.ReactContext
21
- import com.facebook.react.uimanager.events.RCTEventEmitter
22
- import android.text.Selection
23
-
24
- class AdvancedTextView : TextView {
25
-
26
- private val TAG = "AdvancedTextView"
27
-
28
- private var highlightedWords: List<HighlightedWord> = emptyList()
29
- private var menuOptions: List<String> = emptyList()
30
- private var indicatorWordIndex: Int = -1
31
- private var lastSelectedText: String = ""
32
- private var isSelectionEnabled: Boolean = true
33
- private var customActionMode: ActionMode? = null
34
-
35
- // Cache for base text color to avoid parsing multiple times
36
- private var baseTextColor: Int = Color.BLACK
37
-
38
- constructor(context: Context?) : super(context) { init() }
39
- constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
40
- constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
41
-
42
- private fun init() {
43
- Log.d(TAG, "AdvancedTextView initialized")
44
-
45
- // Set default text appearance
46
- textSize = 16f
47
- setPadding(16, 16, 16, 16)
48
- baseTextColor = currentTextColor
49
-
50
- movementMethod = LinkMovementMethod.getInstance()
51
- setTextIsSelectable(true)
52
-
53
- customSelectionActionModeCallback = object : ActionMode.Callback {
54
- override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
55
- Log.d(TAG, "onCreateActionMode triggered")
56
- customActionMode = mode
57
- return true
58
- }
59
-
60
- override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
61
- Log.d(TAG, "onPrepareActionMode triggered")
62
- menu?.clear()
63
-
64
- val selectionStart = selectionStart
65
- val selectionEnd = selectionEnd
66
-
67
- if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
68
- lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
69
- Log.d(TAG, "User selected text: '$lastSelectedText'")
70
-
71
- menuOptions.forEachIndexed { index, option ->
72
- menu?.add(0, index, index, option)
73
- }
74
-
75
- sendSelectionEvent(lastSelectedText, "selection")
76
- return true
77
- }
78
- return false
79
- }
80
-
81
- override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
82
- item?.let {
83
- val menuItemText = it.title.toString()
84
- Log.d(TAG, "Menu item clicked: $menuItemText")
85
- sendSelectionEvent(lastSelectedText, menuItemText)
86
- mode?.finish()
87
- return true
88
- }
89
- return false
90
- }
91
-
92
- override fun onDestroyActionMode(mode: ActionMode?) {
93
- Log.d(TAG, "onDestroyActionMode")
94
- customActionMode = null
95
- }
96
- }
97
- }
98
-
99
- fun setAdvancedText(text: String) {
100
- Log.d(TAG, "setAdvancedText: $text (length=${text.length})")
101
- super.setText(text, BufferType.SPANNABLE)
102
- updateTextWithHighlights()
103
- requestLayout()
104
- invalidate()
105
- }
106
-
107
- // Performance-optimized: Set base font size at view level
108
- fun setFontSize(size: Float) {
109
- Log.d(TAG, "setFontSize: $size")
110
- // Use SP units for accessibility
111
- setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
112
- }
113
-
114
- // Performance-optimized: Set base color at view level
115
- fun setTextColorProp(colorString: String?) {
116
- try {
117
- val color = if (colorString != null) {
118
- Color.parseColor(colorString)
119
- } else {
120
- Color.BLACK
121
- }
122
- Log.d(TAG, "setTextColorProp: $colorString -> $color")
123
- baseTextColor = color
124
- setTextColor(color)
125
- } catch (e: IllegalArgumentException) {
126
- Log.e(TAG, "Invalid color: $colorString, using black", e)
127
- baseTextColor = Color.BLACK
128
- setTextColor(Color.BLACK)
129
- }
130
- }
131
-
132
- fun setMenuOptions(menuOptions: List<String>) {
133
- Log.d(TAG, "setMenuOptions received: $menuOptions")
134
- this.menuOptions = menuOptions
135
- }
136
-
137
- fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
138
- Log.d(TAG, "setHighlightedWords received: $highlightedWords")
139
- this.highlightedWords = highlightedWords
140
- updateTextWithHighlights()
141
- }
142
-
143
- fun setIndicatorWordIndex(index: Int) {
144
- Log.d(TAG, "setIndicatorWordIndex received: $index")
145
- this.indicatorWordIndex = index
146
- updateTextWithHighlights()
147
- }
148
-
149
- private fun updateTextWithHighlights() {
150
- val textValue = this.text?.toString() ?: ""
151
- Log.d(TAG, "updateTextWithHighlights called")
152
-
153
- if (textValue.isEmpty()) {
154
- Log.d(TAG, "No text available, skipping")
155
- return
156
- }
157
-
158
- val spannableString = SpannableString(textValue)
159
- val words = textValue.split("\\s+".toRegex()).filter { it.isNotEmpty() }
160
-
161
- var currentIndex = 0
162
- words.forEachIndexed { wordIndex, word ->
163
- val wordStart = textValue.indexOf(word, currentIndex)
164
- if (wordStart >= 0) {
165
- val wordEnd = wordStart + word.length
166
-
167
- // Apply clickable span
168
- spannableString.setSpan(
169
- WordClickableSpan(wordIndex, word),
170
- wordStart,
171
- wordEnd,
172
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
173
- )
174
-
175
- // Apply background highlight
176
- highlightedWords.find { it.index == wordIndex }?.let { highlightedWord ->
177
- val color = try {
178
- Color.parseColor(highlightedWord.highlightColor)
179
- } catch (e: IllegalArgumentException) {
180
- Log.e(TAG, "Invalid color: ${highlightedWord.highlightColor}")
181
- Color.YELLOW
182
- }
183
-
184
- spannableString.setSpan(
185
- BackgroundColorSpan(color),
186
- wordStart,
187
- wordEnd,
188
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
189
- )
190
- }
191
-
192
- // Apply indicator span
193
- if (wordIndex == indicatorWordIndex) {
194
- spannableString.setSpan(
195
- IndicatorSpan(),
196
- wordStart,
197
- wordEnd,
198
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
199
- )
200
- }
201
-
202
- currentIndex = wordEnd
203
- }
204
- }
205
-
206
- setText(spannableString, BufferType.SPANNABLE)
207
- movementMethod = LinkMovementMethod.getInstance()
208
- }
209
-
210
- private fun sendSelectionEvent(selectedText: String, eventType: String) {
211
- Log.d(TAG, "sendSelectionEvent -> eventType='$eventType' selectedText='$selectedText'")
212
-
213
- try {
214
- val reactContext = context as? ReactContext ?: return
215
- val event = Arguments.createMap().apply {
216
- putString("selectedText", selectedText)
217
- putString("event", eventType)
218
- }
219
-
220
- reactContext.getJSModule(RCTEventEmitter::class.java)
221
- .receiveEvent(id, "onSelection", event)
222
- } catch (e: Exception) {
223
- Log.e(TAG, "Error sending selection event", e)
224
- }
225
- }
226
-
227
- private inner class WordClickableSpan(
228
- private val wordIndex: Int,
229
- private val word: String
230
- ) : ClickableSpan() {
231
-
232
- override fun onClick(widget: View) {
233
- Log.d(TAG, "WordClickableSpan onClick: '$word' (index=$wordIndex)")
234
- widget.post {
235
- sendWordPressEvent(word, wordIndex)
236
- }
237
- }
238
-
239
- override fun updateDrawState(ds: TextPaint) {
240
- // Preserve the base text color instead of forcing a color
241
- ds.color = baseTextColor
242
- ds.isUnderlineText = false
243
- ds.bgColor = Color.TRANSPARENT
244
- }
245
- }
246
-
247
- private inner class IndicatorSpan : ClickableSpan() {
248
- override fun onClick(widget: View) {
249
- Log.d(TAG, "IndicatorSpan clicked")
250
- }
251
-
252
- override fun updateDrawState(ds: TextPaint) {
253
- ds.color = baseTextColor
254
- ds.isFakeBoldText = true
255
- ds.isUnderlineText = false
256
- }
257
- }
258
-
259
- private fun sendWordPressEvent(word: String, index: Int) {
260
- Log.d(TAG, "sendWordPressEvent -> word='$word', index=$index")
261
-
262
- try {
263
- val reactContext = context as? ReactContext ?: return
264
- val event = Arguments.createMap().apply {
265
- putString("word", word)
266
- putInt("index", index)
267
- }
268
-
269
- reactContext.getJSModule(RCTEventEmitter::class.java)
270
- .receiveEvent(id, "onWordPress", event)
271
- } catch (e: Exception) {
272
- Log.e(TAG, "Error sending word press event", e)
273
- }
274
- }
275
-
276
- fun clearSelection() {
277
- Log.d(TAG, "clearSelection called")
278
- val spannable = this.text as? android.text.Spannable ?: return
279
- Selection.removeSelection(spannable)
280
- }
281
-
282
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
283
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
284
- Log.d(TAG, "onMeasure: width=${measuredWidth}, height=${measuredHeight}")
285
- }
286
-
287
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
288
- super.onLayout(changed, left, top, right, bottom)
289
- Log.d(TAG, "onLayout: changed=$changed")
290
- }
291
- }
292
-
293
- data class HighlightedWord(
294
- val index: Int,
295
- val highlightColor: String
296
- )
1
+ package com.advancedtext
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.text.SpannableString
6
+ import android.text.Spanned
7
+ import android.text.TextPaint
8
+ import android.text.method.LinkMovementMethod
9
+ import android.text.style.ClickableSpan
10
+ import android.text.style.BackgroundColorSpan
11
+ import android.text.style.ForegroundColorSpan
12
+ import android.util.AttributeSet
13
+ import android.util.Log
14
+ import android.view.ActionMode
15
+ import android.view.Menu
16
+ import android.view.MenuItem
17
+ import android.view.View
18
+ import android.widget.TextView
19
+ import com.facebook.react.bridge.Arguments
20
+ import com.facebook.react.bridge.ReactContext
21
+ import com.facebook.react.uimanager.events.RCTEventEmitter
22
+ import android.text.Selection
23
+
24
+ class AdvancedTextView : TextView {
25
+
26
+ private val TAG = "AdvancedTextView"
27
+
28
+ private var highlightedWords: List<HighlightedWord> = emptyList()
29
+ private var menuOptions: List<String> = emptyList()
30
+ private var indicatorWordIndex: Int = -1
31
+ private var lastSelectedText: String = ""
32
+ private var isSelectionEnabled: Boolean = true
33
+ private var customActionMode: ActionMode? = null
34
+
35
+ constructor(context: Context?) : super(context) { init() }
36
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
37
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
38
+
39
+ private fun init() {
40
+ Log.d(TAG, "AdvancedTextView initialized")
41
+
42
+ // Set default text appearance - DON'T set black color here
43
+ textSize = 16f
44
+ setPadding(16, 16, 16, 16)
45
+
46
+ movementMethod = LinkMovementMethod.getInstance()
47
+ setTextIsSelectable(true)
48
+
49
+ customSelectionActionModeCallback = object : ActionMode.Callback {
50
+ override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
51
+ Log.d(TAG, "onCreateActionMode triggered")
52
+ customActionMode = mode
53
+ return true
54
+ }
55
+
56
+ override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
57
+ Log.d(TAG, "onPrepareActionMode triggered")
58
+ menu?.clear()
59
+
60
+ val selectionStart = selectionStart
61
+ val selectionEnd = selectionEnd
62
+
63
+ if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
64
+ lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
65
+ Log.d(TAG, "User selected text: '$lastSelectedText'")
66
+ Log.d(TAG, "Menu options available: $menuOptions")
67
+
68
+ menuOptions.forEachIndexed { index, option ->
69
+ menu?.add(0, index, index, option)
70
+ }
71
+
72
+ sendSelectionEvent(lastSelectedText, "selection")
73
+ return true
74
+ }
75
+ return false
76
+ }
77
+
78
+ override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
79
+ item?.let {
80
+ val menuItemText = it.title.toString()
81
+ Log.d(TAG, "Menu item clicked: $menuItemText")
82
+ sendSelectionEvent(lastSelectedText, menuItemText)
83
+ mode?.finish()
84
+ return true
85
+ }
86
+ return false
87
+ }
88
+
89
+ override fun onDestroyActionMode(mode: ActionMode?) {
90
+ Log.d(TAG, "onDestroyActionMode")
91
+ customActionMode = null
92
+ }
93
+ }
94
+ }
95
+
96
+ fun setAdvancedText(text: String) {
97
+ Log.d(TAG, "setAdvancedText: $text (length=${text.length})")
98
+
99
+ // Set the text first
100
+ super.setText(text, BufferType.SPANNABLE)
101
+
102
+ // Then apply highlights
103
+ updateTextWithHighlights()
104
+
105
+ // Force layout update
106
+ requestLayout()
107
+ invalidate()
108
+ }
109
+
110
+ fun setMenuOptions(menuOptions: List<String>) {
111
+ Log.d(TAG, "setMenuOptions received from RN: $menuOptions")
112
+ this.menuOptions = menuOptions
113
+ }
114
+
115
+ fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
116
+ Log.d(TAG, "setHighlightedWords received from RN: $highlightedWords")
117
+ this.highlightedWords = highlightedWords
118
+ updateTextWithHighlights()
119
+ }
120
+
121
+ fun setIndicatorWordIndex(index: Int) {
122
+ Log.d(TAG, "setIndicatorWordIndex received: $index")
123
+ this.indicatorWordIndex = index
124
+ updateTextWithHighlights()
125
+ }
126
+
127
+ private fun updateTextWithHighlights() {
128
+ val textValue = this.text?.toString() ?: ""
129
+ Log.d(TAG, "updateTextWithHighlights called")
130
+ Log.d(TAG, "Current text: $textValue")
131
+ Log.d(TAG, "Highlighted words: $highlightedWords")
132
+ Log.d(TAG, "Indicator index: $indicatorWordIndex")
133
+
134
+ if (textValue.isEmpty()) {
135
+ Log.d(TAG, "No text available, skipping")
136
+ return
137
+ }
138
+
139
+ val spannableString = SpannableString(textValue)
140
+
141
+ // Split words while preserving spaces for accurate indexing
142
+ val words = textValue.split("\\s+".toRegex()).filter { it.isNotEmpty() }
143
+
144
+ var currentIndex = 0
145
+ words.forEachIndexed { wordIndex, word ->
146
+
147
+ // Find the actual position of the word in the text
148
+ val wordStart = textValue.indexOf(word, currentIndex)
149
+ if (wordStart >= 0) {
150
+ val wordEnd = wordStart + word.length
151
+
152
+ Log.d(TAG, "Processing word '$word' at position $wordStart-$wordEnd, index $wordIndex")
153
+
154
+ // Apply clickable span FIRST (this is important)
155
+ spannableString.setSpan(
156
+ WordClickableSpan(wordIndex, word),
157
+ wordStart,
158
+ wordEnd,
159
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
160
+ )
161
+
162
+ // Then apply background color for highlighted words
163
+ highlightedWords.find { it.index == wordIndex }?.let { highlightedWord ->
164
+ val color = try {
165
+ Color.parseColor(highlightedWord.highlightColor)
166
+ } catch (e: IllegalArgumentException) {
167
+ Log.e(TAG, "Invalid color: ${highlightedWord.highlightColor}, using yellow")
168
+ Color.YELLOW
169
+ }
170
+ Log.d(TAG, "Applying highlight to word '$word' at index $wordIndex with color ${highlightedWord.highlightColor}")
171
+
172
+ spannableString.setSpan(
173
+ BackgroundColorSpan(color),
174
+ wordStart,
175
+ wordEnd,
176
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
177
+ )
178
+ }
179
+
180
+ // Then apply indicator span
181
+ if (wordIndex == indicatorWordIndex) {
182
+ Log.d(TAG, "Applying indicator span to word '$word' at index $wordIndex")
183
+
184
+ spannableString.setSpan(
185
+ IndicatorSpan(),
186
+ wordStart,
187
+ wordEnd,
188
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
189
+ )
190
+ }
191
+
192
+ currentIndex = wordEnd
193
+ }
194
+ }
195
+
196
+ // Set the spannable text
197
+ setText(spannableString, BufferType.SPANNABLE)
198
+
199
+ // Ensure movement method is still set
200
+ movementMethod = LinkMovementMethod.getInstance()
201
+
202
+ Log.d(TAG, "Text updated with spans, total spans")
203
+ }
204
+
205
+
206
+
207
+ private fun onMenuItemClick(item: MenuItem, selectedText: String): Boolean {
208
+ val menuItemText = menuOptions[item.itemId]
209
+ Log.d(TAG, "onMenuItemClick: menuOption='$menuItemText', selectedText='$selectedText'")
210
+ sendSelectionEvent(selectedText, menuItemText)
211
+ return true
212
+ }
213
+
214
+ private fun sendSelectionEvent(selectedText: String, eventType: String) {
215
+ Log.d(TAG, "sendSelectionEvent -> eventType='$eventType' selectedText='$selectedText'")
216
+
217
+ try {
218
+ val reactContext = context as? ReactContext ?: return
219
+ val event = Arguments.createMap().apply {
220
+ putString("selectedText", selectedText)
221
+ putString("event", eventType)
222
+ }
223
+
224
+ reactContext.getJSModule(RCTEventEmitter::class.java)
225
+ .receiveEvent(id, "onSelection", event)
226
+ } catch (e: Exception) {
227
+ Log.e(TAG, "Error sending selection event", e)
228
+ }
229
+ }
230
+
231
+ private inner class WordClickableSpan(
232
+ private val wordIndex: Int,
233
+ private val word: String
234
+ ) : ClickableSpan() {
235
+
236
+ override fun onClick(widget: View) {
237
+ Log.d(TAG, "WordClickableSpan onClick triggered: '$word' (index=$wordIndex)")
238
+
239
+ // Small delay to ensure the click is processed
240
+ widget.post {
241
+ sendWordPressEvent(word, wordIndex)
242
+ }
243
+ }
244
+
245
+ override fun updateDrawState(ds: TextPaint) {
246
+ // Don't call super to avoid default link styling (blue color, underline)
247
+ // Keep the original text appearance
248
+
249
+ ds.isUnderlineText = false
250
+ ds.bgColor = Color.TRANSPARENT
251
+ }
252
+ }
253
+
254
+ private inner class IndicatorSpan : ClickableSpan() {
255
+ override fun onClick(widget: View) {
256
+ Log.d(TAG, "IndicatorSpan clicked (shouldn't trigger action)")
257
+ }
258
+
259
+ override fun updateDrawState(ds: TextPaint) {
260
+
261
+ ds.isFakeBoldText = true
262
+ ds.isUnderlineText = false
263
+ }
264
+ }
265
+
266
+ private fun sendWordPressEvent(word: String, index: Int) {
267
+ Log.d(TAG, "sendWordPressEvent -> word='$word', index=$index")
268
+
269
+ try {
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
+ )