5413 lines
136 KiB
JavaScript
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…"},
|
|
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",
|
|
"-": "−"
|
|
};
|
|
|
|
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); }
|
|
}
|
|
}
|