react-native-enriched-markdown-scaffold 0.7.4-scaffold.10 → 0.7.4-scaffold.12

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 (74) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +6 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +2 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +15 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +3 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +39 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +11 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +8 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +14 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +8 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +18 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +128 -0
  18. package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +8 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +19 -2
  22. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +2 -0
  23. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +2 -0
  24. package/docs/API_REFERENCE.md +64 -0
  25. package/docs/COPY_OPTIONS.md +13 -0
  26. package/docs/INPUT.md +39 -0
  27. package/docs/WEB.md +1 -0
  28. package/ios/EnrichedMarkdown.mm +14 -0
  29. package/ios/EnrichedMarkdownText.mm +10 -0
  30. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
  31. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
  32. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +15 -0
  33. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +3 -0
  34. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +29 -0
  35. package/ios/input/ENRMInputTextView.mm +11 -1
  36. package/ios/input/EnrichedMarkdownTextInput.h +5 -0
  37. package/ios/input/EnrichedMarkdownTextInput.mm +33 -0
  38. package/ios/views/ENRMMathContainerView.h +4 -0
  39. package/ios/views/ENRMMathContainerView.m +11 -0
  40. package/ios/views/TableContainerView.h +4 -0
  41. package/ios/views/TableContainerView.m +7 -0
  42. package/lib/module/EnrichedMarkdownNativeComponent.ts +4 -0
  43. package/lib/module/EnrichedMarkdownTextInput.js +15 -0
  44. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  45. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +22 -0
  46. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +4 -0
  47. package/lib/module/index.js.map +1 -1
  48. package/lib/module/native/EnrichedMarkdownText.js +2 -0
  49. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  50. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +4 -0
  51. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  52. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +24 -1
  53. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  54. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +16 -0
  55. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  56. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +4 -0
  57. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  58. package/lib/typescript/src/index.d.ts +1 -1
  59. package/lib/typescript/src/index.d.ts.map +1 -1
  60. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +1 -1
  61. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  62. package/lib/typescript/src/types/MarkdownTextProps.d.ts +16 -0
  63. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  64. package/lib/typescript/src/types/events.d.ts +15 -0
  65. package/lib/typescript/src/types/events.d.ts.map +1 -1
  66. package/package.json +1 -1
  67. package/src/EnrichedMarkdownNativeComponent.ts +4 -0
  68. package/src/EnrichedMarkdownTextInput.tsx +37 -0
  69. package/src/EnrichedMarkdownTextInputNativeComponent.ts +22 -0
  70. package/src/EnrichedMarkdownTextNativeComponent.ts +4 -0
  71. package/src/index.tsx +1 -0
  72. package/src/native/EnrichedMarkdownText.tsx +2 -0
  73. package/src/types/MarkdownTextProps.ts +16 -0
  74. package/src/types/events.ts +16 -0
@@ -65,6 +65,9 @@ public class EnrichedMarkdownManagerDelegate<T extends View, U extends BaseViewM
65
65
  case "spoilerOverlay":
66
66
  mViewManager.setSpoilerOverlay(view, value == null ? "particles" : (String) value);
67
67
  break;
68
+ case "contextMenuHidden":
69
+ mViewManager.setContextMenuHidden(view, value == null ? false : (boolean) value);
70
+ break;
68
71
  case "contextMenuItems":
69
72
  mViewManager.setContextMenuItems(view, (ReadableArray) value);
70
73
  break;
@@ -29,6 +29,7 @@ public interface EnrichedMarkdownManagerInterface<T extends View> extends ViewMa
29
29
  void setStreamingAnimation(T view, boolean value);
30
30
  void setStreamingConfig(T view, @Nullable ReadableMap value);
31
31
  void setSpoilerOverlay(T view, @Nullable String value);
32
+ void setContextMenuHidden(T view, boolean value);
32
33
  void setContextMenuItems(T view, @Nullable ReadableArray value);
33
34
  void setSelectionMenuConfig(T view, @Nullable ReadableMap value);
34
35
  void setAccessibilityLabels(T view, @Nullable ReadableMap value);
@@ -50,6 +50,9 @@ public class EnrichedMarkdownTextInputManagerDelegate<T extends View, U extends
50
50
  case "autoCapitalize":
51
51
  mViewManager.setAutoCapitalize(view, value == null ? null : (String) value);
52
52
  break;
53
+ case "interceptPaste":
54
+ mViewManager.setInterceptPaste(view, value == null ? false : (boolean) value);
55
+ break;
53
56
  case "multiline":
54
57
  mViewManager.setMultiline(view, value == null ? false : (boolean) value);
55
58
  break;
@@ -160,6 +163,9 @@ public class EnrichedMarkdownTextInputManagerDelegate<T extends View, U extends
160
163
  case "copyToClipboard":
