react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 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 +60 -0
  4. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +31 -2
  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 +23 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +50 -13
  12. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +19 -3
  14. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +7 -5
  15. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +6 -4
  16. package/docs/API_REFERENCE.md +60 -4
  17. package/docs/COPY_OPTIONS.md +40 -0
  18. package/ios/EnrichedMarkdown.mm +63 -8
  19. package/ios/EnrichedMarkdownText.mm +26 -5
  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 +60 -0
  23. package/ios/utils/ENRMBlockMenu.h +66 -0
  24. package/ios/utils/EditMenuUtils.h +32 -6
  25. package/ios/utils/EditMenuUtils.m +51 -14
  26. package/ios/views/ENRMCodeBlockContainerView.h +13 -2
  27. package/ios/views/ENRMCodeBlockContainerView.m +19 -19
  28. package/ios/views/ENRMMathContainerView.h +8 -0
  29. package/ios/views/ENRMMathContainerView.m +10 -18
  30. package/ios/views/TableContainerView.h +8 -0
  31. package/ios/views/TableContainerView.m +10 -18
  32. package/lib/module/EnrichedMarkdownNativeComponent.ts +24 -0
  33. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +24 -0
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/native/EnrichedMarkdownText.js +29 -2
  36. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  37. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +17 -0
  38. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  39. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +17 -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 +53 -10
  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 +24 -0
  51. package/src/EnrichedMarkdownTextNativeComponent.ts +24 -0
  52. package/src/index.tsx +1 -0
  53. package/src/native/EnrichedMarkdownText.tsx +37 -1
  54. package/src/types/MarkdownTextProps.ts +50 -10
  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
@@ -2462,6 +2462,11 @@ 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};
2469
+ bool interceptCopy{false};
2465
2470
 
2466
2471
 
2467
2472
  #ifdef RN_SERIALIZABLE_STATE
@@ -2476,6 +2481,11 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2476
2481
  result["copyImageUrlLabel"] = copyImageUrlLabel;
2477
2482
  result["copyImageUrlsLabel"] = copyImageUrlsLabel;
2478
2483
  result["copyImageUrlPluralTemplates"] = ::facebook::react::toDynamic(copyImageUrlPluralTemplates);
2484
+ result["copyEnabled"] = copyEnabled;
2485
+ result["copyFormatMarkdown"] = copyFormatMarkdown;
2486
+ result["systemItemsNone"] = systemItemsNone;
2487
+ result["codeBlockCopyButton"] = codeBlockCopyButton;
2488
+ result["interceptCopy"] = interceptCopy;
2479
2489
  return result;
2480
2490
  }
2481
2491
  #endif
@@ -2512,6 +2522,26 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2512
2522
  if (tmp_copyImageUrlPluralTemplates != map.end()) {
2513
2523
  fromRawValue(context, tmp_copyImageUrlPluralTemplates->second, result.copyImageUrlPluralTemplates);
2514
2524
  }
2525
+ auto tmp_copyEnabled = map.find("copyEnabled");
2526
+ if (tmp_copyEnabled != map.end()) {
2527
+ fromRawValue(context, tmp_copyEnabled->second, result.copyEnabled);
2528
+ }
2529
+ auto tmp_copyFormatMarkdown = map.find("copyFormatMarkdown");
2530
+ if (tmp_copyFormatMarkdown != map.end()) {
2531
+ fromRawValue(context, tmp_copyFormatMarkdown->second, result.copyFormatMarkdown);
2532
+ }
2533
+ auto tmp_systemItemsNone = map.find("systemItemsNone");
2534
+ if (tmp_systemItemsNone != map.end()) {
2535
+ fromRawValue(context, tmp_systemItemsNone->second, result.systemItemsNone);
2536
+ }
2537
+ auto tmp_codeBlockCopyButton = map.find("codeBlockCopyButton");
2538
+ if (tmp_codeBlockCopyButton != map.end()) {
2539
+ fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
2540
+ }
2541
+ auto tmp_interceptCopy = map.find("interceptCopy");
2542
+ if (tmp_interceptCopy != map.end()) {
2543
+ fromRawValue(context, tmp_interceptCopy->second, result.interceptCopy);
2544
+ }
2515
2545
  }
2516
2546
 
