Skip to main content
Canvas Editor provides a flexible context menu system that allows you to add custom menu items that appear on right-click.

Basic Usage

Register context menus using editor.register.contextMenuList():
import Editor, { Command, IContextMenuContext } from 'canvas-editor'

const editor = new Editor(container, data, options)

editor.register.contextMenuList([
  {
    key: 'myCustomMenu',
    name: 'My Custom Action',
    when: (context) => {
      // Show only when text is selected
      return context.editorHasSelection
    },
    callback: (command, context) => {
      // Execute action
      const text = command.getRangeText()
      console.log('Selected text:', text)
    }
  }
])

Context Menu Configuration

IRegisterContextMenu Interface

interface IRegisterContextMenu {
  key?: string                                          // Unique identifier
  i18nPath?: string                                     // Translation key
  isDivider?: boolean                                   // Menu divider
  icon?: string                                         // Icon class name
  name?: string                                         // Display name
  shortCut?: string                                     // Shortcut hint text
  disable?: boolean                                     // Disable menu item
  when?: (payload: IContextMenuContext) => boolean      // Show condition
  callback?: (command: Command, context: IContextMenuContext) => void
  childMenus?: IRegisterContextMenu[]                   // Submenu items
}

Context Information

The when and callback functions receive context about the current editor state:
interface IContextMenuContext {
  startElement: IElement | null      // Element at selection start
  endElement: IElement | null        // Element at selection end
  isReadonly: boolean                // Editor readonly state
  editorHasSelection: boolean        // Has selected text
  editorTextFocus: boolean           // Editor has focus
  isInTable: boolean                 // Cursor in table
  isCrossRowCol: boolean             // Selection spans rows/cols
  zone: EditorZone                   // Current zone (main/header/footer)
  trIndex: number | null             // Table row index
  tdIndex: number | null             // Table cell index
  tableElement: IElement | null      // Table element
  options: DeepRequired<IEditorOption> // Editor options
}

Built-in Context Menus

Canvas Editor includes several built-in context menus:
Shown when an image is selected:
{
  key: 'imageChange',
  i18nPath: 'contextmenu.image.change',
  icon: 'image-change',
  when: (payload) => {
    return !payload.isReadonly && 
           !payload.editorHasSelection &&
           payload.startElement?.type === ElementType.IMAGE
  },
  callback: (command) => {
    command.executeReplaceImageElement(newImageSrc)
  }
}
Source: /src/editor/core/contextmenu/menus/imageMenus.ts:23
Shown when inside a table cell:
  • Insert row/column (top, bottom, left, right)
  • Delete row/column/table
  • Merge/unmerge cells
  • Vertical alignment
  • Table borders
Source: /src/editor/core/contextmenu/menus/tableMenus.ts

Creating Custom Menus

1

Simple Menu Item

editor.register.contextMenuList([
  {
    key: 'uppercase',
    name: 'Convert to Uppercase',
    icon: 'text-uppercase',
    when: (context) => context.editorHasSelection,
    callback: (command, context) => {
      const text = command.getRangeText()
      command.executeInsertElementList([{
        value: text.toUpperCase()
      }])
    }
  }
])
2

Conditional Menu

Show menu only in specific contexts:
editor.register.contextMenuList([
  {
    key: 'tableStats',
    name: 'Table Statistics',
    when: (context) => {
      // Only show when in a table
      return context.isInTable && !context.isReadonly
    },
    callback: (command, context) => {
      const { tableElement } = context
      if (tableElement) {
        const rows = tableElement.trList?.length || 0
        const cols = tableElement.tdList?.length || 0
        alert(`Table: ${rows} rows × ${cols} columns`)
      }
    }
  }
])
3

Menu with Submenu

