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
@@ -0,0 +1,66 @@
1
+ #pragma once
2
+ #import "ENRMUIKit.h"
3
+ #include <TargetConditionals.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ /// Selection-menu options that table, math and code block views need for their
8
+ /// own long-press menus. Every field is phrased so that the zero value keeps the
9
+ /// legacy behavior, so a block view that has not received options yet behaves
10
+ /// exactly as before.
11
+ typedef struct {
12
+ // `selectionMenuConfig.copy.enabled == false`.
13
+ BOOL copyHidden;
14
+ // `selectionMenuConfig.copy.format == 'markdown'`: the Copy item writes the
15
+ // block's Markdown instead of its raw content.
16
+ BOOL copyWritesMarkdown;
17
+ // `selectionMenuConfig.copyAsMarkdown.enabled == false`.
18
+ BOOL copyAsMarkdownHidden;
19
+ // `selectionMenuConfig.codeBlockCopyButton.enabled == false`.
20
+ BOOL copyButtonHidden;
21
+ // JS `onCopy` is set: the Copy item hands the block's content to the view's
22
+ // `onCopyRequest` instead of writing the pasteboard.
23
+ BOOL interceptCopy;
24
+ } ENRMBlockMenuOptions;
25
+
26
+ /// Receives an intercepted block Copy: the block's Markdown and its raw content.
27
+ typedef void (^ENRMBlockCopyRequestBlock)(NSString *markdown, NSString *text);
28
+
29
+ #if !TARGET_OS_OSX
30
+ /// Builds the long-press menu shared by the table, math and code block views.
31
+ /// Returns nil when no item is left — nil is how UIKit suppresses a context
32
+ /// menu, whereas an empty menu would still lift the view. `requestCopy` replaces
33
+ /// the Copy item's handler when `options.interceptCopy` is set.
34
+ static inline UIContextMenuConfiguration *_Nullable ENRMBlockContextMenuConfiguration(
35
+ ENRMBlockMenuOptions options, NSString *_Nullable copyLabel, NSString *_Nullable copyAsMarkdownLabel,
36
+ void (^copyRaw)(void), void (^copyMarkdown)(void), void (^_Nullable requestCopy)(void))
37
+ {
38
+ if (options.copyHidden && options.copyAsMarkdownHidden) {
39
+ return nil;
40
+ }
41
+ void (^copyHandler)(void) = (options.interceptCopy && requestCopy) ? requestCopy
42
+ : options.copyWritesMarkdown ? copyMarkdown
43
+ : copyRaw;
44
+ return [UIContextMenuConfiguration
45
+ configurationWithIdentifier:nil
46
+ previewProvider:nil
47
+ actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
48
+ NSMutableArray<UIMenuElement *> *children = [NSMutableArray arrayWithCapacity:2];
49
+ if (!options.copyHidden) {
50
+ [children addObject:[UIAction actionWithTitle:copyLabel ?: @""
51
+ image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
52
+ identifier:nil
53
+ handler:^(__kindof UIAction *action) { copyHandler(); }]];
54
+ }
55
+ if (!options.copyAsMarkdownHidden) {
56
+ [children addObject:[UIAction actionWithTitle:copyAsMarkdownLabel ?: @""
57
+ image:[RCTUIImage systemImageNamed:@"doc.text"]
58
+ identifier:nil
59
+ handler:^(__kindof UIAction *action) { copyMarkdown(); }]];
60
+ }
61
+ return [UIMenu menuWithTitle:@"" children:children];
62
+ }];
63
+ }
64
+ #endif
65
+
66
+ NS_ASSUME_NONNULL_END
@@ -9,6 +9,15 @@ NS_ASSUME_NONNULL_BEGIN
9
9
  typedef struct {
10
10
  BOOL copyAsMarkdown;
11
11
  BOOL copyImageURL;
12
+ BOOL copyEnabled;
13
+ // Copy writes Markdown as plain text instead of rich text.
14
+ BOOL copyFormatMarkdown;
15
+ // Drop every system-provided item from the text selection menu.
16
+ BOOL systemItemsNone;
17
+ BOOL codeBlockCopyButton;
18
+ // Copy hands the selection to the owner (JS `onCopy`) instead of writing the
19
+ // pasteboard.
20
+ BOOL interceptCopy;
12
21
  // The owner must keep these strings alive for the duration of the call (the
13
22
  // view holds them in strong ivars).
14
23
  __unsafe_unretained NSString *_Nullable copyLabel;
@@ -18,6 +27,10 @@ typedef struct {
18
27
  __unsafe_unretained NSArray<NSString *> *_Nullable copyImageUrlPluralTemplates;
19
28
  } ENRMSelectionMenuConfig;
20
29
 
30
+ /// Receives an intercepted Copy. `fullSelection` is whether the range spans the
31
+ /// whole attributed text, in which case `markdown` is the cached markdown.
32
+ typedef void (^ENRMCopyRequestHandler)(NSString *markdown, NSString *text, BOOL fullSelection);
33
+
21
34
  /// Resolves the "Copy Image URL(s)" menu title for a given image count.
22
35
  /// Uses the precomputed plural templates (indexed by count, 0..100) when present;
23
36
  /// counts > 100 use the "other" form (copyImageUrlsLabel). Otherwise the
@@ -36,6 +49,13 @@ static inline NSString *ENRMResolveImageURLsTitle(ENRMSelectionMenuConfig config
36
49
  return [titleTemplate stringByReplacingOccurrencesOfString:@"{count}" withString:[@(count) stringValue]];
37
50
  }
38
51
 
52
+ /// Defaults matching the JS-side defaults, used until the first props update.
53
+ static inline ENRMSelectionMenuConfig ENRMDefaultSelectionMenuConfig(void)
54
+ {
55
+ return (ENRMSelectionMenuConfig){
56
+ .copyAsMarkdown = YES, .copyImageURL = YES, .copyEnabled = YES, .codeBlockCopyButton = YES};
57
+ }
58
+
39
59
  #ifdef __cplusplus
40
60
 
41
61
  /// Strong owners for the label strings in ENRMSelectionMenuConfig.
@@ -63,13 +83,18 @@ template <typename T> static inline ENRMSelectionMenuLabels ENRMParseSelectionMe
63
83
  };
