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

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 (38) hide show
  1. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +48 -0
  2. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +14 -2
  3. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +4 -0
  4. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +30 -13
  5. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +45 -0
  6. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -3
  7. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +4 -4
  8. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +4 -4
  9. package/docs/API_REFERENCE.md +12 -4
  10. package/docs/COPY_OPTIONS.md +26 -0
  11. package/ios/EnrichedMarkdown.mm +22 -8
  12. package/ios/EnrichedMarkdownText.mm +4 -5
  13. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +48 -0
  14. package/ios/utils/ENRMBlockMenu.h +57 -0
  15. package/ios/utils/EditMenuUtils.h +22 -5
  16. package/ios/utils/EditMenuUtils.m +36 -13
  17. package/ios/views/ENRMCodeBlockContainerView.h +9 -2
  18. package/ios/views/ENRMCodeBlockContainerView.m +12 -19
  19. package/ios/views/ENRMMathContainerView.h +4 -0
  20. package/ios/views/ENRMMathContainerView.m +4 -19
  21. package/ios/views/TableContainerView.h +4 -0
  22. package/ios/views/TableContainerView.m +6 -20
  23. package/lib/module/EnrichedMarkdownNativeComponent.ts +8 -0
  24. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +8 -0
  25. package/lib/module/native/EnrichedMarkdownText.js +7 -1
  26. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  27. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +4 -0
  28. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  29. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +4 -0
  30. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  31. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  32. package/lib/typescript/src/types/MarkdownTextProps.d.ts +40 -9
  33. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/EnrichedMarkdownNativeComponent.ts +8 -0
  36. package/src/EnrichedMarkdownTextNativeComponent.ts +8 -0
  37. package/src/native/EnrichedMarkdownText.tsx +13 -0
  38. package/src/types/MarkdownTextProps.ts +37 -10
@@ -2462,6 +2462,10 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2462
2462
  std::string copyImageUrlLabel{};
2463
2463
  std::string copyImageUrlsLabel{};
2464
2464
  std::vector<std::string> copyImageUrlPluralTemplates{};
2465
+ bool copyEnabled{false};
2466
+ bool copyFormatMarkdown{false};
2467
+ bool systemItemsNone{false};
2468
+ bool codeBlockCopyButton{false};
2465
2469
 
2466
2470
 
2467
2471
  #ifdef RN_SERIALIZABLE_STATE
@@ -2476,6 +2480,10 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2476
2480
  result["copyImageUrlLabel"] = copyImageUrlLabel;
2477
2481
  result["copyImageUrlsLabel"] = copyImageUrlsLabel;
2478
2482
  result["copyImageUrlPluralTemplates"] = ::facebook::react::toDynamic(copyImageUrlPluralTemplates);
2483
+ result["copyEnabled"] = copyEnabled;
2484
+ result["copyFormatMarkdown"] = copyFormatMarkdown;
2485
+ result["systemItemsNone"] = systemItemsNone;
2486
+ result["codeBlockCopyButton"] = codeBlockCopyButton;
2479
2487
  return result;
2480
2488
  }
2481
2489
  #endif
@@ -2512,6 +2520,22 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2512
2520
  if (tmp_copyImageUrlPluralTemplates != map.end()) {
2513
2521
  fromRawValue(context, tmp_copyImageUrlPluralTemplates->second, result.copyImageUrlPluralTemplates);
2514
2522
  }
2523
+ auto tmp_copyEnabled = map.find("copyEnabled");
2524
+ if (tmp_copyEnabled != map.end()) {
2525
+ fromRawValue(context, tmp_copyEnabled->second, result.copyEnabled);
2526
+ }
2527
+ auto tmp_copyFormatMarkdown = map.find("copyFormatMarkdown");
2528
+ if (tmp_copyFormatMarkdown != map.end()) {
2529
+ fromRawValue(context, tmp_copyFormatMarkdown->second, result.copyFormatMarkdown);
2530
+ }
2531
+ auto tmp_systemItemsNone = map.find("systemItemsNone");
2532
+ if (tmp_systemItemsNone != map.end()) {
2533
+ fromRawValue(context, tmp_systemItemsNone->second, result.systemItemsNone);
2534
+ }
2535
+ auto tmp_codeBlockCopyButton = map.find("codeBlockCopyButton");
2536
+ if (tmp_codeBlockCopyButton != map.end()) {
2537
+ fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
2538
+ }
2515
2539
  }