2517
2547
  static inline std::string toString(const EnrichedMarkdownSelectionMenuConfigStruct &value) {
@@ -6415,6 +6445,11 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6415
6445
  std::string copyImageUrlLabel{};
6416
6446
  std::string copyImageUrlsLabel{};
6417
6447
  std::vector<std::string> copyImageUrlPluralTemplates{};
6448
+ bool copyEnabled{false};
6449
+ bool copyFormatMarkdown{false};
6450
+ bool systemItemsNone{false};
6451
+ bool codeBlockCopyButton{false};
6452
+ bool interceptCopy{false};
6418
6453
 
6419
6454
 
6420
6455
  #ifdef RN_SERIALIZABLE_STATE
@@ -6429,6 +6464,11 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6429
6464
  result["copyImageUrlLabel"] = copyImageUrlLabel;
6430
6465
  result["copyImageUrlsLabel"] = copyImageUrlsLabel;
6431
6466
  result["copyImageUrlPluralTemplates"] = ::facebook::react::toDynamic(copyImageUrlPluralTemplates);
6467
+ result["copyEnabled"] = copyEnabled;
6468
+ result["copyFormatMarkdown"] = copyFormatMarkdown;
6469
+ result["systemItemsNone"] = systemItemsNone;
6470
+ result["codeBlockCopyButton"] = codeBlockCopyButton;
6471
+ result["interceptCopy"] = interceptCopy;
6432
6472
  return result;
6433
6473
  }
6434
6474
  #endif
@@ -6465,6 +6505,26 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
6465
6505
  if (tmp_copyImageUrlPluralTemplates != map.end()) {
6466
6506
  fromRawValue(context, tmp_copyImageUrlPluralTemplates->second, result.copyImageUrlPluralTemplates);
6467
6507
  }
6508
+ auto tmp_copyEnabled = map.find("copyEnabled");
6509
+ if (tmp_copyEnabled != map.end()) {
6510
+ fromRawValue(context, tmp_copyEnabled->second, result.copyEnabled);
6511
+ }
6512
+ auto tmp_copyFormatMarkdown = map.find("copyFormatMarkdown");
6513
+ if (tmp_copyFormatMarkdown != map.end()) {
6514
+ fromRawValue(context, tmp_copyFormatMarkdown->second, result.copyFormatMarkdown);
6515
+ }
6516
+ auto tmp_systemItemsNone = map.find("systemItemsNone");
6517
+ if (tmp_systemItemsNone != map.end()) {
6518
+ fromRawValue(context, tmp_systemItemsNone->second, result.systemItemsNone);
6519
+ }
6520
+ auto tmp_codeBlockCopyButton = map.find("codeBlockCopyButton");
6521
+ if (tmp_codeBlockCopyButton != map.end()) {
6522
+ fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
6523
+ }
6524
+ auto tmp_interceptCopy = map.find("interceptCopy");
6525
+ if (tmp_interceptCopy != map.end()) {
6526
+ fromRawValue(context, tmp_interceptCopy->second, result.interceptCopy);
6527
+ }
6468
6528
  }
6469
6529
 
6470
6530
  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
@@ -105,6 +106,7 @@ class EnrichedMarkdown
105
106
  private var onCopyPressCallback: ((String, String) -> Unit)? = null
106
107
  private var contextMenuItemTexts: List<String> = emptyList()
107
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
108
110
  var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES
109
111
  set(value) {
110
112
  if (field == value) return
@@ -309,8 +311,8 @@ class EnrichedMarkdown
309
311
  segmentViews.filterIsInstance<EnrichedMarkdownInternalText>().forEach {
310
312
  it.selectionMenuConfig = config
311
313
  }
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.
314
+ // Block views cache the copy labels and menu options, so re-push them on
315
+ // update (e.g. a language change without a remount) to avoid stale values.
314
316
  pushCopyLabelsToBlockSegments()
315
317
  }
316
318
 
@@ -322,11 +324,13 @@ class EnrichedMarkdown
322
324
  view is TableContainerView -> {
323
325
  view.copyLabel = copyLabel
324
326
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
327
+ view.menuOptions = blockMenuOptions("table")
325
328
  }
326
329
 
327
330
  view is CodeBlockContainerView -> {
328
331
  view.copyLabel = copyLabel
329
332
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
333
+ view.menuOptions = blockMenuOptions("code")
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, blockMenuOptions("math"))
338
345
  }
339
346
  }
340
347
  }
@@ -361,6 +368,22 @@ class EnrichedMarkdown
361
368
  }
362
369
  }
