Skip to content

Extending the Editor

David Benson edited this page Oct 7, 2026 · 1 revision

This page shows how the editor UI is assembled from actions, menus, the toolbar, the format panel, the sidebar and dialogs, and where to hook into each of them. It is for plugin authors and for developers who fork and customise the editor. For a plugin tutorial and ready-made snippets, start with Plugins.

How the UI is put together

App.main creates the editor with new App(new Editor(...)) (App.js). App extends EditorUi, whose constructor (grapheditor/EditorUi.js) builds the UI in this order:

Step Code Result
Actions this.actions = new Actions(this) Actions.prototype.init registers every action by name
Menus this.menus = this.createMenus() Menus.prototype.init registers every menu by name
Containers this.createDivs() menubarContainer, toolbarContainer, sidebarContainer, formatContainer, diagramContainer
Widgets this.createUi() this.menubar (Menus.prototype.createMenubar), this.sidebar (createSidebar), this.format (createFormat), this.toolbar (createToolbar); all four are null in chromeless views
Keys this.keyHandler = this.createKeyHandler(editor) an mxKeyHandler with the default bindings

The draw.io layer adds to all of these by wrapping the generic functions: diagramly/Menus.js wraps Menus.prototype.init and registers most draw.io actions and menus there, diagramly/EditorUi.js and diagramly/Editor.js extend EditorUi, Editor and the format panels, and Minimal.js and Simple.js install the themes. The full start-up sequence is in Architecture.

The override pattern

All extension in draw.io, by the code itself and by plugins, follows one idiom: keep a reference to the original function, replace it, and call the original from the replacement.

var graphIsCellVisible = graph.isCellVisible;

graph.isCellVisible = function(cell)
{
	return graphIsCellVisible.apply(this, arguments) && !isHiddenByMyPlugin(cell);
};

Timing decides where to override:

  • A fork changes or wraps prototypes in a source file that loads before App.main, so the change applies while the UI is built.
  • A plugin runs after the UI is built. Overriding a prototype function that has already run (for example Menus.prototype.init or Toolbar.prototype.init) has no effect. Override functions that run again later: menu.funct (each time a menu opens), ui.menus.createPopupMenu (each context menu), the init of the format panel classes (each refresh), or functions of the instance (graph, ui).
To change Where Plugin hook
What a command does Actions ui.actions.addAction, replace ui.actions.get(name).funct
Menu entries Menus ui.menus.addPluginMenuItems, wrap ui.menus.get(name).funct
Context menu Menus.prototype.createPopupMenu wrap ui.menus.createPopupMenu
Shortcuts EditorUi.prototype.createKeyHandler ui.keyHandler.bindAction, ui.altShiftActions
Classic toolbar Toolbar.prototype.init ui.toolbar.addItems
Format panel Format, *FormatPanel.prototype.init wrap the panel's init
Shape palettes Sidebar ui.sidebar.addPaletteFunctions
Dialogs EditorUi.prototype.showDialog new CustomDialog(...), ui.showDialog
Rendering and behaviour Graph, mxGraph and handlers override functions on ui.editor.graph

Many changes need no code at all: the editor configuration can hide menus (hideMenus) and menu items (hideMenuItems), rebind keys (keyboardShortcuts), change UI strings (resources), and set the default libraries, fonts, colours and styles. See Configuration and configure-diagram-editor.

Actions

An action is a named command shared by menus, toolbars, keyboard shortcuts and the format panel (Actions.js).

var action = ui.actions.addAction('myAction...', function(trigger, evt)
{
	// ...
}, null, null, Editor.ctrlKey + '+Shift+X');

ui.actions.get('myAction').funct(); // runs it
  • Actions.prototype.addAction(key, funct, enabled, iconCls, shortcut, visible) creates an Action and registers it with put(name, action). A key that ends in ... is registered without the dots; the menu label is mxResources.get(name) with the dots appended, the convention for actions that open a dialog.
  • Actions.prototype.get(name) returns the action or null. Replacing funct changes what its menu items and key bindings do (the Trello plugin does this for exit); toolbar buttons that already exist keep calling the function they were created with.
  • Action (an mxEventSource) has setEnabled/isEnabled and setVisible/isVisible, which fire stateChanged so that toolbar buttons update. setToggleAction(true) and setSelectedCallback(fn) add a checkmark in menus (see number.js). shortcut is only the text shown in menus and tooltips.
  • EditorUi.prototype.updateActionStates enables and disables the built-in actions after every selection and model change. For your own actions, listen to the selection model (see Plugins), or replace isEnabled, as Actions.prototype.init does for actions that need an enabled graph (action.isEnabled = isGraphEnabled).
  • draw.io actions that depend on the app layer (files, pages, export) are registered in diagramly/Menus.js and diagramly/EditorUi.js rather than Actions.js.