2516
2540
 
2517
2541
  static inline std::string toString(const EnrichedMarkdownSelectionMenuConfigStruct &value) {
@@ -6415,6 +6439,10 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6415
6439
  std::string copyImageUrlLabel{};
6416
6440
  std::string copyImageUrlsLabel{};
6417
6441
  std::vector<std::string> copyImageUrlPluralTemplates{};
6442
+ bool copyEnabled{false};
6443
+ bool copyFormatMarkdown{false};
6444
+ bool systemItemsNone{false};
6445
+ bool codeBlockCopyButton{false};
6418
6446
 
6419
6447
 
6420
6448
  #ifdef RN_SERIALIZABLE_STATE
@@ -6429,6 +6457,10 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6429
6457
  result["copyImageUrlLabel"] = copyImageUrlLabel;
6430
6458
  result["copyImageUrlsLabel"] = copyImageUrlsLabel;
6431
6459
  result["copyImageUrlPluralTemplates"] = ::facebook::react::toDynamic(copyImageUrlPluralTemplates);
6460
+ result["copyEnabled"] = copyEnabled;
6461
+ result["copyFormatMarkdown"] = copyFormatMarkdown;
6462
+ result["systemItemsNone"] = systemItemsNone;
6463
+ result["codeBlockCopyButton"] = codeBlockCopyButton;
6432
6464
  return result;
6433
6465
  }
6434
6466
  #endif
@@ -6465,6 +6497,22 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
6465
6497
  if (tmp_copyImageUrlPluralTemplates != map.end()) {
6466
6498
  fromRawValue(context, tmp_copyImageUrlPluralTemplates->second, result.copyImageUrlPluralTemplates);
6467
6499
  }
6500
+ auto tmp_copyEnabled = map.find("copyEnabled");
6501
+ if (tmp_copyEnabled != map.end()) {
6502
+ fromRawValue(context, tmp_copyEnabled->second, result.copyEnabled);
6503
+ }
6504
+ auto tmp_copyFormatMarkdown = map.find("copyFormatMarkdown");
6505
+ if (tmp_copyFormatMarkdown != map.end()) {
6506
+ fromRawValue(context, tmp_copyFormatMarkdown->second, result.copyFormatMarkdown);
6507
+ }
6508
+ auto tmp_systemItemsNone = map.find("systemItemsNone");
6509
+ if (tmp_systemItemsNone != map.end()) {
6510
+ fromRawValue(context, tmp_systemItemsNone->second, result.systemItemsNone);
6511
+ }
6512
+ auto tmp_codeBlockCopyButton = map.find("codeBlockCopyButton");
6513
+ if (tmp_codeBlockCopyButton != map.end()) {
6514
+ fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
6515
+ }
6468
6516
  }
6469
6517
 
6470
6518
  static inline std::string toString(const EnrichedMarkdownTextSelectionMenuConfigStruct &value) {
@@ -30,6 +30,7 @@ import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments
30
30
  import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
31
31
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
32
32
  import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors
33
+ import com.swmansion.enriched.markdown.views.BlockMenuOptions
33
34
  import com.swmansion.enriched.markdown.views.BlockSegmentView
34
35
  import com.swmansion.enriched.markdown.views.CodeBlockContainerView
35
36
  import com.swmansion.enriched.markdown.views.TableContainerView
@@ -309,24 +310,27 @@ class EnrichedMarkdown
309
310
  segmentViews.filterIsInstance<EnrichedMarkdownInternalText>().forEach {
310
311
  it.selectionMenuConfig = config
311
312
  }
312
- // Table and math views cache the copy labels, so re-push them on update
313
- // (e.g. a language change without a remount) to avoid stale labels.
313
+ // Block views cache the copy labels and menu options, so re-push them on
314
+ // update (e.g. a language change without a remount) to avoid stale values.
314
315
  pushCopyLabelsToBlockSegments()
315
316
  }
