@mind-wired/core
v0.2.0
Published
Javascript based mindmap library. It provides dragging, aligment, styling(node, edge) and editing functionality.
Downloads
68
Readme
MindWired
mind-wired is javascript library to build mindmap.
1. installing
npm install @mind-wired/[email protected]
2. Client type
2.1. Javascript modules(Typescript)
The example code in this document was generated using Vite(Vanilla + TS).
[PROJECT]
+- assets
+- src
+- api.ts
+- main.ts
+- index.html
index.html
The library needs a placeholder for mindmap
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MindWired Demo</title>
</head>
<body>
<div id="mmap-root"><!-- viewport generated here--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
#mmap-root
- placeholder for mindmap(You can name it freely)
main.ts
It is a minimal initialization code for an instance of mind-wired
/* src/main.ts */
import type { MindWired, NodeSpec, UISetting } from "@mind-wired/core";
import { initMindWired } from "@mind-wired/core";
import "@mind-wired/core/mind-wired.css";
import "@mind-wired/core/mind-wired-form.scss";
import { loadFromServer } from "./api";
window.onload = async () => {
const mapData: { node: NodeSpec } = await loadFromServer();
const el = document.querySelector<HTMLDivElement>("#mmap-root")!;
const mwd: MindWired = await initMindWired({
el,
ui: {
width: "100%",
height: 500,
} as UISetting,
});
mwd.nodes(mapData.node);
};
- initMindWired({el, ui}) - called to initialize mindmap.
el
- placeholder for mindmap.ui
- size, scale, class namings and snap layout etc.@mind-wired/core/mind-wired.css
- minimal css style for mindmap. You can add or modify style for your own css(scss). See section3. Style
@mind-wired/core/mind-wired-form.scss
- style for default editing form.loadFromServer
- fetch mindmap data(nodes, ui, and schema) from your server
You might fetch mindmap data from server like this.
/* src/api.ts */
export const loadFromServer = (): Promise<{
node: NodeSpec;
}> => {
// using axis(...) or fetch(...) in production code
return Promise.resolve({
node: {
model: { text: "Countries\nand\nCities" },
view: {
x: 0,
y: 0,
},
subs: [
{
model: { text: "Canada" },
view: { x: -100, y: 0 },
subs: [
{ model: { text: "Toronto" }, view: { x: -90, y: 0 } },
{ model: { text: "Quebec City" }, view: { x: -10, y: -40 } },
],
},
{
model: { text: "Spain" },
view: { x: 100, y: 0 },
subs: [
{ model: { text: "Madrid" }, view: { x: 90, y: 90 } },
{ model: { text: "Barcelona" }, view: { x: 100, y: 0 } },
{ model: { text: "Valencia" }, view: { x: 90, y: 45 } },
],
},
],
},
});
};
- root node is positioned at the center of viewport
view: {x:0, y:0}
NodeSpec
has three key properties
- model - data of node(plain text, icon badge, or thumbnail)
- view - relative offset (x, y) from it's direct parent node
- subs - direct child nodes, which are also type of
NodeSpec[]
.
For examples,
- Node
Spain(100, 0)
is positioned to the right of the root node. - Three cities of
Madrid, Barcelona, Valencia
are also positioned to the right of the parent nodeSpain
2.2. Svelte
- See /docs/svelte.md
2.3. Vue
- See /docs/vue3.md
2.4. UMD
- See /docs/umd.md
3. Style
mind-wired generates base structure.
<div id="mmap-root">
<!-- generated automatically by mind-wired -->
<div data-mind-wired-viewport>
<canvas></canvas>
<div class="mwd-selection-area"></div>
<div class="mwd-nodes"></div>
</div>
</div>
[data-mind-wired-viewport]
- reserved data attribute meaning root element of mindmap<canvas></canvas>
- placeholer for edges.mwd-selection-area
- used to highlight selected nodes.mwd-nodes
- placeholder for node structure
3.1 Style file
To define your node styles, create a css(scss) file
[PROJECT]
+- assets
+- mindmap.css (+)
+- src
+- main.ts
+- index.html
assets/mindmap.css
- you can name it as you want
Then, import the (s)css file
/* /src/main.ts */
...
import "@mind-wired/core/mind-wired.css";
import "@mind-wired/core/mind-wired-form.scss";
...
import "./assets/mindmap.css"
window.onload = async () => {
...
};
3.2. Snap to node
MindWired supports snap to node, which helps node alignment while dragging.
initinitMindWired({
el,
ui: {
...
snap: { # optional
limit: 4, # within 4 pixels
width: 0.4, # snap line width
dash: [6, 2], # dashed line style
color: "red", # line color
},
})
- Snap guide lines are displayed when a node is whithin 4 pixels to the adjacent nodes.
- Lines are rendered on
<canvas/>
You can disable it by setting false
initinitMindWired({
el,
ui: {
...
snap: false,
})
// or
ui: {
snap: { # optional
limit: 4, # within 4 pixels
width: 0.4, # snap line width
dash: [6, 2], # dashed line style
color: "red", # line color
enabled: false # disable snap
},
}
3.3. Node Style
All nodes are placed in the .mwd-nodes
with tree structure(recursively)
<div id="mmap-root">
<div data-mind-wired-viewport>
...
<div class="mwd-nodes">
<!-- root node -->
<div class="mwd-node">
<div class="mwd-body"></div>
<div class="mwd-subs">
<!--child nodes -->
<div class="mwd-node">..Canada..</div>
<div class="mwd-node">..Spain..</div>
</div>
</div>
</div>
</div>
</div>
3.3.1. Level
Each node is assigned level
number, 0 for root node, 1 for sub nodes of the root.
[TOP]
+- [Left]
|
+- [Right]
|
+--[Cat]
- Root node
TOP
-class="level-0"
- Node
Left
-class="level-1"
- Node
Right
-class="level-1"
- Node
Cat
-class="level-2"
<div class="mwd-nodes">
<div class="mwd-node">
<div class="mwd-body level-0">..TOP..</div>
<div class="mwd-subs">
<div class="mwd-node">
<div class="mwd-body level-1">..LEFT..</div>
</div>
<div class="mwd-node">
<div class="mwd-body level-1">..RIGHT..</div>
<div class="mwd-subs">
<div class="mwd-node">
<div class="mwd-body level-2">..Cat..</div>
</div>
</div>
</div>
</div>
</div>
</div>
- level classname(
level-x
) is attached at.mwd-body
- level number changes whenever depth of node changes(except root node)
For example, here is css to assign rounded border with bigger text to root node,
/* assets/mindmap.css */
[data-mind-wired-viewport] .mwd-body.level-0 {
border: 1px solid #444;
border-radius: 8px;
font-size: 1.5rem;
}
- be sure to keep
.node-body
together to override default css style
Style for level-1(Left
, Right
)
/* assets/mindmap.css */
[data-mind-wired-viewport] .mwd-body.level-1 {
color: 'red'
font-size: 1.25rem;
}
3.3.2. Schema
A group of nodes(Canada
, Spain
) need to have same style(border, background and font style etc) regardless of level.
Schema can be specified in each node
{
node: {
model: { text: "Countries\nand\nCities" },
view: {...},
subs: [
{
model: { text: "Canada", schema: 'country' },
view: {...},
subs: [...],
},
{
model: { text: "Spain", schema: 'country' },
view: {...},
subs: [...],
},
],
},
}
// schemas
const schema = [{
name: 'country',
style: { // optional
fontSize: '1rem',
border: '1px solid #2323FF',
color: '#2323FF',
borderRadius: '6px'
}
}]
- path -
model.schema
in eachNodeSpec
- type:
string
If you have your schema, pass them to
initMindWired
const yourSchemap: [...] initMindWired({ el: mapEl!, ui, schema: yourSchema }) .then((mwd: MindWired) => { ... });
It is rendered as class value
<div class="mwd-nodes">
<div class="mwd-node">
<div class="mwd-body level-0">..Countries...</div>
<div class="mwd-subs">
<div class="mwd-node country">
<div class="mwd-body country level-1">..Canada..</div>
</div>
<div class="mwd-node country">
<div class="mwd-body country level-1">..Span..</div>
...
</div>
</div>
</div>
</div>
- class
city
(schema) is assigned at.mwd-node
and.mwd-body
<style>...</style>
for schemas are injected into<head/>
Dynamic Schema Style
If schema has property style
defined, <style>...</style>
for each schema is created in <head/>
<!-- automatically created from property schema.style -->
<head>
<style id="...">
[data-mind-wired-viewport] .mwd-node.country > .mwd-body {
font-size: 1rem;
border: 1px dashed #2323ff;
color: #2323ff;
border-radius: 6px;
}
</style>
</head>
Static Schema Style
You can define style for schema without property style
.
const yourSchema = [
{name: 'coutnry', style: {...}},
{name: 'city'}
]
- schema
city
has no style.
Nodes with schema city
can be styled like this
/* assets/mindmap.css */
[data-mind-wired-viewport] .mwd-node.city > .mwd-body.city {
color: #777;
box-shadow: 0 0 8px #0000002d, 0 0 2px #00000041;
}
- Child Combinator syntax(
.parent > .child
) should be used. - https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator
3.3.3. Style per node
You can define separate CSS styles for each node, which add or override styles defined by level and schema.
return Promise.resolve({
node: {
model: { text: "Countries\nand\nCities" },
view: {...},
subs: [
{
model: { text: "Canada", schema: 'country'},
...
},
{
model: { text: "Spain", schema: 'country' },
view: { x: 100, y: 0,
style: {
backgroundColor: '#9a7baf',
color: 'white',
border:'none'
}
},
},
{
model: { text: "South Korea", schema: 'country' },
...
},
],
},
schema : [{
name: 'country',
style: {...}
}]
});
- Three countries share schema
country
. Spain
has additional style atview.style
, which override background(#9a7baf
), font color(white
) and border(none
)
3.4. EdgeSpec
Edges are rendered on
<canvas/>
node: {
model { ... },
view: {
x: ...,
y: ...,
layout: ...,
edge: {
name: 'line', # name of edge renderer
color: 'blue', # edge color
width: 4 # edge width
}
}
}
- path :
view.edge
ofNodeSpec
- 4 edge styles(
line
,natural_curve
,mustache_lr
andmustache_tb
) are available. - All nodes inherite edge style from it's ancenstors
For example, mustache_lr
edge on the root node
export const loadFromServer = () => {
return Promise.resolve({
node: {
model: { text: "Countries\nand\nCities" },
view: {
x: 0,
y: 0,
edge: { name: "mustache_lr", color: "#2378ff", width: 2 },
},
subs: [...],
},
});
};
- path :
view.edge
(typeofEdgeSpec
) - color - keyword defined in css color keywords or web color (ex
#acdeff
) - All descendant nodes inherite
EdgeSpec
from the root node, if they has no one.
3.4.1. Edge preview
1. line
// for line edge
view: {
x: ..., y: ...,
edge: {
name: 'line',
color: ...,
width: ...
}
}
2. mustache_lr (bottom)
// for mustach_lr bottom edge
view: {
x: ...,
y: ...,
edge: {
name: 'mustache_lr',
option: {
valign: "bottom",
},
color: ...,
width: ...
}
}
3. mustache_lr(center)
// for mustach_lr center edge
view: {
x: ...,
y: ...,
edge: {
name: 'mustache_lr',
// option: {
// valign: "center",
// },
color: ...,
width: ...
}
}
center
is default
4. mustache_tb
// for mustach_lr center edge
view: {
x: ...,
y: ...,
edge: {
name: 'mustache_tb',
color: ...,
width: ...
}
}
5. natural_curve
// for natural_curve center edge
view: {
x: ...,
y: ...,
edge: {
name: 'natural_curve',
color: ...,
width: ...
}
}
4. Layout
When you drag node Right
to the left side of the root node, child nodes cat
and Dog
keep their side, which results in annoying troublesome(have to move all sub nodes to the left of the parent Right
).
Layout can help moving all descendant nodes to the opposite side when a node moves.
4 layouts are predefined.
- X-AXIS
- Y-AXIS
- XY-AXIS
- DEFAULT
4.1. X-AXIS
[A]
|
[B] | [B`]
[C] [D] | [D`] [C`]
- If node
B
moves to the opposite sideB'
, nodeC, D
also moves toD', C'
Let's install X-AXIS
on the root node
export const loadFromServer = () => {
return Promise.resolve({
node: {
model: { text: "Countries\nand\nCities" },
view: {
x: 0,
y: 0,
edge: {...},
layout: {type: 'X-AXIS'},
},
subs: [...],
},
});
};
- path:
view.layout
ofNodeSpec
- All nodes inherit layout from it's ancenstors if it has no one.
- Dragging node
Right
to the opposite side makesCat
andDog
change their sides.
4.2. Y-AXIS
[C] [D]
[B]
---------------[A]---------------
[B']
[C'][D']
4.3. XY-AXIS
X-AXIS
+Y-AXIS
4.4. DEFAULT
If root node has no layout, layout DEFAULT
is assign, which does nothing.
5. Events
5.1. Node Event
| event name | description |
| --------------- | ----------------------------------- |
| node.selected
| nodes are selected |
| node.clicked
| a node is clicked |
| node.created
| nodes are created |
| node.updated
| nodes are updated(model, pos, path) |
| node.deleted
| nodes are deleted |
node.selected
triggered when nodes have been selected(activate sate).
import {..., type NodeEventArg} from "@mind-wired/core";
window.onload = async () => {
...
mwd.listen("node.selected", async (e: NodeEventArg) => {
const {type, nodes} = e;
console.log(type, nodes);
})
};
node.selected
always preocedsnode.clicked
- Clicking viewport also triggers the event with empty nodes.
node.clicked
triggered when a node has been clicked.
window.onload = async () => {
...
mwd.listen("node.clicked", async (e: NodeEventArg) => {
const {type, nodes} = e;
console.log(type, nodes);
})
};
node.created
triggered when nodes have been created(for example Enter
, or Shift+Enter
)
window.onload = async () => {
...
mwd.listen("node.created", (e: NodeEventArg) => {
const {type, nodes} = e;
console.log(type, nodes);
})
};
node.updated
triggered when nodes have been updated by
- offset (x, y) changed(type :
'pos'
) - changing parent(type:
'path'
) - content updated(type:
'model'
) - schema (un)bound(type:
schema
) - folding state changed (type:
folding
)
window.onload = async () => {
...
mwd.listen("node.updated", (e: NodeEventArg) => {
const {type, nodes} = e; // type: 'pos' | 'path' | 'model' | 'schema' | 'folding'
console.log(type, nodes);
})
};
- nodes - updated nodes
- type - cause of updates,
path
,pos
,model
,schema
,folding
type
have one of five values.
path
- means the nodes have changed parent(by dragging control icon).pos
- means the nodes move by draggingmodel
- content has updated(text, icon, etc)schema
- a schema has been (un)bounded to nodefolding
- folding state has been changed of node
node.deleted
triggered when nodes have been deleted(pressing delete
key, fn+delete
in mac)
window.onload = async () => {
...
mwd.listen("node.deleted", (e: NodeDeletionArg) => {
const { type, nodes, updated } = e; // type: 'delete'
console.log(type, nodes, updated);
})
};
- Children
node[]
of deleted nodeP
are attached to parent of nodeP
, keeping their position on viewport.NodeDeletionArg.updated
references childrennode[]
If deleted node has children, they are moved to node.parent, which triggers node.updated
event
5.2. Schema Event
| event name | description |
| ---------------- | ----------------------------- |
| schema.created
| new schema(s) is(are) created |
| schema.updated
| schemas are updated |
| schema.deleted
| schemas are deleted |
schema.created
triggered when new schemas are created.
window.onload = async () => {
...
mwd.listen("schema.created", (e: SchemaEventArg) => {
const { type, schemas } = e; // type: 'create'
console.log(type, schemas);
})
};
// or
import { EVENT } from '@mind-wired/core'
mwd.listenStrict(EVENT.SCHEMA.CREATED, (e: SchemaEventArg) => {
const { type, schemas } = e; // type: 'create'
console.log(type, schemas);
});
schema.updated
triggered when schemas are updated.
window.onload = async () => {
...
mwd.listen("schema.updated", (e: SchemaEventArg) => {
const { type, schemas } = e; // type: 'create'
console.log(type, schemas);
})
};
// or
mwd.listenStrict(EVENT.SCHEMA.UPDATED, (e: SchemaEventArg) => {
const { type, schemas } = e; // type: 'update'
console.log(type, schemas);
});
schema.deleted
triggered when schemas are updated.
window.onload = async () => {
...
mwd.listen("schema.deleted", (e: SchemaEventArg) => {
const { type, schemas } = e; // type: 'delete'
console.log(type, schemas);
})
};
// or
mwd.listenStrict(EVENT.SCHEMA.DELETED, (e: SchemaEventArg) => {
const { type, schemas } = e; // type: 'update'
console.log(type, schemas);
});
6. Short Key
Node
on idle node
| Ctrl | Alt | Shift | KEY | description | | ---- | --- | ----- | --- | ----------- | | | | | | none |
| Ctrl | Alt | Shift | Click | description |
| ---- | --- | ------- | ------- | ------------------------ |
| | | | click
| make node active |
| | | shift
| click
| add node to active state |
on active state
- When one or more nodes are selected
| Ctrl | Alt | Shift | KEY | description |
| ---- | --- | ------- | -------- | ----------------------------------------- |
| | | | Enter
| insert sinbling of active node enter
|
| | | shift
| Enter
| insert child on active node shift+enter
|
| | | | Delete
| delete active node(s), fn+delete
in mac |
| | | | Space
| start editing state of active node |
on editing state
- When editor of an active node is open
| Ctrl | Alt | Shift | KEY | description |
| ---- | --- | ----- | ------- | --------------------------------- |
| | | | Enter
| save data and finish editing |
| | | | esc
| finish editing state without save |
7. Store
Calling MindWired.exportWith()
exports current state of mindmap.
7.1. by listeners
/* /src/main.ts */
import type {..., NodeEventArg } from "@mind-wired/core";
...
const sendToBackend = (data: ExportResponse) => {
console.log(data)
}
window.onload = async () => {
...
const mwd: MindWired = await initMindWired({...});
mwd.nodes(mapData.node);
mwd
.listen("node.updated", async (e: NodeEventArg) => {
const data = await mwd.exportWith();
sendToBackend(data)
}).listen("node.created", async (e: NodeEventArg) => {
const data = await mwd.exportWith();
sendToBackend(data)
}).listen("node.deleted", async (e: NodeDeletionArg) => {
const data = await mwd.exportWith();
sendToBackend(data)
});
};
7.2. by control ui
You could provide, for example, <button/>
to export current state of mindmap
<body>
<nav id="controls">
<button data-export>EXPORT</button>
</nav>
<div id="mmap-root">...</div>
</body>
window.onload = async () => {
const mwd: MindWired = await initMindWired({...});
...
const btnExport = document.querySelector<HTMLButtonElement>('#controls > [data-export]')
btnExport!.addEventListener('click', async () => {
const data = await mwd.exportWith();
sendToBackend(data)
}, false)
};