react-native-highlight-text-view 0.1.2 → 0.1.4
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 +48 -9
- package/android/src/main/java/com/highlighttext/HighlightTextView.kt +290 -6
- package/android/src/main/java/com/highlighttext/HighlightTextViewManager.kt +131 -2
- package/ios/HighlightTextView.mm +63 -5
- package/lib/module/HighlightTextViewNativeComponent.ts +34 -0
- package/lib/typescript/src/HighlightTextViewNativeComponent.d.ts +19 -0
- package/lib/typescript/src/HighlightTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/HighlightTextViewNativeComponent.ts +34 -0
package/README.md
CHANGED
|
@@ -12,14 +12,56 @@ npm install react-native-highlight-text
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
```js
|
|
17
|
-
import {
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { HighlightTextView } from 'react-native-highlight-text-view';
|
|
18
|
+
|
|
19
|
+
export default function App() {
|
|
20
|
+
const [text, setText] = useState('Hello World');
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<HighlightTextView
|
|
24
|
+
color="#00A4A3"
|
|
25
|
+
textColor="#000000"
|
|
26
|
+
textAlign="flex-start"
|
|
27
|
+
fontSize="32"
|
|
28
|
+
paddingLeft="8"
|
|
29
|
+
paddingRight="8"
|
|
30
|
+
paddingTop="4"
|
|
31
|
+
paddingBottom="4"
|
|
32
|
+
text={text}
|
|
33
|
+
isEditable={true}
|
|
34
|
+
onChange={(e) => {
|
|
35
|
+
setText(e.nativeEvent.text);
|
|
36
|
+
}}
|
|
37
|
+
style={{ width: '100%', height: 200 }}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
18
42
|
|
|
19
|
-
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
|------|------|---------|-------------|
|
|
47
|
+
| `color` | `string` | `#FFFF00` | Background highlight color (hex format) |
|
|
48
|
+
| `textColor` | `string` | - | Text color (hex format) |
|
|
49
|
+
| `textAlign` | `string` | `left` | Text alignment. Supports: `'left'`, `'center'`, `'right'`, `'justify'`, `'flex-start'`, `'flex-end'`, `'top'`, `'bottom'`, `'top-left'`, `'top-center'`, `'top-right'`, `'bottom-left'`, `'bottom-center'`, `'bottom-right'` |
|
|
50
|
+
| `verticalAlign` | `'top' \| 'center' \| 'middle' \| 'bottom'` | - | Vertical alignment (iOS only). Alternative to using combined `textAlign` values |
|
|
51
|
+
| `fontFamily` | `string` | - | Font family name |
|
|
52
|
+
| `fontSize` | `string` | `32` | Font size in points |
|
|
53
|
+
| `padding` | `string` | `4` | Padding around each character highlight |
|
|
54
|
+
| `paddingLeft` | `string` | - | Left padding for character highlight |
|
|
55
|
+
| `paddingRight` | `string` | - | Right padding for character highlight |
|
|
56
|
+
| `paddingTop` | `string` | - | Top padding for character highlight |
|
|
57
|
+
| `paddingBottom` | `string` | - | Bottom padding for character highlight |
|
|
58
|
+
| `text` | `string` | - | Controlled text value |
|
|
59
|
+
| `isEditable` | `boolean` | `true` | Whether the text is editable |
|
|
60
|
+
| `onChange` | `(event: { nativeEvent: { text: string } }) => void` | - | Callback fired when text changes |
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
**Note:** Vertical alignment is currently supported on iOS only. On Android, text will use default vertical positioning.
|
|
20
64
|
|
|
21
|
-
<HighlightTextView color="tomato" />
|
|
22
|
-
```
|
|
23
65
|
|
|
24
66
|
|
|
25
67
|
## Contributing
|
|
@@ -28,10 +70,7 @@ import { HighlightTextView } from "react-native-highlight-text";
|
|
|
28
70
|
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
29
71
|
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
30
72
|
|
|
31
|
-
## License
|
|
32
|
-
|
|
33
|
-
MIT
|
|
34
73
|
|
|
35
|
-
|
|
74
|
+
## License MIT
|
|
36
75
|
|
|
37
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
|
@@ -147,13 +147,54 @@ using namespace facebook::react;
|
|
|
147
147
|
|
|
148
148
|
if (oldViewProps.textAlign != newViewProps.textAlign) {
|
|
149
149
|
NSString *alignment = [[NSString alloc] initWithUTF8String: newViewProps.textAlign.c_str()];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
|
|
151
|
+
// Parse combined alignment (e.g., "top-left", "bottom-center")
|
|
152
|
+
NSArray *parts = [alignment componentsSeparatedByString:@"-"];
|
|
153
|
+
NSString *verticalPart = nil;
|
|
154
|
+
NSString *horizontalPart = nil;
|
|
155
|
+
|
|
156
|
+
if (parts.count == 2) {
|
|
157
|
+
// Combined format: "top-left", "bottom-center", etc.
|
|
158
|
+
verticalPart = parts[0];
|
|
159
|
+
horizontalPart = parts[1];
|
|
154
160
|
} else {
|
|
155
|
-
|
|
161
|
+
// Single value - determine if it's horizontal or vertical
|
|
162
|
+
if ([alignment isEqualToString:@"top"] || [alignment isEqualToString:@"bottom"]) {
|
|
163
|
+
verticalPart = alignment;
|
|
164
|
+
} else {
|
|
165
|
+
horizontalPart = alignment;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply horizontal alignment
|
|
170
|
+
if (horizontalPart) {
|
|
171
|
+
if ([horizontalPart isEqualToString:@"center"]) {
|
|
172
|
+
_textView.textAlignment = NSTextAlignmentCenter;
|
|
173
|
+
} else if ([horizontalPart isEqualToString:@"right"] || [horizontalPart isEqualToString:@"flex-end"]) {
|
|
174
|
+
_textView.textAlignment = NSTextAlignmentRight;
|
|
175
|
+
} else if ([horizontalPart isEqualToString:@"left"] || [horizontalPart isEqualToString:@"flex-start"]) {
|
|
176
|
+
_textView.textAlignment = NSTextAlignmentLeft;
|
|
177
|
+
} else if ([horizontalPart isEqualToString:@"justify"]) {
|
|
178
|
+
_textView.textAlignment = NSTextAlignmentJustified;
|
|
179
|
+
} else {
|
|
180
|
+
_textView.textAlignment = NSTextAlignmentLeft;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Apply vertical alignment via textContainerInset
|
|
185
|
+
if ([verticalPart isEqualToString:@"top"]) {
|
|
186
|
+
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 0, 10);
|
|
187
|
+
} else if ([verticalPart isEqualToString:@"bottom"]) {
|
|
188
|
+
// Calculate bottom alignment by adjusting top inset
|
|
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) {
|
|
194
|
+
// Default vertical centering for horizontal-only alignments
|
|
195
|
+
_textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
156
196
|
}
|
|
197
|
+
|
|
157
198
|
[self applyCharacterBackgrounds]; // Reapply to update alignment
|
|
158
199
|
}
|
|
159
200
|
|
|
@@ -235,6 +276,23 @@ using namespace facebook::react;
|
|
|
235
276
|
if (oldViewProps.isEditable != newViewProps.isEditable) {
|
|
236
277
|
_textView.editable = newViewProps.isEditable;
|
|
237
278
|
}
|
|
279
|
+
|
|
280
|
+
if (oldViewProps.verticalAlign != newViewProps.verticalAlign) {
|
|
281
|
+
NSString *verticalAlign = [[NSString alloc] initWithUTF8String: newViewProps.verticalAlign.c_str()];
|
|
282
|
+
|
|
283
|
+
if ([verticalAlign isEqualToString:@"top"]) {
|
|
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
|
+
|
|
294
|
+
[self applyCharacterBackgrounds];
|
|
295
|
+
}
|
|
238
296
|
|
|
239
297
|
[super updateProps:props oldProps:oldProps];
|
|
240
298
|
}
|
|
@@ -6,10 +6,44 @@ export interface OnChangeEventData {
|
|
|
6
6
|
readonly text: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Text alignment options
|
|
11
|
+
*
|
|
12
|
+
* Horizontal alignment:
|
|
13
|
+
* - 'left' or 'flex-start': Align text to the left
|
|
14
|
+
* - 'center': Center align text
|
|
15
|
+
* - 'right' or 'flex-end': Align text to the right
|
|
16
|
+
* - 'justify': Justify text (distribute evenly)
|
|
17
|
+
*
|
|
18
|
+
* Vertical alignment (iOS only):
|
|
19
|
+
* - 'top': Align to top
|
|
20
|
+
* - 'bottom': Align to bottom
|
|
21
|
+
*
|
|
22
|
+
* Combined alignment (iOS only):
|
|
23
|
+
* - 'top-left', 'top-center', 'top-right'
|
|
24
|
+
* - 'bottom-left', 'bottom-center', 'bottom-right'
|
|
25
|
+
*/
|
|
26
|
+
export type TextAlignment =
|
|
27
|
+
| 'left'
|
|
28
|
+
| 'center'
|
|
29
|
+
| 'right'
|
|
30
|
+
| 'justify'
|
|
31
|
+
| 'flex-start'
|
|
32
|
+
| 'flex-end'
|
|
33
|
+
| 'top'
|
|
34
|
+
| 'bottom'
|
|
35
|
+
| 'top-left'
|
|
36
|
+
| 'top-center'
|
|
37
|
+
| 'top-right'
|
|
38
|
+
| 'bottom-left'
|
|
39
|
+
| 'bottom-center'
|
|
40
|
+
| 'bottom-right';
|
|
41
|
+
|
|
9
42
|
export interface HighlightTextViewProps extends ViewProps {
|
|
10
43
|
color?: string;
|
|
11
44
|
textColor?: string;
|
|
12
45
|
textAlign?: string;
|
|
46
|
+
verticalAlign?: string;
|
|
13
47
|
fontFamily?: string;
|
|
14
48
|
fontSize?: string;
|
|
15
49
|
padding?: string;
|
|
@@ -3,10 +3,29 @@ import type { BubblingEventHandler } from 'react-native/Libraries/Types/CodegenT
|
|
|
3
3
|
export interface OnChangeEventData {
|
|
4
4
|
readonly text: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Text alignment options
|
|
8
|
+
*
|
|
9
|
+
* Horizontal alignment:
|
|
10
|
+
* - 'left' or 'flex-start': Align text to the left
|
|
11
|
+
* - 'center': Center align text
|
|
12
|
+
* - 'right' or 'flex-end': Align text to the right
|
|
13
|
+
* - 'justify': Justify text (distribute evenly)
|
|
14
|
+
*
|
|
15
|
+
* Vertical alignment (iOS only):
|
|
16
|
+
* - 'top': Align to top
|
|
17
|
+
* - 'bottom': Align to bottom
|
|
18
|
+
*
|
|
19
|
+
* Combined alignment (iOS only):
|
|
20
|
+
* - 'top-left', 'top-center', 'top-right'
|
|
21
|
+
* - 'bottom-left', 'bottom-center', 'bottom-right'
|
|
22
|
+
*/
|
|
23
|
+
export type TextAlignment = 'left' | 'center' | 'right' | 'justify' | 'flex-start' | 'flex-end' | 'top' | 'bottom' | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
6
24
|
export interface HighlightTextViewProps extends ViewProps {
|
|
7
25
|
color?: string;
|
|
8
26
|
textColor?: string;
|
|
9
27
|
textAlign?: string;
|
|
28
|
+
verticalAlign?: string;
|
|
10
29
|
fontFamily?: string;
|
|
11
30
|
fontSize?: string;
|
|
12
31
|
padding?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HighlightTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/HighlightTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAuB,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpD;;AAED,wBAEE"}
|
|
1
|
+
{"version":3,"file":"HighlightTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/HighlightTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,YAAY,GACZ,UAAU,GACV,KAAK,GACL,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,WAAW,GACX,aAAa,GACb,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,sBAAuB,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpD;;AAED,wBAEE"}
|
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.4",
|
|
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",
|
|
@@ -6,10 +6,44 @@ export interface OnChangeEventData {
|
|
|
6
6
|
readonly text: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Text alignment options
|
|
11
|
+
*
|
|
12
|
+
* Horizontal alignment:
|
|
13
|
+
* - 'left' or 'flex-start': Align text to the left
|
|
14
|
+
* - 'center': Center align text
|
|
15
|
+
* - 'right' or 'flex-end': Align text to the right
|
|
16
|
+
* - 'justify': Justify text (distribute evenly)
|
|
17
|
+
*
|
|
18
|
+
* Vertical alignment (iOS only):
|
|
19
|
+
* - 'top': Align to top
|
|
20
|
+
* - 'bottom': Align to bottom
|
|
21
|
+
*
|
|
22
|
+
* Combined alignment (iOS only):
|
|
23
|
+
* - 'top-left', 'top-center', 'top-right'
|
|
24
|
+
* - 'bottom-left', 'bottom-center', 'bottom-right'
|
|
25
|
+
*/
|
|
26
|
+
export type TextAlignment =
|
|
27
|
+
| 'left'
|
|
28
|
+
| 'center'
|
|
29
|
+
| 'right'
|
|
30
|
+
| 'justify'
|
|
31
|
+
| 'flex-start'
|
|
32
|
+
| 'flex-end'
|
|
33
|
+
| 'top'
|
|
34
|
+
| 'bottom'
|
|
35
|
+
| 'top-left'
|
|
36
|
+
| 'top-center'
|
|
37
|
+
| 'top-right'
|
|
38
|
+
| 'bottom-left'
|
|
39
|
+
| 'bottom-center'
|
|
40
|
+
| 'bottom-right';
|
|
41
|
+
|
|
9
42
|
export interface HighlightTextViewProps extends ViewProps {
|
|
10
43
|
color?: string;
|
|
11
44
|
textColor?: string;
|
|
12
45
|
textAlign?: string;
|
|
46
|
+
verticalAlign?: string;
|
|
13
47
|
fontFamily?: string;
|
|
14
48
|
fontSize?: string;
|
|
15
49
|
padding?: string;
|