Plugins entwickeln
Ein Plugin ist eine einzelne .js-Datei. Sie verändert Aussehen oder Funktionen von mind3D, ohne den Kern der App anzufassen. Plugins lassen sich in der App über den 🧩-Button im Startbildschirm ein- und ausschalten – ohne Neustart. Alles, was ein Plugin über die API registriert, wird beim Ausschalten automatisch wieder entfernt.
Gilt für beide Versionen (Android und USB/Desktop) – der Plugin-Code ist identisch.
Vorlage zum Kopieren: plugin-vorlage.js (zum Herunterladen) Ausführliche Beispiele in den App-Assets: plugins/farbschema.js (Design), plugins/projekte.js (eigene Ansichten, Daten im Plugin-Bereich der Mindmap), plugins/ansicht3d.js (Vollbild-Ansicht mit nachgeladener Bibliothek) und plugins/markdown-import.js (Import-Format)
Grundgerüst
mind3D.plugins.register({
id: "mein-plugin", // eindeutig, nur a-z 0-9 . _ -
name: "Mein Plugin",
version: "1.0.0",
author: "Name",
icon: "✨", // Emoji in der Plugin-Liste
description: "Was das Plugin macht.",
apiVersion: 1, // API-Version, für die das Plugin geschrieben ist
settings: [ /* optional, siehe unten */ ],
activate(api) {
// Hier alles registrieren. Läuft beim Einschalten und bei jedem Seitenstart.
},
deactivate(api) {
// Optional. Registrierungen über die API werden automatisch entfernt.
},
});
activate läuft auf beiden Seiten der App. api.page sagt, wo:
api.page | Seite |
|---|---|
"menu" | Startbildschirm / Projektliste |
"map" | geöffnete Mindmap |
Funktionen, die auf einer Seite keinen Sinn haben (z. B. Toolbar-Button im Menü), tun dort einfach nichts.
API-Übersicht
Alle Funktionen, die etwas registrieren, geben eine Funktion zurück, mit der man die Registrierung vorzeitig wieder entfernen kann. Normalerweise braucht man das nicht.
Aussehen
| Aufruf | Wirkung |
|---|---|
api.theme.set({ accent, accent2, colors, css, nodeTypes, palette }) | Farben ändern, erneut aufrufen = ersetzen. Details unten. |
api.theme.clear() | eigene Farben entfernen |
api.ui.addStyle(css) | eigenes CSS einfügen (z. B. Buttons, Dialoge) |
api.background.replace(factory) | Hintergrund austauschen (siehe unten) |
Theme im Detail
api.theme.set({
accent: "#a07cff", // färbt das GESAMTE Design: Buttons, Logo, Rahmen, Knoten, Verbindungen
accent2: "#ff4fd8", // optional: Farbverlauf accent → accent2 (null = einfarbig)
colors: { text: "#ffffff" }, // einzelne Canvas-Farben aus COLORS
css: { "--accent-border": "#333" }, // einzelne CSS-Variablen
nodeTypes: { text: { card: "#101010", hdr: "#181818" } },
palette: ["#ff0000", "#00ff00"], // Farbauswahl für Rahmen (ersetzt sie)
paletteExtra: ["#a07cff"], // zusätzliche Farben vorne in der Farbauswahl
});
Aus accent leitet die App automatisch alle Abstufungen ab (hell, gedämpft, dunkel, getönte Rahmen/Hover-Flächen) und wählt Schwarz oder Weiß als Textfarbe auf Akzent-Buttons. Die CSS-Variablen dafür sind --accent, --accent-rgb, --accent-bright, --accent-deep, --accent-border, --on-accent, --accent-2 und --accent-gradient – eigenes Plugin-CSS kann sie ebenfalls verwenden. Die zuletzt gesetzten Werte werden beim nächsten Start sofort angewendet (kein Aufblitzen).
Zeichnen auf der Mindmap
api.render.before((ctx, info) => { … }); // vor allem anderen
api.render.after((ctx, info) => { … }); // über allem
api.render.nodeBefore((ctx, node, rect, info) => { … }); // hinter jedem Knoten
api.render.nodeAfter((ctx, node, rect, info) => { … }); // über jedem Knoten
api.render.redraw(); // Neuzeichnen anfordern
ctx– Canvas-2D-Kontext in CSS-Pixeln (Zustand wird automatisch gesichert/zurückgesetzt)info–{ cssW, cssH, viewport, nodes, selectedId }rect– Bildschirm-Rechteck des Knotens{ x1, y1, x2, y2, z, selected }(z= Zoom)
Die Mindmap zeichnet nur neu, wenn sich etwas ändert. Für Animationen regelmäßig api.render.redraw() aufrufen (z. B. mit api.every(33, …)).
Eigener Hintergrund
api.background.replace((canvas, { viewport, page }) => {
const ctx = canvas.getContext("2d"); // oder "webgl"
return {
resize(w, h, dpr) { … }, // Pflicht: Größe in CSS-Pixeln + Pixeldichte
start() { … }, // Animation starten
destroy() { … }, // Animation beenden (oder stop())
};
});
Der Canvas ist bei jedem Austausch frisch. viewport (zoom, panX, panY) gibt es nur auf der Mindmap-Seite. Beim Ausschalten kommt der Standard-Weltraum zurück.
Eigene Knotentypen
api.nodes.registerType({
type: "checkliste", // nicht: mindmap, text, image, sound, document
label: "Checkliste", // Name im ➕-Menü und im Editor
icon: "☑",
colors: { card: "#0a1a10", hdr: "#0d2818" },
create(node) { api.data.set(node, { items: [] }); }, // beim Anlegen
draw(ctx, node, rect) { … }, // Inhaltsbereich zeichnen (rect wie oben, ohne Kopf)
open(node) { return false; }, // Doppelklick; true = selbst erledigt, sonst Editor
editor: {
render(container, node) { … }, // eigene Felder in den Knoten-Editor einfügen
save(container, node) { … }, // beim Speichern Werte übernehmen
},
});
Der Typ erscheint automatisch im ➕-Menü. Ist das Plugin später aus, bleiben diese Knoten samt Daten erhalten und erscheinen als Platzhalter „Plugin nicht aktiv“.
Daten
| Aufruf | Gespeichert |
|---|---|
api.data.get(node) / api.data.set(node, obj) | am Knoten, im Projekt (wandert mit Export/Import). Nach set mit api.map.save() übernehmen. |
api.storage.get(key, fallback) / api.storage.set(key, value) | pro Gerät, projektübergreifend |
api.settings.get(key) / api.settings.set(key, value) | Einstellungen (siehe unten) |
Sicherheitsmodus (api.security, nur lesen): mode() → "standard" | "global" | "tresor", protectsMindmaps() → true im Modus „Alles verschlüsseln“, isUnlocked(). api.storage und api.settings liegen unverschlüsselt in localStorage. Gibt protectsMindmaps() true zurück, dürfen dort keine Inhalte oder Namen von Mindmaps landen. Zwischenspeicher dann nur im Arbeitsspeicher halten (Beispiel: plugins/projekte.js). Mindmaps selbst immer über api.mindmaps/api.map lesen und schreiben: Diese Wege verschlüsseln automatisch. Tresor-Mindmaps sind für Plugins nicht sichtbar.
Die geöffnete Mindmap (api.map, nur auf "map", sonst null)
| Aufruf | |
|---|---|
api.map.nodes() | Knoten der aktuellen Ebene |
api.map.allNodes() | alle Knoten des Projekts |
api.map.selected() / api.map.select(node) | Auswahl |
api.map.addNode(type, text, x?, y?) | Knoten anlegen (ohne Position: Bildschirmmitte) |
api.map.save() | Änderung übernehmen: Undo-Schritt + Autosave + Neuzeichnen |
api.map.viewport() | { zoom, panX, panY, worldToScreen(), screenToWorld() } |
api.map.navigateTo(id) | direkt auf die Ebene einer Node springen und sie auswählen (Zoom-Hysterese wird korrekt gesetzt) |
api.map.suspend() / resume() | 2D-Ansicht pausieren/fortsetzen (für Vollbild-Ansichten wie die 3D-Ansicht) |
api.map.state / api.map.app | interner Zugriff – kein stabiler Vertrag, nur im Notfall |
Mindmaps verwalten (api.mindmaps, auf beiden Seiten)
Lesen und Schreiben über die normale Speicherlogik der App – im JSON-Format der Mindmaps.
| Aufruf | |
|---|---|
await api.mindmaps.list() | Mindmaps des angemeldeten Benutzers ([{ name, last_modified, saves }]) |
await api.mindmaps.load(name) / save(name, data) | Mindmap-JSON laden / speichern |
await api.mindmaps.create(name) | neue Mindmap, gibt den tatsächlichen (ggf. nummerierten) Namen zurück |
api.mindmaps.open(name) | im Editor öffnen (nur Startbildschirm) |
api.mindmaps.root(data) / children(data, id) | Knoten finden |
api.mindmaps.addNode(data, { text, parent_id, node_type, body_text, plugin_data }) | Knoten anlegen (Position automatisch) |
api.mindmaps.removeNode(data, id) | Knoten samt Unterknoten und Verbindungen entfernen |
api.mindmaps.meta(data) / setMeta(data, obj) | Plugin-Bereich im JSON: data.plugins[pluginId] |
Grundregel für Daten: Alles, was auch ohne das Plugin sichtbar sein soll, gehört in normale Knoten (Text, Unterknoten). Plugin-eigene Zusatzinfos gehören in den Plugin-Bereich plugins.<id>. Die App reicht diesen Bereich beim Laden und Speichern unverändert durch, auch wenn das Plugin gerade aus ist. So bleibt jede Mindmap ohne Plugin lesbar, und mit Plugin geht nichts verloren.
In der geöffneten Mindmap: api.map.meta() / api.map.setMeta(obj), danach api.map.save().
Ereignisse
api.events.on("map:loaded", (state) => …);
api.events.on("map:changed", (state) => …); // nach jeder Änderung
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) => …); // Ebene betreten/verlassen
api.events.on("menu:projectsShown", (username) => …); // Projektliste angezeigt
Oberfläche
| Aufruf | |
|---|---|
api.ui.toast(text, "ok" | "error") | kurzer Hinweis |
api.ui.dialog({ title, message, content, buttons }) | Dialog → Promise mit dem value des Buttons. buttons: [{ label, value, primary?, danger? }], content: eigenes DOM-Element |
api.ui.confirm(title, message, okLabel) | Ja/Nein → Promise<boolean> |
api.ui.addToolbarButton({ icon, title, onClick }) | Button in der Mindmap-Toolbar |
api.ui.addStartButton({ icon, title, onClick }) | runder Button im Startbildschirm |
api.ui.addProjectTab({ id, label, icon, render(el, { username }) }) | eigener Tab neben „Mindmaps“ in der Projektliste |
api.ui.selectProjectTab(id) | Tab wählen (null = „Mindmaps“) |
api.ui.decorateMindmapCard((cardEl, eintrag, username) => …) | Karten der Mindmap-Liste ergänzen |
api.ui.addImportOption({ icon, label, accept, directory, onFile(file, files) }) | eigenes Import-Format im „Neues Projekt“-Dialog; onFile liefert { name, data } (Mindmap-JSON). directory: true = Ordnerauswahl (alle Dateien in files, auf Android automatisch ausgeblendet) |
api.ui.onBack(() => true/false) | Android-Zurück-Taste abfangen; true = erledigt |
api.ui.addElement(el, parent?) | beliebiges Element einhängen |
api.keys.add("ctrl+k", fn) | Tastenkürzel (ctrl, shift, alt; nicht während Texteingabe) |
Aufräumen automatisch
| Aufruf | |
|---|---|
api.every(ms, fn) / api.after(ms, fn) | Timer, die beim Ausschalten gestoppt werden |
api.listen(target, type, fn) | DOM-Listener, der beim Ausschalten entfernt wird |
api.onDispose(fn) | eigenes Aufräumen |
Wichtig für das Ein-/Ausschalten ohne Neustart: Keine eigenen setInterval, addEventListener usw. direkt verwenden, sondern die Varianten oben – sonst laufen sie nach dem Ausschalten weiter.
Sprache
Die App spricht Deutsch und Englisch. Die aktuelle Sprache steht in document.documentElement.lang ("de" oder "en"). Gib die Texte deines Plugins in beiden Sprachen an und wähle sie nach diesem Wert; die App übersetzt nur ihre eigenen Texte, nicht die deines Plugins.
Einstellungen
Die App baut den Einstellungsdialog (⚙ in der Plugin-Liste) automatisch:
settings: [
{ key: "farbe", type: "color", label: "Farbe", default: "#00d4ff" }, // Wähler + Hex-Feld
{ key: "vorgabe", type: "swatches", label: "Vorgabe", default: "blau", // Farbfelder
options: [{ value: "blau", label: "Blau", color: "#4d8dff" },
{ value: "rot", label: "Rot", color: "#ff4d5e" }] },
{ key: "staerke", type: "range", label: "Stärke", default: 0.5, min: 0, max: 1, step: 0.05 },
{ key: "anzahl", type: "number", label: "Anzahl", default: 3, min: 1 },
{ key: "an", type: "boolean", label: "Aktiviert", default: true }, // Schalter
{ key: "modus", type: "select", label: "Modus", default: "a",
options: [{ value: "a", label: "Variante A" }, { value: "b", label: "Variante B" }] },
{ key: "text", type: "text", label: "Beschriftung", default: "", hint: "Optionaler Hinweis" },
]
Änderungen wirken sofort: api.settings.onChange((key, value, all) => …).
colorzeigt einen Farbwähler und ein Hex-Feld (#rrggbb, auchrgb-Kurzform; ungültige Eingaben werden markiert und nicht übernommen).swatcheszeigt abgerundete Farbfelder; gespeichert wird dervaluedes gewählten Feldes.booleanerscheint als Schalter.
Ein Feld nur unter einer Bedingung anzeigen: { key: "farbe2", type: "color", label: "Zweite Farbe", showIf: { verlauf: true } }. Ändert das Plugin selbst eine Einstellung (api.settings.set), zieht der offene Dialog mit.
Sicherheitsregeln der App (gelten auch für Plugins)
- Kein Netz: Die Content Security Policy blockiert jedes Laden oder Senden ins Internet (
fetch, Bilder, Skripte von Servern). Plugins arbeiten nur mit lokalen Daten und Dateien ausplugins/. Bilder/Töne alsdata:oderblob:gehen. - Erlaubt sind eigene Skripte,
eval/new Functionund Inline-Styles (api.ui.addStyle). - Nutzertext nie ungeprüft als HTML einsetzen:
textContentverwenden oder maskieren – auch Anführungszeichen, wenn der Wert in einem Attribut landet (value="…"). - Keine Mindmap-Daten am Kern vorbei speichern (kein eigenes
localStorage/IndexedDB für Inhalte). Sonst landen sie im Hochsicherheitsmodus als Klartext auf dem Gerät. - Eigene Import-Formate liefern ihre Daten an den Kern; dort laufen sie durch die Import-Prüfung (
js/map-validate.js) und Bilder durch die Metadaten-Bereinigung (js/media-clean.js).
Stabilität
- Jeder Aufruf eines Plugins ist abgesichert. Ein Fehler bringt die App nicht zum Absturz.
- Nach 5 Fehlern wird ein Plugin automatisch ausgeschaltet (mit Hinweis).
- Schlägt
activatefehl, wird alles bereits Registrierte zurückgenommen. apiVersionhöher als die der App → Plugin wird als „benötigt neuere Version“ angezeigt.
Installieren und verteilen
- Aus Datei: 🧩 → „Plugin aus Datei installieren“ →
.jswählen. Gleicheiderneut installieren = Update.