363
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
+
364
387
  private fun forwardContextMenuItemPress(
365
388
  itemText: String,
366
389
  selectedText: String,
@@ -608,6 +631,7 @@ class EnrichedMarkdown
608
631
  onTaskListItemPressCallback = { taskIndex, checked, itemText ->
609
632
  this@EnrichedMarkdown.onTaskListItemPressCallback?.invoke(taskIndex, checked, itemText)
610
633
  }
634
+ onCopyRequest = ::forwardSegmentCopyRequest
611
635
 
612
636
  if (contextMenuItemTexts.isNotEmpty()) {
613
637
  setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress)
@@ -628,6 +652,7 @@ class EnrichedMarkdown
628
652
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
629
653
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
630
654
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
655
+ menuOptions = blockMenuOptions("table")
631
656
  applyTableNode(segment.node)
632
657
  }
633
658
 
@@ -638,6 +663,7 @@ class EnrichedMarkdown
638
663
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
639
664
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
640
665
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
666
+ menuOptions = blockMenuOptions("code")
641
667
  onCopyPress = { code, language ->
642
668
  this@EnrichedMarkdown.onCopyPressCallback?.invoke(code, language)
643
669
  }
@@ -667,6 +693,9 @@ class EnrichedMarkdown
667
693
  resolvedClass
668
694
  .getMethod("setCopyAsMarkdownLabel", String::class.java)
669
695
  .invoke(view, selectionMenuConfig.copyAsMarkdownLabel)
696
+ resolvedClass
697
+ .getMethod("setMenuOptions", BlockMenuOptions::class.java)
698
+ .invoke(view, blockMenuOptions("math"))
670
699
  setMathContextMenuHidden(view)
671
700
  resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
672
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,
@@ -128,6 +146,11 @@ fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
128
146
  copyImageUrlLabel = value.getString("copyImageUrlLabel") ?: "",
129
147
  copyImageUrlsLabel = value.getString("copyImageUrlsLabel") ?: "",
130
148
  copyImageUrlPluralTemplates = parseStringList(value.getArray("copyImageUrlPluralTemplates")),
149
+ copyEnabled = value.getBooleanOrDefault("copyEnabled", true),
150
+ copyFormatMarkdown = value.getBooleanOrDefault("copyFormatMarkdown", false),
151
+ systemItemsNone = value.getBooleanOrDefault("systemItemsNone", false),
152
+ codeBlockCopyButton = value.getBooleanOrDefault("codeBlockCopyButton", true),
153
+ interceptCopy = value.getBooleanOrDefault("interceptCopy", false),
131
154
  )
132
155
  }
133
156
 
@@ -32,6 +32,14 @@ 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,
41
+ // Copy hands the selection to onCopyRequest instead of writing the clipboard.
42
+ val interceptCopy: Boolean = false,
35
43
  )
36
44
 
