react-native-highlight-text-view 0.1.19 → 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,61 +309,8 @@ class HighlightTextView : AppCompatEditText {
|
|
|
315
309
|
Typeface.create(baseTypeface, style)
|
|
316
310
|
}
|
|
317
311
|
|
|
318
|
-
// Save current text and selection
|
|
319
|
-
val currentText = text?.toString() ?: ""
|
|
320
|
-
val currentSelection = selectionStart
|
|
321
|
-
|
|
322
|
-
// Apply new typeface
|
|
323
312
|
this.typeface = typeface
|
|
324
|
-
|
|
325
|
-
// CRITICAL: Force complete layout rebuild for fonts with different metrics (like Eczar)
|
|
326
|
-
// Multiple post calls ensure all layout phases complete with new font
|
|
327
|
-
post {
|
|
328
|
-
isUpdatingText = true
|
|
329
|
-
|
|
330
|
-
// Phase 1: Clear everything
|
|
331
|
-
setText("")
|
|
332
|
-
paint.typeface = typeface // Ensure paint also has new typeface
|
|
333
|
-
|
|
334
|
-
// Force measure with empty text and new font
|
|
335
|
-
measure(
|
|
336
|
-
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
337
|
-
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
338
|
-
)
|
|
339
|
-
layout(left, top, right, bottom)
|
|
340
|
-
|
|
341
|
-
// Phase 2: Restore text and rebuild (next frame)
|
|
342
|
-
post {
|
|
343
|
-
setText(currentText)
|
|
344
|
-
|
|
345
|
-
// Force another measure/layout cycle with actual text
|
|
346
|
-
measure(
|
|
347
|
-
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
348
|
-
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
349
|
-
)
|
|
350
|
-
layout(left, top, right, bottom)
|
|
351
|
-
|
|
352
|
-
// Phase 3: Apply backgrounds and finalize (next frame)
|
|
353
|
-
post {
|
|
354
|
-
applyCharacterBackgrounds()
|
|
355
|
-
|
|
356
|
-
// Restore cursor position
|
|
357
|
-
val safePosition = currentSelection.coerceIn(0, currentText.length)
|
|
358
|
-
setSelection(safePosition)
|
|
359
|
-
|
|
360
|
-
isUpdatingText = false
|
|
361
|
-
|
|
362
|
-
// Final layout pass
|
|
363
|
-
requestLayout()
|
|
364
|
-
invalidate()
|
|
365
|
-
|
|
366
|
-
// Extra invalidate to ensure rendering
|
|
367
|
-
post {
|
|
368
|
-
invalidate()
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
313
|
+
applyCharacterBackgrounds()
|
|
373
314
|
}
|
|
374
315
|
|
|
375
316
|
fun setVerticalAlign(align: String?) {
|
|
@@ -427,8 +368,6 @@ class HighlightTextView : AppCompatEditText {
|
|
|
427
368
|
if (autoFocus && isFocusable && isFocusableInTouchMode) {
|
|
428
369
|
post {
|
|
429
370
|
requestFocus()
|
|
430
|
-
// Set cursor at the end of text
|
|
431
|
-
text?.length?.let { setSelection(it) }
|
|
432
371
|
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
|
|
433
372
|
imm?.showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
|
434
373
|
}
|
|
@@ -439,9 +378,6 @@ class HighlightTextView : AppCompatEditText {
|
|
|
439
378
|
val text = text?.toString() ?: return
|
|
440
379
|
if (text.isEmpty()) return
|
|
441
380
|
|
|
442
|
-
// Save current cursor position
|
|
443
|
-
val currentSelection = selectionStart
|
|
444
|
-
|
|
445
381
|
val spannable = SpannableString(text)
|
|
446
382
|
|
|
447
383
|
// Apply line height if specified
|
|
@@ -493,11 +429,16 @@ class HighlightTextView : AppCompatEditText {
|
|
|
493
429
|
}
|
|
494
430
|
}
|
|
495
431
|
|
|
432
|
+
// Save current selection to prevent cursor jumping (smooth editing)
|
|
433
|
+
val currentSelection = selectionStart
|
|
434
|
+
|
|
496
435
|
isUpdatingText = true
|
|
497
436
|
setText(spannable)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
437
|
+
|
|
438
|
+
// Restore cursor position if valid (prevents jerking during editing)
|
|
439
|
+
if (currentSelection >= 0 && currentSelection <= text.length) {
|
|
440
|
+
setSelection(currentSelection)
|
|
441
|
+
}
|
|
501
442
|
isUpdatingText = false
|
|
502
443
|
|
|
503
444
|
// Detect line wraps after layout is ready
|
|
@@ -521,13 +462,19 @@ class HighlightTextView : AppCompatEditText {
|
|
|
521
462
|
|
|
522
463
|
// Check for manual line break (\n) before this character
|
|
523
464
|
val hasNewlineBefore = spanStart > 0 && textStr[spanStart - 1] == '\n'
|
|
524
|
-
// Check for manual line break (\n) after this character
|
|
465
|
+
// Check for manual line break (\n) after this character
|
|
525
466
|
val hasNewlineAfter = spanEnd < textStr.length && textStr[spanEnd] == '\n'
|
|
526
467
|
|
|
468
|
+
// Check if this is the last line of text
|
|
469
|
+
val isLastLine = line == layout.lineCount - 1
|
|
470
|
+
|
|
527
471
|
// Check if this char is at start of visual line (wrapped OR after \n)
|
|
528
472
|
val isAtLineStart = (spanStart == lineStart && !span.isFirstInGroup) || hasNewlineBefore
|
|
529
|
-
|
|
530
|
-
|
|
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)
|
|
531
478
|
|
|
532
479
|
if (isAtLineStart || isAtLineEnd) {
|
|
533
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;
|
|
@@ -528,51 +518,8 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
528
518
|
newFont = [UIFont systemFontOfSize:fontSize weight:fontWeight];
|
|
529
519
|
}
|
|
530
520
|
|
|
531
|
-
// Save current text and selection
|
|
532
|
-
NSString *currentText = [_textView.text copy];
|
|
533
|
-
NSRange currentSelection = _textView.selectedRange;
|
|
534
|
-
|
|
535
|
-
// Update font on text view
|
|
536
521
|
_textView.font = newFont;
|
|
537
|
-
|
|
538
|
-
// CRITICAL: Force complete rebuild by removing and re-adding the text container
|
|
539
|
-
// This ensures all glyph and layout caches are cleared for the new font metrics
|
|
540
|
-
NSTextContainer *textContainer = _textView.textContainer;
|
|
541
|
-
NSTextStorage *textStorage = _textView.textStorage;
|
|
542
|
-
|
|
543
|
-
// Remove text container from layout manager
|
|
544
|
-
[_layoutManager removeTextContainerAtIndex:0];
|
|
545
|
-
|
|
546
|
-
// Completely invalidate all layout
|
|
547
|
-
[_layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, textStorage.length) actualCharacterRange:NULL];
|
|
548
|
-
[_layoutManager invalidateDisplayForCharacterRange:NSMakeRange(0, textStorage.length)];
|
|
549
|
-
|
|
550
|
-
// Re-add text container - forces complete layout recalculation
|
|
551
|
-
[_layoutManager addTextContainer:textContainer];
|
|
552
|
-
|
|
553
|
-
// Force immediate layout with new font metrics
|
|
554
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
555
|
-
// Restore text through applyCharacterBackgrounds which rebuilds attributed string
|
|
556
|
-
[self applyCharacterBackgrounds];
|
|
557
|
-
|
|
558
|
-
// Ensure layout is calculated with new font
|
|
559
|
-
[self->_layoutManager ensureLayoutForTextContainer:self->_textView.textContainer];
|
|
560
|
-
|
|
561
|
-
// Restore selection if valid
|
|
562
|
-
if (currentSelection.location <= currentText.length) {
|
|
563
|
-
self->_textView.selectedRange = currentSelection;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Force complete redraw
|
|
567
|
-
[self->_textView setNeedsDisplay];
|
|
568
|
-
[self->_textView setNeedsLayout];
|
|
569
|
-
[self->_textView layoutIfNeeded];
|
|
570
|
-
|
|
571
|
-
// Trigger another display update to ensure rendering
|
|
572
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
573
|
-
[self->_textView setNeedsDisplay];
|
|
574
|
-
});
|
|
575
|
-
});
|
|
522
|
+
[self applyCharacterBackgrounds];
|
|
576
523
|
}
|
|
577
524
|
|
|
578
525
|
- (void)applyCharacterBackgrounds
|
|
@@ -582,9 +529,6 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
582
529
|
return;
|
|
583
530
|
}
|
|
584
531
|
|
|
585
|
-
// Save current cursor position
|
|
586
|
-
NSRange currentSelection = _textView.selectedRange;
|
|
587
|
-
|
|
588
532
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
|
|
589
533
|
UIColor *bgColor = [self hexStringToColor:_characterBackgroundColor];
|
|
590
534
|
|
|
@@ -633,12 +577,6 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
633
577
|
}
|
|
634
578
|
|
|
635
579
|
_textView.attributedText = attributedString;
|
|
636
|
-
|
|
637
|
-
// Restore cursor position if valid
|
|
638
|
-
if (currentSelection.location <= text.length) {
|
|
639
|
-
_textView.selectedRange = currentSelection;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
580
|
[_textView setNeedsDisplay];
|
|
643
581
|
}
|
|
644
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",
|