64
84
  }
65
85
 
66
- /// Builds an ENRMSelectionMenuConfig from pre-parsed labels and boolean flags.
67
- static inline ENRMSelectionMenuConfig ENRMBuildSelectionMenuConfig(const ENRMSelectionMenuLabels &labels,
68
- bool copyAsMarkdown, bool copyImageUrl)
86
+ /// Builds an ENRMSelectionMenuConfig from pre-parsed labels and the codegen struct's flags.
87
+ template <typename T>
88
+ static inline ENRMSelectionMenuConfig ENRMBuildSelectionMenuConfig(const ENRMSelectionMenuLabels &labels, const T &src)
69
89
  {
70
90
  return (ENRMSelectionMenuConfig){
71
- .copyAsMarkdown = copyAsMarkdown,
72
- .copyImageURL = copyImageUrl,
91
+ .copyAsMarkdown = src.copyAsMarkdown,
92
+ .copyImageURL = src.copyImageUrl,
93
+ .copyEnabled = src.copyEnabled,
94
+ .copyFormatMarkdown = src.copyFormatMarkdown,
95
+ .systemItemsNone = src.systemItemsNone,
96
+ .codeBlockCopyButton = src.codeBlockCopyButton,
97
+ .interceptCopy = src.interceptCopy,
73
98
  .copyLabel = labels.copyLabel,
74
99
  .copyAsMarkdownLabel = labels.copyAsMarkdownLabel,
75
100
  .copyImageUrlLabel = labels.copyImageUrlLabel,
@@ -90,7 +115,8 @@ extern "C" {
90
115
  UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown,
91
116
  StyleConfig *styleConfig, NSArray<UIMenuElement *> *suggestedActions,
92
117
  NSArray<UIAction *> *_Nullable customActions,
93
- ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0));
118
+ ENRMSelectionMenuConfig selectionMenuConfig,
119
+ ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0));
94
120
  #else
95
121
  NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range,
96
122
  NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig,
@@ -10,14 +10,34 @@ static NSString *const kActionIdentifierCopy = @"com.swmansion.enriched.markdown
10
10
  static NSString *const kActionIdentifierCopyMarkdown = @"com.swmansion.enriched.markdown.copyMarkdown";
11
11
  static NSString *const kActionIdentifierCopyImageURL = @"com.swmansion.enriched.markdown.copyImageURL";
12
12
 
