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 (respects line wraps)
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, just rectangle
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
- // Restore cursor position, or set to end if position is invalid
479
- val safePosition = currentSelection.coerceIn(0, text.length)
480
- setSelection(safePosition)
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
- // Check if this char is at end of visual line (wrapped OR before \n)
510
- val isAtLineEnd = (spanEnd == lineEnd && !span.isLastInGroup) || hasNewlineAfter
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
@@ -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.18",
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",