react-native-enriched-markdown-scaffold 1.0.2-scaffold.2 → 1.0.2-scaffold.3

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 (55) hide show
  1. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  2. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +18 -0
  3. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +12 -0
  4. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +24 -7
  5. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
  6. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +5 -0
  7. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +5 -0
  8. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +4 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +19 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +21 -1
  12. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +27 -8
  13. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +9 -1
  14. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +4 -2
  15. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  16. package/docs/API_REFERENCE.md +48 -0
  17. package/docs/COPY_OPTIONS.md +14 -0
  18. package/ios/EnrichedMarkdown.mm +43 -2
  19. package/ios/EnrichedMarkdownText.mm +25 -3
  20. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  21. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +18 -0
  22. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +12 -0
  23. package/ios/utils/ENRMBlockMenu.h +12 -3
  24. package/ios/utils/EditMenuUtils.h +10 -1
  25. package/ios/utils/EditMenuUtils.m +18 -4
  26. package/ios/views/ENRMCodeBlockContainerView.h +4 -0
  27. package/ios/views/ENRMCodeBlockContainerView.m +8 -1
  28. package/ios/views/ENRMMathContainerView.h +4 -0
  29. package/ios/views/ENRMMathContainerView.m +8 -1
  30. package/ios/views/TableContainerView.h +4 -0
  31. package/ios/views/TableContainerView.m +9 -3
  32. package/lib/module/EnrichedMarkdownNativeComponent.ts +16 -0
  33. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +16 -0
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/native/EnrichedMarkdownText.js +23 -2
  36. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  37. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +13 -0
  38. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  39. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +13 -0
  40. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +1 -1
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
  44. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  45. package/lib/typescript/src/types/MarkdownTextProps.d.ts +13 -1
  46. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  47. package/lib/typescript/src/types/events.d.ts +22 -0
  48. package/lib/typescript/src/types/events.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/EnrichedMarkdownNativeComponent.ts +16 -0
  51. package/src/EnrichedMarkdownTextNativeComponent.ts +16 -0
  52. package/src/index.tsx +1 -0
  53. package/src/native/EnrichedMarkdownText.tsx +24 -1
  54. package/src/types/MarkdownTextProps.ts +13 -0
  55. package/src/types/events.ts +23 -0
@@ -52,6 +52,18 @@ payload.setProperty(runtime, "language", event.language);
52
52
  }
53
53
 
54
54
 
55
+ void EnrichedMarkdownEventEmitter::onCopyRequest(OnCopyRequest event) const {
56
+ dispatchEvent("copyRequest", [event=std::move(event)](jsi::Runtime &runtime) {
57
+ auto payload = jsi::Object(runtime);
58
+ payload.setProperty(runtime, "source", event.source);
59
+ payload.setProperty(runtime, "markdown", event.markdown);
60
+ payload.setProperty(runtime, "text", event.text);
61
+ payload.setProperty(runtime, "whole", event.whole);
62
+ return payload;
63
+ });
64
+ }
65
+
66
+
55
67
  void EnrichedMarkdownEventEmitter::onContextMenuItemPress(OnContextMenuItemPress event) const {
56
68
  dispatchEvent("contextMenuItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
57
69
  auto payload = jsi::Object(runtime);
@@ -379,6 +391,18 @@ payload.setProperty(runtime, "language", event.language);
379
391
  }
380
392
 
381
393
 
394
+ void EnrichedMarkdownTextEventEmitter::onCopyRequest(OnCopyRequest event) const {
395
+ dispatchEvent("copyRequest", [event=std::move(event)](jsi::Runtime &runtime) {
396
+ auto payload = jsi::Object(runtime);
397
+ payload.setProperty(runtime, "source", event.source);
398
+ payload.setProperty(runtime, "markdown", event.markdown);
399
+ payload.setProperty(runtime, "text", event.text);
400
+ payload.setProperty(runtime, "whole", event.whole);
401
+ return payload;
402
+ });
403
+ }
404
+
405
+
382
406
  void EnrichedMarkdownTextEventEmitter::onContextMenuItemPress(OnContextMenuItemPress event) const {
383
407
  dispatchEvent("contextMenuItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
384
408
  auto payload = jsi::Object(runtime);
@@ -36,6 +36,13 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
36
36
  std::string language;
37
37
  };
38
38
 
39
+ struct OnCopyRequest {
40
+ std::string source;
41
+ std::string markdown;
42
+ std::string text;
43
+ bool whole;
44
+ };
45
+
39
46
  struct OnContextMenuItemPress {
40
47
  std::string itemText;
41
48
  std::string selectedText;
@@ -50,6 +57,8 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
50
57
 
51
58
  void onCopyPress(OnCopyPress value) const;
52
59
 
60
+ void onCopyRequest(OnCopyRequest value) const;
61
+
53
62
  void onContextMenuItemPress(OnContextMenuItemPress value) const;
54
63
  };
55
64
  class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
@@ -303,6 +312,13 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
303
312
  std::string language;
304
313
  };
305
314
 
315
+ struct OnCopyRequest {
316
+ std::string source;
317
+ std::string markdown;
318
+ std::string text;
319
+ bool whole;
320
+ };
321
+
306
322
  struct OnContextMenuItemPress {
307
323
  std::string itemText;
308
324
  std::string selectedText;
@@ -317,6 +333,8 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
317
333
 
318
334
  void onCopyPress(OnCopyPress value) const;
319
335
 
336
+ void onCopyRequest(OnCopyRequest value) const;
337
+
320
338
  void onContextMenuItemPress(OnContextMenuItemPress value) const;
321
339
  };
322
340
  } // namespace facebook::react
