diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt index 96dff18d..b08af478 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt @@ -22,6 +22,7 @@ import com.swmansion.enriched.markdown.utils.common.StreamingMarkdownFilter import com.swmansion.enriched.markdown.utils.common.TableStreamingMode import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator +import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors import com.swmansion.enriched.markdown.views.BlockSegmentView import com.swmansion.enriched.markdown.views.TableContainerView @@ -78,6 +79,7 @@ class EnrichedMarkdown private var selectable: Boolean = true private var selectionColor: Int? = null private var selectionHandleColor: Int? = null + private var selectionMenuConfig = SelectionMenuConfig() private var onLinkPressCallback: ((String) -> Unit)? = null private var onLinkLongPressCallback: ((String) -> Unit)? = null @@ -207,6 +209,14 @@ class EnrichedMarkdown } } + fun setSelectionMenuConfig(config: SelectionMenuConfig) { + if (selectionMenuConfig == config) return + selectionMenuConfig = config + segmentViews.filterIsInstance().forEach { + it.selectionMenuConfig = config + } + } + private fun forwardContextMenuItemPress( itemText: String, selectedText: String, @@ -405,6 +415,7 @@ class EnrichedMarkdown private fun createTextView(segment: RenderedSegment.Text) = EnrichedMarkdownInternalText(context).apply { spoilerOverlay = this@EnrichedMarkdown.spoilerOverlay + selectionMenuConfig = this@EnrichedMarkdown.selectionMenuConfig setIsSelectable(selectable) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && segment.needsJustify) { justificationMode = android.text.Layout.JUSTIFICATION_MODE_INTER_WORD diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt index 75516569..98fb639b 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt @@ -12,6 +12,7 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.spoiler.SpoilerOverlayDrawer import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod +import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.view.applySelectableState import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap @@ -45,6 +46,7 @@ class EnrichedMarkdownInternalText var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES private var contextMenuItemTexts: List = emptyList() private var onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null + var selectionMenuConfig: SelectionMenuConfig = SelectionMenuConfig() init { setupAsMarkdownTextView() @@ -52,6 +54,7 @@ class EnrichedMarkdownInternalText createSelectionActionModeCallback( this, getCustomItemTexts = { contextMenuItemTexts }, + getSelectionMenuConfig = { selectionMenuConfig }, onCustomItemPress = { itemText, selectedText, start, end -> onContextMenuItemPress?.invoke(itemText, selectedText, start, end) }, diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt index fdb71143..f12c19e3 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt @@ -20,6 +20,7 @@ import com.swmansion.enriched.markdown.utils.common.emitTaskListItemPress import com.swmansion.enriched.markdown.utils.common.markdownEventTypeConstants import com.swmansion.enriched.markdown.utils.common.parseContextMenuItems import com.swmansion.enriched.markdown.utils.common.parseMd4cFlags +import com.swmansion.enriched.markdown.utils.common.parseSelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.interaction.TaskListToggleUtils @ReactModule(name = EnrichedMarkdownManager.NAME) @@ -186,6 +187,15 @@ class EnrichedMarkdownManager : view.setContextMenuItems(parseContextMenuItems(value)) } + @ReactProp(name = "selectionMenuConfig") + override fun setSelectionMenuConfig( + view: EnrichedMarkdown?, + value: ReadableMap?, + ) { + if (view == null) return + view.setSelectionMenuConfig(parseSelectionMenuConfig(value)) + } + override fun setPadding( view: EnrichedMarkdown, left: Int, diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt index fb0910c6..62620d6d 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt @@ -22,6 +22,7 @@ import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod +import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.view.applySelectableState import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap @@ -84,6 +85,7 @@ class EnrichedMarkdownText private var selectionColor: Int? = null private var selectionHandleColor: Int? = null + private var selectionMenuConfig = SelectionMenuConfig() init { setupAsMarkdownTextView() @@ -91,6 +93,7 @@ class EnrichedMarkdownText createSelectionActionModeCallback( this, getCustomItemTexts = { contextMenuItemTexts }, + getSelectionMenuConfig = { selectionMenuConfig }, onCustomItemPress = { itemText, selectedText, start, end -> onContextMenuItemPressCallback?.invoke(itemText, selectedText, start, end) }, @@ -260,6 +263,11 @@ class EnrichedMarkdownText contextMenuItemTexts = items } + fun setSelectionMenuConfig(config: SelectionMenuConfig) { + if (selectionMenuConfig == config) return + selectionMenuConfig = config + } + fun setIsSelectable(selectable: Boolean) { applySelectableState(selectable) } diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt index 3da83143..39374bb5 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt @@ -19,6 +19,7 @@ import com.swmansion.enriched.markdown.utils.common.emitTaskListItemPress import com.swmansion.enriched.markdown.utils.common.markdownEventTypeConstants import com.swmansion.enriched.markdown.utils.common.parseContextMenuItems import com.swmansion.enriched.markdown.utils.common.parseMd4cFlags +import com.swmansion.enriched.markdown.utils.common.parseSelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.interaction.TaskListTapUtils import com.swmansion.enriched.markdown.utils.text.interaction.TaskListToggleUtils @@ -187,6 +188,15 @@ class EnrichedMarkdownTextManager : view.setContextMenuItems(parseContextMenuItems(value)) } + @ReactProp(name = "selectionMenuConfig") + override fun setSelectionMenuConfig( + view: EnrichedMarkdownText?, + value: ReadableMap?, + ) { + if (view == null) return + view.setSelectionMenuConfig(parseSelectionMenuConfig(value)) + } + override fun setPadding( view: EnrichedMarkdownText, left: Int, diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt index 9f3152da..736e8efd 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt @@ -9,6 +9,7 @@ import com.swmansion.enriched.markdown.events.LinkLongPressEvent import com.swmansion.enriched.markdown.events.LinkPressEvent import com.swmansion.enriched.markdown.events.TaskListItemPressEvent import com.swmansion.enriched.markdown.parser.Md4cFlags +import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig fun markdownEventTypeConstants(): MutableMap { val map = mutableMapOf() @@ -86,3 +87,11 @@ fun parseMd4cFlags(flags: ReadableMap?): Md4cFlags = fun parseContextMenuItems(value: ReadableArray?): List = (0 until (value?.size() ?: 0)).mapNotNull { value?.getMap(it)?.getString("text") } + +fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig { + if (value == null) return SelectionMenuConfig() + return SelectionMenuConfig( + copyAsMarkdown = value.getBoolean("copyAsMarkdown"), + copyImageUrl = value.getBoolean("copyImageUrl"), + ) +} diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt index e358b1e2..b457f965 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt @@ -22,6 +22,11 @@ private const val MENU_ITEM_COPY_IMAGE_URL = 1001 private const val MENU_ITEM_CUSTOM_BASE = 2000 private const val MENU_ITEM_CUSTOM_GROUP = 2001 +data class SelectionMenuConfig( + val copyAsMarkdown: Boolean = true, + val copyImageUrl: Boolean = true, +) + /** * Creates an ActionMode.Callback that adds custom copy options and * overrides the default "Copy" action to include HTML for rich text support. @@ -29,7 +34,9 @@ private const val MENU_ITEM_CUSTOM_GROUP = 2001 fun createSelectionActionModeCallback( textView: TextView, getCustomItemTexts: () -> List = { emptyList() }, - onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit = { _, _, _, _ -> }, + getSelectionMenuConfig: () -> SelectionMenuConfig = { SelectionMenuConfig() }, + onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit = + { _, _, _, _ -> }, ): ActionMode.Callback = object : ActionMode.Callback { override fun onCreateActionMode( @@ -47,9 +54,17 @@ fun createSelectionActionModeCallback( menu.removeItem(MENU_ITEM_COPY_IMAGE_URL) menu.removeGroup(MENU_ITEM_CUSTOM_GROUP) - if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) { + val selectionMenuConfig = getSelectionMenuConfig() + + if ( + selectionMenuConfig.copyAsMarkdown && + textView.selectionStart >= 0 && + textView.selectionEnd > textView.selectionStart + ) { menu.add(Menu.NONE, MENU_ITEM_COPY_MARKDOWN, Menu.NONE, "Copy as Markdown") + } + if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) { val customItems = getCustomItemTexts() customItems.forEachIndexed { index, text -> menu @@ -58,7 +73,12 @@ fun createSelectionActionModeCallback( } } - val imageUrls = textView.getImageUrlsInSelection() + val imageUrls = + if (selectionMenuConfig.copyImageUrl) { + textView.getImageUrlsInSelection() + } else { + emptyList() + } if (imageUrls.isNotEmpty()) { val title = if (imageUrls.size == 1) { diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 71a87117..322651a1 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2117,7 +2117,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da - hermes-engine: b0f9c82a51be8e938eb979ada628323e1e093f1d + hermes-engine: 40811a005e96e04818cff405ec04a5b4c4411c1c iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba RCTDeprecation: af44b104091a34482596cd9bd7e8d90c4e9b4bd7 RCTRequired: bb77b070f75f53398ce43c0aaaa58337cebe2bf6 @@ -2127,7 +2127,7 @@ SPEC CHECKSUMS: React: 1ba7d364ade7d883a1ec055bfc3606f35fdee17b React-callinvoker: bc2a26f8d84fb01f003fc6de6c9337b64715f95b React-Core: 7840d3a80b43a95c5e80ef75146bd70925ebab0f - React-Core-prebuilt: 3dc04e91547fc0f260f4b84c12da0f672b813862 + React-Core-prebuilt: 7965d06a81dcc544164f8e98b26d35ae2a4eb36e React-CoreModules: 2eb010400b63b89e53a324ffb3c112e4c7c3ce42 React-cxxreact: a558e92199d26f145afa9e62c4233cf8e7950efe React-debug: 755200a6e7f5e6e0a40ff8d215493d43cce285fc @@ -2189,7 +2189,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: e96e93b493d8d86eeaee3e590ba0be53f6abe46f ReactCodegen: 797de5178718324c6eba3327b07f9a423fbd5787 ReactCommon: 07572bf9e687c8a52fbe4a3641e9e3a1a477c78e - ReactNativeDependencies: 44f7326a697de7f6c8629036b1a4689f0e64c684 + ReactNativeDependencies: 0811b43c669e637a9f3c485fdb106f187fa88398 ReactNativeEnrichedMarkdown: e6fea750b1ca4cbd853c1c78de9bd1ccb638ab77 Yoga: c0b3f2c7e8d3e327e450223a2414ca3fa296b9a2 diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index b9d71b60..703d8e61 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -104,6 +104,10 @@ export default function App() { contextMenuItems={contextMenuItems} selectionColor={Platform.OS === 'ios' ? '#5A52FA' : '#DCDDFE'} selectionHandleColor="#5A52FA" + selectionMenuConfig={{ + copyAsMarkdown: true, + copyImageUrl: true, + }} /> )} diff --git a/apps/macos-example/src/App.tsx b/apps/macos-example/src/App.tsx index 6f681aff..877be3a6 100644 --- a/apps/macos-example/src/App.tsx +++ b/apps/macos-example/src/App.tsx @@ -84,6 +84,10 @@ export default function App() { markdownStyle={markdownStyle} contextMenuItems={contextMenuItems} selectionColor="#DCDDFE" + selectionMenuConfig={{ + copyAsMarkdown: true, + copyImageUrl: true, + }} /> {lastLink != null && ( diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index c297497d..4ab58b97 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -285,6 +285,37 @@ interface ContextMenuItem { /> ``` +### `selectionMenuConfig` + +Controls built-in actions added to the native text selection menu. Custom app-provided actions are controlled separately with `contextMenuItems`. + +| Type | Default Value | Platform | +| -------------------- | ---------------------------------------------- | -------- | +| `SelectionMenuConfig` | `{ copyAsMarkdown: true, copyImageUrl: true }` | iOS, Android, macOS | + +**`SelectionMenuConfig` shape:** + +```ts +interface SelectionMenuConfig { + /** Shows the built-in "Copy as Markdown" action for text selections. */ + copyAsMarkdown?: boolean; + /** Shows the built-in "Copy Image URL" action when selected content contains images. */ + copyImageUrl?: boolean; +} +``` + +**Example:** + +```tsx + +``` + > **Note:** When using `flavor="github"`, `selection.start` and `selection.end` are relative to the text segment the selection is in, not the full markdown string. With `flavor="commonmark"` (default) they are always absolute within the full rendered text. --- diff --git a/docs/COPY_OPTIONS.md b/docs/COPY_OPTIONS.md index 235876e5..6d4d944e 100644 --- a/docs/COPY_OPTIONS.md +++ b/docs/COPY_OPTIONS.md @@ -29,3 +29,17 @@ A dedicated **Copy as Markdown** option is available in the context menu on both ## Copy Image URL When selecting text that contains images, a **Copy Image URL** option appears to copy the image's source URL. On Android, if multiple images are selected, all URLs are copied (one per line). + +## Controlling Built-in Menu Items + +Use `selectionMenuConfig` to hide built-in selection menu actions while keeping the native menu and any `contextMenuItems` intact: + +```tsx + +``` diff --git a/docs/WEB.md b/docs/WEB.md index eae9f3bd..9292d818 100644 --- a/docs/WEB.md +++ b/docs/WEB.md @@ -40,6 +40,7 @@ The web implementation also exports `WebMarkdownTextProps` which extends `Enrich | `streamingAnimation` | Native-only tail fade-in animation. Not yet implemented on web. | | `streamingConfig` | Native-only streaming table configuration. Not yet implemented on web. | | `contextMenuItems` | Not supported — browsers don't allow extending the native context menu. | +| `selectionMenuConfig` | Not supported — native-only built-in selection menu actions. | | `selectionHandleColor` | Android-only — desktop browsers don't render selection handles. | ## Not supported on web diff --git a/ios/EnrichedMarkdown.mm b/ios/EnrichedMarkdown.mm index 9247b1de..e67c9964 100644 --- a/ios/EnrichedMarkdown.mm +++ b/ios/EnrichedMarkdown.mm @@ -92,6 +92,7 @@ @implementation EnrichedMarkdown { NSArray *_contextMenuItemTexts; NSArray *_contextMenuItemIcons; + ENRMSelectionMenuConfig _selectionMenuConfig; ENRMSpoilerOverlay _spoilerOverlay; } @@ -124,6 +125,7 @@ - (instancetype)initWithFrame:(CGRect)frame _enableLinkPreview = YES; _streamingAnimation = NO; _tableStreamingMode = ENRMTableStreamingModeHidden; + _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES}; _fontScaleObserver = [[FontScaleObserver alloc] init]; __weak EnrichedMarkdown *weakSelf = self; @@ -517,7 +519,7 @@ - (EnrichedMarkdownInternalText *)createTextViewForRenderedSegment:(ENRMRenderRe } }); return buildEditMenuForSelection(textView.textStorage, textView.selectedRange, segmentMarkdown, strongSelf->_config, - @[ baseMenu ], customItems); + @[ baseMenu ], customItems, strongSelf -> _selectionMenuConfig); }]; #endif @@ -721,6 +723,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems); } + _selectionMenuConfig = (ENRMSelectionMenuConfig){ + .copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown, + .copyImageURL = newViewProps.selectionMenuConfig.copyImageUrl, + }; + if (newViewProps.spoilerOverlay != oldViewProps.spoilerOverlay) { NSString *modeStr = [[NSString alloc] initWithUTF8String:newViewProps.spoilerOverlay.c_str()]; _spoilerOverlay = ENRMSpoilerOverlayFromString(modeStr); @@ -913,7 +920,7 @@ - (UIMenu *)textView:(UITextView *)textView NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.attributedText, range); return buildEditMenuForSelection(textView.attributedText, range, segmentMarkdown, _config, suggestedActions, - customActions); + customActions, _selectionMenuConfig); } - (BOOL)textView:(UITextView *)textView diff --git a/ios/EnrichedMarkdownText.mm b/ios/EnrichedMarkdownText.mm index 91660748..92ce8d2c 100644 --- a/ios/EnrichedMarkdownText.mm +++ b/ios/EnrichedMarkdownText.mm @@ -81,6 +81,7 @@ @implementation EnrichedMarkdownText { NSArray *_contextMenuItemTexts; NSArray *_contextMenuItemIcons; + ENRMSelectionMenuConfig _selectionMenuConfig; ENRMSpoilerOverlayManager *_spoilerManager; } @@ -145,6 +146,7 @@ - (instancetype)initWithFrame:(CGRect)frame _allowTrailingMargin = NO; _enableLinkPreview = YES; _forceHeightUpdateOnNextRender = NO; + _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES}; _fontScaleObserver = [[FontScaleObserver alloc] init]; __weak EnrichedMarkdownText *weakSelf = self; @@ -204,7 +206,8 @@ - (void)setupTextView } }); return buildEditMenuForSelection(textView.textStorage, textView.selectedRange, strongSelf->_cachedMarkdown, - strongSelf->_config, @[ baseMenu ], customItems); + strongSelf->_config, @[ baseMenu ], customItems, + strongSelf -> _selectionMenuConfig); }; #endif @@ -474,6 +477,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems); } + _selectionMenuConfig = (ENRMSelectionMenuConfig){ + .copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown, + .copyImageURL = newViewProps.selectionMenuConfig.copyImageUrl, + }; + if (newViewProps.streamingAnimation != oldViewProps.streamingAnimation) { _streamingAnimation = newViewProps.streamingAnimation; if (_streamingAnimation) { @@ -621,7 +629,7 @@ - (UIMenu *)textView:(ENRMPlatformTextView *)textView ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler); return buildEditMenuForSelection(textView.attributedText, range, _cachedMarkdown, _config, suggestedActions, - customActions); + customActions, _selectionMenuConfig); } #endif diff --git a/ios/utils/EditMenuUtils+macOS.m b/ios/utils/EditMenuUtils+macOS.m index 36aad256..7876f604 100644 --- a/ios/utils/EditMenuUtils+macOS.m +++ b/ios/utils/EditMenuUtils+macOS.m @@ -9,7 +9,8 @@ NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig, - NSArray *suggestedActions, NSArray *_Nullable customItems) + NSArray *suggestedActions, NSArray *_Nullable customItems, + ENRMSelectionMenuConfig selectionMenuConfig) { NSMenu *menu = ([suggestedActions.firstObject isKindOfClass:[NSMenu class]]) ? (NSMenu *)suggestedActions.firstObject : [[NSMenu alloc] initWithTitle:@""]; @@ -37,11 +38,11 @@ [menu addItem:enhancedCopy]; } - if (markdown.length > 0) { + if (selectionMenuConfig.copyAsMarkdown && markdown.length > 0) { [menu addItem:ENRMCreateMenuItem(@"Copy as Markdown", ^{ copyStringToPasteboard(markdown); })]; } - if (imageURLs.count > 0) { + if (selectionMenuConfig.copyImageURL && imageURLs.count > 0) { NSString *title = (imageURLs.count == 1) ? @"Copy Image URL" : [NSString stringWithFormat:@"Copy %lu Image URLs", (unsigned long)imageURLs.count]; diff --git a/ios/utils/EditMenuUtils.h b/ios/utils/EditMenuUtils.h index 0bc7d5d8..ed17afbf 100644 --- a/ios/utils/EditMenuUtils.h +++ b/ios/utils/EditMenuUtils.h @@ -6,6 +6,11 @@ NS_ASSUME_NONNULL_BEGIN +typedef struct { + BOOL copyAsMarkdown; + BOOL copyImageURL; +} ENRMSelectionMenuConfig; + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,13 @@ extern "C" { // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16. UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig, NSArray *suggestedActions, - NSArray *_Nullable customActions) API_AVAILABLE(ios(16.0)); + NSArray *_Nullable customActions, + ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0)); #else NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig, - NSArray *suggestedActions, NSArray *_Nullable customItems); + NSArray *suggestedActions, NSArray *_Nullable customItems, + ENRMSelectionMenuConfig selectionMenuConfig); #endif #ifdef __cplusplus diff --git a/ios/utils/EditMenuUtils.m b/ios/utils/EditMenuUtils.m index 9a76a4c5..f4852478 100644 --- a/ios/utils/EditMenuUtils.m +++ b/ios/utils/EditMenuUtils.m @@ -73,15 +73,16 @@ static void insertOptionalAction(NSMutableArray *array, UIActio // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16. UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig, NSArray *suggestedActions, - NSArray *_Nullable customActions) API_AVAILABLE(ios(16.0)) + NSArray *_Nullable customActions, + ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0)) { NSAttributedString *selectedText = [attributedText attributedSubstringFromRange:range]; NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown); NSArray *imageURLs = imageURLsInRange(attributedText, range); UIAction *copyAction = createCopyAction(selectedText, markdown, styleConfig); - UIAction *copyMarkdownAction = createCopyMarkdownAction(markdown); - UIAction *copyImageURLAction = createCopyImageURLAction(imageURLs); + UIAction *copyMarkdownAction = selectionMenuConfig.copyAsMarkdown ? createCopyMarkdownAction(markdown) : nil; + UIAction *copyImageURLAction = selectionMenuConfig.copyImageURL ? createCopyImageURLAction(imageURLs) : nil; NSMutableArray *result = [NSMutableArray array]; BOOL foundStandardEditMenu = NO; diff --git a/src/EnrichedMarkdownNativeComponent.ts b/src/EnrichedMarkdownNativeComponent.ts index ba399cad..168e853c 100644 --- a/src/EnrichedMarkdownNativeComponent.ts +++ b/src/EnrichedMarkdownNativeComponent.ts @@ -199,6 +199,11 @@ export interface ContextMenuItemConfig { icon?: string; } +export interface SelectionMenuConfig { + copyAsMarkdown: boolean; + copyImageUrl: boolean; +} + export interface OnContextMenuItemPressEvent { itemText: string; selectedText: string; @@ -342,6 +347,10 @@ export interface NativeProps extends ViewProps { * Custom items to show in the text selection context menu. */ contextMenuItems?: ReadonlyArray>; + /** + * Built-in items to show in the text selection context menu. + */ + selectionMenuConfig: Readonly; /** * Fired when a custom context menu item is pressed. * Receives the item label, the currently selected text, and the selection range. diff --git a/src/EnrichedMarkdownTextNativeComponent.ts b/src/EnrichedMarkdownTextNativeComponent.ts index 9ce638ce..f26af53d 100644 --- a/src/EnrichedMarkdownTextNativeComponent.ts +++ b/src/EnrichedMarkdownTextNativeComponent.ts @@ -199,6 +199,11 @@ export interface ContextMenuItemConfig { icon?: string; } +export interface SelectionMenuConfig { + copyAsMarkdown: boolean; + copyImageUrl: boolean; +} + export interface OnContextMenuItemPressEvent { itemText: string; selectedText: string; @@ -341,6 +346,10 @@ export interface NativeProps extends ViewProps { * Custom items to show in the text selection context menu. */ contextMenuItems?: ReadonlyArray>; + /** + * Built-in items to show in the text selection context menu. + */ + selectionMenuConfig: Readonly; /** * Fired when a custom context menu item is pressed. * Receives the item label, the currently selected text, and the selection range. diff --git a/src/index.tsx b/src/index.tsx index 1c482b32..91414ce3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ export type { MarkdownStyle, Md4cFlags, ContextMenuItem as TextContextMenuItem, + SelectionMenuConfig as TextSelectionMenuConfig, } from './native/EnrichedMarkdownText'; export type { LinkPressEvent, diff --git a/src/native/EnrichedMarkdownText.tsx b/src/native/EnrichedMarkdownText.tsx index dd9caf3b..00ebadcd 100644 --- a/src/native/EnrichedMarkdownText.tsx +++ b/src/native/EnrichedMarkdownText.tsx @@ -9,6 +9,7 @@ import type { EnrichedMarkdownTextProps, StreamingConfig, ContextMenuItem, + SelectionMenuConfig, } from '../types/MarkdownTextProps'; import type { LinkPressEvent, @@ -18,7 +19,12 @@ import type { } from '../types/events'; export type { MarkdownStyle, Md4cFlags }; -export type { EnrichedMarkdownTextProps, StreamingConfig, ContextMenuItem }; +export type { + EnrichedMarkdownTextProps, + StreamingConfig, + ContextMenuItem, + SelectionMenuConfig, +}; export type { LinkPressEvent, LinkLongPressEvent, TaskListItemPressEvent }; const defaultMd4cFlags: Md4cFlags = { @@ -44,6 +50,7 @@ export const EnrichedMarkdownText = ({ streamingConfig, spoilerOverlay = 'particles', contextMenuItems, + selectionMenuConfig, selectionColor, selectionHandleColor, ...rest @@ -126,6 +133,13 @@ export const EnrichedMarkdownText = ({ const tableMode = streamingConfig?.tableMode ?? 'hidden'; const normalizedStreamingConfig = useMemo(() => ({ tableMode }), [tableMode]); + const normalizedSelectionMenuConfig = useMemo( + () => ({ + copyAsMarkdown: selectionMenuConfig?.copyAsMarkdown ?? true, + copyImageUrl: selectionMenuConfig?.copyImageUrl ?? true, + }), + [selectionMenuConfig] + ); const sharedProps = { markdown, @@ -144,6 +158,7 @@ export const EnrichedMarkdownText = ({ spoilerOverlay, style: containerStyle, contextMenuItems: nativeContextMenuItems, + selectionMenuConfig: normalizedSelectionMenuConfig, onContextMenuItemPress: handleContextMenuItemPress, selectionColor, selectionHandleColor, diff --git a/src/types/MarkdownTextProps.ts b/src/types/MarkdownTextProps.ts index 6a827af5..335b4775 100644 --- a/src/types/MarkdownTextProps.ts +++ b/src/types/MarkdownTextProps.ts @@ -20,6 +20,19 @@ export interface ContextMenuItem { visible?: boolean; } +export interface SelectionMenuConfig { + /** + * Shows the built-in "Copy as Markdown" action for text selections. + * @default true + */ + copyAsMarkdown?: boolean; + /** + * Shows the built-in "Copy Image URL" action when selected content contains images. + * @default true + */ + copyImageUrl?: boolean; +} + export interface StreamingConfig { /** * Controls how incomplete tables are handled during streaming. @@ -186,6 +199,13 @@ export interface EnrichedMarkdownTextProps extends Omit { * @platform ios, android */ contextMenuItems?: ContextMenuItem[]; + /** + * Controls built-in items added to the native text selection menu. + * Custom app-provided actions are controlled separately with `contextMenuItems`. + * @default { copyAsMarkdown: true, copyImageUrl: true } + * @platform ios, android, macos + */ + selectionMenuConfig?: SelectionMenuConfig; /** * Sets the text direction on the root container. * Useful for RTL languages — CSS logical properties in the renderers