13
- static UIAction *createCopyAction(NSAttributedString *selectedText, NSString *markdown, StyleConfig *styleConfig,
14
- NSString *copyLabel)
13
+ static UIAction *_Nullable createCopyAction(NSAttributedString *selectedText, NSString *markdown, BOOL fullSelection,
14
+ StyleConfig *styleConfig, ENRMSelectionMenuConfig selectionMenuConfig,
15
+ ENRMCopyRequestHandler _Nullable onCopyRequest)
15
16
  {
16
- return [UIAction actionWithTitle:copyLabel
17
+ if (!selectionMenuConfig.copyEnabled)
18
+ return nil;
19
+
20
+ if (selectionMenuConfig.interceptCopy && onCopyRequest) {
21
+ NSString *text = selectedText.string;
22
+ NSString *copied = markdown.length > 0 ? markdown : text;
23
+ return [UIAction actionWithTitle:selectionMenuConfig.copyLabel
24
+ image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
25
+ identifier:kActionIdentifierCopy
26
+ handler:^(__kindof UIAction *action) { onCopyRequest(copied, text, fullSelection); }];
27
+ }
28
+
29
+ BOOL writesMarkdown = selectionMenuConfig.copyFormatMarkdown;
30
+ return [UIAction actionWithTitle:selectionMenuConfig.copyLabel
17
31
  image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
18
32
  identifier:kActionIdentifierCopy
19
33
  handler:^(__kindof UIAction *action) {
20
- copyAttributedStringToPasteboard(selectedText, markdown, styleConfig);
34
+ if (!writesMarkdown) {
35
+ copyAttributedStringToPasteboard(selectedText, markdown, styleConfig);
36
+ } else {
37
+ // Plain text only: with HTML/RTF alongside, rich-paste targets
38
+ // would take those and drop the Markdown.
39
+ copyStringToPasteboard(markdown.length > 0 ? markdown : selectedText.string);
40
+ }
21
41
  }];
22
42
  }
23
43
 
@@ -48,10 +68,11 @@ static UIAction *_Nullable createCopyImageURLAction(NSArray<NSString *> *imageUR
48
68
  }
49
69
 
50
70
  /// Rebuilds the standard-edit menu with the system Copy swapped for the enhanced
51
- /// copy action. All other system items are kept — dropping them would remove
52
- /// Select All, which on iOS 16+ is the only way to start a selection from the
53
- /// long-press menu of a non-editable text view.
54
- static UIMenu *createEnhancedStandardEditMenu(UIMenu *originalMenu, UIAction *copyAction)
71
+ /// copy action, or dropped when `copyAction` is nil (Copy disabled). All other
72
+ /// system items are kept — dropping them would remove Select All, which on
73
+ /// iOS 16+ is the only way to start a selection from the long-press menu of a
74
+ /// non-editable text view.
75
+ static UIMenu *createEnhancedStandardEditMenu(UIMenu *originalMenu, UIAction *_Nullable copyAction)
55
76
  {
56
77
  NSMutableArray<UIMenuElement *> *children = [NSMutableArray arrayWithCapacity:originalMenu.children.count];
57
78
  BOOL replacedCopy = NO;
@@ -59,14 +80,16 @@ static UIMenu *createEnhancedStandardEditMenu(UIMenu *originalMenu, UIAction *co
59
80
  for (UIMenuElement *element in originalMenu.children) {
60
81
  if (!replacedCopy && [element isKindOfClass:[UICommand class]] &&
61
82
  ((UICommand *)element).action == @selector(copy:)) {
62
- [children addObject:copyAction];
83
+ if (copyAction) {
84
+ [children addObject:copyAction];
85
+ }
63
86
  replacedCopy = YES;
64
87
  continue;
65
88
  }
66
89
  [children addObject:element];
67
90
  }
68
91
 
69
- if (!replacedCopy) {
92
+ if (!replacedCopy && copyAction) {
70
93
  [children insertObject:copyAction atIndex:0];
71
94
  }
72
95
 
@@ -95,13 +118,17 @@ static void insertOptionalAction(NSMutableArray<UIMenuElement *> *array, UIActio
95
118
  UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown,
96
119
  StyleConfig *styleConfig, NSArray<UIMenuElement *> *suggestedActions,
97
120
  NSArray<UIAction *> *_Nullable customActions,
98
- ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0))
121
+ ENRMSelectionMenuConfig selectionMenuConfig,
122
+ ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0))
99
123
  {
100
124
  NSAttributedString *selectedText = [attributedText attributedSubstringFromRange:range];
101
125
  NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown);
102
126
  NSArray<NSString *> *imageURLs = imageURLsInRange(attributedText, range);
127
+ // Same threshold markdownForRange uses to return the cached markdown.
128
+ BOOL fullSelection = range.location == 0 && range.length >= attributedText.length - 1;
103
129
 
104
- UIAction *copyAction = createCopyAction(selectedText, markdown, styleConfig, selectionMenuConfig.copyLabel);
130
+ UIAction *copyAction =
131
+ createCopyAction(selectedText, markdown, fullSelection, styleConfig, selectionMenuConfig, onCopyRequest);
105
132
  UIAction *copyMarkdownAction = selectionMenuConfig.copyAsMarkdown
106
133
  ? createCopyMarkdownAction(markdown, selectionMenuConfig.copyAsMarkdownLabel)
107
134
  : nil;
@@ -109,6 +136,16 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
109
136
  selectionMenuConfig.copyImageURL ? createCopyImageURLAction(imageURLs, selectionMenuConfig) : nil;
110
137
 
111
138
  NSMutableArray<UIMenuElement *> *result = [NSMutableArray array];
139
+
140
+ if (selectionMenuConfig.systemItemsNone) {
141
+ // Skip suggestedActions entirely. An empty result still yields an (empty)
142
+ // menu: returning nil would make UIKit present its default menu.
143
+ addOptionalAction(result, copyAction);
144
+ addOptionalAction(result, copyMarkdownAction);
145
+ addOptionalAction(result, copyImageURLAction);
146
+ return [UIMenu menuWithChildren:[(customActions ?: @[]) arrayByAddingObjectsFromArray:result]];
147
+ }
148
+
112
149
  BOOL foundStandardEditMenu = NO;
113
150
 
114
151
  for (UIMenuElement *element in suggestedActions) {
@@ -128,8 +165,8 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
128
165
  }
129
166
 
130
167
  if (!foundStandardEditMenu) {
131
- [result insertObject:copyAction atIndex:0];
132
- insertOptionalAction(result, copyMarkdownAction, 1);
168
+ insertOptionalAction(result, copyMarkdownAction, 0);
169
+ insertOptionalAction(result, copyAction, 0);
133
170
  addOptionalAction(result, copyImageURLAction);
134
171
  }
135
172
 
@@ -1,4 +1,5 @@
1
1
  #pragma once
2
+ #import "ENRMBlockMenu.h"
2
3
  #import "ENRMUIKit.h"
3
4
  #import "StyleConfig.h"
4
5
 
@@ -33,12 +34,22 @@ typedef void (^ENRMCodeBlockCopyBlock)(NSString *code, NSString *language);
33
34
 
34
35
  /// Suppresses the long-press copy menu on this block. Mirrors the
35
36
  /// `contextMenuHidden` prop; the header copy button is a visible control, not a
36
- /// context menu, and stays.
37
+ /// context menu, and stays (hide it with `menuOptions.copyButtonHidden`).
37
38
  @property (nonatomic, assign) BOOL contextMenuHidden;
38
39
 
40
+ /// Copy / Copy as Markdown visibility, Copy format and the header copy button
41
+ /// from `selectionMenuConfig`. `copyHidden` also drops the VoiceOver copy action;
42
+ /// the header button and VoiceOver action always copy the raw code.
43
+ @property (nonatomic, assign) ENRMBlockMenuOptions menuOptions;
44
+
45
+ /// Set by the host; receives the Copy item's content when
46
+ /// `menuOptions.interceptCopy` is set.
47
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
48
+
39
49
  // Fired when the code is copied (header button, context-menu Copy, or the
40
50
  // VoiceOver copy action); set by the host to bridge up to the JS onCopyPress
41
- // event. Not fired for "Copy as Markdown".
51
+ // event. Not fired for "Copy as Markdown", nor for the context-menu Copy when
52
+ // `menuOptions.copyWritesMarkdown` is set.
42
53
  @property (nonatomic, copy, nullable) ENRMCodeBlockCopyBlock onCopyPress;
43
54
 
44
55
  @end
@@ -175,6 +175,13 @@ static BOOL ENRMColorIsDark(RCTUIColor *color)
175
175
  _copyButton.accessibilityLabel = _copyLabel;
176
176
  }
