Skip to main content

Quick start

Three pieces: a registration that contributes an item, a contextmenu listener that opens the menu, and a <SuperDocEditor disableContextMenu> to keep the built-in out of the way.
Suppress the built-in menu so your own takes over:
disableContextMenu switches off SuperDoc’s own menu UI and lets the browser’s native contextmenu event proceed. When getContextMenuItems(context) returns nothing for a click, the listener returns without preventDefault and the browser native menu falls through (Copy / Paste / Inspect). No dead right-click.

The bundle

ui.viewport.contextAt({ x, y }) always returns an object, never null. Empty defaults make destructuring safe. position.target is a collapsed SelectionTarget at the click, story-aware when the click landed inside a header / footer / footnote. Pass it straight to editor.doc.insert for “Paste here” / “Insert clause here” actions.

Contribute an item

Add a contextMenu field to your registration. The when predicate filters on the same bundle the handler will receive.
Each contribution is grouped (built-ins are format, clipboard, review, comment, link, then customs in registration order). Items inside a group sort by order. Predicates that throw are caught and the item is hidden for that menu.

Predicate examples

The bundle’s optional fields make scope rules direct.
The predicate sees entities, selection, point, position, insideSelection. Old predicates that only destructure { entities, selection } keep working.

item.invoke()

Items returned from getContextMenuItems(context) carry an invoke() closure that fires the registered execute with the bundle bound to context. Your menu component dispatches without re-threading the click target through a payload.
Inside execute, the same bundle the predicate filtered on is available as context:
context is undefined when the command is dispatched directly (ui.commands.get(id)?.execute(payload), ui.commands.require(id).execute(...), or ui.toolbar.execute(id, payload) for built-ins). Handlers that only depend on payload keep working unchanged.

Falling through to the native menu

When getContextMenuItems(context) returns no items, your listener returns early without calling event.preventDefault(). The browser shows its native menu (Copy / Paste / Inspect) instead of producing a dead right-click. This relies on disableContextMenu: true on the editor: with the built-in menu suppressed, no other listener swallows the event. If you’d rather suppress the native menu in the empty case too, call event.preventDefault() regardless of items length and render nothing.

Worked example

The reference workspace at demos/editor/custom-ui wires the full pattern end-to-end. The four registrations below mirror the demo’s ContextMenuRegistrations.tsx. They cover the three subjects the menu can act on: an entity, the selection, or the click point.

Trade-offs

  • The bundle is computed once when the menu opens. If your registration’s execute runs much later (popover, multi-step picker), context.selection reflects the open-time selection, not the current one. Re-read ui.selection.getSnapshot() when you need fresh selection.
  • item.invoke?.() is undefined for items returned from the legacy getContextMenuItems({ entities }) shape. Always call as item.invoke?.(). The full bundle path always populates it.
  • Scope your contextmenu listener to ui.viewport.getHost(). An empty bundle alone isn’t a scope signal: it can mean “outside the editor” or “inside plain text with no selection and no entities”.
  • position is null when the click is outside the painted host. Predicates that act on the click point should check position !== null first.