Create nested menus:
editor.register.contextMenuList([
  {
    key: 'textTransform',
    name: 'Text Transform',
    when: (context) => context.editorHasSelection,
    childMenus: [
      {
        key: 'uppercase',
        name: 'UPPERCASE',
        when: () => true,
        callback: (command) => {
          const text = command.getRangeText()
          command.executeInsertElementList([{
            value: text.toUpperCase()
          }])
        }
      },
      {
        key: 'lowercase',
        name: 'lowercase',
        when: () => true,
        callback: (command) => {
          const text = command.getRangeText()
          command.executeInsertElementList([{
            value: text.toLowerCase()
          }])
        }
      },
      {
        key: 'titlecase',
        name: 'Title Case',
        when: () => true,
        callback: (command) => {
          const text = command.getRangeText()
          const titleCase = text.replace(/\w\S*/g, (txt) => 
            txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
          )
          command.executeInsertElementList([{
            value: titleCase
          }])
        }
      }
    ]
  }
])
Add visual separators between menu groups:
editor.register.contextMenuList([
  {
    name: 'Action 1',
    when: () => true,
    callback: (command) => { /* ... */ }
  },
  {
    isDivider: true // Creates a horizontal line
  },
  {
    name: 'Action 2',
    when: () => true,
    callback: (command) => { /* ... */ }
  }
])
Dividers at the beginning or end of a menu, or adjacent dividers, are automatically hidden.

Using Icons

Add icons to menu items using CSS classes:
editor.register.contextMenuList([
  {
    name: 'Print',
    icon: 'print', // Adds class: canvas-editor-contextmenu-print
    when: () => true,
    callback: (command) => command.executePrint()
  }
])
Define icon styles in your CSS:
.canvas-editor-contextmenu-print::before {
  content: '🖨️';
  margin-right: 8px;
}

Internationalization

Use i18nPath instead of name for translated menu items:
// Register translations first
editor.register.langMap('en', {
  contextmenu: {
    custom: {
      myAction: 'My Action'
    }
  }
})

editor.register.langMap('es', {
  contextmenu: {
    custom: {
      myAction: 'Mi Acción'
    }
  }
})

// Use i18nPath in menu
editor.register.contextMenuList([
  {
    key: 'myAction',
    i18nPath: 'contextmenu.custom.myAction', // Uses current locale
    when: () => true,
    callback: (command) => { /* ... */ }
  }
])
See Internationalization for more details on i18n configuration.

Showing Keyboard Shortcuts

Display keyboard shortcuts as hints:
import { isApple } from 'canvas-editor'

editor.register.contextMenuList([
  {
    name: 'Save',
    shortCut: `${isApple ? '⌘' : 'Ctrl'} + S`,
    when: () => true,
    callback: (command) => {
      // Save logic
    }
  }
])

Disabling Built-in Menus

Disable built-in context menus using editor options:
import { INTERNAL_CONTEXT_MENU_KEY } from 'canvas-editor'

const editor = new Editor(container, data, {
  contextMenuDisableKeys: [
    INTERNAL_CONTEXT_MENU_KEY.GLOBAL.CUT,
    INTERNAL_CONTEXT_MENU_KEY.GLOBAL.COPY,
    INTERNAL_CONTEXT_MENU_KEY.IMAGE.CHANGE
  ]
})
Available keys:
INTERNAL_CONTEXT_MENU_KEY.GLOBAL.CUT
INTERNAL_CONTEXT_MENU_KEY.GLOBAL.COPY
INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE
INTERNAL_CONTEXT_MENU_KEY.GLOBAL.SELECT_ALL
INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PRINT

Advanced Examples

editor.register.contextMenuList([
  {
    key: 'wordCount',
    name: 'Word Count',
    icon: 'calculator',
    when: (context) => context.editorHasSelection,
    callback: (command) => {
      const text = command.getRangeText()
      const words = text.trim().split(/\s+/).length
      const chars = text.length
      alert(`Words: ${words}\nCharacters: ${chars}`)
    }
  }
])

Programmatic Menu Display

Get the list of registered menus:
const menus = editor.register.getContextMenuList()
console.log('Registered context menus:', menus)
Context menus are automatically filtered based on the when condition. Disabled menus and menus that don’t meet their condition won’t appear.

Best Practices

Use descriptive keys

Always provide unique, descriptive keys for your menu items to avoid conflicts.

Implement proper conditions

Use the when function to show menus only in relevant contexts.

Respect readonly mode

Check context.isReadonly before showing edit actions.

Provide keyboard shortcuts

Include shortCut hints for actions that have keyboard shortcuts.

Troubleshooting

  • Ensure CSS class is properly defined
  • Check that icon class name doesn’t conflict with built-in icons
  • Verify CSS is loaded after editor styles

Next Steps

Custom Shortcuts

Register keyboard shortcuts

Internationalization

Add multi-language support

Commands API

Explore available commands

Plugin System

Create custom plugins

Build docs developers (and LLMs) love