161
164
  mViewManager.copyToClipboard(view);
162
165
  break;
166
+ case "resolvePaste":
167
+ mViewManager.resolvePaste(view, args.getString(0), args.getString(1));
168
+ break;
163
169
  case "requestMarkdown":
164
170
  mViewManager.requestMarkdown(view, args.getInt(0));
165
171
  break;
@@ -24,6 +24,7 @@ public interface EnrichedMarkdownTextInputManagerInterface<T extends View> exten
24
24
  void setScrollEnabled(T view, boolean value);
25
25
  void setBlockSyntax(T view, boolean value);
26
26
  void setAutoCapitalize(T view, @Nullable String value);
27
+ void setInterceptPaste(T view, boolean value);
27
28
  void setMultiline(T view, boolean value);
28
29
  void setCursorColor(T view, @Nullable Integer value);
29
30
  void setSelectionColor(T view, @Nullable Integer value);
@@ -58,6 +59,7 @@ public interface EnrichedMarkdownTextInputManagerInterface<T extends View> exten
58
59
  void updateMath(T view, int start, String latex, boolean displayMode);
59
60
  void removeMath(T view, int start);
60
61
  void copyToClipboard(T view);
62
+ void resolvePaste(T view, String text, String markdown);
61
63
  void requestMarkdown(T view, int requestId);
62
64
  void requestCaretRect(T view, int requestId);
63
65
  }
@@ -65,6 +65,9 @@ public class EnrichedMarkdownTextManagerDelegate<T extends View, U extends BaseV
65
65
  case "spoilerOverlay":
66
66
  mViewManager.setSpoilerOverlay(view, value == null ? "particles" : (String) value);
67
67
  break;
68
+ case "contextMenuHidden":
69
+ mViewManager.setContextMenuHidden(view, value == null ? false : (boolean) value);
70
+ break;
68
71
  case "contextMenuItems":
69
72
  mViewManager.setContextMenuItems(view, (ReadableArray) value);
70
73
  break;
@@ -29,6 +29,7 @@ public interface EnrichedMarkdownTextManagerInterface<T extends View> extends Vi
29
29
  void setStreamingAnimation(T view, boolean value);
30
30
  void setStreamingConfig(T view, @Nullable ReadableMap value);
31
31
  void setSpoilerOverlay(T view, @Nullable String value);
32
+ void setContextMenuHidden(T view, boolean value);
32
33
  void setContextMenuItems(T view, @Nullable ReadableArray value);
33
34
  void setSelectionMenuConfig(T view, @Nullable ReadableMap value);
34
35
  void setAccessibilityLabels(T view, @Nullable ReadableMap value);
@@ -247,6 +247,16 @@ payload.setProperty(runtime, "height", event.height);
247
247
  }
248
248
 
249
249
 
