react-native-enriched 0.1.6 → 0.2.1

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 (123) hide show
  1. package/README.md +4 -14
  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/EventEmitters.cpp +10 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +7 -0
  6. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +0 -45
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +111 -2
  8. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +9 -3
  9. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
  10. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +1 -1
  11. package/android/src/main/java/com/swmansion/enriched/events/OnRequestHtmlResultEvent.kt +33 -0
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -0
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +6 -0
  14. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +42 -1
  15. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +6 -0
  16. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +6 -0
  17. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +6 -0
  18. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +135 -9
  19. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +6 -0
  20. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +5 -0
  21. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +6 -0
  22. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +6 -0
  23. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +6 -0
  24. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +13 -3
  25. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +5 -0
  26. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +5 -0
  27. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +6 -0
  28. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +4 -0
  29. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
  30. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +80 -9
  31. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
  32. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +188 -5
  33. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +57 -30
  34. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
  35. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +24 -13
  36. package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
  37. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -0
  38. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +6 -6
  39. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +6 -6
  40. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +19 -19
  41. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +40 -51
  42. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +13 -15
  43. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +23 -21
  44. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +35 -36
  45. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +4 -4
  46. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +13 -14
  47. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +12 -13
  48. package/android/src/main/res/drawable/broken_image.xml +10 -0
  49. package/ios/EnrichedTextInputView.h +27 -12
  50. package/ios/EnrichedTextInputView.mm +906 -547
  51. package/ios/attachments/ImageAttachment.h +10 -0
  52. package/ios/attachments/ImageAttachment.mm +34 -0
  53. package/ios/attachments/MediaAttachment.h +23 -0
  54. package/ios/attachments/MediaAttachment.mm +31 -0
  55. package/ios/config/InputConfig.h +12 -6
  56. package/ios/config/InputConfig.mm +71 -33
  57. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +10 -0
  58. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +7 -0
  59. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +0 -45
  60. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +41 -4
  61. package/ios/inputParser/InputParser.h +5 -5
  62. package/ios/inputParser/InputParser.mm +867 -333
  63. package/ios/inputTextView/InputTextView.h +1 -1
  64. package/ios/inputTextView/InputTextView.mm +100 -59
  65. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +11 -9
  66. package/ios/internals/EnrichedTextInputViewShadowNode.h +28 -24
  67. package/ios/internals/EnrichedTextInputViewShadowNode.mm +64 -47
  68. package/ios/internals/EnrichedTextInputViewState.h +3 -1
  69. package/ios/styles/BlockQuoteStyle.mm +192 -142
  70. package/ios/styles/BoldStyle.mm +96 -62
  71. package/ios/styles/CodeBlockStyle.mm +304 -0
  72. package/ios/styles/H1Style.mm +10 -3
  73. package/ios/styles/H2Style.mm +10 -3
  74. package/ios/styles/H3Style.mm +10 -3
  75. package/ios/styles/HeadingStyleBase.mm +129 -84
  76. package/ios/styles/ImageStyle.mm +160 -0
  77. package/ios/styles/InlineCodeStyle.mm +149 -84
  78. package/ios/styles/ItalicStyle.mm +77 -51
  79. package/ios/styles/LinkStyle.mm +353 -224
  80. package/ios/styles/MentionStyle.mm +434 -220
  81. package/ios/styles/OrderedListStyle.mm +172 -105
  82. package/ios/styles/StrikethroughStyle.mm +53 -34
  83. package/ios/styles/UnderlineStyle.mm +69 -45
  84. package/ios/styles/UnorderedListStyle.mm +170 -105
  85. package/ios/utils/BaseStyleProtocol.h +3 -2
  86. package/ios/utils/ColorExtension.mm +7 -5
  87. package/ios/utils/FontExtension.mm +42 -27
  88. package/ios/utils/ImageData.h +10 -0
  89. package/ios/utils/ImageData.mm +4 -0
  90. package/ios/utils/LayoutManagerExtension.h +1 -1
  91. package/ios/utils/LayoutManagerExtension.mm +334 -109
  92. package/ios/utils/MentionParams.h +0 -1
  93. package/ios/utils/MentionStyleProps.h +1 -1
  94. package/ios/utils/MentionStyleProps.mm +27 -20
  95. package/ios/utils/OccurenceUtils.h +42 -38
  96. package/ios/utils/OccurenceUtils.mm +177 -107
  97. package/ios/utils/ParagraphAttributesUtils.h +6 -1
  98. package/ios/utils/ParagraphAttributesUtils.mm +152 -41
  99. package/ios/utils/ParagraphsUtils.h +2 -1
  100. package/ios/utils/ParagraphsUtils.mm +40 -26
  101. package/ios/utils/StringExtension.h +1 -1
  102. package/ios/utils/StringExtension.mm +19 -16
  103. package/ios/utils/StyleHeaders.h +35 -11
  104. package/ios/utils/TextInsertionUtils.h +13 -2
  105. package/ios/utils/TextInsertionUtils.mm +38 -20
  106. package/ios/utils/WordsUtils.h +2 -1
  107. package/ios/utils/WordsUtils.mm +32 -22
  108. package/ios/utils/ZeroWidthSpaceUtils.h +3 -1
  109. package/ios/utils/ZeroWidthSpaceUtils.mm +153 -75
  110. package/lib/module/EnrichedTextInput.js +41 -3
  111. package/lib/module/EnrichedTextInput.js.map +1 -1
  112. package/lib/module/EnrichedTextInputNativeComponent.ts +17 -5
  113. package/lib/module/normalizeHtmlStyle.js +0 -4
  114. package/lib/module/normalizeHtmlStyle.js.map +1 -1
  115. package/lib/typescript/src/EnrichedTextInput.d.ts +2 -5
  116. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  117. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +7 -5
  118. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  119. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
  120. package/package.json +8 -1
  121. package/src/EnrichedTextInput.tsx +48 -7
  122. package/src/EnrichedTextInputNativeComponent.ts +17 -5
  123. 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
 
