Developing plugins
A plugin is a single .js file. It changes the look or features of mind3D without touching the core of the app. Plugins can be switched on and off in the app via the 🧩 button on the start screen – without restarting. Everything a plugin registers through the API is removed automatically when it is switched off.
Applies to both versions (Android and USB/desktop) – the plugin code is identical.
Template to copy: plugin-vorlage.js (download) Detailed examples in the app assets: plugins/farbschema.js (design), plugins/projekte.js (own views, data in the mind map's plugin area), plugins/ansicht3d.js (full-screen view with a lazily loaded library) and plugins/markdown-import.js (import format)
Skeleton
mind3D.plugins.register({
id: "my-plugin", // unique, only a-z 0-9 . _ -
name: "My plugin",
version: "1.0.0",
author: "Name",
icon: "✨", // emoji in the plugin list
description: "What the plugin does.",
apiVersion: 1, // API version the plugin was written for
settings: [ /* optional, see below */ ],
activate(api) {
// Register everything here. Runs when switched on and on every page start.
},
deactivate(api) {
// Optional. Registrations made through the API are removed automatically.
},
});
activate runs on both pages of the app. api.page tells you where:
api.page | Page |
|---|---|
"menu" | start screen / project list |
"map" | open mind map |
Functions that make no sense on a page (e.g. a toolbar button on the menu) simply do nothing there.
API overview
All functions that register something return a function that removes the registration early. Normally you don't need it.
Appearance
| Call | Effect |
|---|---|
api.theme.set({ accent, accent2, colors, css, nodeTypes, palette }) | change colours, calling again = replace. Details below. |
api.theme.clear() | remove own colours |
api.ui.addStyle(css) | insert own CSS (e.g. buttons, dialogs) |
api.background.replace(factory) | replace the background (see below) |
Theme in detail
api.theme.set({
accent: "#a07cff", // colours the ENTIRE design: buttons, logo, borders, nodes, connections
accent2: "#ff4fd8", // optional: gradient accent → accent2 (null = single colour)
colors: { text: "#ffffff" }, // individual canvas colours from COLORS
css: { "--accent-border": "#333" }, // individual CSS variables
nodeTypes: { text: { card: "#101010", hdr: "#181818" } },
palette: ["#ff0000", "#00ff00"], // colour picker for borders (replaces it)
paletteExtra: ["#a07cff"], // extra colours at the front of the picker
});
From accent the app automatically derives all shades (bright, muted, dark, tinted borders/hover areas) and picks black or white as the text colour on accent buttons. The CSS variables for this are --accent, --accent-rgb, --accent-bright, --accent-deep, --accent-border, --on-accent, --accent-2 and --accent-gradient – your own plugin CSS can use them too. The values set last are applied immediately on the next start (no flash).
Drawing on the mind map
api.render.before((ctx, info) => { … }); // before everything else
api.render.after((ctx, info) => { … }); // on top of everything
api.render.nodeBefore((ctx, node, rect, info) => { … }); // behind each node
api.render.nodeAfter((ctx, node, rect, info) => { … }); // on top of each node
api.render.redraw(); // request a redraw
ctx– canvas 2D context in CSS pixels (state is saved/restored automatically)info–{ cssW, cssH, viewport, nodes, selectedId }rect– screen rectangle of the node{ x1, y1, x2, y2, z, selected }(z= zoom)
The mind map only redraws when something changes. For animations, call api.render.redraw() regularly (e.g. with api.every(33, …)).
Own background
api.background.replace((canvas, { viewport, page }) => {
const ctx = canvas.getContext("2d"); // or "webgl"
return {
resize(w, h, dpr) { … }, // required: size in CSS pixels + pixel density
start() { … }, // start the animation
destroy() { … }, // end the animation (or stop())
};
});
The canvas is fresh on every replacement. viewport (zoom, panX, panY) only exists on the mind map page. When switched off, the default space background returns.
Own node types
api.nodes.registerType({
type: "checklist", // not: mindmap, text, image, sound, document
label: "Checklist", // name in the ➕ menu and in the editor
icon: "☑",
colors: { card: "#0a1a10", hdr: "#0d2818" },
create(node) { api.data.set(node, { items: [] }); }, // when created
draw(ctx, node, rect) { … }, // draw the content area (rect as above, without header)
open(node) { return false; }, // double-click; true = handled, otherwise editor
editor: {
render(container, node) { … }, // add own fields to the node editor
save(container, node) { … }, // take over values on save
},
});
The type appears in the ➕ menu automatically. If the plugin is switched off later, these nodes are kept along with their data and appear as a “Plugin not active” placeholder.
Data
| Call | Stored |
|---|---|
api.data.get(node) / api.data.set(node, obj) | on the node, in the project (travels with export/import). After set, commit with api.map.save(). |
api.storage.get(key, fallback) / api.storage.set(key, value) | per device, across projects |
api.settings.get(key) / api.settings.set(key, value) | settings (see below) |
Security mode (api.security, read only): mode() → "standard" | "global" | "tresor", protectsMindmaps() → true in “Encrypt everything” mode, isUnlocked(). api.storage and api.settings live unencrypted in localStorage. If protectsMindmaps() returns true, no content or names of mind maps may end up there. Keep caches in memory only in that case (example: plugins/projekte.js). Always read and write mind maps themselves through api.mindmaps/api.map: these paths encrypt automatically. Vault mind maps are not visible to plugins.
The open mind map (api.map, only on "map", otherwise null)
| Call | |
|---|---|
api.map.nodes() | nodes of the current level |
api.map.allNodes() | all nodes of the project |
api.map.selected() / api.map.select(node) | selection |
api.map.addNode(type, text, x?, y?) | create a node (without position: centre of the screen) |
api.map.save() | commit a change: undo step + autosave + redraw |
api.map.viewport() | { zoom, panX, panY, worldToScreen(), screenToWorld() } |
api.map.navigateTo(id) | jump straight to a node's level and select it (zoom hysteresis is set correctly) |
api.map.suspend() / resume() | pause/resume the 2D view (for full-screen views like the 3D view) |
api.map.state / api.map.app | internal access – no stable contract, emergencies only |
Managing mind maps (api.mindmaps, on both pages)
Reading and writing through the app's normal saving logic – in the mind map JSON format.
| Call | |
|---|---|
await api.mindmaps.list() | mind maps of the signed-in user ([{ name, last_modified, saves }]) |
await api.mindmaps.load(name) / save(name, data) | load / save mind map JSON |
await api.mindmaps.create(name) | new mind map, returns the actual (possibly numbered) name |
api.mindmaps.open(name) | open in the editor (start screen only) |
api.mindmaps.root(data) / children(data, id) | find nodes |
api.mindmaps.addNode(data, { text, parent_id, node_type, body_text, plugin_data }) | create a node (position automatic) |
api.mindmaps.removeNode(data, id) | remove a node with its child nodes and connections |
api.mindmaps.meta(data) / setMeta(data, obj) | plugin area in the JSON: data.plugins[pluginId] |
Basic rule for data: everything that should be visible without the plugin belongs in normal nodes (text, child nodes). Plugin-specific extra information belongs in the plugin area plugins.<id>. The app passes this area through unchanged when loading and saving, even while the plugin is switched off. So every mind map stays readable without the plugin, and nothing is lost with it.
In the open mind map: api.map.meta() / api.map.setMeta(obj), then api.map.save().
Events
api.events.on("map:loaded", (state) => …);
api.events.on("map:changed", (state) => …); // after every change
api.events.on("map:saved", (state) => …);
api.events.on("node:created", (node) => …);
api.events.on("node:deleted", (node) => …);
api.events.on("node:selected", (nodeOrNull) => …);
api.events.on("level:changed", (contextNodeOrNull) => …); // level entered/left
api.events.on("menu:projectsShown", (username) => …); // project list shown
User interface
| Call | |
|---|---|
api.ui.toast(text, "ok" | "error") | short notice |
api.ui.dialog({ title, message, content, buttons }) | dialog → Promise with the button's value. buttons: [{ label, value, primary?, danger? }], content: own DOM element |
api.ui.confirm(title, message, okLabel) | yes/no → Promise<boolean> |
api.ui.addToolbarButton({ icon, title, onClick }) | button in the mind map toolbar |
api.ui.addStartButton({ icon, title, onClick }) | round button on the start screen |
api.ui.addProjectTab({ id, label, icon, render(el, { username }) }) | own tab next to “Mindmaps” in the project list |
api.ui.selectProjectTab(id) | select a tab (null = “Mindmaps”) |
api.ui.decorateMindmapCard((cardEl, entry, username) => …) | add to the cards of the mind map list |
api.ui.addImportOption({ icon, label, accept, directory, onFile(file, files) }) | own import format in the “New project” dialog; onFile returns { name, data } (mind map JSON). directory: true = folder selection (all files in files, hidden automatically on Android) |
api.ui.onBack(() => true/false) | intercept the Android back key; true = handled |
api.ui.addElement(el, parent?) | attach any element |
api.keys.add("ctrl+k", fn) | keyboard shortcut (ctrl, shift, alt; not while typing text) |
Automatic clean-up
| Call | |
|---|---|
api.every(ms, fn) / api.after(ms, fn) | timers that are stopped when switched off |
api.listen(target, type, fn) | DOM listener that is removed when switched off |
api.onDispose(fn) | own clean-up |
Important for switching on/off without restarting: don't use your own setInterval, addEventListener etc. directly, use the variants above – otherwise they keep running after the plugin is switched off.
Language
The app speaks English and German. The current language is in document.documentElement.lang ("en" or "de"). Give your plugin's texts in both languages and pick them by this value; the app only translates its own texts, not those of your plugin.
Settings
The app builds the settings dialog (⚙ in the plugin list) automatically:
settings: [
{ key: "colour", type: "color", label: "Colour", default: "#00d4ff" }, // picker + hex field
{ key: "preset", type: "swatches", label: "Preset", default: "blue", // colour swatches
options: [{ value: "blue", label: "Blue", color: "#4d8dff" },
{ value: "red", label: "Red", color: "#ff4d5e" }] },
{ key: "strength", type: "range", label: "Strength", default: 0.5, min: 0, max: 1, step: 0.05 },
{ key: "count", type: "number", label: "Count", default: 3, min: 1 },
{ key: "on", type: "boolean", label: "Enabled", default: true }, // switch
{ key: "mode", type: "select", label: "Mode", default: "a",
options: [{ value: "a", label: "Variant A" }, { value: "b", label: "Variant B" }] },
{ key: "text", type: "text", label: "Caption", default: "", hint: "Optional hint" },
]
Changes take effect immediately: api.settings.onChange((key, value, all) => …).
colorshows a colour picker and a hex field (#rrggbb, also the shortrgbform; invalid input is marked and not applied).swatchesshows rounded colour swatches; thevalueof the chosen swatch is stored.booleanappears as a switch.
Show a field only under a condition: { key: "colour2", type: "color", label: "Second colour", showIf: { gradient: true } }. If the plugin changes a setting itself (api.settings.set), the open dialog follows.
The app's security rules (apply to plugins too)
- No network: the Content Security Policy blocks any loading from or sending to the internet (
fetch, images, scripts from servers). Plugins only work with local data and files fromplugins/. Images/sounds asdata:orblob:work. - Allowed are own scripts,
eval/new Functionand inline styles (api.ui.addStyle). - Never insert user text as HTML unchecked: use
textContentor escape it – including quotes if the value ends up in an attribute (value="…"). - Don't store mind map data past the core (no own
localStorage/IndexedDB for content). Otherwise it ends up on the device as plain text in high-security mode. - Own import formats hand their data to the core; there it goes through the import check (
js/map-validate.js) and images through the metadata clean-up (js/media-clean.js).
Stability
- Every call into a plugin is guarded. An error does not crash the app.
- After 5 errors a plugin is switched off automatically (with a notice).
- If
activatefails, everything already registered is rolled back. apiVersionhigher than the app's → the plugin is shown as “requires a newer version”.
Installing and distributing
- From a file: 🧩 → “Install plugin from file” → choose the
.js. Installing the sameidagain = update.