react-native-enriched 0.1.5 → 0.1.6

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.
Files changed (38) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -0
  3. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -0
  5. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +34 -10
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +16 -19
  8. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
  9. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +61 -46
  10. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +18 -2
  11. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +23 -3
  12. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +0 -1
  13. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  14. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  15. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  16. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  17. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  18. package/ios/EnrichedTextInputView.h +0 -1
  19. package/ios/EnrichedTextInputView.mm +71 -40
  20. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  21. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -0
  22. package/ios/inputParser/InputParser.mm +32 -7
  23. package/ios/inputTextView/InputTextView.mm +3 -5
  24. package/ios/styles/LinkStyle.mm +7 -7
  25. package/ios/styles/OrderedListStyle.mm +3 -8
  26. package/ios/styles/UnorderedListStyle.mm +3 -8
  27. package/ios/utils/StringExtension.h +1 -1
  28. package/ios/utils/StringExtension.mm +17 -8
  29. package/lib/module/EnrichedTextInput.js +2 -0
  30. package/lib/module/EnrichedTextInput.js.map +1 -1
  31. package/lib/module/EnrichedTextInputNativeComponent.ts +1 -0
  32. package/lib/typescript/src/EnrichedTextInput.d.ts +2 -1
  33. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  34. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -0
  35. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/EnrichedTextInput.tsx +3 -0
  38. package/src/EnrichedTextInputNativeComponent.ts +1 -0