@@ -2466,6 +2466,7 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2466
2466
  bool copyFormatMarkdown{false};
2467
2467
  bool systemItemsNone{false};
2468
2468
  bool codeBlockCopyButton{false};
2469
+ bool interceptCopy{false};
2469
2470
 
2470
2471
 
2471
2472
  #ifdef RN_SERIALIZABLE_STATE
@@ -2484,6 +2485,7 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2484
2485
  result["copyFormatMarkdown"] = copyFormatMarkdown;
2485
2486
  result["systemItemsNone"] = systemItemsNone;
2486
2487
  result["codeBlockCopyButton"] = codeBlockCopyButton;
2488
+ result["interceptCopy"] = interceptCopy;
2487
2489
  return result;
2488
2490
  }
2489
2491
  #endif
@@ -2536,6 +2538,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2536
2538
  if (tmp_codeBlockCopyButton != map.end()) {
2537
2539
  fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
2538
2540
  }
2541
+ auto tmp_interceptCopy = map.find("interceptCopy");
2542
+ if (tmp_interceptCopy != map.end()) {
2543
+ fromRawValue(context, tmp_interceptCopy->second, result.interceptCopy);
2544
+ }
2539
2545
  }
2540
2546
 
2541
2547
  static inline std::string toString(const EnrichedMarkdownSelectionMenuConfigStruct &value) {
@@ -6443,6 +6449,7 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6443
6449
  bool copyFormatMarkdown{false};
6444
6450
  bool systemItemsNone{false};
6445
6451
  bool codeBlockCopyButton{false};
6452
+ bool interceptCopy{false};
6446
6453
 
6447
6454
 
6448
6455
  #ifdef RN_SERIALIZABLE_STATE
@@ -6461,6 +6468,7 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6461
6468
  result["copyFormatMarkdown"] = copyFormatMarkdown;
6462
6469
  result["systemItemsNone"] = systemItemsNone;
6463
6470
  result["codeBlockCopyButton"] = codeBlockCopyButton;
6471
+ result["interceptCopy"] = interceptCopy;
6464
6472
  return result;
6465
6473
  }
6466
6474
  #endif
@@ -6513,6 +6521,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
6513
6521
  if (tmp_codeBlockCopyButton != map.end()) {
6514
6522
  fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
6515
6523
  }
6524
+ auto tmp_interceptCopy = map.find("interceptCopy");
6525
+ if (tmp_interceptCopy != map.end()) {
6526
+ fromRawValue(context, tmp_interceptCopy->second, result.interceptCopy);
6527
+ }
6516
6528
  }
