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
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <img src="https://github.com/user-attachments/assets/abc75d3b-495b-4a76-a72f-d87ce3ca1ff9" alt="react-native-enriched by Software Mansion" width="100%">
1
+ <img src="https://github.com/user-attachments/assets/b010571e-e4a3-4d92-a409-4f9fe614025d" alt="react-native-enriched by Software Mansion" width="100%">
2
2
 
3
3
  # react-native-enriched
4
4
 
@@ -101,11 +101,11 @@ export default function App() {
101
101
  <View style={styles.container}>
102
102
  <EnrichedTextInput
103
103
  ref={ref}
104
- onChangeState={(e) => setStylesState(e.nativeEvent)}
104
+ onChangeState={e => setStylesState(e.nativeEvent)}
105
105
  style={styles.input}
106
106
  />
107
107
  <Button
108
- title="Toggle bold"
108
+ title={stylesState?.isBold ? 'Unbold' : 'Bold'}
109
109
  color={stylesState?.isBold ? 'green' : 'gray'}
110
110
  onPress={() => ref.current?.toggleBold()}
111
111
  />
@@ -152,9 +152,6 @@ Supported styles:
152
152
  - ordered list
153
153
  - unordered list
154
154
 
155
- > [!NOTE]
156
- > The iOS doesn't support codeblocks just yet, but it's planned in the near future!
157
-
158
155
  Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.
159
156
 
160
157
  Each call toggles the style within the current text selection. We can still divide styles into two categories based on how they treat the selection:
@@ -212,9 +209,6 @@ You can insert an image into the input using [setImage](docs/API_REFERENCE.md#se
212
209
 
213
210
  The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection.
214
211
 
215
- > [!NOTE]
216
- > The iOS doesn't support inline images just yet, but it's planned in the near future!
217
-
218
212
  ## Style Detection
219
213
 
220
214
  All of the above styles can be detected with the use of [onChangeState](docs/API_REFERENCE.md#onchangestate) event payload.
@@ -55,6 +55,9 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
55
55
  case "htmlStyle":
56
56
  mViewManager.setHtmlStyle(view, (ReadableMap) value);
57
57
  break;
58
+ case "scrollEnabled":
59
+ mViewManager.setScrollEnabled(view, value == null ? false : (boolean) value);
60
+ break;
58
61
  case "color":
59
62
  mViewManager.setColor(view, ColorPropConverter.getColor(value, view.getContext()));
60
63
  break;
@@ -133,7 +136,7 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
133
136
  mViewManager.addLink(view, args.getInt(0), args.getInt(1), args.getString(2), args.getString(3));
134
137
  break;
135
138
  case "addImage":
136
- mViewManager.addImage(view, args.getString(0));
139
+ mViewManager.addImage(view, args.getString(0), (float) args.getDouble(1), (float) args.getDouble(2));
137
140
  break;
138
141
  case "startMention":
139
142
  mViewManager.startMention(view, args.getString(0));
@@ -26,6 +26,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
26
26
  void setSelectionColor(T view, @Nullable Integer value);
27
27
  void setAutoCapitalize(T view, @Nullable String value);
28
28
  void setHtmlStyle(T view, @Nullable ReadableMap value);
29
+ void setScrollEnabled(T view, boolean value);
29
30
  void setColor(T view, @Nullable Integer value);
30
31
  void setFontSize(T view, float value);
31
32
  void setFontFamily(T view, @Nullable String value);
@@ -49,7 +50,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
49
50
  void toggleOrderedList(T view);
50
51
  void toggleUnorderedList(T view);
51
52
  void addLink(T view, int start, int end, String text, String url);
52
- void addImage(T view, String uri);
53
+ void addImage(T view, String uri, float width, float height);
53
54
  void startMention(T view, String indicator);
54
55
  void addMention(T view, String indicator, String text, String payload);
55
56
  }
@@ -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
  }
@@ -309,45 +309,6 @@ static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleAStru
309
309
  }
310
310
  #endif
311
311
 
