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
@@ -57,6 +57,10 @@ fun createSelectionActionModeCallback(
57
57
  textView: TextView,
58
58
  getCustomItemTexts: () -> List<String> = { emptyList() },
59
59
  getSelectionMenuConfig: () -> SelectionMenuConfig = { SelectionMenuConfig() },
60
+ isContextMenuHidden: () -> Boolean = { false },
61
+ // Lets the owning view hold the live ActionMode so it can invalidate() the
62
+ // toolbar when a prop changes while the menu is already on screen.
63
+ onActionModeChanged: (mode: ActionMode?) -> Unit = {},
60
64
  onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit =
61
65
  { _, _, _, _ -> },
62
66
  ): ActionMode.Callback =
@@ -64,7 +68,10 @@ fun createSelectionActionModeCallback(
64
68
  override fun onCreateActionMode(
65
69
  mode: ActionMode?,
66
70
  menu: Menu?,
67
- ): Boolean = true
71
+ ): Boolean {
72
+ onActionModeChanged(mode)
73
+ return true
74
+ }
68
75
 
69
76
  override fun onPrepareActionMode(
70
77
  mode: ActionMode?,
@@ -72,6 +79,14 @@ fun createSelectionActionModeCallback(
72
79
  ): Boolean {
73
80
  if (menu == null) return false
74
81
 
82
+ // Keep the ActionMode alive (selection handles and highlighting stay
83
+ // usable, which is what TalkBack relies on) but ship an empty menu so
84
+ // the floating toolbar has nothing to show.
85
+ if (isContextMenuHidden()) {
86
+ menu.clear()
87
+ return true
88
+ }
89
+
75
90
  menu.removeItem(MENU_ITEM_COPY_MARKDOWN)
76
91
  menu.removeItem(MENU_ITEM_COPY_IMAGE_URL)
77
92
  menu.removeGroup(MENU_ITEM_CUSTOM_GROUP)
@@ -161,7 +176,9 @@ fun createSelectionActionModeCallback(
161
176
  return false
162
177
  }
163
178
 
164
- override fun onDestroyActionMode(mode: ActionMode?) {}
179
+ override fun onDestroyActionMode(mode: ActionMode?) {
180
+ onActionModeChanged(null)
181
+ }
165
182
  }
166
183
 
167
184
  /** Copies selection as both plain text and HTML with inline styles. */
@@ -63,6 +63,7 @@ class TableContainerView(
63
63
 
64
64
  var copyLabel: String = ""
65
65
  var copyAsMarkdownLabel: String = ""
66
+ var contextMenuHidden: Boolean = false
66
67
 
67
68
  private val scrollView =
68
69
  HorizontalScrollView(context).apply {
@@ -337,6 +338,7 @@ class TableContainerView(
337
338
  }
338
339
 
339
340
  private fun showContextMenu(anchor: View) {
341
+ if (contextMenuHidden) return
340
342
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
341
343
  ContextMenuPopup.show(anchor, this) {
342
344
  item(ContextMenuPopup.Icon.COPY, copyLabel) {
@@ -41,6 +41,7 @@ class MathContainerView(
41
41
  // Set reflectively by EnrichedMarkdown (math is an optional module).
42
42
  var copyLabel: String = ""
43
43
  var copyAsMarkdownLabel: String = ""
44
+ var contextMenuHidden: Boolean = false
44
45
 
45
46
  override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
46
47
  override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()
@@ -117,6 +118,7 @@ class MathContainerView(
117
118
  }
118
119
 
119
120
  private fun showContextMenu(anchor: View) {
121
+ if (contextMenuHidden) return
120
122
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
121
123
  ContextMenuPopup.show(anchor, this) {
122
124
  item(ContextMenuPopup.Icon.COPY, copyLabel) {
@@ -105,6 +105,36 @@ Whether text can be selected.
105
105
  | --------- | ------------- | -------- |
106
106
  | `boolean` | `true` | Both |
107
107
 
108
+ ### `contextMenuHidden`
109
+
110
+ Hides the text selection context menu. Text stays selectable — only the menu is
111
+ suppressed. This is deliberately different from `selectable={false}`, which
112
+ disables selection altogether and, on Android, also removes the text from the
113
+ TalkBack focus order.
114
+
115
+ | Type | Default Value | Platform |
116
+ | --------- | ------------- | ----------- |
117
+ | `boolean` | `false` | Both, macOS |
118
+
119
+ ```tsx
120
+ // Text can still be selected, but long-press shows no menu
121
+ <EnrichedMarkdownText markdown={content} contextMenuHidden />
122
+ ```
123
+
124
+ Notes:
125
+
126
+ - It hides the **whole** menu. The custom items from `contextMenuItems` and the
127
+ built-in items from `selectionMenuConfig` share that same menu, so they are
128
+ hidden too. To drop individual items instead, use `selectionMenuConfig`.
129
+ - It also covers the separate copy menus on table and math blocks (which appear
130
+ with `flavor="github"`), not just the text selection menu.
131
+ - It does **not** suppress the system "Save to Camera Roll / Copy" sheet that
132
+ UIKit shows when long-pressing an image — that sheet is owned by UIKit.
133
+ - On macOS the menu opens on click-with-selection rather than long-press; the
134
+ prop suppresses it the same way.
135
+ - Applies to `EnrichedMarkdownText` only — it does not affect
136
+ `EnrichedMarkdownTextInput`.
137
+
108
138
  ### `selectionColor`
109
139
 
110
140
  Color of the text selection highlight. On iOS, this also affects the caret and selection handle colors (they share a single tint). On macOS, only the selection background is affected. On Android, use `selectionHandleColor` to override the handle color independently.
@@ -532,6 +562,18 @@ Auto-capitalization behavior.
532
562
  | -------- | -------------- | -------- |
533
563
  | `string` | `'sentences'` | Both |
534
564
 
565
+ ### `interceptPaste`
566
+
567
+ Reports pastes through [`onPaste`](#onpaste) instead of inserting them. Only takes effect when `onPaste` is also set — otherwise native would swallow every paste with nobody left to resolve it, so the library keeps the prop off.
568
+
569
+ See [Intercepting paste](./INPUT.md#intercepting-paste).
570
+
571
+ | Type | Default Value | Platform |
572
+ | --------- | ------------- | ------------- |
573
+ | `boolean` | `false` | iOS, Android |
574
+
575
+ macOS ignores this prop and always pastes inline.
576
+
535
577
  ### `multiline`
536
578
 
537
579
  Whether the input supports multiple lines.
@@ -710,6 +752,18 @@ Fires when the active mention flow ends.
710
752
  | ---- | ------------- | -------- |
711
753
  | `(event: { indicator: string }) => void` | - | Both |
712
754
 
755
+ ### `onPaste`
756
+
757
+ Fires instead of the insert when [`interceptPaste`](#interceptpaste) is on. Native reads the clipboard and leaves the buffer untouched, so the handler owns the paste from that point: it must call [`resolvePaste`](#resolvepastetext-string-markdown-string) exactly once, on every path including failures, or the paste is silently dropped.
758
+
759
+ | Type | Default Value | Platform |
760
+ | ---- | ------------- | -------- |
761
+ | `(event: { text: string; markdown: string }) => void` | - | iOS, Android |
762
+
763
+ `markdown` is non-empty only when the copy source was an `EnrichedMarkdownTextInput`, which the system clipboard can only carry on iOS; Android always reports `''`.
764
+
765
+ See [Intercepting paste](./INPUT.md#intercepting-paste).
766
+
713
767
  ### `writingDirection`
714
768
 
715
769
  Paragraph writing direction in the input. iOS only — Android's `EditText` already resolves direction per paragraph via `TEXT_DIRECTION_FIRST_STRONG` and is unaffected by this prop.
@@ -942,6 +996,16 @@ Copies the input's full content to the system clipboard, matching the result of
942
996
 
943
997
  On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
944
998
 
999
+ ### `resolvePaste(text: string, markdown?: string)`
1000
+
1001
+ Completes a paste reported through [`onPaste`](#onpaste). Call it exactly once per event:
1002
+
1003
+ - `resolvePaste(text, markdown)` — inserts, restoring the copied formatting when `markdown` is non-empty.
1004
+ - `resolvePaste(text)` — inserts `text` verbatim.
1005
+ - `resolvePaste('', '')` — drops the clipboard content, replacing the current selection. With an empty selection this is a no-op that still emits `onChangeText` with unchanged content.
1006
+
1007
+ Anything that replaces a selection replaces it here too, so a paste over selected text behaves the same on every branch.
1008
+
945
1009
  ### `startMention(indicator: string)`
946
1010
 
947
1011
  Programmatically triggers a mention flow by inserting the indicator character at the current cursor position. The indicator must be listed in the `mentionIndicators` prop. Useful for toolbar buttons.
@@ -44,6 +44,19 @@ Use `selectionMenuConfig` to hide built-in selection menu actions while keeping
44
44
  />
45
45
  ```
46
46
 
47
+ To turn the menu off entirely rather than item by item, use `contextMenuHidden`:
48
+
49
+ ```tsx
50
+ // No selection menu at all — text is still selectable
51
+ <EnrichedMarkdownText markdown={content} contextMenuHidden />
52
+ ```
53
+
54
+ The two are complementary: `selectionMenuConfig` prunes individual built-in
55
+ items and leaves the menu in place, while `contextMenuHidden` suppresses the
56
+ menu as a whole — including `contextMenuItems` and the copy menus on table and
57
+ math blocks. Note that `contextMenuHidden` applies to `EnrichedMarkdownText`
58
+ only; it does not affect `EnrichedMarkdownTextInput`.
59
+
47
60
  `EnrichedMarkdownTextInput` supports the same `{ enabled, label }` shape. In addition to `copyAsMarkdown`, the input's `selectionMenuConfig` exposes the built-in **Format** submenu, and `formatMenuConfig` controls the items inside it:
48
61
 
49
62
  ```tsx
package/docs/INPUT.md CHANGED
@@ -205,6 +205,45 @@ The input's content can be copied to the system clipboard from a ref, without re
205
205
 
206
206
  On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
207
207
 
208
+ ## Intercepting paste
209
+
210
+ By default a paste goes straight into the buffer. Set [`interceptPaste`](API_REFERENCE.md#interceptpaste) together with [`onPaste`](API_REFERENCE.md#onpaste) to decide what a paste means before any text lands — turning a long paste into an attachment chip, stripping it, or inserting it unchanged.
211
+
212
+ ```tsx
213
+ const inputRef = useRef<EnrichedMarkdownTextInputInstance>(null);
214
+
215
+ <EnrichedMarkdownTextInput
216
+ ref={inputRef}
217
+ interceptPaste
218
+ onPaste={({ text, markdown }) => {
219
+ if (text.length > 2000) {
220
+ attachAsFile(text);
221
+ inputRef.current?.resolvePaste('', ''); // drops it, and replaces the selection
222
+ return;
223
+ }
224
+ inputRef.current?.resolvePaste(text, markdown);
225
+ }}
226
+ />
227
+ ```
228
+
229
+ Native reads the clipboard and **does not touch the buffer**. That makes the handler the only thing that can complete the paste, so the contract is strict:
230
+
231
+ - Call [`resolvePaste`](API_REFERENCE.md#resolvepastetext-string-markdown-string) **exactly once per event, on every path including failures**. A handler that throws before resolving leaves the paste dropped — the user sees nothing happen.
232
+ - Pass `markdown` back through when inserting. It is the copied formatting; dropping it turns a copy-then-paste inside your app into flat text.
233
+ - `interceptPaste` is ignored unless `onPaste` is set, so switching the prop on alone cannot make the input swallow pastes.
234
+
235
+ Because nothing is written until you resolve, an async decision is safe: the selection is still there, and the user sees an unchanged input rather than text that vanished.
236
+
237
+ ### Platform behavior
238
+
239
+ | | Behavior |
240
+ | --- | --- |
241
+ | **iOS** | Context menu and hardware ⌘V. `markdown` is set when the copy source was an `EnrichedMarkdownTextInput`. |
242
+ | **Android** | Context menu, hardware Ctrl+V and `KEYCODE_PASTE`. `markdown` is always `''` — the Android clipboard carries plain text only. |
243
+ | **macOS** | `interceptPaste` is ignored, `onPaste` never fires, and pastes insert inline as usual. |
244
+
245
+ Only clipboard content is intercepted. Drag-and-drop, autofill and IME content insertion bypass `onPaste` on both platforms and insert normally — on Android they are filtered out explicitly, so an autofill never arrives looking like a paste.
246
+
208
247
  ## Style Detection
209
248
 
210
249
  All of the above styles can be detected with the use of [onChangeState](API_REFERENCE.md#onchangestate) callback payload.
package/docs/WEB.md CHANGED
@@ -41,6 +41,7 @@ The web implementation also exports `WebMarkdownTextProps` which extends `Enrich
41
41
  | `streamingConfig` | Native-only streaming table configuration. Not yet implemented on web. |
42
42
  | `contextMenuItems` | Not supported — browsers don't allow extending the native context menu. |
43
43
  | `selectionMenuConfig` | Not supported — native-only built-in selection menu actions. |
44
+ | `contextMenuHidden` | Not supported — browsers own the context menu on selected text. |
44
45
  | `selectionHandleColor` | Android-only — desktop browsers don't render selection handles. |
45
46
 
46
47
  ## Not supported on web
@@ -98,6 +98,7 @@ static char kENRMSegmentFadeAnimatorKey;
98
98
  BOOL _allowTrailingMargin;
99
99
  BOOL _selectable;
100
100
  BOOL _enableLinkPreview;
101
+ BOOL _contextMenuHidden;
101
102
  BOOL _streamingAnimation;
102
103
  ENRMTableStreamingMode _tableStreamingMode;
103
104
 
@@ -179,6 +180,7 @@ static char kENRMSegmentFadeAnimatorKey;
179
180
 
180
181
  _selectable = YES;
181
182
  _enableLinkPreview = YES;
183
+ _contextMenuHidden = NO;
182
184
  _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
183
185
  _resolvedLayoutDirection =
184
186
  [[RCTI18nUtil sharedInstance] isRTL] ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
@@ -450,12 +452,14 @@ static char kENRMSegmentFadeAnimatorKey;
450
452
  TableContainerView *tableView = (TableContainerView *)segment;
451
453
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
452
454
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
455
+ tableView.contextMenuHidden = _contextMenuHidden;
453
456
  }
454
457
  #if ENRICHED_MARKDOWN_MATH
455
458
  else if ([segment isKindOfClass:[ENRMMathContainerView class]]) {
456
459
  ENRMMathContainerView *mathView = (ENRMMathContainerView *)segment;
457
460
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
458
461
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
462
+ mathView.contextMenuHidden = _contextMenuHidden;
459
463
  }
460
464
  #endif
461
465
  }
@@ -647,6 +651,9 @@ static char kENRMSegmentFadeAnimatorKey;
647
651
  if (!strongSelf) {
648
652
  return baseMenu;
649
653
  }
654
+ if (strongSelf->_contextMenuHidden) {
655
+ return nil;
656
+ }
650
657
  NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.textStorage, textView.selectedRange);
651
658
  NSArray<NSMenuItem *> *customItems = ENRMBuildContextMenuItems(
652
659
  strongSelf->_contextMenuItemTexts, strongSelf->_contextMenuItemIcons, textView,
@@ -676,6 +683,7 @@ static char kENRMSegmentFadeAnimatorKey;
676
683
  tableView.accessibilityLabels = _accessibilityLabels;
677
684
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
678
685
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
686
+ tableView.contextMenuHidden = _contextMenuHidden;
679
687
 
680
688
  __weak EnrichedMarkdown *weakSelf = self;
681
689
 
@@ -715,6 +723,7 @@ static char kENRMSegmentFadeAnimatorKey;
715
723
  mathView.accessibilityLabels = _accessibilityLabels;
716
724
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
717
725
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
726
+ mathView.contextMenuHidden = _contextMenuHidden;
718
727
  [mathView applyLatex:mathSegment.latex];
719
728
  return mathView;
720
729
  }
@@ -838,6 +847,7 @@ static char kENRMSegmentFadeAnimatorKey;
838
847
  }
839
848
 
840
849
  _enableLinkPreview = newViewProps.enableLinkPreview;
850
+ _contextMenuHidden = newViewProps.contextMenuHidden;
841
851
 
842
852
  if (newViewProps.streamingAnimation != oldViewProps.streamingAnimation) {
843
853
  _streamingAnimation = newViewProps.streamingAnimation;
@@ -1132,6 +1142,10 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1132
1142
  editMenuForTextInRange:(NSRange)range
1133
1143
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
1134
1144
  {
1145
+ if (_contextMenuHidden) {
1146
+ return nil;
1147
+ }
1148
+
1135
1149
  __weak EnrichedMarkdown *weakSelf = self;
1136
1150
  ENRMContextMenuPressHandler handler =
1137
1151
  ^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
@@ -80,6 +80,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
80
80
  CGFloat _lastElementMarginBottom;
81
81
  BOOL _allowTrailingMargin;
82
82
  BOOL _enableLinkPreview;
83
+ BOOL _contextMenuHidden;
83
84
  BOOL _streamingAnimation;
84
85
  BOOL _forceHeightUpdateOnNextRender;
85
86
 
@@ -231,6 +232,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
231
232
  [[ENRMAsyncRenderCoordinator alloc] initWithQueueLabel:"com.swmansion.enriched.markdown.render"];
232
233
 
233
234
  _enableLinkPreview = YES;
235
+ _contextMenuHidden = NO;
234
236
  _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
235
237
  _resolvedLayoutDirection =
236
238
  [[RCTI18nUtil sharedInstance] isRTL] ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
@@ -280,6 +282,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
280
282
  if (!strongSelf) {
281
283
  return baseMenu;
282
284
  }
285
+ if (strongSelf->_contextMenuHidden) {
286
+ return nil;
287
+ }
283
288
  NSArray<NSMenuItem *> *customItems = ENRMBuildContextMenuItems(
284
289
  strongSelf->_contextMenuItemTexts, strongSelf->_contextMenuItemIcons, textView,
285
290
  ^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
@@ -566,6 +571,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
566
571
  }
567
572
 
568
573
  _enableLinkPreview = newViewProps.enableLinkPreview;
574
+ _contextMenuHidden = newViewProps.contextMenuHidden;
569
575
 
570
576
  if (ENRMContextMenuItemsChanged(oldViewProps.contextMenuItems, newViewProps.contextMenuItems)) {
571
577
  _contextMenuItemTexts = ENRMContextMenuTextsFromItems(newViewProps.contextMenuItems);
@@ -790,6 +796,10 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
790
796
  editMenuForTextInRange:(NSRange)range
791
797
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
792
798
  {
799
+ if (_contextMenuHidden) {
800
+ return nil;
801
+ }
802
+
793
803
  __weak EnrichedMarkdownText *weakSelf = self;
794
804
  ENRMContextMenuPressHandler handler =
795
805
  ^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
@@ -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{};
@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
36
36
  - (void)updateMath:(NSInteger)start latex:(NSString *)latex displayMode:(BOOL)displayMode;
37
37
  - (void)removeMath:(NSInteger)start;
38
38
  - (void)copyToClipboard;
39
+ - (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown;
39
40
  - (void)requestMarkdown:(NSInteger)requestId;
40
41
  - (void)requestCaretRect:(NSInteger)requestId;
41
42
  @end
@@ -399,6 +400,34 @@ if ([commandName isEqualToString:@"copyToClipboard"]) {
399
400
  return;
400
401
  }
401
402
 
403
+ if ([commandName isEqualToString:@"resolvePaste"]) {
404
+ #if RCT_DEBUG
405
+ if ([args count] != 2) {
406
+ RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"EnrichedMarkdownTextInput", commandName, (int)[args count], 2);
407
+ return;
408
+ }
409
+ #endif
410
+
411
+ NSObject *arg0 = args[0];
412
+ #if RCT_DEBUG
413
+ if (!RCTValidateTypeOfViewCommandArgument(arg0, [NSString class], @"string", @"EnrichedMarkdownTextInput", commandName, @"1st")) {
414
+ return;
415
+ }
416
+ #endif
417
+ NSString * text = (NSString *)arg0;
418
+
419
+ NSObject *arg1 = args[1];
420
+ #if RCT_DEBUG
421
+ if (!RCTValidateTypeOfViewCommandArgument(arg1, [NSString class], @"string", @"EnrichedMarkdownTextInput", commandName, @"2nd")) {
422
+ return;
423
+ }
424
+ #endif
425
+ NSString * markdown = (NSString *)arg1;
426
+
427
+ [componentView resolvePaste:text markdown:markdown];
428
+ return;
429
+ }
430
+
402
431
  if ([commandName isEqualToString:@"requestMarkdown"]) {
403
432
  #if RCT_DEBUG
404
433
  if ([args count] != 1) {
@@ -46,12 +46,22 @@ NSString *const kENRMMarkdownPasteboardType = @"com.swmansion.enriched-markdown.
46
46
  markdown = [[NSString alloc] initWithData:markdownValue encoding:NSUTF8StringEncoding];
47
47
  }
48
48
 
49
+ NSString *plainText = pasteboard.string;
50
+
51
+ // Both flavors are read before anything is decided, then nothing is written:
52
+ // JS owns the paste from here and must call resolvePaste. Mutating the buffer
53
+ // first would mean a handler that dies mid-flight leaves the selection gone
54
+ // with no text to show for it — a no-op paste is the visible failure.
55
+ if (self.markdownTextInput.interceptPaste && (plainText.length > 0 || markdown.length > 0)) {
56
+ [self.markdownTextInput emitOnPasteWithText:(plainText ?: @"") markdown:(markdown ?: @"")];
57
+ return;
58
+ }
59
+
49
60
  if (markdown.length > 0 && self.markdownTextInput != nil) {
50
61
  [self.markdownTextInput pasteMarkdown:markdown];
51
62
  return;
52
63
  }
53
64
 
54
- NSString *plainText = pasteboard.string;
55
65
  if (plainText.length > 0) {
56
66
  [self replaceRange:self.selectedTextRange withText:plainText];
57
67
  }
@@ -8,12 +8,17 @@ NS_ASSUME_NONNULL_BEGIN
8
8
 
9
9
  @interface EnrichedMarkdownTextInput : RCTViewComponentView
10
10
  @property (nonatomic, assign) BOOL blockEmitting;
11
+ @property (nonatomic, assign) BOOL interceptPaste;
11
12
  - (CGSize)measureSize:(CGFloat)maxWidth;
12
13
  - (nullable NSString *)markdownForSelectedRange;
13
14
  - (void)copyToClipboard;
14
15
  - (void)pasteMarkdown:(NSString *)markdown;
15
16
  - (void)replaceSelectedTextWith:(NSString *)text formattingRanges:(NSArray *)ranges;
16
17
  - (void)scheduleRelayoutIfNeeded;
18
+ // Paste interception. Declared here rather than in +Internal.h because
19
+ // ENRMInputTextView.mm only imports that header on macOS.
20
+ - (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown;
21
+ - (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown;
17
22
  @end
18
23
 
19
24
  NS_ASSUME_NONNULL_END
@@ -359,6 +359,35 @@ using namespace facebook::react;
359
359
 
360
360
  #endif
361
361
 
362
+ #pragma mark - Paste
363
+
364
+ - (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown
365
+ {
366
+ auto emitter = [self getEventEmitter];
367
+ if (emitter == nullptr)
368
+ return;
369
+ emitter->onPaste({
370
+ .text = std::string([text UTF8String] ?: ""),
371
+ .markdown = std::string([markdown UTF8String] ?: ""),
372
+ });
373
+ }
374
+
375
+ /// The single exit for an intercepted paste. One command rather than two makes
376
+ /// "never swallow a paste" structural: the branch that turns the clipboard into
377
+ /// something else still has to come through here, with empty strings.
378
+ - (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown
379
+ {
380
+ // Round-trips the formatting when the copy came from an input of ours;
381
+ // without this a copy-then-paste inside the app would land as flat text.
382
+ if (markdown.length > 0) {
383
+ [self pasteMarkdown:markdown];
384
+ return;
385
+ }
386
+ // Empty text with an empty selection is a zero-length edit: it still emits
387
+ // onChangeText with unchanged content, which the caller reads as a no-op.
388
+ [self replaceSelectedTextWith:text formattingRanges:@[]];
389
+ }
390
+
362
391
  #pragma mark - State
363
392
 
364
393
  - (void)updateState:(const facebook::react::State::Shared &)state
@@ -456,6 +485,10 @@ using namespace facebook::react;
456
485
  }
457
486
  }
458
487
 
488
+ if (newViewProps.interceptPaste != oldViewProps.interceptPaste) {
489
+ _interceptPaste = newViewProps.interceptPaste;
490
+ }
491
+
459
492
  if (newViewProps.multiline != oldViewProps.multiline) {
460
493
  _textView.textContainer.maximumNumberOfLines = newViewProps.multiline ? 0 : 1;
461
494
  _textView.textContainer.lineBreakMode =
@@ -23,6 +23,10 @@ NS_ASSUME_NONNULL_BEGIN
23
23
  @property (nonatomic, copy, nullable, getter=menuCopyLabel) NSString *copyLabel;
24
24
  @property (nonatomic, copy, nullable, getter=menuCopyAsMarkdownLabel) NSString *copyAsMarkdownLabel;
25
25
 
26
+ /// Suppresses the long-press copy menu on this block. Mirrors the
27
+ /// `contextMenuHidden` prop; selection and link handling are unaffected.
28
+ @property (nonatomic, assign) BOOL contextMenuHidden;
29
+
26
30
  @end
27
31
 
28
32
  NS_ASSUME_NONNULL_END
@@ -117,6 +117,11 @@
117
117
  - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
118
118
  configurationForMenuAtLocation:(CGPoint)location
119
119
  {
120
+ // Returning nil is the UIKit-sanctioned way to suppress a context menu.
121
+ if (self.contextMenuHidden) {
122
+ return nil;
123
+ }
124
+
120
125
  return [UIContextMenuConfiguration
121
126
  configurationWithIdentifier:nil
122
127
  previewProvider:nil
@@ -141,6 +146,12 @@
141
146
  #if TARGET_OS_OSX
142
147
  - (NSMenu *)menuForEvent:(NSEvent *)event
143
148
  {
149
+ // This override never falls through to [super menuForEvent:], so nil fully
150
+ // suppresses the menu rather than restoring the AppKit default.
151
+ if (self.contextMenuHidden) {
152
+ return nil;
153
+ }
154
+
144
155
  NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
145
156
  [menu addItem:ENRMCreateMenuItem(self.copyLabel, ^{ [self copyLatexToPasteboard]; })];
146
157
  [menu addItem:ENRMCreateMenuItem(self.copyAsMarkdownLabel, ^{ [self copyMarkdownToPasteboard]; })];