Better Rich Text Buy licence

Menus and dialogs

On this page
  1. Menu bar
  2. Keyboard
  3. Context menu
  4. Adding items
  5. Styles
  6. Help
  7. Preview
  8. Block handles
  9. Keyboard
  10. Phones

Opt-in interface features for a classic editor: a menu bar, a context menu, a Styles menu, help, preview and block handles. Each is only bundled when you import it. The last section covers the toolbar on phones, which needs no setup.

TypeScript
import { createRichTextEditor, standardPreset } from '@better-rich-text/core';
import { contextMenuFeature, helpFeature, menubarFeature, previewFeature } from '@better-rich-text/ui';

createRichTextEditor('#editor', {
  features: [...standardPreset.features, menubarFeature, contextMenuFeature, helpFeature, previewFeature],
  menubar: 'file edit view insert format tools table help',
  menu: { custom: { title: 'Custom', items: 'bold italic | link' } },
  contextmenu: 'spelling link image table clipboard',
});

menubarFeature adds File, Edit, View, Insert, Format, Tools, Table and Help above the toolbar.

  • menubar picks the menus and their order. false hides the bar.

  • menu adds menus or replaces the defaults (DEFAULT_MENUS). Each is { title, items }, where items is a space-separated list with | between groups.

  • An item is a toolbar item id (bold, link, table, templates), a command name (history.undo) or a TinyMCE menu item name (strikethrough, forecolor, inserttable, searchreplace, cell, row, column, tableprops).

  • Labels, shortcuts, checked and disabled states come from the commands. Menu buttons such as Text style and Styles become submenus. Items that are not installed are left out, and a menu with nothing in it is hidden.

  • Built-in items: cut, copy, paste, pastetext, selectall, newdocument, print (with plugin-documents) and wordcount.

Keyboard

KeysAction
Alt+F9 Focus the menu bar
Left, Right Move between menus; with a menu open, open the next one
Down, Enter, Space Open a menu at its first item
Up Open a menu at its last item
Home, End First or last menu or item
A letter Next menu or item starting with it
Right, Left inside a menu Open or close a submenu
Escape Close one level; on the bar, return to the text

Context menu

contextMenuFeature replaces the browser menu inside the editor. It opens on right-click, on a touch long-press, and with Shift+F10 or the context-menu key at the caret.

SectionShown whenItems
spelling Proofing has an issue at the caret Suggestions, Ignore, Ignore all, Add to dictionary
suggestion A tracked change is at the caret or in the selection Accept, Reject
link The caret is in a link Edit, Open, Copy, Remove
image An image is under the pointer Properties, Edit image (plugin-image-editor), Alternative text, Align
table The caret is in a table Cell, Row, Column and Table submenus
list The caret is in a list List types, Increase indent, Decrease indent
code The caret is in a code block Copy code, Indent, Outdent, Exit
footnote, anchor, mention, media That node is under the pointer Added by those plugins
comment The comments feature is installed Comment
format Text is selected Bold, Italic, Underline, Clear formatting, Link
clipboard Always Cut, Copy, Paste, Paste as text, Select all
  • The contextmenu option lists sections and their order, TinyMCE style. false keeps the browser menu.

  • Mod+right-click (Cmd on macOS, Ctrl elsewhere) shows the browser menu. createContextMenuFeature({ native: 'never' }) turns that off; longPress: false leaves long-press to the browser.

  • Paste reads the async Clipboard API. When the browser refuses, a short note says which shortcut to use instead.

  • Menus flip and shift to stay on screen and mount in the overlay layer, so they work in shadow roots and modal dialogs.

Adding items

Plugins and hosts add sections through the ui.contextMenu filter. Sections are ordered by priority (built-ins use 10 to 90) unless the host sets an order.

TypeScript
editor.editor.hooks.addFilter('ui.contextMenu', (sections, context) => {
  if (context.node?.type !== 'callout') return sections;
  return [...sections, { id: 'callout', priority: 25, items: [{ id: 'remove', label: 'Remove callout', run: () => context.ui.run('callout.remove') }] }];
});

context has the interface (ui), the element under the pointer (target), the selected node (node), the node types around the caret (ancestors), whether text is selected (hasRange) and where dialogs should open (at()). Items can have icon, shortcut, checked, disabled with a reason, separatorBefore and items for a submenu. commandNode(ui, command, input) builds an item from a command.

Styles

createStylesPlugin({ formats }) from @better-rich-text/plugin-styles adds a Styles menu of your block and inline styles, previewed as they look in the content.

TypeScript
createStylesPlugin({
  formats: [
    { title: 'Lead', block: 'p', classes: 'lead' },
    { title: 'Headings', items: [{ title: 'Section title', block: 'h2', classes: 'section-title' }] },
    { title: 'Marker', inline: 'span', classes: 'marker' },
  ],
  allowClasses: ['legacy-highlight'],
});
  • block is p, h1 to h6 or pre. A block style sets the block type and exports the format's classes (<p class="lead">); loading that HTML restores the style.

  • inline is span. Inline styles use the className mark, which only accepts the inline formats' classes and allowClasses, so classes in pasted content are dropped.

  • attributes may set lang and dir; other attributes are ignored.

  • Groups ({ title, items }) become submenus.

  • Commands: styles.apply { name } (applying an active style removes it), styles.remove { name? } and styles.list.

