mmp
Version:
JavaScript library to draw mind maps.
1,286 lines (1,275 loc) • 93.9 kB
JavaScript
/**
* @module mmp
* @version 0.2.20
* @file JavaScript library to draw mind maps.
* @copyright Omar Desogus 2020
* @license MIT
* @see {@link https://github.com/Mindmapp/mmp|GitHub}
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) :
(factory((global.mmp = {}),global.d3));
}(this, (function (exports,d3) { 'use strict';
var version = "0.2.20";
/**
* Manage console messages and errors.
*/
var Log = /** @class */ (function () {
function Log() {
}
/**
* Throw an Error with a message.
* @param {string} message
* @param {string} type
*/
Log.error = function (message, type) {
switch (type) {
case "eval":
throw new EvalError(message);
case "range":
throw new RangeError(message);
case "reference":
throw new ReferenceError(message);
case "syntax":
throw new SyntaxError(message);
case "type":
throw new TypeError(message);
case "uri":
throw new URIError(message);
default:
throw new Error(message);
}
};
/**
* Print an info message.
* @param {string} message
*/
Log.info = function (message) {
console.log(message);
};
/**
* Print a debug message.
* @param {string} message
*/
Log.debug = function (message) {
console.debug(message);
};
return Log;
}());
/**
* A list of general useful functions.
*/
var Utils = /** @class */ (function () {
function Utils() {
}
/**
* Clone an object in depth.
* @param {object} object
* @returns object
*/
Utils.cloneObject = function (object) {
if (object === null) {
return null;
}
else if (typeof object === "object") {
return JSON.parse(JSON.stringify(object));
}
else {
Log.error("Impossible to clone a non-object", "type");
}
};
/**
* Clear an object.
* @param {object} object
*/
Utils.clearObject = function (object) {
for (var property in object) {
delete object[property];
}
};
/**
* Convert an Object to an array.
* @param {object} object
* @returns {Array}
*/
Utils.fromObjectToArray = function (object) {
var array = [];
for (var p in object) {
array.push(object[p]);
}
return array;
};
/**
* Merge two objects.
* @param {object} object1
* @param {object} object2
* @param {boolean} restricted
* @returns {object} result
*/
Utils.mergeObjects = function (object1, object2, restricted) {
if (restricted === void 0) { restricted = false; }
if (object2 === undefined && this.isPureObjectType(object1)) {
return this.cloneObject(object1);
}
else if (object1 === undefined && this.isPureObjectType(object2)) {
return this.cloneObject(object2);
}
else if (!this.isPureObjectType(object1) || !this.isPureObjectType(object2)) {
Log.error("Only two pure objects can be merged", "type");
}
var result = this.cloneObject(object1);
for (var property in object2) {
var value = object2[property];
if (!restricted || result[property] !== undefined) {
if (this.isPrimitiveType(value) || value === null) {
result[property] = value;
}
else if (Array.isArray(value)) {
result[property] = Utils.cloneObject(value);
}
else if (this.isPureObjectType(value)) {
if (this.isPureObjectType(result[property])) {
result[property] = Utils.mergeObjects(result[property], value);
}
else {
result[property] = Utils.cloneObject(value);
}
}
else {
Log.error("Type \"" + typeof value + "\" not allowed here", "type");
}
}
}
return result;
};
/**
* Return css rules of an element.
* @param {HTMLElement} element
* @return {string} css
*/
Utils.cssRules = function (element) {
var css = "", sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
var rules = sheets[i].cssRules;
if (rules) {
for (var j = 0; j < rules.length; j++) {
var rule = rules[j], fontFace = rule.cssText.match(/^@font-face/);
if (element.querySelector(rule.selectorText) || fontFace) {
css += rule.cssText;
}
}
}
}
return css;
};
/**
* Return true if the value is a primitive type.
* @param value
* @returns {boolean}
*/
Utils.isPrimitiveType = function (value) {
return typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
typeof value === "undefined";
};
/**
* Return true if the value is a pure object.
* @param value
* @returns {boolean}
*/
Utils.isPureObjectType = function (value) {
return typeof value === "object" && !Array.isArray(value) && value !== null;
};
/**
* Remove all ranges of window.
*/
Utils.removeAllRanges = function () {
window.getSelection().removeAllRanges();
};
/**
* Focus an element putting the cursor in the end.
* @param {HTMLElement} element
*/
Utils.focusWithCaretAtEnd = function (element) {
var range = document.createRange(), sel = window.getSelection();
element.focus();
range.selectNodeContents(element);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
};
return Utils;
}());
/**
* Manage the events of the map.
*/
var Events = /** @class */ (function () {
/**
* Initialize the events.
*/
function Events() {
var _this = this;
/**
* Add a callback for specific map event.
* @param {string} event
* @param {Function} callback
*/
this.on = function (event, callback) {
if (typeof event !== "string") {
Log.error("The event must be a string", "type");
}
if (!Event[event]) {
Log.error("The event does not exist");
}
_this.dispatcher.on(Event[event], callback);
};
var events = Utils.fromObjectToArray(Event);
this.dispatcher = d3.dispatch.apply(void 0, events);
}
/**
* Call all registered callbacks for specified map event.
* @param {Event} event
* @param parameters
*/
Events.prototype.call = function (event) {
var parameters = [];
for (var _i = 1; _i < arguments.length; _i++) {
parameters[_i - 1] = arguments[_i];
}
var _a;
return (_a = this.dispatcher).call.apply(_a, [event].concat(parameters));
};
return Events;
}());
var Event;
(function (Event) {
Event["create"] = "mmp-create";
Event["center"] = "mmp-center";
Event["undo"] = "mmp-undo";
Event["redo"] = "mmp-redo";
Event["exportJSON"] = "mmp-export-json";
Event["exportImage"] = "mmp-export-image";
Event["zoomIn"] = "mmp-zoom-in";
Event["zoomOut"] = "mmp-zoom-out";
Event["nodeSelect"] = "mmp-node-select";
Event["nodeDeselect"] = "mmp-node-deselect";
Event["nodeUpdate"] = "mmp-node-update";
Event["nodeCreate"] = "mmp-node-create";
Event["nodeRemove"] = "mmp-node-remove";
})(Event || (Event = {}));
/**
* Manage the zoom events of the map.
*/
var Zoom = /** @class */ (function () {
/**
* Get the associated map instance and initialize the d3 zoom behavior.
* @param {Map} map
*/
function Zoom(map) {
var _this = this;
/**
* Zoom in the map.
* @param {number} duration
*/
this.zoomIn = function (duration) {
if (duration && typeof duration !== "number") {
Log.error("The parameter must be a number", "type");
}
_this.move(true, duration);
_this.map.events.call(Event.zoomIn);
};
/**
* Zoom out the map.
* @param {number} duration
*/
this.zoomOut = function (duration) {
if (duration && typeof duration !== "number") {
Log.error("The parameter must be a number", "type");
}
_this.move(false, duration);
_this.map.events.call(Event.zoomOut);
};
/**
* Center the root node in the mind map.
* @param {number} duration
* @param {number} type
*/
this.center = function (type, duration) {
if (duration === void 0) { duration = 500; }
if (type && type !== "zoom" && type !== "position") {
Log.error("The type must be a string (\"zoom\" or \"position\")", "type");
}
if (duration && typeof duration !== "number") {
Log.error("The duration must be a number", "type");
}
var root = _this.map.nodes.getRoot(), w = _this.map.dom.container.node().clientWidth, h = _this.map.dom.container.node().clientHeight, x = w / 2 - root.coordinates.x, y = h / 2 - root.coordinates.y, svg = _this.map.dom.svg.transition().duration(duration);
switch (type) {
case "zoom":
_this.zoomBehavior.scaleTo(svg, 1);
break;
case "position":
_this.zoomBehavior.translateTo(svg, w / 2 - x, h / 2 - y);
break;
default:
_this.zoomBehavior.transform(svg, d3.zoomIdentity.translate(x, y));
}
_this.map.events.call(Event.center);
};
this.map = map;
this.zoomBehavior = d3.zoom().scaleExtent([0.5, 2]).on("zoom", function () {
_this.map.dom.g.attr("transform", d3.event.transform);
});
}
/**
* Return the d3 zoom behavior.
* @returns {ZoomBehavior} zoom
*/
Zoom.prototype.getZoomBehavior = function () {
return this.zoomBehavior;
};
/**
* Move the zoom in a direction (true: in, false: out).
* @param {boolean} direction
* @param {number} duration
*/
Zoom.prototype.move = function (direction, duration) {
if (duration === void 0) { duration = 50; }
var svg = this.map.dom.svg.transition().duration(duration);
this.zoomBehavior.scaleBy(svg, direction ? 4 / 3 : 3 / 4);
};
return Zoom;
}());
/**
* Draw the map and update it.
*/
var Draw = /** @class */ (function () {
/**
* Get the associated map instance.
* @param {Map} map
*/
function Draw(map) {
this.map = map;
}
/**
* Create svg and main css map properties.
*/
Draw.prototype.create = function () {
var _this = this;
this.map.dom.container = d3.select("#" + this.map.id)
.style("position", "relative");
this.map.dom.svg = this.map.dom.container.append("svg")
.style("position", "absolute")
.style("width", "100%")
.style("height", "100%")
.style("top", 0)
.style("left", 0);
this.map.dom.svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "white")
.attr("pointer-events", "all")
.on("click", function () {
// Deselect the selected node when click on the map background
_this.map.nodes.deselectNode();
});
this.map.dom.g = this.map.dom.svg.append("g");
};
/**
* Update the dom of the map with the (new) nodes.
*/
Draw.prototype.update = function () {
var _this = this;
var nodes = this.map.nodes.getNodes(), dom = {
nodes: this.map.dom.g.selectAll("." + this.map.id + "_node").data(nodes),
branches: this.map.dom.g.selectAll("." + this.map.id + "_branch").data(nodes.slice(1))
};
var tapedTwice = false;
var outer = dom.nodes.enter().append("g")
.style("cursor", "pointer")
.attr("class", this.map.id + "_node")
.attr("id", function (node) {
node.dom = this;
return node.id;
})
.attr("transform", function (node) { return "translate(" + node.coordinates.x + "," + node.coordinates.y + ")"; })
.on("dblclick", function (node) {
d3.event.stopPropagation();
_this.enableNodeNameEditing(node);
}).on('touchstart', function (node) {
if (!tapedTwice) {
tapedTwice = true;
setTimeout(function () {
tapedTwice = false;
}, 300);
return false;
}
_this.enableNodeNameEditing(node);
});
if (this.map.options.drag === true) {
outer.call(this.map.drag.getDragBehavior());
}
else {
outer.on("mousedown", function (node) {
_this.map.nodes.selectNode(node.id);
});
}
// Set text of the node
outer.insert("foreignObject")
.html(function (node) { return _this.createNodeNameDOM(node); })
.each(function (node) {
_this.updateNodeNameContainer(node);
});
// Set background of the node
outer.insert("path", "foreignObject")
.style("fill", function (node) { return node.colors.background; })
.style("stroke-width", 3)
.attr("d", function (node) { return _this.drawNodeBackground(node); });
// Set image of the node
outer.each(function (node) {
_this.setImage(node);
});
dom.branches.enter().insert("path", "g")
.style("fill", function (node) { return node.colors.branch; })
.style("stroke", function (node) { return node.colors.branch; })
.attr("class", this.map.id + "_branch")
.attr("id", function (node) { return node.id + "_branch"; })
.attr("d", function (node) { return _this.drawBranch(node); });
dom.nodes.exit().remove();
dom.branches.exit().remove();
};
/**
* Remove all nodes and branches of the map.
*/
Draw.prototype.clear = function () {
d3.selectAll("." + this.map.id + "_node, ." + this.map.id + "_branch").remove();
};
/**
* Draw the background shape of the node.
* @param {Node} node
* @returns {Path} path
*/
Draw.prototype.drawNodeBackground = function (node) {
var name = node.getNameDOM(), path = d3.path();
node.dimensions.width = name.clientWidth + 45;
node.dimensions.height = name.clientHeight + 30;
var x = node.dimensions.width / 2, y = node.dimensions.height / 2, k = node.k;
path.moveTo(-x, k / 3);
path.bezierCurveTo(-x, -y + 10, -x + 10, -y, k, -y);
path.bezierCurveTo(x - 10, -y, x, -y + 10, x, k / 3);
path.bezierCurveTo(x, y - 10, x - 10, y, k, y);
path.bezierCurveTo(-x + 10, y, -x, y - 10, -x, k / 3);
path.closePath();
return path;
};
/**
* Draw the branch of the node.
* @param {Node} node
* @returns {Path} path
*/
Draw.prototype.drawBranch = function (node) {
var parent = node.parent, path = d3.path(), level = node.getLevel(), width = 22 - (level < 6 ? level : 6) * 3, mx = (parent.coordinates.x + node.coordinates.x) / 2, ory = parent.coordinates.y < node.coordinates.y + node.dimensions.height / 2 ? -1 : 1, orx = parent.coordinates.x > node.coordinates.x ? -1 : 1, inv = orx * ory;
path.moveTo(parent.coordinates.x, parent.coordinates.y - width * .8);
path.bezierCurveTo(mx - width * inv, parent.coordinates.y - width / 2, parent.coordinates.x - width / 2 * inv, node.coordinates.y + node.dimensions.height / 2 - width / 3, node.coordinates.x - node.dimensions.width / 3 * orx, node.coordinates.y + node.dimensions.height / 2 + 3);
path.bezierCurveTo(parent.coordinates.x + width / 2 * inv, node.coordinates.y + node.dimensions.height / 2 + width / 3, mx + width * inv, parent.coordinates.y + width / 2, parent.coordinates.x, parent.coordinates.y + width * .8);
path.closePath();
return path;
};
/**
* Update the node HTML elements.
* @param {Node} node
*/
Draw.prototype.updateNodeShapes = function (node) {
var _this = this;
var background = node.getBackgroundDOM();
d3.select(background).attr("d", function (node) { return _this.drawNodeBackground(node); });
d3.selectAll("." + this.map.id + "_branch").attr("d", function (node) { return _this.drawBranch(node); });
this.updateImagePosition(node);
this.updateNodeNameContainer(node);
};
/**
* Set main properties of node image and create it if it does not exist.
* @param {Node} node
*/
Draw.prototype.setImage = function (node) {
var domImage = node.getImageDOM();
if (!domImage) {
domImage = document.createElementNS("http://www.w3.org/2000/svg", "image");
node.dom.appendChild(domImage);
}
if (node.image.src !== "") {
var image = new Image();
image.src = node.image.src;
image.onload = function () {
var h = node.image.size, w = this.width * h / this.height, y = -(h + node.dimensions.height / 2 + 5), x = -w / 2;
domImage.setAttribute("href", node.image.src);
domImage.setAttribute("height", h.toString());
domImage.setAttribute("width", w.toString());
domImage.setAttribute("y", y.toString());
domImage.setAttribute("x", x.toString());
};
image.onerror = function () {
domImage.remove();
node.image.src = "";
};
}
else {
domImage.remove();
}
};
/**
* Update the node image position.
* @param {Node} node
*/
Draw.prototype.updateImagePosition = function (node) {
if (node.image.src !== "") {
var image = node.getImageDOM(), y = -(image.getBBox().height + node.dimensions.height / 2 + 5);
image.setAttribute("y", y.toString());
}
};
/**
* Enable and manage all events for the name editing.
* @param {Node} node
*/
Draw.prototype.enableNodeNameEditing = function (node) {
var _this = this;
var name = node.getNameDOM();
Utils.focusWithCaretAtEnd(name);
name.style.setProperty("cursor", "auto");
name.ondblclick = name.onmousedown = function (event) {
event.stopPropagation();
};
name.oninput = function () {
_this.updateNodeShapes(node);
};
// Allow only some shortcuts.
name.onkeydown = function (event) {
// Unfocus the node.
if (event.code === 'Escape') {
Utils.removeAllRanges();
name.blur();
}
if (event.ctrlKey || event.metaKey) {
switch (event.code) {
case 'KeyA':
case 'KeyC':
case 'KeyV':
case 'KeyX':
case 'KeyZ':
case 'ArrowLeft':
case 'ArrowRight':
case 'ArrowUp':
case 'ArrowDown':
case 'Backspace':
case 'Delete':
return true;
default:
return false;
}
}
switch (event.code) {
case 'Tab':
return false;
default:
return true;
}
};
// Remove html formatting when paste text on node
name.onpaste = function (event) {
event.preventDefault();
var text = event.clipboardData.getData("text/plain");
document.execCommand("insertHTML", false, text);
};
name.onblur = function () {
name.innerHTML = name.innerHTML === "<br>" ? "" : name.innerHTML;
if (name.innerHTML !== node.name) {
_this.map.nodes.updateNode("name", name.innerHTML);
}
name.ondblclick = name.onmousedown = name.onblur =
name.onkeydown = name.oninput = name.onpaste = null;
name.style.setProperty("cursor", "pointer");
name.blur();
};
};
/**
* Update node name container (foreign object) dimensions.
* @param {Node} node
*/
Draw.prototype.updateNodeNameContainer = function (node) {
var name = node.getNameDOM(), foreignObject = name.parentNode;
if (name.offsetWidth !== 0) {
foreignObject.setAttribute("x", (-name.clientWidth / 2).toString());
foreignObject.setAttribute("y", (-name.clientHeight / 2).toString());
foreignObject.setAttribute("width", name.clientWidth.toString());
foreignObject.setAttribute("height", name.clientHeight.toString());
}
};
/**
* Create a string with HTML of the node name div.
* @param {Node} node
* @returns {string} html
*/
Draw.prototype.createNodeNameDOM = function (node) {
var div = document.createElement("div");
div.style.setProperty("font-size", node.font.size.toString() + "px");
div.style.setProperty("color", node.colors.name);
div.style.setProperty("font-style", node.font.style);
div.style.setProperty("font-weight", node.font.weight);
div.style.setProperty("text-decoration", node.font.decoration);
div.style.setProperty("display", "inline-block");
div.style.setProperty("white-space", "pre");
div.style.setProperty("font-family", this.map.options.fontFamily);
div.style.setProperty("text-align", "center");
div.setAttribute("contenteditable", "true");
div.innerHTML = node.name;
return div.outerHTML;
};
return Draw;
}());
/**
* Manage default map options.
*/
var Options = /** @class */ (function () {
/**
* Initialize all options.
* @param {OptionParameters} parameters
* @param {Map} map
*/
function Options(parameters, map) {
if (parameters === void 0) { parameters = {}; }
var _this = this;
this.update = function (property, value) {
if (typeof property !== "string") {
Log.error("The property must be a string", "type");
}
switch (property) {
case "fontFamily":
_this.updateFontFamily(value);
break;
case "centerOnResize":
_this.updateCenterOnResize(value);
break;
case "drag":
_this.updateDrag(value);
break;
case "zoom":
_this.updateZoom(value);
break;
case "defaultNode":
_this.updateDefaultNode(value);
break;
case "rootNode":
_this.updateDefaultRootNode(value);
break;
default:
Log.error("The property does not exist");
}
};
this.map = map;
this.fontFamily = parameters.fontFamily || "Arial, Helvetica, sans-serif";
this.centerOnResize = parameters.centerOnResize !== undefined ? parameters.centerOnResize : true;
this.drag = parameters.drag !== undefined ? parameters.drag : true;
this.zoom = parameters.zoom !== undefined ? parameters.zoom : true;
// Default node properties
this.defaultNode = Utils.mergeObjects({
name: "Node",
image: {
src: "",
size: 60
},
colors: {
name: "#787878",
background: "#f9f9f9",
branch: "#577a96"
},
font: {
size: 16,
style: "normal",
weight: "normal"
},
locked: true
}, parameters.defaultNode, true);
// Default root node properties
this.rootNode = Utils.mergeObjects({
name: "Root node",
image: {
src: "",
size: 70
},
colors: {
name: "#787878",
background: "#f0f6f5",
branch: ""
},
font: {
size: 20,
style: "normal",
weight: "normal"
}
}, parameters.rootNode, true);
}
/**
* Update the font family of all nodes.
* @param {string} font
*/
Options.prototype.updateFontFamily = function (font) {
if (typeof font !== "string") {
Log.error("The font must be a string", "type");
}
this.fontFamily = font;
this.map.draw.update();
};
/**
* Update centerOnResize behavior.
* @param {boolean} flag
*/
Options.prototype.updateCenterOnResize = function (flag) {
var _this = this;
if (typeof flag !== "boolean") {
Log.error("The value must be a boolean", "type");
}
this.centerOnResize = flag;
if (this.centerOnResize === true) {
d3.select(window).on("resize." + this.map.id, function () {
_this.map.zoom.center();
});
}
else {
d3.select(window).on("resize." + this.map.id, null);
}
};
/**
* Update drag behavior.
* @param {boolean} flag
*/
Options.prototype.updateDrag = function (flag) {
if (typeof flag !== "boolean") {
Log.error("The value must be a boolean", "type");
}
this.drag = flag;
this.map.draw.clear();
this.map.draw.update();
};
/**
* Update zoom behavior.
* @param {boolean} flag
*/
Options.prototype.updateZoom = function (flag) {
if (typeof flag !== "boolean") {
Log.error("The value must be a boolean", "type");
}
this.zoom = flag;
if (this.zoom === true) {
this.map.dom.svg.call(this.map.zoom.getZoomBehavior());
}
else {
this.map.dom.svg.on(".zoom", null);
}
};
/**
* Update default node properties.
* @param {DefaultNodeProperties} properties
*/
Options.prototype.updateDefaultNode = function (properties) {
this.defaultNode = Utils.mergeObjects(this.defaultNode, properties, true);
};
/**
* Update default root node properties.
* @param {DefaultNodeProperties} properties
*/
Options.prototype.updateDefaultRootNode = function (properties) {
this.rootNode = Utils.mergeObjects(this.rootNode, properties, true);
};
return Options;
}());
/**
* Model of the nodes.
*/
var Node = /** @class */ (function () {
/**
* Initialize the node properties, the dimensions and the k coefficient.
* @param {NodeProperties} properties
*/
function Node(properties) {
this.id = properties.id;
this.parent = properties.parent;
this.name = properties.name;
this.coordinates = properties.coordinates;
this.colors = properties.colors;
this.image = properties.image;
this.font = properties.font;
this.locked = properties.locked;
this.dimensions = {
width: 0,
height: 0
};
this.k = properties.k || d3.randomUniform(-20, 20)();
}
/**
* Return true if the node is the root or false.
* @returns {boolean}
*/
Node.prototype.isRoot = function () {
var words = this.id.split("_");
return words[words.length - 1] === "0";
};
/**
* Return the level of the node.
* @returns {number} level
*/
Node.prototype.getLevel = function () {
var level = 1, parent = this.parent;
while (parent) {
level++;
parent = parent.parent;
}
return level;
};
/**
* Return the div element of the node name.
* @returns {HTMLDivElement} div
*/
Node.prototype.getNameDOM = function () {
return this.dom.querySelector("foreignObject > div");
};
/**
* Return the SVG path of the node background.
* @returns {SVGPathElement} path
*/
Node.prototype.getBackgroundDOM = function () {
return this.dom.querySelector("path");
};
/**
* Return the SVG image of the node image.
* @returns {SVGImageElement} image
*/
Node.prototype.getImageDOM = function () {
return this.dom.querySelector("image");
};
return Node;
}());
/**
* Manage map history, for each change save a snapshot.
*/
var History = /** @class */ (function () {
/**
* Get the associated map instance, initialize index and snapshots.
* @param {Map} map
*/
function History(map) {
var _this = this;
/**
* Return last snapshot of the current map.
* @return {MapSnapshot} [snapshot] - Last snapshot of the map.
*/
this.current = function () {
return _this.snapshots[_this.index];
};
/**
* Replace old map with a new one or create a new empty map.
* @param {MapSnapshot} snapshot
*/
this.new = function (snapshot) {
if (snapshot === undefined) {
var oldRootCoordinates = Utils.cloneObject(_this.map.nodes.getRoot().coordinates);
_this.map.nodes.setCounter(0);
_this.map.nodes.clear();
_this.map.draw.clear();
_this.map.draw.update();
_this.map.nodes.addRootNode(oldRootCoordinates);
_this.map.zoom.center(null, 0);
_this.save();
_this.map.events.call(Event.create);
}
else if (_this.checkSnapshotStructure(snapshot)) {
_this.redraw(snapshot);
_this.map.zoom.center("position", 0);
_this.save();
_this.map.events.call(Event.create);
}
else {
Log.error("The snapshot is not correct");
}
};
/**
* Undo last changes.
*/
this.undo = function () {
if (_this.index > 0) {
_this.redraw(_this.snapshots[--_this.index]);
_this.map.events.call(Event.undo);
}
};
/**
* Redo one change which was undone.
*/
this.redo = function () {
if (_this.index < _this.snapshots.length - 1) {
_this.redraw(_this.snapshots[++_this.index]);
_this.map.events.call(Event.redo);
}
};
/**
* Return all history of map with all snapshots.
* @returns {MapSnapshot[]}
*/
this.getHistory = function () {
return {
snapshots: _this.snapshots.slice(0),
index: _this.index
};
};
this.map = map;
this.index = -1;
this.snapshots = [];
}
/**
* Save the current snapshot of the mind map.
*/
History.prototype.save = function () {
if (this.index < this.snapshots.length - 1) {
this.snapshots.splice(this.index + 1);
}
this.snapshots.push(this.getSnapshot());
this.index++;
};
/**
* Redraw the map with a new snapshot.
* @param {MapSnapshot} snapshot
*/
History.prototype.redraw = function (snapshot) {
var _this = this;
this.map.nodes.clear();
snapshot.forEach(function (property) {
var properties = {
id: _this.sanitizeNodeId(property.id),
parent: _this.map.nodes.getNode(_this.sanitizeNodeId(property.parent)),
k: property.k,
name: property.name,
coordinates: Utils.cloneObject(property.coordinates),
image: Utils.cloneObject(property.image),
colors: Utils.cloneObject(property.colors),
font: Utils.cloneObject(property.font),
locked: property.locked
};
var node = new Node(properties);
_this.map.nodes.setNode(node.id, node);
});
this.map.draw.clear();
this.map.draw.update();
this.map.nodes.selectRootNode();
this.setCounter();
};
/**
* Return a copy of all fundamental node properties.
* @return {MapSnapshot} properties
*/
History.prototype.getSnapshot = function () {
var _this = this;
return this.map.nodes.getNodes().map(function (node) {
return _this.map.nodes.getNodeProperties(node, false);
}).slice();
};
/**
* Set the right counter value of the nodes.
*/
History.prototype.setCounter = function () {
var id = this.map.nodes.getNodes().map(function (node) {
var words = node.id.split("_");
return parseInt(words[words.length - 1]);
});
this.map.nodes.setCounter(Math.max.apply(Math, id) + 1);
};
/**
* Sanitize an old map node id with a new.
* @param {string} oldId
* @returns {string} newId
*/
History.prototype.sanitizeNodeId = function (oldId) {
if (typeof oldId === "string") {
var words = oldId.split("_");
return this.map.id + "_" + words[words.length - 2] + "_" + words[words.length - 1];
}
};
/**
* Check the snapshot structure and return true if it is authentic.
* @param {MapSnapshot} snapshot
* @return {boolean} result
*/
History.prototype.checkSnapshotStructure = function (snapshot) {
if (!Array.isArray(snapshot)) {
return false;
}
if ((snapshot[0].key && snapshot[0].value)) {
this.convertOldMmp(snapshot);
}
for (var _i = 0, snapshot_1 = snapshot; _i < snapshot_1.length; _i++) {
var node = snapshot_1[_i];
if (!this.checkNodeProperties(node)) {
return false;
}
}
this.translateNodePositions(snapshot);
return true;
};
/**
* Check the snapshot node properties and return true if they are authentic.
* @param {ExportNodeProperties} node
* @return {boolean} result
*/
History.prototype.checkNodeProperties = function (node) {
var conditions = [
typeof node.id === "string",
typeof node.parent === "string",
typeof node.k === "number",
typeof node.name === "string",
typeof node.locked === "boolean",
node.coordinates
&& typeof node.coordinates.x === "number"
&& typeof node.coordinates.y === "number",
node.image
&& typeof node.image.size === "number"
&& typeof node.image.src === "string",
node.colors
&& typeof node.colors.background === "string"
&& typeof node.colors.branch === "string"
&& typeof node.colors.name === "string",
node.font
&& typeof node.font.size === "number"
&& typeof node.font.weight === "string"
&& typeof node.font.style === "string"
];
return conditions.every(function (condition) { return condition; });
};
/**
* Convert the old mmp (version: 0.1.7) snapshot to new.
* @param {Array} snapshot
*/
History.prototype.convertOldMmp = function (snapshot) {
for (var _i = 0, snapshot_2 = snapshot; _i < snapshot_2.length; _i++) {
var node = snapshot_2[_i];
var oldNode = Utils.cloneObject(node);
Utils.clearObject(node);
node.id = "map_node_" + oldNode.key.substr(4);
node.parent = oldNode.value.parent ? "map_node_" + oldNode.value.parent.substr(4) : "";
node.k = oldNode.value.k;
node.name = oldNode.value.name;
node.locked = oldNode.value.fixed;
node.coordinates = {
x: oldNode.value.x,
y: oldNode.value.y
};
node.image = {
size: parseInt(oldNode.value["image-size"]),
src: oldNode.value["image-src"]
};
node.colors = {
background: oldNode.value["background-color"],
branch: oldNode.value["branch-color"] || "",
name: oldNode.value["text-color"]
};
node.font = {
size: parseInt(oldNode.value["font-size"]),
weight: oldNode.value.bold ? "bold" : "normal",
style: oldNode.value.italic ? "italic" : "normal"
};
}
};
/**
* Adapt the coordinates to the old map.
* @param {MapSnapshot} snapshot
*/
History.prototype.translateNodePositions = function (snapshot) {
var oldRootNode = this.map.nodes.getRoot(), newRootNode = snapshot.find(function (node) {
var words = node.id.split("_");
return words[words.length - 1] === "0";
}), dx = newRootNode.coordinates.x - oldRootNode.coordinates.x, dy = newRootNode.coordinates.y - oldRootNode.coordinates.y;
for (var _i = 0, snapshot_3 = snapshot; _i < snapshot_3.length; _i++) {
var node = snapshot_3[_i];
node.coordinates.x -= dx;
node.coordinates.y -= dy;
}
};
return History;
}());
/**
* Manage the drag events of the nodes.
*/
var Drag = /** @class */ (function () {
/**
* Get the associated map instance and initialize the d3 drag behavior.
* @param {Map} map
*/
function Drag(map) {
var _this = this;
this.map = map;
this.dragBehavior = d3.drag()
.on("start", function (node) { return _this.started(node); })
.on("drag", function (node) { return _this.dragged(node); })
.on("end", function (node) { return _this.ended(node); });
}
/**
* Return the d3 drag behavior
* @returns {DragBehavior} dragBehavior
*/
Drag.prototype.getDragBehavior = function () {
return this.dragBehavior;
};
/**
* Select the node and calculate node position data for dragging.
* @param {Node} node
*/
Drag.prototype.started = function (node) {
d3.event.sourceEvent.preventDefault();
this.orientation = this.map.nodes.getOrientation(node);
this.descendants = this.map.nodes.getDescendants(node);
this.map.nodes.selectNode(node.id);
};
/**
* Move the dragged node and if it is locked all their descendants.
* @param {Node} node
*/
Drag.prototype.dragged = function (node) {
var _this = this;
var dy = d3.event.dy, dx = d3.event.dx;
// Set new coordinates
var x = node.coordinates.x += dx, y = node.coordinates.y += dy;
// Move graphically the node in new coordinates
node.dom.setAttribute("transform", "translate(" + [x, y] + ")");
// If the node is locked move also descendants
if (node.locked) {
// Check if old and new orientation are equal
var newOrientation = this.map.nodes.getOrientation(node), orientationIsChanged = newOrientation !== this.orientation, root = node;
for (var _i = 0, _a = this.descendants; _i < _a.length; _i++) {
var node_1 = _a[_i];
var x_1 = node_1.coordinates.x += dx, y_1 = node_1.coordinates.y += dy;
if (orientationIsChanged) {
x_1 = node_1.coordinates.x += (root.coordinates.x - node_1.coordinates.x) * 2;
}
node_1.dom.setAttribute("transform", "translate(" + [x_1, y_1] + ")");
}
if (orientationIsChanged) {
this.orientation = newOrientation;
}
}
// Update all mind map branches
d3.selectAll("." + this.map.id + "_branch").attr("d", function (node) {
return _this.map.draw.drawBranch(node);
});
// This is here and not in the started function because started function
// is also executed when there is no drag events
this.dragging = true;
};
/**
* If the node was actually dragged change the state of dragging and save the snapshot.
* @param {Node} node
*/
Drag.prototype.ended = function (node) {
if (this.dragging) {
this.dragging = false;
this.map.history.save();
this.map.events.call(Event.nodeUpdate, node.dom, this.map.nodes.getNodeProperties(node));
}
};
return Drag;
}());
/**
* Manage the nodes of the map.
*/
var Nodes = /** @class */ (function () {
/**
* Get the associated map instance and initialize counter and nodes.
* @param {Map} map
*/
function Nodes(map) {
var _this = this;
/**
* Add a node in the map.
* @param {UserNodeProperties} userProperties
* @param {string} id
*/
this.addNode = function (userProperties, id) {
if (id && typeof id !== "string") {
Log.error("The node id must be a string", "type");
}
var parentNode = id ? _this.getNode(id) : _this.selectedNode;
if (parentNode === undefined) {
Log.error("There are no nodes with id \"" + id + "\"");
}
var properties = Utils.mergeObjects(_this.map.options.defaultNode, userProperties, true);
properties.id = _this.map.id + "_node_" + _this.counter;
properties.parent = parentNode;
var node = new Node(properties);
_this.nodes.set(properties.id, node);
_this.counter++;
// Set coordinates
node.coordinates = _this.calculateCoordinates(node);
if (userProperties && userProperties.coordinates) {
var fixedCoordinates = _this.fixCoordinates(userProperties.coordinates);
node.coordinates = Utils.mergeObjects(node.coordinates, fixedCoordinates, true);
}
_this.map.draw.update();
_this.map.history.save();
_this.map.events.call(Event.nodeCreate, node.dom, _this.getNodeProperties(node));
};
/**
* Select a node or return the current selected node.
* @param {string} id
* @returns {ExportNodeProperties}
*/
this.selectNode = function (id) {
if (id !== undefined) {
if (typeof id !== "string") {
Log.error("The node id must be a string", "type");
}
if (!_this.nodeSelectionTo(id)) {
if (_this.nodes.has(id)) {
var node = _this.nodes.get(id), background = node.getBackgroundDOM();
if (!background.style.stroke) {
if (_this.selectedNode) {
_this.selectedNode.getBackgroundDOM().style.stroke = "";
}
var color = d3.color(background.style.fill).darker(.5);
background.style.stroke = color.toString();
Utils.removeAllRanges();
_this.selectedNode.getNameDOM().blur();
_this.selectedNode = node;
_this.map.events.call(Event.nodeSelect, node.dom, _this.getNodeProperties(node));
}
}
else {
Log.error("The node id or the direction is not correct");
}
}
}
return _this.getNodeProperties(_this.selectedNode);
};
/**
* Enable the node name editing of the selected node.
*/
this.editNode = function () {
if (_this.selectedNode) {
_this.map.draw.enableNodeNameEditing(_this.selectedNode);
}
};
/**
* Deselect the current selected node.
*/
this.deselectNode = function () {
if (_this.selectedNode) {
_this.selectedNode.getBackgroundDOM().style.stroke = "";
Utils.removeAllRanges();
}
_this.selectRootNode();
_this.map.events.call(Event.nodeDeselect);
};
/**
* Update the properties of the selected node.
* @param {string} property
* @param value
* @param {string} id
* @param {boolean} graphic
*/
this.updateNode = function (property, value, graphic, id) {
if (graphi