react-native-highlight-text-view 0.1.3 → 0.1.5
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
CHANGED
|
@@ -61,7 +61,7 @@ export default function App() {
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
**Note:** Vertical alignment is currently supported on iOS only. On Android, text will use default vertical positioning.
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
## Contributing
|
|
@@ -70,10 +70,7 @@ export default function App() {
|
|
|
70
70
|
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
71
71
|
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
72
72
|
|
|
73
|
-
## License
|
|
74
|
-
|
|
75
|
-
MIT
|
|
76
73
|
|
|
77
|
-
|
|
74
|
+
## License MIT
|
|
78
75
|
|
|
79
76
|
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -1,15 +1,299 @@
|
|
|
1
1
|
package com.highlighttext
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.text.Editable
|
|
9
|
+
import android.text.Spannable
|
|
10
|
+
import android.text.SpannableString
|
|
11
|
+
import android.text.TextWatcher
|
|
12
|
+
import android.text.style.ReplacementSpan
|
|
4
13
|
import android.util.AttributeSet
|
|
5
|
-
import android.
|
|
14
|
+
import android.util.TypedValue
|
|
15
|
+
import android.view.Gravity
|
|
16
|
+
import androidx.appcompat.widget.AppCompatEditText
|
|
6
17
|
|
|
7
|
-
class
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
class RoundedBackgroundSpan(
|
|
19
|
+
private val backgroundColor: Int,
|
|
20
|
+
private val textColor: Int,
|
|
21
|
+
private val paddingLeft: Float,
|
|
22
|
+
private val paddingRight: Float,
|
|
23
|
+
private val paddingTop: Float,
|
|
24
|
+
private val paddingBottom: Float,
|
|
25
|
+
private val cornerRadius: Float,
|
|
26
|
+
private val isFirstInGroup: Boolean = false,
|
|
27
|
+
private val isLastInGroup: Boolean = false
|
|
28
|
+
) : ReplacementSpan() {
|
|
29
|
+
|
|
30
|
+
override fun getSize(
|
|
31
|
+
paint: Paint,
|
|
32
|
+
text: CharSequence?,
|
|
33
|
+
start: Int,
|
|
34
|
+
end: Int,
|
|
35
|
+
fm: Paint.FontMetricsInt?
|
|
36
|
+
): Int {
|
|
37
|
+
val width = paint.measureText(text, start, end)
|
|
38
|
+
// Only add padding for first and last characters in a group
|
|
39
|
+
val leftPad = if (isFirstInGroup) paddingLeft else 0f
|
|
40
|
+
val rightPad = if (isLastInGroup) paddingRight else 0f
|
|
41
|
+
return (width + leftPad + rightPad).toInt()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun draw(
|
|
45
|
+
canvas: Canvas,
|
|
46
|
+
text: CharSequence?,
|
|
47
|
+
start: Int,
|
|
48
|
+
end: Int,
|
|
49
|
+
x: Float,
|
|
50
|
+
top: Int,
|
|
51
|
+
y: Int,
|
|
52
|
+
bottom: Int,
|
|
53
|
+
paint: Paint
|
|
54
|
+
) {
|
|
55
|
+
// Draw background with padding
|
|
56
|
+
val bgPaint = Paint().apply {
|
|
57
|
+
color = backgroundColor
|
|
58
|
+
style = Paint.Style.FILL
|
|
59
|
+
isAntiAlias = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
val width = paint.measureText(text, start, end)
|
|
63
|
+
|
|
64
|
+
// Use font metrics for consistent height instead of character bounds
|
|
65
|
+
val fontMetrics = paint.fontMetrics
|
|
66
|
+
val textHeight = fontMetrics.descent - fontMetrics.ascent
|
|
67
|
+
val textTop = y + fontMetrics.ascent
|
|
68
|
+
|
|
69
|
+
// Only add padding for first and last characters in a group
|
|
70
|
+
val leftPad = if (isFirstInGroup) paddingLeft else 0f
|
|
71
|
+
val rightPad = if (isLastInGroup) paddingRight else 0f
|
|
72
|
+
|
|
73
|
+
// Extend background slightly to overlap and eliminate gaps
|
|
74
|
+
val overlapExtension = 1f
|
|
75
|
+
val leftExtension = if (!isFirstInGroup) overlapExtension else 0f
|
|
76
|
+
val rightExtension = if (!isLastInGroup) overlapExtension else 0f
|
|
77
|
+
|
|
78
|
+
// Calculate proper bounds with padding using consistent font metrics
|
|
79
|
+
val rect = RectF(
|
|
80
|
+
x - leftExtension,
|
|
81
|
+
textTop - paddingTop,
|
|
82
|
+
x + width + leftPad + rightPad + rightExtension,
|
|
83
|
+
textTop + textHeight + paddingBottom
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Draw background with selective corner rounding
|
|
87
|
+
when {
|
|
88
|
+
isFirstInGroup && isLastInGroup -> {
|
|
89
|
+
// Single character or isolated group - round all corners
|
|
90
|
+
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint)
|
|
91
|
+
}
|
|
92
|
+
isFirstInGroup -> {
|
|
93
|
+
// First character - round left corners only
|
|
94
|
+
val path = android.graphics.Path()
|
|
95
|
+
path.addRoundRect(
|
|
96
|
+
rect,
|
|
97
|
+
floatArrayOf(
|
|
98
|
+
cornerRadius, cornerRadius, // top-left
|
|
99
|
+
0f, 0f, // top-right
|
|
100
|
+
0f, 0f, // bottom-right
|
|
101
|
+
cornerRadius, cornerRadius // bottom-left
|
|
102
|
+
),
|
|
103
|
+
android.graphics.Path.Direction.CW
|
|
104
|
+
)
|
|
105
|
+
canvas.drawPath(path, bgPaint)
|
|
106
|
+
}
|
|
107
|
+
isLastInGroup -> {
|
|
108
|
+
// Last character - round right corners only
|
|
109
|
+
val path = android.graphics.Path()
|
|
110
|
+
path.addRoundRect(
|
|
111
|
+
rect,
|
|
112
|
+
floatArrayOf(
|
|
113
|
+
0f, 0f, // top-left
|
|
114
|
+
cornerRadius, cornerRadius, // top-right
|
|
115
|
+
cornerRadius, cornerRadius, // bottom-right
|
|
116
|
+
0f, 0f // bottom-left
|
|
117
|
+
),
|
|
118
|
+
android.graphics.Path.Direction.CW
|
|
119
|
+
)
|
|
120
|
+
canvas.drawPath(path, bgPaint)
|
|
121
|
+
}
|
|
122
|
+
else -> {
|
|
123
|
+
// Middle character - no rounded corners, just rectangle
|
|
124
|
+
canvas.drawRect(rect, bgPaint)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Draw text with left padding offset only if first in group
|
|
129
|
+
val textPaint = Paint(paint).apply {
|
|
130
|
+
color = textColor
|
|
131
|
+
isAntiAlias = true
|
|
132
|
+
}
|
|
133
|
+
canvas.drawText(text!!, start, end, x + leftPad, y.toFloat(), textPaint)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class HighlightTextView : AppCompatEditText {
|
|
138
|
+
private var characterBackgroundColor: Int = Color.parseColor("#FFFF00")
|
|
139
|
+
private var textColorValue: Int = Color.BLACK
|
|
140
|
+
private var cornerRadius: Float = 4f
|
|
141
|
+
private var charPaddingLeft: Float = 8f
|
|
142
|
+
private var charPaddingRight: Float = 8f
|
|
143
|
+
private var charPaddingTop: Float = 4f
|
|
144
|
+
private var charPaddingBottom: Float = 4f
|
|
145
|
+
private var isUpdatingText: Boolean = false
|
|
146
|
+
|
|
147
|
+
var onTextChangeListener: ((String) -> Unit)? = null
|
|
148
|
+
|
|
149
|
+
constructor(context: Context?) : super(context!!) {
|
|
150
|
+
init()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
|
|
154
|
+
init()
|
|
155
|
+
}
|
|
156
|
+
|
|
10
157
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
11
|
-
context
|
|
158
|
+
context!!,
|
|
12
159
|
attrs,
|
|
13
160
|
defStyleAttr
|
|
14
|
-
)
|
|
161
|
+
) {
|
|
162
|
+
init()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private fun init() {
|
|
166
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
167
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 32f)
|
|
168
|
+
gravity = Gravity.CENTER
|
|
169
|
+
setPadding(20, 20, 20, 20)
|
|
170
|
+
textColorValue = currentTextColor
|
|
171
|
+
|
|
172
|
+
// Enable text wrapping
|
|
173
|
+
maxLines = Int.MAX_VALUE
|
|
174
|
+
isSingleLine = false
|
|
175
|
+
setHorizontallyScrolling(false)
|
|
176
|
+
|
|
177
|
+
addTextChangedListener(object : TextWatcher {
|
|
178
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
179
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
180
|
+
override fun afterTextChanged(s: Editable?) {
|
|
181
|
+
if (!isUpdatingText) {
|
|
182
|
+
onTextChangeListener?.invoke(s?.toString() ?: "")
|
|
183
|
+
applyCharacterBackgrounds()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fun setCharacterBackgroundColor(color: Int) {
|
|
190
|
+
characterBackgroundColor = color
|
|
191
|
+
applyCharacterBackgrounds()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override fun setTextColor(color: Int) {
|
|
195
|
+
super.setTextColor(color)
|
|
196
|
+
textColorValue = color
|
|
197
|
+
applyCharacterBackgrounds()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fun setCharPadding(left: Float, top: Float, right: Float, bottom: Float) {
|
|
201
|
+
charPaddingLeft = left
|
|
202
|
+
charPaddingTop = top
|
|
203
|
+
charPaddingRight = right
|
|
204
|
+
charPaddingBottom = bottom
|
|
205
|
+
applyCharacterBackgrounds()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fun setCharPaddingLeft(padding: Float) {
|
|
209
|
+
charPaddingLeft = padding
|
|
210
|
+
applyCharacterBackgrounds()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fun setCharPaddingRight(padding: Float) {
|
|
214
|
+
charPaddingRight = padding
|
|
215
|
+
applyCharacterBackgrounds()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fun setCharPaddingTop(padding: Float) {
|
|
219
|
+
charPaddingTop = padding
|
|
220
|
+
applyCharacterBackgrounds()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fun setCharPaddingBottom(padding: Float) {
|
|
224
|
+
charPaddingBottom = padding
|
|
225
|
+
applyCharacterBackgrounds()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fun setTextProp(text: String) {
|
|
229
|
+
if (this.text?.toString() != text) {
|
|
230
|
+
isUpdatingText = true
|
|
231
|
+
setText(text)
|
|
232
|
+
applyCharacterBackgrounds()
|
|
233
|
+
isUpdatingText = false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun applyCharacterBackgrounds() {
|
|
238
|
+
val text = text?.toString() ?: return
|
|
239
|
+
if (text.isEmpty()) return
|
|
240
|
+
|
|
241
|
+
val spannable = SpannableString(text)
|
|
242
|
+
|
|
243
|
+
// Apply character-by-character for proper line wrapping
|
|
244
|
+
for (i in text.indices) {
|
|
245
|
+
val char = text[i]
|
|
246
|
+
|
|
247
|
+
// Check if this is a space that should be highlighted
|
|
248
|
+
val shouldHighlight = when {
|
|
249
|
+
char == '\n' || char == '\t' -> false // Never highlight newlines or tabs
|
|
250
|
+
char == ' ' -> {
|
|
251
|
+
// Highlight space only if it's a single space (not multiple consecutive)
|
|
252
|
+
val hasSpaceBefore = i > 0 && text[i - 1] == ' '
|
|
253
|
+
val hasSpaceAfter = i < text.length - 1 && text[i + 1] == ' '
|
|
254
|
+
!hasSpaceBefore && !hasSpaceAfter
|
|
255
|
+
}
|
|
256
|
+
else -> true // Highlight all other characters
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (shouldHighlight) {
|
|
260
|
+
// Determine if this is the first or last character in a word group
|
|
261
|
+
val isFirst = i == 0 || !shouldHighlightChar(text, i - 1)
|
|
262
|
+
val isLast = i == text.length - 1 || !shouldHighlightChar(text, i + 1)
|
|
263
|
+
|
|
264
|
+
val span = RoundedBackgroundSpan(
|
|
265
|
+
characterBackgroundColor,
|
|
266
|
+
textColorValue,
|
|
267
|
+
charPaddingLeft,
|
|
268
|
+
charPaddingRight,
|
|
269
|
+
charPaddingTop,
|
|
270
|
+
charPaddingBottom,
|
|
271
|
+
cornerRadius,
|
|
272
|
+
isFirst,
|
|
273
|
+
isLast
|
|
274
|
+
)
|
|
275
|
+
spannable.setSpan(span, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
isUpdatingText = true
|
|
280
|
+
setText(spannable)
|
|
281
|
+
setSelection(text.length) // Keep cursor at end
|
|
282
|
+
isUpdatingText = false
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private fun shouldHighlightChar(text: String, index: Int): Boolean {
|
|
286
|
+
if (index < 0 || index >= text.length) return false
|
|
287
|
+
val char = text[index]
|
|
288
|
+
|
|
289
|
+
return when {
|
|
290
|
+
char == '\n' || char == '\t' -> false
|
|
291
|
+
char == ' ' -> {
|
|
292
|
+
val hasSpaceBefore = index > 0 && text[index - 1] == ' '
|
|
293
|
+
val hasSpaceAfter = index < text.length - 1 && text[index + 1] == ' '
|
|
294
|
+
!hasSpaceBefore && !hasSpaceAfter
|
|
295
|
+
}
|
|
296
|
+
else -> true
|
|
297
|
+
}
|
|
298
|
+
}
|
|
15
299
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
package com.highlighttext
|
|
2
2
|
|
|
3
3
|
import android.graphics.Color
|
|
4
|
+
import android.text.InputType
|
|
5
|
+
import android.view.Gravity
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import com.facebook.react.bridge.ReactContext
|
|
8
|
+
import com.facebook.react.bridge.WritableMap
|
|
4
9
|
import com.facebook.react.module.annotations.ReactModule
|
|
5
10
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
11
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
12
|
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
13
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
14
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
9
15
|
import com.facebook.react.viewmanagers.HighlightTextViewManagerInterface
|
|
10
16
|
import com.facebook.react.viewmanagers.HighlightTextViewManagerDelegate
|
|
11
17
|
|
|
@@ -27,12 +33,135 @@ class HighlightTextViewManager : SimpleViewManager<HighlightTextView>(),
|
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
public override fun createViewInstance(context: ThemedReactContext): HighlightTextView {
|
|
30
|
-
|
|
36
|
+
val view = HighlightTextView(context)
|
|
37
|
+
view.onTextChangeListener = { text ->
|
|
38
|
+
val event: WritableMap = Arguments.createMap()
|
|
39
|
+
event.putString("text", text)
|
|
40
|
+
|
|
41
|
+
val reactContext = context as ReactContext
|
|
42
|
+
reactContext
|
|
43
|
+
.getJSModule(RCTEventEmitter::class.java)
|
|
44
|
+
.receiveEvent(view.id, "onChange", event)
|
|
45
|
+
}
|
|
46
|
+
return view
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
@ReactProp(name = "color")
|
|
34
50
|
override fun setColor(view: HighlightTextView?, color: String?) {
|
|
35
|
-
|
|
51
|
+
color?.let {
|
|
52
|
+
try {
|
|
53
|
+
view?.setCharacterBackgroundColor(Color.parseColor(it))
|
|
54
|
+
} catch (e: IllegalArgumentException) {
|
|
55
|
+
// Invalid color format, use default
|
|
56
|
+
view?.setCharacterBackgroundColor(Color.parseColor("#FFFF00"))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@ReactProp(name = "textColor")
|
|
62
|
+
override fun setTextColor(view: HighlightTextView?, value: String?) {
|
|
63
|
+
value?.let {
|
|
64
|
+
try {
|
|
65
|
+
view?.setTextColor(Color.parseColor(it))
|
|
66
|
+
} catch (e: IllegalArgumentException) {
|
|
67
|
+
// Invalid color format, use default
|
|
68
|
+
view?.setTextColor(Color.BLACK)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@ReactProp(name = "textAlign")
|
|
74
|
+
override fun setTextAlign(view: HighlightTextView?, value: String?) {
|
|
75
|
+
// Parse combined alignment (e.g., "top-left", "bottom-center")
|
|
76
|
+
val parts = value?.split("-") ?: emptyList()
|
|
77
|
+
var horizontalAlign = value
|
|
78
|
+
|
|
79
|
+
if (parts.size == 2) {
|
|
80
|
+
// Combined format: use the second part for horizontal alignment
|
|
81
|
+
horizontalAlign = parts[1]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply horizontal alignment
|
|
85
|
+
view?.gravity = when (horizontalAlign) {
|
|
86
|
+
"left", "flex-start" -> Gravity.START or Gravity.CENTER_VERTICAL
|
|
87
|
+
"right", "flex-end" -> Gravity.END or Gravity.CENTER_VERTICAL
|
|
88
|
+
"center" -> Gravity.CENTER
|
|
89
|
+
"justify" -> Gravity.START or Gravity.CENTER_VERTICAL // Android doesn't support justify natively
|
|
90
|
+
else -> Gravity.CENTER
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@ReactProp(name = "verticalAlign")
|
|
95
|
+
override fun setVerticalAlign(view: HighlightTextView?, value: String?) {
|
|
96
|
+
// Android EditText doesn't support vertical alignment as easily as iOS
|
|
97
|
+
// This would require custom implementation with layout adjustments
|
|
98
|
+
// For now, we'll keep the default behavior
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@ReactProp(name = "fontFamily")
|
|
102
|
+
override fun setFontFamily(view: HighlightTextView?, value: String?) {
|
|
103
|
+
// Font family handling can be added if needed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@ReactProp(name = "fontSize")
|
|
107
|
+
override fun setFontSize(view: HighlightTextView?, value: String?) {
|
|
108
|
+
value?.toFloatOrNull()?.let { size ->
|
|
109
|
+
view?.textSize = size
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@ReactProp(name = "padding")
|
|
114
|
+
override fun setPadding(view: HighlightTextView?, value: String?) {
|
|
115
|
+
value?.toFloatOrNull()?.let { padding ->
|
|
116
|
+
view?.setCharPadding(padding, padding, padding, padding)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@ReactProp(name = "paddingLeft")
|
|
121
|
+
override fun setPaddingLeft(view: HighlightTextView?, value: String?) {
|
|
122
|
+
value?.toFloatOrNull()?.let { padding ->
|
|
123
|
+
view?.setCharPaddingLeft(padding)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@ReactProp(name = "paddingRight")
|
|
128
|
+
override fun setPaddingRight(view: HighlightTextView?, value: String?) {
|
|
129
|
+
value?.toFloatOrNull()?.let { padding ->
|
|
130
|
+
view?.setCharPaddingRight(padding)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@ReactProp(name = "paddingTop")
|
|
135
|
+
override fun setPaddingTop(view: HighlightTextView?, value: String?) {
|
|
136
|
+
value?.toFloatOrNull()?.let { padding ->
|
|
137
|
+
view?.setCharPaddingTop(padding)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@ReactProp(name = "paddingBottom")
|
|
142
|
+
override fun setPaddingBottom(view: HighlightTextView?, value: String?) {
|
|
143
|
+
value?.toFloatOrNull()?.let { padding ->
|
|
144
|
+
view?.setCharPaddingBottom(padding)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@ReactProp(name = "text")
|
|
149
|
+
override fun setText(view: HighlightTextView?, value: String?) {
|
|
150
|
+
view?.setTextProp(value ?: "")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@ReactProp(name = "isEditable")
|
|
154
|
+
override fun setIsEditable(view: HighlightTextView?, value: Boolean) {
|
|
155
|
+
view?.apply {
|
|
156
|
+
isFocusable = value
|
|
157
|
+
isFocusableInTouchMode = value
|
|
158
|
+
isEnabled = value
|
|
159
|
+
inputType = if (value) {
|
|
160
|
+
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
161
|
+
} else {
|
|
162
|
+
InputType.TYPE_NULL
|
|
163
|
+
}
|
|
164
|
+
}
|
|
36
165
|
}
|
|
37
166
|
|
|
38
167
|
companion object {
|
package/ios/HighlightTextView.mm
CHANGED
|
@@ -74,6 +74,8 @@ using namespace facebook::react;
|
|
|
74
74
|
CGFloat _paddingBottom;
|
|
75
75
|
CGFloat _cornerRadius;
|
|
76
76
|
BOOL _isUpdatingText;
|
|
77
|
+
NSString * _currentVerticalAlignment;
|
|
78
|
+
NSTextAlignment _currentHorizontalAlignment;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
@@ -94,6 +96,8 @@ using namespace facebook::react;
|
|
|
94
96
|
_paddingTop = 4.0;
|
|
95
97
|
_paddingBottom = 4.0;
|
|
96
98
|
_cornerRadius = 4.0;
|
|
99
|
+
_currentVerticalAlignment = nil;
|
|
100
|
+
_currentHorizontalAlignment = NSTextAlignmentCenter;
|
|
97
101
|
|
|
98
102
|
// Create text storage, layout manager, and text container
|
|
99
103
|
NSTextStorage *textStorage = [[NSTextStorage alloc] init];
|
|
@@ -128,6 +132,52 @@ using namespace facebook::react;
|
|
|
128
132
|
return self;
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
- (void)layoutSubviews
|
|
136
|
+
{
|
|
137
|
+
[super layoutSubviews];
|
|
138
|
+
|
|
139
|
+
// Recalculate vertical alignment after layout
|
|
140
|
+
if (_currentVerticalAlignment) {
|
|
141
|
+
[self updateVerticalAlignment:_currentVerticalAlignment];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
- (void)updateVerticalAlignment:(NSString *)verticalAlign
|
|
146
|
+
{
|
|
147
|
+
if ([verticalAlign isEqualToString:@"top"]) {
|
|
148
|
+
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 0, 10);
|
|
149
|
+
} else if ([verticalAlign isEqualToString:@"bottom"]) {
|
|
150
|
+
// Force layout to get accurate content height
|
|
151
|
+
[_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
|
|
152
|
+
|
|
153
|
+
CGFloat contentHeight = [_textView.layoutManager usedRectForTextContainer:_textView.textContainer].size.height;
|
|
154
|
+
CGFloat viewHeight = _textView.bounds.size.height;
|
|
155
|
+
|
|
156
|
+
// Only apply bottom alignment if we have valid dimensions
|
|
157
|
+
if (viewHeight > 0 && contentHeight > 0) {
|
|
158
|
+
if (contentHeight + 20 <= viewHeight) {
|
|
159
|
+
// Content fits in view - align to bottom with inset
|
|
160
|
+
CGFloat topInset = MAX(10, viewHeight - contentHeight - 10);
|
|
161
|
+
_textView.textContainerInset = UIEdgeInsetsMake(topInset, 10, 10, 10);
|
|
162
|
+
} else {
|
|
163
|
+
// Content exceeds view - use minimal inset and scroll to bottom
|
|
164
|
+
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
165
|
+
|
|
166
|
+
// Scroll to bottom to show the latest text
|
|
167
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
168
|
+
CGFloat bottomOffset = self->_textView.contentSize.height - self->_textView.bounds.size.height + self->_textView.contentInset.bottom;
|
|
169
|
+
if (bottomOffset > 0) {
|
|
170
|
+
[self->_textView setContentOffset:CGPointMake(0, bottomOffset) animated:NO];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
// Default or center
|
|
177
|
+
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
131
181
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
132
182
|
{
|
|
133
183
|
const auto &oldViewProps = *std::static_pointer_cast<HighlightTextViewProps const>(_props);
|
|
@@ -169,29 +219,26 @@ using namespace facebook::react;
|
|
|
169
219
|
// Apply horizontal alignment
|
|
170
220
|
if (horizontalPart) {
|
|
171
221
|
if ([horizontalPart isEqualToString:@"center"]) {
|
|
172
|
-
|
|
222
|
+
_currentHorizontalAlignment = NSTextAlignmentCenter;
|
|
173
223
|
} else if ([horizontalPart isEqualToString:@"right"] || [horizontalPart isEqualToString:@"flex-end"]) {
|
|
174
|
-
|
|
224
|
+
_currentHorizontalAlignment = NSTextAlignmentRight;
|
|
175
225
|
} else if ([horizontalPart isEqualToString:@"left"] || [horizontalPart isEqualToString:@"flex-start"]) {
|
|
176
|
-
|
|
226
|
+
_currentHorizontalAlignment = NSTextAlignmentLeft;
|
|
177
227
|
} else if ([horizontalPart isEqualToString:@"justify"]) {
|
|
178
|
-
|
|
228
|
+
_currentHorizontalAlignment = NSTextAlignmentJustified;
|
|
179
229
|
} else {
|
|
180
|
-
|
|
230
|
+
_currentHorizontalAlignment = NSTextAlignmentLeft;
|
|
181
231
|
}
|
|
232
|
+
_textView.textAlignment = _currentHorizontalAlignment;
|
|
182
233
|
}
|
|
183
234
|
|
|
184
|
-
//
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
CGFloat contentHeight = [_textView.layoutManager usedRectForTextContainer:_textView.textContainer].size.height;
|
|
190
|
-
CGFloat viewHeight = _textView.bounds.size.height;
|
|
191
|
-
CGFloat topInset = MAX(10, viewHeight - contentHeight - 10);
|
|
192
|
-
_textView.textContainerInset = UIEdgeInsetsMake(topInset, 10, 10, 10);
|
|
193
|
-
} else if (!verticalPart) {
|
|
235
|
+
// Store and apply vertical alignment
|
|
236
|
+
if (verticalPart) {
|
|
237
|
+
_currentVerticalAlignment = verticalPart;
|
|
238
|
+
[self updateVerticalAlignment:verticalPart];
|
|
239
|
+
} else if (!verticalPart && horizontalPart) {
|
|
194
240
|
// Default vertical centering for horizontal-only alignments
|
|
241
|
+
_currentVerticalAlignment = nil;
|
|
195
242
|
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
196
243
|
}
|
|
197
244
|
|
|
@@ -269,6 +316,12 @@ using namespace facebook::react;
|
|
|
269
316
|
_isUpdatingText = YES;
|
|
270
317
|
_textView.text = text;
|
|
271
318
|
[self applyCharacterBackgrounds];
|
|
319
|
+
|
|
320
|
+
// Recalculate vertical alignment when text changes
|
|
321
|
+
if (_currentVerticalAlignment) {
|
|
322
|
+
[self updateVerticalAlignment:_currentVerticalAlignment];
|
|
323
|
+
}
|
|
324
|
+
|
|
272
325
|
_isUpdatingText = NO;
|
|
273
326
|
}
|
|
274
327
|
}
|
|
@@ -279,18 +332,8 @@ using namespace facebook::react;
|
|
|
279
332
|
|
|
280
333
|
if (oldViewProps.verticalAlign != newViewProps.verticalAlign) {
|
|
281
334
|
NSString *verticalAlign = [[NSString alloc] initWithUTF8String: newViewProps.verticalAlign.c_str()];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 0, 10);
|
|
285
|
-
} else if ([verticalAlign isEqualToString:@"bottom"]) {
|
|
286
|
-
CGFloat contentHeight = [_textView.layoutManager usedRectForTextContainer:_textView.textContainer].size.height;
|
|
287
|
-
CGFloat viewHeight = _textView.bounds.size.height;
|
|
288
|
-
CGFloat topInset = MAX(10, viewHeight - contentHeight - 10);
|
|
289
|
-
_textView.textContainerInset = UIEdgeInsetsMake(topInset, 10, 10, 10);
|
|
290
|
-
} else if ([verticalAlign isEqualToString:@"center"] || [verticalAlign isEqualToString:@"middle"]) {
|
|
291
|
-
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
292
|
-
}
|
|
293
|
-
|
|
335
|
+
_currentVerticalAlignment = verticalAlign;
|
|
336
|
+
[self updateVerticalAlignment:verticalAlign];
|
|
294
337
|
[self applyCharacterBackgrounds];
|
|
295
338
|
}
|
|
296
339
|
|
|
@@ -305,6 +348,12 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
305
348
|
- (void)textViewDidChange:(UITextView *)textView
|
|
306
349
|
{
|
|
307
350
|
[self applyCharacterBackgrounds];
|
|
351
|
+
|
|
352
|
+
// Recalculate vertical alignment when text changes
|
|
353
|
+
if (_currentVerticalAlignment) {
|
|
354
|
+
[self updateVerticalAlignment:_currentVerticalAlignment];
|
|
355
|
+
}
|
|
356
|
+
|
|
308
357
|
if (!_isUpdatingText) {
|
|
309
358
|
if (_eventEmitter != nullptr) {
|
|
310
359
|
std::dynamic_pointer_cast<const HighlightTextViewEventEmitter>(_eventEmitter)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-highlight-text-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A native text input for React Native that supports inline text highlighting",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|