importCssFormats(stylesheet) builds formats from class selectors, like TinyMCE's importcss: .lead becomes an inline style, p.note and h2.title block styles. It takes CSS text, a CSSStyleSheet or a list of them, plus filter, title and groups options.

Help

helpFeature adds a help button and Alt+0. The dialog has three tabs:

  • Keyboard shortcuts, generated from the installed keymaps and labelled with the commands' labels, with a filter.

  • Plugins, with their descriptions and versions.

  • Accessibility notes on moving between the text, toolbar, menu bar and dialogs.

Preview

previewFeature (or createPreviewFeature({ contentCss, contentStyle, devices })) adds a preview button. The dialog shows the exported HTML in a sandboxed frame with no scripts, styled with the editor's content styles and your contentCss, at desktop, tablet or phone width, and prints it.

Block handles

@better-rich-text/plugin-block-handle puts a handle in the left gutter beside the block under the mouse, or beside the block holding the caret. The document and full presets include it.

TypeScript
import { createRichTextEditor, standardPreset } from '@better-rich-text/core';
import { blockHandleFeature, blockHandlePlugin } from '@better-rich-text/plugin-block-handle';

createRichTextEditor('#editor', {
  plugins: [blockHandlePlugin],
  features: [...standardPreset.features, blockHandleFeature],
});
  • Drag the handle to move the block. A line shows where it will land, the editor scrolls near its edges, and Escape cancels. On a touch screen, press and hold the handle first.

  • Click the handle for the block menu: Move up, Move down, Duplicate, Delete, and Turn into a paragraph, heading, quote, list or code block.

  • A selection across several blocks moves them together.

  • List items move among list items and take their nested lists with them. Inside a table, the whole table moves.

  • The handle hides while you type and comes back when the mouse moves. It never shows in a read-only or disabled editor or in the source view.

  • The content gets a wider start padding so the handle has room, and the handle takes its colours from the skin.

The plugin works without the feature: its commands and shortcuts are enough for inline editors and your own interface.

CommandInput
block.moveUp, block.moveDown { blockId? } Swap the selected blocks (or blockId) with the block above or below. Disabled at the first or last position.
block.move { blockIds, target, position } Move sibling blocks before or after another block. This is what a drop runs.
block.duplicate { blockId? } Insert a copy after the blocks.
block.delete { blockId? } Delete the blocks. Deleting every item of a list deletes the list, and an emptied document keeps one paragraph.

Each command is one transaction, so it is one undo step. Moves are node moves rather than a delete and an insert, so collaborators' edits inside a moved block are kept.

Keyboard

KeysAction
Alt+Shift+Up, Alt+Shift+Down Move the selected blocks
Tab from a paragraph Focus the handle (Option+Tab in Safari)
Enter, Space or Down on the handle Open the block menu
Alt+Shift+Up, Alt+Shift+Down on the handle Move its block and keep focus on the handle
Escape Close the menu, cancel a drag, or return from the handle to the text

Moves, copies and deletions are announced to screen readers, for example "Moved block up".

On macOS, Alt+Shift+Up and Down replace Option+Shift+Up and Down, which otherwise extend the selection to the start or end of the paragraph.

Phones

While someone edits on a phone, the toolbar docks to the bottom of the screen, just above the on-screen keyboard, and goes back to its place when the editor loses focus. A spacer keeps its place, so the text does not jump.

  • The docked toolbar is one row that scrolls sideways, with 44px buttons. The More button and its drawer are not used.

  • Menus and dialogs opened from it open upwards and stay above the keyboard.

  • It follows the keyboard with window.visualViewport, so it works in Safari on iOS and Chrome on Android without changes to your page.

  • The caret is scrolled clear of it while you type near the bottom of the screen.

  • It respects the safe area (env(safe-area-inset-*), which needs viewport-fit=cover in your viewport meta tag) and reduced motion, and takes its colours from the skin.

toolbarMobile controls it:

ValueBehaviour
'auto' (default) Docks on touch screens of phone size: a coarse pointer and a window narrower than 768px or a screen shorter than 600px on its short side.
'docked' Docks on every device while the editor has focus.
'off' Leaves the toolbar where it is.
TypeScript
createRichTextEditor('#editor', { toolbarMobile: 'off' });

If your page sets navigator.virtualKeyboard.overlaysContent = true, the keyboard no longer shrinks the visual viewport, so the toolbar reads the keyboard's height from the VirtualKeyboard API instead. The editor never changes that setting itself, because it applies to the whole page.

The docked toolbar uses position: fixed. Inside an ancestor with a transform, filter or contain it is positioned against that ancestor instead of the screen; set toolbarMobile: 'off' there.