Skip to content

yeori/mind-wired

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MindWired

mind-wired-demo

mind-wired is javascript library to build mindmap.

1. installing

npm install @mind-wired/core

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 section 3. 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 node Spain

2.2. Svelte

2.3. Vue

2.4. UMD

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 each NodeSpec
  • 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;
}

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 at view.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 of NodeSpec
  • 4 edge styles(line, natural_curve, mustache_lr and mustache_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 (typeof EdgeSpec)
  • 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

edge-line

      // for line edge
      view: {
        x: ..., y: ...,
        edge: {
          name: 'line',
          color: ...,
          width: ...
        }
      }

2. mustache_lr (bottom)

edge-mustache_lr

      // for mustach_lr bottom edge
      view: {
        x: ...,
        y: ...,
        edge: {
          name: 'mustache_lr',
          option: {
            valign: "bottom",
          },
          color: ...,
          width: ...
        }
      }

3. mustache_lr(center)

edge-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

edge-mustache_tb

      // for mustach_lr center edge
      view: {
        x: ...,
        y: ...,
        edge: {
          name: 'mustache_tb',
          color: ...,
          width: ...
        }
      }

5. natural_curve

edge_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).

should move the child nodes

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 side B', node C, D also moves to D', 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 of NodeSpec
  • All nodes inherit layout from it's ancenstors if it has no one.
  • Dragging node Right to the opposite side makes Cat and Dog 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 preoceds node.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.

  1. path - means the nodes have changed parent(by dragging control icon).
  2. pos - means the nodes move by dragging
  3. model - content has updated(text, icon, etc)
  4. schema - a schema has been (un)bounded to node
  5. folding - 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 node P are attached to parent of node P, keeping their position on viewport. NodeDeletionArg.updated references children node[]

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)
};