react-native-highlight-text-view 0.1.18 → 0.1.20
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
|
@@ -128,6 +128,46 @@ To automatically open the keyboard when the component mounts, use the `autoFocus
|
|
|
128
128
|
|
|
129
129
|
This eliminates the need for double-tapping to open the keyboard - it will open on first render.
|
|
130
130
|
|
|
131
|
+
### Dynamic Font Family Changes
|
|
132
|
+
|
|
133
|
+
**IMPORTANT**: When changing `fontFamily` dynamically at runtime (especially to fonts with different ascender/descender values like Eczar, Georgia, etc.), you must use the `key` prop to force React to remount the component. This ensures the native layout recalculates with the new font metrics.
|
|
134
|
+
|
|
135
|
+
**Why this is needed**: Fonts like Eczar have significantly larger vertical metrics than system fonts. Without remounting, the highlight background may appear cut off at the bottom or lose corner radius.
|
|
136
|
+
|
|
137
|
+
**Solution**: Pass the `fontFamily` as the `key` prop:
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
const [fontFamily, setFontFamily] = useState('system');
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<HighlightTextView
|
|
144
|
+
key={fontFamily}
|
|
145
|
+
fontFamily={fontFamily}
|
|
146
|
+
fontSize="32"
|
|
147
|
+
color="#00A4A3"
|
|
148
|
+
textColor="#FFFFFF"
|
|
149
|
+
paddingLeft="8"
|
|
150
|
+
paddingRight="8"
|
|
151
|
+
paddingTop="4"
|
|
152
|
+
paddingBottom="4"
|
|
153
|
+
backgroundInsetTop="6"
|
|
154
|
+
backgroundInsetBottom="6"
|
|
155
|
+
highlightBorderRadius="8"
|
|
156
|
+
text="Beautiful Eczar Font"
|
|
157
|
+
style={{ width: '100%', height: 150 }}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**What happens**:
|
|
163
|
+
|
|
164
|
+
- Font changes → `key` changes → React unmounts old component and mounts new one
|
|
165
|
+
- New mount → Native component calculates fresh layout with correct font metrics
|
|
166
|
+
- Perfect rendering → Background highlights render correctly without cutting
|
|
167
|
+
|
|
168
|
+
**Without key prop**: Background may cut off, corner radius may disappear
|
|
169
|
+
**With key prop**: Perfect rendering every time ✅
|
|
170
|
+
|
|
131
171
|
## Contributing
|
|
132
172
|
|
|
133
173
|
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
@@ -96,10 +96,11 @@ class RoundedBackgroundSpan(
|
|
|
96
96
|
insetTop + insetHeight + paddingBottom
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
// Draw background with selective corner rounding (
|
|
99
|
+
// Draw background with selective corner rounding (matches iOS behavior)
|
|
100
|
+
// iOS draws per-character backgrounds with full corner radius, so we do the same
|
|
100
101
|
when {
|
|
101
102
|
isReallyFirst && isReallyLast -> {
|
|
102
|
-
// Single character or isolated group - round all corners
|
|
103
|
+
// Single character or isolated group - round all corners (matches iOS)
|
|
103
104
|
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint)
|
|
104
105
|
}
|
|
105
106
|
isReallyFirst -> {
|
|
@@ -109,8 +110,8 @@ class RoundedBackgroundSpan(
|
|
|
109
110
|
rect,
|
|
110
111
|
floatArrayOf(
|
|
111
112
|
cornerRadius, cornerRadius, // top-left
|
|
112
|
-
0f, 0f, // top-right
|
|
113
|
-
0f, 0f, // bottom-right
|
|
113
|
+
0f, 0f, // top-right (flat for connection)
|
|
114
|
+
0f, 0f, // bottom-right (flat for connection)
|
|
114
115
|
cornerRadius, cornerRadius // bottom-left
|
|
115
116
|
),
|
|
116
117
|
android.graphics.Path.Direction.CW
|
|
@@ -123,17 +124,17 @@ class RoundedBackgroundSpan(
|
|
|
123
124
|
path.addRoundRect(
|
|
124
125
|
rect,
|
|
125
126
|
floatArrayOf(
|
|
126
|
-
0f, 0f, // top-left
|
|
127
|
+
0f, 0f, // top-left (flat for connection)
|
|
127
128
|
cornerRadius, cornerRadius, // top-right
|
|
128
129
|
cornerRadius, cornerRadius, // bottom-right
|
|
129
|
-
0f, 0f // bottom-left
|
|
130
|
+
0f, 0f // bottom-left (flat for connection)
|
|
130
131
|
),
|
|
131
132
|
android.graphics.Path.Direction.CW
|
|
132
133
|
)
|
|
133
134
|
canvas.drawPath(path, bgPaint)
|
|
134
135
|
}
|
|
135
136
|
else -> {
|
|
136
|
-
// Middle character - no rounded corners
|
|
137
|
+
// Middle character - no rounded corners for seamless connection
|
|
137
138
|
canvas.drawRect(rect, bgPaint)
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -206,13 +207,6 @@ class HighlightTextView : AppCompatEditText {
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
})
|
|
209
|
-
|
|
210
|
-
// Set cursor at end when view gains focus
|
|
211
|
-
setOnFocusChangeListener { _, hasFocus ->
|
|
212
|
-
if (hasFocus) {
|
|
213
|
-
text?.length?.let { setSelection(it) }
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
210
|
}
|
|
217
211
|
|
|
218
212
|
fun setCharacterBackgroundColor(color: Int) {
|
|
@@ -315,41 +309,8 @@ class HighlightTextView : AppCompatEditText {
|
|
|
315
309
|
Typeface.create(baseTypeface, style)
|
|
316
310
|
}
|
|
317
311
|
|
|
318
|
-
// Save current text to force complete rebuild
|
|
319
|
-
val currentText = text?.toString() ?: ""
|
|
320
|
-
val currentSelection = selectionStart
|
|
321
|
-
|
|
322
312
|
this.typeface = typeface
|
|
323
|
-
|
|
324
|
-
// Force complete text rebuild with new font metrics
|
|
325
|
-
// This is crucial for fonts with different ascender/descender values like Eczar
|
|
326
|
-
post {
|
|
327
|
-
// Clear text first to force layout recalculation
|
|
328
|
-
isUpdatingText = true
|
|
329
|
-
setText("")
|
|
330
|
-
|
|
331
|
-
// Force measure and layout with empty text
|
|
332
|
-
measure(
|
|
333
|
-
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
334
|
-
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
// Restore text - this triggers complete rebuild with new font metrics
|
|
338
|
-
setText(currentText)
|
|
339
|
-
|
|
340
|
-
// Reapply all character backgrounds with new font metrics
|
|
341
|
-
applyCharacterBackgrounds()
|
|
342
|
-
|
|
343
|
-
// Restore cursor position
|
|
344
|
-
val safePosition = currentSelection.coerceIn(0, currentText.length)
|
|
345
|
-
setSelection(safePosition)
|
|
346
|
-
|
|
347
|
-
isUpdatingText = false
|
|
348
|
-
|
|
349
|
-
// Force complete layout recalculation
|
|
350
|
-
requestLayout()
|
|
351
|
-
invalidate()
|
|
352
|
-
}
|
|
313
|
+
applyCharacterBackgrounds()
|
|
353
314
|
}
|
|
354
315
|
|
|
355
316
|
fun setVerticalAlign(align: String?) {
|
|
@@ -407,8 +368,6 @@ class HighlightTextView : AppCompatEditText {
|
|
|
407
368
|
if (autoFocus && isFocusable && isFocusableInTouchMode) {
|
|
408
369
|
post {
|
|
409
370
|
requestFocus()
|
|
410
|
-
// Set cursor at the end of text
|
|
411
|
-
text?.length?.let { setSelection(it) }
|
|
412
371
|
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
|
|
413
372
|
imm?.showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
|
414
373
|
}
|
|
@@ -419,9 +378,6 @@ class HighlightTextView : AppCompatEditText {
|
|
|
419
378
|
val text = text?.toString() ?: return
|
|
420
379
|
if (text.isEmpty()) return
|
|
421
380
|
|
|
422
|
-
// Save current cursor position
|
|
423
|
-
val currentSelection = selectionStart
|
|
424
|
-
|
|
425
381
|
val spannable = SpannableString(text)
|
|
426
382
|
|
|
427
383
|
// Apply line height if specified
|
|
@@ -473,11 +429,16 @@ class HighlightTextView : AppCompatEditText {
|
|
|
473
429
|
}
|
|
474
430
|
}
|
|
475
431
|
|
|
432
|
+
// Save current selection to prevent cursor jumping (smooth editing)
|
|
433
|
+
val currentSelection = selectionStart
|
|
434
|
+
|
|
476
435
|
isUpdatingText = true
|
|
477
436
|
setText(spannable)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
437
|
+
|
|
438
|
+
// Restore cursor position if valid (prevents jerking during editing)
|
|
439
|
+
if (currentSelection >= 0 && currentSelection <= text.length) {
|
|
440
|
+
setSelection(currentSelection)
|
|
441
|
+
}
|
|
481
442
|
isUpdatingText = false
|
|
482
443
|
|
|
483
444
|
// Detect line wraps after layout is ready
|
|
@@ -501,13 +462,19 @@ class HighlightTextView : AppCompatEditText {
|
|
|
501
462
|
|
|
502
463
|
// Check for manual line break (\n) before this character
|
|
503
464
|
val hasNewlineBefore = spanStart > 0 && textStr[spanStart - 1] == '\n'
|
|
504
|
-
// Check for manual line break (\n) after this character
|
|
465
|
+
// Check for manual line break (\n) after this character
|
|
505
466
|
val hasNewlineAfter = spanEnd < textStr.length && textStr[spanEnd] == '\n'
|
|
506
467
|
|
|
468
|
+
// Check if this is the last line of text
|
|
469
|
+
val isLastLine = line == layout.lineCount - 1
|
|
470
|
+
|
|
507
471
|
// Check if this char is at start of visual line (wrapped OR after \n)
|
|
508
472
|
val isAtLineStart = (spanStart == lineStart && !span.isFirstInGroup) || hasNewlineBefore
|
|
509
|
-
|
|
510
|
-
|
|
473
|
+
|
|
474
|
+
// Check if this char is at end of visual line (wrapped OR before \n OR end of last line)
|
|
475
|
+
// CRITICAL: Ensure last character of entire text gets rounded corners
|
|
476
|
+
val isAtLineEnd = (spanEnd == lineEnd && !span.isLastInGroup) || hasNewlineAfter ||
|
|
477
|
+
(isLastLine && spanEnd == lineEnd)
|
|
511
478
|
|
|
512
479
|
if (isAtLineStart || isAtLineEnd) {
|
|
513
480
|
// Create new span with line boundary flags
|
package/ios/HighlightTextView.mm
CHANGED
|
@@ -430,9 +430,6 @@ using namespace facebook::react;
|
|
|
430
430
|
if (newViewProps.autoFocus && _textView.isEditable) {
|
|
431
431
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
432
432
|
[self->_textView becomeFirstResponder];
|
|
433
|
-
// Set cursor at the end of text
|
|
434
|
-
NSUInteger textLength = self->_textView.text.length;
|
|
435
|
-
self->_textView.selectedRange = NSMakeRange(textLength, 0);
|
|
436
433
|
});
|
|
437
434
|
}
|
|
438
435
|
}
|
|
@@ -471,13 +468,6 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
471
468
|
}
|
|
472
469
|
}
|
|
473
470
|
|
|
474
|
-
- (void)textViewDidBeginEditing:(UITextView *)textView
|
|
475
|
-
{
|
|
476
|
-
// Set cursor at the end when editing begins
|
|
477
|
-
NSUInteger textLength = textView.text.length;
|
|
478
|
-
textView.selectedRange = NSMakeRange(textLength, 0);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
471
|
- (void)updateFont
|
|
482
472
|
{
|
|
483
473
|
CGFloat fontSize = _fontSize > 0 ? _fontSize : 32.0;
|
|
@@ -529,32 +519,7 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
529
519
|
}
|
|
530
520
|
|
|
531
521
|
_textView.font = newFont;
|
|
532
|
-
|
|
533
|
-
// Save current text to force complete rebuild
|
|
534
|
-
NSString *currentText = [_textView.text copy];
|
|
535
|
-
|
|
536
|
-
// Clear text storage to force complete recalculation
|
|
537
|
-
[_textView.textStorage beginEditing];
|
|
538
|
-
[_textView.textStorage replaceCharactersInRange:NSMakeRange(0, _textView.textStorage.length) withString:@""];
|
|
539
|
-
[_textView.textStorage endEditing];
|
|
540
|
-
|
|
541
|
-
// Force layout manager to invalidate everything
|
|
542
|
-
[_layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, 0) actualCharacterRange:NULL];
|
|
543
|
-
[_layoutManager invalidateDisplayForCharacterRange:NSMakeRange(0, 0)];
|
|
544
|
-
|
|
545
|
-
// Restore text and rebuild with new font
|
|
546
|
-
_textView.text = currentText;
|
|
547
|
-
|
|
548
|
-
// Reapply character backgrounds with new font metrics - this rebuilds attributed text
|
|
549
522
|
[self applyCharacterBackgrounds];
|
|
550
|
-
|
|
551
|
-
// Force complete layout recalculation
|
|
552
|
-
[_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
|
|
553
|
-
|
|
554
|
-
// Force the text view to redraw
|
|
555
|
-
[_textView setNeedsDisplay];
|
|
556
|
-
[_textView setNeedsLayout];
|
|
557
|
-
[_textView layoutIfNeeded];
|
|
558
523
|
}
|
|
559
524
|
|
|
560
525
|
- (void)applyCharacterBackgrounds
|
|
@@ -564,9 +529,6 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
564
529
|
return;
|
|
565
530
|
}
|
|
566
531
|
|
|
567
|
-
// Save current cursor position
|
|
568
|
-
NSRange currentSelection = _textView.selectedRange;
|
|
569
|
-
|
|
570
532
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
|
|
571
533
|
UIColor *bgColor = [self hexStringToColor:_characterBackgroundColor];
|
|
572
534
|
|
|
@@ -615,12 +577,6 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
615
577
|
}
|
|
616
578
|
|
|
617
579
|
_textView.attributedText = attributedString;
|
|
618
|
-
|
|
619
|
-
// Restore cursor position if valid
|
|
620
|
-
if (currentSelection.location <= text.length) {
|
|
621
|
-
_textView.selectedRange = currentSelection;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
580
|
[_textView setNeedsDisplay];
|
|
625
581
|
}
|
|
626
582
|
|
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.20",
|
|
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",
|