316
317
 
317
318
  private fun pushCopyLabelsToBlockSegments() {
318
319
  val copyLabel = selectionMenuConfig.copyLabel
319
320
  val copyAsMarkdownLabel = selectionMenuConfig.copyAsMarkdownLabel
321
+ val menuOptions = BlockMenuOptions.from(selectionMenuConfig)
320
322
  segmentViews.forEach { view ->
321
323
  when {
322
324
  view is TableContainerView -> {
323
325
  view.copyLabel = copyLabel
324
326
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
327
+ view.menuOptions = menuOptions
325
328
  }
326
329
 
327
330
  view is CodeBlockContainerView -> {
328
331
  view.copyLabel = copyLabel
329
332
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
333
+ view.menuOptions = menuOptions
330
334
  }
331
335
 
332
336
  isMathContainerView(view) -> {
@@ -335,6 +339,9 @@ class EnrichedMarkdown
335
339
  view.javaClass
336
340
  .getMethod("setCopyAsMarkdownLabel", String::class.java)
337
341
  .invoke(view, copyAsMarkdownLabel)
342
+ view.javaClass
343
+ .getMethod("setMenuOptions", BlockMenuOptions::class.java)
344
+ .invoke(view, menuOptions)
338
345
  }
339
346
  }
340
347
  }
@@ -628,6 +635,7 @@ class EnrichedMarkdown
628
635
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
629
636
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
630
637
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
638
+ menuOptions = BlockMenuOptions.from(this@EnrichedMarkdown.selectionMenuConfig)
631
639
  applyTableNode(segment.node)
632
640
  }
633
641
 
@@ -638,6 +646,7 @@ class EnrichedMarkdown
638
646
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
639
647
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
640
648
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
649
+ menuOptions = BlockMenuOptions.from(this@EnrichedMarkdown.selectionMenuConfig)
641
650
  onCopyPress = { code, language ->
642
651
  this@EnrichedMarkdown.onCopyPressCallback?.invoke(code, language)
643
652
  }
@@ -667,6 +676,9 @@ class EnrichedMarkdown
667
676
  resolvedClass
668
677
  .getMethod("setCopyAsMarkdownLabel", String::class.java)
669
678
  .invoke(view, selectionMenuConfig.copyAsMarkdownLabel)
679
+ resolvedClass
680
+ .getMethod("setMenuOptions", BlockMenuOptions::class.java)
681
+ .invoke(view, BlockMenuOptions.from(selectionMenuConfig))
670
682
  setMathContextMenuHidden(view)
671
683
  resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
672
684
  view
@@ -128,6 +128,10 @@ fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
128
128
  copyImageUrlLabel = value.getString("copyImageUrlLabel") ?: "",
129
129
  copyImageUrlsLabel = value.getString("copyImageUrlsLabel") ?: "",
130
130
  copyImageUrlPluralTemplates = parseStringList(value.getArray("copyImageUrlPluralTemplates")),
131
+ copyEnabled = value.getBooleanOrDefault("copyEnabled", true),
132
+ copyFormatMarkdown = value.getBooleanOrDefault("copyFormatMarkdown", false),
133
+ systemItemsNone = value.getBooleanOrDefault("systemItemsNone", false),
134
+ codeBlockCopyButton = value.getBooleanOrDefault("codeBlockCopyButton", true),
131
135
  )
132
136
  }
133
137
 
@@ -32,6 +32,12 @@ data class SelectionMenuConfig(
32
32
  // Plural templates indexed by image count (0..100). Empty when no pluralLabels
33
33
  // are set; counts > 100 use copyImageUrlsLabel (the "other" form).
34
34
  val copyImageUrlPluralTemplates: List<String> = emptyList(),
35
+ val copyEnabled: Boolean = true,
36
+ // Copy writes Markdown (plain text) instead of plain + HTML / raw block content.
37
+ val copyFormatMarkdown: Boolean = false,
38
+ // Drop every system-provided item (Select all, Share, text assist, PROCESS_TEXT…).
39
+ val systemItemsNone: Boolean = false,
40
+ val codeBlockCopyButton: Boolean = true,
35
41
  )