@@ -40,7 +40,7 @@ We can help you build your next dream product –
40
40
  ## Prerequisites
41
41
 
42
42
  - `react-native-enriched` currently supports only Android and iOS platforms
43
- - It works only with [the React Native New Architecture (Fabric)](https://reactnative.dev/architecture/landing-page) and supports following React Native releases: `0.79`, `0.80`, `0.81` and `0.82`
43
+ - It works only with [the React Native New Architecture (Fabric)](https://reactnative.dev/architecture/landing-page) and supports following React Native releases: `0.79`, `0.80`, `0.81`, `0.82` and `0.83`
44
44
 
45
45
  ## Installation
46
46
 
@@ -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.
@@ -244,14 +238,10 @@ See the [API Reference](docs/API_REFERENCE.md) for a detailed overview of all th
244
238
  ## Known limitations
245
239
 
246
240
  - Only one level of lists is supported. We currently do not support nested lists.
247
- - Inline images are supported only on Android.
248
- - Codeblocks are supported only on Android.
249
241
  - iOS headings can't have the same `fontSize` in their config as input's `fontSize`. Doing so results in incorrect headings behavior.
250
242
 
251
243
  ## Future Plans
252
244
 
253
- - Adding Codeblocks and Inline Images to iOS input.
254
- - Making some optimizations around `onChangeHtml` event, maybe some imperative API to get the HTML output.
255
245
  - Creating `EnrichedText` text component that supports our HTML output format with all additional interactions like pressing links or mentions.
256
246
  - Adding API for custom link detection regex.
257
247
  - Web library implementation via `react-native-web`.
@@ -136,7 +136,7 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
136
136
  mViewManager.addLink(view, args.getInt(0), args.getInt(1), args.getString(2), args.getString(3));
137
137
  break;
138
138
  case "addImage":
139
- mViewManager.addImage(view, args.getString(0));
139
+ mViewManager.addImage(view, args.getString(0), (float) args.getDouble(1), (float) args.getDouble(2));
140
140
  break;
141
141
  case "startMention":
142
142
  mViewManager.startMention(view, args.getString(0));
@@ -144,6 +144,9 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
144
144
  case "addMention":
145
145
  mViewManager.addMention(view, args.getString(0), args.getString(1), args.getString(2));
146
146
  break;
147
+ case "requestHTML":
148
+ mViewManager.requestHTML(view, args.getInt(0));
149
+ break;
147
150
  }
148
151
  }
