Repository navigation
Extending the Editor
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.
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.
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.initorToolbar.prototype.init) has no effect. Override functions that run again later:menu.funct(each time a menu opens),ui.menus.createPopupMenu(each context menu), theinitof 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.
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 anActionand registers it withput(name, action). A key that ends in...is registered without the dots; the menu label ismxResources.get(name)with the dots appended, the convention for actions that open a dialog. -
Actions.prototype.get(name)returns the action ornull. Replacingfunctchanges what its menu items and key bindings do (the Trello plugin does this forexit); toolbar buttons that already exist keep calling the function they were created with. -
Action(anmxEventSource) hassetEnabled/isEnabledandsetVisible/isVisible, which firestateChangedso that toolbar buttons update.setToggleAction(true)andsetSelectedCallback(fn)add a checkmark in menus (seenumber.js).shortcutis only the text shown in menus and tooltips. -
EditorUi.prototype.updateActionStatesenables and disables the built-in actions after every selection and model change. For your own actions, listen to the selection model (see Plugins), or replaceisEnabled, asActions.prototype.initdoes 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.jsanddiagramly/EditorUi.jsrather thanActions.js.
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.
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.
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.
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.
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:
- Create
js/diagramly/sidebar/Sidebar-MyLib.jswithSidebar.prototype.addMyLibPalette, callingthis.setCurrentSearchEntryLibrary('mylib')and thenthis.addPaletteFunctions('mylib', 'My Library', false, [...]), asSidebar-DFD.jsdoes. - Call
this.addMyLibPalette()inSidebar.prototype.initPalettes. - Add
{id: 'mylib'}toSidebar.prototype.configuration, and an entry toupdateEntriesto list it in More Shapes. - Add the file to
js/diagramly/Devel.jsand to the sidebar file list ofetc/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).
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).
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.
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 | 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).
Describes the dev branch of jgraph/drawio as of release 32.3.0 (October 2026). Internal JavaScript APIs change between releases; check the code of the version you use. · Questions: Discussions · Bugs: Issues · Vulnerabilities: report privately · User docs: drawio.com/docs
Get started
Concepts
Guides
- Self-hosting
- Configuration
- Storage backends
- OneDrive app registration
- Embedding
- Diagrams in GitHub
- Plugins
- Extending the editor
- Shapes and stencils
- Import and export
- Desktop app
- Internationalization
Reference
Project
Elsewhere