6517
6529
 
6518
6530
  static inline std::string toString(const EnrichedMarkdownTextSelectionMenuConfigStruct &value) {
@@ -106,6 +106,7 @@ class EnrichedMarkdown
106
106
  private var onCopyPressCallback: ((String, String) -> Unit)? = null
107
107
  private var contextMenuItemTexts: List<String> = emptyList()
108
108
  var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
109
+ var onCopyRequestCallback: ((source: String, markdown: String, text: String, whole: Boolean) -> Unit)? = null
109
110
  var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES
110
111
  set(value) {
111
112
  if (field == value) return
@@ -318,19 +319,18 @@ class EnrichedMarkdown
318
319
  private fun pushCopyLabelsToBlockSegments() {
319
320
  val copyLabel = selectionMenuConfig.copyLabel
320
321
  val copyAsMarkdownLabel = selectionMenuConfig.copyAsMarkdownLabel
321
- val menuOptions = BlockMenuOptions.from(selectionMenuConfig)
322
322
  segmentViews.forEach { view ->
323
323
  when {
324
324
  view is TableContainerView -> {
325
325
  view.copyLabel = copyLabel
326
326
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
327
- view.menuOptions = menuOptions
327
+ view.menuOptions = blockMenuOptions("table")
328
328
  }
329
329
 
330
330
  view is CodeBlockContainerView -> {
331
331
  view.copyLabel = copyLabel
332
332
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
333
- view.menuOptions = menuOptions
333
+ view.menuOptions = blockMenuOptions("code")
334
334
  }
335
335
 
336
336
  isMathContainerView(view) -> {
@@ -341,7 +341,7 @@ class EnrichedMarkdown
341
341
  .invoke(view, copyAsMarkdownLabel)
342
342
  view.javaClass
343
343
  .getMethod("setMenuOptions", BlockMenuOptions::class.java)
344
- .invoke(view, menuOptions)
344
+ .invoke(view, blockMenuOptions("math"))
345
345
  }
346
346
  }
347
347
  }
@@ -368,6 +368,22 @@ class EnrichedMarkdown
368
368
  }
369
369
  }
370
370
 
371
+ private fun blockMenuOptions(source: String) =
372
+ BlockMenuOptions.from(selectionMenuConfig) { markdown, text ->
373
+ onCopyRequestCallback?.invoke(source, markdown, text, true)
374
+ }
375
+
376
+ // Segments only see their own text, so a selection is the whole component
377
+ // only when it spans the sole segment; the markdown is then the original.
378
+ private fun forwardSegmentCopyRequest(
379
+ markdown: String,
380
+ text: String,
381
+ fullSelection: Boolean,
382
+ ) {
383
+ val whole = fullSelection && segmentViews.size == 1 && currentMarkdown.isNotEmpty()
384
+ onCopyRequestCallback?.invoke("selection", if (whole) currentMarkdown else markdown, text, whole)
385
+ }
386
+
371
387
  private fun forwardContextMenuItemPress(
372
388
  itemText: String,
373
389
  selectedText: String,
@@ -615,6 +631,7 @@ class EnrichedMarkdown
615
631
  onTaskListItemPressCallback = { taskIndex, checked, itemText ->
616
632
  this@EnrichedMarkdown.onTaskListItemPressCallback?.invoke(taskIndex, checked, itemText)
617
633
  }
634
+ onCopyRequest = ::forwardSegmentCopyRequest
618
635
 
619
636
  if (contextMenuItemTexts.isNotEmpty()) {
620
637
  setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress)
@@ -635,7 +652,7 @@ class EnrichedMarkdown
635
652
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
636
653
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
637
654
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
638
- menuOptions = BlockMenuOptions.from(this@EnrichedMarkdown.selectionMenuConfig)
655
+ menuOptions = blockMenuOptions("table")
639
656
  applyTableNode(segment.node)
640
657
  }
641
658
 
@@ -646,7 +663,7 @@ class EnrichedMarkdown
646
663
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
647
664
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
648
665
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
649
- menuOptions = BlockMenuOptions.from(this@EnrichedMarkdown.selectionMenuConfig)
666
+ menuOptions = blockMenuOptions("code")
650
667
  onCopyPress = { code, language ->
651
668
  this@EnrichedMarkdown.onCopyPressCallback?.invoke(code, language)
652
669
  }
@@ -678,7 +695,7 @@ class EnrichedMarkdown
678
695
  .invoke(view, selectionMenuConfig.copyAsMarkdownLabel)
679
696
  resolvedClass
680
697
  .getMethod("setMenuOptions", BlockMenuOptions::class.java)
681
- .invoke(view, BlockMenuOptions.from(selectionMenuConfig))
698
+ .invoke(view, blockMenuOptions("math"))
682
699
  setMathContextMenuHidden(view)
683
700
  resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
684
701
  view
@@ -56,6 +56,7 @@ class EnrichedMarkdownInternalText
56
56
  private var contextMenuItemTexts: List<String> = emptyList()
57
57
  private var onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
58
58
  var selectionMenuConfig: SelectionMenuConfig = SelectionMenuConfig()
59
+ var onCopyRequest: ((markdown: String, text: String, fullSelection: Boolean) -> Unit)? = null
59
60
  private var activeActionMode: ActionMode? = null
60
61
  var contextMenuHidden: Boolean = false
61
62
  set(value) {
@@ -82,6 +83,9 @@ class EnrichedMarkdownInternalText
82
83
  onCustomItemPress = { itemText, selectedText, start, end ->
83
84
  onContextMenuItemPress?.invoke(itemText, selectedText, start, end)
84
85
  },
86
+ onCopyRequest = { markdown, text, fullSelection ->
87
+ onCopyRequest?.invoke(markdown, text, fullSelection)
88
+ },
85
89
  )
86
90
  }