37
45
  /**
@@ -63,6 +71,9 @@ fun createSelectionActionModeCallback(
63
71
  onActionModeChanged: (mode: ActionMode?) -> Unit = {},
64
72
  onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit =
65
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 = { _, _, _ -> },
66
77
  ): ActionMode.Callback =
67
78
  object : ActionMode.Callback {
68
79
  override fun onCreateActionMode(
@@ -92,19 +103,26 @@ fun createSelectionActionModeCallback(
92
103
  menu.removeGroup(MENU_ITEM_CUSTOM_GROUP)
93
104
 
94
105
  val selectionMenuConfig = getSelectionMenuConfig()
106
+ val hasSelection = textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart
95
107
 
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()) {
108
+ if (selectionMenuConfig.systemItemsNone) {
109
+ // Editor re-adds Select all and the text-assist items before calling
110
+ // us on every prepare, so clear on each pass and put back only Copy.
111
+ // Its click is still intercepted in onActionItemClicked.
112
+ menu.clear()
113
+ if (selectionMenuConfig.copyEnabled && hasSelection) {
114
+ menu.add(Menu.NONE, android.R.id.copy, Menu.NONE, selectionMenuConfig.copyLabel)
115
+ }
116
+ } else if (!selectionMenuConfig.copyEnabled) {
117
+ menu.removeItem(android.R.id.copy)
118
+ } else if (selectionMenuConfig.copyLabel.isNotEmpty()) {
119
+ // The system Copy item is added by the platform; relabel it so it
120
+ // matches the rest of the localized menu (iOS rebuilds the menu so
121
+ // gets this for free; Android only intercepts the click).
100
122
  menu.findItem(android.R.id.copy)?.title = selectionMenuConfig.copyLabel
101
123
  }
102
124
 
103
- if (
104
- selectionMenuConfig.copyAsMarkdown &&
105
- textView.selectionStart >= 0 &&
106
- textView.selectionEnd > textView.selectionStart
107
- ) {
125
+ if (selectionMenuConfig.copyAsMarkdown && hasSelection) {
108
126
  menu.add(
109
127
  Menu.NONE,
110
128
  MENU_ITEM_COPY_MARKDOWN,
@@ -113,7 +131,7 @@ fun createSelectionActionModeCallback(
113
131
  )
114
132
  }
115
133
 
116
- if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) {
134
+ if (hasSelection) {
117
135
  val customItems = getCustomItemTexts()
118
136
  customItems.forEachIndexed { index, text ->
119
137
  menu
@@ -144,7 +162,12 @@ fun createSelectionActionModeCallback(
144
162
 
145
163
  when (itemId) {
146
164
  android.R.id.copy -> {
147
- textView.copyWithHTML()
165
+ val config = getSelectionMenuConfig()
166
+ if (config.interceptCopy) {
167
+ textView.requestCopy(onCopyRequest)
168
+ } else if (!config.copyFormatMarkdown || !textView.copyMarkdownToClipboard()) {
169
+ textView.copyWithHTML()
170
+ }
148
171
  mode?.finish()
149
172
  return true
150
173
  }
@@ -214,10 +237,24 @@ private fun TextView.copyWithHTML() {
214
237
  }
215
238
  }
216
239
 
217
- private fun TextView.copyMarkdownToClipboard() {
218
- val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return
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
+
252
+ /** Returns false when the selection has no Markdown to copy. */
253
+ private fun TextView.copyMarkdownToClipboard(): Boolean {
254
+ val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return false
219
255
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
220
256
  clipboard.setPrimaryClip(ClipData.newPlainText("Markdown", markdown))
257
+ return true
221
258
  }
222
259
 
223
260
  /** Returns remote image URLs (http/https only) from the current selection. */
@@ -0,0 +1,64 @@
1
+ package com.swmansion.enriched.markdown.views
2
+
3
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
4
+
5
+ /** Receives an intercepted block Copy: the block's Markdown and its raw content. */
6
+ typealias BlockCopyRequest = (markdown: String, text: String) -> Unit
7
+
8
+ /**
9
+ * Selection-menu options that table, math and code block views need for their
10
+ * own long-press menus. Every default keeps the legacy behavior, so a block view
11
+ * that has not received options yet behaves exactly as before.
12
+ */
13
+ data class BlockMenuOptions(
14
+ val copyHidden: Boolean = false,
15
+ // The Copy item writes the block's Markdown instead of its raw content.
16
+ val copyWritesMarkdown: Boolean = false,
17
+ val copyAsMarkdownHidden: Boolean = false,
18
+ // Hides the copy button in the code block header.
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,
23
+ ) {
24
+ val hasItems: Boolean get() = !copyHidden || !copyAsMarkdownHidden
25
+
26
+ companion object {
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
+ )
37
+ }
38
+ }
39
+
40
+ /** Adds the Copy / Copy as Markdown items shared by the table, math and code block menus. */
41
+ fun ContextMenuPopup.Builder.blockCopyItems(
42
+ options: BlockMenuOptions,
43
+ copyLabel: String,
44
+ copyAsMarkdownLabel: String,
45
+ // The block's Markdown and raw content, read only for an intercepted Copy.
46
+ markdown: () -> String,
47
+ rawText: () -> String,
48
+ copyRaw: () -> Unit,
49
+ copyMarkdown: () -> Unit,
50
+ ) {
51
+ if (!options.copyHidden) {
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)
60
+ }
61
+ if (!options.copyAsMarkdownHidden) {
62
+ item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel, copyMarkdown)
63
+ }
64
+ }
@@ -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,17 @@ 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(
265
+ menuOptions,
266
+ copyLabel,
267
+ copyAsMarkdownLabel,
268
+ markdown = { CodeBlockNode.fencedMarkdown(code, language, fenceChar) },
269
+ rawText = { code },
270
+ copyRaw = ::copyCode,
271
+ copyMarkdown = ::copyFencedMarkdown,
272
+ )
257
273
  }
258
274
  return true
259
275
  }