337 lines
14 KiB
HTML
337 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Mind Map</title>
|
|
<meta name="description" content="A mind map editor, showing how subtrees can be moved, copied, deleted, and laid out." />
|
|
<!-- Copyright 1998-2017 by Northwoods Software Corporation. -->
|
|
<meta charset="UTF-8">
|
|
<script src="../release/go.js"></script>
|
|
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
|
<script id="code">
|
|
function init() {
|
|
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
|
|
var $ = go.GraphObject.make;
|
|
|
|
myDiagram =
|
|
$(go.Diagram, "myDiagramDiv",
|
|
{
|
|
// when the user drags a node, also move/copy/delete the whole subtree starting with that node
|
|
"commandHandler.copiesTree": true,
|
|
"commandHandler.deletesTree": true,
|
|
"draggingTool.dragsTree": true,
|
|
initialContentAlignment: go.Spot.Center, // center the whole graph
|
|
"undoManager.isEnabled": true
|
|
});
|
|
|
|
// when the document is modified, add a "*" to the title and enable the "Save" button
|
|
myDiagram.addDiagramListener("Modified", function(e) {
|
|
var button = document.getElementById("SaveButton");
|
|
if (button) button.disabled = !myDiagram.isModified;
|
|
var idx = document.title.indexOf("*");
|
|
if (myDiagram.isModified) {
|
|
if (idx < 0) document.title += "*";
|
|
} else {
|
|
if (idx >= 0) document.title = document.title.substr(0, idx);
|
|
}
|
|
});
|
|
|
|
// a node consists of some text with a line shape underneath
|
|
myDiagram.nodeTemplate =
|
|
$(go.Node, "Vertical",
|
|
{ selectionObjectName: "TEXT" },
|
|
$(go.TextBlock,
|
|
{
|
|
name: "TEXT",
|
|
minSize: new go.Size(30, 15),
|
|
editable: true
|
|
},
|
|
// remember not only the text string but the scale and the font in the node data
|
|
new go.Binding("text", "text").makeTwoWay(),
|
|
new go.Binding("scale", "scale").makeTwoWay(),
|
|
new go.Binding("font", "font").makeTwoWay()),
|
|
$(go.Shape, "LineH",
|
|
{
|
|
stretch: go.GraphObject.Horizontal,
|
|
strokeWidth: 3, height: 3,
|
|
// this line shape is the port -- what links connect with
|
|
portId: "", fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides
|
|
},
|
|
new go.Binding("stroke", "brush"),
|
|
// make sure links come in from the proper direction and go out appropriately
|
|
new go.Binding("fromSpot", "dir", function(d) { return spotConverter(d, true); }),
|
|
new go.Binding("toSpot", "dir", function(d) { return spotConverter(d, false); })),
|
|
// remember the locations of each node in the node data
|
|
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
|
|
// make sure text "grows" in the desired direction
|
|
new go.Binding("locationSpot", "dir", function(d) { return spotConverter(d, false); })
|
|
);
|
|
|
|
// selected nodes show a button for adding children
|
|
myDiagram.nodeTemplate.selectionAdornmentTemplate =
|
|
$(go.Adornment, "Spot",
|
|
$(go.Panel, "Auto",
|
|
// this Adornment has a rectangular blue Shape around the selected node
|
|
$(go.Shape, { fill: null, stroke: "dodgerblue", strokeWidth: 3 }),
|
|
$(go.Placeholder, { margin: new go.Margin(4, 4, 0, 4) })
|
|
),
|
|
// and this Adornment has a Button to the right of the selected node
|
|
$("Button",
|
|
{
|
|
alignment: go.Spot.Right,
|
|
alignmentFocus: go.Spot.Left,
|
|
click: addNodeAndLink // define click behavior for this Button in the Adornment
|
|
},
|
|
$(go.TextBlock, "+", // the Button content
|
|
{ font: "bold 8pt sans-serif" })
|
|
)
|
|
);
|
|
|
|
// the context menu allows users to change the font size and weight,
|
|
// and to perform a limited tree layout starting at that node
|
|
myDiagram.nodeTemplate.contextMenu =
|
|
$(go.Adornment, "Vertical",
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Bigger"),
|
|
{ click: function(e, obj) { changeTextSize(obj, 1.1); } }),
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Smaller"),
|
|
{ click: function(e, obj) { changeTextSize(obj, 1/1.1); } }),
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Bold/Normal"),
|
|
{ click: function(e, obj) { toggleTextWeight(obj); } }),
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Layout"),
|
|
{
|
|
click: function(e, obj) {
|
|
var adorn = obj.part;
|
|
adorn.diagram.startTransaction("Subtree Layout");
|
|
layoutTree(adorn.adornedPart);
|
|
adorn.diagram.commitTransaction("Subtree Layout");
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
// a link is just a Bezier-curved line of the same color as the node to which it is connected
|
|
myDiagram.linkTemplate =
|
|
$(go.Link,
|
|
{
|
|
curve: go.Link.Bezier,
|
|
fromShortLength: -2,
|
|
toShortLength: -2,
|
|
selectable: false
|
|
},
|
|
$(go.Shape,
|
|
{ strokeWidth: 3 },
|
|
new go.Binding("stroke", "toNode", function(n) {
|
|
if (n.data.brush) return n.data.brush;
|
|
return "black";
|
|
}).ofObject())
|
|
);
|
|
|
|
// the Diagram's context menu just displays commands for general functionality
|
|
myDiagram.contextMenu =
|
|
$(go.Adornment, "Vertical",
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Undo"),
|
|
{ click: function(e, obj) { e.diagram.commandHandler.undo(); } },
|
|
new go.Binding("visible", "", function(o) { return o.diagram && o.diagram.commandHandler.canUndo(); }).ofObject()),
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Redo"),
|
|
{ click: function(e, obj) { e.diagram.commandHandler.redo(); } },
|
|
new go.Binding("visible", "", function(o) { return o.diagram && o.diagram.commandHandler.canRedo(); }).ofObject()),
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Save"),
|
|
{ click: function(e, obj) { save(); } }),
|
|
$("ContextMenuButton",
|
|
$(go.TextBlock, "Load"),
|
|
{ click: function(e, obj) { load(); } })
|
|
);
|
|
|
|
myDiagram.addDiagramListener("SelectionMoved", function(e) {
|
|
var rootX = myDiagram.findNodeForKey(0).location.x;
|
|
myDiagram.selection.each(function(node) {
|
|
if (node.data.parent !== 0) return; // Only consider nodes connected to the root
|
|
var nodeX = node.location.x;
|
|
if (rootX < nodeX && node.data.dir !== "right") {
|
|
updateNodeDirection(node, "right");
|
|
} else if (rootX > nodeX && node.data.dir !== "left") {
|
|
updateNodeDirection(node, "left");
|
|
}
|
|
layoutTree(node);
|
|
});
|
|
});
|
|
|
|
// read in the predefined graph using the JSON format data held in the "mySavedModel" textarea
|
|
load();
|
|
}
|
|
|
|
function spotConverter(dir, from) {
|
|
if (dir === "left") {
|
|
return (from ? go.Spot.Left : go.Spot.Right);
|
|
} else {
|
|
return (from ? go.Spot.Right : go.Spot.Left);
|
|
}
|
|
}
|
|
|
|
function changeTextSize(obj, factor) {
|
|
var adorn = obj.part;
|
|
adorn.diagram.startTransaction("Change Text Size");
|
|
var node = adorn.adornedPart;
|
|
var tb = node.findObject("TEXT");
|
|
tb.scale *= factor;
|
|
adorn.diagram.commitTransaction("Change Text Size");
|
|
}
|
|
|
|
function toggleTextWeight(obj) {
|
|
var adorn = obj.part;
|
|
adorn.diagram.startTransaction("Change Text Weight");
|
|
var node = adorn.adornedPart;
|
|
var tb = node.findObject("TEXT");
|
|
// assume "bold" is at the start of the font specifier
|
|
var idx = tb.font.indexOf("bold");
|
|
if (idx < 0) {
|
|
tb.font = "bold " + tb.font;
|
|
} else {
|
|
tb.font = tb.font.substr(idx + 5);
|
|
}
|
|
adorn.diagram.commitTransaction("Change Text Weight");
|
|
}
|
|
|
|
function updateNodeDirection(node, dir) {
|
|
myDiagram.model.setDataProperty(node.data, "dir", dir);
|
|
// recursively update the direction of the child nodes
|
|
var chl = node.findTreeChildrenNodes(); // gives us an iterator of the child nodes related to this particular node
|
|
while(chl.next()) {
|
|
updateNodeDirection(chl.value, dir);
|
|
}
|
|
}
|
|
|
|
function addNodeAndLink(e, obj) {
|
|
var adorn = obj.part;
|
|
var diagram = adorn.diagram;
|
|
diagram.startTransaction("Add Node");
|
|
var oldnode = adorn.adornedPart;
|
|
var olddata = oldnode.data;
|
|
// copy the brush and direction to the new node data
|
|
var newdata = { text: "idea", brush: olddata.brush, dir: olddata.dir, parent: olddata.key };
|
|
diagram.model.addNodeData(newdata);
|
|
layoutTree(oldnode);
|
|
diagram.commitTransaction("Add Node");
|
|
}
|
|
|
|
function layoutTree(node) {
|
|
if (node.data.key === 0) { // adding to the root?
|
|
layoutAll(); // lay out everything
|
|
} else { // otherwise lay out only the subtree starting at this parent node
|
|
var parts = node.findTreeParts();
|
|
layoutAngle(parts, node.data.dir === "left" ? 180 : 0);
|
|
}
|
|
}
|
|
|
|
function layoutAngle(parts, angle) {
|
|
var layout = go.GraphObject.make(go.TreeLayout,
|
|
{ angle: angle,
|
|
arrangement: go.TreeLayout.ArrangementFixedRoots,
|
|
nodeSpacing: 5,
|
|
layerSpacing: 20,
|
|
setsPortSpot: false, // don't set port spots since we're managing them with our spotConverter function
|
|
setsChildPortSpot: false });
|
|
layout.doLayout(parts);
|
|
}
|
|
|
|
function layoutAll() {
|
|
var root = myDiagram.findNodeForKey(0);
|
|
if (root === null) return;
|
|
myDiagram.startTransaction("Layout");
|
|
// split the nodes and links into two collections
|
|
var rightward = new go.Set(go.Part);
|
|
var leftward = new go.Set(go.Part);
|
|
root.findLinksConnected().each(function(link) {
|
|
var child = link.toNode;
|
|
if (child.data.dir === "left") {
|
|
leftward.add(root); // the root node is in both collections
|
|
leftward.add(link);
|
|
leftward.addAll(child.findTreeParts());
|
|
} else {
|
|
rightward.add(root); // the root node is in both collections
|
|
rightward.add(link);
|
|
rightward.addAll(child.findTreeParts());
|
|
}
|
|
});
|
|
// do one layout and then the other without moving the shared root node
|
|
layoutAngle(rightward, 0);
|
|
layoutAngle(leftward, 180);
|
|
myDiagram.commitTransaction("Layout");
|
|
}
|
|
|
|
// Show the diagram's model in JSON format
|
|
function save() {
|
|
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
|
|
myDiagram.isModified = false;
|
|
}
|
|
function load() {
|
|
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
|
|
}
|
|
</script>
|
|
</head>
|
|
<body onload="init()">
|
|
<div id="sample">
|
|
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:300px;"></div>
|
|
<p>
|
|
A mind map is a kind of spider diagram that organizes information around a central concept, with connecting branches.
|
|
</p>
|
|
<p>
|
|
The layout is controlled by moving the nodes closest to the tree's root node.
|
|
When one of these nodes is moved horizontally to the other side of the root,
|
|
all of its children will be sent to <a>Layout.doLayout</a> with a new direction,
|
|
causing text to always be moved outwards from the root. The <b>spotConverter</b> function is used to manage
|
|
<a>GraphObject.fromSpot</a> and <a>GraphObject.toSpot</a> for nodes manually, so the <a>TreeLayout.setsPortSpot</a> and <a>TreeLayout.setsChildPortSpot</a>
|
|
properties are set to false so that laying out the diagram will not overwrite the values.
|
|
</p>
|
|
<p>
|
|
When a node is deleted the <a>CommandHandler.deletesTree</a> property ensures that
|
|
all of its children are deleted with it. When a node is dragged the <a>DraggingTool.dragsTree</a>
|
|
property ensures that all its children are dragged with it.
|
|
Both of these are set during the the Diagram's initalization.
|
|
</p>
|
|
<p>
|
|
Node templates also have a <a>Part.selectionAdornmentTemplate</a> defined to allow for new nodes to be created and a <a>GraphObject.contextMenu</a> with additional commands.
|
|
</p>
|
|
|
|
<button id="SaveButton" onclick="save()">Save</button>
|
|
<button onclick="load()">Load</button>
|
|
<button onclick="layoutAll()">Layout</button>
|
|
Diagram Model saved in JSON format:
|
|
<br />
|
|
<textarea id="mySavedModel" style="width:100%;height:400px">
|
|
{ "class": "go.TreeModel",
|
|
"nodeDataArray": [
|
|
{"key":0, "text":"Mind Map", "loc":"0 0"},
|
|
{"key":1, "parent":0, "text":"Getting more time", "brush":"skyblue", "dir":"right", "loc":"77 -22"},
|
|
{"key":11, "parent":1, "text":"Wake up early", "brush":"skyblue", "dir":"right", "loc":"200 -48"},
|
|
{"key":12, "parent":1, "text":"Delegate", "brush":"skyblue", "dir":"right", "loc":"200 -22"},
|
|
{"key":13, "parent":1, "text":"Simplify", "brush":"skyblue", "dir":"right", "loc":"200 4"},
|
|
{"key":2, "parent":0, "text":"More effective use", "brush":"darkseagreen", "dir":"right", "loc":"77 43"},
|
|
{"key":21, "parent":2, "text":"Planning", "brush":"darkseagreen", "dir":"right", "loc":"203 30"},
|
|
{"key":211, "parent":21, "text":"Priorities", "brush":"darkseagreen", "dir":"right", "loc":"274 17"},
|
|
{"key":212, "parent":21, "text":"Ways to focus", "brush":"darkseagreen", "dir":"right", "loc":"274 43"},
|
|
{"key":22, "parent":2, "text":"Goals", "brush":"darkseagreen", "dir":"right", "loc":"203 56"},
|
|
{"key":3, "parent":0, "text":"Time wasting", "brush":"palevioletred", "dir":"left", "loc":"-20 -31.75"},
|
|
{"key":31, "parent":3, "text":"Too many meetings", "brush":"palevioletred", "dir":"left", "loc":"-117 -64.25"},
|
|
{"key":32, "parent":3, "text":"Too much time spent on details", "brush":"palevioletred", "dir":"left", "loc":"-117 -25.25"},
|
|
{"key":33, "parent":3, "text":"Message fatigue", "brush":"palevioletred", "dir":"left", "loc":"-117 0.75"},
|
|
{"key":331, "parent":31, "text":"Check messages less", "brush":"palevioletred", "dir":"left", "loc":"-251 -77.25"},
|
|
{"key":332, "parent":31, "text":"Message filters", "brush":"palevioletred", "dir":"left", "loc":"-251 -51.25"},
|
|
{"key":4, "parent":0, "text":"Key issues", "brush":"coral", "dir":"left", "loc":"-20 52.75"},
|
|
{"key":41, "parent":4, "text":"Methods", "brush":"coral", "dir":"left", "loc":"-103 26.75"},
|
|
{"key":42, "parent":4, "text":"Deadlines", "brush":"coral", "dir":"left", "loc":"-103 52.75"},
|
|
{"key":43, "parent":4, "text":"Checkpoints", "brush":"coral", "dir":"left", "loc":"-103 78.75"}
|
|
]
|
|
}
|
|
</textarea>
|
|
</div>
|
|
</body>
|
|
</html>
|