87
91
 
@@ -18,6 +18,7 @@ import com.swmansion.enriched.markdown.utils.common.CodeBlockStreamingMode
18
18
  import com.swmansion.enriched.markdown.utils.common.TableStreamingMode
19
19
  import com.swmansion.enriched.markdown.utils.common.emitContextMenuItemPress
20
20
  import com.swmansion.enriched.markdown.utils.common.emitCopyPress
21
+ import com.swmansion.enriched.markdown.utils.common.emitCopyRequest
21
22
  import com.swmansion.enriched.markdown.utils.common.emitLinkLongPress
22
23
  import com.swmansion.enriched.markdown.utils.common.emitLinkPress
23
24
  import com.swmansion.enriched.markdown.utils.common.emitTaskListItemPress
@@ -65,6 +66,10 @@ class EnrichedMarkdownManager :
65
66
  emitCopyPress(view, code, language)
66
67
  }
67
68
 
69
+ view.onCopyRequestCallback = { source, markdown, text, whole ->
70
+ emitCopyRequest(view, source, markdown, text, whole)
71
+ }
72
+
68
73
  return view
69
74
  }
70
75
 
@@ -68,6 +68,7 @@ class EnrichedMarkdownText
68
68
 
69
69
  private var contextMenuItemTexts: List<String> = emptyList()
70
70
  var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
71
+ var onCopyRequestCallback: ((markdown: String, text: String, whole: Boolean) -> Unit)? = null
71
72
 
72
73
  var markdownStyle: StyleConfig? = null
73
74
  private set
@@ -113,6 +114,10 @@ class EnrichedMarkdownText
113
114
  onCustomItemPress = { itemText, selectedText, start, end ->
114
115
  onContextMenuItemPressCallback?.invoke(itemText, selectedText, start, end)
115
116
  },
117
+ // A full selection already carries the original markdown.
118
+ onCopyRequest = { markdown, text, fullSelection ->
119
+ onCopyRequestCallback?.invoke(markdown, text, fullSelection && currentMarkdown.isNotEmpty())
120
+ },
116
121
  )
117
122
  }
118
123
 
@@ -15,6 +15,7 @@ import com.facebook.react.viewmanagers.EnrichedMarkdownTextManagerInterface
15
15
  import com.facebook.yoga.YogaMeasureMode
16
16
  import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay
17
17
  import com.swmansion.enriched.markdown.utils.common.emitContextMenuItemPress