149
152
  }
@@ -50,7 +50,8 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
50
50
  void toggleOrderedList(T view);
51
51
  void toggleUnorderedList(T view);
52
52
  void addLink(T view, int start, int end, String text, String url);
53
- void addImage(T view, String uri);
53
+ void addImage(T view, String uri, float width, float height);
54
54
  void startMention(T view, String indicator);
55
55
  void addMention(T view, String indicator, String text, String payload);
56
+ void requestHTML(T view, int requestId);
56
57
  }
@@ -115,4 +115,14 @@ payload.setProperty(runtime, "text", event.text);
115
115
  });
116
116
  }
117
117
 
118
+
119
+ void EnrichedTextInputViewEventEmitter::onRequestHtmlResult(OnRequestHtmlResult event) const {
120
+ dispatchEvent("requestHtmlResult", [event=std::move(event)](jsi::Runtime &runtime) {
121
+ auto payload = jsi::Object(runtime);
122
+ payload.setProperty(runtime, "requestId", event.requestId);
123
+ payload.setProperty(runtime, "html", jsi::valueFromDynamic(runtime, event.html));
124
+ return payload;
125
+ });
126
+ }
127
+
118
128
  } // namespace facebook::react
@@ -74,6 +74,11 @@ class EnrichedTextInputViewEventEmitter : public ViewEventEmitter {
74
74
  int end;
75
75
  std::string text;
76
76
  };
77
+
78
+ struct OnRequestHtmlResult {
79
+ int requestId;
80
+ folly::dynamic html;
81
+ };
77
82
  void onInputFocus(OnInputFocus value) const;
78
83
 
79
84
  void onInputBlur(OnInputBlur value) const;
@@ -91,5 +96,7 @@ class EnrichedTextInputViewEventEmitter : public ViewEventEmitter {
91
96
  void onMention(OnMention value) const;
92
97
 
93
98
  void onChangeSelection(OnChangeSelection value) const;
99
+
100
+ void onRequestHtmlResult(OnRequestHtmlResult value) const;
94
101
  };
95
102
  } // namespace facebook::react
@@ -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);
@@ -29,7 +29,13 @@ 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.events.OnRequestHtmlResultEvent
33
+ import com.swmansion.enriched.spans.EnrichedH1Span
34
+ import com.swmansion.enriched.spans.EnrichedH2Span
35
+ import com.swmansion.enriched.spans.EnrichedH3Span
36
+ import com.swmansion.enriched.spans.EnrichedImageSpan
32
37
  import com.swmansion.enriched.spans.EnrichedSpans
38
+ import com.swmansion.enriched.spans.interfaces.EnrichedSpan
33
39
  import com.swmansion.enriched.styles.InlineStyles
34
40
  import com.swmansion.enriched.styles.ListStyles
35
41
  import com.swmansion.enriched.styles.ParagraphStyles