@@ -350,6 +350,7 @@ class HtmlToSpannedConverter implements ContentHandler {
350
350
  private final EnrichedParser.ImageGetter mImageGetter;
351
351
  private static Integer currentOrderedListItemIndex = 0;
352
352
  private static Boolean isInOrderedList = false;
353
+ private static Boolean isEmptyTag = false;
353
354
 
354
355
  public HtmlToSpannedConverter(String source, HtmlStyle style, EnrichedParser.ImageGetter imageGetter, Parser parser) {
355
356
  mStyle = style;
@@ -396,19 +397,27 @@ class HtmlToSpannedConverter implements ContentHandler {
396
397
  for (EnrichedZeroWidthSpaceSpan zeroWidthSpaceSpan : zeroWidthSpaceSpans) {
397
398
  int start = mSpannableStringBuilder.getSpanStart(zeroWidthSpaceSpan);
398
399
  int end = mSpannableStringBuilder.getSpanEnd(zeroWidthSpaceSpan);
399
- mSpannableStringBuilder.insert(start, "\u200B");
400
+
401
+ if (mSpannableStringBuilder.charAt(start) != '\u200B') {
402
+ // Insert zero-width space character at the start if it's not already present.
403
+ mSpannableStringBuilder.insert(start, "\u200B");
404
+ end++; // Adjust end position due to insertion.
405
+ }
406
+
400
407
  mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan);
401
- mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
408
+ mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
402
409
  }
403
410
 
404
411
  return mSpannableStringBuilder;
405
412
  }
406
413
 
407
414
  private void handleStartTag(String tag, Attributes attributes) {
415
+ isEmptyTag = false;
408
416
  if (tag.equalsIgnoreCase("br")) {
409
417
  // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
410
418
  // so we can safely emit the linebreaks when we handle the close tag.
411
419
  } else if (tag.equalsIgnoreCase("p")) {
420
+ isEmptyTag = true;
412
421
  startBlockElement(mSpannableStringBuilder);
413
422
  } else if (tag.equalsIgnoreCase("ul")) {
414
423
  isInOrderedList = false;
@@ -418,14 +427,17 @@ class HtmlToSpannedConverter implements ContentHandler {
418
427
  currentOrderedListItemIndex = 0;
419
428
  startBlockElement(mSpannableStringBuilder);
420
429
  } else if (tag.equalsIgnoreCase("li")) {
430
+ isEmptyTag = true;
421
431
  startLi(mSpannableStringBuilder);
422
432
  } else if (tag.equalsIgnoreCase("b")) {
423
433
  start(mSpannableStringBuilder, new Bold());
424
434
  } else if (tag.equalsIgnoreCase("i")) {
425
435
  start(mSpannableStringBuilder, new Italic());
426
436
  } else if (tag.equalsIgnoreCase("blockquote")) {
437
+ isEmptyTag = true;
427
438
  startBlockquote(mSpannableStringBuilder);
428
439
  } else if (tag.equalsIgnoreCase("codeblock")) {
440
+ isEmptyTag = true;
429
441
  startCodeBlock(mSpannableStringBuilder);
430
442
  } else if (tag.equalsIgnoreCase("a")) {
431
443
  startA(mSpannableStringBuilder, attributes);
@@ -632,11 +644,17 @@ class HtmlToSpannedConverter implements ContentHandler {
632
644
  }
633
645
  }
634
646
 
635
- private static void setParagraphSpanFromMark(Spannable text, Object mark, Object... spans) {
647
+ private static void setParagraphSpanFromMark(Editable text, Object mark, Object... spans) {
636
648
  int where = text.getSpanStart(mark);
637
649
  text.removeSpan(mark);
638
650
  int len = text.length();
639
651
 
652
+ // Block spans require at least one character to be applied.
653
+ if (isEmptyTag) {
654
+ text.append("\u200B");
655
+ len++;
656
+ }
657
+
640
658
  // Adjust the end position to exclude the newline character, if present
641
659
  if (len > 0 && text.charAt(len - 1) == '\n') {
642
660
  len--;
@@ -741,6 +759,8 @@ class HtmlToSpannedConverter implements ContentHandler {
741
759
 
742
760
  public void characters(char[] ch, int start, int length) {
743
761
  StringBuilder sb = new StringBuilder();
762
+ if (length > 0) isEmptyTag = false;
763
+
744
764
  /*
745
765
  * Ignore whitespace that immediately follows other whitespace;
746
766
  * newlines count as spaces.
@@ -59,7 +59,6 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
59
59
  if (html == previousHtml) return
60
60
 
61
61
  previousHtml = html
62
- view.layoutManager.invalidateLayout(view.text)
63
62
  val context = view.context as ReactContext
64
63
  val surfaceId = UIManagerHelper.getSurfaceId(context)
65
64
  val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
@@ -17,7 +17,7 @@ class EnrichedTextWatcher(private val view: EnrichedTextInputView) : TextWatcher
17
17
 
18
18
  override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
19
19
  endCursorPosition = start + count
20
- view.layoutManager.measureSize(s ?: "")
20
+ view.layoutManager.invalidateLayout()
21
21
  view.isRemovingMany = !view.isDuringTransaction && before > count + 1
22
22
  }
23
23
 
@@ -1,4 +1,5 @@
1
1
  #include "EnrichedTextInputMeasurementManager.h"
2
+ #include "conversions.h"
2
3
 
3
4
  #include <fbjni/fbjni.h>
4
5
  #include <react/jni/ReadableNativeMap.h>
@@ -10,6 +11,7 @@ namespace facebook::react {
10
11
 
11
12
  Size EnrichedTextInputMeasurementManager::measure(
12
13
  SurfaceId surfaceId,
14
+ int viewTag,
13
15
  const EnrichedTextInputViewProps& props,
14
16
  LayoutConstraints layoutConstraints) const {
15
17
  const jni::global_ref<jobject>& fabricUIManager =
@@ -33,12 +35,23 @@ namespace facebook::react {
33
35
 
34
36
  local_ref<JString> componentName = make_jstring("EnrichedTextInputView");
35
37
 
38
+ // Prepare extraData map with viewTag
39
+ folly::dynamic extraData = folly::dynamic::object();
40
+ extraData["viewTag"] = viewTag;
41
+ local_ref<ReadableNativeMap::javaobject> extraDataRNM = ReadableNativeMap::newObjectCxxArgs(extraData);
42
+ local_ref<ReadableMap::javaobject> extraDataRM = make_local(reinterpret_cast<ReadableMap::javaobject>(extraDataRNM.get()));
43
+
44
+ // Prepare layout metrics affecting props
45
+ auto serializedProps = toDynamic(props);
46
+ local_ref<ReadableNativeMap::javaobject> propsRNM = ReadableNativeMap::newObjectCxxArgs(serializedProps);
47
+ local_ref<ReadableMap::javaobject> propsRM = make_local(reinterpret_cast<ReadableMap::javaobject>(propsRNM.get()));
48
+
36
49
  auto measurement = yogaMeassureToSize(measure(
37
50
  fabricUIManager,
38
51
  surfaceId,
39
52
  componentName.get(),
40
- nullptr,
41
- nullptr,
53
+ extraDataRM.get(),
54
+ propsRM.get(),
42
55
  nullptr,
43
56
  minimumSize.width,
44
57
  maximumSize.width,
@@ -16,6 +16,7 @@ namespace facebook::react {
16
16
 
17
17
  Size measure(
18
18
  SurfaceId surfaceId,
19
+ int viewTag,
19
20
  const EnrichedTextInputViewProps& props,
20
21
  LayoutConstraints layoutConstraints) const;
21
22
 
@@ -27,8 +27,7 @@ extern const char EnrichedTextInputComponentName[] = "EnrichedTextInputView";
27
27
  Size EnrichedTextInputShadowNode::measureContent(
28
28
  const LayoutContext &layoutContext,
29
29
  const LayoutConstraints &layoutConstraints) const {
30
-
31
- return measurementsManager_->measure(getSurfaceId(), getConcreteProps(), layoutConstraints);
30
+ return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
32
31
  }
33
32
 
34
33
  } // namespace facebook::react
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+
3
+ #include <folly/dynamic.h>
4
+ #include <react/renderer/components/FBReactNativeSpec/Props.h>
5
+ #include <react/renderer/core/propsConversions.h>
6
+ #include <react/renderer/components/RNEnrichedTextInputViewSpec/Props.h>
7
+
8
+ namespace facebook::react {
9
+
10
+ #ifdef RN_SERIALIZABLE_STATE
11
+ inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props)
12
+ {
13
+ // Serialize only metrics affecting props
14
+ folly::dynamic serializedProps = folly::dynamic::object();
15
+ serializedProps["defaultValue"] = props.defaultValue;
16
+ serializedProps["placeholder"] = props.placeholder;
17
+ serializedProps["fontSize"] = props.fontSize;
18
+ serializedProps["fontWeight"] = props.fontWeight;
19
+ serializedProps["fontStyle"] = props.fontStyle;
20
+ serializedProps["fontFamily"] = props.fontFamily;
21
+ serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
22
+
23
+ return serializedProps;
24
+ }
25
+ #endif
26
+
27
+ } // namespace facebook::react
@@ -19,7 +19,6 @@ NS_ASSUME_NONNULL_BEGIN
19
19
  @public NSMutableDictionary<NSAttributedStringKey, id> *defaultTypingAttributes;
20
20
  @public NSDictionary<NSNumber *, id<BaseStyleProtocol>> *stylesDict;
21
21
  @public BOOL blockEmitting;
22
- @public BOOL emitHtml;
23
22
  }
24
23
  - (CGSize)measureSize:(CGFloat)maxWidth;
25
24
  - (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range;
@@ -34,6 +34,7 @@ using namespace facebook::react;
34
34
  MentionParams *_recentlyActiveMentionParams;
35
35
  NSRange _recentlyActiveMentionRange;
36
36
  NSString *_recentlyEmittedHtml;
37
+ BOOL _emitHtml;
37
38
  UILabel *_placeholderLabel;
38
39
  UIColor *_placeholderColor;
39
40
  BOOL _emitFocusBlur;
@@ -74,8 +75,8 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
74
75
  _recentlyActiveMentionRange = NSMakeRange(0, 0);
75
76
  recentlyChangedRange = NSMakeRange(0, 0);
76
77
  _recentlyEmittedString = @"";
77
- _recentlyEmittedHtml = @"";
78
- emitHtml = NO;
78
+ _recentlyEmittedHtml = @"<html>\n<p></p>\n</html>";
79
+ _emitHtml = NO;
79
80
  blockEmitting = NO;
80
81
  _emitFocusBlur = YES;
81
82
 
@@ -357,6 +358,10 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
357
358
  stylePropChanged = YES;
358
359
  }
359
360
 
361
+ if(newViewProps.scrollEnabled != oldViewProps.scrollEnabled || textView.scrollEnabled != newViewProps.scrollEnabled) {
362
+ [textView setScrollEnabled:newViewProps.scrollEnabled];
363
+ }
364
+
360
365
  folly::dynamic oldMentionStyle = oldViewProps.htmlStyle.mention;
361
366
  folly::dynamic newMentionStyle = newViewProps.htmlStyle.mention;
362
367
  if(oldMentionStyle != newMentionStyle) {
@@ -393,22 +398,17 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
393
398
 
394
399
  // now set the new config
395
400
  config = newConfig;
396
-
397
- // we don't want to emit these html changes in here
398
- BOOL prevEmitHtml = emitHtml;
399
- if(prevEmitHtml) {
400
- emitHtml = NO;
401
- }
402
-
401
+
402
+ // no emitting during styles reload
403
+ blockEmitting = YES;
404
+
403
405
  // make sure everything is sound in the html
404
406
  NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:currentHtml];
405
407
  if(initiallyProcessedHtml != nullptr) {
406
408
  [parser replaceWholeFromHtml:initiallyProcessedHtml];
407
409
  }
408
410
 
409
- if(prevEmitHtml) {
410
- emitHtml = YES;
411
- }
411
+ blockEmitting = NO;
412
412
 
413
413
  // fill the typing attributes with style props
414
414
  defaultTypingAttributes[NSForegroundColorAttributeName] = [config primaryColor];
@@ -509,7 +509,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
509
509
  }
510
510
 
511
511
  // isOnChangeHtmlSet
512
- emitHtml = newViewProps.isOnChangeHtmlSet;
512
+ _emitHtml = newViewProps.isOnChangeHtmlSet;
513
513
 
514
514
  [super updateProps:props oldProps:oldProps];
515
515
  // mandatory text and height checks
@@ -605,6 +605,9 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
605
605
  // style updates are emitted only if something differs from the previously active styles
606
606
  BOOL updateNeeded = NO;
607
607
 
608
+ // active styles are kept in a separate set until we're sure they can be emitted
609
+ NSMutableSet *newActiveStyles = [_activeStyles mutableCopy];
610
+
608
611
  // data for onLinkDetected event
609
612
  LinkData *detectedLinkData;
610
613
  NSRange detectedLinkRange = NSMakeRange(0, 0);
@@ -615,14 +618,14 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
615
618
 
616
619
  for (NSNumber* type in stylesDict) {
617
620
  id<BaseStyleProtocol> style = stylesDict[type];
618
- BOOL wasActive = [_activeStyles containsObject: type];
621
+ BOOL wasActive = [newActiveStyles containsObject: type];
619
622
  BOOL isActive = [style detectStyle:textView.selectedRange];
620
623
  if(wasActive != isActive) {
621
624
  updateNeeded = YES;
622
625
  if(isActive) {
623
- [_activeStyles addObject:type];
626
+ [newActiveStyles addObject:type];
624
627
  } else {
625
- [_activeStyles removeObject:type];
628
+ [newActiveStyles removeObject:type];
626
629
  }
627
630
  }
628
631
 
@@ -682,6 +685,9 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
682
685
  if(updateNeeded) {
683
686
  auto emitter = [self getEventEmitter];
684
687
  if(emitter != nullptr) {
688
+ // update activeStyles only if emitter is available
689
+ _activeStyles = newActiveStyles;
690
+
685
691
  emitter->onChangeState({
686
692
  .isBold = [_activeStyles containsObject: @([BoldStyle getStyleType])],
687
693
  .isItalic = [_activeStyles containsObject: @([ItalicStyle getStyleType])],
@@ -705,9 +711,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
705
711
  if(detectedLinkData != nullptr) {
706
712
  // emit onLinkeDetected event
707
713
  [self emitOnLinkDetectedEvent:detectedLinkData.text url:detectedLinkData.url range:detectedLinkRange];
708
-
709
- _recentlyActiveLinkData = detectedLinkData;
710
- _recentlyActiveLinkRange = detectedLinkRange;
711
714
  }
712
715
 
713
716
  if(detectedMentionParams != nullptr) {
@@ -806,6 +809,13 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
806
809
  - (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range {
807
810
  auto emitter = [self getEventEmitter];
808
811
  if(emitter != nullptr) {
812
+ // update recently active link info
813
+ LinkData *newLinkData = [[LinkData alloc] init];
814
+ newLinkData.text = text;
815
+ newLinkData.url = url;
816
+ _recentlyActiveLinkData = newLinkData;
817
+ _recentlyActiveLinkRange = range;
818
+
809
819
  emitter->onLinkDetected({
810
820
  .text = [text toCppString],
811
821
  .url = [url toCppString],
@@ -846,7 +856,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
846
856
  }
847
857
 
848
858
  - (void)tryEmittingOnChangeHtmlEvent {
849
- if(!emitHtml || textView.markedTextRange != nullptr) {
859
+ if(!_emitHtml || textView.markedTextRange != nullptr) {
850
860
  return;
851
861
  }
852
862
  auto emitter = [self getEventEmitter];
@@ -994,6 +1004,9 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
994
1004
  textView.typingAttributes = defaultTypingAttributes;
995
1005
  }
996
1006
  }
1007
+
1008
+ // update active styles as well
1009
+ [self tryUpdatingActiveStyles];
997
1010
  }
998
1011
 
999
1012
  - (void)handleWordModificationBasedChanges:(NSString*)word inRange:(NSRange)range {
@@ -1043,6 +1056,12 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1043
1056
  [h3Style handleImproperHeadings];
1044
1057
  }
1045
1058
 
1059
+ // mentions removal management
1060
+ MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
1061
+ if(mentionStyleClass != nullptr) {
1062
+ [mentionStyleClass handleExistingMentions];
1063
+ }
1064
+
1046
1065
  // placholder management
1047
1066
  if(!_placeholderLabel.hidden && textView.textStorage.string.length > 0) {
1048
1067
  [self setPlaceholderLabelShown:NO];
@@ -1051,12 +1070,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1051
1070
  }
1052
1071
 
1053
1072
  if(![textView.textStorage.string isEqualToString:_recentlyEmittedString]) {
1054
- // mentions removal management
1055
- MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
1056
- if(mentionStyleClass != nullptr) {
1057
- [mentionStyleClass handleExistingMentions];
1058
- }
1059
-
1060
1073
  // modified words handling
1061
1074
  NSArray *modifiedWords = [WordsUtils getAffectedWordsFromText:textView.textStorage.string modificationRange:recentlyChangedRange];
1062
1075
  if(modifiedWords != nullptr) {
@@ -1078,28 +1091,49 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1078
1091
  // emit onChangeText event
1079
1092
  auto emitter = [self getEventEmitter];
1080
1093
  if(emitter != nullptr) {
1094
+ // set the recently emitted string only if the emitter is defined
1095
+ _recentlyEmittedString = stringToBeEmitted;
1096
+
1081
1097
  emitter->onChangeText({
1082
1098
  .value = [stringToBeEmitted toCppString]
1083
1099
  });
1084
1100
  }
1085
-
1086
- // set the recently emitted string
1087
- _recentlyEmittedString = stringToBeEmitted;
1088
1101
  }
1089
1102
 
1090
1103
  // update height on each character change
1091
1104
  [self tryUpdatingHeight];
1092
1105
  // update active styles as well
1093
1106
  [self tryUpdatingActiveStyles];
1107
+ // update drawing - schedule debounced relayout
1108
+ [self scheduleRelayoutIfNeeded];
1109
+ }
1094
1110
 
1095
- // update drawing
1096
- dispatch_async(dispatch_get_main_queue(), ^{
1097
- NSRange wholeRange = NSMakeRange(0, textView.textStorage.string.length);
1098
- NSRange actualRange = NSMakeRange(0, 0);
1099
- [textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
1100
- [textView.layoutManager ensureLayoutForCharacterRange:actualRange];
1101
- [textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
1102
- });
1111
+ // Debounced relayout helper - coalesces multiple requests into one per runloop tick
1112
+ - (void)scheduleRelayoutIfNeeded
1113
+ {
1114
+ // Cancel any previously scheduled invocation to debounce
1115
+ [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_performRelayout) object:nil];
1116
+ // Schedule on next runloop cycle
1117
+ [self performSelector:@selector(_performRelayout) withObject:nil afterDelay:0];
1118
+ }
1119
+
1120
+ - (void)_performRelayout
1121
+ {
1122
+ if (!textView) { return; }
1123
+
1124
+ dispatch_async(dispatch_get_main_queue(), ^{
1125
+ NSRange wholeRange = NSMakeRange(0, self->textView.textStorage.string.length);
1126
+ NSRange actualRange = NSMakeRange(0, 0);
1127
+ [self->textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
1128
+ [self->textView.layoutManager ensureLayoutForCharacterRange:actualRange];
1129
+ [self->textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
1130
+ });
1131
+ }
1132
+
1133
+ - (void)didMoveToWindow {
1134
+ [super didMoveToWindow];
1135
+ // used to run all lifecycle callbacks
1136
+ [self anyTextMayHaveBeenModified];
1103
1137
  }
1104
1138
 
1105
1139
  // MARK: - UITextView delegate methods
@@ -1184,9 +1218,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1184
1218
 
1185
1219
  // manage selection changes
1186
1220
  [self manageSelectionBasedChanges];
1187
-
1188
- // update active styles
1189
- [self tryUpdatingActiveStyles];
1190
1221
  }
1191
1222
 
1192
1223
  // this function isn't called always when some text changes (for example setting link or starting mention with indicator doesn't fire it)
@@ -30,6 +30,7 @@ EnrichedTextInputViewProps::EnrichedTextInputViewProps(
30
30
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
31
31
  autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
32
32
  htmlStyle(convertRawProp(context, rawProps, "htmlStyle", sourceProps.htmlStyle, {})),
33
+ scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
33
34
  color(convertRawProp(context, rawProps, "color", sourceProps.color, {})),
34
35
  fontSize(convertRawProp(context, rawProps, "fontSize", sourceProps.fontSize, {0.0})),
35
36
  fontFamily(convertRawProp(context, rawProps, "fontFamily", sourceProps.fontFamily, {})),
@@ -94,6 +95,10 @@ folly::dynamic EnrichedTextInputViewProps::getDiffProps(
94
95
  result["htmlStyle"] = toDynamic(htmlStyle);
95
96
  }
96
97
 
98
+ if (scrollEnabled != oldProps->scrollEnabled) {
99
+ result["scrollEnabled"] = scrollEnabled;
100
+ }
101
+
97
102
  if (color != oldProps->color) {
98
103
  result["color"] = *color;
99
104
  }
@@ -559,6 +559,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
559
559
  SharedColor selectionColor{};
560
560
  std::string autoCapitalize{};
561
561
  EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
562
+ bool scrollEnabled{false};
562
563
  SharedColor color{};
563
564
  Float fontSize{0.0};
564
565
  std::string fontFamily{};
@@ -3,6 +3,7 @@
3
3
  #import "StyleHeaders.h"
4
4
  #import "UIView+React.h"
5
5
  #import "TextInsertionUtils.h"
6
+ #import "StringExtension.h"
6
7
 
7
8
  @implementation InputParser {
8
9
  EnrichedTextInputView *_input;
@@ -204,8 +205,8 @@
204
205
  [result appendString: [NSString stringWithFormat:@"<%@>", tagContent]];
205
206
  }
206
207
 
207
- // append the letter
208
- [result appendString:currentCharacterStr];
208
+ // append the letter and escape it if needed
209
+ [result appendString: [NSString stringByEscapingHtml:currentCharacterStr]];
209
210
 
210
211
  // save current styles for next character's checks
211
212
  previousActiveStyles = currentActiveStyles;
@@ -490,6 +491,7 @@
490
491
  BOOL closingTag = NO;
491
492
  NSMutableString *currentTagName = [[NSMutableString alloc] initWithString:@""];
492
493
  NSMutableString *currentTagParams = [[NSMutableString alloc] initWithString:@""];
494
+ NSDictionary *htmlEntitiesDict = [NSString getEscapedCharactersInfoFrom:fixedHtml];
493
495
 
494
496
  // firstly, extract text and initially processed tags
495
497
  for(int i = 0; i < fixedHtml.length; i++) {
@@ -511,7 +513,7 @@
511
513
  } else if(!closingTag) {
512
514
  // we finish opening tag - get its location and optionally params and put them under tag name key in ongoingTags
513
515
  NSMutableArray *tagArr = [[NSMutableArray alloc] init];
514
- [tagArr addObject:[NSNumber numberWithInt:plainText.length]];
516
+ [tagArr addObject:[NSNumber numberWithInteger:plainText.length]];
515
517
  if(currentTagParams.length > 0) {
516
518
  [tagArr addObject:[currentTagParams copy]];
517
519
  }
@@ -550,8 +552,19 @@
550
552
  currentTagParams = [[NSMutableString alloc] initWithString:@""];
551
553
  } else {
552
554
  if(!insideTag) {
553
- // no tags logic - just append text
554
- [plainText appendString:currentCharacterStr];
555
+ // no tags logic - just append the right text
556
+
557
+ // html entity on the index; use unescaped character and forward iterator accordingly
558
+ NSArray *entityInfo = htmlEntitiesDict[@(i)];
559
+ if(entityInfo != nullptr) {
560
+ NSString *escaped = entityInfo[0];
561
+ NSString *unescaped = entityInfo[1];
562
+ [plainText appendString:unescaped];
563
+ // the iterator will forward by 1 itself
564
+ i += escaped.length - 1;
565
+ } else {
566
+ [plainText appendString:currentCharacterStr];
567
+ }
555
568
  } else {
556
569
  if(gettingTagName) {
557
570
  if(currentCharacterChar == ' ') {
@@ -597,9 +610,21 @@
597
610
  } else if([tagName isEqualToString:@"code"]) {
598
611
  [styleArr addObject:@([InlineCodeStyle getStyleType])];
599
612
  } else if([tagName isEqualToString:@"a"]) {
613
+ NSRegularExpression *hrefRegex = [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
614
+ options:0
615
+ error:nullptr
616
+ ];
617
+ NSTextCheckingResult* match = [hrefRegex firstMatchInString:params options:0 range: NSMakeRange(0, params.length)];
618
+
619
+ if(match == nullptr) {
620
+ // same as on Android, no href (or empty href) equals no link style
621
+ continue;
622
+ }
623
+
624
+ NSRange hrefRange = match.range;
600
625
  [styleArr addObject:@([LinkStyle getStyleType])];
601
626
  // cut only the url from the href="..." string
602
- NSString *url = [params substringWithRange:NSMakeRange(6, params.length - 7)];
627
+ NSString *url = [params substringWithRange:NSMakeRange(hrefRange.location + 6, hrefRange.length - 7)];
603
628
  stylePair.styleValue = url;
604
629
  } else if([tagName isEqualToString:@"mention"]) {
605
630
  [styleArr addObject:@([MentionStyle getStyleType])];
@@ -644,7 +669,7 @@
644
669
  } else if([tagName isEqualToString:@"blockquote"]) {
645
670
  [styleArr addObject:@([BlockQuoteStyle getStyleType])];
646
671
  } else {
647
- // some other external tags like span just don't get put into the processed styles
672
+ // some other external tags like span just don't get put into the processed styles
648
673
  continue;
649
674
  }
650
675
 
@@ -14,7 +14,7 @@
14
14
  NSString *plainText = [typedInput->textView.textStorage.string substringWithRange:typedInput->textView.selectedRange];
15
15
  NSString *fixedPlainText = [plainText stringByReplacingOccurrencesOfString:@"\u200B" withString:@""];
16
16
 
17
- NSString *escapedHtml = [NSString stringByEscapingHtml:[typedInput->parser parseToHtmlFromRange:typedInput->textView.selectedRange]];
17
+ NSString *parsedHtml = [typedInput->parser parseToHtmlFromRange:typedInput->textView.selectedRange];
18
18
 
19
19
  NSMutableAttributedString *attrStr = [[typedInput->textView.textStorage attributedSubstringFromRange:typedInput->textView.selectedRange] mutableCopy];
20
20
  NSRange fullAttrStrRange = NSMakeRange(0, attrStr.length);
@@ -28,7 +28,7 @@
28
28
  UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
29
29
  [pasteboard setItems:@[@{
30
30
  UTTypeUTF8PlainText.identifier : fixedPlainText,
31
- UTTypeHTML.identifier : escapedHtml,
31
+ UTTypeHTML.identifier : parsedHtml,
32
32
  UTTypeRTF.identifier : rtfData
33
33
  }]];
34
34
  }
@@ -53,9 +53,7 @@
53
53
  htmlString = htmlValue;
54
54
  }
55
55
 
56
- // unescape the html
57
- htmlString = [NSString stringByUnescapingHtml:htmlString];
58
- // validate it
56
+ // validate the html
59
57
  NSString *initiallyProcessedHtml = [typedInput->parser initiallyProcessHtml:htmlString];
60
58
 
61
59
  if(initiallyProcessedHtml != nullptr) {
@@ -167,7 +167,6 @@ static NSString *const AutomaticLinkAttributeName = @"AutomaticLinkAttributeName
167
167
  }
168
168
 
169
169
  [self manageLinkTypingAttributes];
170
- [_input anyTextMayHaveBeenModified];
171
170
  }
172
171
 
173
172
  // get exact link data at the given location if it exists
@@ -224,20 +223,22 @@ static NSString *const AutomaticLinkAttributeName = @"AutomaticLinkAttributeName
224
223
  }
225
224
  }
226
225
 
227
- [_input->textView.textStorage
226
+ NSString *manualLink = [_input->textView.textStorage
228
227
  attribute:ManualLinkAttributeName
229
228
  atIndex:searchLocation
230
229
  longestEffectiveRange: &manualLinkRange
231
230
  inRange:inputRange
232
231
  ];
233
- [_input->textView.textStorage
232
+ NSString *automaticLink = [_input->textView.textStorage
234
233
  attribute:AutomaticLinkAttributeName
235
234
  atIndex:searchLocation
236
235
  longestEffectiveRange: &automaticLinkRange
237
236
  inRange:inputRange
238
237
  ];
239
238
 
240
- return manualLinkRange.length == 0 ? automaticLinkRange : manualLinkRange;
239
+ return manualLink == nullptr
240
+ ? automaticLink == nullptr ? NSMakeRange(0, 0) : automaticLinkRange
241
+ : manualLinkRange;
241
242
  }
242
243
 
243
244
  - (void)manageLinkTypingAttributes {
@@ -349,10 +350,9 @@ static NSString *const AutomaticLinkAttributeName = @"AutomaticLinkAttributeName
349
350
  }
350
351
  if(addStyle) {
351
352
  [self addLink:word url:regexPassedUrl range:wordRange manual:NO];
353
+ // emit onLinkDetected if style was added
354
+ [_input emitOnLinkDetectedEvent:word url:regexPassedUrl range:wordRange];
352
355
  }
353
-
354
- // emit onLinkDetected
355
- [_input emitOnLinkDetectedEvent:word url:regexPassedUrl range:wordRange];
356
356
  }
357
357
  }
358
358
 
@@ -158,18 +158,13 @@
158
158
  if(charBefore == '1') {
159
159
  // we got a match - add a list if possible
160
160
  if([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] range:paragraphRange]) {
161
- // don't emit some html updates during the replacing
162
- BOOL prevEmitHtml = _input->emitHtml;
163
- if(prevEmitHtml) {
164
- _input->emitHtml = NO;
165
- }
161
+ // don't emit during the replacing
162
+ _input->blockEmitting = YES;
166
163
 
167
164
  // remove the number
168
165
  [TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr input:_input withSelection:YES];
169
166
 
170
- if(prevEmitHtml) {
171
- _input->emitHtml = YES;
172
- }
167
+ _input->blockEmitting = NO;
173
168
 
174
169
  // add attributes on the paragraph
175
170
  [self addAttributes:NSMakeRange(paragraphRange.location, paragraphRange.length - 1)];