177
177
 
178
+ - (void)setMenuOptions:(ENRMBlockMenuOptions)menuOptions
179
+ {
180
+ _menuOptions = menuOptions;
181
+ // Hiding the button leaves the header (and its height) untouched.
182
+ _copyButton.hidden = menuOptions.copyButtonHidden;
183
+ }
184
+
178
185
  - (void)setPending:(BOOL)pending
179
186
  {
180
187
  if (_pending == pending) {
@@ -490,24 +497,17 @@ static BOOL ENRMColorIsDark(RCTUIColor *color)
490
497
  if (_pending || self.contextMenuHidden) {
491
498
  return nil;
492
499
  }
493
- return [UIContextMenuConfiguration
494
- configurationWithIdentifier:nil
495
- previewProvider:nil
496
- actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
497
- UIAction *copyCode =
498
- [UIAction actionWithTitle:self.copyLabel
499
- image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
500
- identifier:nil
501
- handler:^(__kindof UIAction *action) { [self copyCodeToPasteboard]; }];
502
-
503
- UIAction *copyMarkdown =
504
- [UIAction actionWithTitle:self.copyAsMarkdownLabel
505
- image:[RCTUIImage systemImageNamed:@"doc.text"]
506
- identifier:nil
507
- handler:^(__kindof UIAction *action) { [self copyMarkdownToPasteboard]; }];
508
-
509
- return [UIMenu menuWithTitle:@"" children:@[ copyCode, copyMarkdown ]];
510
- }];
500
+ __weak __typeof(self) weakSelf = self;
501
+ return ENRMBlockContextMenuConfiguration(
502
+ self.menuOptions, self.copyLabel, self.copyAsMarkdownLabel, ^{ [weakSelf copyCodeToPasteboard]; },
503
+ ^{ [weakSelf copyMarkdownToPasteboard]; }, ^{ [weakSelf requestCopy]; });
504
+ }
505
+
506
+ - (void)requestCopy
507
+ {
508
+ if (self.onCopyRequest) {
509
+ self.onCopyRequest([self fencedMarkdown], _cachedCode ?: @"");
510
+ }
511
511
  }