36
42
 
37
43
  /**
@@ -92,19 +98,26 @@ fun createSelectionActionModeCallback(
92
98
  menu.removeGroup(MENU_ITEM_CUSTOM_GROUP)
93
99
 
94
100
  val selectionMenuConfig = getSelectionMenuConfig()
101
+ val hasSelection = textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart
95
102
 
96
- // The system Copy item is added by the platform; relabel it so it
97
- // matches the rest of the localized menu (iOS rebuilds the menu so
98
- // gets this for free; Android only intercepts the click).
99
- if (selectionMenuConfig.copyLabel.isNotEmpty()) {
103
+ if (selectionMenuConfig.systemItemsNone) {
104
+ // Editor re-adds Select all and the text-assist items before calling
105
+ // us on every prepare, so clear on each pass and put back only Copy.
106
+ // Its click is still intercepted in onActionItemClicked.
107
+ menu.clear()
108
+ if (selectionMenuConfig.copyEnabled && hasSelection) {
109
+ menu.add(Menu.NONE, android.R.id.copy, Menu.NONE, selectionMenuConfig.copyLabel)
110
+ }
111
+ } else if (!selectionMenuConfig.copyEnabled) {
112
+ menu.removeItem(android.R.id.copy)
113
+ } else if (selectionMenuConfig.copyLabel.isNotEmpty()) {
114
+ // The system Copy item is added by the platform; relabel it so it
115
+ // matches the rest of the localized menu (iOS rebuilds the menu so
116
+ // gets this for free; Android only intercepts the click).
100
117
  menu.findItem(android.R.id.copy)?.title = selectionMenuConfig.copyLabel
101
118
  }
102
119
 
103
- if (
104
- selectionMenuConfig.copyAsMarkdown &&
105
- textView.selectionStart >= 0 &&
106
- textView.selectionEnd > textView.selectionStart
107
- ) {
120
+ if (selectionMenuConfig.copyAsMarkdown && hasSelection) {
108
121
  menu.add(
109
122
  Menu.NONE,
110
123
  MENU_ITEM_COPY_MARKDOWN,
@@ -113,7 +126,7 @@ fun createSelectionActionModeCallback(
113
126
  )
114
127
  }
115
128
 
116
- if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) {
129
+ if (hasSelection) {
117
130
  val customItems = getCustomItemTexts()
118
131
  customItems.forEachIndexed { index, text ->
119
132
  menu
@@ -144,7 +157,9 @@ fun createSelectionActionModeCallback(
144
157
 
145
158
  when (itemId) {
146
159
  android.R.id.copy -> {
147
- textView.copyWithHTML()
160
+ if (!getSelectionMenuConfig().copyFormatMarkdown || !textView.copyMarkdownToClipboard()) {
161
+ textView.copyWithHTML()
162
+ }
148
163
  mode?.finish()
149
164
  return true
150
165
  }
@@ -214,10 +229,12 @@ private fun TextView.copyWithHTML() {
214
229
  }
215
230
  }
216
231
 
217
- private fun TextView.copyMarkdownToClipboard() {
218
- val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return
232
+ /** Returns false when the selection has no Markdown to copy. */
233
+ private fun TextView.copyMarkdownToClipboard(): Boolean {
234
+ val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return false
219
235
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
220
236
  clipboard.setPrimaryClip(ClipData.newPlainText("Markdown", markdown))
237
+ return true
221
238
  }
222
239
 
223
240
  /** Returns remote image URLs (http/https only) from the current selection. */
