react-native-advanced-text 0.1.5 → 0.1.7
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 +232 -63
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +2 -1
- package/lib/module/AdvancedTextViewNativeComponent.ts +3 -2
- package/lib/typescript/src/AdvancedText.d.ts +2 -2
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +2 -2
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AdvancedText.tsx +2 -2
- package/src/AdvancedTextViewNativeComponent.ts +3 -2
|
@@ -1,94 +1,263 @@
|
|
|
1
|
+
// File: AdvancedTextView.kt
|
|
1
2
|
package com.advancedtext
|
|
2
3
|
|
|
4
|
+
import android.content.Context
|
|
3
5
|
import android.graphics.Color
|
|
4
|
-
import android.
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.util.AttributeSet
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import android.view.ContextMenu
|
|
15
|
+
import android.view.MenuItem
|
|
16
|
+
import android.view.View
|
|
17
|
+
import android.widget.TextView
|
|
18
|
+
import com.facebook.react.bridge.Arguments
|
|
19
|
+
import com.facebook.react.bridge.ReactContext
|
|
20
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
21
|
+
import android.text.Selection
|
|
22
|
+
|
|
23
|
+
class AdvancedTextView : TextView, View.OnCreateContextMenuListener {
|
|
24
|
+
|
|
25
|
+
private val TAG = "AdvancedTextView"
|
|
26
|
+
|
|
27
|
+
private var highlightedWords: List<HighlightedWord> = emptyList()
|
|
28
|
+
private var menuOptions: List<String> = emptyList()
|
|
29
|
+
private var indicatorWordIndex: Int = -1
|
|
30
|
+
private var lastSelectedText: String = ""
|
|
31
|
+
private var isSelectionEnabled: Boolean = true
|
|
32
|
+
|
|
33
|
+
constructor(context: Context?) : super(context) { init() }
|
|
34
|
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
|
|
35
|
+
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
|
|
36
|
+
|
|
37
|
+
private fun init() {
|
|
38
|
+
Log.d(TAG, "AdvancedTextView initialized")
|
|
39
|
+
|
|
40
|
+
// Set default text appearance
|
|
41
|
+
setTextColor(Color.BLACK)
|
|
42
|
+
textSize = 16f
|
|
43
|
+
setPadding(16, 16, 16, 16)
|
|
44
|
+
|
|
45
|
+
movementMethod = LinkMovementMethod.getInstance()
|
|
46
|
+
setTextIsSelectable(true)
|
|
47
|
+
setOnCreateContextMenuListener(this)
|
|
48
|
+
|
|
49
|
+
// Ensure minimum height for visibility during debugging
|
|
50
|
+
minHeight = 100
|
|
21
51
|
}
|
|
22
52
|
|
|
23
|
-
|
|
24
|
-
|
|
53
|
+
fun setAdvancedText(text: String) {
|
|
54
|
+
Log.d(TAG, "setAdvancedText: $text (length=${text.length})")
|
|
55
|
+
this.text = text
|
|
56
|
+
updateTextWithHighlights()
|
|
57
|
+
// Force layout update
|
|
58
|
+
requestLayout()
|
|
59
|
+
invalidate()
|
|
25
60
|
}
|
|
26
61
|
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
fun setMenuOptions(menuOptions: List<String>) {
|
|
63
|
+
Log.d(TAG, "setMenuOptions received from RN: $menuOptions")
|
|
64
|
+
this.menuOptions = menuOptions
|
|
29
65
|
}
|
|
30
66
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
36
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
37
|
-
)
|
|
38
|
-
return view
|
|
67
|
+
fun setHighlightedWords(highlightedWords: List<HighlightedWord>) {
|
|
68
|
+
Log.d(TAG, "setHighlightedWords received from RN: $highlightedWords")
|
|
69
|
+
this.highlightedWords = highlightedWords
|
|
70
|
+
updateTextWithHighlights()
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
fun setIndicatorWordIndex(index: Int) {
|
|
74
|
+
Log.d(TAG, "setIndicatorWordIndex received: $index")
|
|
75
|
+
this.indicatorWordIndex = index
|
|
76
|
+
updateTextWithHighlights()
|
|
44
77
|
}
|
|
45
78
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
private fun updateTextWithHighlights() {
|
|
80
|
+
val textValue = this.text?.toString() ?: ""
|
|
81
|
+
Log.d(TAG, "updateTextWithHighlights called")
|
|
82
|
+
Log.d(TAG, "Current text: $textValue")
|
|
83
|
+
Log.d(TAG, "Highlighted words: $highlightedWords")
|
|
84
|
+
Log.d(TAG, "Indicator index: $indicatorWordIndex")
|
|
85
|
+
|
|
86
|
+
if (textValue.isEmpty()) {
|
|
87
|
+
Log.d(TAG, "No text available, skipping")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
val spannableString = SpannableString(textValue)
|
|
92
|
+
val words = textValue.split("\\s+".toRegex())
|
|
93
|
+
|
|
94
|
+
var currentIndex = 0
|
|
95
|
+
words.forEachIndexed { wordIndex, word ->
|
|
96
|
+
|
|
97
|
+
if (word.isNotEmpty()) {
|
|
98
|
+
val wordStart = textValue.indexOf(word, currentIndex)
|
|
99
|
+
if (wordStart >= 0) {
|
|
100
|
+
val wordEnd = wordStart + word.length
|
|
101
|
+
|
|
102
|
+
highlightedWords.find { it.index == wordIndex }?.let { highlightedWord ->
|
|
103
|
+
val color = try {
|
|
104
|
+
Color.parseColor(highlightedWord.highlightColor)
|
|
105
|
+
} catch (e: IllegalArgumentException) {
|
|
106
|
+
Log.e(TAG, "Invalid color: ${highlightedWord.highlightColor}, using yellow")
|
|
107
|
+
Color.YELLOW
|
|
108
|
+
}
|
|
109
|
+
Log.d(TAG, "Applying highlight to word '$word' at index $wordIndex with color ${highlightedWord.highlightColor}")
|
|
110
|
+
|
|
111
|
+
spannableString.setSpan(
|
|
112
|
+
BackgroundColorSpan(color),
|
|
113
|
+
wordStart,
|
|
114
|
+
wordEnd,
|
|
115
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (wordIndex == indicatorWordIndex) {
|
|
120
|
+
Log.d(TAG, "Applying indicator span to word '$word' at index $wordIndex")
|
|
121
|
+
|
|
122
|
+
spannableString.setSpan(
|
|
123
|
+
IndicatorSpan(),
|
|
124
|
+
wordStart,
|
|
125
|
+
wordEnd,
|
|
126
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
57
127
|
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// clickable span
|
|
131
|
+
spannableString.setSpan(
|
|
132
|
+
WordClickableSpan(wordIndex, word),
|
|
133
|
+
wordStart,
|
|
134
|
+
wordEnd,
|
|
135
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
58
136
|
)
|
|
137
|
+
|
|
138
|
+
currentIndex = wordEnd
|
|
59
139
|
}
|
|
60
140
|
}
|
|
61
141
|
}
|
|
62
|
-
|
|
142
|
+
|
|
143
|
+
setText(spannableString, BufferType.SPANNABLE)
|
|
144
|
+
Log.d(TAG, "Text updated with spans")
|
|
63
145
|
}
|
|
64
146
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
val
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
147
|
+
override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
|
|
148
|
+
val selectionStart = selectionStart
|
|
149
|
+
val selectionEnd = selectionEnd
|
|
150
|
+
|
|
151
|
+
Log.d(TAG, "onCreateContextMenu triggered. selectionStart=$selectionStart selectionEnd=$selectionEnd")
|
|
152
|
+
|
|
153
|
+
if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
|
|
154
|
+
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
155
|
+
|
|
156
|
+
Log.d(TAG, "User selected text: '$lastSelectedText'")
|
|
157
|
+
Log.d(TAG, "Menu options available: $menuOptions")
|
|
158
|
+
|
|
159
|
+
menu?.clear()
|
|
160
|
+
|
|
161
|
+
menuOptions.forEachIndexed { index, option ->
|
|
162
|
+
menu?.add(0, index, index, option)?.setOnMenuItemClickListener {
|
|
163
|
+
Log.d(TAG, "Menu item clicked: $option")
|
|
164
|
+
onMenuItemClick(it, lastSelectedText)
|
|
165
|
+
true
|
|
72
166
|
}
|
|
73
167
|
}
|
|
168
|
+
|
|
169
|
+
sendSelectionEvent(lastSelectedText, "selection")
|
|
74
170
|
}
|
|
75
|
-
view?.setMenuOptions(options)
|
|
76
171
|
}
|
|
77
172
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
173
|
+
private fun onMenuItemClick(item: MenuItem, selectedText: String): Boolean {
|
|
174
|
+
val menuItemText = menuOptions[item.itemId]
|
|
175
|
+
Log.d(TAG, "onMenuItemClick: menuOption='$menuItemText', selectedText='$selectedText'")
|
|
176
|
+
sendSelectionEvent(selectedText, menuItemText)
|
|
177
|
+
return true
|
|
81
178
|
}
|
|
82
179
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
180
|
+
private fun sendSelectionEvent(selectedText: String, eventType: String) {
|
|
181
|
+
Log.d(TAG, "sendSelectionEvent -> eventType='$eventType' selectedText='$selectedText'")
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
val reactContext = context as? ReactContext ?: return
|
|
185
|
+
val event = Arguments.createMap().apply {
|
|
186
|
+
putString("selectedText", selectedText)
|
|
187
|
+
putString("event", eventType)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
191
|
+
.receiveEvent(id, "onSelection", event)
|
|
192
|
+
} catch (e: Exception) {
|
|
193
|
+
Log.e(TAG, "Error sending selection event", e)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private inner class WordClickableSpan(
|
|
198
|
+
private val wordIndex: Int,
|
|
199
|
+
private val word: String
|
|
200
|
+
) : ClickableSpan() {
|
|
201
|
+
|
|
202
|
+
override fun onClick(widget: View) {
|
|
203
|
+
Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
|
|
204
|
+
sendWordPressEvent(word, wordIndex)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun updateDrawState(ds: TextPaint) {
|
|
208
|
+
super.updateDrawState(ds)
|
|
209
|
+
ds.color = currentTextColor
|
|
210
|
+
ds.isUnderlineText = false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private inner class IndicatorSpan : ClickableSpan() {
|
|
215
|
+
override fun onClick(widget: View) {
|
|
216
|
+
Log.d(TAG, "IndicatorSpan clicked (shouldn't trigger action)")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
override fun updateDrawState(ds: TextPaint) {
|
|
220
|
+
ds.color = Color.RED
|
|
221
|
+
ds.isFakeBoldText = true
|
|
222
|
+
ds.isUnderlineText = false
|
|
223
|
+
}
|
|
89
224
|
}
|
|
90
225
|
|
|
91
|
-
|
|
92
|
-
|
|
226
|
+
private fun sendWordPressEvent(word: String, index: Int) {
|
|
227
|
+
Log.d(TAG, "sendWordPressEvent -> word='$word', index=$index")
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
val reactContext = context as? ReactContext ?: return
|
|
231
|
+
val event = Arguments.createMap().apply {
|
|
232
|
+
putString("word", word)
|
|
233
|
+
putInt("index", index)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
237
|
+
.receiveEvent(id, "onWordPress", event)
|
|
238
|
+
} catch (e: Exception) {
|
|
239
|
+
Log.e(TAG, "Error sending word press event", e)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fun clearSelection() {
|
|
244
|
+
Log.d(TAG, "clearSelection called")
|
|
245
|
+
val spannable = this.text as? android.text.Spannable ?: return
|
|
246
|
+
Selection.removeSelection(spannable)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
250
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
251
|
+
Log.d(TAG, "onMeasure: width=${measuredWidth}, height=${measuredHeight}")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
255
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
256
|
+
Log.d(TAG, "onLayout: changed=$changed, bounds=[$left,$top,$right,$bottom]")
|
|
93
257
|
}
|
|
94
258
|
}
|
|
259
|
+
|
|
260
|
+
data class HighlightedWord(
|
|
261
|
+
val index: Int,
|
|
262
|
+
val highlightColor: String
|
|
263
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
// File: AdvancedTextViewManager.kt
|
|
2
|
+
// This should be the ONLY content in this file
|
|
1
3
|
package com.advancedtext
|
|
2
4
|
|
|
3
|
-
import android.graphics.Color
|
|
4
5
|
import android.view.ViewGroup
|
|
5
6
|
import com.facebook.react.bridge.ReadableArray
|
|
6
7
|
import com.facebook.react.module.annotations.ReactModule
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { codegenNativeComponent } from 'react-native';
|
|
2
|
-
import type {
|
|
2
|
+
import type { TextProps } from 'react-native';
|
|
3
3
|
// @ts-ignore
|
|
4
|
+
// eslint-disable-next-line prettier/prettier
|
|
4
5
|
import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
|
|
5
6
|
|
|
6
7
|
interface HighlightedWord {
|
|
@@ -8,7 +9,7 @@ interface HighlightedWord {
|
|
|
8
9
|
highlightColor: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
interface NativeProps extends
|
|
12
|
+
interface NativeProps extends TextProps {
|
|
12
13
|
text: string;
|
|
13
14
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
14
15
|
menuOptions?: ReadonlyArray<string>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { NativeSyntheticEvent,
|
|
1
|
+
import type { NativeSyntheticEvent, TextProps } from 'react-native';
|
|
2
2
|
interface HighlightedWord {
|
|
3
3
|
index: number;
|
|
4
4
|
highlightColor: string;
|
|
5
5
|
}
|
|
6
|
-
interface NativeProps extends
|
|
6
|
+
interface NativeProps extends TextProps {
|
|
7
7
|
text: string;
|
|
8
8
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
9
9
|
menuOptions?: ReadonlyArray<string>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TextProps } from 'react-native';
|
|
2
2
|
import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
3
|
interface HighlightedWord {
|
|
4
4
|
index: Int32;
|
|
5
5
|
highlightColor: string;
|
|
6
6
|
}
|
|
7
|
-
interface NativeProps extends
|
|
7
|
+
interface NativeProps extends TextProps {
|
|
8
8
|
text: string;
|
|
9
9
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
10
10
|
menuOptions?: ReadonlyArray<string>;
|
|
@@ -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;
|
|
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,EAAE,MAAM,2CAA2C,CAAC;AAE3F,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,WAAY,SAAQ,SAAS;IACrC,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,CAAA;KAAE,CAAC,CAAC;IACnD,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,kBAAkB,CAAC,EAAE,KAAK,CAAC;CAC5B;;AAED,wBAAuE"}
|
package/package.json
CHANGED
package/src/AdvancedText.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NativeSyntheticEvent,
|
|
1
|
+
import type { NativeSyntheticEvent, TextProps } from 'react-native';
|
|
2
2
|
import AdvancedTextViewNativeComponent from './AdvancedTextViewNativeComponent';
|
|
3
3
|
|
|
4
4
|
interface HighlightedWord {
|
|
@@ -6,7 +6,7 @@ interface HighlightedWord {
|
|
|
6
6
|
highlightColor: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
interface NativeProps extends
|
|
9
|
+
interface NativeProps extends TextProps {
|
|
10
10
|
text: string;
|
|
11
11
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
12
12
|
menuOptions?: ReadonlyArray<string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { codegenNativeComponent } from 'react-native';
|
|
2
|
-
import type {
|
|
2
|
+
import type { TextProps } from 'react-native';
|
|
3
3
|
// @ts-ignore
|
|
4
|
+
// eslint-disable-next-line prettier/prettier
|
|
4
5
|
import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
|
|
5
6
|
|
|
6
7
|
interface HighlightedWord {
|
|
@@ -8,7 +9,7 @@ interface HighlightedWord {
|
|
|
8
9
|
highlightColor: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
interface NativeProps extends
|
|
12
|
+
interface NativeProps extends TextProps {
|
|
12
13
|
text: string;
|
|
13
14
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
14
15
|
menuOptions?: ReadonlyArray<string>;
|