MindElixir can render an editable mind map quickly, but a useful online editor needs more than mind.init(data). Users expect the visual tree and a portable outline to stay synchronized, keyboard editing to feel predictable, the canvas to pan, fullscreen to refit, and exports to preserve their work.
The architecture below treats MindElixir as an imperative browser runtime and Markdown as a readable interchange format. React owns the shell and derived UI state; pure utility functions own Markdown and JSON conversion; the editor instance owns interactive nodes, history, links, summaries, focus mode, zoom, and rendering.
Use one canonical tree shape at each boundary
MindElixir’s native data centers on nodeData: every node has an id, topic, and optional children, while direction describes left, right, or two-sided layout. The live editor should operate on this native tree. Markdown and JSON are import or export representations, not competing live stores.
For the product shell, derive a snapshot after editor operations: current Markdown, selected topic, selected count, whether delete or focus is available, zoom percentage, focus mode, and layout. This avoids re-rendering the imperative editor from React on every selection change.
| Representation | Purpose |
|---|---|
| MindElixirData | Live editor data and native JSON backup |
| Markdown outline | Human-readable editing and interchange |
| React snapshot | Buttons, status, selection, zoom, and layout UI |
| SVG or PNG | Sharing a visual result |
| Standalone HTML | Portable visual plus readable outline |
Initialize the imperative editor after mount
Import mind-elixir/style.css from the route layout, then dynamically load both the runtime and its i18n module inside an effect. Create the instance with a real host element, initialize it once, and destroy it during cleanup. A disposed flag prevents late asynchronous work from reviving an unmounted editor.
Store the instance in a ref, not React state. Changing selection, camera position, or node text should not cause React to recreate the canvas. A ResizeObserver can call scaleFit after the host size changes.
const [{ default: MindElixir }, i18n] = await Promise.all([
import('mind-elixir'),
import('mind-elixir/i18n'),
]);
const mind = new MindElixir({
el: hostElement,
direction: MindElixir.SIDE,
editable: true,
contextMenu: {
locale: locale === 'zh' ? i18n.zh_CN : i18n.en,
focus: true,
link: true,
},
keypress: true,
overflowHidden: true,
toolBar: false,
});
mind.init(data);
mind.scaleFit();
// On unmount:
mind.destroy();Convert Markdown with a pure stack-based parser
A practical outline format can accept ATX headings and indented bullets. Parse each non-empty line into a level and topic, then maintain a stack of the most recent node at each level. Before appending a new node, pop stack entries whose level is greater than or equal to the incoming level; the remaining top is its parent.
If the document contains multiple top-level roots, create a synthetic fallback root. Generate fresh node IDs during import and normalize line breaks out of topic text during export. Return an outcome object instead of throwing parser errors into the component.
type Outcome =
| { ok: true; data: MindElixirData }
| { ok: false; message: string };
function parseLine(line: string) {
const heading = line.match(/^\s*(#{1,6})\s+(.+)$/u);
if (heading) return { level: heading[1].length, topic: heading[2] };
const bullet = line.match(/^(\s*)[-*+]\s+(.+)$/u);
if (bullet) {
return {
level: Math.floor(bullet[1].replace(/\t/g, ' ').length / 2) + 2,
topic: bullet[2],
};
}
return null;
}- Use two spaces as one bullet nesting level, and normalize tabs consistently.
- Trim bullet markers without removing meaningful punctuation inside a topic.
- Export the root as a heading and descendants as indented bullets.
- Keep parsing and serialization independent of React and the DOM.
Synchronize from editor events, not a polling loop
MindElixir exposes an event bus for operations, selection, new nodes, and scaling. Subscribe once after initialization and schedule snapshot reads for the next task so the editor has finished updating its internal data and DOM. Remove the exact listener functions during cleanup.
Do not update the Markdown editor while the user has an outline draft open. Keep latest committed Markdown in a ref and only replace the draft after an explicit import or when the outline is closed. Otherwise a selection event can overwrite half-written Markdown.
- operation: update document-derived UI and detect beginEdit.
- selectNewNode: synchronize a newly created editable node.
- selectNodes and unselectNodes: update action availability.
- scale: update the displayed zoom percentage without rebuilding the map.
Make keyboard editing predictable
Imperative editors often create a temporary contenteditable element for node editing. Global shortcuts can fire before that editor commits, causing Enter or Tab to add another node while the current text is still transient. Capture keydown at the editor container, commit Enter or Tab, restore the original text on Escape, and then synchronize the snapshot.
When starting an edit programmatically, wait for the input element to exist, focus it, and select its text. After finishing, return focus to the map container so undo, redo, insertion, and deletion shortcuts continue to work.
Add canvas panning without breaking node interaction
A full editor should pan when the user drags empty canvas, but dragging a topic, expansion control, link, summary, input, or button must retain its native behavior. On pointerdown, detect whether the target belongs to an interactive element; only capture the pointer for true background drags.
Track the last pointer position and call mind.move(dx, dy) on pointermove. Release pointer capture on pointerup or pointercancel, reset the cursor, and restore editor focus. Pointer Events give one path for mouse, pen, and touch.
Expose editor features through small action adapters
Wrap imperative actions in one runMapAction helper that catches errors, schedules a snapshot refresh, and restores focus. Buttons can then call addChild, insertSibling, beginEdit, removeNodes, undo, redo, scaleFit, initLeft, initRight, initSide, focusNode, cancelFocus, createSummary, or createArrow without duplicating lifecycle code.
For link creation, store the source topic and one-way or two-way mode in refs. The next valid topic click becomes the target, after which the mode is cleared. This is easier to reason about than trying to infer two selected nodes after unrelated selection events.
Export data for editing, sharing, and recovery
Native JSON is the safest round-trip backup because it retains MindElixir data. Markdown is the most readable interchange format. SVG is ideal for scalable documents, PNG for quick sharing, and a standalone HTML snapshot can combine the exported SVG with a collapsible Markdown outline.
When importing JSON, validate nodeData recursively instead of trusting a cast. Every node should have string id and topic fields, children must be a recursive node array, and direction should be limited to supported values. Download through Blob object URLs and revoke them immediately after use.
- JSON: native editable backup.
- Markdown: portable outline and easy text editing.
- SVG: resolution-independent visual export.
- PNG: convenient image sharing.
- HTML: self-contained snapshot with accessible outline.
Refit after fullscreen and container changes
Use the same native-fullscreen plus fixed-position fallback pattern as other canvas tools. After every transition, call scaleFit after layout settles. A ResizeObserver on the map host handles responsive sidebars, orientation changes, and other container size updates.
Destroy the observer, event listeners, pointer state, link mode, and MindElixir instance when the component unmounts. Hot reload can otherwise leave duplicate keyboard handlers and event bus subscriptions that make every action run twice.
Key takeaways
The maintainable MindElixir architecture separates the imperative editor from React and separates conversion from both. Initialize once, synchronize through events, protect in-progress outline edits, adapt actions through one error boundary, and offer native JSON plus readable Markdown. That produces a real tool rather than a canvas demo.