react-native-enriched 0.1.4 → 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 (44) 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 +65 -28
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -46
  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/EnrichedBlockQuoteSpan.kt +6 -2
  10. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +9 -5
  11. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +10 -5
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +11 -1
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +61 -46
  14. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +11 -1
  15. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +1 -2
  16. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +29 -12
  17. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +23 -3
  18. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +0 -1
  19. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  20. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  21. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  22. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  23. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  24. package/ios/EnrichedTextInputView.h +0 -1
  25. package/ios/EnrichedTextInputView.mm +72 -36
  26. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  27. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -0
  28. package/ios/inputParser/InputParser.mm +32 -7
  29. package/ios/inputTextView/InputTextView.mm +3 -5
  30. package/ios/styles/LinkStyle.mm +7 -7
  31. package/ios/styles/OrderedListStyle.mm +3 -8
  32. package/ios/styles/UnorderedListStyle.mm +3 -8
  33. package/ios/utils/StringExtension.h +1 -1
  34. package/ios/utils/StringExtension.mm +17 -8
  35. package/lib/module/EnrichedTextInput.js +2 -0
  36. package/lib/module/EnrichedTextInput.js.map +1 -1
  37. package/lib/module/EnrichedTextInputNativeComponent.ts +1 -0
  38. package/lib/typescript/src/EnrichedTextInput.d.ts +2 -1
  39. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  40. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -0
  41. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  42. package/package.json +3 -2
  43. package/src/EnrichedTextInput.tsx +3 -0
  44. package/src/EnrichedTextInputNativeComponent.ts +1 -0
@@ -1,5 +1,7 @@
1
1
  package com.swmansion.enriched.spans
2
2
 
3
+ import com.swmansion.enriched.styles.HtmlStyle
4
+
3
5
  data class BaseSpanConfig(val clazz: Class<*>)
4
6
  data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
5
7
  data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
@@ -62,50 +64,63 @@ object EnrichedSpans {
62
64
  MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
63
65
  )
64
66
 
65
- val mergingConfig: Map<String, StylesMergingConfig> = mapOf(
66
- BOLD to StylesMergingConfig(
67
- blockingStyles = arrayOf(CODE_BLOCK)
68
- ),
69
- ITALIC to StylesMergingConfig(
70
- blockingStyles = arrayOf(CODE_BLOCK)
71
- ),
72
- UNDERLINE to StylesMergingConfig(
73
- blockingStyles = arrayOf(CODE_BLOCK)
74
- ),
75
- STRIKETHROUGH to StylesMergingConfig(
76
- blockingStyles = arrayOf(CODE_BLOCK)
77
- ),
78
- INLINE_CODE to StylesMergingConfig(
79
- conflictingStyles = arrayOf(MENTION, LINK),
80
- blockingStyles = arrayOf(CODE_BLOCK)
81
- ),
82
- H1 to StylesMergingConfig(
83
- conflictingStyles = arrayOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
84
- ),
85
- H2 to StylesMergingConfig(
86
- conflictingStyles = arrayOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
87
- ),
88
- H3 to StylesMergingConfig(
89
- conflictingStyles = arrayOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
90
- ),
91
- BLOCK_QUOTE to StylesMergingConfig(
92
- conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST),
93
- ),
94
- CODE_BLOCK to StylesMergingConfig(
95
- conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE),
96
- ),
97
- UNORDERED_LIST to StylesMergingConfig(
98
- conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
99
- ),
100
- ORDERED_LIST to StylesMergingConfig(
101
- conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
102
- ),
103
- LINK to StylesMergingConfig(
104
- blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
105
- ),
106
- IMAGE to StylesMergingConfig(),
107
- MENTION to StylesMergingConfig(
108
- blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
109
- ),
110
- )
67
+ fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
68
+ return when (style) {
69
+ BOLD -> {
70
+ val blockingStyles = mutableListOf(CODE_BLOCK)
71
+ if (htmlStyle.h1Bold) blockingStyles.add(H1)
72
+ if (htmlStyle.h2Bold) blockingStyles.add(H2)
73
+ if (htmlStyle.h3Bold) blockingStyles.add(H3)
74
+ StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
75
+ }
76
+ ITALIC -> StylesMergingConfig(
77
+ blockingStyles = arrayOf(CODE_BLOCK)
78
+ )
79
+ UNDERLINE -> StylesMergingConfig(
80
+ blockingStyles = arrayOf(CODE_BLOCK)
81
+ )
82
+ STRIKETHROUGH -> StylesMergingConfig(
83
+ blockingStyles = arrayOf(CODE_BLOCK)
84
+ )
85
+ INLINE_CODE -> StylesMergingConfig(
86
+ conflictingStyles = arrayOf(MENTION, LINK),
87
+ blockingStyles = arrayOf(CODE_BLOCK)
88
+ )
89
+ H1 -> {
90
+ val conflictingStyles = mutableListOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
91
+ if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
92
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
93
+ }
94
+ H2 -> {
95
+ val conflictingStyles = mutableListOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
96
+ if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
97
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
98
+ }
99
+ H3 -> {
100
+ val conflictingStyles = mutableListOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
101
+ if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
102
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
103
+ }
104
+ BLOCK_QUOTE -> StylesMergingConfig(
105
+ conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST)
106
+ )
107
+ CODE_BLOCK -> StylesMergingConfig(
108
+ conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE)
109
+ )
110
+ UNORDERED_LIST -> StylesMergingConfig(
111
+ conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE)
112
+ )
113
+ ORDERED_LIST -> StylesMergingConfig(
114
+ conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
115
+ )
116
+ LINK -> StylesMergingConfig(
117
+ blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
118
+ )
119
+ IMAGE -> StylesMergingConfig()
120
+ MENTION -> StylesMergingConfig(
121
+ blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
122
+ )
123
+ else -> null
124
+ }
125
+ }
111
126
  }
