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 (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,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
- // Restore cursor position, or set to end if position is invalid
499
- val safePosition = currentSelection.coerceIn(0, text.length)
500
- setSelection(safePosition)
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
- // Check if this char is at end of visual line (wrapped OR before \n)
530
- 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)
531
478
 
532
479
  if (isAtLineStart || isAtLineEnd) {
533
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;
@@ -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.19",
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",