Skip to content

Coding Conventions

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

This page describes the house style of the draw.io code base as it is in mxgraph/src, js/grapheditor and js/diagramly. It is for anyone who maintains a fork or writes plugins and wants code that reads like the code around it and survives updates.

Language level

draw.io is plain browser JavaScript. There are no ES modules, no npm packages and no bundler in the app. Every file is a classic script that defines globals; development mode loads the sources one by one in a fixed order from Devel.js, and the build concatenates and minifies them with the Google Closure Compiler (see Building).

The jscomp tasks in etc/build/build.xml use compilationLevel="simple" and do not set a language mode, so the defaults of the bundled compiler (compiler.jar, v20220502) apply: the input language is ECMASCRIPT_NEXT (whatever that compiler version can parse) and the output is ES5. Newer syntax therefore builds and is transpiled (the few async functions show up as $jscomp helpers in app.min.js).

The house style is ES5 nevertheless:

Use Rather than
var let, const
function expressions assigned to properties class
mxUtils.bind(this, function() { ... }) arrow functions, Function.prototype.bind
String concatenation template literals
!= null / == null (null or undefined) !== null, ?., ??

Newer syntax appears in only a few files, chiefly ElectronApp.js, which only runs in the desktop app and is loaded there as a separate script. Built-in APIs of the supported browsers such as Map, WeakMap and Promise are used directly. HTTP requests go through mxXmlRequest, mxUtils.get / mxUtils.post and Editor.prototype.loadUrl. The editor code does not use 'use strict'.

Formatting

Rule Example
Indent with tabs
Opening braces on their own line (Allman style), also for else, try and catch see below
Single quotes for strings mxResources.get('ok')
One space after keywords and around binary operators if (cell != null && i < n)
Wrap long lines at roughly 80 to 100 columns and indent the continuation with one more tab
Parentheses around conditions in ternaries (cells.length > 0) ? cells : null
/**
 * Returns the link target for the given cell.
 */
Graph.prototype.getLinkTargetForCell = function(cell)
{
	if (cell.value != null && typeof(cell.value) == 'object')
	{
		return cell.value.getAttribute('linkTarget');
	}
	else
	{
		return null;
	}
};

Files and headers

Source files start with a copyright block. Older files name only the first holder; current files look like this:

/**
 * Copyright (c) 2006-2026, JGraph Holdings Ltd
 * Copyright (c) 2006-2026, draw.io AG
 */

The three layers are written differently:

  • mxgraph/src: one class per file (mxGraph.js, mxUtils.js), documented in NaturalDocs style (see Comments).
  • js/grapheditor: a few large files, each defining one or more global constructors at the top level (Format = function(editorUi, container), var ErrorDialog = function(...)) followed by Format.prototype.method = function blocks.
  • js/diagramly: the app layer. Files that change grapheditor classes (Editor.js, EditorUi.js) are wrapped in (function() { ... })();, so their helper functions and saved originals stay local. Files that add classes (DrawioFile.js, LocalFile.js, the storage clients) define globals like grapheditor.

The layers depend in one direction: mxgraph → grapheditor → diagramly. The graph library never refers to editor classes. Grapheditor code that needs an app-level global checks that it exists first, for example typeof mxSettings !== 'undefined', so that bundles without that file, such as the viewer, keep working. Code that only some bundles contain is detected the same way (typeof window.DriveClient === 'function'). See Architecture.

Naming

Kind Convention Examples
Classes PascalCase; mx prefix in the graph library EditorUi, DrawioFile, mxGraphModel
Methods and fields camelCase; is/get/set/create/add prefixes isCustomLink, getLinkForCell, createFormat
Constants UPPER_SNAKE_CASE on the constructor App.MODE_DEVICE, App.ERROR_TIMEOUT
Global switches camelCase static fields, set by Editor.configure or URL parameters Editor.fastRendering, Editor.enableServiceWorker, DrawioFile.SYNC
Instance defaults fields on the prototype, overridable per instance Graph.prototype.linkPolicy, mxGraphView.prototype.modelCoordinates
Event names camelCase strings 'fileLoaded', 'pageSelected', 'darkModeChanged'
Saved originals in overrides class + method name var editorUiCreateFormat = EditorUi.prototype.createFormat;
CSS classes ge prefix geDialog, geBtn, geDialogSection
Global configuration window.NAME = window.NAME || default in Init.js, so PreConfig.js can set it first window.PROXY_URL, window.ALLOW_CUSTOM_PLUGINS

There is no underscore convention for private members. Helpers that are not part of an API are local functions inside a closure or a method.

Classes and inheritance

Classes are constructor functions. mxUtils.extend(ctor, superCtor) sets up the prototype chain; the constructor calls the super constructor with call, and methods call the super method through the prototype with apply. From LocalFile.js:

LocalFile = function(ui, data, title, temp, fileHandle, desc, editable)
{
	DrawioFile.call(this, ui, data);

	this.title = title;
	this.mode = (temp) ? null : App.MODE_DEVICE;
	// ...
};

mxUtils.extend(LocalFile, DrawioFile);

LocalFile.prototype.isAutosave = function()
{
	return this.fileHandle != null && !this.invalidFileHandle &&
		DrawioFile.prototype.isAutosave.apply(this, arguments);
};

Callbacks that need this use mxUtils.bind(this, function() { ... }); code that also needs the outer object inside a nested function keeps it in a local such as var ui = this; or var graph = this.editor.graph;.

Overriding across layers

The app layer changes the generic editor by wrapping its methods, not by editing or copying them. Keep a reference to the original, call it, and add the new behaviour around it. From diagramly/EditorUi.js:

// Avoid: a copy loses every later change to the base method
EditorUi.prototype.createFormat = function(container)
{
	var format = new Format(this, container);
	// new behaviour
	return format;
};

// Prefer: wrap the original
var editorUiCreateFormat = EditorUi.prototype.createFormat;

EditorUi.prototype.createFormat = function(container)
{
	var format = editorUiCreateFormat.apply(this, arguments);

	this.editor.graph.addListener('viewStateChanged', mxUtils.bind(this, function(evt)
	{
		if (this.editor.graph.isSelectionEmpty())
		{
			format.refresh();
		}
	}));

	return format;
};

Methods of the other classes are wrapped the same way; diagramly/Editor.js, for example, wraps Graph.prototype.getLinkTitle. Plugins and forks should follow the same pattern; it keeps your change small and lets upstream fixes reach you. Hooks that exist only to be overridden are documented as such ("Hook for ...") and usually do nothing in the base class, for example Graph.prototype.customLinkClicked. See Extending the editor and Plugins.

User interface strings

Every visible string comes from the resource bundle:

mxUtils.write(hd, mxResources.get('editDiagram'));
page.setName(mxResources.get('pageWithNumber', [i + 1]));   // pageWithNumber=Page-{1}
throw new Error(mxResources.get('pageNotFound') || 'Page not found');

mxResources.get(key, params, defaultValue) replaces {1}, {2}, ... with the values in params. The English strings are in src/main/webapp/resources/dia.txt, the translations in dia_<lang>.txt. Do not hard-code English text in the UI. Adding keys is described in Internationalization.

Building the DOM

Build elements with document.createElement and add text with mxUtils.write (a text node), never by concatenating values into innerHTML. The library helpers cover the common cases: mxUtils.button(label, funct), mxUtils.br(parent), mxUtils.para(parent, text), mxUtils.link(parent, text, funct). From EditDiagramDialog in grapheditor/Dialogs.js:

var div = document.createElement('div');
var snapshot = editorUi.getDiagramSnapshot();

var hd = document.createElement('h3');
mxUtils.write(hd, mxResources.get('editDiagram'));
div.appendChild(hd);

var textarea = document.createElement('textarea');
textarea.setAttribute('wrap', 'off');
textarea.value = mxUtils.getPrettyXml(snapshot.node);
div.appendChild(textarea);

Where HTML strings are unavoidable, escape every value with mxUtils.htmlEntities or pass markup through Graph.sanitizeHtml. The full list of sinks and helpers is in Security.

Styling belongs in src/main/webapp/styles/grapheditor.css with ge classes and the theme variables, so that dark mode and custom colour schemes work. For dialogs, follow the dialog style guide (below).

Events

DOM events go through mxEvent, which also normalises mouse, touch and pointer input:

mxEvent.addListener(button, 'click', mxUtils.bind(this, function(evt)
{
	if (mxEvent.isShiftDown(evt))
	{
		// ...
	}

	mxEvent.consume(evt);
}));

mxEvent.addGestureListeners(elt, startListener, moveListener, endListener);

Objects that publish events extend mxEventSource. Fire an mxEventObject with the event name followed by key/value pairs, and read the values with getProperty:

this.ui.editor.fireEvent(new mxEventObject('pagesPatched', 'patches', patches));

editor.addListener('pagesPatched', function(sender, evt)
{
	var patches = evt.getProperty('patches');
});

Listeners that outlive a dialog or panel must be removed again (removeListener, mxEvent.removeListener), typically in a destroy method as in EditorUi.prototype.destroy.

Model changes are grouped in one transaction so that they are undone as one step:

var model = graph.getModel();
model.beginUpdate();

try
{
	// change cells
}
finally
{
	model.endUpdate();
}

See Graph model for transactions and change events.

Comments

Functions are preceded by a /** ... */ block. In grapheditor and diagramly it is a short prose description:

/**
 * Returns the link for the given cell.
 */