@@ -4,12 +4,22 @@ import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
5
  import android.text.Layout
6
6
  import android.text.Spanned
7
+ import android.text.TextPaint
7
8
  import android.text.style.LeadingMarginSpan
9
+ import android.text.style.MetricAffectingSpan
8
10
  import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
9
11
  import com.swmansion.enriched.styles.HtmlStyle
10
12
 
11
13
  // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/BulletSpan.java
12
- class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : LeadingMarginSpan, EnrichedParagraphSpan {
14
+ class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
15
+ override fun updateMeasureState(p0: TextPaint) {
16
+ // Do nothing, but inform layout that this span affects text metrics
17
+ }
18
+
19
+ override fun updateDrawState(p0: TextPaint?) {
20
+ // Do nothing, but inform layout that this span affects text metrics
21
+ }
22
+
13
23
  override fun getLeadingMargin(p0: Boolean): Int {
14
24
  return htmlStyle.ulBulletSize + htmlStyle.ulGapWidth + htmlStyle.ulMarginLeft
15
25
  }
@@ -58,12 +58,11 @@ class ListStyles(private val view: EnrichedTextInputView) {
58
58
  val spans = ssb.getSpans(start, end, clazz)
59
59
  if (spans.isEmpty()) return false
60
60
 
61
- ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
62
-
63
61
  for (span in spans) {
64
62
  ssb.removeSpan(span)
65
63
  }
66
64
 
65
+ ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
67
66
  return true
68
67
  }
69
68
 
@@ -104,15 +104,31 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
104
104
  return Triple(result, start, end)
105
105
  }
106
106
 
107
+ private fun canLinkBeApplied(): Boolean {
108
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
109
+ val conflictingStyles = mergingConfig.conflictingStyles
110
+ val blockingStyles = mergingConfig.blockingStyles
111
+
112
+ for (style in blockingStyles) {
113
+ if (view.spanState?.getStart(style) != null) return false
114
+ }
115
+
116
+ for (style in conflictingStyles) {
117
+ if (view.spanState?.getStart(style) != null) return false
118
+ }
119
+
120
+ return true
121
+ }
122
+
107
123
  private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
108
124
  // Do not detect link if it's applied manually
109
- if (isSettingLinkSpan) return
125
+ if (isSettingLinkSpan || !canLinkBeApplied()) return
126
+
110
127
  val spannable = view.text as Spannable
111
128
  val (word, start, end) = result
112
129
 
113
130
  // TODO: Consider using more reliable regex, this one matches almost anything
114
131
  val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
115
-
116
132
  val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
117
133
  for (span in spans) {
118
134
  spannable.removeSpan(span)
@@ -201,20 +217,21 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
201
217
  }
202
218
 
203
219
  val start = mentionStart ?: return
204
- view.isDuringTransaction = true
205
- spannable.replace(start, selectionEnd, text)
206
220
 
207
- val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle)
208
- val spanEnd = start + text.length
209
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
210
- spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
221
+ view.runAsATransaction {
222
+ spannable.replace(start, selectionEnd, text)
223
+
224
+ val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle)
225
+ val spanEnd = start + text.length
226
+ val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
227
+ spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
211
228
 
212
- val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
213
- if (!hasSpaceAtTheEnd) {
214
- spannable.insert(safeEnd, " ")
229
+ val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
230
+ if (!hasSpaceAtTheEnd) {
231
+ spannable.insert(safeEnd, " ")
232
+ }
215
233
  }
216
234
 
217
- view.isDuringTransaction = false
218
235
  view.mentionHandler?.reset()
219
236
  view.selection.validateStyles()
220
237
  }
@@ -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,23 +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];
1094
- // update drawing
1095
- NSRange wholeRange = NSMakeRange(0, textView.textStorage.string.length);
1096
- [textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:nullptr];
1097
- [textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
1107
+ // update drawing - schedule debounced relayout
1108
+ [self scheduleRelayoutIfNeeded];
1109
+ }
1110
+
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];
1098
1137
  }
1099
1138
 
1100
1139
  // MARK: - UITextView delegate methods
@@ -1179,9 +1218,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1179
1218
 
1180
1219
  // manage selection changes
1181
1220
  [self manageSelectionBasedChanges];
1182
-
1183
- // update active styles
1184
- [self tryUpdatingActiveStyles];
1185
1221
  }
1186
1222
 
1187
1223
  // this function isn't called always when some text changes (for example setting link or starting mention with indicator doesn't fire it)