Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { EnrichedMarkdownTextInputStory } from '../EnrichedMarkdownTextInputStory';
import { storyMeta } from '../shared/storyMeta';
import type { InputStory } from '../shared/storyTypes';

type FormatMenuStoryExtra = {
bold: boolean;
italic: boolean;
underline: boolean;
strikethrough: boolean;
spoiler: boolean;
link: boolean;
};

const MARKDOWN =
'Select this text and open the Format submenu to see which items are visible.';

const argTypes = {
bold: {
control: 'boolean',
description: 'formatMenuConfig.bold — show "Bold" in the Format submenu.',
},
italic: {
control: 'boolean',
description:
'formatMenuConfig.italic — show "Italic" in the Format submenu.',
},
underline: {
control: 'boolean',
description:
'formatMenuConfig.underline — show "Underline" in the Format submenu.',
},
strikethrough: {
control: 'boolean',
description:
'formatMenuConfig.strikethrough — show "Strikethrough" in the Format submenu.',
},
spoiler: {
control: 'boolean',
description:
'formatMenuConfig.spoiler — show "Spoiler" in the Format submenu.',
},
link: {
control: 'boolean',
description: 'formatMenuConfig.link — show "Link" in the Format submenu.',
},
};

export default storyMeta('Props', 'Format Menu');