@@ -0,0 +1,45 @@
1
+ package com.swmansion.enriched.markdown.views
2
+
3
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
4
+
5
+ /**
6
+ * Selection-menu options that table, math and code block views need for their
7
+ * own long-press menus. Every default keeps the legacy behavior, so a block view
8
+ * that has not received options yet behaves exactly as before.
9
+ */
10
+ data class BlockMenuOptions(
11
+ val copyHidden: Boolean = false,
12
+ // The Copy item writes the block's Markdown instead of its raw content.
13
+ val copyWritesMarkdown: Boolean = false,
14
+ val copyAsMarkdownHidden: Boolean = false,
15
+ // Hides the copy button in the code block header.
16
+ val copyButtonHidden: Boolean = false,
17
+ ) {
18
+ val hasItems: Boolean get() = !copyHidden || !copyAsMarkdownHidden
19
+
20
+ 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
+ )
28
+ }
29
+ }
30
+
31
+ /** Adds the Copy / Copy as Markdown items shared by the table, math and code block menus. */
32
+ fun ContextMenuPopup.Builder.blockCopyItems(
33
+ options: BlockMenuOptions,
34
+ copyLabel: String,
35
+ copyAsMarkdownLabel: String,
36
+ copyRaw: () -> Unit,
37
+ copyMarkdown: () -> Unit,
38
+ ) {
39
+ if (!options.copyHidden) {
40
+ item(ContextMenuPopup.Icon.COPY, copyLabel, if (options.copyWritesMarkdown) copyMarkdown else copyRaw)
41
+ }
42
+ if (!options.copyAsMarkdownHidden) {
43
+ item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel, copyMarkdown)
44
+ }
45
+ }
@@ -70,6 +70,15 @@ class CodeBlockContainerView(
70
70
  // control, not a context menu, and stays.
71
71
  var contextMenuHidden: Boolean = false
72
72
 
73
+ // Copy / Copy as Markdown visibility, Copy format and the header copy button.
74
+ // The header button always copies the raw code.
75
+ var menuOptions: BlockMenuOptions = BlockMenuOptions()
76
+ set(value) {
77
+ field = value
78
+ // Hiding the button leaves the header (and its height) untouched.
79
+ copyButton.visibility = if (value.copyButtonHidden) View.GONE else View.VISIBLE
80
+ }
81
+
73
82
  var onCopyPress: ((code: String, language: String) -> Unit)? = null
74
83
 
75
84
  private var code: String = ""
@@ -250,10 +259,9 @@ class CodeBlockContainerView(
250
259
  // the event when there is one (a pending block has no menu yet, and a hidden
251
260
  // one never does).
252
261
  private fun showContextMenu(anchor: View): Boolean {
253
- if (pending || contextMenuHidden) return false
262
+ if (pending || contextMenuHidden || !menuOptions.hasItems) return false
254
263
  ContextMenuPopup.show(anchor, this) {
255
- item(ContextMenuPopup.Icon.COPY, copyLabel) { copyCode() }
256
- item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) { copyFencedMarkdown() }
264
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, ::copyCode, ::copyFencedMarkdown)
257
265
  }
258
266
  return true
259
267
  }
@@ -64,6 +64,7 @@ class TableContainerView(
64
64
  var copyLabel: String = ""
65
65
  var copyAsMarkdownLabel: String = ""
66
66
  var contextMenuHidden: Boolean = false
67
+ var menuOptions: BlockMenuOptions = BlockMenuOptions()
67
68
 
68
69
  private val scrollView =
69
70
  HorizontalScrollView(context).apply {
@@ -366,7 +367,7 @@ class TableContainerView(
366
367
  if (contextMenuHidden) return
367
368
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
368
369
  ContextMenuPopup.show(anchor, this) {
369
- item(ContextMenuPopup.Icon.COPY, copyLabel) {
370
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, copyRaw = {
370
371
  val plainText = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
371
372
  if (plainText.isNotEmpty()) {
372
373
  val displayMetrics = context.resources.displayMetrics
@@ -377,10 +378,9 @@ class TableContainerView(
377
378
  val html = HTMLGenerator.generateTableHTML(tableRows, styleConfig, displayMetrics.scaledDensity, displayMetrics.density)
378
379
  clipboard.setPrimaryClip(ClipData.newHtmlText("Table", plainText, html))
379
380
  }
380
- }
381
- item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
381
+ }, copyMarkdown = {
382
382
  if (tableMarkdown.isNotEmpty()) clipboard.setPrimaryClip(ClipData.newPlainText("Table", tableMarkdown))
383
- }
383
+ })
384
384
  }
385
385
  }
386
386
 
@@ -42,6 +42,7 @@ class MathContainerView(
42
42
  var copyLabel: String = ""
43
43
  var copyAsMarkdownLabel: String = ""
44
44
  var contextMenuHidden: Boolean = false
45
+ var menuOptions: BlockMenuOptions = BlockMenuOptions()
45
46
 
46
47
  override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
47
48
  override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()
@@ -121,12 +122,11 @@ class MathContainerView(
121
122
  if (contextMenuHidden) return
122
123
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
123
124
  ContextMenuPopup.show(anchor, this) {
124
- item(ContextMenuPopup.Icon.COPY, copyLabel) {
125
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, copyRaw = {
125
126
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
126
- }
127
- item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
127
+ }, copyMarkdown = {
128
128
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
129
- }
129
+ })
130
130
  }
131
131
  }