250
+ void EnrichedMarkdownTextInputEventEmitter::onPaste(OnPaste event) const {
251
+ dispatchEvent("paste", [event=std::move(event)](jsi::Runtime &runtime) {
252
+ auto payload = jsi::Object(runtime);
253
+ payload.setProperty(runtime, "text", event.text);
254
+ payload.setProperty(runtime, "markdown", event.markdown);
255
+ return payload;
256
+ });
257
+ }
258
+
259
+
250
260
  void EnrichedMarkdownTextInputEventEmitter::onStartMention(OnStartMention event) const {
251
261
  dispatchEvent("startMention", [event=std::move(event)](jsi::Runtime &runtime) {
252
262
  auto payload = jsi::Object(runtime);
@@ -182,6 +182,11 @@ class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
182
182
  double height;
183
183
  };
184
184
 
185
+ struct OnPaste {
186
+ std::string text;
187
+ std::string markdown;
188
+ };
189
+
185
190
  struct OnStartMention {
186
191
  std::string indicator;
187
192
  };
@@ -218,6 +223,8 @@ class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
218
223
 
219
224
  void onMathPress(OnMathPress value) const;
220
225
 
226
+ void onPaste(OnPaste value) const;
227
+
221
228
  void onStartMention(OnStartMention value) const;
222
229
 
223
230
  void onChangeMention(OnChangeMention value) const;
@@ -32,6 +32,7 @@ EnrichedMarkdownProps::EnrichedMarkdownProps(
32
32
  streamingAnimation(convertRawProp(context, rawProps, "streamingAnimation", sourceProps.streamingAnimation, {false})),
33
33
  streamingConfig(convertRawProp(context, rawProps, "streamingConfig", sourceProps.streamingConfig, {})),
34
34
  spoilerOverlay(convertRawProp(context, rawProps, "spoilerOverlay", sourceProps.spoilerOverlay, {std::string{"particles"}})),
35
+ contextMenuHidden(convertRawProp(context, rawProps, "contextMenuHidden", sourceProps.contextMenuHidden, {false})),
35
36
  contextMenuItems(convertRawProp(context, rawProps, "contextMenuItems", sourceProps.contextMenuItems, {})),
36
37
  selectionMenuConfig(convertRawProp(context, rawProps, "selectionMenuConfig", sourceProps.selectionMenuConfig, {})),
37
38
  accessibilityLabels(convertRawProp(context, rawProps, "accessibilityLabels", sourceProps.accessibilityLabels, {})),
@@ -107,6 +108,10 @@ folly::dynamic EnrichedMarkdownProps::getDiffProps(
107
108
  result["spoilerOverlay"] = spoilerOverlay;
108
109
  }
109
110
 
111
+ if (contextMenuHidden != oldProps->contextMenuHidden) {
112
+ result["contextMenuHidden"] = contextMenuHidden;
113
+ }
114
+
110
115
  if (contextMenuItems != oldProps->contextMenuItems) {
111
116
  result["contextMenuItems"] = toDynamic(contextMenuItems);
112
117
  }
@@ -146,6 +151,7 @@ EnrichedMarkdownTextInputProps::EnrichedMarkdownTextInputProps(
146
151
  scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
147
152
  blockSyntax(convertRawProp(context, rawProps, "blockSyntax", sourceProps.blockSyntax, {false})),
148
153
  autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
154
+ interceptPaste(convertRawProp(context, rawProps, "interceptPaste", sourceProps.interceptPaste, {false})),
149
155
  multiline(convertRawProp(context, rawProps, "multiline", sourceProps.multiline, {false})),
150
156
  cursorColor(convertRawProp(context, rawProps, "cursorColor", sourceProps.cursorColor, {})),
151
157
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
@@ -211,6 +217,10 @@ folly::dynamic EnrichedMarkdownTextInputProps::getDiffProps(
211
217
  result["autoCapitalize"] = autoCapitalize;
212
218
  }
213
219
 
220
+ if (interceptPaste != oldProps->interceptPaste) {
221
+ result["interceptPaste"] = interceptPaste;
222
+ }
223
+
214
224
  if (multiline != oldProps->multiline) {
215
225
  result["multiline"] = multiline;
216
226
  }
@@ -295,6 +305,7 @@ EnrichedMarkdownTextProps::EnrichedMarkdownTextProps(
295
305
  streamingAnimation(convertRawProp(context, rawProps, "streamingAnimation", sourceProps.streamingAnimation, {false})),
296
306
  streamingConfig(convertRawProp(context, rawProps, "streamingConfig", sourceProps.streamingConfig, {})),
297
307
  spoilerOverlay(convertRawProp(context, rawProps, "spoilerOverlay", sourceProps.spoilerOverlay, {std::string{"particles"}})),
308
+ contextMenuHidden(convertRawProp(context, rawProps, "contextMenuHidden", sourceProps.contextMenuHidden, {false})),
298
309
  contextMenuItems(convertRawProp(context, rawProps, "contextMenuItems", sourceProps.contextMenuItems, {})),
299
310
  selectionMenuConfig(convertRawProp(context, rawProps, "selectionMenuConfig", sourceProps.selectionMenuConfig, {})),
300
311
  accessibilityLabels(convertRawProp(context, rawProps, "accessibilityLabels", sourceProps.accessibilityLabels, {})),
@@ -370,6 +381,10 @@ folly::dynamic EnrichedMarkdownTextProps::getDiffProps(
370
381
  result["spoilerOverlay"] = spoilerOverlay;
371
382
  }
372
383
 
384
+ if (contextMenuHidden != oldProps->contextMenuHidden) {
385
+ result["contextMenuHidden"] = contextMenuHidden;
386
+ }
387
+
373
388
  if (contextMenuItems != oldProps->contextMenuItems) {
374
389
  result["contextMenuItems"] = toDynamic(contextMenuItems);
375
390
  }
@@ -2579,6 +2579,7 @@ class EnrichedMarkdownProps final : public ViewProps {
2579
2579
  bool streamingAnimation{false};
2580
2580
  EnrichedMarkdownStreamingConfigStruct streamingConfig{};
2581
2581
  std::string spoilerOverlay{std::string{"particles"}};
2582
+ bool contextMenuHidden{false};
2582
2583
  std::vector<EnrichedMarkdownContextMenuItemsStruct> contextMenuItems{};
2583
2584
  EnrichedMarkdownSelectionMenuConfigStruct selectionMenuConfig{};
2584
2585
  EnrichedMarkdownAccessibilityLabelsStruct accessibilityLabels{};
@@ -3361,6 +3362,7 @@ class EnrichedMarkdownTextInputProps final : public ViewProps {
3361
3362
  bool scrollEnabled{false};
3362
3363
  bool blockSyntax{false};
3363
3364
  std::string autoCapitalize{};
3365
+ bool interceptPaste{false};
3364
3366
  bool multiline{false};
3365
3367
  SharedColor cursorColor{};
3366
3368
  SharedColor selectionColor{};
@@ -5948,6 +5950,7 @@ class EnrichedMarkdownTextProps final : public ViewProps {
5948
5950
  bool streamingAnimation{false};
5949
5951
  EnrichedMarkdownTextStreamingConfigStruct streamingConfig{};
5950
5952
  std::string spoilerOverlay{std::string{"particles"}};
5953
+ bool contextMenuHidden{false};
5951
5954
  std::vector<EnrichedMarkdownTextContextMenuItemsStruct> contextMenuItems{};
5952
5955
  EnrichedMarkdownTextSelectionMenuConfigStruct selectionMenuConfig{};
5953
5956
  EnrichedMarkdownTextAccessibilityLabelsStruct accessibilityLabels{};
@@ -80,6 +80,7 @@ class EnrichedMarkdown
80
80
  private var maxFontSizeMultiplier: Float = 0f
81
81
  private var allowTrailingMargin: Boolean = false
82
82
  private var selectable: Boolean = true
83
+ private var contextMenuHidden: Boolean = false
83
84
  private var selectionColor: Int? = null
84
85
  private var selectionHandleColor: Int? = null
85
86
  private var selectionMenuConfig = SelectionMenuConfig()
@@ -177,6 +178,41 @@ class EnrichedMarkdown
177
178
  }
178
179
  }
179
180
 
181
+ fun setContextMenuHidden(value: Boolean) {
182
+ if (contextMenuHidden == value) return
183
+ contextMenuHidden = value
184
+ applyContextMenuHiddenToSegments()
185
+ }
186
+
187
+ /**
188
+ * Fans the flag out to every segment that owns a copy menu: text segments
189
+ * (floating ActionMode toolbar), tables and math blocks (both use their own
190
+ * long-press popup, so neither is covered by the ActionMode callback).
191
+ */
192
+ private fun applyContextMenuHiddenToSegments() {
193
+ segmentViews.forEach { view ->
194
+ when {
195
+ view is EnrichedMarkdownInternalText -> view.contextMenuHidden = contextMenuHidden
196
+ view is TableContainerView -> view.contextMenuHidden = contextMenuHidden
197
+ isMathContainerView(view) -> setMathContextMenuHidden(view)
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * MathContainerView lives in the optional `math` source set, so it is only
204
+ * reachable reflectively. The Kotlin `var` compiles to a setter taking a
205
+ * primitive boolean — `Boolean::class.java` would resolve to
206
+ * `java.lang.Boolean` and silently miss.
207
+ */
208
+ private fun setMathContextMenuHidden(view: View) {
209
+ runCatching {
210
+ view.javaClass
211
+ .getMethod("setContextMenuHidden", Boolean::class.javaPrimitiveType)
212
+ .invoke(view, contextMenuHidden)
213
+ }.onFailure { Log.e(TAG, "Failed to set contextMenuHidden on math view", it) }
214
+ }
215
+
180
216
  fun setSelectionColor(color: Int?) {
181
217
  if (selectionColor == color) return
182
218
  selectionColor = color
@@ -480,6 +516,7 @@ class EnrichedMarkdown
480
516
  EnrichedMarkdownInternalText(context).apply {
481
517
  spoilerOverlay = this@EnrichedMarkdown.spoilerOverlay
482
518
  selectionMenuConfig = this@EnrichedMarkdown.selectionMenuConfig
519
+ contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
483
520
  accessibilityLabels = this@EnrichedMarkdown.accessibilityLabels
484
521
  setIsSelectable(selectable)
485
522
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -514,6 +551,7 @@ class EnrichedMarkdown
514
551
  onLinkLongPress = onLinkLongPressCallback
515
552
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
516
553
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
554
+ contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
517
555
  applyTableNode(segment.node)
518
556
  }
519
557
 
@@ -539,6 +577,7 @@ class EnrichedMarkdown
539
577
  resolvedClass
540
578
  .getMethod("setCopyAsMarkdownLabel", String::class.java)
541
579
  .invoke(view, selectionMenuConfig.copyAsMarkdownLabel)
580
+ setMathContextMenuHidden(view)
542
581
  resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
543
582
  view
544
583
  } catch (e: Exception) {
@@ -5,6 +5,7 @@ import android.graphics.Canvas
5
5
  import android.os.Build
6
6
  import android.text.Layout
7
7
  import android.util.AttributeSet
8
+ import android.view.ActionMode
8
9
  import android.view.MotionEvent
9
10
  import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
10
11
  import com.swmansion.enriched.markdown.accessibility.AccessibleMarkdownTextView
@@ -49,6 +50,14 @@ class EnrichedMarkdownInternalText
49
50
  private var contextMenuItemTexts: List<String> = emptyList()
50
51
  private var onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
51
52
  var selectionMenuConfig: SelectionMenuConfig = SelectionMenuConfig()
53
+ private var activeActionMode: ActionMode? = null
54
+ var contextMenuHidden: Boolean = false
55
+ set(value) {
56
+ if (field == value) return
57
+ field = value
58
+ // See EnrichedMarkdownText.setContextMenuHidden.
59
+ activeActionMode?.invalidate()
60
+ }
52
61
  var accessibilityLabels: AccessibilityLabels = AccessibilityLabels()
53
62
  set(value) {
54
63
  field = value
@@ -62,6 +71,8 @@ class EnrichedMarkdownInternalText
62
71
  this,
63
72
  getCustomItemTexts = { contextMenuItemTexts },
64
73
  getSelectionMenuConfig = { selectionMenuConfig },
74
+ isContextMenuHidden = { contextMenuHidden },
75
+ onActionModeChanged = { activeActionMode = it },
65
76
  onCustomItemPress = { itemText, selectedText, start, end ->
66
77
  onContextMenuItemPress?.invoke(itemText, selectedText, start, end)
67
78
  },
@@ -144,6 +144,14 @@ class EnrichedMarkdownManager :
144
144
  view?.setAllowTrailingMargin(allowTrailingMargin)
145
145
  }
146
146
 
147
+ @ReactProp(name = "contextMenuHidden", defaultBoolean = false)
148
+ override fun setContextMenuHidden(
149
+ view: EnrichedMarkdown?,
150
+ contextMenuHidden: Boolean,
151
+ ) {
152
+ view?.setContextMenuHidden(contextMenuHidden)
153
+ }
154
+
147
155
  @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
148
156
  override fun setEnableLinkPreview(
149
157
  view: EnrichedMarkdown?,
@@ -9,6 +9,7 @@ import android.os.Looper
9
9
  import android.text.Layout
10
10
  import android.util.AttributeSet
11
11
  import android.util.Log
12
+ import android.view.ActionMode
12
13
  import android.view.MotionEvent
13
14
  import com.facebook.react.bridge.ReadableMap
14
15
  import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
@@ -91,6 +92,8 @@ class EnrichedMarkdownText
91
92
  private var selectionColor: Int? = null
92
93
  private var selectionHandleColor: Int? = null
93
94
  private var selectionMenuConfig = SelectionMenuConfig()
95
+ private var contextMenuHidden = false
96
+ private var activeActionMode: ActionMode? = null
94
97
 
95
98
  init {
96
99
  setupAsMarkdownTextView()
@@ -99,12 +102,23 @@ class EnrichedMarkdownText
99
102
  this,
100
103
  getCustomItemTexts = { contextMenuItemTexts },
101
104
  getSelectionMenuConfig = { selectionMenuConfig },
105
+ isContextMenuHidden = { contextMenuHidden },
106
+ onActionModeChanged = { activeActionMode = it },
102
107
  onCustomItemPress = { itemText, selectedText, start, end ->
103
108
  onContextMenuItemPressCallback?.invoke(itemText, selectedText, start, end)
104
109
  },
105
110
  )
106
111
  }
107
112
 
113
+ fun setContextMenuHidden(hidden: Boolean) {
114
+ if (contextMenuHidden == hidden) return
115
+ contextMenuHidden = hidden
116
+ // Rebuild a toolbar that is already on screen; without this the flip
117
+ // would only take effect on the next selection, leaving a working Copy
118
+ // menu up after the app asked for it to be hidden.
119
+ activeActionMode?.invalidate()
120
+ }
121
+
108
122
  fun setMarkdownContent(markdown: String) {
109
123
  if (currentMarkdown == markdown) return
110
124
  currentMarkdown = markdown
@@ -148,6 +148,14 @@ class EnrichedMarkdownTextManager :
148
148
  view?.setAllowTrailingMargin(allowTrailingMargin)
149
149
  }
150
150
 
151
+ @ReactProp(name = "contextMenuHidden", defaultBoolean = false)
152
+ override fun setContextMenuHidden(
153
+ view: EnrichedMarkdownText?,
154
+ contextMenuHidden: Boolean,
155
+ ) {
156
+ view?.setContextMenuHidden(contextMenuHidden)
157
+ }
158
+
151
159
  @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
152
160
  override fun setEnableLinkPreview(
153
161
  view: EnrichedMarkdownText?,
@@ -27,6 +27,7 @@ import com.swmansion.enriched.markdown.input.events.OnInputBlurEvent
27
27
  import com.swmansion.enriched.markdown.input.events.OnInputFocusEvent
28
28
  import com.swmansion.enriched.markdown.input.events.OnLinkDetectedEvent
29
29
  import com.swmansion.enriched.markdown.input.events.OnMathPressEvent
30
+ import com.swmansion.enriched.markdown.input.events.OnPasteEvent
30
31
  import com.swmansion.enriched.markdown.input.events.OnRequestCaretRectResultEvent
31
32
  import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
32
33
  import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
@@ -112,6 +113,7 @@ class EnrichedMarkdownTextInputManager :
112
113
  OnContextMenuItemPressEvent.EVENT_NAME,
113
114
  OnLinkDetectedEvent.EVENT_NAME,
114
115
  OnMathPressEvent.EVENT_NAME,
116
+ OnPasteEvent.EVENT_NAME,
115
117
  OnStartMentionEvent.EVENT_NAME,
116
118
  OnChangeMentionEvent.EVENT_NAME,
117
119
  OnEndMentionEvent.EVENT_NAME,
@@ -186,6 +188,14 @@ class EnrichedMarkdownTextInputManager :
186
188
  view?.setAutoCapitalize(value)
187
189
  }
188
190
 
191
+ @ReactProp(name = "interceptPaste", defaultBoolean = false)
192
+ override fun setInterceptPaste(
193
+ view: EnrichedMarkdownTextInputView?,
194
+ value: Boolean,
195
+ ) {
196
+ view?.interceptPaste = value
197
+ }
198
+
189
199
  @ReactProp(name = "multiline", defaultBoolean = true)
190
200
  override fun setMultiline(
191
201
  view: EnrichedMarkdownTextInputView?,
@@ -517,6 +527,14 @@ class EnrichedMarkdownTextInputManager :
517
527
  view?.copyToClipboard()
518
528
  }
519
529
 
530
+ override fun resolvePaste(
531
+ view: EnrichedMarkdownTextInputView?,
532
+ text: String?,
533
+ markdown: String?,
534
+ ) {
535
+ view?.resolvePaste(text ?: "", markdown ?: "")
536
+ }
537
+
520
538
  override fun requestMarkdown(
521
539
  view: EnrichedMarkdownTextInputView?,
522
540
  requestId: Int,
@@ -20,6 +20,9 @@ import android.view.inputmethod.EditorInfo
20
20
  import android.view.inputmethod.InputConnection
21
21
  import android.view.inputmethod.InputMethodManager
22
22
  import androidx.appcompat.widget.AppCompatEditText
23
+ import androidx.core.view.ContentInfoCompat
24
+ import androidx.core.view.OnReceiveContentListener
25
+ import androidx.core.view.ViewCompat
23
26
  import com.facebook.react.common.ReactConstants
24
27
  import com.facebook.react.uimanager.BackgroundStyleApplicator
25
28
  import com.facebook.react.uimanager.PixelUtil
@@ -44,6 +47,7 @@ import com.swmansion.enriched.markdown.input.model.MathChipStyle
44
47
  import com.swmansion.enriched.markdown.input.model.StyleType
45
48
  import com.swmansion.enriched.markdown.input.toolbar.InputContextMenu
46
49
  import com.swmansion.enriched.markdown.utils.input.AutoCapitalizeUtils
50
+ import com.swmansion.enriched.markdown.utils.input.PasteTextUtils
47
51
  import kotlin.math.ceil
48
52
 
49
53
  private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r' || this == '\u0085' || this == '\u2028' || this == '\u2029'
@@ -97,6 +101,25 @@ class EnrichedMarkdownTextInputView(
97
101
  private var detectScrollMovement = false
98
102
  var scrollEnabled: Boolean = true
99
103
 
104
+ // When on, a paste is reported to JS instead of inserted. Native never touches
105
+ // the buffer here (see resolvePaste): if JS dies mid-handler the paste is a
106
+ // visible no-op rather than a selection that silently vanished.
107
+ //
108
+ // The listener is installed only while the prop is on. Registering it
109
+ // unconditionally would route every consumer's paste through AppCompat's
110
+ // receive-content path even with the feature off, which is a behavior change
111
+ // nobody asked for.
112
+ var interceptPaste: Boolean = false
113
+ set(value) {
114
+ if (field == value) return
115
+ field = value
116
+ ViewCompat.setOnReceiveContentListener(
117
+ this,
118
+ if (value) PASTE_MIME_TYPES else null,
119
+ if (value) receiveContentListener else null,
120
+ )
121
+ }
122
+
100
123
  // The markdown last handed over by JS, kept so `blockSyntax` can re-parse it. @ReactProp
101
124
  // setters fire in undefined order, so `defaultValue` regularly lands before the flag does.
102
125
  private var lastMarkdownFromJS: String? = null
@@ -261,6 +284,10 @@ class EnrichedMarkdownTextInputView(
261
284
  x: Int,
262
285
  y: Int,
263
286
  ) {
287
+ if (text.isNullOrEmpty()) {
288
+ super.scrollTo(x, 0)
289
+ return
290
+ }
264
291
  if (!scrollEnabled) return
265
292
  super.scrollTo(x, y)
266
293
  }
@@ -406,6 +433,11 @@ class EnrichedMarkdownTextInputView(
406
433
  }
407
434
 
408
435
  private fun forceScrollToSelection() {
436
+ if (text.isNullOrEmpty()) {
437
+ super.scrollTo(scrollX, 0)
438
+ return
439
+ }
440
+
409
441
  val textLayout = layout ?: return
410
442
  val cursorOffset = selectionStart
411
443
  if (cursorOffset <= 0) return
@@ -488,6 +520,98 @@ class EnrichedMarkdownTextInputView(
488
520
  clipboard.setPrimaryClip(ClipData.newPlainText(null, content))
489
521
  }
490
522
 
523
+ // Paste arrives by two different routes and only the first one is a menu.
524
+ // `onTextContextMenuItem` covers the context menu and the hardware Ctrl+V /
525
+ // KEYCODE_PASTE shortcuts; drag-and-drop, autofill and IME `commitContent`
526
+ // reach the view through `onReceiveContent` instead. Hooking only one of them
527
+ // leaves the other pasting straight into the buffer with nobody intercepting.
528
+ override fun onTextContextMenuItem(id: Int): Boolean {
529
+ val isPaste = id == android.R.id.paste || id == android.R.id.pasteAsPlainText
530
+ if (isPaste && interceptPaste) {
531
+ val text = readClipboardText()
532
+ if (text.isNotEmpty()) {
533
+ eventEmitter.emitPaste(text, "")
534
+ // Stops super, which is what keeps this single-emit: AppCompat routes
535
+ // its own paste handling through the receive-content listener below.
536
+ return true
537
+ }
538
+ // Nothing textual in the clip: hand it to the platform, matching what the
539
+ // listener does with the items it does not claim. Both routes then decide
540
+ // from the same rule, so a clip never behaves differently depending on
541
+ // whether it came from the menu or from Ctrl+V.
542
+ }
543
+ return super.onTextContextMenuItem(id)
544
+ }
545
+
546
+ private val receiveContentListener =
547
+ OnReceiveContentListener { _, payload ->
548
+ // The listener also sees autofill, drag-and-drop, IME commitContent and
549
+ // PROCESS_TEXT. Without this filter an autofill would turn into a paste
550
+ // event — and a consumer would turn that into an attachment chip.
551
+ if (!interceptPaste || payload.source != ContentInfoCompat.SOURCE_CLIPBOARD) {
552
+ return@OnReceiveContentListener payload
553
+ }
554
+ val parts = payload.partition { it.text != null }
555
+ parts.first?.let { accepted ->
556
+ val clip = accepted.clip
557
+ val text = PasteTextUtils.joinClipTexts((0 until clip.itemCount).map { clip.getItemAt(it).text })
558
+ if (text.isNotEmpty()) {
559
+ eventEmitter.emitPaste(text, "")
560
+ }
561
+ }
562
+ // Hand back only what we did not claim, so the buffer stays untouched.
563
+ parts.second
564
+ }
565
+
566
+ private fun readClipboardText(): String {
567
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return ""
568
+ val clip = clipboard.primaryClip ?: return ""
569
+ // `text`, not `coerceToText`: the listener can only claim items that already
570
+ // carry text, and the two routes have to agree on what counts as a paste.
571
+ return PasteTextUtils.joinClipTexts((0 until clip.itemCount).map { clip.getItemAt(it).text })
572
+ }
573
+
574
+ /**
575
+ * Completes a paste reported through `onPaste`. `markdown` is accepted for
576
+ * parity with iOS but ignored here: Android's clipboard is plain text only
577
+ * (see `copyToClipboard`), so there is no flavor to restore.
578
+ *
579
+ * Resolving with an empty string and an empty selection is a zero-length edit
580
+ * — it still emits, which keeps the JS contract identical on both platforms.
581
+ */
582
+ fun resolvePaste(
583
+ pastedText: String,
584
+ @Suppress("UNUSED_PARAMETER") markdown: String,
585
+ ) {
586
+ val editable = text ?: return
587
+ val selStart = selectionStart.coerceAtLeast(0)
588
+ val selEnd = selectionEnd.coerceAtLeast(selStart)
589
+ val safeStart = selStart.coerceAtMost(editable.length)
590
+ val safeEnd = selEnd.coerceAtMost(editable.length)
591
+
592
+ isProcessingTextChange = true
593
+ try {
594
+ editable.replace(safeStart, safeEnd, pastedText)
595
+ formattingStore.adjustForEdit(safeStart, safeEnd - safeStart, pastedText.length)
596
+ lastProcessedText = editable.toString()
597
+ setSelection(safeStart + pastedText.length)
598
+ } finally {
599
+ isProcessingTextChange = false
600
+ }
601
+
602
+ applyFormatting()
603
+ // Parity with the un-intercepted paste, which reached the detectors through
604
+ // the text watcher: a pasted URL still autolinks.
605
+ detectorPipeline.processTextChange(editable, editable.toString(), safeStart, pastedText.length)
606
+ invalidate()
607
+ requestLayout()
608
+ eventEmitter.emitChangeText()
609
+ if (emitMarkdown) eventEmitter.emitChangeMarkdown()
610
+ eventEmitter.emitSelection(selectionStart, selectionEnd)
611
+ eventEmitter.emitState()
612
+ eventEmitter.emitCaretRectChangeIfNeeded()
613
+ }
614
+
491
615
  fun setLinkForSelection(url: String) {
492
616
  val selStart = selectionStart
493
617
  val selEnd = selectionEnd
@@ -1155,6 +1279,10 @@ class EnrichedMarkdownTextInputView(
1155
1279
  .replace("(", "%28")
1156
1280
  .replace(")", "%29")
1157
1281
 
1282
+ private companion object {
1283
+ private val PASTE_MIME_TYPES = arrayOf("text/*")
1284
+ }
1285
+
1158
1286
  private data class MentionCandidate(
1159
1287
  val indicator: String,
1160
1288
  val start: Int,
@@ -0,0 +1,24 @@
1
+ package com.swmansion.enriched.markdown.input.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 OnPasteEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val text: String,
11
+ private val markdown: String,
12
+ ) : Event<OnPasteEvent>(surfaceId, viewId) {
13
+ override fun getEventName(): String = EVENT_NAME
14
+
15
+ override fun getEventData(): WritableMap =
16
+ Arguments.createMap().apply {
17
+ putString("text", text)
18
+ putString("markdown", markdown)
19
+ }
20
+
21
+ companion object {
22
+ const val EVENT_NAME = "onPaste"
23
+ }
24
+ }
@@ -16,6 +16,7 @@ import com.swmansion.enriched.markdown.input.events.OnInputBlurEvent
16
16
  import com.swmansion.enriched.markdown.input.events.OnInputFocusEvent
17
17
  import com.swmansion.enriched.markdown.input.events.OnLinkDetectedEvent
18
18
  import com.swmansion.enriched.markdown.input.events.OnMathPressEvent
19
+ import com.swmansion.enriched.markdown.input.events.OnPasteEvent
19
20
  import com.swmansion.enriched.markdown.input.events.OnRequestCaretRectResultEvent
20
21
  import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
21
22
  import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
@@ -99,6 +100,13 @@ class InputEventEmitter(
99
100
  dispatch(OnMathPressEvent(surfaceId(), view.id, latex, start, end, displayMode, x, y, width, height))
100
101
  }
101
102
 
103
+ fun emitPaste(
104
+ text: String,
105
+ markdown: String,
106
+ ) {
107
+ dispatch(OnPasteEvent(surfaceId(), view.id, text, markdown))
108
+ }
109
+
102
110
  fun emitStartMention(indicator: String) {
103
111
  dispatch(OnStartMentionEvent(surfaceId(), view.id, indicator))
104
112
  }
@@ -0,0 +1,20 @@
1
+ package com.swmansion.enriched.markdown.utils.input
2
+
3
+ /**
4
+ * The pure half of the paste interception path. A `ClipData` cannot be built on
5
+ * the JVM, so the joining rule lives here where a unit test can reach it; the
6
+ * two call sites (context menu and `onReceiveContent`) only extract the items.
7
+ */
8
+ object PasteTextUtils {
9
+ /**
10
+ * Flattens a clip's items into one string. A clip regularly carries more than
11
+ * one item — reading only `getItemAt(0)` silently drops items 2..n. Items
12
+ * without text (a URI or intent) are skipped, and the rest join on newlines,
13
+ * which is what the platform's own paste does.
14
+ */
15
+ fun joinClipTexts(texts: List<CharSequence?>): String =
16
+ texts
17
+ .mapNotNull { it?.toString() }
18
+ .filter { it.isNotEmpty() }
19
+ .joinToString("\n")
20
+ }