@@ -58,9 +64,17 @@ class EnrichedTextInputView : AppCompatEditText {
58
64
 
59
65
  val mentionHandler: MentionHandler? = MentionHandler(this)
60
66
  var htmlStyle: HtmlStyle = HtmlStyle(this, null)
67
+ set(value) {
68
+ if (field != value) {
69
+ val prev = field
70
+ field = value
71
+ reApplyHtmlStyleForSpans(prev, value)
72
+ }
73
+ }
61
74
  var spanWatcher: EnrichedSpanWatcher? = null
62
75
  var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
63
76
 
77
+ var shouldEmitHtml: Boolean = true
64
78
  var experimentalSynchronousEvents: Boolean = false
65
79
 
66
80
  var fontSize: Float? = null
@@ -254,6 +268,7 @@ class EnrichedTextInputView : AppCompatEditText {
254
268
  val newText = parseText(value)
255
269
  setText(newText)
256
270
 
271
+ observeAsyncImages()
257
272
  // Assign SpanWatcher one more time as our previous spannable has been replaced
258
273
  addSpanWatcher(EnrichedSpanWatcher(this))
259
274
 
@@ -262,6 +277,20 @@ class EnrichedTextInputView : AppCompatEditText {
262
277
  }
263
278
  }
264
279
 
280
+ /**
281
+ * Finds all async images in the current text and sets up listeners
282
+ * to redraw the text layout when they finish downloading.
283
+ */
284
+ private fun observeAsyncImages() {
285
+ val liveText = text ?: return
286
+
287
+ val spans = liveText.getSpans(0, liveText.length, EnrichedImageSpan::class.java)
288
+
289
+ for (span in spans) {
290
+ span.observeAsyncDrawableLoaded(liveText)
291
+ }
292
+ }
293
+
265
294
  fun setAutoFocus(autoFocus: Boolean) {
266
295
  this.autoFocus = autoFocus
267
296
  }
@@ -318,6 +347,7 @@ class EnrichedTextInputView : AppCompatEditText {
318
347
  // This ensured that newly created spans will take the new font size into account
319
348
  htmlStyle.invalidateStyles()
320
349
  layoutManager.invalidateLayout()
350
+ forceScrollToSelection()
321
351
  }
322
352
 
323
353
  fun setFontFamily(family: String?) {
@@ -527,11 +557,11 @@ class EnrichedTextInputView : AppCompatEditText {
527
557
  parametrizedStyles?.setLinkSpan(start, end, text, url)
528
558
  }
529
559
 
530
- fun addImage(src: String) {
560
+ fun addImage(src: String, width: Float, height: Float) {
531
561
  val isValid = verifyStyle(EnrichedSpans.IMAGE)
532
562
  if (!isValid) return
533
563
 
534
- parametrizedStyles?.setImageSpan(src)
564
+ parametrizedStyles?.setImageSpan(src, width, height)
535
565
  layoutManager.invalidateLayout()
536
566
  }
537
567
 
@@ -549,6 +579,19 @@ class EnrichedTextInputView : AppCompatEditText {
549
579
  parametrizedStyles?.setMentionSpan(text, indicator, attributes)
550
580
  }
551
581
 
582
+ fun requestHTML(requestId: Int) {
583
+ val html = try {
584
+ EnrichedParser.toHtmlWithDefault(text)
585
+ } catch (e: Exception) {
586
+ null
587
+ }
588
+
589
+ val reactContext = context as ReactContext
590
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
591
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
592
+ dispatcher?.dispatchEvent(OnRequestHtmlResultEvent(surfaceId, id, requestId, html, experimentalSynchronousEvents))
593
+ }
594
+
552
595
  // Sometimes setting up style triggers many changes in sequence
553
596
  // Eg. removing conflicting styles -> changing text -> applying spans
554
597
  // In such scenario we want to prevent from handling side effects (eg. onTextChanged)
@@ -561,6 +604,72 @@ class EnrichedTextInputView : AppCompatEditText {
561
604
  }
562
605
  }
563
606
 
607
+ private fun forceScrollToSelection() {
608
+ val textLayout = layout ?: return
609
+ val cursorOffset = selectionStart
610
+ if (cursorOffset <= 0) return
611
+
612
+ val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
613
+ val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
614
+ val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
615
+ val visibleTextHeight = height - paddingTop - paddingBottom
616
+
617
+ if (visibleTextHeight <= 0) return
618
+
619
+ val visibleTop = scrollY
620
+ val visibleBottom = scrollY + visibleTextHeight
621
+ var targetScrollY = scrollY
622
+
623
+ if (selectedLineTop < visibleTop) {
624
+ targetScrollY = selectedLineTop
625
+ } else if (selectedLineBottom > visibleBottom) {
626
+ targetScrollY = selectedLineBottom - visibleTextHeight
627
+ }
628
+
629
+ val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
630
+ targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
631
+ scrollTo(scrollX, targetScrollY)
632
+ }
633
+
634
+ private fun reApplyHtmlStyleForSpans(previousHtmlStyle: HtmlStyle, nextHtmlStyle: HtmlStyle) {
635
+ val shouldRemoveBoldSpanFromH1Span = !previousHtmlStyle.h1Bold && nextHtmlStyle.h1Bold
636
+ val shouldRemoveBoldSpanFromH2Span = !previousHtmlStyle.h2Bold && nextHtmlStyle.h2Bold
637
+ val shouldRemoveBoldSpanFromH3Span = !previousHtmlStyle.h3Bold && nextHtmlStyle.h3Bold
638
+
639
+ val spannable = text as? Spannable ?: return
640
+ if (spannable.isEmpty()) return
641
+
642
+ var shouldEmitStateChange = false
643
+
644
+ runAsATransaction {
645
+ val spans = spannable.getSpans(0, spannable.length, EnrichedSpan::class.java)
646
+ for (span in spans) {
647
+ if (!span.dependsOnHtmlStyle) continue
648
+
649
+ val start = spannable.getSpanStart(span)
650
+ val end = spannable.getSpanEnd(span)
651
+ val flags = spannable.getSpanFlags(span)
652
+
653
+ if (start == -1 || end == -1) continue
654
+
655
+ if ((span is EnrichedH1Span && shouldRemoveBoldSpanFromH1Span) || (span is EnrichedH2Span && shouldRemoveBoldSpanFromH2Span) || (span is EnrichedH3Span && shouldRemoveBoldSpanFromH3Span)) {
656
+ val isRemoved = removeStyle(EnrichedSpans.BOLD, start, end)
657
+ if (isRemoved) shouldEmitStateChange = true
658
+ }
659
+
660
+ spannable.removeSpan(span)
661
+ val newSpan = span.rebuildWithStyle(htmlStyle)
662
+ spannable.setSpan(newSpan, start, end, flags)
663
+ }
664
+
665
+ if (shouldEmitStateChange) {
666
+ selection?.validateStyles()
667
+ }
668
+ }
669
+ layoutManager.invalidateLayout()
670
+ forceScrollToSelection()
671
+ }
672
+
564
673
  override fun onAttachedToWindow() {
565
674
  super.onAttachedToWindow()
566
675
 
@@ -24,6 +24,7 @@ import com.swmansion.enriched.events.OnInputFocusEvent
24
24
  import com.swmansion.enriched.events.OnLinkDetectedEvent
25
25
  import com.swmansion.enriched.events.OnMentionDetectedEvent
26
26
  import com.swmansion.enriched.events.OnMentionEvent
27
+ import com.swmansion.enriched.events.OnRequestHtmlResultEvent
27
28
  import com.swmansion.enriched.spans.EnrichedSpans
28
29
  import com.swmansion.enriched.styles.HtmlStyle
29
30
  import com.swmansion.enriched.utils.jsonStringToStringMap
@@ -71,6 +72,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
71
72
  map.put(OnMentionDetectedEvent.EVENT_NAME, mapOf("registrationName" to OnMentionDetectedEvent.EVENT_NAME))
72
73
  map.put(OnMentionEvent.EVENT_NAME, mapOf("registrationName" to OnMentionEvent.EVENT_NAME))
73
74
  map.put(OnChangeSelectionEvent.EVENT_NAME, mapOf("registrationName" to OnChangeSelectionEvent.EVENT_NAME))
75
+ map.put(OnRequestHtmlResultEvent.EVENT_NAME, mapOf("registrationName" to OnRequestHtmlResultEvent.EVENT_NAME))
74
76
 
75
77
  return map
76
78
  }
@@ -177,7 +179,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
177
179
  }
178
180
 
179
181
  override fun setIsOnChangeHtmlSet(view: EnrichedTextInputView?, value: Boolean) {
180
- // this prop isn't used on Android as of now, but the setter must be present
182
+ view?.shouldEmitHtml = value
181
183
  }
182
184
 
183
185
  override fun setAutoCapitalize(view: EnrichedTextInputView?, flag: String?) {
@@ -255,8 +257,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
255
257
  view?.addLink(start, end, text, url)
256
258
  }
257
259
 
258
- override fun addImage(view: EnrichedTextInputView?, src: String) {
259
- view?.addImage(src)
260
+ override fun addImage(view: EnrichedTextInputView?, src: String, width: Float, height: Float) {
261
+ view?.addImage(src, width, height)
260
262
  }
261
263
 
262
264
  override fun startMention(view: EnrichedTextInputView?, indicator: String) {
@@ -268,6 +270,10 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
268
270
  view?.addMention(text, indicator, attributes)
269
271
  }
270
272
 
273
+ override fun requestHTML(view: EnrichedTextInputView?, requestId: Int) {
274
+ view?.requestHTML(requestId)
275
+ }
276
+
271
277
  override fun measure(
272
278
  context: Context,
273
279
  localData: ReadableMap?,
@@ -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
@@ -9,8 +9,8 @@ class MentionHandler(private val view: EnrichedTextInputView) {
9
9
  private var previousIndicator: String? = null
10
10
 
11
11
  fun reset() {
12
+ endMention()
12
13
  previousText = null
13
- previousIndicator = null
14
14
  }
15
15
 
16
16
  fun endMention() {
@@ -0,0 +1,33 @@
1
+ package com.swmansion.enriched.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class OnRequestHtmlResultEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val requestId: Int,
11
+ private val html: String?,
12
+ private val experimentalSynchronousEvents: Boolean
13
+ ) : Event<OnRequestHtmlResultEvent>(surfaceId, viewId) {
14
+
15
+ override fun getEventName(): String = EVENT_NAME
16
+
17
+ override fun getEventData(): WritableMap {
18
+ val eventData: WritableMap = Arguments.createMap()
19
+ eventData.putInt("requestId", requestId)
20
+ if (html != null) {
21
+ eventData.putString("html", html)
22
+ } else {
23
+ eventData.putNull("html")
24
+ }
25
+ return eventData
26
+ }
27
+
28
+ override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents
29
+
30
+ companion object {
31
+ const val EVENT_NAME: String = "onRequestHtmlResult"
32
+ }
33
+ }
@@ -11,6 +11,8 @@ import com.swmansion.enriched.styles.HtmlStyle
11
11
 
12
12
  // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/QuoteSpan.java
13
13
  class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedBlockSpan {
14
+ override val dependsOnHtmlStyle: Boolean = true
15
+
14
16
  override fun updateMeasureState(p0: TextPaint) {
15
17
  // Do nothing, but inform layout that this span affects text metrics
16
18
  }
@@ -35,4 +37,8 @@ class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffecting
35
37
  textPaint?.color = color
36
38
  }
37
39
  }
40
+
41
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedBlockQuoteSpan {
42
+ return EnrichedBlockQuoteSpan(htmlStyle)
43
+ }
38
44
  }
@@ -2,9 +2,15 @@ package com.swmansion.enriched.spans
2
2
 
3
3
  import android.graphics.Typeface
4
4
  import android.text.style.StyleSpan
5
+ import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
5
6
  import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
6
7
  import com.swmansion.enriched.styles.HtmlStyle
7
8
 
8
9
  @Suppress("UNUSED_PARAMETER")
9
10
  class EnrichedBoldSpan(htmlStyle: HtmlStyle) : StyleSpan(Typeface.BOLD), EnrichedInlineSpan {
11
+ override val dependsOnHtmlStyle: Boolean = false
12
+
13
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedBoldSpan {
14
+ return EnrichedBoldSpan(htmlStyle)
15
+ }
10
16
  }
@@ -2,8 +2,10 @@ package com.swmansion.enriched.spans
2
2
 
3
3
  import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
+ import android.graphics.Path
5
6
  import android.graphics.RectF
6
7
  import android.graphics.Typeface
8
+ import android.text.Spanned
7
9
  import android.text.TextPaint
8
10
  import android.text.style.LineBackgroundSpan
9
11
  import android.text.style.MetricAffectingSpan
@@ -11,6 +13,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
11
13
  import com.swmansion.enriched.styles.HtmlStyle
12
14
 
13
15
  class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LineBackgroundSpan, EnrichedBlockSpan {
16
+ override val dependsOnHtmlStyle: Boolean = true
17
+
14
18
  override fun updateDrawState(paint: TextPaint) {
15
19
  paint.typeface = Typeface.MONOSPACE
16
20
  paint.color = htmlStyle.codeBlockColor
@@ -33,10 +37,47 @@ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingS
33
37
  end: Int,
34
38
  lineNum: Int
35
39
  ) {
40
+ if (text !is Spanned) {
41
+ return
42
+ }
43
+
36
44
  val previousColor = p.color
37
45
  p.color = htmlStyle.codeBlockBackgroundColor
46
+
47
+ val radius = htmlStyle.codeBlockRadius
48
+
49
+ val spanStart = text.getSpanStart(this)
50
+ val spanEnd = text.getSpanEnd(this)
51
+ val isFirstLineOfSpan = start == spanStart
52
+ val isLastLineOfSpan = end == spanEnd || (spanEnd + 1 == end && text[spanEnd] == '\n')
53
+
54
+ val path = Path()
55
+ val radii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
56
+
57
+ if (isFirstLineOfSpan) {
58
+ // Top-Left and Top-Right corners
59
+ radii[0] = radius
60
+ radii[1] = radius
61
+ radii[2] = radius
62
+ radii[3] = radius
63
+ }
64
+
65
+ if (isLastLineOfSpan) {
66
+ // Bottom-Right and Bottom-Left corners
67
+ radii[4] = radius
68
+ radii[5] = radius
69
+ radii[6] = radius
70
+ radii[7] = radius
71
+ }
72
+
38
73
  val rect = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
39
- canvas.drawRoundRect(rect, htmlStyle.codeBlockRadius, htmlStyle.codeBlockRadius, p)
74
+
75
+ path.addRoundRect(rect, radii, Path.Direction.CW)
76
+ canvas.drawPath(path, p)
40
77
  p.color = previousColor
41
78
  }
79
+
80
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedCodeBlockSpan {
81
+ return EnrichedCodeBlockSpan(htmlStyle)
82
+ }
42
83
  }
@@ -7,6 +7,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
7
7
  import com.swmansion.enriched.styles.HtmlStyle
8
8
 
9
9
  class EnrichedH1Span(private val style: HtmlStyle) : AbsoluteSizeSpan(style.h1FontSize), EnrichedHeadingSpan {
10
+ override val dependsOnHtmlStyle: Boolean = true
11
+
10
12
  override fun updateDrawState(tp: TextPaint) {
11
13
  super.updateDrawState(tp)
12
14
  val bold = style.h1Bold
@@ -14,4 +16,8 @@ class EnrichedH1Span(private val style: HtmlStyle) : AbsoluteSizeSpan(style.h1Fo
14
16
  tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
15
17
  }
16
18
  }
19
+
20
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH1Span {
21
+ return EnrichedH1Span(htmlStyle)
22
+ }
17
23
  }
@@ -7,6 +7,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
7
7
  import com.swmansion.enriched.styles.HtmlStyle
8
8
 
9
9
  class EnrichedH2Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlStyle.h2FontSize), EnrichedHeadingSpan {
10
+ override val dependsOnHtmlStyle: Boolean = true
11
+
10
12
  override fun updateDrawState(tp: TextPaint) {
11
13
  super.updateDrawState(tp)
12
14
  val bold = htmlStyle.h2Bold
@@ -14,4 +16,8 @@ class EnrichedH2Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlSt
14
16
  tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
15
17
  }
16
18
  }
19
+
20
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH2Span {
21
+ return EnrichedH2Span(htmlStyle)
22
+ }
17
23
  }
@@ -7,6 +7,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
7
7
  import com.swmansion.enriched.styles.HtmlStyle
8
8
 
9
9
  class EnrichedH3Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlStyle.h3FontSize), EnrichedHeadingSpan {
10
+ override val dependsOnHtmlStyle: Boolean = true
11
+
10
12
  override fun updateDrawState(tp: TextPaint) {
11
13
  super.updateDrawState(tp)
12
14
  val bold = htmlStyle.h3Bold
@@ -14,4 +16,8 @@ class EnrichedH3Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlSt
14
16
  tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
15
17
  }
16
18
  }
19
+
20
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH3Span {
21
+ return EnrichedH3Span(htmlStyle)
22
+ }
17
23
  }