export const Default: InputStory<FormatMenuStoryExtra> = {
args: {
initialMarkdown: MARKDOWN,
bold: true,
italic: true,
underline: true,
strikethrough: true,
spoiler: true,
link: true,
},
argTypes,
render: ({
bold,
italic,
underline,
strikethrough,
spoiler,
link,
...args
}) => (
<EnrichedMarkdownTextInputStory
title="Format Menu"
description="formatMenuConfig controls which items appear in the Format submenu. Toggle the controls to show/hide individual formatting actions."
{...args}
formatMenuConfig={{
bold,
italic,
underline,
strikethrough,
spoiler,
link,
}}
/>
),
};
30 changes: 30 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,36 @@ interface InputSelectionMenuConfig {
/>
```

### `formatMenuConfig`

Controls which individual items appear inside the Format submenu. Only effective when `selectionMenuConfig.format` is `true` (the default). Omitting the prop or any field shows all items.

| Type | Default Value | Platform |
| ------------------ | ------------------------------------------------------------------------------------ | ------------------- |
| `FormatMenuConfig` | `{ bold: true, italic: true, underline: true, strikethrough: true, spoiler: true, link: true }` | iOS, Android, macOS |

**`FormatMenuConfig` shape:**

```ts
interface FormatMenuConfig {
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
spoiler?: boolean;
link?: boolean;
}
```

**Example:**

```tsx
// Hide Spoiler and Link from the Format submenu
<EnrichedMarkdownTextInput
formatMenuConfig={{ spoiler: false, link: false }}
/>
```

### Ref Methods

All methods are called imperatively on the ref (`ref.current?.methodName()`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
import com.swmansion.enriched.markdown.input.layout.InputMeasurementStore
import com.swmansion.enriched.markdown.input.model.StyleType
import com.swmansion.enriched.markdown.input.toolbar.FormatMenuConfig
import com.swmansion.enriched.markdown.input.toolbar.InputSelectionMenuConfig
import com.swmansion.enriched.markdown.utils.input.BorderPropsApplicator
import com.swmansion.enriched.markdown.utils.input.MarkdownStyleParser
Expand Down Expand Up @@ -281,6 +282,27 @@ class EnrichedMarkdownTextInputManager :
}
}

@ReactProp(name = "formatMenuConfig")
override fun setFormatMenuConfig(
view: EnrichedMarkdownTextInputView?,
value: ReadableMap?,
) {
if (view == null) return
view.contextMenu.formatMenuConfig =
if (value == null) {
FormatMenuConfig()
} else {
FormatMenuConfig(
bold = value.getBoolean("bold"),
italic = value.getBoolean("italic"),
underline = value.getBoolean("underline"),
strikethrough = value.getBoolean("strikethrough"),
spoiler = value.getBoolean("spoiler"),
link = value.getBoolean("link"),
)
}
}

@ReactProp(name = "linkRegex")
override fun setLinkRegex(
view: EnrichedMarkdownTextInputView?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@ data class InputSelectionMenuConfig(
val copyAsMarkdown: Boolean = true,
)

data class FormatMenuConfig(
val bold: Boolean = true,
val italic: Boolean = true,
val underline: Boolean = true,
val strikethrough: Boolean = true,
val spoiler: Boolean = true,
val link: Boolean = true,
)

class InputContextMenu(
private val view: EnrichedMarkdownTextInputView,
) {
private var customItemTexts: List<String> = emptyList()
var selectionMenuConfig: InputSelectionMenuConfig = InputSelectionMenuConfig()
var formatMenuConfig: FormatMenuConfig = FormatMenuConfig()

fun setContextMenuItems(items: List<String>) {
customItemTexts = items
Expand All @@ -50,8 +60,10 @@ class InputContextMenu(

if (selectionMenuConfig.format) {
val formatSubMenu = menu.addSubMenu(FORMAT_MENU_GROUP_ID, MENU_FORMAT_ID, 100, "Format")
FORMAT_ITEMS.forEachIndexed { index, (title, _) ->
formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title)
FORMAT_ITEMS.forEachIndexed { index, (title, styleType) ->
if (isFormatItemVisible(styleType)) {
formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title)
}
}
}

Expand Down Expand Up @@ -106,6 +118,16 @@ class InputContextMenu(
}
}

private fun isFormatItemVisible(styleType: StyleType): Boolean =
when (styleType) {
StyleType.BOLD -> formatMenuConfig.bold
StyleType.ITALIC -> formatMenuConfig.italic
StyleType.UNDERLINE -> formatMenuConfig.underline
StyleType.STRIKETHROUGH -> formatMenuConfig.strikethrough
StyleType.SPOILER -> formatMenuConfig.spoiler
StyleType.LINK -> formatMenuConfig.link
}

private fun applyFormat(styleType: StyleType) {
val start = view.selectionStart
val end = view.selectionEnd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ - (UIMenu *)textView:(UITextView *)textView
}

ENRMInputSelectionMenuConfig menuConfig = [self inputSelectionMenuConfig];
ENRMFormatMenuConfig fmtConfig = [self formatMenuConfig];
__weak EnrichedMarkdownTextInput *weakSelf = self;

// TODO: Localize titles with NSLocalizedString.
Expand All @@ -42,9 +43,17 @@ - (UIMenu *)textView:(UITextView *)textView
{@"Link", @"link", ENRMInputStyleTypeLink},
};
static const NSUInteger kFormatItemCount = sizeof(kFormatItems) / sizeof(kFormatItems[0]);
const BOOL kFormatItemVisible[] = {fmtConfig.bold, fmtConfig.italic, fmtConfig.underline,
fmtConfig.strikethrough, fmtConfig.spoiler, fmtConfig.link};
_Static_assert(sizeof(kFormatItemVisible) / sizeof(kFormatItemVisible[0]) ==
sizeof(kFormatItems) / sizeof(kFormatItems[0]),
"kFormatItemVisible must match kFormatItems length");

NSMutableArray<UIAction *> *formatActions = [NSMutableArray arrayWithCapacity:kFormatItemCount];
for (NSUInteger i = 0; i < kFormatItemCount; i++) {
if (!kFormatItemVisible[i]) {
continue;
}
ENRMInputStyleType styleType = kFormatItems[i].styleType;
UIAction *action = [UIAction actionWithTitle:kFormatItems[i].title
image:[UIImage systemImageNamed:kFormatItems[i].icon]
Expand Down Expand Up @@ -111,6 +120,7 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex
}

ENRMInputSelectionMenuConfig menuConfig = [self inputSelectionMenuConfig];
ENRMFormatMenuConfig fmtConfig = [self formatMenuConfig];
__weak EnrichedMarkdownTextInput *weakSelf = self;
NSArray<NSMenuItem *> *customItems =
ENRMBuildContextMenuItems([self contextMenuItemTexts], [self contextMenuItemIcons], textView,
Expand Down Expand Up @@ -144,8 +154,15 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex
{@"Spoiler", @selector(toggleSpoiler), @"", 0},
{@"Link", @selector(showLinkPrompt), @"", 0},
};
const BOOL visible[] = {fmtConfig.bold, fmtConfig.italic, fmtConfig.underline,
fmtConfig.strikethrough, fmtConfig.spoiler, fmtConfig.link};
_Static_assert(sizeof(visible) / sizeof(visible[0]) == sizeof(items) / sizeof(items[0]),
"visible must match items length");

for (NSUInteger i = 0; i < sizeof(items) / sizeof(items[0]); i++) {
if (!visible[i]) {
continue;
}
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:items[i].title
action:items[i].action
keyEquivalent:items[i].key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ typedef struct {
BOOL copyAsMarkdown;
} ENRMInputSelectionMenuConfig;

typedef struct {
BOOL bold;
BOOL italic;
BOOL underline;
BOOL strikethrough;
BOOL spoiler;
BOOL link;
} ENRMFormatMenuConfig;

@interface EnrichedMarkdownTextInput (Internal)

- (void)toggleBold;
Expand All @@ -26,6 +35,7 @@ typedef struct {
- (NSArray<NSString *> *)contextMenuItemTexts;
- (NSArray<NSString *> *)contextMenuItemIcons;
- (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig;
- (ENRMFormatMenuConfig)formatMenuConfig;

#if TARGET_OS_OSX
- (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu textView:(NSTextView *)textView;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ @implementation EnrichedMarkdownTextInput {
NSWritingDirection _resolvedLayoutDirection;

ENRMInputSelectionMenuConfig _inputSelectionMenuConfig;
ENRMFormatMenuConfig _formatMenuConfig;
}

#pragma mark - Fabric lifecycle
Expand Down Expand Up @@ -132,6 +133,8 @@ - (instancetype)initWithFrame:(CGRect)frame
_resolvedLayoutDirection =
[[RCTI18nUtil sharedInstance] isRTL] ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
_inputSelectionMenuConfig = (ENRMInputSelectionMenuConfig){.format = YES, .copyAsMarkdown = YES};
_formatMenuConfig = (ENRMFormatMenuConfig){
.bold = YES, .italic = YES, .underline = YES, .strikethrough = YES, .spoiler = YES, .link = YES};

[self setupTextView];

Expand Down Expand Up @@ -361,6 +364,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
.copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown,
};

_formatMenuConfig = (ENRMFormatMenuConfig){
.bold = newViewProps.formatMenuConfig.bold,
.italic = newViewProps.formatMenuConfig.italic,
.underline = newViewProps.formatMenuConfig.underline,
.strikethrough = newViewProps.formatMenuConfig.strikethrough,
.spoiler = newViewProps.formatMenuConfig.spoiler,
.link = newViewProps.formatMenuConfig.link,
};

if (newViewProps.mentionIndicators != oldViewProps.mentionIndicators) {
NSMutableArray<NSString *> *indicators = [NSMutableArray array];
for (const auto &indicator : newViewProps.mentionIndicators) {
Expand Down Expand Up @@ -1247,6 +1259,11 @@ - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig
return _inputSelectionMenuConfig;
}

- (ENRMFormatMenuConfig)formatMenuConfig
{
return _formatMenuConfig;
}

- (void)emitContextMenuItemPress:(NSString *)itemText
{
auto eventEmitter = [self getEventEmitter];
Expand Down
Loading
Loading