132
132
 
@@ -469,7 +469,7 @@ Controls built-in actions added to the native text selection menu. Custom app-pr
469
469
  | -------------------- | ---------------------------------------------- | -------- |
470
470
  | `SelectionMenuConfig` | `{}` (see shape below for per-field defaults) | iOS, Android, macOS |
471
471
 
472
- Each item takes an object: `{ enabled }` toggles visibility (the system `copy` item can't be hidden — only relabeled) and `label` overrides the English default. The labels apply to the main text selection menu as well as the table and math block copy menus.
472
+ Each item takes an object: `{ enabled }` toggles visibility and `label` overrides the English default. Items and labels apply to the main text selection menu as well as the table, math and code block copy menus. `copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
473
473
 
474
474
  > **Deprecation:** the previous boolean shape (`copyAsMarkdown: false`) is still accepted at runtime for backward compatibility but logs a one-time warning. It will be removed in 0.8 — migrate to `{ enabled: false }`.
475
475
 
@@ -477,9 +477,13 @@ Each item takes an object: `{ enabled }` toggles visibility (the system `copy` i
477
477
 
478
478
  ```ts
479
479
  interface SelectionMenuConfig {
480
- /** System "Copy" item — can't be hidden, only relabeled. @default { label: "Copy" } */
481
- copy?: { label?: string };
482
- /** "Copy as Markdown" action. @default { enabled: true, label: "Copy as Markdown" } */
480
+ /**
481
+ * "Copy" item. `format: 'markdown'` copies Markdown as plain text instead of
482
+ * rich text (text selections) or the raw content (blocks).
483
+ * @default { enabled: true, label: "Copy", format: "rich" }
484
+ */
485
+ copy?: { enabled?: boolean; label?: string; format?: 'rich' | 'markdown' };
486
+ /** "Copy as Markdown" action, in the text and block menus. @default { enabled: true, label: "Copy as Markdown" } */
483
487
  copyAsMarkdown?: { enabled?: boolean; label?: string };
484
488
  /** "Copy Image URL" action, shown when the selection contains images. */
485
489
  copyImageUrl?: {
@@ -489,6 +493,10 @@ interface SelectionMenuConfig {
489
493
  /** Forms for multiple images, chosen with Intl.PluralRules. @default { other: "Copy {count} Image URLs" } */
490
494
  pluralLabels?: SelectionMenuPluralLabels;
491
495
  };
496
+ /** `'none'` drops every system item (Select All, Look Up, Share, Translate…). @default 'default' */
497
+ systemItems?: 'default' | 'none';
498
+ /** Copy button in the code block header. @default { enabled: true } */
499
+ codeBlockCopyButton?: { enabled?: boolean };
492
500
  }
493
501
 
494
502
  interface SelectionMenuPluralLabels {
@@ -44,6 +44,32 @@ Use `selectionMenuConfig` to hide built-in selection menu actions while keeping
44
44
  />
45
45
  ```
46
46
 
47
+ ### Copy as Markdown only, no system items
48
+
49
+ `copy` can also be hidden (`{ enabled: false }`) or switched to Markdown with
50
+ `format: 'markdown'`, and `systemItems: 'none'` drops everything the OS adds
51
+ (Select All, Look Up, Share, Translate, app text actions…):
52
+
53
+ ```tsx
54
+ <EnrichedMarkdownText
55
+ markdown={content}
56
+ selectionMenuConfig={{
57
+ copy: { format: 'markdown' }, // inline math → $…$, plain text only
58
+ copyAsMarkdown: { enabled: false },
59
+ systemItems: 'none',
60
+ codeBlockCopyButton: { enabled: false },
61
+ }}
62
+ />
63
+ ```
64
+
65
+ The same `copy` and `copyAsMarkdown` settings drive the long-press menus on
66
+ table, math and code blocks: with `format: 'markdown'` their Copy item writes
67
+ the block's Markdown (`$$…$$`, a fenced code block, a Markdown table). On code
68
+ blocks, the header copy button and the VoiceOver copy action always copy the raw
69
+ code and fire `onCopyPress`; `codeBlockCopyButton: { enabled: false }` hides the
70
+ button, and `copy: { enabled: false }` also removes the VoiceOver action.
71
+ `copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
72
+
47
73
  To turn the menu off entirely rather than item by item, use `contextMenuHidden`:
48
74
 
49
75
  ```tsx
@@ -173,7 +173,7 @@ static char kENRMSegmentFadeAnimatorKey;
173
173
  _tableStreamingMode = ENRMTableStreamingModeProgressive;
174
174
  _codeBlockStreamingMode = ENRMCodeBlockStreamingModeProgressive;
175
175
  _contextMenuHidden = NO;
176
- _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
176
+ _selectionMenuConfig = ENRMDefaultSelectionMenuConfig();
177
177
  _lineBreakStrategy = NSLineBreakStrategyNone;
178
178
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
179
179
  _resolvedLayoutDirection =
@@ -505,9 +505,19 @@ static char kENRMSegmentFadeAnimatorKey;
505
505
  }
506
506
  }
507
507
 
508
- // Table and math views cache the copy labels at creation time, so re-push them
509
- // on prop updates (e.g. a language change without a remount) to avoid stale
510
- // labels. Only the copy/copy-as-markdown labels apply to these block menus.
508
+ - (ENRMBlockMenuOptions)blockMenuOptions
509
+ {
510
+ return (ENRMBlockMenuOptions){
511
+ .copyHidden = !_selectionMenuConfig.copyEnabled,
512
+ .copyWritesMarkdown = _selectionMenuConfig.copyFormatMarkdown,
513
+ .copyAsMarkdownHidden = !_selectionMenuConfig.copyAsMarkdown,
514
+ .copyButtonHidden = !_selectionMenuConfig.codeBlockCopyButton,
515
+ };
516
+ }
517
+
518
+ // Table, math and code block views cache the copy labels and menu options at
519
+ // creation time, so re-push them on prop updates (e.g. a language change
520
+ // without a remount) to avoid stale values.
511
521
  - (void)pushSelectionMenuLabelsToSegments
512
522
  {
513
523
  for (RCTUIView *segment in _segmentViews) {
@@ -516,6 +526,7 @@ static char kENRMSegmentFadeAnimatorKey;
516
526
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
517
527
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
518
528
  tableView.contextMenuHidden = _contextMenuHidden;
529
+ tableView.menuOptions = [self blockMenuOptions];
519
530
  }
520
531
  #if ENRICHED_MARKDOWN_MATH
521
532
  else if ([segment isKindOfClass:[ENRMMathContainerView class]]) {
@@ -523,6 +534,7 @@ static char kENRMSegmentFadeAnimatorKey;
523
534
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
524
535
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
525
536
  mathView.contextMenuHidden = _contextMenuHidden;
537
+ mathView.menuOptions = [self blockMenuOptions];
526
538
  }
527
539
  #endif
528
540
  else if ([segment isKindOfClass:[ENRMCodeBlockContainerView class]]) {
@@ -530,6 +542,7 @@ static char kENRMSegmentFadeAnimatorKey;
530
542
  codeBlockView.copyLabel = _selectionMenuLabels.copyLabel;
531
543
  codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
532
544
  codeBlockView.contextMenuHidden = _contextMenuHidden;
545
+ codeBlockView.menuOptions = [self blockMenuOptions];
533
546
  }
534
547
  }
535
548
  }
@@ -787,7 +800,7 @@ static char kENRMSegmentFadeAnimatorKey;
787
800
  selectionEnd:selectionEnd];
788
801
  });
789
802
  return buildEditMenuForSelection(textView.textStorage, textView.selectedRange, segmentMarkdown, strongSelf->_config,
790
- @[ baseMenu ], customItems, strongSelf->_selectionMenuConfig);
803
+ @[ baseMenu ], customItems, strongSelf -> _selectionMenuConfig);
791
804
  }];
792
805
  #endif
793
806
 
@@ -807,6 +820,7 @@ static char kENRMSegmentFadeAnimatorKey;
807
820
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
808
821
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
809
822
  tableView.contextMenuHidden = _contextMenuHidden;
823
+ tableView.menuOptions = [self blockMenuOptions];
810
824
 
811
825
  __weak EnrichedMarkdown *weakSelf = self;
812
826
 
@@ -847,6 +861,7 @@ static char kENRMSegmentFadeAnimatorKey;
847
861
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
848
862
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
849
863
  mathView.contextMenuHidden = _contextMenuHidden;
864
+ mathView.menuOptions = [self blockMenuOptions];
850
865
  [mathView applyLatex:mathSegment.latex];
851
866
  return mathView;
852
867
  }
@@ -858,6 +873,7 @@ static char kENRMSegmentFadeAnimatorKey;
858
873
  codeBlockView.copyLabel = _selectionMenuLabels.copyLabel;
859
874
  codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
860
875
  codeBlockView.contextMenuHidden = _contextMenuHidden;
876
+ codeBlockView.menuOptions = [self blockMenuOptions];
861
877
 
862
878
  __weak EnrichedMarkdown *weakSelf = self;
863
879
  codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
@@ -1026,9 +1042,7 @@ static char kENRMSegmentFadeAnimatorKey;
1026
1042
  }
1027
1043
 
1028
1044
  _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
1029
- _selectionMenuConfig =
1030
- ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsMarkdown,
1031
- newViewProps.selectionMenuConfig.copyImageUrl);
1045
+ _selectionMenuConfig = ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig);
1032
1046
  [self pushSelectionMenuLabelsToSegments];
1033
1047
 
1034
1048
  if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
@@ -239,7 +239,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
239
239
  _enableTaskListItemToggle = YES;
240
240
  _forceHeightUpdateOnNextRender = NO;
241
241
  _contextMenuHidden = NO;
242
- _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
242
+ _selectionMenuConfig = ENRMDefaultSelectionMenuConfig();
243
243
  _lineBreakStrategy = NSLineBreakStrategyNone;
244
244
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
245
245
  _resolvedLayoutDirection =
@@ -301,7 +301,8 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
301
301
  selectionEnd:selectionEnd];
302
302
  });
303
303
  return buildEditMenuForSelection(textView.textStorage, textView.selectedRange, strongSelf->_cachedMarkdown,
304
- strongSelf->_config, @[ baseMenu ], customItems, strongSelf->_selectionMenuConfig);
304
+ strongSelf->_config, @[ baseMenu ], customItems,
305
+ strongSelf -> _selectionMenuConfig);
305
306
  };
306
307
  #endif
307
308
 
@@ -577,9 +578,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
577
578
  }
578
579
 
579
580
  _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
580
- _selectionMenuConfig =
581
- ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsMarkdown,
582
- newViewProps.selectionMenuConfig.copyImageUrl);
581
+ _selectionMenuConfig = ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig);
583
582
 
584
583
  if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
585
584
  _accessibilityLabels = [[ENRMAccessibilityLabels alloc] init];