18
+ import com.swmansion.enriched.markdown.utils.common.emitCopyRequest
18
19
  import com.swmansion.enriched.markdown.utils.common.emitLinkLongPress
19
20
  import com.swmansion.enriched.markdown.utils.common.emitLinkPress
20
21
  import com.swmansion.enriched.markdown.utils.common.emitTaskListItemPress
@@ -42,6 +43,9 @@ class EnrichedMarkdownTextManager :
42
43
  view.onContextMenuItemPressCallback = { itemText, selectedText, selectionStart, selectionEnd ->
43
44
  emitContextMenuItemPress(view, itemText, selectedText, selectionStart, selectionEnd)
44
45
  }
46
+ view.onCopyRequestCallback = { markdown, text, whole ->
47
+ emitCopyRequest(view, "selection", markdown, text, whole)
48
+ }
45
49
 
46
50
  view.setOnLinkPressCallback { url ->
47
51
  emitLinkPress(view, url)
@@ -0,0 +1,28 @@
1
+ package com.swmansion.enriched.markdown.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 CopyRequestEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val source: String,
11
+ private val markdown: String,
12
+ private val text: String,
13
+ private val whole: Boolean,
14
+ ) : Event<CopyRequestEvent>(surfaceId, viewId) {
15
+ override fun getEventName(): String = EVENT_NAME
16
+
17
+ override fun getEventData(): WritableMap =
18
+ Arguments.createMap().apply {
19
+ putString("source", source)
20
+ putString("markdown", markdown)
21
+ putString("text", text)
22
+ putBoolean("whole", whole)
23
+ }
24
+
25
+ companion object {
26
+ const val EVENT_NAME: String = "onCopyRequest"
27
+ }
28
+ }
@@ -7,6 +7,7 @@ import com.facebook.react.uimanager.UIManagerHelper
7
7
  import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
8
8
  import com.swmansion.enriched.markdown.events.ContextMenuItemPressEvent
9
9
  import com.swmansion.enriched.markdown.events.CopyPressEvent
10
+ import com.swmansion.enriched.markdown.events.CopyRequestEvent
10
11
  import com.swmansion.enriched.markdown.events.LinkLongPressEvent
11
12
  import com.swmansion.enriched.markdown.events.LinkPressEvent
12
13
  import com.swmansion.enriched.markdown.events.TaskListItemPressEvent
@@ -22,6 +23,8 @@ fun markdownEventTypeConstants(): MutableMap<String, Any> {
22
23
  mapOf("registrationName" to TaskListItemPressEvent.EVENT_NAME)
23
24
  map[CopyPressEvent.EVENT_NAME] =
24
25
  mapOf("registrationName" to CopyPressEvent.EVENT_NAME)
26
+ map[CopyRequestEvent.EVENT_NAME] =
27
+ mapOf("registrationName" to CopyRequestEvent.EVENT_NAME)
25
28
  map[ContextMenuItemPressEvent.EVENT_NAME] =
26
29
  mapOf("registrationName" to ContextMenuItemPressEvent.EVENT_NAME)
27
30
  return map
@@ -74,6 +77,21 @@ fun emitCopyPress(
74
77
  )
75
78
  }
76
79
 
80
+ fun emitCopyRequest(
81
+ view: View,
82
+ source: String,
83
+ markdown: String,
84
+ text: String,
85
+ whole: Boolean,
86
+ ) {
87
+ val context = view.context as com.facebook.react.bridge.ReactContext
88
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
89
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
90
+ eventDispatcher?.dispatchEvent(
91
+ CopyRequestEvent(surfaceId, view.id, source, markdown, text, whole),
92
+ )
93
+ }
94
+
77
95
  fun emitContextMenuItemPress(
78
96
  view: View,
79
97
  itemText: String,
@@ -132,6 +150,7 @@ fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
132
150
  copyFormatMarkdown = value.getBooleanOrDefault("copyFormatMarkdown", false),
133
151
  systemItemsNone = value.getBooleanOrDefault("systemItemsNone", false),
134
152
  codeBlockCopyButton = value.getBooleanOrDefault("codeBlockCopyButton", true),
153
+ interceptCopy = value.getBooleanOrDefault("interceptCopy", false),
135
154
  )