Menus

A menu is a named Menu object whose funct(menu, parent) fills an mxPopupMenu each time the menu opens (grapheditor/Menus.js).

// A submenu, defined like the built-in ones
ui.menus.put('myMenu', new Menu(function(menu, parent)
{
	ui.menus.addMenuItems(menu, ['myAction', '-', 'copy', 'paste'], parent);
}));

// Shown inside Extras
ui.menus.addPluginMenuItems('extras', ['-', 'myAction']);

var extras = ui.menus.get('extras');
var extrasFunct = extras.funct;

extras.funct = function(menu, parent)
{
	extrasFunct.apply(this, arguments);
	ui.menus.addSubmenu('myMenu', menu, parent, 'My Menu');
};
Function Purpose
Menus.prototype.put(name, menu), get(name) Register and look up menus
addMenuItem(menu, key, parent, trigger, sprite, label) One action; skipped if hidden by configuration, invisible, or disabled in a menu that hides disabled items
addMenuItems(menu, keys, parent, trigger, sprites) Several actions, '-' for a separator
addSubmenu(name, menu, parent, label) A registered menu as a submenu
addPluginMenuItems(menuName, items) Items appended to a named menu wherever it is shown (29.6.2 and later)
createPopupMenu(menu, cell, evt) The context menu: addPopupMenuItems calls addPopupMenuHistoryItems, addPopupMenuEditItems, addPopupMenuStyleItems, addPopupMenuArrangeItems, addPopupMenuCellItems and addPopupMenuSelectionItems
createMenubar(container) The classic menu bar from Menus.prototype.defaultMenuItems (file, edit, view, arrange, extras, help)