512
512
  #endif
513
513
 
@@ -546,7 +546,7 @@ static BOOL ENRMColorIsDark(RCTUIColor *color)
546
546
  // button subview from VoiceOver; expose the copy action explicitly instead.
547
547
  - (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
548
548
  {
549
- if (_pending || self.copyLabel.length == 0) {
549
+ if (_pending || self.menuOptions.copyHidden || self.copyLabel.length == 0) {
550
550
  return @[];
551
551
  }
552
552
  UIAccessibilityCustomAction *copyAction =
@@ -1,4 +1,5 @@
1
1
  #pragma once
2
+ #import "ENRMBlockMenu.h"
2
3
  #import "ENRMUIKit.h"
3
4
  #import "StyleConfig.h"
4
5
 
@@ -35,6 +36,13 @@ NS_ASSUME_NONNULL_BEGIN
35
36
  /// `contextMenuHidden` prop; selection and link handling are unaffected.
36
37
  @property (nonatomic, assign) BOOL contextMenuHidden;
37
38
 
39
+ /// Copy / Copy as Markdown visibility and Copy format from `selectionMenuConfig`.
40
+ @property (nonatomic, assign) ENRMBlockMenuOptions menuOptions;
41
+
42
+ /// Set by the host; receives the Copy item's content when
43
+ /// `menuOptions.interceptCopy` is set.
44
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
45
+
38
46
  @end
39
47
 
40
48
  NS_ASSUME_NONNULL_END
@@ -130,25 +130,17 @@
130
130
  if (self.contextMenuHidden) {
131
131
  return nil;
132
132
  }
133
+ __weak __typeof(self) weakSelf = self;
134
+ return ENRMBlockContextMenuConfiguration(
135
+ self.menuOptions, self.copyLabel, self.copyAsMarkdownLabel, ^{ [weakSelf copyLatexToPasteboard]; },
136
+ ^{ [weakSelf copyMarkdownToPasteboard]; }, ^{ [weakSelf requestCopy]; });
137
+ }
133
138
 
134
- return [UIContextMenuConfiguration
135
- configurationWithIdentifier:nil
136
- previewProvider:nil
137
- actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
138
- UIAction *copyPlainText =
139
- [UIAction actionWithTitle:self.copyLabel
140
- image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
141
- identifier:nil
142
- handler:^(__kindof UIAction *action) { [self copyLatexToPasteboard]; }];
143
-
144
- UIAction *copyMarkdown =
145
- [UIAction actionWithTitle:self.copyAsMarkdownLabel
146
- image:[RCTUIImage systemImageNamed:@"doc.text"]
147
- identifier:nil
148
- handler:^(__kindof UIAction *action) { [self copyMarkdownToPasteboard]; }];
149
-
150
- return [UIMenu menuWithTitle:@"" children:@[ copyPlainText, copyMarkdown ]];
151
- }];
139
+ - (void)requestCopy
140
+ {
141
+ if (self.onCopyRequest) {
142
+ self.onCopyRequest([NSString stringWithFormat:@"$$\n%@\n$$", _cachedLatex ?: @""], _cachedLatex ?: @"");
143
+ }
152
144
  }
153
145
  #endif
154
146
 
@@ -1,4 +1,5 @@
1
1
  #pragma once
2
+ #import "ENRMBlockMenu.h"
2
3
  #import "ENRMUIKit.h"
3
4
  #import "ParagraphStyleUtils.h"
4
5
  #import "StyleConfig.h"
@@ -56,6 +57,13 @@ typedef void (^TableLinkPressBlock)(NSString *url);
56
57
  /// `contextMenuHidden` prop; selection and link handling are unaffected.
57
58
  @property (nonatomic, assign) BOOL contextMenuHidden;
58
59
 
60
+ /// Copy / Copy as Markdown visibility and Copy format from `selectionMenuConfig`.
61
+ @property (nonatomic, assign) ENRMBlockMenuOptions menuOptions;
62
+
63
+ /// Set by the host; receives the Copy item's content when
64
+ /// `menuOptions.interceptCopy` is set.
65
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
66
+
59
67
  @property (nonatomic, readonly) NSUInteger rowCount;
60
68
 
61
69
  - (void)animateNewRowsFromPreviousCount:(NSUInteger)previousRowCount duration:(NSTimeInterval)duration;
@@ -467,25 +467,17 @@ static void ENRMTableComputeLayout(NSArray<NSArray<TableCellData *> *> *rows, NS
467
467
  if (self.contextMenuHidden) {
468
468
  return nil;
469
469
  }
470
+ __weak __typeof(self) weakSelf = self;
471
+ return ENRMBlockContextMenuConfiguration(
472
+ self.menuOptions, self.copyLabel, self.copyAsMarkdownLabel, ^{ [weakSelf copyTableToPasteboard]; },
473
+ ^{ [weakSelf copyMarkdownToPasteboard]; }, ^{ [weakSelf requestCopy]; });
474
+ }
470
475
 
471
- return [UIContextMenuConfiguration
472
- configurationWithIdentifier:nil
473
- previewProvider:nil
474
- actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
475
- UIAction *copyMarkdown =
476
- [UIAction actionWithTitle:self.copyAsMarkdownLabel
477
- image:[RCTUIImage systemImageNamed:@"doc.text"]
478
- identifier:nil
479
- handler:^(__kindof UIAction *action) { [self copyMarkdownToPasteboard]; }];
480
-
481
- UIAction *copyPlainText =
482
- [UIAction actionWithTitle:self.copyLabel
483
- image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
484
- identifier:nil
485
- handler:^(__kindof UIAction *action) { [self copyTableToPasteboard]; }];
486
-
487
- return [UIMenu menuWithTitle:@"" children:@[ copyPlainText, copyMarkdown ]];
488
- }];
476
+ - (void)requestCopy
477
+ {
478
+ if (self.onCopyRequest) {
479
+ self.onCopyRequest(_cachedMarkdown ?: @"", [self buildPlainTextFromRows] ?: @"");
480
+ }
489
481
  }