312
- struct EnrichedTextInputViewHtmlStyleImgStruct {
313
- Float width{0.0};
314
- Float height{0.0};
315
-
316
- #ifdef RN_SERIALIZABLE_STATE
317
- bool operator==(const EnrichedTextInputViewHtmlStyleImgStruct&) const = default;
318
-
319
- folly::dynamic toDynamic() const {
320
- folly::dynamic result = folly::dynamic::object();
321
- result["width"] = width;
322
- result["height"] = height;
323
- return result;
324
- }
325
- #endif
326
- };
327
-
328
- static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, EnrichedTextInputViewHtmlStyleImgStruct &result) {
329
- auto map = (std::unordered_map<std::string, RawValue>)value;
330
-
331
- auto tmp_width = map.find("width");
332
- if (tmp_width != map.end()) {
333
- fromRawValue(context, tmp_width->second, result.width);
334
- }
335
- auto tmp_height = map.find("height");
336
- if (tmp_height != map.end()) {
337
- fromRawValue(context, tmp_height->second, result.height);
338
- }
339
- }
340
-
341
- static inline std::string toString(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
342
- return "[Object EnrichedTextInputViewHtmlStyleImgStruct]";
343
- }
344
-
345
- #ifdef RN_SERIALIZABLE_STATE
346
- static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
347
- return value.toDynamic();
348
- }
349
- #endif
350
-
351
312
  struct EnrichedTextInputViewHtmlStyleOlStruct {
352
313
  Float gapWidth{0.0};
353
314
  Float marginLeft{0.0};
@@ -459,7 +420,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
459
420
  EnrichedTextInputViewHtmlStyleCodeStruct code{};
460
421
  EnrichedTextInputViewHtmlStyleAStruct a{};
461
422
  folly::dynamic mention{};
462
- EnrichedTextInputViewHtmlStyleImgStruct img{};
463
423
  EnrichedTextInputViewHtmlStyleOlStruct ol{};
464
424
  EnrichedTextInputViewHtmlStyleUlStruct ul{};
465
425
 
@@ -476,7 +436,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
476
436
  result["code"] = ::facebook::react::toDynamic(code);
477
437
  result["a"] = ::facebook::react::toDynamic(a);
478
438
  result["mention"] = mention;
479
- result["img"] = ::facebook::react::toDynamic(img);
480
439
  result["ol"] = ::facebook::react::toDynamic(ol);
481
440
  result["ul"] = ::facebook::react::toDynamic(ul);
482
441
  return result;
@@ -519,10 +478,6 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
519
478
  if (tmp_mention != map.end()) {
520
479
  fromRawValue(context, tmp_mention->second, result.mention);
521
480
  }
522
- auto tmp_img = map.find("img");
523
- if (tmp_img != map.end()) {
524
- fromRawValue(context, tmp_img->second, result.img);
525
- }
526
481
  auto tmp_ol = map.find("ol");
527
482
  if (tmp_ol != map.end()) {
528
483
  fromRawValue(context, tmp_ol->second, result.ol);
@@ -559,6 +514,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
559
514
  SharedColor selectionColor{};
560
515
  std::string autoCapitalize{};
561
516
  EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
517
+ bool scrollEnabled{false};
562
518
  SharedColor color{};
563
519
  Float fontSize{0.0};
564
520
  std::string fontFamily{};
@@ -29,6 +29,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
29
29
  import com.swmansion.enriched.events.MentionHandler
30
30
  import com.swmansion.enriched.events.OnInputBlurEvent
31
31
  import com.swmansion.enriched.events.OnInputFocusEvent
32
+ import com.swmansion.enriched.spans.EnrichedImageSpan
32
33
  import com.swmansion.enriched.spans.EnrichedSpans
33
34
  import com.swmansion.enriched.styles.InlineStyles
34
35
  import com.swmansion.enriched.styles.ListStyles
@@ -54,12 +55,14 @@ class EnrichedTextInputView : AppCompatEditText {
54
55
  val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
55
56
  var isDuringTransaction: Boolean = false
56
57
  var isRemovingMany: Boolean = false
58
+ var scrollEnabled: Boolean = true
57
59
 
58
60
  val mentionHandler: MentionHandler? = MentionHandler(this)
59
61
  var htmlStyle: HtmlStyle = HtmlStyle(this, null)
60
62
  var spanWatcher: EnrichedSpanWatcher? = null
61
63
  var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
62
64
 
65
+ var shouldEmitHtml: Boolean = true
63
66
  var experimentalSynchronousEvents: Boolean = false
64
67
 
65
68
  var fontSize: Float? = null
@@ -70,6 +73,8 @@ class EnrichedTextInputView : AppCompatEditText {
70
73
  private var fontFamily: String? = null
71
74
  private var fontStyle: Int = ReactConstants.UNSET
72
75
  private var fontWeight: Int = ReactConstants.UNSET
76
+ private var defaultValue: CharSequence? = null
77
+ private var defaultValueDirty: Boolean = false
73
78
 
74
79
  private var inputMethodManager: InputMethodManager? = null
75
80
 
@@ -137,6 +142,14 @@ class EnrichedTextInputView : AppCompatEditText {
137
142
  return super.onTouchEvent(ev)
138
143
  }
139
144
 
145
+ override fun canScrollVertically(direction: Int): Boolean {
146
+ return scrollEnabled
147
+ }
148
+
149
+ override fun canScrollHorizontally(direction: Int): Boolean {
150
+ return scrollEnabled
151
+ }
152
+
140
153
  override fun onSelectionChanged(selStart: Int, selEnd: Int) {
141
154
  super.onSelectionChanged(selStart, selEnd)
142
155
  selection?.onSelection(selStart, selEnd)
@@ -243,6 +256,7 @@ class EnrichedTextInputView : AppCompatEditText {
243
256
  val newText = parseText(value)
244
257
  setText(newText)
245
258
 
259
+ observeAsyncImages()
246
260
  // Assign SpanWatcher one more time as our previous spannable has been replaced
247
261
  addSpanWatcher(EnrichedSpanWatcher(this))
248
262
 
@@ -251,6 +265,20 @@ class EnrichedTextInputView : AppCompatEditText {
251
265
  }
252
266
  }
253
267
 
268
+ /**
269
+ * Finds all async images in the current text and sets up listeners
270
+ * to redraw the text layout when they finish downloading.
271
+ */
272
+ private fun observeAsyncImages() {
273
+ val liveText = text ?: return
274
+
275
+ val spans = liveText.getSpans(0, liveText.length, EnrichedImageSpan::class.java)
276
+
277
+ for (span in spans) {
278
+ span.observeAsyncDrawableLoaded(liveText)
279
+ }
280
+ }
281
+
254
282
  fun setAutoFocus(autoFocus: Boolean) {
255
283
  this.autoFocus = autoFocus
256
284
  }
@@ -306,7 +334,7 @@ class EnrichedTextInputView : AppCompatEditText {
306
334
 
307
335
  // This ensured that newly created spans will take the new font size into account
308
336
  htmlStyle.invalidateStyles()
309
- layoutManager.invalidateLayout(text)
337
+ layoutManager.invalidateLayout()
310
338
  }
311
339
 
312
340
  fun setFontFamily(family: String?) {
@@ -360,7 +388,24 @@ class EnrichedTextInputView : AppCompatEditText {
360
388
  return false
361
389
  }
362
390
 
363
- fun updateTypeface() {
391
+ fun afterUpdateTransaction() {
392
+ updateTypeface()
393
+ updateDefaultValue()
394
+ }
395
+
396
+ fun setDefaultValue(value: CharSequence?) {
397
+ defaultValue = value
398
+ defaultValueDirty = true
399
+ }
400
+
401
+ private fun updateDefaultValue() {
402
+ if (!defaultValueDirty) return
403
+
404
+ defaultValueDirty = false
405
+ setValue(defaultValue ?: "")
406
+ }
407
+
408
+ private fun updateTypeface() {
364
409
  if (!typefaceDirty) return
365
410
  typefaceDirty = false
366
411
 
@@ -368,7 +413,7 @@ class EnrichedTextInputView : AppCompatEditText {
368
413
  typeface = newTypeface
369
414
  paint.typeface = newTypeface
370
415
 
371
- layoutManager.invalidateLayout(text)
416
+ layoutManager.invalidateLayout()
372
417
  }
373
418
 
374
419
  private fun toggleStyle(name: String) {
@@ -388,7 +433,7 @@ class EnrichedTextInputView : AppCompatEditText {
388
433
  else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
389
434
  }
390
435
 
391
- layoutManager.invalidateLayout(text)
436
+ layoutManager.invalidateLayout()
392
437
  }
393
438
 
394
439
  private fun removeStyle(name: String, start: Int, end: Int): Boolean {
@@ -438,7 +483,7 @@ class EnrichedTextInputView : AppCompatEditText {
438
483
  }
439
484
 
440
485
  private fun verifyStyle(name: String): Boolean {
441
- val mergingConfig = EnrichedSpans.mergingConfig[name] ?: return true
486
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
442
487
  val conflictingStyles = mergingConfig.conflictingStyles
443
488
  val blockingStyles = mergingConfig.blockingStyles
444
489
  val isEnabling = spanState?.getStart(name) == null
@@ -499,11 +544,12 @@ class EnrichedTextInputView : AppCompatEditText {
499
544
  parametrizedStyles?.setLinkSpan(start, end, text, url)
500
545
  }
501
546
 
502
- fun addImage(src: String) {
547
+ fun addImage(src: String, width: Float, height: Float) {
503
548
  val isValid = verifyStyle(EnrichedSpans.IMAGE)
504
549
  if (!isValid) return
505
550
 
506
- parametrizedStyles?.setImageSpan(src)
551
+ parametrizedStyles?.setImageSpan(src, width, height)
552
+ layoutManager.invalidateLayout()
507
553
  }
508
554
 
509
555
  fun startMention(indicator: String) {
@@ -542,11 +588,6 @@ class EnrichedTextInputView : AppCompatEditText {
542
588
  didAttachToWindow = true
543
589
  }
544
590
 
545
- override fun onDetachedFromWindow() {
546
- layoutManager.cleanup()
547
- super.onDetachedFromWindow()
548
- }
549
-
550
591
  companion object {
551
592
  const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
552
593
  }
@@ -1,24 +1,16 @@
1
1
  package com.swmansion.enriched
2
2
 
3
- import android.graphics.text.LineBreaker
4
- import android.os.Build
5
- import android.text.Editable
6
- import android.text.StaticLayout
7
3
  import com.facebook.react.bridge.Arguments
8
- import com.facebook.react.uimanager.PixelUtil
9
4
 
10
5
  class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
11
- private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
12
- private var cachedYogaWidth: Float = 0f
13
6
  private var forceHeightRecalculationCounter: Int = 0
14
7
 
15
- fun cleanup() {
16
- forceHeightRecalculationCounter = 0
17
- }
8
+ fun invalidateLayout() {
9
+ val text = view.text
10
+ val paint = view.paint
18
11
 
19
- // Update shadow node's state in order to recalculate layout
20
- fun invalidateLayout(text: Editable?) {
21
- measureSize(text ?: "")
12
+ val needUpdate = MeasurementStore.store(view.id, text, paint)
13
+ if (!needUpdate) return
22
14
 
23
15
  val counter = forceHeightRecalculationCounter
24
16
  forceHeightRecalculationCounter++
@@ -27,48 +19,7 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
27
19
  view.stateWrapper?.updateState(state)
28
20
  }
29
21
 
30
- fun getMeasuredSize(maxWidth: Float): Pair<Float, Float> {
31
- if (maxWidth == cachedYogaWidth) {
32
- return cachedSize
33
- }
34
-
35
- val text = view.text ?: ""
36
- val result = measureAndCacheSize(text, maxWidth)
37
- cachedYogaWidth = maxWidth
38
- return result
39
- }
40
-
41
- fun measureSize(text: CharSequence): Pair<Float, Float> {
42
- return measureAndCacheSize(text, cachedYogaWidth)
43
- }
44
-
45
- private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
46
- val result = measureSize(text, maxWidth)
47
- cachedSize = result
48
- return result
49
- }
50
-
51
- private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
52
- val paint = view.paint
53
- val textLength = text.length
54
-
55
- val builder = StaticLayout.Builder
56
- .obtain(text, 0, textLength, paint, maxWidth.toInt())
57
- .setIncludePad(true)
58
- .setLineSpacing(0f, 1f)
59
-
60
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
61
- builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
62
- }
63
-
64
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
65
- builder.setUseLineSpacingFromFallbacks(true)
66
- }
67
-
68
- val staticLayout = builder.build()
69
- val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
70
- val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
71
-
72
- return Pair(widthInSP, heightInSP)
22
+ fun releaseMeasurementStore() {
23
+ MeasurementStore.release(view.id)
73
24
  }
74
25
  }
@@ -15,7 +15,6 @@ import com.facebook.react.uimanager.annotations.ReactProp
15
15
  import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerDelegate
16
16
  import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerInterface
17
17
  import com.facebook.yoga.YogaMeasureMode
18
- import com.facebook.yoga.YogaMeasureOutput
19
18
  import com.swmansion.enriched.events.OnInputBlurEvent
20
19
  import com.swmansion.enriched.events.OnChangeHtmlEvent
21
20
  import com.swmansion.enriched.events.OnChangeSelectionEvent
@@ -32,12 +31,8 @@ import com.swmansion.enriched.utils.jsonStringToStringMap
32
31
  @ReactModule(name = EnrichedTextInputViewManager.NAME)
33
32
  class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
34
33
  EnrichedTextInputViewManagerInterface<EnrichedTextInputView> {
35
- private val mDelegate: ViewManagerDelegate<EnrichedTextInputView>
36
- private var view: EnrichedTextInputView? = null
37
-
38
- init {
39
- mDelegate = EnrichedTextInputViewManagerDelegate(this)
40
- }
34
+ private val mDelegate: ViewManagerDelegate<EnrichedTextInputView> =
35
+ EnrichedTextInputViewManagerDelegate(this)
41
36
 
42
37
  override fun getDelegate(): ViewManagerDelegate<EnrichedTextInputView>? {
43
38
  return mDelegate
@@ -48,10 +43,12 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
48
43
  }
49
44
 
50
45
  public override fun createViewInstance(context: ThemedReactContext): EnrichedTextInputView {
51
- val view = EnrichedTextInputView(context)
52
- this.view = view
46
+ return EnrichedTextInputView(context)
47
+ }
53
48
 
54
- return view
49
+ override fun onDropViewInstance(view: EnrichedTextInputView) {
50
+ super.onDropViewInstance(view)
51
+ view.layoutManager.releaseMeasurementStore()
55
52
  }
56
53
 
57
54
  override fun updateState(
@@ -80,7 +77,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
80
77
 
81
78
  @ReactProp(name = "defaultValue")
82
79
  override fun setDefaultValue(view: EnrichedTextInputView?, value: String?) {
83
- view?.setValue(value)
80
+ view?.setDefaultValue(value)
84
81
  }
85
82
 
86
83
  @ReactProp(name = "placeholder")
@@ -157,9 +154,14 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
157
154
  view?.setFontStyle(style)
158
155
  }
159
156
 
157
+ @ReactProp(name = "scrollEnabled")
158
+ override fun setScrollEnabled(view: EnrichedTextInputView, scrollEnabled: Boolean) {
159
+ view.scrollEnabled = scrollEnabled
160
+ }
161
+
160
162
  override fun onAfterUpdateTransaction(view: EnrichedTextInputView) {
161
163
  super.onAfterUpdateTransaction(view)
162
- view.updateTypeface()
164
+ view.afterUpdateTransaction()
163
165
  }
164
166
 
165
167
  override fun setPadding(
@@ -175,7 +177,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
175
177
  }
176
178
 
177
179
  override fun setIsOnChangeHtmlSet(view: EnrichedTextInputView?, value: Boolean) {
178
- // this prop isn't used on Android as of now, but the setter must be present
180
+ view?.shouldEmitHtml = value
179
181
  }
180
182
 
181
183
  override fun setAutoCapitalize(view: EnrichedTextInputView?, flag: String?) {
@@ -253,8 +255,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
253
255
  view?.addLink(start, end, text, url)
254
256
  }
255
257
 
256
- override fun addImage(view: EnrichedTextInputView?, src: String) {
257
- view?.addImage(src)
258
+ override fun addImage(view: EnrichedTextInputView?, src: String, width: Float, height: Float) {
259
+ view?.addImage(src, width, height)
258
260
  }
259
261
 
260
262
  override fun startMention(view: EnrichedTextInputView?, indicator: String) {
@@ -277,13 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
277
279
  heightMode: YogaMeasureMode?,
278
280
  attachmentsPositions: FloatArray?
279
281
  ): Long {
280
- val size = this.view?.layoutManager?.getMeasuredSize(width)
281
-
282
- if (size != null) {
283
- return YogaMeasureOutput.make(size.first, size.second)
284
- }
285
-
286
- return YogaMeasureOutput.make(0, 0)
282
+ val id = localData?.getInt("viewTag")
283
+ return MeasurementStore.getMeasureById(context, id, width, props)
287
284
  }
288
285
 
289
286
  companion object {
@@ -4,10 +4,12 @@ import com.facebook.react.ReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.uimanager.ViewManager
7
+ import com.swmansion.enriched.utils.ResourceManager
7
8
  import java.util.ArrayList
8
9
 
9
10
  class EnrichedTextInputViewPackage : ReactPackage {
10
11
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
12
+ ResourceManager.init(reactContext.applicationContext)
11
13
  val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
12
14
  viewManagers.add(EnrichedTextInputViewManager())
13
15
  return viewManagers
@@ -0,0 +1,158 @@
1
+ package com.swmansion.enriched
2
+
3
+ import android.content.Context
4
+ import android.graphics.Typeface
5
+ import android.graphics.text.LineBreaker
6
+ import android.os.Build
7
+ import android.text.Spannable
8
+ import android.text.StaticLayout
9
+ import android.text.TextPaint
10
+ import android.util.Log
11
+ import com.facebook.react.bridge.ReadableMap
12
+ import com.facebook.react.uimanager.PixelUtil
13
+ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
14
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
15
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
16
+ import com.facebook.yoga.YogaMeasureOutput
17
+ import com.swmansion.enriched.styles.HtmlStyle
18
+ import com.swmansion.enriched.utils.EnrichedParser
19
+ import java.util.concurrent.ConcurrentHashMap
20
+ import kotlin.math.ceil
21
+
22
+ object MeasurementStore {
23
+ data class PaintParams(
24
+ val typeface: Typeface,
25
+ val fontSize: Float,
26
+ )
27
+
28
+ data class MeasurementParams(
29
+ val initialized: Boolean,
30
+
31
+ val cachedWidth: Float,
32
+ val cachedSize: Long,
33
+
34
+ val spannable: CharSequence?,
35
+ val paintParams: PaintParams,
36
+ )
37
+
38
+ private val data = ConcurrentHashMap<Int, MeasurementParams>()
39
+
40
+ fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean {
41
+ val cachedWidth = data[id]?.cachedWidth ?: 0f
42
+ val cachedSize = data[id]?.cachedSize ?: 0L
43
+ val initialized = data[id]?.initialized ?: true
44
+
45
+ val size = measure(cachedWidth, spannable, paint)
46
+ val paintParams = PaintParams(paint.typeface, paint.textSize)
47
+
48
+ data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams)
49
+ return cachedSize != size
50
+ }
51
+
52
+ fun release(id: Int) {
53
+ data.remove(id)
54
+ }
55
+
56
+ private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long {
57
+ val paint = TextPaint().apply {
58
+ typeface = paintParams.typeface
59
+ textSize = paintParams.fontSize
60
+ }
61
+
62
+ return measure(maxWidth, spannable, paint)
63
+ }
64
+
65
+ private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long {
66
+ val text = spannable ?: ""
67
+ val textLength = text.length
68
+ val builder = StaticLayout.Builder
69
+ .obtain(text, 0, textLength, paint, maxWidth.toInt())
70
+ .setIncludePad(true)
71
+ .setLineSpacing(0f, 1f)
72
+
73
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
74
+ builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
75
+ }
76
+
77
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
78
+ builder.setUseLineSpacingFromFallbacks(true)
79
+ }
80
+
81
+ val staticLayout = builder.build()
82
+ val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
83
+ val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
84
+ return YogaMeasureOutput.make(widthInSP, heightInSP)
85
+ }
86
+
87
+ // Returns either: Spannable parsed from HTML defaultValue, or plain text defaultValue, or "I" if no defaultValue
88
+ private fun getInitialText(defaultView: EnrichedTextInputView, props: ReadableMap?): CharSequence {
89
+ val defaultValue = props?.getString("defaultValue")
90
+
91
+ // If there is no default value, assume text is one line, "I" is a good approximation of height
92
+ if (defaultValue == null) return "I"
93
+
94
+ val isHtml = defaultValue.startsWith("<html>") && defaultValue.endsWith("</html>")
95
+ if (!isHtml) return defaultValue
96
+
97
+ try {
98
+ val htmlStyle = HtmlStyle(defaultView, props.getMap("htmlStyle"))
99
+ val parsed = EnrichedParser.fromHtml(defaultValue, htmlStyle, null)
100
+ return parsed.trimEnd('\n')
101
+ } catch (e: Exception) {
102
+ Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}")
103
+ return defaultValue
104
+ }
105
+ }
106
+
107
+ private fun getInitialFontSize(defaultView: EnrichedTextInputView, props: ReadableMap?): Float {
108
+ val propsFontSize = props?.getDouble("fontSize")?.toFloat()
109
+ if (propsFontSize == null) return defaultView.textSize
110
+
111
+ return ceil(PixelUtil.toPixelFromSP(propsFontSize))
112
+ }
113
+
114
+ // Called when view measurements are not available in the store
115
+ // Most likely first measurement, we can use defaultValue, as no native state is set yet
116
+ private fun initialMeasure(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
117
+ val defaultView = EnrichedTextInputView(context)
118
+
119
+ val text = getInitialText(defaultView, props)
120
+ val fontSize = getInitialFontSize(defaultView, props)
121
+
122
+ val fontFamily = props?.getString("fontFamily")
123
+ val fontStyle = parseFontStyle(props?.getString("fontStyle"))
124
+ val fontWeight = parseFontWeight(props?.getString("fontWeight"))
125
+
126
+ val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
127
+ val paintParams = PaintParams(typeface, fontSize)
128
+ val size = measure(width, text, PaintParams(typeface, fontSize))
129
+
130
+ if (id != null) {
131
+ data[id] = MeasurementParams(true, width, size, text, paintParams)
132
+ }
133
+
134
+ return size
135
+ }
136
+
137
+ fun getMeasureById(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
138
+ val id = id ?: return initialMeasure(context, id, width, props)
139
+ val value = data[id] ?: return initialMeasure(context, id, width, props)
140
+
141
+ // First measure has to be done using initialMeasure
142
+ // That way it's free of any side effects and async initializations
143
+ if (!value.initialized) return initialMeasure(context, id, width, props)
144
+
145
+ if (width == value.cachedWidth) {
146
+ return value.cachedSize
147
+ }
148
+
149
+ val paint = TextPaint().apply {
150
+ typeface = value.paintParams.typeface
151
+ textSize = value.paintParams.fontSize
152
+ }
153
+
154
+ val size = measure(width, value.spannable, paint)
155
+ data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams)
156
+ return size
157
+ }
158
+ }