Creating custom Menu toolbar buttons

A toolbar menu button is a toolbar button that opens a menu when clicked. This menu can also contain submenus. This is useful for grouping together actions that would otherwise be several buttons on the toolbar. It can also be used to reduce visual clutter and save UI space, as menubar menu items and some toolbar buttons could be moved into a toolbar menu button. Potentially, all menubar menu items could be moved into toolbar menu buttons, allowing for the editor to be used without a menubar at all.

For example: The table plugin’s table toolbar button opens a menu similar to the menubar Table menu.

Options

Name Value Requirement Description

fetch

(success: (menu) => void) => void

required

Function that takes a callback which must be passed the list of options for the button’s dropdown.

text

string

optional

Text to display if no icon is found.

icon

string

optional

Name of the icon to be displayed. Must correspond to an icon: in the icon pack, in a custom icon pack, or added using the addIcon API.

tooltip

string

optional

Text for button tooltip.

onSetup

(api) => (api) => void

optional

default: () => () => {} - Function that’s invoked when the button is rendered. For details, see: Using onSetup.

API

Name Value Description

isEnabled

() => boolean

Checks if the button is enabled.

setEnabled

(state: boolean) => void

Sets the button’s enabled state.

The following is a simple toolbar menu button example:

  • TinyMCE

  • HTML

  • JS

  • Edit on CodePen

<textarea id="custom-toolbar-menu-button">
  <p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
  <h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
  <p>Select a menu item from the listbox above and it will insert contents into the editor at the caret position.</p>

  <h2>Got questions or need help?</h2>
  <ul>
    <li>Our <a href="https://www.tiny.cloud/docs/tinymce/6/">documentation</a> is a great resource for learning how to configure TinyMCE.</li>
    <li>Have a specific question? Try the <a href="https://stackoverflow.com/questions/tagged/tinymce" target="_blank" rel="noopener"><code>tinymce</code> tag at Stack Overflow</a>.</li>
    <li>We also offer enterprise grade support as part of <a href="https://www.tiny.cloud/pricing">TinyMCE premium plans</a>.</li>
  </ul>

  <h2>Found a bug?</h2>
  <p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p>

  <h2>Finally ...</h2>
  <p>Don't forget to check out our other product <a href="http://www.plupload.com" target="_blank">Plupload</a>, your ultimate upload solution featuring HTML5 upload support.</p>
  <p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    <br>All the best from the TinyMCE team.</p>
</textarea>
tinymce.init({
  selector: 'textarea#custom-toolbar-menu-button',
  height: 500,
  toolbar: 'mybutton',

  setup: (editor) => {
    /* Menu items are recreated when the menu is closed and opened, so we need
       a variable to store the toggle menu item state. */
    let toggleState = false;

    /* example, adding a toolbar menu button */
    editor.ui.registry.addMenuButton('mybutton', {
      text: 'My button',
      fetch: (callback) => {
        const items = [
          {
            type: 'menuitem',
            text: 'Menu item 1',
            onAction: () => editor.insertContent('&nbsp;<em>You clicked menu item 1!</em>')
          },
          {
            type: 'nestedmenuitem',
            text: 'Menu item 2',
            icon: 'user',
            getSubmenuItems: () => [
              {
                type: 'menuitem',
                text: 'Sub menu item 1',
                icon: 'unlock',
                onAction: () => editor.insertContent('&nbsp;<em>You clicked Sub menu item 1!</em>')
              },
              {
                type: 'menuitem',
                text: 'Sub menu item 2',
                icon: 'lock',
                onAction: () => editor.insertContent('&nbsp;<em>You clicked Sub menu item 2!</em>')
              }
            ]
          },
          {
            type: 'togglemenuitem',
            text: 'Toggle menu item',
            onAction: () => {
              toggleState = !toggleState;
              editor.insertContent('&nbsp;<em>You toggled a menuitem ' + (toggleState ? 'on' : 'off') + '</em>');
            },
            onSetup: (api) => {
              api.setActive(toggleState);
              return () => {};
            }
          }
        ];
        callback(items);
      }
    });

  },
  content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }'
});

This example configures a toolbar menu button with the label My Button that opens the specified menu when clicked. The top-level menu contains two items. The first menu item inserts content when clicked and the second menu item opens a submenu containing two menu items which insert content when clicked.

The fetch function is called when the toolbar menu button’s menu is opened. It is a function that takes a callback and passes it an array of menu items to be rendered in the drop-down menu. This allows for asynchronous fetching of the menu items.

Using onSetup

onSetup is a complex property. It takes a function that is passed the component’s API and should return a callback that is passed the component’s API and returns nothing. This occurs because onSetup runs whenever the component is rendered, and the returned callback is executed when the component is destroyed. This is essentially an onTeardown handler, and can be used to unbind events and callbacks.

To clarify, in code onSetup may look like this:

onSetup: (api) => {
  // Do something here on component render, like set component properties or bind an event listener

  return (api) => {
    // Do something here on teardown, like unbind an event listener
  };
};

To bind a callback function to an editor event use editor.on(eventName, callback). To unbind an event listener use editor.off(eventName, callback). Any event listeners should be unbound in the teardown callback. The only editor event which does not need to be unbound is init e.g. editor.on('init', callback).

  • The callback function for editor.off() should be the same function passed to editor.on(). For example, if a editorEventCallback function is bound to the NodeChange event when the button is created, onSetup should return (api) => editor.off('NodeChange', editorEventCallback).

  • If onSetup does not have any event listeners or only listens to the init event, onSetup can return an empty function e.g. return () => {};.