490
482
  #endif // !TARGET_OS_OSX
491
483
 
@@ -256,6 +256,14 @@ export interface CopyPressEvent {
256
256
  language: string;
257
257
  }
258
258
 
259
+ export interface CopyRequestEvent {
260
+ // 'selection' | 'math' | 'table' | 'code'
261
+ source: string;
262
+ markdown: string;
263
+ text: string;
264
+ whole: boolean;
265
+ }
266
+
259
267
  export interface ContextMenuItemConfig {
260
268
  text: string;
261
269
  icon?: string;
@@ -276,6 +284,16 @@ export interface SelectionMenuConfig {
276
284
  // Precomputed plural templates indexed by image count (0..100). Empty when no
277
285
  // pluralLabels are set; native then uses copyImageUrlLabel/copyImageUrlsLabel.
278
286
  copyImageUrlPluralTemplates: ReadonlyArray<string>;
287
+ // false removes the Copy item from the text and block menus.
288
+ copyEnabled: boolean;
289
+ // true: Copy writes Markdown (plain text) instead of rich text / raw block content.
290
+ copyFormatMarkdown: boolean;
291
+ // true: the text selection menu drops every system-provided item.
292
+ systemItemsNone: boolean;
293
+ // false hides the copy button in the code block header.
294
+ codeBlockCopyButton: boolean;
295
+ // true: the Copy item emits onCopyRequest instead of writing the clipboard.
296
+ interceptCopy: boolean;
279
297
  }
280
298
 
281
299
  export interface OnContextMenuItemPressEvent {
@@ -402,6 +420,12 @@ export interface NativeProps extends ViewProps {
402
420
  * action. Receives the copied code and its language.
403
421
  */
404
422
  onCopyPress?: CodegenTypes.BubblingEventHandler<CopyPressEvent>;
423
+ /**
424
+ * Fired instead of writing the clipboard when the Copy item of the text
425
+ * selection menu or a table / math / code block menu is pressed while
426
+ * `selectionMenuConfig.interceptCopy` is set.
427
+ */
428
+ onCopyRequest?: CodegenTypes.BubblingEventHandler<CopyRequestEvent>;
405
429
  /**
406
430
  * Controls whether the system link preview is shown on long press (iOS only).
407
431
  *
@@ -257,6 +257,14 @@ export interface CopyPressEvent {
257
257
  language: string;
258
258
  }
259
259
 
260
+ export interface CopyRequestEvent {
261
+ // 'selection' | 'math' | 'table' | 'code'
262
+ source: string;
263
+ markdown: string;
264
+ text: string;
265
+ whole: boolean;
266
+ }
267
+
260
268
  export interface ContextMenuItemConfig {
261
269
  text: string;
262
270
  icon?: string;
@@ -277,6 +285,16 @@ export interface SelectionMenuConfig {
277
285
  // Precomputed plural templates indexed by image count (0..100). Empty when no
278
286
  // pluralLabels are set; native then uses copyImageUrlLabel/copyImageUrlsLabel.
279
287
  copyImageUrlPluralTemplates: ReadonlyArray<string>;
288
+ // false removes the Copy item from the text and block menus.
289
+ copyEnabled: boolean;
290
+ // true: Copy writes Markdown (plain text) instead of rich text / raw block content.
291
+ copyFormatMarkdown: boolean;
292
+ // true: the text selection menu drops every system-provided item.
293
+ systemItemsNone: boolean;
294
+ // false hides the copy button in the code block header.
295
+ codeBlockCopyButton: boolean;
296
+ // true: the Copy item emits onCopyRequest instead of writing the clipboard.
297
+ interceptCopy: boolean;
280
298
  }
281
299
 
282
300
  export interface OnContextMenuItemPressEvent {
@@ -403,6 +421,12 @@ export interface NativeProps extends ViewProps {
403
421
  * action. Receives the copied code and its language.
404
422
  */
405
423
  onCopyPress?: CodegenTypes.BubblingEventHandler<CopyPressEvent>;
424
+ /**
425
+ * Fired instead of writing the clipboard when the Copy item of the text
426
+ * selection menu or a table / math / code block menu is pressed while
427
+ * `selectionMenuConfig.interceptCopy` is set.
428
+ */
429
+ onCopyRequest?: CodegenTypes.BubblingEventHandler<CopyRequestEvent>;
406
430
  /**
407
431
  * Controls whether the system link preview is shown on long press (iOS only).
408
432
  *
@@ -1 +1 @@
1
- {"version":3,"names":["default","EnrichedMarkdownText","DEFAULT_ACCESSIBILITY_LABELS","resolveAccessibilityLabels","EnrichedMarkdownTextInput"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,OAAO,IAAIC,oBAAoB,QAAQ,kCAA+B;AAqB/E,SACEC,4BAA4B,EAC5BC,0BAA0B,QACrB,iCAA8B;AAErC,SAASC,yBAAyB,QAAQ,gCAA6B","ignoreList":[]}
1
+ {"version":3,"names":["default","EnrichedMarkdownText","DEFAULT_ACCESSIBILITY_LABELS","resolveAccessibilityLabels","EnrichedMarkdownTextInput"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,OAAO,IAAIC,oBAAoB,QAAQ,kCAA+B;AAsB/E,SACEC,4BAA4B,EAC5BC,0BAA0B,QACrB,iCAA8B;AAErC,SAASC,yBAAyB,QAAQ,gCAA6B","ignoreList":[]}
@@ -73,6 +73,7 @@ export const EnrichedMarkdownText = ({
73
73
  onTaskListItemPress,
74
74
  enableTaskListItemToggle = true,
75
75
  onCopyPress,
76
+ onCopy,
76
77
  enableLinkPreview,
77
78
  selectable = true,
78
79
  md4cFlags = defaultMd4cFlags,
@@ -183,6 +184,24 @@ export const EnrichedMarkdownText = ({
183
184
  language
184
185
  });
185
186
  }, [onCopyPress]);
187
+ const handleCopyRequest = useCallback(
188
+ // Codegen types `source` as a plain string; native only sends the four
189
+ // values of CopyEvent['source'].
190
+ e => {
191
+ const {
192
+ source,
193
+ markdown: copied,
194
+ text,
195
+ whole
196
+ } = e.nativeEvent;
197
+ onCopy?.({
198
+ source: source,
199
+ markdown: copied,
200
+ text,
201
+ whole
202
+ });
203
+ }, [onCopy]);
204
+ const interceptCopy = onCopy != null;
186
205
  const tableMode = streamingConfig?.tableMode ?? 'progressive';
187
206
  const codeBlockMode = streamingConfig?.codeBlockMode ?? 'progressive';
188
207
  const normalizedStreamingConfig = useMemo(() => ({
@@ -194,6 +213,8 @@ export const EnrichedMarkdownText = ({
194
213
  // `as unknown` cast; the public type only exposes the object shape.
195
214
  const config = selectionMenuConfig;
196
215
  const copy = normalizeMenuItem(config?.copy, true, DEFAULT_COPY_LABEL);
216
+ const copyFormat = config?.copy?.format;
217
+ const codeBlockCopyButton = normalizeMenuItem(config?.codeBlockCopyButton, true, '');
197
218
  const copyAsMarkdown = normalizeLegacyBooleanMenuItem(config?.copyAsMarkdown, 'selectionMenuConfig', 'copyAsMarkdown', true, DEFAULT_COPY_AS_MARKDOWN_LABEL);
198
219
  const copyImageUrl = normalizeLegacyBooleanMenuItem(config?.copyImageUrl, 'selectionMenuConfig', 'copyImageUrl', true, DEFAULT_COPY_IMAGE_URL_LABEL);
199
220
  const pluralLabels = config?.copyImageUrl?.pluralLabels;
@@ -204,9 +225,14 @@ export const EnrichedMarkdownText = ({
204
225
  copyAsMarkdownLabel: copyAsMarkdown.label,
205
226
  copyImageUrlLabel: copyImageUrl.label,
206
227
  copyImageUrlsLabel: pluralLabels?.other ?? DEFAULT_COPY_IMAGE_URLS_LABEL,
207
- copyImageUrlPluralTemplates: buildPluralTemplates(pluralLabels, copyImageUrl.label)
228
+ copyImageUrlPluralTemplates: buildPluralTemplates(pluralLabels, copyImageUrl.label),
229
+ copyEnabled: copy.enabled,
230
+ copyFormatMarkdown: copyFormat === 'markdown',
231
+ systemItemsNone: config?.systemItems === 'none',
232
+ codeBlockCopyButton: codeBlockCopyButton.enabled,
233
+ interceptCopy
208
234
  };
209
- }, [selectionMenuConfig]);
235
+ }, [selectionMenuConfig, interceptCopy]);
210
236
  const resolvedAccessibilityLabels = useMemo(() => resolveAccessibilityLabels(accessibilityLabels), [accessibilityLabels]);
211
237
  const sharedProps = {
212
238
  markdown,
@@ -216,6 +242,7 @@ export const EnrichedMarkdownText = ({
216
242
  onTaskListItemPress: handleTaskListItemPress,
217
243
  enableTaskListItemToggle,
218
244
  onCopyPress: handleCopyPress,
245
+ onCopyRequest: handleCopyRequest,
219
246
  enableLinkPreview: onLinkLongPress == null && (enableLinkPreview ?? true),
220
247
  selectable,
221
248
  md4cFlags: normalizedMd4cFlags,