136
155
  }
137
156
 
@@ -38,6 +38,8 @@ data class SelectionMenuConfig(
38
38
  // Drop every system-provided item (Select all, Share, text assist, PROCESS_TEXT…).
39
39
  val systemItemsNone: Boolean = false,
40
40
  val codeBlockCopyButton: Boolean = true,
41
+ // Copy hands the selection to onCopyRequest instead of writing the clipboard.
42
+ val interceptCopy: Boolean = false,
41
43
  )
42
44
 
43
45
  /**
@@ -69,6 +71,9 @@ fun createSelectionActionModeCallback(
69
71
  onActionModeChanged: (mode: ActionMode?) -> Unit = {},
70
72
  onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit =
71
73
  { _, _, _, _ -> },
74
+ // Receives the intercepted Copy when `interceptCopy` is set. `fullSelection`
75
+ // is whether the selection spans the whole text view.
76
+ onCopyRequest: (markdown: String, text: String, fullSelection: Boolean) -> Unit = { _, _, _ -> },
72
77
  ): ActionMode.Callback =
73
78
  object : ActionMode.Callback {
74
79
  override fun onCreateActionMode(
@@ -157,7 +162,10 @@ fun createSelectionActionModeCallback(
157
162
 
158
163
  when (itemId) {
159
164
  android.R.id.copy -> {
160
- if (!getSelectionMenuConfig().copyFormatMarkdown || !textView.copyMarkdownToClipboard()) {
165
+ val config = getSelectionMenuConfig()
166
+ if (config.interceptCopy) {
167
+ textView.requestCopy(onCopyRequest)
168
+ } else if (!config.copyFormatMarkdown || !textView.copyMarkdownToClipboard()) {
161
169
  textView.copyWithHTML()
162
170
  }
163
171
  mode?.finish()
@@ -229,6 +237,18 @@ private fun TextView.copyWithHTML() {
229
237
  }
230
238
  }
231
239
 
240
+ /** Hands the selection to [onCopyRequest] without touching the clipboard. */
241
+ private fun TextView.requestCopy(onCopyRequest: (markdown: String, text: String, fullSelection: Boolean) -> Unit) {
242
+ val start = selectionStart
243
+ val end = selectionEnd
244
+ if (start < 0 || end < 0 || start >= end) return
245
+
246
+ val selectedText = text.subSequence(start, end).toString()
247
+ val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: selectedText
248
+ // Same threshold MarkdownExtractor uses to return the original markdown.
249
+ onCopyRequest(markdown, selectedText, start == 0 && end >= text.length - 1)
250
+ }
251
+
232
252
  /** Returns false when the selection has no Markdown to copy. */
