react-native-enriched 0.1.5 → 0.2.0

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 (80) hide show
  1. package/README.md +3 -9
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -45
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +53 -12
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
  8. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +19 -22
  9. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
  10. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
  11. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +65 -46
  14. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
  15. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +2 -9
  16. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
  17. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +110 -3
  18. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +75 -32
  19. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
  20. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +38 -15
  21. package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
  22. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -1
  23. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  24. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  25. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  26. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  27. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  28. package/android/src/main/res/drawable/broken_image.xml +10 -0
  29. package/ios/EnrichedTextInputView.h +3 -1
  30. package/ios/EnrichedTextInputView.mm +167 -68
  31. package/ios/config/InputConfig.h +6 -0
  32. package/ios/config/InputConfig.mm +32 -0
  33. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  34. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -45
  35. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
  36. package/ios/inputParser/InputParser.mm +179 -31
  37. package/ios/inputTextView/InputTextView.mm +3 -5
  38. package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
  39. package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
  40. package/ios/styles/BlockQuoteStyle.mm +5 -26
  41. package/ios/styles/BoldStyle.mm +2 -0
  42. package/ios/styles/CodeBlockStyle.mm +228 -0
  43. package/ios/styles/H1Style.mm +1 -0
  44. package/ios/styles/H2Style.mm +1 -0
  45. package/ios/styles/H3Style.mm +1 -0
  46. package/ios/styles/ImageStyle.mm +158 -0
  47. package/ios/styles/InlineCodeStyle.mm +2 -0
  48. package/ios/styles/ItalicStyle.mm +2 -0
  49. package/ios/styles/LinkStyle.mm +15 -7
  50. package/ios/styles/MentionStyle.mm +133 -36
  51. package/ios/styles/OrderedListStyle.mm +5 -8
  52. package/ios/styles/StrikethroughStyle.mm +2 -0
  53. package/ios/styles/UnderlineStyle.mm +2 -0
  54. package/ios/styles/UnorderedListStyle.mm +5 -8
  55. package/ios/utils/BaseStyleProtocol.h +1 -0
  56. package/ios/utils/ImageData.h +10 -0
  57. package/ios/utils/ImageData.mm +4 -0
  58. package/ios/utils/LayoutManagerExtension.mm +118 -3
  59. package/ios/utils/OccurenceUtils.h +4 -0
  60. package/ios/utils/OccurenceUtils.mm +47 -0
  61. package/ios/utils/ParagraphAttributesUtils.h +1 -0
  62. package/ios/utils/ParagraphAttributesUtils.mm +87 -20
  63. package/ios/utils/StringExtension.h +1 -1
  64. package/ios/utils/StringExtension.mm +17 -8
  65. package/ios/utils/StyleHeaders.h +12 -0
  66. package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
  67. package/lib/module/EnrichedTextInput.js +4 -2
  68. package/lib/module/EnrichedTextInput.js.map +1 -1
  69. package/lib/module/EnrichedTextInputNativeComponent.ts +7 -5
  70. package/lib/module/normalizeHtmlStyle.js +0 -4
  71. package/lib/module/normalizeHtmlStyle.js.map +1 -1
  72. package/lib/typescript/src/EnrichedTextInput.d.ts +3 -6
  73. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  74. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +2 -5
  75. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  76. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/EnrichedTextInput.tsx +6 -7
  79. package/src/EnrichedTextInputNativeComponent.ts +7 -5
  80. package/src/normalizeHtmlStyle.ts +0 -4
@@ -1,6 +1,5 @@
1
1
  package com.swmansion.enriched.styles
2
2
 
3
- import android.net.Uri
4
3
  import android.text.Editable
5
4
  import android.text.Spannable
6
5
  import android.text.SpannableStringBuilder
@@ -11,7 +10,6 @@ import com.swmansion.enriched.spans.EnrichedLinkSpan
11
10
  import com.swmansion.enriched.spans.EnrichedMentionSpan