Menu names you are likely to need: file, edit, view, arrange, insert, layout, extras, help, exportAs, importFrom, diagram (the main menu of the simple, sketch and min themes), pages and editCell. Search for this.put(' in both Menus.js files for the full list. The configuration key hideMenus removes entries from Menus.prototype.defaultMenuItems, and hideMenuItems fills Menus.prototype.hiddenMenuItems. ui.menubar.addMenu(label, funct) adds a top-level menu to the classic menu bar (import.js does this); the menu bar is not shown in the simple, sketch and min themes.

Keyboard shortcuts

EditorUi.prototype.createKeyHandler creates the mxKeyHandler (ui.keyHandler) and binds the default keys; diagramly/EditorUi.js wraps it for page navigation. isControlDown treats Cmd as Ctrl on macOS.

API Effect
keyHandler.bindAction(code, control, key, shift) Runs action key for the key code while the action is enabled. The action must exist when you bind.
keyHandler.bindKey, bindShiftKey, bindControlKey, bindControlShiftKey(code, funct) Binds a function directly (mxKeyHandler)
EditorUi.prototype.altShiftActions, ctrlAltShiftActions, ctrlAltActions, altActions Maps from key code to action name for Alt combinations, read on every key press
Editor.ctrlKey, Editor.altKey, Editor.shiftKey Platform names (Ctrl or ⌘, ...) for the shortcut text of an action

Shortcuts are not handled while a dialog is open, and most are ignored while a label is being edited. The keyboardShortcuts configuration key binds keys to action names at run time (resolved when the key is pressed, so plugin actions work). The default shortcuts are listed in shortcuts.svg (Help > Keyboard Shortcuts) and on drawio.com; update the file if your fork changes them.

Toolbar

The toolbar of the classic theme is a Toolbar (Toolbar.js) built by Toolbar.prototype.init:

Function Purpose
addItems(keys, container, noListeners, icons, minWidth) A button per action (or '-' for a separator) that follows the action's enabled and visible state
addItem(sprite, key, container, noListeners) One action button
addMenu(menu, label, icon, container) A button that opens a Menu
addSeparator(container, minWidth) A separator

Icons must be data: URIs; EditorUi.prototype.createToolbarButton ignores other values. The Editor.*Image constants (Editor.undoImage, Editor.checkmarkImage, ...) are data URIs. A minWidth (stored as data-min-width) hides the item when the window is narrower. The simple and sketch themes do not show this toolbar: their compact toolbars are built in Simple.js with EditorUi.prototype.createMenuItem(key, img) and createMenu(name, img), and have no extension point. In those themes ui.toolbar exists but is hidden.

Format panel

The format panel on the right is a Format (Format.js). Format.prototype.init listens to selection, model, editing and other changes and calls refresh, which clears the panel and lets immediateRefresh create the tabs for the current state:

Selection Tabs (panel classes)
Nothing selected Diagram (DiagramFormatPanel), Style (DiagramStylePanel)
Editing a label Text (TextFormatPanel)
Cells selected Style (StyleFormatPanel), Text (TextFormatPanel), Arrange (ArrangePanel)

Each panel extends BaseFormatPanel and fills this.container in its init. Because new panel objects are created on every refresh, wrapping the init of a panel class adds a section reliably, also from a plugin (see Plugins). diagramly/Editor.js itself wraps StyleFormatPanel.prototype.init this way to add the colour schemes and the property table.

Helpers on BaseFormatPanel.prototype:

Function Purpose
createPanel() A geFormatSection div
createTitle(title) A section title
createCollapsibleSection(title, defaultCollapsed) Returns {wrapper, contentDiv}
createOption(label, isCheckedFn, setCheckedFn, listener, fn) A checkbox row
createCellOption(label, key, defaultValue, enabledValue, disabledValue, fn, action, stopEditing, cells) A checkbox bound to a style key of the selected cells
addAction(div, name) A button that runs an action

this.editorUi.getSelectionState() describes the selection (cells, vertices, edges, style, ...); call ui.format.refresh() to rebuild the panel after a change it cannot see. The Property table of the Style tab lists the customProperties of the selected shapes and the entries of Editor.commonVertexProperties or Editor.commonEdgeProperties (both include Editor.commonProperties, copied when the code loads); add an entry to one of the latter two for a single style property. A new tab requires a fork, in Format.prototype.immediateRefresh.

Sidebar palettes

The shapes panel on the left is a Sidebar. The palette framework is in grapheditor/Sidebar.js, the draw.io palettes in js/diagramly/sidebar.

Function Purpose
addPalette(id, title, expanded, onInit, eager) A collapsible palette; onInit(content) fills it when it is first shown
addPaletteFunctions(id, title, expanded, fns) A palette from an array of entry functions
createVertexTemplateEntry(style, width, height, value, title, showLabel, showTitle, tags) An entry for one vertex, also added to the shape search under tags
createEdgeTemplateEntry(style, width, height, value, title, showLabel, tags, ...) An entry for one edge
createVertexTemplateFromCells(cells, width, height, title, ...) An entry for prepared cells (groups, containers, edges between them)
addEntry(tags, fn) Wraps a function that returns an entry and registers it with the search index
addStencilPalette(id, title, stencilFile, style, ignore, onInit, scale, tags, customFns, groupId) A palette with every shape of a stencil set
addImagePalette(id, title, prefix, postfix, items, titles, tags) A palette of image shapes
removePalette(id) Removes a palette

The draw.io layer (diagramly/sidebar/Sidebar.js) overrides Sidebar.prototype.init, which calls initPalettes to create every palette, hidden or not. Sidebar.prototype.configuration maps each library id to its palette ids ({id: 'dfd'}, or a prefix and libs for libraries split into several palettes), showEntries shows the libraries the user selected (the libs URL parameter, the saved settings, or Sidebar.prototype.defaultEntries), and updateEntries lists the libraries in the More Shapes dialog with preview images from images/sidebar-<id>.png. Each Sidebar-*.js file adds one Sidebar.prototype.add...Palette function, for example addDFDPalette in Sidebar-DFD.js.

To add a library in a fork:

  1. Create js/diagramly/sidebar/Sidebar-MyLib.js with Sidebar.prototype.addMyLibPalette, calling this.setCurrentSearchEntryLibrary('mylib') and then this.addPaletteFunctions('mylib', 'My Library', false, [...]), as Sidebar-DFD.js does.
  2. Call this.addMyLibPalette() in Sidebar.prototype.initPalettes.
  3. Add {id: 'mylib'} to Sidebar.prototype.configuration, and an entry to updateEntries to list it in More Shapes.
  4. Add the file to js/diagramly/Devel.js and to the sidebar file list of etc/build/build.xml, then rebuild (see Building).

New shapes used by the palette are covered in Shapes and stencils. From a plugin, use ui.sidebar.addPaletteFunctions and re-add the palette when the sidebar is rebuilt: Sidebar.prototype.refresh, which runs on the languageChanged and sidebarTitlesChanged events, calls init again (see Plugins). Users and deployments can add shapes without code through custom libraries (clibs URL parameter, libraries and defaultCustomLibraries configuration keys).

Dialogs and windows

EditorUi.prototype.showDialog(elt, w, h, modal, closable, onClose, noScroll, transparent, minSize, ignoreBgClick, persistenceKey) shows any element in a modal Dialog; hideDialog() closes the topmost one. Pass null as h to size the dialog to its content.

Class or function File Use
CustomDialog(editorUi, content, okFn, cancelFn, okButtonText, helpLink, buttonsContent, hideCancel, cancelButtonText, hideAfterOKFn, customButtons, marginTop) diagramly/Dialogs.js Your content with an OK and a Cancel button. If okFn returns a string, it is shown as an error.
FilenameDialog(editorUi, filename, buttonText, fn, label, validateFn, content, helpLink, closeOnBtn, cancelFn, hints) grapheditor/Editor.js A single text field. With the default closeOnBtn, it closes itself before calling fn.
ui.alert(msg, fn, width), ui.confirm(msg, okFn, cancelFn, okLabel, cancelLabel), ui.prompt(title, defaultValue, fn), ui.showError(title, msg, btn, fn, ...) EditorUi.js Message boxes
mxWindow(title, content, x, y, width, height, minimizable, movable, ...) mxgraph/src/util/mxWindow.js A floating, non-modal window (used by props.js and tags.js)

Dialogs should follow the dialog style guide: a plain <h3> title, geDialogSection, geDialogFormRow and geDialogCheckRow for layout, CSS variables instead of literal colours so that dark mode works, and auto height. Build the content with DOM methods and mxUtils.write; diagram text must never reach innerHTML unsanitised (see Security).

Strings

Every label in the UI is a resource key. mxResources.get(key, params, defaultValue) returns the string for the current language; action and menu names double as keys. In a fork, add new keys to src/main/webapp/resources/dia.txt (English) and translations to the dia_<lang>.txt files. A plugin adds its own keys with mxResources.parse('key=value'), and deployments override strings with the resources configuration key. Details, including how plugins ship translations, are in Internationalization.

UI themes

The theme decides where menus and tools live. It comes from the ui URL parameter or the user's saved choice (Extras > Theme); without either, the classic theme is used, except that on app.diagrams.net a narrow or touch screen gets simple (App.isSimpleThemePreferred). Editor.currentTheme holds the active theme and Editor.themes the installed ones; see also Architecture.

Editor.currentTheme Built by Menus Toolbar Format panel and shapes
kennedy (classic, also ui=dark) grapheditor Menu bar (defaultMenuItems) Toolbar.js Docked panels
atlas Classic layout with the geAtlas look; registered in Simple.js Menu bar Toolbar.js Docked panels
simple Simple.js Main menu button (diagram menu) with Settings for extras Compact toolbar from Simple.js Docked panels
sketch Simple.js diagram menu with Settings for extras Compact floating toolbar Floating windows
min Minimal.js diagram menu and buttons (EditorUi.initMinimalTheme) None Windows

Code that adds items by menu name (addPluginMenuItems, wrapping funct) works in every theme, because all menu buttons call the same Menu objects. Code that touches DOM elements of one theme (the toolbar, ui.menubar) only works there. Switching between kennedy and simple happens without a reload (EditorUi.prototype.doSetCurrentTheme fires currentThemeChanged and the menu bar is rebuilt), so do not keep references to menu bar elements. In development mode of this repository only the classic and min themes are available (see Getting started).

Plugin or fork?

Plugin Fork
Distribution One .js file, added per user or per deployment (Plugins) Your own build and deployment
Desktop app Not possible beyond the built-in plugins Your own build of drawio-desktop
Hook points Anything reachable from ui after start-up; no access to code that runs during construction Any code, including start-up, bundles and the server side
Updates Load into each new release; breaks when internal functions change Merge each release into your fork; conflicts where you changed core files
Typical use Extra actions, palettes, integrations, behaviour tweaks New UI concepts, removed features, different defaults that the configuration cannot express

Try the configuration first, then a plugin; fork when neither reaches the code you need to change. In a fork, prefer adding a new file that wraps existing functions (registered in Devel.js and build.xml, see Building) over editing the core files, which keeps merges small. Note that draw.io does not accept code pull requests (see Contributing).

See also

Clone this wiki locally