Graph.prototype.getLinkForCell = function(cell, allowUnsafe)

The graph library uses NaturalDocs keywords:

/**
 * Function: write
 *
 * Creates a text node for the given string and appends it to the given
 * parent. Returns the text node.
 *
 * Parameters:
 *
 * parent - DOM node to append the text node to.
 * text - String representing the text to be added.
 */

Inline comments explain why the code does something, especially for browser workarounds, security checks and ordering constraints. Where a change fixes a reported problem, the comment often cites the public issue, for example [jgraph/drawio-desktop#2161].

Errors and logging

Situation Convention
A failure is harmless (optional feature, best-effort cleanup) try { ... } catch (e) { // ignore }
The user must be told ui.handleError(e) or ui.handleError(e, title), which shows the message, escaped, in an error dialog
Unexpected error worth reporting EditorUi.logError(message, url, line, column, err)
Diagnostic output EditorUi.debug(...), which prints only when the test=1 URL parameter is set (see Debugging)
Direct console output Guarded with if (window.console != null)

EditorUi.logError always writes to console.error (unless quiet) and sends a report only from the draw.io hosts (EditorUi.enableLogging), never from development mode. Reports use EditorUi.getLogUrl, which keeps the names but not the values of URL parameters and replaces the location hash (which can hold a file ID or a whole diagram) with a hash value, so no diagram data, file ID or token is logged. Do the same in your own code: never put diagram content, user names, file IDs, URLs or tokens into logs, URLs or error reports.

Configuration switches

A new option of the editor configuration is read in Editor.configure in diagramly/Editor.js and stored in a static switch:

if (config.fastRendering != null)
{
	Editor.fastRendering = config.fastRendering;
}

Boolean options also appear as toggles in the Configuration dialog (toggleGroups in ConfigEditor.js). The options themselves are documented at Configure the diagram editor; see also Configuration.

Files you do not edit

These files are generated or come from elsewhere. Change their sources or replace them as a whole:

Files Source
js/app.min.js, viewer.min.js, viewer-static.min.js, extensions.min.js, shapes-*.min.js, stencils.min.js, integrate.min.js, orgchart.min.js Built by etc/build/build.xml from the sources (Building)
mxgraph/mxClient.js Minified build of mxgraph/src, updated with each release; the build in this repository uses it as it is and does not regenerate it
js/elk/, js/mermaid/, js/plantuml/, js/vsd/, js/libavoid-js/libavoid.min.js Bundles built outside this repository
js/sanitizer/purify.min.js, js/deflate/pako.min.js, js/jszip/, ... Third-party libraries, replaced by upgrading
service-worker.js, workbox-*.js Generated for the release

Because mxgraph/mxClient.js is not rebuilt by the public build, changes to mxgraph/src are visible in development mode (?dev=1) but not in the bundles you build.

Adding a new file

A new source file must be added in two places: to Devel.js (in the right position, since files depend on the ones loaded before them) for development mode, and to every bundle in etc/build/build.xml that should contain it. Some files are in more than one bundle; Pages.js, for example, is in the viewer, the base and the app bundle. Details in Building.

Code review guidelines

docs/code-review.md is the checklist the team reviews changes against. In short:

  • Functionality: the change does what it claims, the reason for it is valid, and edge cases and errors are handled.
  • Readability: organised, consistently named and indented, with comments for anything non-obvious.
  • Structure: modular, follows the existing patterns, functions of reasonable size, one responsibility each.
  • Performance: no needless loops or leaks, suitable data structures, caching where it helps.
  • Errors and logging: errors caught at the right level, messages clear and actionable.
  • Security: input validated and sanitised, no new injection or access-control problems (see Security).
  • Tests: existing tests still pass, new tests where needed.
  • Reuse and dependencies: existing components reused, dependencies maintained, secure and necessary.
  • Standards and documentation: project conventions followed, functions and complex modules documented.

Dialog style guide

docs/dialog-style-guide.md describes how dialogs built with CustomDialog should look. The main rules:

  • Pass null as the height to showDialog so the dialog sizes itself; do not pass a minSize unless the content needs resizing.
  • Group controls in geDialogSection containers and wrap each control in a row class (geDialogFormRow, geDialogCheckRow, geDialogInlineFields); row spacing comes from CSS, so do not add margins per row.
  • Use addCheckbox(..., useCheckRow = true) for checkboxes and addAdvancedSection(parent) for options that are rarely needed.
  • Titles are plain <h3> elements; never set fonts, sizes or colours inline. Muted text uses geDialogHint.
  • Colours come from the theme variables in styles/grapheditor.css and light-dark(), never from literals in JavaScript.
  • Test with ?dev=1&ui=classic, with single-page and multi-page diagrams.

See also

Clone this wiki locally