12
11
  import com.swmansion.enriched.spans.EnrichedSpans
13
12
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
14
- import java.io.File
15
13
 
16
14
  class ParametrizedStyles(private val view: EnrichedTextInputView) {
17
15
  private var mentionStart: Int? = null
@@ -85,7 +83,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
85
83
  }
86
84
  }
87
85
 
88
- private fun getWordAtIndex(s: Editable, index: Int): Triple<String, Int, Int>? {
86
+ private fun getWordAtIndex(s: CharSequence, index: Int): TextRange? {
89
87
  if (index < 0 ) return null
90
88
 
91
89
  var start = index
@@ -101,18 +99,34 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
101
99
 
102
100
  val result = s.subSequence(start, end).toString()
103
101
 
104
- return Triple(result, start, end)
102
+ return TextRange(result, start, end)
105
103
  }
106
104
 
107
- private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
105
+ private fun canLinkBeApplied(): Boolean {
106
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
107
+ val conflictingStyles = mergingConfig.conflictingStyles
108
+ val blockingStyles = mergingConfig.blockingStyles
109
+
110
+ for (style in blockingStyles) {
111
+ if (view.spanState?.getStart(style) != null) return false
112
+ }
113
+
114
+ for (style in conflictingStyles) {
115
+ if (view.spanState?.getStart(style) != null) return false
116
+ }
117
+
118
+ return true
119
+ }
120
+
121
+ private fun afterTextChangedLinks(result: TextRange) {
108
122
  // Do not detect link if it's applied manually
109
- if (isSettingLinkSpan) return
123
+ if (isSettingLinkSpan || !canLinkBeApplied()) return
124
+
110
125
  val spannable = view.text as Spannable
111
126
  val (word, start, end) = result
112
127
 
113
128
  // TODO: Consider using more reliable regex, this one matches almost anything
114
129
  val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
115
-
116
130
  val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
117
131
  for (span in spans) {
118
132
  spannable.removeSpan(span)
@@ -125,55 +139,80 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
125
139
  }
126
140
  }
127
141
 
128
- private fun afterTextChangedMentions(result: Triple<String, Int, Int>) {
142
+ private fun afterTextChangedMentions(currentWord: TextRange) {
129
143
  val mentionHandler = view.mentionHandler ?: return
130
144
  val spannable = view.text as Spannable
131
- val (word, start, end) = result
132
145
 
133
146
  val indicatorsPattern = mentionIndicators.joinToString("|") { Regex.escape(it) }
134
147
  val mentionIndicatorRegex = Regex("^($indicatorsPattern)")
135
148
  val mentionRegex= Regex("^($indicatorsPattern)\\w*")
136
149
 
137
- val spans = spannable.getSpans(start, end, EnrichedMentionSpan::class.java)
150
+ val spans = spannable.getSpans(currentWord.start, currentWord.end, EnrichedMentionSpan::class.java)
138
151
  for (span in spans) {
139
152
  spannable.removeSpan(span)
140
153
  }
141
154
 
142
- if (mentionRegex.matches(word)) {
143
- val indicator = mentionIndicatorRegex.find(word)?.value ?: ""
144
- val text = word.replaceFirst(indicator, "")
155
+ var indicator: String
156
+ var finalStart: Int
157
+ val finalEnd = currentWord.end
158
+
159
+ // No mention in the current word, check previous one
160
+ if (!mentionRegex.matches(currentWord.text)) {
161
+ val previousWord = getWordAtIndex(spannable, currentWord.start - 1)
145
162
 
146
- // Means we are starting mention
147
- if (text.isEmpty()) {
148
- mentionStart = start
163
+ // No previous word -> no mention to be detected
164
+ if (previousWord == null) {
165
+ mentionHandler.endMention()
166
+ return
149
167
  }
150
168
 
151
- mentionHandler.onMention(indicator, text)
169
+ // Previous word is not a mention -> end mention
170
+ if (!mentionRegex.matches(previousWord.text)) {
171
+ mentionHandler.endMention()
172
+ return
173
+ }
174
+
175
+ // Previous word is a mention -> use it
176
+ finalStart = previousWord.start
177
+ indicator = mentionIndicatorRegex.find(previousWord.text)?.value ?: ""
152
178
  } else {
153
- mentionHandler.endMention()
179
+ // Current word is a mention -> use it
180
+ finalStart = currentWord.start
181
+ indicator = mentionIndicatorRegex.find(currentWord.text)?.value ?: ""
154
182
  }
183
+
184
+ // Extract text without indicator
185
+ val text = spannable.subSequence(finalStart, finalEnd).toString().replaceFirst(indicator, "")
186
+
187
+ // Means we are starting mention
188
+ if (text.isEmpty()) {
189
+ mentionStart = finalStart
190
+ }
191
+
192
+ mentionHandler.onMention(indicator, text)
155
193
  }
156
194
 
157
- fun setImageSpan(src: String) {
195
+ fun setImageSpan(src: String, width: Float, height: Float) {
158
196
  if (view.selection == null) return
159
-
160
197
  val spannable = view.text as SpannableStringBuilder
161
- var (start, end) = view.selection.getInlineSelection()
162
- val spans = spannable.getSpans(start, end, EnrichedImageSpan::class.java)
198
+ val (start, originalEnd) = view.selection.getInlineSelection()
163
199
 
164
- for (s in spans) {
165
- spannable.removeSpan(s)
166
- }
167
-
168
- if (start == end) {
200
+ if (start == originalEnd) {
169
201
  spannable.insert(start, "\uFFFC")
170
- end++
202
+ } else {
203
+ val spans = spannable.getSpans(start, originalEnd, EnrichedImageSpan::class.java)
204
+ for (s in spans) {
205
+ spannable.removeSpan(s)
206
+ }
207
+
208
+ spannable.replace(start, originalEnd, "\uFFFC")
171
209
  }
172
210
 
173
- val uri = Uri.fromFile(File(src))
174
- val span = EnrichedImageSpan(view.context, uri, view.htmlStyle)
175
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
176
- spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
211
+ val (imageStart, imageEnd) = spannable.getSafeSpanBoundaries(start, start + 1)
212
+ val span = EnrichedImageSpan.createEnrichedImageSpan(src, width.toInt(), height.toInt())
213
+ span.observeAsyncDrawableLoaded(view.text)
214
+
215
+ spannable.setSpan(span, imageStart, imageEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
177
216
  }
178
217
 
179
218
  fun startMention(indicator: String) {
@@ -229,4 +268,8 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
229
268
  val spannable = view.text as Spannable
230
269
  return removeSpansForRange(spannable, start, end, config.clazz)
231
270
  }
271
+
272
+ companion object {
273
+ data class TextRange(val text: String, val start: Int, val end: Int)
274
+ }
232
275
  }
@@ -0,0 +1,91 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.res.Resources
5
+ import android.graphics.BitmapFactory
6
+ import android.graphics.Canvas
7
+ import android.graphics.Color
8
+ import android.graphics.ColorFilter
9
+ import android.graphics.PixelFormat
10
+ import android.graphics.drawable.Drawable
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.util.Log
14
+ import androidx.core.content.res.ResourcesCompat
15
+ import androidx.core.graphics.drawable.DrawableCompat
16
+ import java.net.URL
17
+ import java.util.concurrent.Executors
18
+ import androidx.core.graphics.drawable.toDrawable
19
+ import com.swmansion.enriched.R
20
+
21
+ class AsyncDrawable (
22
+ private val url: String,
23
+ ) : Drawable() {
24
+ private var internalDrawable: Drawable = Color.TRANSPARENT.toDrawable()
25
+ private val mainHandler = Handler(Looper.getMainLooper())
26
+ private val executor = Executors.newSingleThreadExecutor()
27
+ var isLoaded = false
28
+
29
+ init {
30
+ internalDrawable.bounds = bounds
31
+
32
+ load()
33
+ }
34
+
35
+ private fun load() {
36
+ executor.execute {
37
+ try {
38
+ isLoaded = false
39
+ val inputStream = URL(url).openStream()
40
+ val bitmap = BitmapFactory.decodeStream(inputStream)
41
+
42
+ // Switch to Main Thread to update UI
43
+ mainHandler.post {
44
+ if (bitmap != null) {
45
+ val d = bitmap.toDrawable(Resources.getSystem())
46
+
47
+ d.bounds = bounds
48
+ internalDrawable = d
49
+ } else {
50
+ loadPlaceholderImage()
51
+ }
52
+ }
53
+ } catch (e: Exception) {
54
+ Log.e("AsyncDrawable", "Failed to load: $url", e)
55
+
56
+ loadPlaceholderImage()
57
+ } finally {
58
+ isLoaded = true
59
+ onLoaded?.invoke()
60
+ }
61
+ }
62
+ }
63
+
64
+ private fun loadPlaceholderImage() {
65
+ internalDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
66
+ }
67
+
68
+ override fun draw(canvas: Canvas) {
69
+ internalDrawable.draw(canvas)
70
+ }
71
+
72
+ override fun setAlpha(alpha: Int) {
73
+ internalDrawable.alpha = alpha
74
+ }
75
+
76
+ override fun setColorFilter(colorFilter: ColorFilter?) {
77
+ internalDrawable.colorFilter = colorFilter
78
+ }
79
+
80
+ @Deprecated("Deprecated in Java")
81
+ override fun getOpacity(): Int {
82
+ return PixelFormat.TRANSLUCENT
83
+ }
84
+
85
+ override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
86
+ super.setBounds(left, top, right, bottom)
87
+ internalDrawable.setBounds(left, top, right, bottom)
88
+ }
89
+
90
+ var onLoaded: (() -> Unit)? = null
91
+ }
@@ -275,7 +275,16 @@ public class EnrichedParser {
275
275
  if (style[j] instanceof EnrichedImageSpan) {
276
276
  out.append("<img src=\"");
277
277
  out.append(((EnrichedImageSpan) style[j]).getSource());
278
- out.append("\">");
278
+ out.append("\"");
279
+
280
+ out.append(" width=\"");
281
+ out.append(((EnrichedImageSpan) style[j]).getWidth());
282
+ out.append("\"");
283
+
284
+ out.append(" height=\"");
285
+ out.append(((EnrichedImageSpan) style[j]).getHeight());
286
+
287
+ out.append("\"/>");
279
288
  // Don't output the placeholder character underlying the image.
280
289
  i = next;
281
290
  }
@@ -350,6 +359,7 @@ class HtmlToSpannedConverter implements ContentHandler {
350
359
  private final EnrichedParser.ImageGetter mImageGetter;
351
360
  private static Integer currentOrderedListItemIndex = 0;
352
361
  private static Boolean isInOrderedList = false;
362
+ private static Boolean isEmptyTag = false;
353
363
 
354
364
  public HtmlToSpannedConverter(String source, HtmlStyle style, EnrichedParser.ImageGetter imageGetter, Parser parser) {
355
365
  mStyle = style;
@@ -396,9 +406,15 @@ class HtmlToSpannedConverter implements ContentHandler {
396
406
  for (EnrichedZeroWidthSpaceSpan zeroWidthSpaceSpan : zeroWidthSpaceSpans) {
397
407
  int start = mSpannableStringBuilder.getSpanStart(zeroWidthSpaceSpan);
398
408
  int end = mSpannableStringBuilder.getSpanEnd(zeroWidthSpaceSpan);
399
- mSpannableStringBuilder.insert(start, "\u200B");
409
+
410
+ if (mSpannableStringBuilder.charAt(start) != '\u200B') {
411
+ // Insert zero-width space character at the start if it's not already present.
412
+ mSpannableStringBuilder.insert(start, "\u200B");
413
+ end++; // Adjust end position due to insertion.
414
+ }
415
+
400
416
  mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan);
401
- mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
417
+ mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
402
418
  }
403
419
 
404
420
  return mSpannableStringBuilder;
@@ -409,6 +425,7 @@ class HtmlToSpannedConverter implements ContentHandler {
409
425
  // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
410
426
  // so we can safely emit the linebreaks when we handle the close tag.
411
427
  } else if (tag.equalsIgnoreCase("p")) {
428
+ isEmptyTag = true;
412
429
  startBlockElement(mSpannableStringBuilder);
413
430
  } else if (tag.equalsIgnoreCase("ul")) {
414
431
  isInOrderedList = false;
@@ -418,14 +435,17 @@ class HtmlToSpannedConverter implements ContentHandler {
418
435
  currentOrderedListItemIndex = 0;
419
436
  startBlockElement(mSpannableStringBuilder);
420
437
  } else if (tag.equalsIgnoreCase("li")) {
438
+ isEmptyTag = true;
421
439
  startLi(mSpannableStringBuilder);
422
440
  } else if (tag.equalsIgnoreCase("b")) {
423
441
  start(mSpannableStringBuilder, new Bold());
424
442
  } else if (tag.equalsIgnoreCase("i")) {
425
443
  start(mSpannableStringBuilder, new Italic());
426
444
  } else if (tag.equalsIgnoreCase("blockquote")) {
445
+ isEmptyTag = true;
427
446
  startBlockquote(mSpannableStringBuilder);
428
447
  } else if (tag.equalsIgnoreCase("codeblock")) {
448
+ isEmptyTag = true;
429
449
  startCodeBlock(mSpannableStringBuilder);
430
450
  } else if (tag.equalsIgnoreCase("a")) {
431
451
  startA(mSpannableStringBuilder, attributes);
@@ -442,7 +462,7 @@ class HtmlToSpannedConverter implements ContentHandler {
442
462
  } else if (tag.equalsIgnoreCase("h3")) {
443
463
  startHeading(mSpannableStringBuilder, 3);
444
464
  } else if (tag.equalsIgnoreCase("img")) {
445
- startImg(mSpannableStringBuilder, attributes, mImageGetter, mStyle);
465
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
446
466
  } else if (tag.equalsIgnoreCase("code")) {
447
467
  start(mSpannableStringBuilder, new Code());
448
468
  } else if (tag.equalsIgnoreCase("mention")) {
@@ -632,11 +652,17 @@ class HtmlToSpannedConverter implements ContentHandler {
632
652
  }
633
653
  }
634
654
 
635
- private static void setParagraphSpanFromMark(Spannable text, Object mark, Object... spans) {
655
+ private static void setParagraphSpanFromMark(Editable text, Object mark, Object... spans) {
636
656
  int where = text.getSpanStart(mark);
637
657
  text.removeSpan(mark);
638
658
  int len = text.length();
639
659
 
660
+ // Block spans require at least one character to be applied.
661
+ if (isEmptyTag) {
662
+ text.append("\u200B");
663
+ len++;
664
+ }
665
+
640
666
  // Adjust the end position to exclude the newline character, if present
641
667
  if (len > 0 && text.charAt(len - 1) == '\n') {
642
668
  len--;
@@ -661,20 +687,15 @@ class HtmlToSpannedConverter implements ContentHandler {
661
687
  }
662
688
  }
663
689
 
664
- private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img, HtmlStyle style) {
690
+ private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img) {
665
691
  String src = attributes.getValue("", "src");
666
- Drawable d = null;
667
- if (img != null) {
668
- d = img.getDrawable(src);
669
- }
670
-
671
- if (d == null) {
672
- return;
673
- }
692
+ String width = attributes.getValue("", "width");
693
+ String height = attributes.getValue("", "height");
674
694
 
675
695
  int len = text.length();
696
+ EnrichedImageSpan span = EnrichedImageSpan.Companion.createEnrichedImageSpan(src, Integer.parseInt(width), Integer.parseInt(height));
676
697
  text.append("");
677
- text.setSpan(new EnrichedImageSpan(d, src, style), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
698
+ text.setSpan(span, len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
678
699
  }
679
700
 
680
701
  private static void startA(Editable text, Attributes attributes) {
@@ -741,6 +762,8 @@ class HtmlToSpannedConverter implements ContentHandler {
741
762
 
742
763
  public void characters(char[] ch, int start, int length) {
743
764
  StringBuilder sb = new StringBuilder();
765
+ if (length > 0) isEmptyTag = false;
766
+
744
767
  /*
745
768
  * Ignore whitespace that immediately follows other whitespace;
746
769
  * newlines count as spaces.
@@ -0,0 +1,26 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.content.res.Resources
6
+ import android.graphics.drawable.Drawable
7
+ import androidx.core.content.res.ResourcesCompat
8
+ import androidx.core.graphics.drawable.DrawableCompat
9
+
10
+ object ResourceManager {
11
+ private var appContext: Context? = null
12
+
13
+ fun init(context: Context) {
14
+ this.appContext = context.applicationContext
15
+ }
16
+
17
+ @SuppressLint("UseCompatLoadingForDrawables")
18
+ fun getDrawableResource(id: Int): Drawable {
19
+ val context = appContext ?: throw IllegalStateException("ResourceManager not initialized! Call init() first.")
20
+
21
+ val image = ResourcesCompat.getDrawable(context.resources, id, null)
22
+ val finalImage = image ?: Resources.getSystem().getDrawable(android.R.drawable.ic_menu_report_image)
23
+
24
+ return DrawableCompat.wrap(finalImage)
25
+ }
26
+ }
@@ -52,6 +52,9 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
52
52
  }
53
53
 
54
54
  fun emitEvent(s: Spannable, what: Any?) {
55
+ // Do not parse spannable and emit event if onChangeHtml is not provided
56
+ if (!view.shouldEmitHtml) return
57
+
55
58
  // Emit event only if we change one of ours spans
56
59
  if (what != null && what !is EnrichedSpan) return
57
60
 
@@ -59,7 +62,6 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
59
62
  if (html == previousHtml) return
60
63
 
61
64
  previousHtml = html
62
- view.layoutManager.invalidateLayout(view.text)
63
65
  val context = view.context as ReactContext
64
66
  val surfaceId = UIManagerHelper.getSurfaceId(context)
65
67
  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
@@ -0,0 +1,10 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:width="24dp"
3
+ android:height="24dp"
4
+ android:viewportWidth="960"
5
+ android:viewportHeight="960"
6
+ android:tint="?attr/colorControlNormal">
7
+ <path
8
+ android:fillColor="@android:color/white"
9
+ android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z"/>
10
+ </vector>
@@ -18,14 +18,16 @@ NS_ASSUME_NONNULL_BEGIN
18
18
  @public InputParser *parser;
19
19
  @public NSMutableDictionary<NSAttributedStringKey, id> *defaultTypingAttributes;
20
20
  @public NSDictionary<NSNumber *, id<BaseStyleProtocol>> *stylesDict;
21
+ NSDictionary<NSNumber *, NSArray<NSNumber *> *> *conflictingStyles;
22
+ NSDictionary<NSNumber *, NSArray<NSNumber *> *> *blockingStyles;
21
23
  @public BOOL blockEmitting;
22
- @public BOOL emitHtml;
23
24
  }
24
25
  - (CGSize)measureSize:(CGFloat)maxWidth;
25
26
  - (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range;
26
27
  - (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text;
27
28
  - (void)anyTextMayHaveBeenModified;
28
29
  - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
30
+ - (NSArray<NSNumber *> *)getPresentStyleTypesFrom:(NSArray<NSNumber *> *)types range:(NSRange)range;
29
31
  @end
30
32
 
31
33
  NS_ASSUME_NONNULL_END