react-native-advanced-text 0.1.17 → 0.1.18

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,303 +1,269 @@
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
- )
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
+ )