2022-11-10 09:10:58 +08:00

5413 lines
136 KiB
JavaScript

/* My Mind web app: all source files combined. */
if (!Function.prototype.bind) {
Function.prototype.bind = function(thisObj) {
var fn = this;
var args = Array.prototype.slice.call(arguments, 1);
return function() {
return fn.apply(thisObj, args.concat(Array.prototype.slice.call(arguments)));
}
}
};
var MM = {
_subscribers: {},
publish: function(message, publisher, data) {
var subscribers = this._subscribers[message] || [];
subscribers.forEach(function(subscriber) {
subscriber.handleMessage(message, publisher, data);
});
},
subscribe: function(message, subscriber) {
if (!(message in this._subscribers)) {
this._subscribers[message] = [];
}
var index = this._subscribers[message].indexOf(subscriber);
if (index == -1) { this._subscribers[message].push(subscriber); }
},
unsubscribe: function(message, subscriber) {
var index = this._subscribers[message].indexOf(subscriber);
if (index > -1) { this._subscribers[message].splice(index, 1); }
},
generateId: function() {
var str = "";
for (var i=0;i<8;i++) {
var code = Math.floor(Math.random()*26);
str += String.fromCharCode("a".charCodeAt(0) + code);
}
return str;
},
isMac: function() {
return !!navigator.platform.match(/mac/i);
}
};
/*
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
*/
/**
* @class A promise - value to be resolved in the future.
* Implements the "Promises/A+" specification.
*/
var Promise = function(executor) {
this._state = 0; /* 0 = pending, 1 = fulfilled, 2 = rejected */
this._value = null; /* fulfillment / rejection value */
this._cb = {
fulfilled: [],
rejected: []
}
this._thenPromises = []; /* promises returned by then() */
executor && executor(this.fulfill.bind(this), this.reject.bind(this));
}
Promise.resolve = function(value) {
return new Promise().fulfill(value);
}
Promise.reject = function(value) {
return new Promise().reject(value);
}
/**
* @param {function} onFulfilled To be called once this promise gets fulfilled
* @param {function} onRejected To be called once this promise gets rejected
* @returns {Promise}
*/
Promise.prototype.then = function(onFulfilled, onRejected) {
this._cb.fulfilled.push(onFulfilled);
this._cb.rejected.push(onRejected);
var thenPromise = new Promise();
this._thenPromises.push(thenPromise);
if (this._state > 0) {
setTimeout(this._processQueue.bind(this), 0);
}
/* 3.2.6. then must return a promise. */
return thenPromise;
}
/**
* Fulfill this promise with a given value
* @param {any} value
*/
Promise.prototype.fulfill = function(value) {
if (this._state != 0) { return this; }
this._state = 1;
this._value = value;
this._processQueue();
return this;
}
/**
* Reject this promise with a given value
* @param {any} value
*/
Promise.prototype.reject = function(value) {
if (this._state != 0) { return this; }
this._state = 2;
this._value = value;
this._processQueue();
return this;
}
/**
* Pass this promise's resolved value to another promise
* @param {Promise} promise
*/
Promise.prototype.chain = function(promise) {
return this.then(promise.fulfill.bind(promise), promise.reject.bind(promise));
}
/**
* @param {function} onRejected To be called once this promise gets rejected
* @returns {Promise}
*/
Promise.prototype["catch"] = function(onRejected) {
return this.then(null, onRejected);
}
Promise.prototype._processQueue = function() {
while (this._thenPromises.length) {
var onFulfilled = this._cb.fulfilled.shift();
var onRejected = this._cb.rejected.shift();
this._executeCallback(this._state == 1 ? onFulfilled : onRejected);
}
}
Promise.prototype._executeCallback = function(cb) {
var thenPromise = this._thenPromises.shift();
if (typeof(cb) != "function") {
if (this._state == 1) {
/* 3.2.6.4. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value. */
thenPromise.fulfill(this._value);
} else {
/* 3.2.6.5. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason. */
thenPromise.reject(this._value);
}
return;
}
try {
var returned = cb(this._value);
if (returned && typeof(returned.then) == "function") {
/* 3.2.6.3. If either onFulfilled or onRejected returns a promise (call it returnedPromise), promise2 must assume the state of returnedPromise */
var fulfillThenPromise = function(value) { thenPromise.fulfill(value); }
var rejectThenPromise = function(value) { thenPromise.reject(value); }
returned.then(fulfillThenPromise, rejectThenPromise);
} else {
/* 3.2.6.1. If either onFulfilled or onRejected returns a value that is not a promise, promise2 must be fulfilled with that value. */
thenPromise.fulfill(returned);
}
} catch (e) {
/* 3.2.6.2. If either onFulfilled or onRejected throws an exception, promise2 must be rejected with the thrown exception as the reason. */
thenPromise.reject(e);
}
}
/**
* Wait for all these promises to complete. One failed => this fails too.
*/
Promise.all = Promise.when = function(all) {
var promise = new this();
var counter = 0;
var results = [];
for (var i=0;i<all.length;i++) {
counter++;
all[i].then(function(index, result) {
results[index] = result;
counter--;
if (!counter) { promise.fulfill(results); }
}.bind(null, i), function(reason) {
counter = 1/0;
promise.reject(reason);
});
}
return promise;
}
/**
* Promise-based version of setTimeout
*/
Promise.setTimeout = function(ms) {
var promise = new this();
setTimeout(function() { promise.fulfill(); }, ms);
return promise;
}
/**
* Promise-based version of addEventListener
*/
Promise.event = function(element, event, capture) {
var promise = new this();
var cb = function(e) {
element.removeEventListener(event, cb, capture);
promise.fulfill(e);
}
element.addEventListener(event, cb, capture);
return promise;
}
/**
* Promise-based wait for CSS transition end
*/
Promise.transition = function(element) {
if ("transition" in element.style) {
return this.event(element, "transitionend", false);
} else if ("webkitTransition" in element.style) {
return this.event(element, "webkitTransitionEnd", false);
} else {
return new this().fulfill();
}
}
/**
* Promise-based version of XMLHttpRequest::send
*/
Promise.send = function(xhr, data) {
var promise = new this();
xhr.addEventListener("readystatechange", function(e) {
if (e.target.readyState != 4) { return; }
if (e.target.status.toString().charAt(0) == "2") {
promise.fulfill(e.target);
} else {
promise.reject(e.target);
}
});
xhr.send(data);
return promise;
}
Promise.worker = function(url, message) {
var promise = new this();
var worker = new Worker(url);
Promise.event(worker, "message").then(function(e) {
promise.fulfill(e.data);
});
Promise.event(worker, "error").then(function(e) {
promise.reject(e.message);
});
worker.postMessage(message);
return promise;
}
/**
* Prototype for all things categorizable: shapes, layouts, commands, formats, backends...
*/
MM.Repo = {
id: "", /* internal ID */
label: "", /* human-readable label */
getAll: function() {
var all = [];
for (var p in this) {
var val = this[p];
if (this.isPrototypeOf(val)) { all.push(val); }
}
return all;
},
getByProperty: function(property, value) {
return this.getAll().filter(function(item) {
return item[property] == value;
})[0] || null;
},
getById: function(id) {
return this.getByProperty("id", id);
},
buildOption: function() {
var o = document.createElement("option");
o.value = this.id;
o.innerHTML = this.label;
return o;
}
}
MM.Item = function() {
this._parent = null;
this._children = [];
this._collapsed = false;
this._layout = null;
this._shape = null;
this._autoShape = true;
this._color = null;
this._value = null;
this._status = null;
this._side = null; /* side preference */
this._icon = null;
this._notes = null;
this._id = MM.generateId();
this._oldText = "";
this._computed = {
value: 0,
status: null
}
this._dom = {
node: document.createElement("li"),
content: document.createElement("div"),
notes: document.createElement("div"),
status: document.createElement("span"),
icon: document.createElement("span"),
value: document.createElement("span"),
text: document.createElement("div"),
children: document.createElement("ul"),
toggle: document.createElement("div"),
canvas: document.createElement("canvas")
}
this._dom.node.classList.add("item");
this._dom.content.classList.add("content");
this._dom.notes.classList.add("notes-indicator");
this._dom.status.classList.add("status");
this._dom.icon.classList.add("icon");
this._dom.value.classList.add("value");
this._dom.text.classList.add("text");
this._dom.toggle.classList.add("toggle");
this._dom.children.classList.add("children");
this._dom.content.appendChild(this._dom.text); /* status+value are appended in layout */
this._dom.node.appendChild(this._dom.canvas);
this._dom.node.appendChild(this._dom.content);
this._dom.content.appendChild(this._dom.notes);
/* toggle+children are appended when children exist */
this._dom.toggle.addEventListener("click", this);
}
MM.Item.COLOR = "#999";
/* RE explanation:
* _________________________________________________________________________ One of the three possible variants
* ____________________ scheme://x
* ___________________________ aa.bb.cc
* _______________________ aa.bb/
* ______ path, search
* __________________________ end with a non-forbidden char
* ______ end of word or end of string
*/
MM.Item.RE = /\b(([a-z][\w-]+:\/\/\w)|(([\w-]+\.){2,}[a-z][\w-]+)|([\w-]+\.[a-z][\w-]+\/))[^\s]*([^\s,.;:?!<>\(\)\[\]'"])?($|\b)/i;
MM.Item.fromJSON = function(data) {
return new this().fromJSON(data);
}
MM.Item.prototype.toJSON = function() {
var data = {
id: this._id,
text: this.getText(),
notes: this.getNotes()
}
if (this._side) { data.side = this._side; }
if (this._color) { data.color = this._color; }
if (this._icon) { data.icon = this._icon; }
if (this._value) { data.value = this._value; }
if (this._status) { data.status = this._status; }
if (this._layout) { data.layout = this._layout.id; }
if (!this._autoShape) { data.shape = this._shape.id; }
if (this._collapsed) { data.collapsed = 1; }
if (this._children.length) {
data.children = this._children.map(function(child) { return child.toJSON(); });
}
return data;
}
/**
* Only when creating a new item. To merge existing items, use .mergeWith().
*/
MM.Item.prototype.fromJSON = function(data) {
this.setText(data.text);
if (data.notes) {
this.setNotes(data.notes);
}
if (data.id) { this._id = data.id; }
if (data.side) { this._side = data.side; }
if (data.color) { this._color = data.color; }
if (data.icon) { this._icon = data.icon; }
if (data.value) { this._value = data.value; }
if (data.status) {
this._status = data.status;
if (this._status == "maybe") { this._status = "computed"; }
}
if (data.collapsed) { this.collapse(); }
if (data.layout) { this._layout = MM.Layout.getById(data.layout); }
if (data.shape) { this.setShape(MM.Shape.getById(data.shape)); }
(data.children || []).forEach(function(child) {
this.insertChild(MM.Item.fromJSON(child));
}, this);
return this;
}
MM.Item.prototype.mergeWith = function(data) {
var dirty = 0;
if (this.getText() != data.text && !this._dom.text.contentEditable) { this.setText(data.text); }
if (this._side != data.side) {
this._side = data.side;
dirty = 1;
}
if (this._color != data.color) {
this._color = data.color;
dirty = 2;
}
if (this._icon != data.icon) {
this._icon = data.icon;
dirty = 1;
}
if (this._value != data.value) {
this._value = data.value;
dirty = 1;
}
if (this._status != data.status) {
this._status = data.status;
dirty = 1;
}
if (this._collapsed != !!data.collapsed) { this[this._collapsed ? "expand" : "collapse"](); }
if (this.getOwnLayout() != data.layout) {
this._layout = MM.Layout.getById(data.layout);
dirty = 2;
}
var s = (this._autoShape ? null : this._shape.id);
if (s != data.shape) { this.setShape(MM.Shape.getById(data.shape)); }
(data.children || []).forEach(function(child, index) {
if (index >= this._children.length) { /* new child */
this.insertChild(MM.Item.fromJSON(child));
} else { /* existing child */
var myChild = this._children[index];
if (myChild.getId() == child.id) { /* recursive merge */
myChild.mergeWith(child);
} else { /* changed; replace */
this.removeChild(this._children[index]);
this.insertChild(MM.Item.fromJSON(child), index);
}
}
}, this);
/* remove dead children */
var newLength = (data.children || []).length;
while (this._children.length > newLength) { this.removeChild(this._children[this._children.length-1]); }
if (dirty == 1) { this.update(); }
if (dirty == 2) { this.updateSubtree(); }
}
MM.Item.prototype.clone = function() {
var data = this.toJSON();
var removeId = function(obj) {
delete obj.id;
obj.children && obj.children.forEach(removeId);
}
removeId(data);
return this.constructor.fromJSON(data);
}
MM.Item.prototype.select = function() {
this._dom.node.classList.add("current");
if (window.editor) {
if (this._notes) {
window.editor.setContent(this._notes);
} else {
window.editor.setContent('');
}
}
this.getMap().ensureItemVisibility(this);
MM.Clipboard.focus(); /* going to mode 2c */
MM.publish("item-select", this);
}
MM.Item.prototype.deselect = function() {
/* we were in 2b; finish that via 3b */
if (MM.App.editing) { MM.Command.Finish.execute(); }
this._dom.node.classList.remove("current");
}
MM.Item.prototype.update = function(doNotRecurse) {
var map = this.getMap();
if (!map || !map.isVisible()) { return this; }
MM.publish("item-change", this);
if (this._autoShape) { /* check for changed auto-shape */
var autoShape = this._getAutoShape();
if (autoShape != this._shape) {
if (this._shape) { this._shape.unset(this); }
this._shape = autoShape;
this._shape.set(this);
}
}
this._updateStatus();
this._updateIcon();
this._updateNotesIndicator();
this._updateValue();
this._dom.node.classList[this._collapsed ? "add" : "remove"]("collapsed");
this.getLayout().update(this);
this.getShape().update(this);
if (!this.isRoot() && !doNotRecurse) { this._parent.update(); }
return this;
}
MM.Item.prototype.updateSubtree = function(isSubChild) {
this._children.forEach(function(child) {
child.updateSubtree(true);
});
return this.update(isSubChild);
}
MM.Item.prototype.setText = function(text) {
this._dom.text.innerHTML = text;
this._findLinks(this._dom.text);
return this.update();
}
MM.Item.prototype.setNotes = function(notes) {
this._notes = notes;
return this.update();
}
MM.Item.prototype.getId = function() {
return this._id;
}
MM.Item.prototype.getText = function() {
return this._dom.text.innerHTML;
}
MM.Item.prototype.getNotes = function() {
return this._notes;
}
MM.Item.prototype.collapse = function() {
if (this._collapsed) { return; }
this._collapsed = true;
return this.update();
}
MM.Item.prototype.expand = function() {
if (!this._collapsed) { return; }
this._collapsed = false;
this.update();
return this.updateSubtree();
}
MM.Item.prototype.isCollapsed = function() {
return this._collapsed;
}
MM.Item.prototype.setValue = function(value) {
this._value = value;
return this.update();
}
MM.Item.prototype.getValue = function() {
return this._value;
}
MM.Item.prototype.getComputedValue = function() {
return this._computed.value;
}
MM.Item.prototype.setStatus = function(status) {
this._status = status;
return this.update();
}
MM.Item.prototype.getStatus = function() {
return this._status;
}
MM.Item.prototype.setIcon = function(icon) {
this._icon = icon;
return this.update();
}
MM.Item.prototype.getIcon = function() {
return this._icon;
}
MM.Item.prototype.getComputedStatus = function() {
return this._computed.status;
}
MM.Item.prototype.setSide = function(side) {
this._side = side;
return this;
}
MM.Item.prototype.getSide = function() {
return this._side;
}
MM.Item.prototype.getChildren = function() {
return this._children;
}
MM.Item.prototype.setColor = function(color) {
this._color = color;
return this.updateSubtree();
}
MM.Item.prototype.getColor = function() {
return this._color || (this.isRoot() ? MM.Item.COLOR : this._parent.getColor());
}
MM.Item.prototype.getOwnColor = function() {
return this._color;
}
MM.Item.prototype.getLayout = function() {
return this._layout || this._parent.getLayout();
}
MM.Item.prototype.getOwnLayout = function() {
return this._layout;
}
MM.Item.prototype.setLayout = function(layout) {
this._layout = layout;
return this.updateSubtree();
}
MM.Item.prototype.getShape = function() {
return this._shape;
}
MM.Item.prototype.getOwnShape = function() {
return (this._autoShape ? null : this._shape);
}
MM.Item.prototype.setShape = function(shape) {
if (this._shape) { this._shape.unset(this); }
if (shape) {
this._autoShape = false;
this._shape = shape;
} else {
this._autoShape = true;
this._shape = this._getAutoShape();
}
this._shape.set(this);
return this.update();
}
MM.Item.prototype.getDOM = function() {
return this._dom;
}
MM.Item.prototype.getMap = function() {
var item = this._parent;
while (item) {
if (item instanceof MM.Map) { return item; }
item = item.getParent();
}
return null;
}
MM.Item.prototype.getParent = function() {
return this._parent;
}
MM.Item.prototype.isRoot = function() {
return (this._parent instanceof MM.Map);
}
MM.Item.prototype.setParent = function(parent) {
this._parent = parent;
return this.updateSubtree();
}
MM.Item.prototype.insertChild = function(child, index) {
/* Create or remove child as necessary. This must be done before computing the index (inserting own child) */
var newChild = false;
if (!child) {
child = new MM.Item();
newChild = true;
} else if (child.getParent() && child.getParent().removeChild) { /* only when the child has non-map parent */
child.getParent().removeChild(child);
}
if (!this._children.length) {
this._dom.node.appendChild(this._dom.toggle);
this._dom.node.appendChild(this._dom.children);
}
if (arguments.length < 2) { index = this._children.length; }
var next = null;
if (index < this._children.length) { next = this._children[index].getDOM().node; }
this._dom.children.insertBefore(child.getDOM().node, next);
this._children.splice(index, 0, child);
return child.setParent(this);
}
MM.Item.prototype.removeChild = function(child) {
var index = this._children.indexOf(child);
this._children.splice(index, 1);
var node = child.getDOM().node;
node.parentNode.removeChild(node);
child.setParent(null);
if (!this._children.length) {
this._dom.toggle.parentNode.removeChild(this._dom.toggle);
this._dom.children.parentNode.removeChild(this._dom.children);
}
return this.update();
}
MM.Item.prototype.startEditing = function() {
this._oldText = this.getText();
this._dom.text.contentEditable = true;
this._dom.text.focus(); /* switch to 2b */
document.execCommand("styleWithCSS", null, false);
this._dom.text.addEventListener("input", this);
this._dom.text.addEventListener("keydown", this);
this._dom.text.addEventListener("blur", this);
return this;
}
MM.Item.prototype.stopEditing = function() {
this._dom.text.removeEventListener("input", this);
this._dom.text.removeEventListener("keydown", this);
this._dom.text.removeEventListener("blur", this);
this._dom.text.blur();
this._dom.text.contentEditable = false;
var result = this._dom.text.innerHTML;
this._dom.text.innerHTML = this._oldText;
this._oldText = "";
this.update(); /* text changed */
MM.Clipboard.focus();
return result;
}
MM.Item.prototype.handleEvent = function(e) {
switch (e.type) {
case "input":
this.update();
this.getMap().ensureItemVisibility(this);
break;
case "keydown":
if (e.keyCode == 9) { e.preventDefault(); } /* TAB has a special meaning in this app, do not use it to change focus */
break;
case "blur": /* 3d */
MM.Command.Finish.execute();
break;
case "click":
if (this._collapsed) { this.expand(); } else { this.collapse(); }
MM.App.select(this);
break;
}
}
MM.Item.prototype._getAutoShape = function() {
var depth = 0;
var node = this;
while (!node.isRoot()) {
depth++;
node = node.getParent();
}
switch (depth) {
case 0: return MM.Shape.Ellipse;
case 1: return MM.Shape.Box;
default: return MM.Shape.Underline;
}
}
MM.Item.prototype._updateStatus = function() {
this._dom.status.className = "status";
this._dom.status.style.display = "";
var status = this._status;
if (this._status == "computed") {
var childrenStatus = this._children.every(function(child) {
return (child.getComputedStatus() !== false);
});
status = (childrenStatus ? "yes" : "no");
}
switch (status) {
case "yes":
this._dom.status.classList.add("yes");
this._computed.status = true;
break;
case "no":
this._dom.status.classList.add("no");
this._computed.status = false;
break;
default:
this._computed.status = null;
this._dom.status.style.display = "none";
break;
}
}
MM.Item.prototype._updateIcon = function() {
this._dom.icon.className = "icon";
this._dom.icon.style.display = "";
var icon = this._icon;
if (icon)
{
this._dom.icon.classList.add('fa');
this._dom.icon.classList.add(icon);
this._computed.icon = true;
} else {
this._computed.icon = null;
this._dom.icon.style.display = "none";
}
}
MM.Item.prototype._updateNotesIndicator = function() {
if (this._notes)
{
this._dom.notes.classList.add("notes-indicator-visible");
} else {
this._dom.notes.classList.remove("notes-indicator-visible");
}
}
MM.Item.prototype._updateValue = function() {
this._dom.value.style.display = "";
if (typeof(this._value) == "number") {
this._computed.value = this._value;
this._dom.value.innerHTML = this._value;
return;
}
var childValues = this._children.map(function(child) {
return child.getComputedValue();
});
var result = 0;
switch (this._value) {
case "sum":
result = childValues.reduce(function(prev, cur) {
return prev+cur;
}, 0);
break;
case "avg":
var sum = childValues.reduce(function(prev, cur) {
return prev+cur;
}, 0);
result = (childValues.length ? sum/childValues.length : 0);
break;
case "max":
result = Math.max.apply(Math, childValues);
break;
case "min":
result = Math.min.apply(Math, childValues);
break;
default:
this._computed.value = 0;
this._dom.value.innerHTML = "";
this._dom.value.style.display = "none";
return;
break;
}
this._computed.value = result;
this._dom.value.innerHTML = (Math.round(result) == result ? result : result.toFixed(3));
}
MM.Item.prototype._findLinks = function(node) {
var children = [].slice.call(node.childNodes);
for (var i=0;i<children.length;i++) {
var child = children[i];
switch (child.nodeType) {
case 1: /* element */
if (child.nodeName.toLowerCase() == "a") { continue; }
this._findLinks(child);
break;
case 3: /* text */
var result = child.nodeValue.match(this.constructor.RE);
if (result) {
var before = child.nodeValue.substring(0, result.index);
var after = child.nodeValue.substring(result.index + result[0].length);
var link = document.createElement("a");
link.innerHTML = link.href = result[0];
if (before) {
node.insertBefore(document.createTextNode(before), child);
}
node.insertBefore(link, child);
if (after) {
child.nodeValue = after;
i--; /* re-try with the aftertext */
} else {
node.removeChild(child);
}
}
break;
}
}
}
MM.Map = function(options) {
var o = {
root: "My Mind Map",
layout: MM.Layout.Map
}
for (var p in options) { o[p] = options[p]; }
this._root = null;
this._visible = false;
this._position = [0, 0];
this._setRoot(new MM.Item().setText(o.root).setLayout(o.layout));
}
MM.Map.fromJSON = function(data) {
return new this().fromJSON(data);
}
MM.Map.prototype.toJSON = function() {
var data = {
root: this._root.toJSON()
};
return data;
}
MM.Map.prototype.fromJSON = function(data) {
this._setRoot(MM.Item.fromJSON(data.root));
return this;
}
MM.Map.prototype.mergeWith = function(data) {
/* store a sequence of nodes to be selected when merge is over */
var ids = [];
var current = MM.App.current;
var node = current;
while (node != this) {
ids.push(node.getId());
node = node.getParent();
}
this._root.mergeWith(data.root);
if (current.getMap()) { /* selected node still in tree, cool */
/* if one of the parents got collapsed, act as if the node got removed */
var node = current.getParent();
var hidden = false;
while (node != this) {
if (node.isCollapsed()) { hidden = true; }
node = node.getParent();
}
if (!hidden) { return; } /* nothing bad happened, continue */
}
/* previously selected node is no longer in the tree OR it is folded */
/* what if the node was being edited? */
if (MM.App.editing) { current.stopEditing(); }
/* get all items by their id */
var idMap = {};
var scan = function(item) {
idMap[item.getId()] = item;
item.getChildren().forEach(scan);
}
scan(this._root);
/* select the nearest existing parent */
while (ids.length) {
var id = ids.shift();
if (id in idMap) {
MM.App.select(idMap[id]);
return;
}
}
}
MM.Map.prototype.isVisible = function() {
return this._visible;
}
MM.Map.prototype.update = function() {
this._root.updateSubtree();
return this;
}
MM.Map.prototype.show = function(where) {
var node = this._root.getDOM().node;
where.appendChild(node);
this._visible = true;
this._root.updateSubtree();
this.center();
MM.App.select(this._root);
return this;
}
MM.Map.prototype.hide = function() {
var node = this._root.getDOM().node;
node.parentNode.removeChild(node);
this._visible = false;
return this;
}
MM.Map.prototype.center = function() {
var node = this._root.getDOM().node;
var port = MM.App.portSize;
var left = (port[0] - node.offsetWidth)/2;
var top = (port[1] - node.offsetHeight)/2;
this._moveTo(Math.round(left), Math.round(top));
return this;
}
MM.Map.prototype.moveBy = function(dx, dy) {
return this._moveTo(this._position[0]+dx, this._position[1]+dy);
}
MM.Map.prototype.getClosestItem = function(x, y) {
var all = [];
var scan = function(item) {
var rect = item.getDOM().content.getBoundingClientRect();
var dx = rect.left + rect.width/2 - x;
var dy = rect.top + rect.height/2 - y;
all.push({
item: item,
dx: dx,
dy: dy
});
if (!item.isCollapsed()) { item.getChildren().forEach(scan); }
}
scan(this._root);
all.sort(function(a, b) {
var da = a.dx*a.dx + a.dy*a.dy;
var db = b.dx*b.dx + b.dy*b.dy;
return da-db;
});
return all[0];
}
MM.Map.prototype.getItemFor = function(node) {
var port = this._root.getDOM().node.parentNode;
while (node != port && !node.classList.contains("content")) {
node = node.parentNode;
}
if (node == port) { return null; }
var scan = function(item, node) {
if (item.getDOM().content == node) { return item; }
var children = item.getChildren();
for (var i=0;i<children.length;i++) {
var result = scan(children[i], node);
if (result) { return result; }
}
return null;
}
return scan(this._root, node);
}
MM.Map.prototype.ensureItemVisibility = function(item) {
var padding = 10;
var node = item.getDOM().content;
var itemRect = node.getBoundingClientRect();
var root = this._root.getDOM().node;
var parentRect = root.parentNode.getBoundingClientRect();
var delta = [0, 0];
var dx = parentRect.left-itemRect.left+padding;
if (dx > 0) { delta[0] = dx; }
var dx = parentRect.right-itemRect.right-padding;
if (dx < 0) { delta[0] = dx; }
var dy = parentRect.top-itemRect.top+padding;
if (dy > 0) { delta[1] = dy; }
var dy = parentRect.bottom-itemRect.bottom-padding;
if (dy < 0) { delta[1] = dy; }
if (delta[0] || delta[1]) {
this.moveBy(delta[0], delta[1]);
}
}
MM.Map.prototype.getParent = function() {
return null;
}
MM.Map.prototype.getRoot = function() {
return this._root;
}
MM.Map.prototype.getName = function() {
var name = this._root.getText();
return MM.Format.br2nl(name).replace(/\n/g, " ").replace(/<.*?>/g, "").trim();
}
MM.Map.prototype.getId = function() {
return this._root.getId();
}
MM.Map.prototype.pick = function(item, direction) {
var candidates = [];
var currentRect = item.getDOM().content.getBoundingClientRect();
this._getPickCandidates(currentRect, this._root, direction, candidates);
if (!candidates.length) { return item; }
candidates.sort(function(a, b) {
return a.dist - b.dist;
});
return candidates[0].item;
}
MM.Map.prototype._getPickCandidates = function(currentRect, item, direction, candidates) {
if (!item.isCollapsed()) {
item.getChildren().forEach(function(child) {
this._getPickCandidates(currentRect, child, direction, candidates);
}, this);
}
var node = item.getDOM().content;
var rect = node.getBoundingClientRect();
if (direction == "left" || direction == "right") {
var x1 = currentRect.left + currentRect.width/2;
var x2 = rect.left + rect.width/2;
if (direction == "left" && x2 > x1) { return; }
if (direction == "right" && x2 < x1) { return; }
var diff1 = currentRect.top - rect.bottom;
var diff2 = rect.top - currentRect.bottom;
var dist = Math.abs(x2-x1);
} else {
var y1 = currentRect.top + currentRect.height/2;
var y2 = rect.top + rect.height/2;
if (direction == "top" && y2 > y1) { return; }
if (direction == "bottom" && y2 < y1) { return; }
var diff1 = currentRect.left - rect.right;
var diff2 = rect.left - currentRect.right;
var dist = Math.abs(y2-y1);
}
var diff = Math.max(diff1, diff2);
if (diff > 0) { return; }
if (!dist || dist < diff) { return; }
candidates.push({item:item, dist:dist});
}
MM.Map.prototype._moveTo = function(left, top) {
this._position = [left, top];
var node = this._root.getDOM().node;
node.style.left = left + "px";
node.style.top = top + "px";
}
MM.Map.prototype._setRoot = function(item) {
this._root = item;
this._root.setParent(this);
}
MM.Keyboard = {};
MM.Keyboard.init = function() {
window.addEventListener("keydown", this);
window.addEventListener("keypress", this);
}
MM.Keyboard.handleEvent = function(e) {
/* mode 2a: ignore keyboard when the activeElement resides somewhere inside of the UI pane */
var node = document.activeElement;
while (node && node != document) {
if (node.classList.contains("ui")) { return; }
node = node.parentNode;
}
var commands = MM.Command.getAll();
for (var i=0;i<commands.length;i++) {
var command = commands[i];
if (!command.isValid()) { continue; }
var keys = command.keys;
for (var j=0;j<keys.length;j++) {
if (this._keyOK(keys[j], e)) {
command.prevent && e.preventDefault();
command.execute(e);
return;
}
}
}
}
MM.Keyboard._keyOK = function(key, e) {
if ("keyCode" in key && e.type != "keydown") { return false; }
if ("charCode" in key && e.type != "keypress") { return false; }
for (var p in key) {
if (key[p] != e[p]) { return false; }
}
return true;
}
MM.Tip = {
_node: null,
handleEvent: function() {
this._hide();
},
handleMessage: function() {
this._hide();
},
init: function() {
this._node = document.querySelector("#tip");
this._node.addEventListener("click", this);
MM.subscribe("command-child", this);
MM.subscribe("command-sibling", this);
},
_hide: function() {
MM.unsubscribe("command-child", this);
MM.unsubscribe("command-sibling", this);
this._node.removeEventListener("click", this);
this._node.classList.add("hidden");
this._node = null;
}
}
MM.Action = function() {}
MM.Action.prototype.perform = function() {}
MM.Action.prototype.undo = function() {}
MM.Action.Multi = function(actions) {
this._actions = actions;
}
MM.Action.Multi.prototype = Object.create(MM.Action.prototype);
MM.Action.Multi.prototype.perform = function() {
this._actions.forEach(function(action) {
action.perform();
});
}
MM.Action.Multi.prototype.undo = function() {
this._actions.slice().reverse().forEach(function(action) {
action.undo();
});
}
MM.Action.InsertNewItem = function(parent, index) {
this._parent = parent;
this._index = index;
this._item = new MM.Item();
}
MM.Action.InsertNewItem.prototype = Object.create(MM.Action.prototype);
MM.Action.InsertNewItem.prototype.perform = function() {
this._parent.expand(); /* FIXME remember? */
this._item = this._parent.insertChild(this._item, this._index);
MM.App.select(this._item);
}
MM.Action.InsertNewItem.prototype.undo = function() {
this._parent.removeChild(this._item);
MM.App.select(this._parent);
}
MM.Action.AppendItem = function(parent, item) {
this._parent = parent;
this._item = item;
}
MM.Action.AppendItem.prototype = Object.create(MM.Action.prototype);
MM.Action.AppendItem.prototype.perform = function() {
this._parent.insertChild(this._item);
MM.App.select(this._item);
}
MM.Action.AppendItem.prototype.undo = function() {
this._parent.removeChild(this._item);
MM.App.select(this._parent);
}
MM.Action.RemoveItem = function(item) {
this._item = item;
this._parent = item.getParent();
this._index = this._parent.getChildren().indexOf(this._item);
}
MM.Action.RemoveItem.prototype = Object.create(MM.Action.prototype);
MM.Action.RemoveItem.prototype.perform = function() {
this._parent.removeChild(this._item);
MM.App.select(this._parent);
}
MM.Action.RemoveItem.prototype.undo = function() {
this._parent.insertChild(this._item, this._index);
MM.App.select(this._item);
}
MM.Action.MoveItem = function(item, newParent, newIndex, newSide) {
this._item = item;
this._newParent = newParent;
this._newIndex = (arguments.length < 3 ? null : newIndex);
this._newSide = newSide || "";
this._oldParent = item.getParent();
this._oldIndex = this._oldParent.getChildren().indexOf(item);
this._oldSide = item.getSide();
}
MM.Action.MoveItem.prototype = Object.create(MM.Action.prototype);
MM.Action.MoveItem.prototype.perform = function() {
this._item.setSide(this._newSide);
if (this._newIndex === null) {
this._newParent.insertChild(this._item);
} else {
this._newParent.insertChild(this._item, this._newIndex);
}
MM.App.select(this._item);
}
MM.Action.MoveItem.prototype.undo = function() {
this._item.setSide(this._oldSide);
this._oldParent.insertChild(this._item, this._oldIndex);
MM.App.select(this._newParent);
}
MM.Action.Swap = function(item, diff) {
this._item = item;
this._parent = item.getParent();
var children = this._parent.getChildren();
var sibling = this._parent.getLayout().pickSibling(this._item, diff);
this._sourceIndex = children.indexOf(this._item);
this._targetIndex = children.indexOf(sibling);
}
MM.Action.Swap.prototype = Object.create(MM.Action.prototype);
MM.Action.Swap.prototype.perform = function() {
this._parent.insertChild(this._item, this._targetIndex);
}
MM.Action.Swap.prototype.undo = function() {
this._parent.insertChild(this._item, this._sourceIndex);
}
MM.Action.SetLayout = function(item, layout) {
this._item = item;
this._layout = layout;
this._oldLayout = item.getOwnLayout();
}
MM.Action.SetLayout.prototype = Object.create(MM.Action.prototype);
MM.Action.SetLayout.prototype.perform = function() {
this._item.setLayout(this._layout);
}
MM.Action.SetLayout.prototype.undo = function() {
this._item.setLayout(this._oldLayout);
}
MM.Action.SetShape = function(item, shape) {
this._item = item;
this._shape = shape;
this._oldShape = item.getOwnShape();
}
MM.Action.SetShape.prototype = Object.create(MM.Action.prototype);
MM.Action.SetShape.prototype.perform = function() {
this._item.setShape(this._shape);
}
MM.Action.SetShape.prototype.undo = function() {
this._item.setShape(this._oldShape);
}
MM.Action.SetColor = function(item, color) {
this._item = item;
this._color = color;
this._oldColor = item.getOwnColor();
}
MM.Action.SetColor.prototype = Object.create(MM.Action.prototype);
MM.Action.SetColor.prototype.perform = function() {
this._item.setColor(this._color);
}
MM.Action.SetColor.prototype.undo = function() {
this._item.setColor(this._oldColor);
}
MM.Action.SetText = function(item, text) {
this._item = item;
this._text = text;
this._oldText = item.getText();
this._oldValue = item.getValue(); /* adjusting text can also modify value! */
}
MM.Action.SetText.prototype = Object.create(MM.Action.prototype);
MM.Action.SetText.prototype.perform = function() {
this._item.setText(this._text);
var numText = Number(this._text);
if (numText == this._text) { this._item.setValue(numText); }
}
MM.Action.SetText.prototype.undo = function() {
this._item.setText(this._oldText);
this._item.setValue(this._oldValue);
}
MM.Action.SetValue = function(item, value) {
this._item = item;
this._value = value;
this._oldValue = item.getValue();
}
MM.Action.SetValue.prototype = Object.create(MM.Action.prototype);
MM.Action.SetValue.prototype.perform = function() {
this._item.setValue(this._value);
}
MM.Action.SetValue.prototype.undo = function() {
this._item.setValue(this._oldValue);
}
MM.Action.SetStatus = function(item, status) {
this._item = item;
this._status = status;
this._oldStatus = item.getStatus();
}
MM.Action.SetStatus.prototype = Object.create(MM.Action.prototype);
MM.Action.SetStatus.prototype.perform = function() {
this._item.setStatus(this._status);
}
MM.Action.SetStatus.prototype.undo = function() {
this._item.setStatus(this._oldStatus);
}
MM.Action.SetIcon = function(item, icon) {
this._item = item;
this._icon = icon;
this._oldIcon = item.getIcon();
}
MM.Action.SetIcon.prototype = Object.create(MM.Action.prototype);
MM.Action.SetIcon.prototype.perform = function() {
this._item.setIcon(this._icon);
}
MM.Action.SetIcon.prototype.undo = function() {
this._item.setIcon(this._oldIcon);
}
MM.Action.SetSide = function(item, side) {
this._item = item;
this._side = side;
this._oldSide = item.getSide();
}
MM.Action.SetSide.prototype = Object.create(MM.Action.prototype);
MM.Action.SetSide.prototype.perform = function() {
this._item.setSide(this._side);
this._item.getMap().update();
}
MM.Action.SetSide.prototype.undo = function() {
this._item.setSide(this._oldSide);
this._item.getMap().update();
}
MM.Clipboard = {
_item: null,
_mode: "",
_delay: 50,
_node: document.createElement("textarea")
};
MM.Clipboard.init = function() {
this._node.style.position = "absolute";
this._node.style.width = 0;
this._node.style.height = 0;
this._node.style.left = "-100px";
this._node.style.top = "-100px";
document.body.appendChild(this._node);
}
MM.Clipboard.focus = function() {
this._node.focus();
this._empty();
}
MM.Clipboard.copy = function(sourceItem) {
this._endCut();
this._item = sourceItem.clone();
this._mode = "copy";
this._expose();
}
MM.Clipboard.paste = function(targetItem) {
setTimeout(function() {
var pasted = this._node.value;
this._empty();
if (!pasted) { return; } /* nothing */
if (this._item && pasted == MM.Format.Plaintext.to(this._item.toJSON())) { /* pasted a previously copied/cut item */
this._pasteItem(this._item, targetItem);
} else { /* pasted some external data */
this._pastePlaintext(pasted, targetItem);
}
}.bind(this), this._delay);
}
MM.Clipboard._pasteItem = function(sourceItem, targetItem) {
switch (this._mode) {
case "cut":
if (sourceItem == targetItem || sourceItem.getParent() == targetItem) { /* abort by pasting on the same node or the parent */
this._endCut();
return;
}
var item = targetItem;
while (!item.isRoot()) {
if (item == sourceItem) { return; } /* moving to a child => forbidden */
item = item.getParent();
}
var action = new MM.Action.MoveItem(sourceItem, targetItem);
MM.App.action(action);
this._endCut();
break;
case "copy":
var action = new MM.Action.AppendItem(targetItem, sourceItem.clone());
MM.App.action(action);
break;
}
}
MM.Clipboard._pastePlaintext = function(plaintext, targetItem) {
if (this._mode == "cut") { this._endCut(); } /* external paste => abort cutting */
var json = MM.Format.Plaintext.from(plaintext);
var map = MM.Map.fromJSON(json);
var root = map.getRoot();
if (root.getText()) {
var action = new MM.Action.AppendItem(targetItem, root);
MM.App.action(action);
} else {
var actions = root.getChildren().map(function(item) {
return new MM.Action.AppendItem(targetItem, item);
});
var action = new MM.Action.Multi(actions);
MM.App.action(action);
}
}
MM.Clipboard.cut = function(sourceItem) {
this._endCut();
this._item = sourceItem;
this._item.getDOM().node.classList.add("cut");
this._mode = "cut";
this._expose();
}
/**
* Expose plaintext data to the textarea to be copied to system clipboard. Clear afterwards.
*/
MM.Clipboard._expose = function() {
var json = this._item.toJSON();
var plaintext = MM.Format.Plaintext.to(json);
this._node.value = plaintext;
this._node.selectionStart = 0;
this._node.selectionEnd = this._node.value.length;
setTimeout(this._empty.bind(this), this._delay);
}
MM.Clipboard._empty = function() {
/* safari needs a non-empty selection in order to actually perfrom a real copy on cmd+c */
this._node.value = "\n";
this._node.selectionStart = 0;
this._node.selectionEnd = this._node.value.length;
}
MM.Clipboard._endCut = function() {
if (this._mode != "cut") { return; }
this._item.getDOM().node.classList.remove("cut");
this._item = null;
this._mode = "";
}
MM.Menu = {
_dom: {},
_port: null,
open: function(x, y) {
this._dom.node.style.display = "";
var w = this._dom.node.offsetWidth;
var h = this._dom.node.offsetHeight;
var left = x;
var top = y;
if (left > this._port.offsetWidth / 2) { left -= w; }
if (top > this._port.offsetHeight / 2) { top -= h; }
this._dom.node.style.left = left+"px";
this._dom.node.style.top = top+"px";
},
close: function() {
this._dom.node.style.display = "none";
},
handleEvent: function(e) {
if (e.currentTarget != this._dom.node) {
this.close();
return;
}
e.stopPropagation(); /* no dragdrop, no blur of activeElement */
e.preventDefault(); /* we do not want to focus the button */
var command = e.target.getAttribute("data-command");
if (!command) { return; }
command = MM.Command[command];
if (!command.isValid()) { return; }
command.execute();
this.close();
},
init: function(port) {
this._port = port;
this._dom.node = document.querySelector("#menu");
var buttons = this._dom.node.querySelectorAll("[data-command]");
[].slice.call(buttons).forEach(function(button) {
button.innerHTML = MM.Command[button.getAttribute("data-command")].label;
});
this._port.addEventListener("mousedown", this);
this._dom.node.addEventListener("mousedown", this);
this.close();
}
}
MM.Command = Object.create(MM.Repo, {
keys: {value: []},
editMode: {value: false},
prevent: {value: true}, /* prevent default keyboard action? */
label: {value: ""}
});
MM.Command.isValid = function() {
return (this.editMode === null || this.editMode == MM.App.editing);
}
MM.Command.execute = function() {}
MM.Command.Notes = Object.create(MM.Command, {
label: {value: "Notes"},
keys: {value: [{keyCode: "M".charCodeAt(0), ctrlKey: true}]}
});
MM.Command.Notes.isValid = function() {
return MM.Command.isValid.call(this);
}
MM.Command.Notes.execute = function() {
MM.App.notes.toggle();
}
MM.Command.Undo = Object.create(MM.Command, {
label: {value: "Undo"},
keys: {value: [{keyCode: "Z".charCodeAt(0), ctrlKey: true}]}
});
MM.Command.Undo.isValid = function() {
return MM.Command.isValid.call(this) && !!MM.App.historyIndex;
}
MM.Command.Undo.execute = function() {
MM.App.history[MM.App.historyIndex-1].undo();
MM.App.historyIndex--;
}
MM.Command.Redo = Object.create(MM.Command, {
label: {value: "Redo"},
keys: {value: [{keyCode: "Y".charCodeAt(0), ctrlKey: true}]},
});
MM.Command.Redo.isValid = function() {
return (MM.Command.isValid.call(this) && MM.App.historyIndex != MM.App.history.length);
}
MM.Command.Redo.execute = function() {
MM.App.history[MM.App.historyIndex].perform();
MM.App.historyIndex++;
}
MM.Command.InsertSibling = Object.create(MM.Command, {
label: {value: "Insert a sibling"},
keys: {value: [{keyCode: 13}]}
});
MM.Command.InsertSibling.execute = function() {
var item = MM.App.current;
if (item.isRoot()) {
var action = new MM.Action.InsertNewItem(item, item.getChildren().length);
} else {
var parent = item.getParent();
var index = parent.getChildren().indexOf(item);
var action = new MM.Action.InsertNewItem(parent, index+1);
}
MM.App.action(action);
MM.Command.Edit.execute();
MM.publish("command-sibling");
}
MM.Command.InsertChild = Object.create(MM.Command, {
label: {value: "Insert a child"},
keys: {value: [
{keyCode: 9, ctrlKey:false},
{keyCode: 45}
]}
});
MM.Command.InsertChild.execute = function() {
var item = MM.App.current;
var action = new MM.Action.InsertNewItem(item, item.getChildren().length);
MM.App.action(action);
MM.Command.Edit.execute();
MM.publish("command-child");
}
MM.Command.Delete = Object.create(MM.Command, {
label: {value: "Delete an item"},
keys: {value: [{keyCode: MM.isMac() ? 8 : 46}]} // Mac keyboards' "delete" button generates 8 (backspace)
});
MM.Command.Delete.isValid = function() {
return MM.Command.isValid.call(this) && !MM.App.current.isRoot();
}
MM.Command.Delete.execute = function() {
var action = new MM.Action.RemoveItem(MM.App.current);
MM.App.action(action);
}
MM.Command.Swap = Object.create(MM.Command, {
label: {value: "Swap sibling"},
keys: {value: [
{keyCode: 38, ctrlKey:true},
{keyCode: 40, ctrlKey:true},
]}
});
MM.Command.Swap.execute = function(e) {
var current = MM.App.current;
if (current.isRoot() || current.getParent().getChildren().length < 2) { return; }
var diff = (e.keyCode == 38 ? -1 : 1);
var action = new MM.Action.Swap(MM.App.current, diff);
MM.App.action(action);
}
MM.Command.Side = Object.create(MM.Command, {
label: {value: "Change side"},
keys: {value: [
{keyCode: 37, ctrlKey:true},
{keyCode: 39, ctrlKey:true},
]}
});
MM.Command.Side.execute = function(e) {
var current = MM.App.current;
if (current.isRoot() || !current.getParent().isRoot()) { return; }
var side = (e.keyCode == 37 ? "left" : "right");
var action = new MM.Action.SetSide(MM.App.current, side);
MM.App.action(action);
}
MM.Command.Save = Object.create(MM.Command, {
label: {value: "Save map"},
keys: {value: [{keyCode: "S".charCodeAt(0), ctrlKey:true, shiftKey:false}]}
});
MM.Command.Save.execute = function() {
MM.App.io.quickSave();
}
MM.Command.SaveAs = Object.create(MM.Command, {
label: {value: "Save as&hellip;"},
keys: {value: [{keyCode: "S".charCodeAt(0), ctrlKey:true, shiftKey:true}]}
});
MM.Command.SaveAs.execute = function() {
MM.App.io.show("save");
}
MM.Command.Load = Object.create(MM.Command, {
label: {value: "Load map"},
keys: {value: [{keyCode: "O".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.Load.execute = function() {
MM.App.io.show("load");
}
MM.Command.Center = Object.create(MM.Command, {
label: {value: "Center map"},
keys: {value: [{keyCode: 35}]}
});
MM.Command.Center.execute = function() {
MM.App.map.center();
}
MM.Command.New = Object.create(MM.Command, {
label: {value: "New map"},
keys: {value: [{keyCode: "N".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.New.execute = function() {
if (!confirm("Throw away your current map and start a new one?")) { return; }
var map = new MM.Map();
MM.App.setMap(map);
MM.publish("map-new", this);
}
MM.Command.ZoomIn = Object.create(MM.Command, {
label: {value: "Zoom in"},
keys: {value: [{charCode:"+".charCodeAt(0)}]}
});
MM.Command.ZoomIn.execute = function() {
MM.App.adjustFontSize(1);
}
MM.Command.ZoomOut = Object.create(MM.Command, {
label: {value: "Zoom out"},
keys: {value: [{charCode:"-".charCodeAt(0)}]}
});
MM.Command.ZoomOut.execute = function() {
MM.App.adjustFontSize(-1);
}
MM.Command.Help = Object.create(MM.Command, {
label: {value: "Show/hide help"},
keys: {value: [{charCode: "?".charCodeAt(0)}]}
});
MM.Command.Help.execute = function() {
MM.App.help.toggle();
}
MM.Command.UI = Object.create(MM.Command, {
label: {value: "Show/hide UI"},
keys: {value: [{charCode: "*".charCodeAt(0)}]}
});
MM.Command.UI.execute = function() {
MM.App.ui.toggle();
}
MM.Command.Pan = Object.create(MM.Command, {
label: {value: "Pan the map"},
keys: {value: [
{keyCode: "W".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false},
{keyCode: "A".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false},
{keyCode: "S".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false},
{keyCode: "D".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false}
]},
chars: {value: []}
});
MM.Command.Pan.execute = function(e) {
var ch = String.fromCharCode(e.keyCode);
var index = this.chars.indexOf(ch);
if (index > -1) { return; }
if (!this.chars.length) {
window.addEventListener("keyup", this);
this.interval = setInterval(this._step.bind(this), 50);
}
this.chars.push(ch);
this._step();
}
MM.Command.Pan._step = function() {
var dirs = {
"W": [0, 1],
"A": [1, 0],
"S": [0, -1],
"D": [-1, 0]
}
var offset = [0, 0];
this.chars.forEach(function(ch) {
offset[0] += dirs[ch][0];
offset[1] += dirs[ch][1];
});
MM.App.map.moveBy(15*offset[0], 15*offset[1]);
}
MM.Command.Pan.handleEvent = function(e) {
var ch = String.fromCharCode(e.keyCode);
var index = this.chars.indexOf(ch);
if (index > -1) {
this.chars.splice(index, 1);
if (!this.chars.length) {
window.removeEventListener("keyup", this);
clearInterval(this.interval);
}
}
}
MM.Command.Copy = Object.create(MM.Command, {
label: {value: "Copy"},
prevent: {value: false},
keys: {value: [
{keyCode: "C".charCodeAt(0), ctrlKey:true},
{keyCode: "C".charCodeAt(0), metaKey:true}
]}
});
MM.Command.Copy.execute = function() {
MM.Clipboard.copy(MM.App.current);
}
MM.Command.Cut = Object.create(MM.Command, {
label: {value: "Cut"},
prevent: {value: false},
keys: {value: [
{keyCode: "X".charCodeAt(0), ctrlKey:true},
{keyCode: "X".charCodeAt(0), metaKey:true}
]}
});
MM.Command.Cut.execute = function() {
MM.Clipboard.cut(MM.App.current);
}
MM.Command.Paste = Object.create(MM.Command, {
label: {value: "Paste"},
prevent: {value: false},
keys: {value: [
{keyCode: "V".charCodeAt(0), ctrlKey:true},
{keyCode: "V".charCodeAt(0), metaKey:true}
]}
});
MM.Command.Paste.execute = function() {
MM.Clipboard.paste(MM.App.current);
}
MM.Command.Fold = Object.create(MM.Command, {
label: {value: "Fold/Unfold"},
keys: {value: [{charCode: "f".charCodeAt(0), ctrlKey:false}]}
});
MM.Command.Fold.execute = function() {
var item = MM.App.current;
if (item.isCollapsed()) { item.expand(); } else { item.collapse(); }
MM.App.map.ensureItemVisibility(item);
}
MM.Command.Edit = Object.create(MM.Command, {
label: {value: "Edit item"},
keys: {value: [
{keyCode: 32},
{keyCode: 113}
]}
});
MM.Command.Edit.execute = function() {
MM.App.current.startEditing();
MM.App.editing = true;
}
MM.Command.Finish = Object.create(MM.Command, {
keys: {value: [{keyCode: 13, altKey:false, ctrlKey:false, shiftKey:false}]},
editMode: {value: true}
});
MM.Command.Finish.execute = function() {
MM.App.editing = false;
var text = MM.App.current.stopEditing();
if (text) {
var action = new MM.Action.SetText(MM.App.current, text);
} else {
var action = new MM.Action.RemoveItem(MM.App.current);
}
MM.App.action(action);
}
MM.Command.Newline = Object.create(MM.Command, {
label: {value: "Line break"},
keys: {value: [
{keyCode: 13, shiftKey:true},
{keyCode: 13, ctrlKey:true}
]},
editMode: {value: true}
});
MM.Command.Newline.execute = function() {
var range = getSelection().getRangeAt(0);
var br = document.createElement("br");
range.insertNode(br);
range.setStartAfter(br);
MM.App.current.updateSubtree();
}
MM.Command.Cancel = Object.create(MM.Command, {
editMode: {value: true},
keys: {value: [{keyCode: 27}]}
});
MM.Command.Cancel.execute = function() {
MM.App.editing = false;
MM.App.current.stopEditing();
var oldText = MM.App.current.getText();
if (!oldText) { /* newly added node */
var action = new MM.Action.RemoveItem(MM.App.current);
MM.App.action(action);
}
}
MM.Command.Style = Object.create(MM.Command, {
editMode: {value: null},
command: {value: ""}
});
MM.Command.Style.execute = function() {
if (MM.App.editing) {
document.execCommand(this.command, null, null);
} else {
MM.Command.Edit.execute();
var selection = getSelection();
var range = selection.getRangeAt(0);
range.selectNodeContents(MM.App.current.getDOM().text);
selection.removeAllRanges();
selection.addRange(range);
this.execute();
MM.Command.Finish.execute();
}
}
MM.Command.Bold = Object.create(MM.Command.Style, {
command: {value: "bold"},
label: {value: "Bold"},
keys: {value: [{keyCode: "B".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.Underline = Object.create(MM.Command.Style, {
command: {value: "underline"},
label: {value: "Underline"},
keys: {value: [{keyCode: "U".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.Italic = Object.create(MM.Command.Style, {
command: {value: "italic"},
label: {value: "Italic"},
keys: {value: [{keyCode: "I".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.Strikethrough = Object.create(MM.Command.Style, {
command: {value: "strikeThrough"},
label: {value: "Strike-through"},
keys: {value: [{keyCode: "S".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.Value = Object.create(MM.Command, {
label: {value: "Set value"},
keys: {value: [{charCode: "v".charCodeAt(0), ctrlKey:false, metaKey:false}]}
});
MM.Command.Value.execute = function() {
var item = MM.App.current;
var oldValue = item.getValue();
var newValue = prompt("Set item value", oldValue);
if (newValue == null) { return; }
if (!newValue.length) { newValue = null; }
var numValue = parseFloat(newValue);
var action = new MM.Action.SetValue(item, isNaN(numValue) ? newValue : numValue);
MM.App.action(action);
}
MM.Command.Yes = Object.create(MM.Command, {
label: {value: "Yes"},
keys: {value: [{charCode: "y".charCodeAt(0), ctrlKey:false}]}
});
MM.Command.Yes.execute = function() {
var item = MM.App.current;
var status = (item.getStatus() == "yes" ? null : "yes");
var action = new MM.Action.SetStatus(item, status);
MM.App.action(action);
}
MM.Command.No = Object.create(MM.Command, {
label: {value: "No"},
keys: {value: [{charCode: "n".charCodeAt(0), ctrlKey:false}]}
});
MM.Command.No.execute = function() {
var item = MM.App.current;
var status = (item.getStatus() == "no" ? null : "no");
var action = new MM.Action.SetStatus(item, status);
MM.App.action(action);
}
MM.Command.Computed = Object.create(MM.Command, {
label: {value: "Computed"},
keys: {value: [{charCode: "c".charCodeAt(0), ctrlKey:false, metaKey:false}]}
});
MM.Command.Computed.execute = function() {
var item = MM.App.current;
var status = (item.getStatus() == "computed" ? null : "computed");
var action = new MM.Action.SetStatus(item, status);
MM.App.action(action);
}
MM.Command.Select = Object.create(MM.Command, {
label: {value: "Move selection"},
keys: {value: [
{keyCode: 38, ctrlKey:false},
{keyCode: 37, ctrlKey:false},
{keyCode: 40, ctrlKey:false},
{keyCode: 39, ctrlKey:false}
]}
});
MM.Command.Select.execute = function(e) {
var dirs = {
37: "left",
38: "top",
39: "right",
40: "bottom"
}
var dir = dirs[e.keyCode];
var layout = MM.App.current.getLayout();
var item = /*MM.App.map*/layout.pick(MM.App.current, dir);
MM.App.select(item);
}
MM.Command.SelectRoot = Object.create(MM.Command, {
label: {value: "Select root"},
keys: {value: [{keyCode: 36}]}
});
MM.Command.SelectRoot.execute = function() {
var item = MM.App.current;
while (!item.isRoot()) { item = item.getParent(); }
MM.App.select(item);
}
// Macs use keyCode 8 to delete instead
if (!MM.isMac()) {
MM.Command.SelectParent = Object.create(MM.Command, {
label: {value: "Select parent"},
keys: {value: [{keyCode: 8}]}
});
MM.Command.SelectParent.execute = function() {
if (MM.App.current.isRoot()) { return; }
MM.App.select(MM.App.current.getParent());
}
}
MM.Layout = Object.create(MM.Repo, {
ALL: {value: []},
SPACING_RANK: {value: 4},
SPACING_CHILD: {value: 4},
});
MM.Layout.getAll = function() {
return this.ALL;
}
/**
* Re-draw an item and its children
*/
MM.Layout.update = function(item) {
return this;
}
/**
* @param {MM.Item} child Child node (its parent uses this layout)
*/
MM.Layout.getChildDirection = function(child) {
return "";
}
MM.Layout.pick = function(item, dir) {
var opposite = {
left: "right",
right: "left",
top: "bottom",
bottom: "top"
}
/* direction for a child */
if (!item.isCollapsed()) {
var children = item.getChildren();
for (var i=0;i<children.length;i++) {
var child = children[i];
if (this.getChildDirection(child) == dir) { return child; }
}
}
if (item.isRoot()) { return item; }
var parentLayout = item.getParent().getLayout();
var thisChildDirection = parentLayout.getChildDirection(item);
if (thisChildDirection == dir) {
return item;
} else if (thisChildDirection == opposite[dir]) {
return item.getParent();
} else {
return parentLayout.pickSibling(item, (dir == "left" || dir == "top" ? -1 : +1));
}
}
MM.Layout.pickSibling = function(item, dir) {
if (item.isRoot()) { return item; }
var children = item.getParent().getChildren();
var index = children.indexOf(item);
index += dir;
index = (index+children.length) % children.length;
return children[index];
}
/**
* Adjust canvas size and position
*/
MM.Layout._anchorCanvas = function(item) {
var dom = item.getDOM();
dom.canvas.width = dom.node.offsetWidth;
dom.canvas.height = dom.node.offsetHeight;
}
MM.Layout._anchorToggle = function(item, x, y, side) {
var node = item.getDOM().toggle;
var w = node.offsetWidth;
var h = node.offsetHeight;
var l = x;
var t = y;
switch (side) {
case "left":
t -= h/2;
l -= w;
break;
case "right":
t -= h/2;
break;
case "top":
l -= w/2;
t -= h;
break;
case "bottom":
l -= w/2;
break;
}
node.style.left = Math.round(l) + "px";
node.style.top = Math.round(t) + "px";
}
MM.Layout._getChildAnchor = function(item, side) {
var dom = item.getDOM();
if (side == "left" || side == "right") {
var pos = dom.node.offsetLeft + dom.content.offsetLeft;
if (side == "left") { pos += dom.content.offsetWidth; }
} else {
var pos = dom.node.offsetTop + dom.content.offsetTop;
if (side == "top") { pos += dom.content.offsetHeight; }
}
return pos;
}
MM.Layout._computeChildrenBBox = function(children, childIndex) {
var bbox = [0, 0];
var rankIndex = (childIndex+1) % 2;
children.forEach(function(child, index) {
var node = child.getDOM().node;
var childSize = [node.offsetWidth, node.offsetHeight];
bbox[rankIndex] = Math.max(bbox[rankIndex], childSize[rankIndex]); /* adjust cardinal size */
bbox[childIndex] += childSize[childIndex]; /* adjust orthogonal size */
}, this);
if (children.length > 1) { bbox[childIndex] += this.SPACING_CHILD * (children.length-1); } /* child separation */
return bbox;
}
MM.Layout._alignItem = function(item, side) {
var dom = item.getDOM();
switch (side) {
case "left":
dom.content.insertBefore(dom.icon, dom.content.firstChild);
dom.content.appendChild(dom.value);
dom.content.appendChild(dom.status);
break;
case "right":
dom.content.insertBefore(dom.icon, dom.content.firstChild);
dom.content.insertBefore(dom.value, dom.content.firstChild);
dom.content.insertBefore(dom.status, dom.content.firstChild);
break;
}
}
MM.Layout.Graph = Object.create(MM.Layout, {
SPACING_RANK: {value: 16},
childDirection: {value: ""}
});
MM.Layout.Graph.getChildDirection = function(child) {
return this.childDirection;
}
MM.Layout.Graph.create = function(direction, id, label) {
var layout = Object.create(this, {
childDirection: {value:direction},
id: {value:id},
label: {value:label}
});
MM.Layout.ALL.push(layout);
return layout;
}
MM.Layout.Graph.update = function(item) {
var side = this.childDirection;
if (!item.isRoot()) {
side = item.getParent().getLayout().getChildDirection(item);
}
this._alignItem(item, side);
this._layoutItem(item, this.childDirection);
if (this.childDirection == "left" || this.childDirection == "right") {
this._drawLinesHorizontal(item, this.childDirection);
} else {
this._drawLinesVertical(item, this.childDirection);
}
return this;
}
/**
* Generic graph child layout routine. Updates item's orthogonal size according to the sum of its children.
*/
MM.Layout.Graph._layoutItem = function(item, rankDirection) {
var sizeProps = ["width", "height"];
var posProps = ["left", "top"];
var rankIndex = (rankDirection == "left" || rankDirection == "right" ? 0 : 1);
var childIndex = (rankIndex+1) % 2;
var rankPosProp = posProps[rankIndex];
var childPosProp = posProps[childIndex];
var rankSizeProp = sizeProps[rankIndex];
var childSizeProp = sizeProps[childIndex];
var dom = item.getDOM();
/* content size */
var contentSize = [dom.content.offsetWidth, dom.content.offsetHeight];
/* children size */
var bbox = this._computeChildrenBBox(item.getChildren(), childIndex);
/* node size */
var rankSize = contentSize[rankIndex];
if (bbox[rankIndex]) { rankSize += bbox[rankIndex] + this.SPACING_RANK; }
var childSize = Math.max(bbox[childIndex], contentSize[childIndex]);
dom.node.style[rankSizeProp] = rankSize + "px";
dom.node.style[childSizeProp] = childSize + "px";
var offset = [0, 0];
if (rankDirection == "right") { offset[0] = contentSize[0] + this.SPACING_RANK; }
if (rankDirection == "bottom") { offset[1] = contentSize[1] + this.SPACING_RANK; }
offset[childIndex] = Math.round((childSize - bbox[childIndex])/2);
this._layoutChildren(item.getChildren(), rankDirection, offset, bbox);
/* label position */
var labelPos = 0;
if (rankDirection == "left") { labelPos = rankSize - contentSize[0]; }
if (rankDirection == "top") { labelPos = rankSize - contentSize[1]; }
dom.content.style[childPosProp] = Math.round((childSize - contentSize[childIndex])/2) + "px";
dom.content.style[rankPosProp] = labelPos + "px";
return this;
}
MM.Layout.Graph._layoutChildren = function(children, rankDirection, offset, bbox) {
var posProps = ["left", "top"];
var rankIndex = (rankDirection == "left" || rankDirection == "right" ? 0 : 1);
var childIndex = (rankIndex+1) % 2;
var rankPosProp = posProps[rankIndex];
var childPosProp = posProps[childIndex];
children.forEach(function(child, index) {
var node = child.getDOM().node;
var childSize = [node.offsetWidth, node.offsetHeight];
if (rankDirection == "left") { offset[0] = bbox[0] - childSize[0]; }
if (rankDirection == "top") { offset[1] = bbox[1] - childSize[1]; }
node.style[childPosProp] = offset[childIndex] + "px";
node.style[rankPosProp] = offset[rankIndex] + "px";
offset[childIndex] += childSize[childIndex] + this.SPACING_CHILD; /* offset for next child */
}, this);
return bbox;
}
MM.Layout.Graph._drawLinesHorizontal = function(item, side) {
this._anchorCanvas(item);
this._drawHorizontalConnectors(item, side, item.getChildren());
}
MM.Layout.Graph._drawLinesVertical = function(item, side) {
this._anchorCanvas(item);
this._drawVerticalConnectors(item, side, item.getChildren());
}
MM.Layout.Graph._drawHorizontalConnectors = function(item, side, children) {
if (children.length == 0) { return; }
var dom = item.getDOM();
var canvas = dom.canvas;
var ctx = canvas.getContext("2d");
ctx.strokeStyle = item.getColor();
var R = this.SPACING_RANK/2;
/* first part */
var y1 = item.getShape().getVerticalAnchor(item);
if (side == "left") {
var x1 = dom.content.offsetLeft - 0.5;
} else {
var x1 = dom.content.offsetWidth + dom.content.offsetLeft + 0.5;
}
this._anchorToggle(item, x1, y1, side);
if (item.isCollapsed()) { return; }
if (children.length == 1) {
var child = children[0];
var y2 = child.getShape().getVerticalAnchor(child) + child.getDOM().node.offsetTop;
var x2 = this._getChildAnchor(child, side);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.bezierCurveTo((x1+x2)/2, y1, (x1+x2)/2, y2, x2, y2);
ctx.stroke();
return;
}
if (side == "left") {
var x2 = x1 - R;
} else {
var x2 = x1 + R;
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y1);
ctx.stroke();
/* rounded connectors */
var c1 = children[0];
var c2 = children[children.length-1];
var x = x2;
var xx = x + (side == "left" ? -R : R);
var y1 = c1.getShape().getVerticalAnchor(c1) + c1.getDOM().node.offsetTop;
var y2 = c2.getShape().getVerticalAnchor(c2) + c2.getDOM().node.offsetTop;
var x1 = this._getChildAnchor(c1, side);
var x2 = this._getChildAnchor(c2, side);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(xx, y1)
ctx.arcTo(x, y1, x, y1+R, R);
ctx.lineTo(x, y2-R);
ctx.arcTo(x, y2, xx, y2, R);
ctx.lineTo(x2, y2);
for (var i=1; i<children.length-1; i++) {
var c = children[i];
var y = c.getShape().getVerticalAnchor(c) + c.getDOM().node.offsetTop;
ctx.moveTo(x, y);
ctx.lineTo(this._getChildAnchor(c, side), y);
}
ctx.stroke();
}
MM.Layout.Graph._drawVerticalConnectors = function(item, side, children) {
if (children.length == 0) { return; }
var dom = item.getDOM();
var canvas = dom.canvas;
var ctx = canvas.getContext("2d");
ctx.strokeStyle = item.getColor();
/* first part */
var R = this.SPACING_RANK/2;
var x = item.getShape().getHorizontalAnchor(item);
var height = (children.length == 1 ? 2*R : R);
if (side == "top") {
var y1 = canvas.height - dom.content.offsetHeight;
var y2 = y1 - height;
this._anchorToggle(item, x, y1, side);
} else {
var y1 = item.getShape().getVerticalAnchor(item);
var y2 = dom.content.offsetHeight + height;
this._anchorToggle(item, x, dom.content.offsetHeight, side);
}
ctx.beginPath();
ctx.moveTo(x, y1);
ctx.lineTo(x, y2);
ctx.stroke();
if (children.length == 1) { return; }
/* rounded connectors */
var c1 = children[0];
var c2 = children[children.length-1];
var offset = dom.content.offsetHeight + height;
var y = Math.round(side == "top" ? canvas.height - offset : offset) + 0.5;
var x1 = c1.getShape().getHorizontalAnchor(c1) + c1.getDOM().node.offsetLeft;
var x2 = c2.getShape().getHorizontalAnchor(c2) + c2.getDOM().node.offsetLeft;
var y1 = this._getChildAnchor(c1, side);
var y2 = this._getChildAnchor(c2, side);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.arcTo(x1, y, x1+R, y, R);
ctx.lineTo(x2-R, y);
ctx.arcTo(x2, y, x2, y2, R);
for (var i=1; i<children.length-1; i++) {
var c = children[i];
var x = c.getShape().getHorizontalAnchor(c) + c.getDOM().node.offsetLeft;
ctx.moveTo(x, y);
ctx.lineTo(x, this._getChildAnchor(c, side));
}
ctx.stroke();
}
MM.Layout.Graph.Down = MM.Layout.Graph.create("bottom", "graph-bottom", "Bottom");
MM.Layout.Graph.Up = MM.Layout.Graph.create("top", "graph-top", "Top");
MM.Layout.Graph.Left = MM.Layout.Graph.create("left", "graph-left", "Left");
MM.Layout.Graph.Right = MM.Layout.Graph.create("right", "graph-right", "Right");
MM.Layout.Tree = Object.create(MM.Layout, {
SPACING_RANK: {value: 32},
childDirection: {value: ""}
});
MM.Layout.Tree.getChildDirection = function(child) {
return this.childDirection;
}
MM.Layout.Tree.create = function(direction, id, label) {
var layout = Object.create(this, {
childDirection: {value:direction},
id: {value:id},
label: {value:label}
});
MM.Layout.ALL.push(layout);
return layout;
}
MM.Layout.Tree.update = function(item) {
var side = this.childDirection;
if (!item.isRoot()) {
side = item.getParent().getLayout().getChildDirection(item);
}
this._alignItem(item, side);
this._layoutItem(item, this.childDirection);
this._anchorCanvas(item);
this._drawLines(item, this.childDirection);
return this;
}
/**
* Generic graph child layout routine. Updates item's orthogonal size according to the sum of its children.
*/
MM.Layout.Tree._layoutItem = function(item, rankDirection) {
var dom = item.getDOM();
/* content size */
var contentSize = [dom.content.offsetWidth, dom.content.offsetHeight];
/* children size */
var bbox = this._computeChildrenBBox(item.getChildren(), 1);
/* node size */
var rankSize = contentSize[0];
var childSize = bbox[1] + contentSize[1];
if (bbox[0]) {
rankSize = Math.max(rankSize, bbox[0] + this.SPACING_RANK);
childSize += this.SPACING_CHILD;
}
dom.node.style.width = rankSize + "px";
dom.node.style.height = childSize + "px";
var offset = [this.SPACING_RANK, contentSize[1]+this.SPACING_CHILD];
if (rankDirection == "left") { offset[0] = rankSize - bbox[0] - this.SPACING_RANK; }
this._layoutChildren(item.getChildren(), rankDirection, offset, bbox);
/* label position */
var labelPos = 0;
if (rankDirection == "left") { labelPos = rankSize - contentSize[0]; }
dom.content.style.left = labelPos + "px";
dom.content.style.top = 0;
return this;
}
MM.Layout.Tree._layoutChildren = function(children, rankDirection, offset, bbox) {
children.forEach(function(child, index) {
var node = child.getDOM().node;
var childSize = [node.offsetWidth, node.offsetHeight];
var left = offset[0];
if (rankDirection == "left") { left += (bbox[0] - childSize[0]); }
node.style.left = left + "px";
node.style.top = offset[1] + "px";
offset[1] += childSize[1] + this.SPACING_CHILD; /* offset for next child */
}, this);
return bbox;
}
MM.Layout.Tree._drawLines = function(item, side) {
var dom = item.getDOM();
var canvas = dom.canvas;
var R = this.SPACING_RANK/4;
var x = (side == "left" ? canvas.width - 2*R : 2*R) + 0.5;
this._anchorToggle(item, x, dom.content.offsetHeight, "bottom");
var children = item.getChildren();
if (children.length == 0 || item.isCollapsed()) { return; }
var ctx = canvas.getContext("2d");
ctx.strokeStyle = item.getColor();
var y1 = item.getShape().getVerticalAnchor(item);
var last = children[children.length-1];
var y2 = last.getShape().getVerticalAnchor(last) + last.getDOM().node.offsetTop;
ctx.beginPath();
ctx.moveTo(x, y1);
ctx.lineTo(x, y2 - R);
/* rounded connectors */
for (var i=0; i<children.length; i++) {
var c = children[i];
var y = c.getShape().getVerticalAnchor(c) + c.getDOM().node.offsetTop;
var anchor = this._getChildAnchor(c, side);
ctx.moveTo(x, y - R);
ctx.arcTo(x, y, anchor, y, R);
ctx.lineTo(anchor, y);
}
ctx.stroke();
}
MM.Layout.Tree.Left = MM.Layout.Tree.create("left", "tree-left", "Left");
MM.Layout.Tree.Right = MM.Layout.Tree.create("right", "tree-right", "Right");
MM.Layout.Map = Object.create(MM.Layout.Graph, {
id: {value:"map"},
label: {value:"Map"},
LINE_THICKNESS: {value:8}
});
MM.Layout.ALL.push(MM.Layout.Map);
MM.Layout.Map.update = function(item) {
if (item.isRoot()) {
this._layoutRoot(item);
} else {
var side = this.getChildDirection(item);
var name = side.charAt(0).toUpperCase() + side.substring(1);
MM.Layout.Graph[name].update(item);
}
}
/**
* @param {MM.Item} child Child node
*/
MM.Layout.Map.getChildDirection = function(child) {
while (!child.getParent().isRoot()) {
child = child.getParent();
}
/* child is now the sub-root node */
var side = child.getSide();
if (side) { return side; }
var counts = {left:0, right:0};
var children = child.getParent().getChildren();
for (var i=0;i<children.length;i++) {
var side = children[i].getSide();
if (!side) {
side = (counts.right > counts.left ? "left" : "right");
children[i].setSide(side);
}
counts[side]++;
}
return child.getSide();
}
MM.Layout.Map.pickSibling = function(item, dir) {
if (item.isRoot()) { return item; }
var parent = item.getParent();
var children = parent.getChildren();
if (parent.isRoot()) {
var side = this.getChildDirection(item);
children = children.filter(function(child) {
return (this.getChildDirection(child) == side);
}, this);
}
var index = children.indexOf(item);
index += dir;
index = (index+children.length) % children.length;
return children[index];
}
MM.Layout.Map._layoutRoot = function(item) {
this._alignItem(item, "right");
var dom = item.getDOM();
var children = item.getChildren();
var childrenLeft = [];
var childrenRight = [];
children.forEach(function(child, index) {
var node = child.getDOM().node;
var side = this.getChildDirection(child);
if (side == "left") {
childrenLeft.push(child);
} else {
childrenRight.push(child);
}
}, this);
var bboxLeft = this._computeChildrenBBox(childrenLeft, 1);
var bboxRight = this._computeChildrenBBox(childrenRight, 1);
var height = Math.max(bboxLeft[1], bboxRight[1], dom.content.offsetHeight);
var left = 0;
this._layoutChildren(childrenLeft, "left", [left, Math.round((height-bboxLeft[1])/2)], bboxLeft);
left += bboxLeft[0];
if (childrenLeft.length) { left += this.SPACING_RANK; }
dom.content.style.left = left + "px";
left += dom.content.offsetWidth;
if (childrenRight.length) { left += this.SPACING_RANK; }
this._layoutChildren(childrenRight, "right", [left, Math.round((height-bboxRight[1])/2)], bboxRight);
left += bboxRight[0];
dom.content.style.top = Math.round((height - dom.content.offsetHeight)/2) + "px";
dom.node.style.height = height + "px";
dom.node.style.width = left + "px";
this._anchorCanvas(item);
this._drawRootConnectors(item, "left", childrenLeft);
this._drawRootConnectors(item, "right", childrenRight);
}
MM.Layout.Map._drawRootConnectors = function(item, side, children) {
if (children.length == 0 || item.isCollapsed()) { return; }
var dom = item.getDOM();
var canvas = dom.canvas;
var ctx = canvas.getContext("2d");
var R = this.SPACING_RANK/2;
var x1 = dom.content.offsetLeft + dom.content.offsetWidth/2;
var y1 = item.getShape().getVerticalAnchor(item);
var half = this.LINE_THICKNESS/2;
for (var i=0;i<children.length;i++) {
var child = children[i];
var x2 = this._getChildAnchor(child, side);
var y2 = child.getShape().getVerticalAnchor(child) + child.getDOM().node.offsetTop;
var angle = Math.atan2(y2-y1, x2-x1) + Math.PI/2;
var dx = Math.cos(angle) * half;
var dy = Math.sin(angle) * half;
ctx.fillStyle = ctx.strokeStyle = child.getColor();
ctx.beginPath();
ctx.moveTo(x1-dx, y1-dy);
ctx.quadraticCurveTo((x2+x1)/2, y2, x2, y2);
ctx.quadraticCurveTo((x2+x1)/2, y2, x1+dx, y1+dy);
ctx.fill();
ctx.stroke();
}
}
MM.Shape = Object.create(MM.Repo, {
VERTICAL_OFFSET: {value: 0.5},
});
MM.Shape.set = function(item) {
item.getDOM().node.classList.add("shape-"+this.id);
return this;
}
MM.Shape.unset = function(item) {
item.getDOM().node.classList.remove("shape-"+this.id);
return this;
}
MM.Shape.update = function(item) {
item.getDOM().content.style.borderColor = item.getColor();
return this;
}
MM.Shape.getHorizontalAnchor = function(item) {
var node = item.getDOM().content;
return Math.round(node.offsetLeft + node.offsetWidth/2) + 0.5;
}
MM.Shape.getVerticalAnchor = function(item) {
var node = item.getDOM().content;
return node.offsetTop + Math.round(node.offsetHeight * this.VERTICAL_OFFSET) + 0.5;
}
MM.Shape.Underline = Object.create(MM.Shape, {
id: {value: "underline"},
label: {value: "Underline"},
VERTICAL_OFFSET: {value: -3}
});
MM.Shape.Underline.update = function(item) {
var dom = item.getDOM();
var ctx = dom.canvas.getContext("2d");
ctx.strokeStyle = item.getColor();
var left = dom.content.offsetLeft;
var right = left + dom.content.offsetWidth;
var top = this.getVerticalAnchor(item);
ctx.beginPath();
ctx.moveTo(left, top);
ctx.lineTo(right, top);
ctx.stroke();
}
MM.Shape.Underline.getVerticalAnchor = function(item) {
var node = item.getDOM().content;
return node.offsetTop + node.offsetHeight + this.VERTICAL_OFFSET + 0.5;
}
MM.Shape.Box = Object.create(MM.Shape, {
id: {value: "box"},
label: {value: "Box"}
});
MM.Shape.Ellipse = Object.create(MM.Shape, {
id: {value: "ellipse"},
label: {value: "Ellipse"}
});
MM.Format = Object.create(MM.Repo, {
extension: {value:""},
mime: {value:""}
});
MM.Format.getByName = function(name) {
var index = name.lastIndexOf(".");
if (index == -1) { return null; }
var extension = name.substring(index+1).toLowerCase();
return this.getByProperty("extension", extension);
}
MM.Format.getByMime = function(mime) {
return this.getByProperty("mime", mime);
}
MM.Format.to = function(data) {}
MM.Format.from = function(data) {}
MM.Format.nl2br = function(str) {
return str.replace(/\n/g, "<br/>");
}
MM.Format.br2nl = function(str) {
return str.replace(/<br\s*\/?>/g, "\n");
}
MM.Format.JSON = Object.create(MM.Format, {
id: {value: "json"},
label: {value: "Native (JSON)"},
extension: {value: "mymind"},
mime: {value: "application/vnd.mymind+json"}
});
MM.Format.JSON.to = function(data) {
return JSON.stringify(data, null, "\t") + "\n";
}
MM.Format.JSON.from = function(data) {
return JSON.parse(data);
}
MM.Format.FreeMind = Object.create(MM.Format, {
id: {value: "freemind"},
label: {value: "FreeMind"},
extension: {value: "mm"},
mime: {value: "application/x-freemind"}
});
MM.Format.FreeMind.to = function(data) {
var doc = document.implementation.createDocument(null, null, null);
var map = doc.createElement("map");
map.setAttribute("version", "1.0.1");
map.appendChild(this._serializeItem(doc, data.root));
doc.appendChild(map);
var serializer = new XMLSerializer();
return serializer.serializeToString(doc);
}
MM.Format.FreeMind.from = function(data) {
var parser = new DOMParser();
var doc = parser.parseFromString(data, "application/xml");
if (doc.documentElement.nodeName.toLowerCase() == "parsererror") { throw new Error(doc.documentElement.textContent); }
var root = doc.documentElement.getElementsByTagName("node")[0];
if (!root) { throw new Error("No root node found"); }
var json = {
root: this._parseNode(root, {shape:"underline"})
};
json.root.layout = "map";
json.root.shape = "ellipse";
return json;
}
MM.Format.FreeMind._serializeItem = function(doc, json) {
var elm = this._serializeAttributes(doc, json);
(json.children || []).forEach(function(child) {
elm.appendChild(this._serializeItem(doc, child));
}, this);
return elm;
}
MM.Format.FreeMind._serializeAttributes = function(doc, json) {
var elm = doc.createElement("node");
elm.setAttribute("TEXT", MM.Format.br2nl(json.text));
elm.setAttribute("ID", json.id);
if (json.side) { elm.setAttribute("POSITION", json.side); }
if (json.shape == "box") { elm.setAttribute("STYLE", "bubble"); }
if (json.collapsed) { elm.setAttribute("FOLDED", "true"); }
if (json.notes) {
var notesElm = doc.createElement("richcontent");
notesElm.setAttribute("TYPE", "NOTE");
// note: the freemind file format isn't very good.
notesElm.appendChild(doc.createCDATASection('<html><head></head><body>' + json.notes + '</body></html>'));
elm.appendChild(notesElm);
}
return elm;
}
MM.Format.FreeMind._parseNode = function(node, parent) {
var json = this._parseAttributes(node, parent);
for (var i=0;i<node.childNodes.length;i++) {
var child = node.childNodes[i];
if (child.nodeName.toLowerCase() == "node") {
json.children.push(this._parseNode(child, json));
}
}
return json;
}
MM.Format.FreeMind._parseAttributes = function(node, parent) {
var json = {
children: [],
text: MM.Format.nl2br(node.getAttribute("TEXT") || ""),
id: node.getAttribute("ID")
};
var position = node.getAttribute("POSITION");
if (position) { json.side = position; }
var style = node.getAttribute("STYLE");
if (style == "bubble") {
json.shape = "box";
} else {
json.shape = parent.shape;
}
if (node.getAttribute("FOLDED") == "true") { json.collapsed = 1; }
var children = node.children;
for (var i=0;i<children.length;i++) {
var child = children[i];
switch (child.nodeName.toLowerCase()) {
case "richcontent":
if (child.getAttribute("TYPE") == "NOTE") {
var body = child.querySelector("body > *");
if (body) {
var serializer = new XMLSerializer();
json.notes = serializer.serializeToString(body).trim();
}
}
break;
case "font":
if (child.getAttribute("ITALIC") == "true") { json.text = "<i>" + json.text + "</i>"; }
if (child.getAttribute("BOLD") == "true") { json.text = "<b>" + json.text + "</b>"; }
break;
}
}
return json;
}
MM.Format.MMA = Object.create(MM.Format.FreeMind, {
id: {value: "mma"},
label: {value: "Mind Map Architect"},
extension: {value: "mma"}
});
MM.Format.MMA._parseAttributes = function(node, parent) {
var json = {
children: [],
text: MM.Format.nl2br(node.getAttribute("title") || ""),
shape: "box"
};
if (node.getAttribute("expand") == "false") { json.collapsed = 1; }
var direction = node.getAttribute("direction");
if (direction == "0") { json.side = "left"; }
if (direction == "1") { json.side = "right"; }
var color = node.getAttribute("color");
if (color) {
var re = color.match(/^#(....)(....)(....)$/);
if (re) {
var r = parseInt(re[1], 16) >> 8;
var g = parseInt(re[2], 16) >> 8;
var b = parseInt(re[3], 16) >> 8;
r = Math.round(r/17).toString(16);
g = Math.round(g/17).toString(16);
b = Math.round(b/17).toString(16);
}
json.color = "#" + [r,g,b].join("");
}
json.icon = node.getAttribute("icon");
return json;
}
MM.Format.MMA._serializeAttributes = function(doc, json) {
var elm = doc.createElement("node");
elm.setAttribute("title", MM.Format.br2nl(json.text));
elm.setAttribute("expand", json.collapsed ? "false" : "true");
if (json.side) { elm.setAttribute("direction", json.side == "left" ? "0" : "1"); }
if (json.color) {
var parts = json.color.match(/^#(.)(.)(.)$/);
var r = new Array(5).join(parts[1]);
var g = new Array(5).join(parts[2]);
var b = new Array(5).join(parts[3]);
elm.setAttribute("color", "#" + [r,g,b].join(""));
}
if (json.icon) {
elm.setAttribute("icon", json.icon);
}
return elm;
}
MM.Format.Mup = Object.create(MM.Format, {
id: {value: "mup"},
label: {value: "MindMup"},
extension: {value: "mup"}
});
MM.Format.Mup.to = function(data) {
var root = this._MMtoMup(data.root);
return JSON.stringify(root, null, 2);
}
MM.Format.Mup.from = function(data) {
var source = JSON.parse(data);
var root = this._MupToMM(source);
root.layout = "map";
var map = {
root: root
}
return map;
}
MM.Format.Mup._MupToMM = function(item) {
var json = {
text: MM.Format.nl2br(item.title),
id: item.id,
shape: "box",
icon: item.icon
}
if (item.attr && item.attr.style && item.attr.style.background) {
json.color = item.attr.style.background;
}
if (item.attr && item.attr.collapsed) {
json.collapsed = 1;
}
if (item.ideas) {
var data = [];
for (var key in item.ideas) {
var child = this._MupToMM(item.ideas[key]);
var num = parseFloat(key);
child.side = (num < 0 ? "left" : "right");
data.push({
child: child,
num: num
});
}
data.sort(function(a, b) {
return a.num-b.num;
});
json.children = data.map(function(item) { return item.child; });
}
return json;
}
MM.Format.Mup._MMtoMup = function(item, side) {
var result = {
id: item.id,
title: MM.Format.br2nl(item.text),
icon: item.icon,
attr: {}
}
if (item.color) {
result.attr.style = {background:item.color};
}
if (item.collapsed) {
result.attr.collapsed = true;
}
if (item.children) {
result.ideas = {};
for (var i=0;i<item.children.length;i++) {
var child = item.children[i];
var childSide = side || child.side;
var key = i+1;
if (childSide == "left") { key *= -1; }
result.ideas[key] = this._MMtoMup(child, childSide);
}
}
return result;
}
MM.Format.Plaintext = Object.create(MM.Format, {
id: {value: "plaintext"},
label: {value: "Plain text"},
extension: {value: "txt"},
mime: {value: "application/vnd.mymind+txt"}
});
/**
* Can serialize also a sub-tree
*/
MM.Format.Plaintext.to = function(data) {
return this._serializeItem(data.root || data);
}
MM.Format.Plaintext.from = function(data) {
var lines = data.split("\n").filter(function(line) {
return line.match(/\S/);
});
var items = this._parseItems(lines);
if (items.length == 1) {
var result = {
root: items[0]
}
} else {
var result = {
root: {
text: "",
children: items
}
}
}
result.root.layout = "map";
return result;
}
MM.Format.Plaintext._serializeItem = function(item, depth) {
depth = depth || 0;
var lines = (item.children || []) .map(function(child) {
return this._serializeItem(child, depth+1);
}, this);
var prefix = new Array(depth+1).join("\t");
lines.unshift(prefix + item.text.replace(/\n/g, ""));
return lines.join("\n") + (depth ? "" : "\n");
}
MM.Format.Plaintext._parseItems = function(lines) {
var items = [];
if (!lines.length) { return items; }
var firstPrefix = this._parsePrefix(lines[0]);
var currentItem = null;
var childLines = [];
/* finalize a block of sub-children by converting them to items and appending */
var convertChildLinesToChildren = function() {
if (!currentItem || !childLines.length) { return; }
var children = this._parseItems(childLines);
if (children.length) { currentItem.children = children; }
childLines = [];
}
lines.forEach(function(line, index) {
if (this._parsePrefix(line) == firstPrefix) { /* new top-level item! */
convertChildLinesToChildren.call(this); /* finalize previous item */
currentItem = {text:line.match(/^\s*(.*)/)[1]};
items.push(currentItem);
} else { /* prepare as a future child */
childLines.push(line);
}
}, this);
convertChildLinesToChildren.call(this);
return items;
}
MM.Format.Plaintext._parsePrefix = function(line) {
return line.match(/^\s*/)[0];
}
MM.Backend = Object.create(MM.Repo);
/**
* Backends are allowed to have some internal state.
* This method notifies them that "their" map is no longer used
* (was either replaced by a new one or saved using other backend).
*/
MM.Backend.reset = function() {
}
MM.Backend.save = function(data, name) {
}
MM.Backend.load = function(name) {
}
MM.Backend.Local = Object.create(MM.Backend, {
label: {value: "Browser storage"},
id: {value: "local"},
prefix: {value: "mm.map."}
});
MM.Backend.Local.save = function(data, id, name) {
localStorage.setItem(this.prefix + id, data);
var names = this.list();
names[id] = name;
localStorage.setItem(this.prefix + "names", JSON.stringify(names));
}
MM.Backend.Local.load = function(id) {
var data = localStorage.getItem(this.prefix + id);
if (!data) { throw new Error("There is no such saved map"); }
return data;
}
MM.Backend.Local.remove = function(id) {
localStorage.removeItem(this.prefix + id);
var names = this.list();
delete names[id];
localStorage.setItem(this.prefix + "names", JSON.stringify(names));
}
MM.Backend.Local.list = function() {
try {
var data = localStorage.getItem(this.prefix + "names") || "{}";
return JSON.parse(data);
} catch (e) {
return {};
}
}
MM.Backend.WebDAV = Object.create(MM.Backend, {
id: {value: "webdav"},
label: {value: "Generic WebDAV"}
});
MM.Backend.WebDAV.save = function(data, url) {
return this._request("put", url, data);
}
MM.Backend.WebDAV.load = function(url) {
return this._request("get", url);
}
MM.Backend.WebDAV._request = function(method, url, data) {
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.withCredentials = true;
var promise = new Promise();
Promise.send(xhr, data).then(
function(xhr) { promise.fulfill(xhr.responseText); },
function(xhr) { promise.reject(new Error("HTTP/" + xhr.status + "\n\n" + xhr.responseText)); }
);
return promise;
}
MM.Backend.Image = Object.create(MM.Backend, {
id: {value: "image"},
label: {value: "Image"},
url: {value:"", writable:true}
});
MM.Backend.Image.save = function(data, name) {
var form = document.createElement("form");
form.action = this.url;
form.method = "post";
form.target = "_blank";
var input = document.createElement("input");
input.type = "hidden";
input.name = "data";
input.value = data;
form.appendChild(input);
var input = document.createElement("input");
input.type = "hidden";
input.name = "name";
input.value = name;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
form.parentNode.removeChild(form);
}
MM.Backend.File = Object.create(MM.Backend, {
id: {value: "file"},
label: {value: "File"},
input: {value:document.createElement("input")}
});
MM.Backend.File.save = function(data, name) {
var link = document.createElement("a");
link.download = name;
link.href = "data:text/plain;base64," + btoa(unescape(encodeURIComponent(data)));
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
var promise = new Promise().fulfill();
return promise;
}
MM.Backend.File.load = function() {
var promise = new Promise();
this.input.type = "file";
this.input.onchange = function(e) {
var file = e.target.files[0];
if (!file) { return; }
var reader = new FileReader();
reader.onload = function() { promise.fulfill({data:reader.result, name:file.name}); }
reader.onerror = function() { promise.reject(reader.error); }
reader.readAsText(file);
}.bind(this);
this.input.click();
return promise;
}
MM.Backend.Firebase = Object.create(MM.Backend, {
label: {value: "Firebase"},
id: {value: "firebase"},
ref: {value:null, writable:true},
_current: {value: {
id: null,
name: null,
data: null
}}
});
MM.Backend.Firebase.connect = function(server, auth) {
// Initialize Firebase
var config = {
apiKey: "AIzaSyBO_6uCK8pHjoz1c9htVwZi6Skpm8o4LtQ",
authDomain: "my-mind.firebaseapp.com",
databaseURL: "https://" + server + ".firebaseio.com",
projectId: "firebase-my-mind",
storageBucket: "firebase-my-mind.appspot.com",
messagingSenderId: "666556281676"
};
firebase.initializeApp(config);
this.ref = firebase.database().ref();
this.ref.child("names").on("value", function(snap) {
MM.publish("firebase-list", this, snap.val() || {});
}, this);
if (auth) {
return this._login(auth);
} else {
return new Promise().fulfill();
}
}
MM.Backend.Firebase.save = function(data, id, name) {
var promise = new Promise();
try {
this.ref.child("names/" + id).set(name);
this.ref.child("data/" + id).set(data, function(result) {
if (result) {
promise.reject(result);
} else {
promise.fulfill();
this._listenStart(data, id);
}
}.bind(this));
} catch (e) {
promise.reject(e);
}
return promise;
}
MM.Backend.Firebase.load = function(id) {
var promise = new Promise();
this.ref.child("data/" + id).once("value", function(snap) {
var data = snap.val();
if (data) {
promise.fulfill(data);
this._listenStart(data, id);
} else {
promise.reject(new Error("There is no such saved map"));
}
}, this);
return promise;
}
MM.Backend.Firebase.remove = function(id) {
var promise = new Promise();
try {
this.ref.child("names/" + id).remove();
this.ref.child("data/" + id).remove(function(result) {
if (result) {
promise.reject(result);
} else {
promise.fulfill();
}
});
} catch (e) {
promise.reject(e);
}
return promise;
}
MM.Backend.Firebase.reset = function() {
this._listenStop(); /* do not monitor current firebase ref for changes */
}
/**
* Merge current (remote) data with updated map
*/
MM.Backend.Firebase.mergeWith = function(data, name) {
var id = this._current.id;
if (name != this._current.name) {
this._current.name = name;
this.ref.child("names/" + id).set(name);
}
var dataRef = this.ref.child("data/" + id);
var oldData = this._current.data;
this._listenStop();
this._recursiveRefMerge(dataRef, oldData, data);
this._listenStart(data, id);
}
/**
* @param {Firebase} ref
* @param {object} oldData
* @param {object} newData
*/
MM.Backend.Firebase._recursiveRefMerge = function(ref, oldData, newData) {
var updateObject = {};
if (newData instanceof Array) { /* merge arrays */
for (var i=0; i<newData.length; i++) {
var newValue = newData[i];
if (!(i in oldData)) { /* new key */
updateObject[i] = newValue;
} else if (typeof(newValue) == "object") { /* recurse */
this._recursiveRefMerge(ref.child(i), oldData[i], newValue);
} else if (newValue !== oldData[i]) { /* changed key */
updateObject[i] = newValue;
}
}
for (var i=newData.length; i<oldData.length; i++) { updateObject[i] = null; } /* removed array items */
} else { /* merge objects */
for (var p in newData) { /* new/changed keys */
var newValue = newData[p];
if (!(p in oldData)) { /* new key */
updateObject[p] = newValue;
} else if (typeof(newValue) == "object") { /* recurse */
this._recursiveRefMerge(ref.child(p), oldData[p], newValue);
} else if (newValue !== oldData[p]) { /* changed key */
updateObject[p] = newValue;
}
}
for (var p in oldData) { /* removed keys */
if (!(p in newData)) { updateObject[p] = null; }
}
}
if (Object.keys(updateObject).length) { ref.update(updateObject); }
}
MM.Backend.Firebase._listenStart = function(data, id) {
if (this._current.id && this._current.id == id) { return; }
this._listenStop();
this._current.id = id;
this._current.data = data;
this.ref.child("data/" + id).on("value", this._valueChange, this);
}
MM.Backend.Firebase._listenStop = function() {
if (!this._current.id) { return; }
this.ref.child("data/" + this._current.id).off("value");
this._current.id = null;
this._current.name = null;
this._current.data = null;
}
/**
* Monitored remote ref changed.
* FIXME move timeout logic to ui.backend.firebase?
*/
MM.Backend.Firebase._valueChange = function(snap) {
this._current.data = snap.val();
if (this._changeTimeout) { clearTimeout(this._changeTimeout); }
this._changeTimeout = setTimeout(function() {
MM.publish("firebase-change", this, this._current.data);
}.bind(this), 200);
}
MM.Backend.Firebase._login = function(type) {
var provider;
switch (type) {
case "github":
provider = new firebase.auth.GithubAuthProvider();
break;
case "facebook":
provider = new firebase.auth.FacebookAuthProvider();
break;
case "twitter":
provider = new firebase.auth.TwitterAuthProvider();
break;
case "google":
provider = new firebase.auth.GoogleAuthProvider();
break;
}
return firebase.auth().signInWithPopup(provider).then(function(result) {
return result.user;
});
}
MM.Backend.GDrive = Object.create(MM.Backend, {
id: {value: "gdrive"},
label: {value: "Google Drive"},
scope: {value: "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.install"},
clientId: {value: "767837575056-h87qmlhmhb3djhaaqta5gv2v3koa9hii.apps.googleusercontent.com"},
apiKey: {value: "AIzaSyCzu1qVxlgufneOYpBgDJXN6Z9SNVcHYWM"},
fileId: {value: null, writable: true}
});
MM.Backend.GDrive.reset = function() {
this.fileId = null;
}
MM.Backend.GDrive.save = function(data, name, mime) {
return this._connect().then(
function() {
return this._send(data, name, mime);
}.bind(this)
);
}
MM.Backend.GDrive._send = function(data, name, mime) {
var promise = new Promise();
var path = "/upload/drive/v2/files";
var method = "POST";
if (this.fileId) {
path += "/" + this.fileId;
method = "PUT";
}
var boundary = "b" + Math.random();
var delimiter = "--" + boundary;
var body = [
delimiter,
"Content-Type: application/json", "",
JSON.stringify({title:name}),
delimiter,
"Content-Type: " + mime, "",
data,
delimiter + "--"
].join("\r\n");
var request = gapi.client.request({
path: path,
method: method,
headers: {
"Content-Type": "multipart/mixed; boundary='" + boundary + "'"
},
body: body
});
request.execute(function(response) {
if (!response) {
promise.reject(new Error("Failed to upload to Google Drive"));
} else if (response.error) {
promise.reject(response.error);
} else {
this.fileId = response.id;
promise.fulfill();
}
}.bind(this));
return promise;
}
MM.Backend.GDrive.load = function(id) {
return this._connect().then(
this._load.bind(this, id)
);
}
MM.Backend.GDrive._load = function(id) {
this.fileId = id;
var promise = new Promise();
var request = gapi.client.request({
path: "/drive/v2/files/" + this.fileId,
method: "GET"
});
request.execute(function(response) {
if (response && response.downloadUrl) {
var xhr = new XMLHttpRequest();
xhr.open("get", response.downloadUrl, true);
xhr.setRequestHeader("Authorization", "Bearer " + gapi.auth.getToken().access_token);
Promise.send(xhr).then(
function(xhr) { promise.fulfill({data:xhr.responseText, name:response.title, mime:response.mimeType}); },
function(xhr) { promise.reject(xhr.responseText); }
);
} else {
promise.reject(response && response.error || new Error("Failed to download file"));
}
}.bind(this));
return promise;
}
MM.Backend.GDrive.pick = function() {
return this._connect().then(
this._pick.bind(this)
);
}
MM.Backend.GDrive._pick = function() {
var promise = new Promise();
var token = gapi.auth.getToken();
var formats = MM.Format.getAll();
var mimeTypes = ["application/json; charset=UTF-8", "application/json"];
formats.forEach(function(format) {
if (format.mime) { mimeTypes.unshift(format.mime); }
});
var view = new google.picker.DocsView(google.picker.ViewId.DOCS)
.setMimeTypes(mimeTypes.join(","))
.setMode(google.picker.DocsViewMode.LIST);
var picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.addView(view)
.setOAuthToken(token.access_token)
.setDeveloperKey(this.apiKey)
.setCallback(function(data) {
switch (data[google.picker.Response.ACTION]) {
case google.picker.Action.PICKED:
var doc = data[google.picker.Response.DOCUMENTS][0];
promise.fulfill(doc.id);
break;
case google.picker.Action.CANCEL:
promise.fulfill(null);
break;
}
})
.build();
picker.setVisible(true);
return promise;
}
MM.Backend.GDrive._connect = function() {
if (window.gapi && window.gapi.auth.getToken()) {
return new Promise().fulfill();
} else {
return this._loadGapi().then(this._auth.bind(this));
}
}
MM.Backend.GDrive._loadGapi = function() {
var promise = new Promise();
if (window.gapi) { return promise.fulfill(); }
var script = document.createElement("script");
var name = ("cb"+Math.random()).replace(".", "");
window[name] = promise.fulfill.bind(promise);
script.src = "https://apis.google.com/js/client:picker.js?onload=" + name;
document.body.appendChild(script);
return promise;
}
MM.Backend.GDrive._auth = function(forceUI) {
var promise = new Promise();
gapi.auth.authorize({
"client_id": this.clientId,
"scope": this.scope,
"immediate": !forceUI
}, function(token) {
if (token && !token.error) { /* done */
promise.fulfill();
} else if (!forceUI) { /* try again with ui */
this._auth(true).then(
promise.fulfill.bind(promise),
promise.reject.bind(promise)
);
} else { /* bad luck */
promise.reject(token && token.error || new Error("Failed to authorize with Google"));
}
}.bind(this));
return promise;
}
MM.UI = function() {
this._node = document.querySelector(".ui");
this._toggle = this._node.querySelector("#toggle");
this._layout = new MM.UI.Layout();
this._shape = new MM.UI.Shape();
this._icon = new MM.UI.Icon();
this._color = new MM.UI.Color();
this._value = new MM.UI.Value();
this._status = new MM.UI.Status();
MM.subscribe("item-select", this);
MM.subscribe("item-change", this);
this._node.addEventListener("click", this);
this._node.addEventListener("change", this);
this.toggle();
}
MM.UI.prototype.handleMessage = function(message, publisher) {
switch (message) {
case "item-select":
this._update();
break;
case "item-change":
if (publisher == MM.App.current) { this._update(); }
break;
}
}
MM.UI.prototype.handleEvent = function(e) {
switch (e.type) {
case "click":
if (e.target.nodeName.toLowerCase() != "select") { MM.Clipboard.focus(); } /* focus the clipboard (2c) */
if (e.target == this._toggle) {
this.toggle();
return;
}
var node = e.target;
while (node != document) {
var command = node.getAttribute("data-command");
if (command) {
MM.Command[command].execute();
return;
}
node = node.parentNode;
}
break;
case "change":
MM.Clipboard.focus(); /* focus the clipboard (2c) */
break;
}
}
MM.UI.prototype.toggle = function() {
this._node.classList.toggle("visible");
MM.publish("ui-change", this);
}
MM.UI.prototype.getWidth = function() {
return (this._node.classList.contains("visible") ? this._node.offsetWidth : 0);
}
MM.UI.prototype._update = function() {
this._layout.update();
this._shape.update();
this._icon.update();
this._value.update();
this._status.update();
}
MM.UI.Layout = function() {
this._select = document.querySelector("#layout");
this._select.appendChild(MM.Layout.Map.buildOption());
var label = this._buildGroup("Graph");
label.appendChild(MM.Layout.Graph.Right.buildOption());
label.appendChild(MM.Layout.Graph.Left.buildOption());
label.appendChild(MM.Layout.Graph.Down.buildOption());
label.appendChild(MM.Layout.Graph.Up.buildOption());
var label = this._buildGroup("Tree");
label.appendChild(MM.Layout.Tree.Right.buildOption());
label.appendChild(MM.Layout.Tree.Left.buildOption());
this._select.addEventListener("change", this);
}
MM.UI.Layout.prototype.update = function() {
var value = "";
var layout = MM.App.current.getOwnLayout();
if (layout) { value = layout.id; }
this._select.value = value;
this._getOption("").disabled = MM.App.current.isRoot();
this._getOption(MM.Layout.Map.id).disabled = !MM.App.current.isRoot();
}
MM.UI.Layout.prototype.handleEvent = function(e) {
var layout = MM.Layout.getById(this._select.value);
var action = new MM.Action.SetLayout(MM.App.current, layout);
MM.App.action(action);
}
MM.UI.Layout.prototype._getOption = function(value) {
return this._select.querySelector("option[value='" + value + "']");
}
MM.UI.Layout.prototype._buildGroup = function(label) {
var node = document.createElement("optgroup");
node.label = label;
this._select.appendChild(node);
return node;
}
MM.UI.Shape = function() {
this._select = document.querySelector("#shape");
this._select.appendChild(MM.Shape.Box.buildOption());
this._select.appendChild(MM.Shape.Ellipse.buildOption());
this._select.appendChild(MM.Shape.Underline.buildOption());
this._select.addEventListener("change", this);
}
MM.UI.Shape.prototype.update = function() {
var value = "";
var shape = MM.App.current.getOwnShape();
if (shape) { value = shape.id; }
this._select.value = value;
}
MM.UI.Shape.prototype.handleEvent = function(e) {
var shape = MM.Shape.getById(this._select.value);
var action = new MM.Action.SetShape(MM.App.current, shape);
MM.App.action(action);
}
MM.UI.Value = function() {
this._select = document.querySelector("#value");
this._select.addEventListener("change", this);
}
MM.UI.Value.prototype.update = function() {
var value = MM.App.current.getValue();
if (value === null) { value = ""; }
if (typeof(value) == "number") { value = "num" }
this._select.value = value;
}
MM.UI.Value.prototype.handleEvent = function(e) {
var value = this._select.value;
if (value == "num") {
MM.Command.Value.execute();
} else {
var action = new MM.Action.SetValue(MM.App.current, value || null);
MM.App.action(action);
}
}
MM.UI.Status = function() {
this._select = document.querySelector("#status");
this._select.addEventListener("change", this);
}
MM.UI.Status.prototype.update = function() {
this._select.value = MM.App.current.getStatus() || "";
}
MM.UI.Status.prototype.handleEvent = function(e) {
var action = new MM.Action.SetStatus(MM.App.current, this._select.value || null);
MM.App.action(action);
}
MM.UI.Color = function() {
this._node = document.querySelector("#color");
this._node.addEventListener("click", this);
var items = this._node.querySelectorAll("[data-color]");
for (var i=0;i<items.length;i++) {
var item = items[i];
item.style.backgroundColor = item.getAttribute("data-color");
}
}
MM.UI.Color.prototype.handleEvent = function(e) {
e.preventDefault();
if (!e.target.hasAttribute("data-color")) { return; }
var color = e.target.getAttribute("data-color") || null;
var action = new MM.Action.SetColor(MM.App.current, color);
MM.App.action(action);
}
MM.UI.Icon = function() {
this._select = document.querySelector("#icons");
this._select.addEventListener("change", this);
}
MM.UI.Icon.prototype.update = function() {
this._select.value = MM.App.current.getIcon() || "";
}
MM.UI.Icon.prototype.handleEvent = function(e) {
var action = new MM.Action.SetIcon(MM.App.current, this._select.value || null);
MM.App.action(action);
}
MM.UI.Help = function() {
this._node = document.querySelector("#help");
this._map = {
8: "Backspace",
9: "Tab",
13: "↩",
32: "Spacebar",
33: "PgUp",
34: "PgDown",
35: "End",
36: "Home",
37: "←",
38: "↑",
39: "→",
40: "↓",
45: "Insert",
46: "Delete",
65: "A",
68: "D",
83: "S",
87: "W",
112: "F1",
113: "F2",
114: "F3",
115: "F4",
116: "F5",
117: "F6",
118: "F7",
119: "F8",
120: "F9",
121: "F10",
"-": "&minus;"
};
this._build();
}
MM.UI.Help.prototype.toggle = function() {
this._node.classList.toggle("visible");
}
MM.UI.Help.prototype._build = function() {
var t = this._node.querySelector(".navigation");
this._buildRow(t, "Pan");
this._buildRow(t, "Select");
this._buildRow(t, "SelectRoot");
this._buildRow(t, "SelectParent");
this._buildRow(t, "Center");
this._buildRow(t, "ZoomIn", "ZoomOut");
this._buildRow(t, "Fold");
var t = this._node.querySelector(".manipulation");
this._buildRow(t, "InsertSibling");
this._buildRow(t, "InsertChild");
this._buildRow(t, "Swap");
this._buildRow(t, "Side");
this._buildRow(t, "Delete");
this._buildRow(t, "Copy");
this._buildRow(t, "Cut");
this._buildRow(t, "Paste");
var t = this._node.querySelector(".editing");
this._buildRow(t, "Value");
this._buildRow(t, "Yes", "No", "Computed");
this._buildRow(t, "Edit");
this._buildRow(t, "Newline");
this._buildRow(t, "Bold");
this._buildRow(t, "Italic");
this._buildRow(t, "Underline");
this._buildRow(t, "Strikethrough");
var t = this._node.querySelector(".other");
this._buildRow(t, "Undo", "Redo");
this._buildRow(t, "Save");
this._buildRow(t, "SaveAs");
this._buildRow(t, "Load");
this._buildRow(t, "Help");
this._buildRow(t, "Notes");
this._buildRow(t, "UI");
}
MM.UI.Help.prototype._buildRow = function(table, commandName) {
var row = table.insertRow(-1);
var labels = [];
var keys = [];
for (var i=1;i<arguments.length;i++) {
var command = MM.Command[arguments[i]];
if (!command) { continue; }
labels.push(command.label);
keys = keys.concat(command.keys.map(this._formatKey, this));
}
row.insertCell(-1).innerHTML = labels.join("/");
row.insertCell(-1).innerHTML = keys.join("/");
}
MM.UI.Help.prototype._formatKey = function(key) {
var str = "";
if (key.ctrlKey) { str += "Ctrl+"; }
if (key.altKey) { str += "Alt+"; }
if (key.shiftKey) { str += "Shift+"; }
if (key.charCode) {
var ch = String.fromCharCode(key.charCode);
str += this._map[ch] || ch.toUpperCase();
}
if (key.keyCode) { str += this._map[key.keyCode] || String.fromCharCode(key.keyCode); }
return str;
}
MM.UI.Help.prototype.close = function() {
if (this._node.classList.contains("visible")) {
this._node.classList.toggle("visible");
}
}
MM.UI.Notes = function() {
this._node = document.querySelector("#notes");
}
MM.UI.Notes.prototype.toggle = function() {
this._node.classList.toggle("visible");
}
MM.UI.Notes.prototype.close = function() {
if (this._node.classList.contains("visible")) {
this._node.classList.toggle("visible");
MM.Clipboard.focus();
}
}
MM.UI.Notes.prototype.update = function(html) {
if (html.trim().length === 0) {
MM.App.current._notes = null;
} else {
MM.App.current._notes = html;
}
MM.App.current.update();
}
MM.UI.IO = function() {
this._prefix = "mm.app.";
this._mode = "";
this._node = document.querySelector("#io");
this._heading = this._node.querySelector("h3");
this._backend = this._node.querySelector("#backend");
this._currentBackend = null;
this._backends = {};
var ids = ["local", "firebase", "gdrive", "file", "webdav", "image"];
ids.forEach(function(id) {
var ui = MM.UI.Backend.getById(id);
ui.init(this._backend);
this._backends[id] = ui;
}, this);
this._backend.value = localStorage.getItem(this._prefix + "backend") || MM.Backend.File.id;
this._backend.addEventListener("change", this);
MM.subscribe("map-new", this);
MM.subscribe("save-done", this);
MM.subscribe("load-done", this);
}
MM.UI.IO.prototype.restore = function() {
var parts = {};
location.search.substring(1).split("&").forEach(function(item) {
var keyvalue = item.split("=");
parts[decodeURIComponent(keyvalue[0])] = decodeURIComponent(keyvalue[1]);
});
/* backwards compatibility */
if ("map" in parts) { parts.url = parts.map; }
/* just URL means webdav backend */
if ("url" in parts && !("b" in parts)) { parts.b = "webdav"; }
var backend = MM.UI.Backend.getById(parts.b);
if (backend) { /* saved backend info */
backend.setState(parts);
return;
}
if (parts.state) { /* opened from gdrive */
try {
var state = JSON.parse(parts.state);
if (state.action == "open") {
state = {
b: "gdrive",
id: state.ids[0]
};
MM.UI.Backend.GDrive.setState(state);
} else {
history.replaceState(null, "", ".");
}
return;
} catch (e) { }
}
}
MM.UI.IO.prototype.handleMessage = function(message, publisher) {
switch (message) {
case "map-new":
this._setCurrentBackend(null);
break;
case "save-done":
case "load-done":
this.hide();
this._setCurrentBackend(publisher);
break;
}
}
MM.UI.IO.prototype.show = function(mode) {
this._mode = mode;
this._node.classList.add("visible");
this._heading.innerHTML = mode;
this._syncBackend();
window.addEventListener("keydown", this);
}
MM.UI.IO.prototype.hide = function() {
if (!this._node.classList.contains("visible")) { return; }
this._node.classList.remove("visible");
MM.Clipboard.focus();
window.removeEventListener("keydown", this);
}
MM.UI.IO.prototype.quickSave = function() {
if (this._currentBackend) {
this._currentBackend.save();
} else {
this.show("save");
}
}
MM.UI.IO.prototype.handleEvent = function(e) {
switch (e.type) {
case "keydown":
if (e.keyCode == 27) { this.hide(); }
break;
case "change":
this._syncBackend();
break;
}
}
MM.UI.IO.prototype._syncBackend = function() {
var all = this._node.querySelectorAll("div[id]");
[].slice.apply(all).forEach(function(node) { node.style.display = "none"; });
this._node.querySelector("#" + this._backend.value).style.display = "";
this._backends[this._backend.value].show(this._mode);
}
/**
* @param {MM.UI.Backend} backend
*/
MM.UI.IO.prototype._setCurrentBackend = function(backend) {
if (this._currentBackend && this._currentBackend != backend) { this._currentBackend.reset(); }
if (backend) { localStorage.setItem(this._prefix + "backend", backend.id); }
this._currentBackend = backend;
try {
this._updateURL(); /* fails when on file:/// */
} catch (e) {}
}
MM.UI.IO.prototype._updateURL = function() {
var data = this._currentBackend && this._currentBackend.getState();
if (!data) {
history.replaceState(null, "", ".");
} else {
var arr = Object.keys(data).map(function(key) {
return encodeURIComponent(key)+"="+encodeURIComponent(data[key]);
});
history.replaceState(null, "", "?" + arr.join("&"));
}
}
MM.UI.Backend = Object.create(MM.Repo);
MM.UI.Backend.init = function(select) {
this._backend = MM.Backend.getById(this.id);
this._mode = "";
this._prefix = "mm.app." + this.id + ".";
this._node = document.querySelector("#" + this.id);
this._cancel = this._node.querySelector(".cancel");
this._cancel.addEventListener("click", this);
this._go = this._node.querySelector(".go");
this._go.addEventListener("click", this);
select.appendChild(this._backend.buildOption());
}
MM.UI.Backend.reset = function() {
this._backend.reset();
}
MM.UI.Backend.setState = function(data) {
}
MM.UI.Backend.getState = function() {
return null;
}
MM.UI.Backend.handleEvent = function(e) {
switch (e.target) {
case this._cancel:
MM.App.io.hide();
break;
case this._go:
this._action();
break;
}
}
MM.UI.Backend.save = function() {
}
MM.UI.Backend.load = function() {
}
MM.UI.Backend.show = function(mode) {
this._mode = mode;
this._go.innerHTML = mode.charAt(0).toUpperCase() + mode.substring(1);
var all = this._node.querySelectorAll("[data-for]");
[].concat.apply([], all).forEach(function(node) { node.style.display = "none"; });
var visible = this._node.querySelectorAll("[data-for~=" + mode + "]");
[].concat.apply([], visible).forEach(function(node) { node.style.display = ""; });
/* switch to 2a: steal focus from the current item */
this._go.focus();
}
MM.UI.Backend._action = function() {
switch (this._mode) {
case "save":
this.save();
break;
case "load":
this.load();
break;
}
}
MM.UI.Backend._saveDone = function() {
MM.App.setThrobber(false);
MM.publish("save-done", this);
}
MM.UI.Backend._loadDone = function(json) {
MM.App.setThrobber(false);
try {
MM.App.setMap(MM.Map.fromJSON(json));
MM.publish("load-done", this);
} catch (e) {
this._error(e);
}
}
MM.UI.Backend._error = function(e) {
MM.App.setThrobber(false);
alert("IO error: " + e.message);
}
MM.UI.Backend._buildList = function(list, select) {
var data = [];
for (var id in list) {
data.push({id:id, name:list[id]});
}
data.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
data.forEach(function(item) {
var o = document.createElement("option");
o.value = item.id;
o.innerHTML = item.name;
select.appendChild(o);
});
}
MM.UI.Backend.File = Object.create(MM.UI.Backend, {
id: {value: "file"}
});
MM.UI.Backend.File.init = function(select) {
MM.UI.Backend.init.call(this, select);
this._format = this._node.querySelector(".format");
this._format.appendChild(MM.Format.JSON.buildOption());
this._format.appendChild(MM.Format.FreeMind.buildOption());
this._format.appendChild(MM.Format.MMA.buildOption());
this._format.appendChild(MM.Format.Mup.buildOption());
this._format.appendChild(MM.Format.Plaintext.buildOption());
this._format.value = localStorage.getItem(this._prefix + "format") || MM.Format.JSON.id;
}
MM.UI.Backend.File.show = function(mode) {
MM.UI.Backend.show.call(this, mode);
this._go.innerHTML = (mode == "save" ? "Save" : "Browse");
}
MM.UI.Backend.File._action = function() {
localStorage.setItem(this._prefix + "format", this._format.value);
MM.UI.Backend._action.call(this);
}
MM.UI.Backend.File.save = function() {
var format = MM.Format.getById(this._format.value);
var json = MM.App.map.toJSON();
var data = format.to(json);
var name = MM.App.map.getName() + "." + format.extension;
this._backend.save(data, name).then(
this._saveDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.File.load = function() {
this._backend.load().then(
this._loadDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.File._loadDone = function(data) {
try {
var format = MM.Format.getByName(data.name) || MM.Format.JSON;
var json = format.from(data.data);
} catch (e) {
this._error(e);
}
MM.UI.Backend._loadDone.call(this, json);
}
MM.UI.Backend.WebDAV = Object.create(MM.UI.Backend, {
id: {value: "webdav"}
});
MM.UI.Backend.WebDAV.init = function(select) {
MM.UI.Backend.init.call(this, select);
this._url = this._node.querySelector(".url");
this._url.value = localStorage.getItem(this._prefix + "url") || "";
this._current = "";
}
MM.UI.Backend.WebDAV.getState = function() {
var data = {
url: this._current
};
return data;
}
MM.UI.Backend.WebDAV.setState = function(data) {
this._load(data.url);
}
MM.UI.Backend.WebDAV.save = function() {
MM.App.setThrobber(true);
var map = MM.App.map;
var url = this._url.value;
localStorage.setItem(this._prefix + "url", url);
if (url.match(/\.mymind$/)) { /* complete file name */
} else { /* just a path */
if (url.charAt(url.length-1) != "/") { url += "/"; }
url += map.getName() + "." + MM.Format.JSON.extension;
}
this._current = url;
var json = map.toJSON();
var data = MM.Format.JSON.to(json);
this._backend.save(data, url).then(
this._saveDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.WebDAV.load = function() {
this._load(this._url.value);
}
MM.UI.Backend.WebDAV._load = function(url) {
this._current = url;
MM.App.setThrobber(true);
var lastIndex = url.lastIndexOf("/");
this._url.value = url.substring(0, lastIndex);
localStorage.setItem(this._prefix + "url", this._url.value);
this._backend.load(url).then(
this._loadDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.WebDAV._loadDone = function(data) {
try {
var json = MM.Format.JSON.from(data);
} catch (e) {
this._error(e);
}
MM.UI.Backend._loadDone.call(this, json);
}
MM.UI.Backend.Image = Object.create(MM.UI.Backend, {
id: {value: "image"}
});
MM.UI.Backend.Image.save = function() {
var name = MM.App.map.getName();
var json = MM.App.map.toJSON();
var data = MM.Format.JSON.to(json);
this._backend.save(data, name);
}
MM.UI.Backend.Image.load = null;
MM.UI.Backend.Local = Object.create(MM.UI.Backend, {
id: {value: "local"}
});
MM.UI.Backend.Local.init = function(select) {
MM.UI.Backend.init.call(this, select);
this._list = this._node.querySelector(".list");
this._remove = this._node.querySelector(".remove");
this._remove.addEventListener("click", this);
}
MM.UI.Backend.Local.handleEvent = function(e) {
MM.UI.Backend.handleEvent.call(this, e);
switch (e.target) {
case this._remove:
var id = this._list.value;
if (!id) { break; }
this._backend.remove(id);
this.show(this._mode);
break;
}
}
MM.UI.Backend.Local.show = function(mode) {
MM.UI.Backend.show.call(this, mode);
this._go.disabled = false;
if (mode == "load") {
var list = this._backend.list();
this._list.innerHTML = "";
if (Object.keys(list).length) {
this._go.disabled = false;
this._remove.disabled = false;
this._buildList(list, this._list);
} else {
this._go.disabled = true;
this._remove.disabled = true;
var o = document.createElement("option");
o.innerHTML = "(no maps saved)";
this._list.appendChild(o);
}
}
}
MM.UI.Backend.Local.setState = function(data) {
this._load(data.id);
}
MM.UI.Backend.Local.getState = function() {
var data = {
b: this.id,
id: MM.App.map.getId()
};
return data;
}
MM.UI.Backend.Local.save = function() {
var json = MM.App.map.toJSON();
var data = MM.Format.JSON.to(json);
try {
this._backend.save(data, MM.App.map.getId(), MM.App.map.getName());
this._saveDone();
} catch (e) {
this._error(e);
}
}
MM.UI.Backend.Local.load = function() {
this._load(this._list.value);
}
MM.UI.Backend.Local._load = function(id) {
try {
var data = this._backend.load(id);
var json = MM.Format.JSON.from(data);
this._loadDone(json);
} catch (e) {
this._error(e);
}
}
MM.UI.Backend.Firebase = Object.create(MM.UI.Backend, {
id: {value: "firebase"}
});
MM.UI.Backend.Firebase.init = function(select) {
MM.UI.Backend.init.call(this, select);
this._online = false;
this._itemChangeTimeout = null;
this._list = this._node.querySelector(".list");
this._server = this._node.querySelector(".server");
this._server.value = localStorage.getItem(this._prefix + "server") || "my-mind";
this._auth = this._node.querySelector(".auth");
this._auth.value = localStorage.getItem(this._prefix + "auth") || "";
this._remove = this._node.querySelector(".remove");
this._remove.addEventListener("click", this);
this._go.disabled = false;
MM.subscribe("firebase-list", this);
MM.subscribe("firebase-change", this);
}
MM.UI.Backend.Firebase.setState = function(data) {
this._connect(data.s, data.a).then(
this._load.bind(this, data.id),
this._error.bind(this)
);
}
MM.UI.Backend.Firebase.getState = function() {
var data = {
id: MM.App.map.getId(),
b: this.id,
s: this._server.value
};
if (this._auth.value) { data.a = this._auth.value; }
return data;
}
MM.UI.Backend.Firebase.show = function(mode) {
MM.UI.Backend.show.call(this, mode);
this._sync();
}
MM.UI.Backend.Firebase.handleEvent = function(e) {
MM.UI.Backend.handleEvent.call(this, e);
switch (e.target) {
case this._remove:
var id = this._list.value;
if (!id) { break; }
MM.App.setThrobber(true);
this._backend.remove(id).then(
function() { MM.App.setThrobber(false); },
this._error.bind(this)
);
break;
}
}
MM.UI.Backend.Firebase.handleMessage = function(message, publisher, data) {
switch (message) {
case "firebase-list":
this._list.innerHTML = "";
if (Object.keys(data).length) {
this._buildList(data, this._list);
} else {
var o = document.createElement("option");
o.innerHTML = "(no maps saved)";
this._list.appendChild(o);
}
this._sync();
break;
case "firebase-change":
if (data) {
MM.unsubscribe("item-change", this);
MM.App.map.mergeWith(data);
MM.subscribe("item-change", this);
} else { /* FIXME */
console.log("remote data disappeared");
}
break;
case "item-change":
if (this._itemChangeTimeout) { clearTimeout(this._itemChangeTimeout); }
this._itemChangeTimeout = setTimeout(this._itemChange.bind(this), 200);
break;
}
}
MM.UI.Backend.Firebase.reset = function() {
this._backend.reset();
MM.unsubscribe("item-change", this);
}
MM.UI.Backend.Firebase._itemChange = function() {
var map = MM.App.map;
this._backend.mergeWith(map.toJSON(), map.getName());
}
MM.UI.Backend.Firebase._action = function() {
if (!this._online) {
this._connect(this._server.value, this._auth.value);
return;
}
MM.UI.Backend._action.call(this);
}
MM.UI.Backend.Firebase.save = function() {
MM.App.setThrobber(true);
var map = MM.App.map;
this._backend.save(map.toJSON(), map.getId(), map.getName()).then(
this._saveDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.Firebase.load = function() {
this._load(this._list.value);
}
MM.UI.Backend.Firebase._load = function(id) {
MM.App.setThrobber(true);
/* FIXME posere se kdyz zmenim jeden firebase na jiny, mozna */
this._backend.load(id).then(
this._loadDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.Firebase._connect = function(server, auth) {
var promise = new Promise();
this._server.value = server;
this._auth.value = auth;
this._server.disabled = true;
this._auth.disabled = true;
localStorage.setItem(this._prefix + "server", server);
localStorage.setItem(this._prefix + "auth", auth || "");
this._go.disabled = true;
MM.App.setThrobber(true);
this._backend.connect(server, auth).then(
function() {
this._connected();
promise.fulfill();
}.bind(this),
promise.reject.bind(promise)
);
return promise;
}
MM.UI.Backend.Firebase._connected = function() {
MM.App.setThrobber(false);
this._online = true;
this._sync();
}
MM.UI.Backend.Firebase._sync = function() {
if (!this._online) {
this._go.innerHTML = "Connect";
return;
}
this._go.disabled = false;
if (this._mode == "load" && !this._list.value) { this._go.disabled = true; }
this._go.innerHTML = this._mode.charAt(0).toUpperCase() + this._mode.substring(1);
}
MM.UI.Backend.Firebase._loadDone = function() {
MM.subscribe("item-change", this);
MM.UI.Backend._loadDone.apply(this, arguments);
}
MM.UI.Backend.Firebase._saveDone = function() {
MM.subscribe("item-change", this);
MM.UI.Backend._saveDone.apply(this, arguments);
}
MM.UI.Backend.GDrive = Object.create(MM.UI.Backend, {
id: {value: "gdrive"}
});
MM.UI.Backend.GDrive.init = function(select) {
MM.UI.Backend.init.call(this, select);
this._format = this._node.querySelector(".format");
this._format.appendChild(MM.Format.JSON.buildOption());
this._format.appendChild(MM.Format.FreeMind.buildOption());
this._format.appendChild(MM.Format.MMA.buildOption());
this._format.appendChild(MM.Format.Mup.buildOption());
this._format.appendChild(MM.Format.Plaintext.buildOption());
this._format.value = localStorage.getItem(this._prefix + "format") || MM.Format.JSON.id;
}
MM.UI.Backend.GDrive.save = function() {
MM.App.setThrobber(true);
var format = MM.Format.getById(this._format.value);
var json = MM.App.map.toJSON();
var data = format.to(json);
var name = MM.App.map.getName();
var mime = "text/plain";
if (format.mime) {
mime = format.mime;
} else {
name += "." + format.extension;
}
this._backend.save(data, name, mime).then(
this._saveDone.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.GDrive.load = function() {
MM.App.setThrobber(true);
this._backend.pick().then(
this._picked.bind(this),
this._error.bind(this)
);
}
MM.UI.Backend.GDrive._picked = function(id) {
MM.App.setThrobber(false);
if (!id) { return; }
MM.App.setThrobber(true);
this._backend.load(id).then(
this._loadDone.bind(this),
this._error.bind(this)
)
}
MM.UI.Backend.GDrive.setState = function(data) {
this._picked(data.id);
}
MM.UI.Backend.GDrive.getState = function() {
var data = {
b: this.id,
id: this._backend.fileId
};
return data;
}
MM.UI.Backend.GDrive._loadDone = function(data) {
try {
var format = MM.Format.getByMime(data.mime) || MM.Format.getByName(data.name) || MM.Format.JSON;
var json = format.from(data.data);
} catch (e) {
this._error(e);
}
MM.UI.Backend._loadDone.call(this, json);
}
MM.Mouse = {
TOUCH_DELAY: 500,
_port: null,
_cursor: [0, 0],
_pos: [0, 0], /* ghost pos */
_mode: "",
_item: null,
_ghost: null,
_oldDragState: null,
_touchTimeout: null
}
MM.Mouse.init = function(port) {
this._port = port;
this._port.addEventListener("touchstart", this);
this._port.addEventListener("mousedown", this);
this._port.addEventListener("click", this);
this._port.addEventListener("dblclick", this);
this._port.addEventListener("wheel", this);
this._port.addEventListener("mousewheel", this);
this._port.addEventListener("contextmenu", this);
}
MM.Mouse.handleEvent = function(e) {
switch (e.type) {
case "click":
var item = MM.App.map.getItemFor(e.target);
if (MM.App.editing && item == MM.App.current) { return; } /* ignore on edited node */
if (item) { MM.App.select(item); }
break;
case "dblclick":
var item = MM.App.map.getItemFor(e.target);
if (item) { MM.Command.Edit.execute(); }
break;
case "contextmenu":
this._endDrag();
e.preventDefault();
var item = MM.App.map.getItemFor(e.target);
item && MM.App.select(item);
MM.Menu.open(e.clientX, e.clientY);
break;
case "touchstart":
if (e.touches.length > 1) { return; }
e.clientX = e.touches[0].clientX;
e.clientY = e.touches[0].clientY;
case "mousedown":
var item = MM.App.map.getItemFor(e.target);
if (MM.App.editing) {
if (item == MM.App.current) { return; } /* ignore dnd on edited node */
MM.Command.Finish.execute(); /* clicked elsewhere => finalize edit */
}
if (e.type == "mousedown") { e.preventDefault(); } /* to prevent blurring the clipboard node */
if (e.type == "touchstart") { /* context menu here, after we have the item */
this._touchTimeout = setTimeout(function() {
item && MM.App.select(item);
MM.Menu.open(e.clientX, e.clientY);
}, this.TOUCH_DELAY);
}
this._startDrag(e, item);
break;
case "touchmove":
if (e.touches.length > 1) { return; }
e.clientX = e.touches[0].clientX;
e.clientY = e.touches[0].clientY;
clearTimeout(this._touchTimeout);
case "mousemove":
this._processDrag(e);
break;
case "touchend":
clearTimeout(this._touchTimeout);
case "mouseup":
this._endDrag();
break;
case "wheel":
case "mousewheel":
var dir = 0;
if (e.wheelDelta) {
if (e.wheelDelta < 0) {
dir = -1;
} else if (e.wheelDelta > 0) {
dir = 1;
}
}
if (e.deltaY) {
if (e.deltaY > 0) {
dir = -1;
} else if (e.deltaY < 0) {
dir = 1;
}
}
if (dir) {
MM.App.adjustFontSize(dir);
}
break;
}
}
MM.Mouse._startDrag = function(e, item) {
if (e.type == "mousedown") {
e.preventDefault(); /* no selections allowed. only for mouse; preventing touchstart would prevent Safari from emulating clicks */
this._port.addEventListener("mousemove", this);
this._port.addEventListener("mouseup", this);
} else {
this._port.addEventListener("touchmove", this);
this._port.addEventListener("touchend", this);
}
this._cursor[0] = e.clientX;
this._cursor[1] = e.clientY;
if (item && !item.isRoot()) {
this._mode = "drag";
this._item = item;
} else {
this._mode = "pan";
this._port.style.cursor = "move";
}
}
MM.Mouse._processDrag = function(e) {
e.preventDefault();
var dx = e.clientX - this._cursor[0];
var dy = e.clientY - this._cursor[1];
this._cursor[0] = e.clientX;
this._cursor[1] = e.clientY;
switch (this._mode) {
case "drag":
if (!this._ghost) {
this._port.style.cursor = "move";
this._buildGhost(dx, dy);
}
this._moveGhost(dx, dy);
var state = this._computeDragState();
this._visualizeDragState(state);
break;
case "pan":
MM.App.map.moveBy(dx, dy);
break;
}
}
MM.Mouse._endDrag = function() {
this._port.style.cursor = "";
this._port.removeEventListener("mousemove", this);
this._port.removeEventListener("mouseup", this);
if (this._mode == "pan") { return; } /* no cleanup after panning */
if (this._ghost) {
var state = this._computeDragState();
this._finishDragDrop(state);
this._ghost.parentNode.removeChild(this._ghost);
this._ghost = null;
}
this._item = null;
}
MM.Mouse._buildGhost = function() {
var content = this._item.getDOM().content;
this._ghost = content.cloneNode(true);
this._ghost.classList.add("ghost");
this._pos[0] = content.offsetLeft;
this._pos[1] = content.offsetTop;
content.parentNode.appendChild(this._ghost);
}
MM.Mouse._moveGhost = function(dx, dy) {
this._pos[0] += dx;
this._pos[1] += dy;
this._ghost.style.left = this._pos[0] + "px";
this._ghost.style.top = this._pos[1] + "px";
var state = this._computeDragState();
}
MM.Mouse._finishDragDrop = function(state) {
this._visualizeDragState(null);
var target = state.item;
switch (state.result) {
case "append":
var action = new MM.Action.MoveItem(this._item, target);
break;
case "sibling":
var index = target.getParent().getChildren().indexOf(target);
var targetIndex = index + (state.direction == "right" || state.direction == "bottom" ? 1 : 0);
var action = new MM.Action.MoveItem(this._item, target.getParent(), targetIndex, target.getSide());
break;
default:
return;
break;
}
MM.App.action(action);
}
/**
* Compute a state object for a drag: current result (""/"append"/"sibling"), parent/sibling, direction
*/
MM.Mouse._computeDragState = function() {
var rect = this._ghost.getBoundingClientRect();
var closest = MM.App.map.getClosestItem(rect.left + rect.width/2, rect.top + rect.height/2);
var target = closest.item;
var state = {
result: "",
item: target,
direction: ""
}
var tmp = target;
while (!tmp.isRoot()) {
if (tmp == this._item) { return state; } /* drop on a child or self */
tmp = tmp.getParent();
}
var w1 = this._item.getDOM().content.offsetWidth;
var w2 = target.getDOM().content.offsetWidth;
var w = Math.max(w1, w2);
var h1 = this._item.getDOM().content.offsetHeight;
var h2 = target.getDOM().content.offsetHeight;
var h = Math.max(h1, h2);
if (target.isRoot()) { /* append here */
state.result = "append";
} else if (Math.abs(closest.dx) < w && Math.abs(closest.dy) < h) { /* append here */
state.result = "append";
} else {
state.result = "sibling";
var childDirection = target.getParent().getLayout().getChildDirection(target);
var diff = -1 * (childDirection == "top" || childDirection == "bottom" ? closest.dx : closest.dy);
if (childDirection == "left" || childDirection == "right") {
state.direction = (closest.dy < 0 ? "bottom" : "top");
} else {
state.direction = (closest.dx < 0 ? "right" : "left");
}
}
return state;
}
MM.Mouse._visualizeDragState = function(state) {
if (this._oldState && state && this._oldState.item == state.item && this._oldState.result == state.result) { return; } /* nothing changed */
if (this._oldDragState) { /* remove old vis */
var item = this._oldDragState.item;
var node = item.getDOM().content;
node.style.boxShadow = "";
}
this._oldDragState = state;
if (state) { /* show new vis */
var item = state.item;
var node = item.getDOM().content;
var x = 0;
var y = 0;
var offset = 5;
if (state.result == "sibling") {
if (state.direction == "left") { x = -1; }
if (state.direction == "right") { x = +1; }
if (state.direction == "top") { y = -1; }
if (state.direction == "bottom") { y = +1; }
}
var spread = (x || y ? -2 : 2);
node.style.boxShadow = (x*offset) + "px " + (y*offset) + "px 2px " + spread + "px #000";
}
}
/*
setInterval(function() {
console.log(document.activeElement);
}, 1000);
*/
/*
* Notes regarding app state/modes, activeElements, focusing etc.
* ==============================================================
*
* 1) There is always exactly one item selected. All executed commands
* operate on this item.
*
* 2) The app distinguishes three modes with respect to focus:
* 2a) One of the UI panes has focus (inputs, buttons, selects).
* Keyboard shortcuts are disabled.
* 2b) Current item is being edited. It is contentEditable and focused.
* Blurring ends the edit mode.
* 2c) ELSE the Clipboard is focused (its invisible textarea)
*
* In 2a, we try to lose focus as soon as possible
* (after clicking, after changing select's value), switching to 2c.
*
* 3) Editing mode (2b) can be ended by multiple ways:
* 3a) By calling current.stopEditing();
* this shall be followed by some resolution.
* 3b) By executing MM.Command.{Finish,Cancel};
* these call 3a internally.
* 3c) By blurring the item itself (by selecting another);
* this calls MM.Command.Finish (3b).
* 3b) By blurring the currentElement;
* this calls MM.Command.Finish (3b).
*
*/
MM.App = {
keyboard: null,
current: null,
editing: false,
history: [],
historyIndex: 0,
portSize: [0, 0],
map: null,
ui: null,
io: null,
help: null,
_port: null,
_throbber: null,
_drag: {
pos: [0, 0],
item: null,
ghost: null
},
_fontSize: 100,
action: function(action) {
if (this.historyIndex < this.history.length) { /* remove undoed actions */
this.history.splice(this.historyIndex, this.history.length-this.historyIndex);
}
this.history.push(action);
this.historyIndex++;
action.perform();
return this;
},
setMap: function(map) {
if (this.map) { this.map.hide(); }
this.history = [];
this.historyIndex = 0;
this.map = map;
this.map.show(this._port);
},
select: function(item) {
if (this.current && this.current != item) { this.current.deselect(); }
this.current = item;
this.current.select();
},
adjustFontSize: function(diff) {
this._fontSize = Math.max(30, this._fontSize + 10*diff);
this._port.style.fontSize = this._fontSize + "%";
this.map.update();
this.map.ensureItemVisibility(this.current);
},
handleMessage: function(message, publisher) {
switch (message) {
case "ui-change":
this._syncPort();
break;
case "item-change":
if (publisher.isRoot() && publisher.getMap() == this.map) {
document.title = this.map.getName() + " :: My Mind";
}
break;
}
},
handleEvent: function(e) {
switch (e.type) {
case "resize":
this._syncPort();
break;
case "keyup":
if (e.key === "Escape") {
MM.App.notes.close();
MM.App.help.close();
}
break;
case "message":
if (e.data && e.data.action) {
switch (e.data.action) {
case "setContent":
MM.App.notes.update(e.data.value);
break;
case "closeEditor":
MM.App.notes.close();
break;
}
}
break;
case "beforeunload":
e.preventDefault();
return "";
break;
}
},
setThrobber: function(visible) {
this._throbber.classList[visible ? "add" : "remove"]("visible");
},
init: function() {
this._port = document.querySelector("#port");
this._throbber = document.querySelector("#throbber");
this.ui = new MM.UI();
this.io = new MM.UI.IO();
this.help = new MM.UI.Help();
this.notes = new MM.UI.Notes();
MM.Tip.init();
MM.Keyboard.init();
MM.Menu.init(this._port);
MM.Mouse.init(this._port);
MM.Clipboard.init();
window.addEventListener("resize", this);
window.addEventListener("beforeunload", this);
window.addEventListener("keyup", this);
window.addEventListener("message", this, false);
MM.subscribe("ui-change", this);
MM.subscribe("item-change", this);
this._syncPort();
this.setMap(new MM.Map());
},
_syncPort: function() {
this.portSize = [window.innerWidth - this.ui.getWidth(), window.innerHeight];
this._port.style.width = this.portSize[0] + "px";
this._port.style.height = this.portSize[1] + "px";
this._throbber.style.right = (20 + this.ui.getWidth())+ "px";
if (this.map) { this.map.ensureItemVisibility(this.current); }
}
}