233
253
  private fun TextView.copyMarkdownToClipboard(): Boolean {
234
254
  val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return false
@@ -2,6 +2,9 @@ package com.swmansion.enriched.markdown.views
2
2
 
3
3
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
4
4
 
5
+ /** Receives an intercepted block Copy: the block's Markdown and its raw content. */
6
+ typealias BlockCopyRequest = (markdown: String, text: String) -> Unit
7
+
5
8
  /**
6
9
  * Selection-menu options that table, math and code block views need for their
7
10
  * own long-press menus. Every default keeps the legacy behavior, so a block view
@@ -14,17 +17,23 @@ data class BlockMenuOptions(
14
17
  val copyAsMarkdownHidden: Boolean = false,
15
18
  // Hides the copy button in the code block header.
16
19
  val copyButtonHidden: Boolean = false,
20
+ // Set when `onCopy` is on: the Copy item hands the content here instead of
21
+ // writing the clipboard. The host binds it to the block's source.
22
+ val onCopyRequest: BlockCopyRequest? = null,
17
23
  ) {
18
24
  val hasItems: Boolean get() = !copyHidden || !copyAsMarkdownHidden
19
25
 
20
26
  companion object {
21
- fun from(config: SelectionMenuConfig) =
22
- BlockMenuOptions(
23
- copyHidden = !config.copyEnabled,
24
- copyWritesMarkdown = config.copyFormatMarkdown,
25
- copyAsMarkdownHidden = !config.copyAsMarkdown,
26
- copyButtonHidden = !config.codeBlockCopyButton,
27
- )
27
+ fun from(
28
+ config: SelectionMenuConfig,
29
+ onCopyRequest: BlockCopyRequest? = null,
30
+ ) = BlockMenuOptions(
31
+ copyHidden = !config.copyEnabled,
32
+ copyWritesMarkdown = config.copyFormatMarkdown,
33
+ copyAsMarkdownHidden = !config.copyAsMarkdown,
34
+ copyButtonHidden = !config.codeBlockCopyButton,
35
+ onCopyRequest = onCopyRequest.takeIf { config.interceptCopy },
36
+ )
28
37
  }
29
38
  }
30
39
 
@@ -33,11 +42,21 @@ fun ContextMenuPopup.Builder.blockCopyItems(
33
42
  options: BlockMenuOptions,
34
43
  copyLabel: String,
35
44
  copyAsMarkdownLabel: String,
45
+ // The block's Markdown and raw content, read only for an intercepted Copy.
46
+ markdown: () -> String,
47
+ rawText: () -> String,
36
48
  copyRaw: () -> Unit,
37
49
  copyMarkdown: () -> Unit,
38
50
  ) {
39
51
  if (!options.copyHidden) {
40
- item(ContextMenuPopup.Icon.COPY, copyLabel, if (options.copyWritesMarkdown) copyMarkdown else copyRaw)
52
+ val onCopyRequest = options.onCopyRequest
53
+ val onCopy =
54
+ when {
55
+ onCopyRequest != null -> { -> onCopyRequest(markdown(), rawText()) }
56
+ options.copyWritesMarkdown -> copyMarkdown
57
+ else -> copyRaw
58
+ }
59
+ item(ContextMenuPopup.Icon.COPY, copyLabel, onCopy)
41
60
  }
42
61
  if (!options.copyAsMarkdownHidden) {
43
62
  item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel, copyMarkdown)
@@ -261,7 +261,15 @@ class CodeBlockContainerView(
261
261
  private fun showContextMenu(anchor: View): Boolean {
262
262
  if (pending || contextMenuHidden || !menuOptions.hasItems) return false
263
263
  ContextMenuPopup.show(anchor, this) {
264
- blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, ::copyCode, ::copyFencedMarkdown)
264
+ blockCopyItems(
265
+ menuOptions,
266
+ copyLabel,
267
+ copyAsMarkdownLabel,
268
+ markdown = { CodeBlockNode.fencedMarkdown(code, language, fenceChar) },
269
+ rawText = { code },
270
+ copyRaw = ::copyCode,
271
+ copyMarkdown = ::copyFencedMarkdown,
272
+ )
265
273
  }
266
274
  return true
267
275
  }
@@ -367,8 +367,8 @@ class TableContainerView(
367
367
  if (contextMenuHidden) return
368
368
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
369
369
  ContextMenuPopup.show(anchor, this) {
370
- blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, copyRaw = {
371
- val plainText = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
370
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = { tableMarkdown }, rawText = ::buildPlainText, copyRaw = {
371
+ val plainText = buildPlainText()
372
372
  if (plainText.isNotEmpty()) {
373
373
  val displayMetrics = context.resources.displayMetrics
374
374
  val tableRows =
@@ -384,6 +384,8 @@ class TableContainerView(
384
384
  }
385
385
  }
386
386
 
387
+ private fun buildPlainText(): String = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
388
+
387
389
  private fun buildMarkdownFromRows(): String =
388
390
  rows.joinToString("") { row ->
389
391
  val line = "| ${row.joinToString(" | ") { it.markdownText }} |\n"
@@ -122,7 +122,9 @@ class MathContainerView(
122
122
  if (contextMenuHidden) return
123
123
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
124
124
  ContextMenuPopup.show(anchor, this) {
125
- blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, copyRaw = {
125
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = {
126
+ "$$\n$cachedLatex\n$$"
127
+ }, rawText = { cachedLatex }, copyRaw = {
126
128
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
127
129
  }, copyMarkdown = {
128
130
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
@@ -122,6 +122,54 @@ Only fires when `flavor="github"` — the copy button is part of the GitHub flav
122
122
  />
123
123
  ```
124
124
 
125
+ ### `onCopy`
126
+
127
+ Takes over the **Copy** item of the text selection menu and of the table, math and code block menus. When set, native does **not** write the clipboard: it calls `onCopy` with the copied content and your handler writes whatever it wants — for example with [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/). Use it to post-process what users copy (strip formatting, drop link targets…) without a native change.
128
+
129
+ | Type | Default Value | Platform |
130
+ | ----------------------------- | ------------- | -------- |
131
+ | `(event: CopyEvent) => void` | - | Both |
132
+
133
+ ```ts
134
+ interface CopyEvent {
135
+ /** Which menu the Copy item belongs to. */
136
+ source: 'selection' | 'math' | 'table' | 'code';
137
+ /** What `copy.format: 'markdown'` would write. */
138
+ markdown: string;
139
+ /** Rendered text of the selection, or the block's raw content. */
140
+ text: string;
141
+ /** The selection spans the whole component (`markdown` is the source). Always true for blocks. */
142
+ whole: boolean;
143
+ }
144
+ ```
145
+
146
+ | `source` | `markdown` | `text` |
147
+ | ----------- | -------------------------------------------------------------------------- | ----------------------------- |
148
+ | `selection` | Original source when `whole`, otherwise rebuilt from the rendered styles | Selected text as rendered |
149
+ | `math` | `$$…$$` | LaTeX |
150
+ | `table` | The table as Markdown | Tab-separated cells |
151
+ | `code` | Fenced code block | Raw code |
152
+
153
+ - `onCopy` overrides `selectionMenuConfig.copy.format`; `copy.enabled: false` still hides the item, so no event fires.
154
+ - **Copy as Markdown**, the code block header copy button and the VoiceOver copy action are unaffected. `onCopyPress` does not fire for an intercepted Copy.
155
+ - With `flavor="github"` the text is split into segments around blocks, so a selection is `whole` only when the markdown renders as a single text segment.
156
+ - A rebuilt `markdown` wraps each styled run separately (`**a *****b***** c**`) and does not escape literal `*` / `_`.
157
+ - Copying with a hardware keyboard (Cmd+C / Ctrl+C) does not go through the menu and is not intercepted.
158
+ - Ignored on macOS. On web the prop is not this API: it reaches the container `div` as the DOM `onCopy` clipboard event.
159
+
160
+ **Example:**
161
+
162
+ ```tsx
163
+ import * as Clipboard from 'expo-clipboard';
164
+
165
+ <EnrichedMarkdownText
166
+ markdown={content}
167
+ onCopy={({ source, markdown }) => {
168
+ Clipboard.setStringAsync(source === 'selection' ? stripFormatting(markdown) : markdown);
169
+ }}
170
+ />
171
+ ```
172
+
125
173
  ### `enableLinkPreview`
126
174
 
127
175
  Controls the native link preview on long press (iOS only). Automatically set to `false` when `onLinkLongPress` is provided.
@@ -70,6 +70,20 @@ code and fire `onCopyPress`; `codeBlockCopyButton: { enabled: false }` hides the
70
70
  button, and `copy: { enabled: false }` also removes the VoiceOver action.
71
71
  `copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
72
72
 
73
+ ### Handling Copy in JS
74
+
75
+ To decide in JS what lands on the clipboard, pass `onCopy`. The Copy item
76
+ (text selection and block menus) then writes nothing itself and hands you the
77
+ content — `markdown`, `text`, `source` and `whole` — to write with your
78
+ clipboard library. See [`onCopy`](API_REFERENCE.md#oncopy).
79
+
80
+ ```tsx
81
+ <EnrichedMarkdownText
82
+ markdown={content}
83
+ onCopy={({ markdown }) => Clipboard.setStringAsync(markdown)}
84
+ />
85
+ ```
86
+
73
87
  To turn the menu off entirely rather than item by item, use `contextMenuHidden`:
74
88
 
75
89
  ```tsx