From bc110e31b7baef8b2234cd9871211f5521818c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C5=BD=C3=A1ra?= Date: Sat, 16 Oct 2021 20:05:15 +0200 Subject: [PATCH 01/42] reorganization + build pipeline --- .gitignore | 2 + Makefile | 83 +- my-mind.js | 10184 ++++++++---------- package.json | 6 + src/action.js | 2 +- src/{ => backend}/backend.file.js | 0 src/{ => backend}/backend.firebase.js | 0 src/{ => backend}/backend.gdrive.js | 0 src/{ => backend}/backend.image.js | 0 src/{ => backend}/backend.js | 0 src/{ => backend}/backend.local.js | 0 src/{ => backend}/backend.webdav.js | 0 src/{ => command}/command.edit.js | 0 src/{ => command}/command.js | 0 src/{ => command}/command.select.js | 0 src/{ => format}/format.freemind.js | 0 src/{ => format}/format.js | 0 src/{ => format}/format.json.js | 0 src/{ => format}/format.mma.js | 0 src/{ => format}/format.mup.js | 0 src/{ => format}/format.plaintext.js | 0 src/{ => layout}/layout.graph.js | 0 src/{ => layout}/layout.js | 0 src/{ => layout}/layout.map.js | 0 src/{ => layout}/layout.tree.js | 0 src/mm.js | 2 +- src/{app.js => my-mind.js} | 85 +- src/{ => shape}/shape.box.js | 0 src/{ => shape}/shape.ellipse.js | 0 src/{ => shape}/shape.js | 0 src/{ => shape}/shape.underline.js | 0 src/{ => ui/backend}/ui.backend.file.js | 0 src/{ => ui/backend}/ui.backend.firebase.js | 0 src/{ => ui/backend}/ui.backend.gdrive.js | 0 src/{ => ui/backend}/ui.backend.image.js | 0 src/{ => ui/backend}/ui.backend.js | 0 src/{ => ui/backend}/ui.backend.local.js | 0 src/{ => ui/backend}/ui.backend.webdav.js | 0 src/{ => ui}/ui.color.js | 0 src/{ => ui}/ui.help.js | 0 src/{ => ui}/ui.icon.js | 0 src/{ => ui}/ui.io.js | 0 src/{ => ui}/ui.js | 0 src/{ => ui}/ui.layout.js | 0 src/{ => ui}/ui.notes.js | 0 src/{ => ui}/ui.shape.js | 0 src/{ => ui}/ui.status.js | 0 src/{ => ui}/ui.value.js | 0 tsconfig.json | 10 + 49 files changed, 4892 insertions(+), 5482 deletions(-) create mode 100644 .gitignore create mode 100644 package.json rename src/{ => backend}/backend.file.js (100%) rename src/{ => backend}/backend.firebase.js (100%) rename src/{ => backend}/backend.gdrive.js (100%) rename src/{ => backend}/backend.image.js (100%) rename src/{ => backend}/backend.js (100%) rename src/{ => backend}/backend.local.js (100%) rename src/{ => backend}/backend.webdav.js (100%) rename src/{ => command}/command.edit.js (100%) rename src/{ => command}/command.js (100%) rename src/{ => command}/command.select.js (100%) rename src/{ => format}/format.freemind.js (100%) rename src/{ => format}/format.js (100%) rename src/{ => format}/format.json.js (100%) rename src/{ => format}/format.mma.js (100%) rename src/{ => format}/format.mup.js (100%) rename src/{ => format}/format.plaintext.js (100%) rename src/{ => layout}/layout.graph.js (100%) rename src/{ => layout}/layout.js (100%) rename src/{ => layout}/layout.map.js (100%) rename src/{ => layout}/layout.tree.js (100%) rename src/{app.js => my-mind.js} (71%) rename src/{ => shape}/shape.box.js (100%) rename src/{ => shape}/shape.ellipse.js (100%) rename src/{ => shape}/shape.js (100%) rename src/{ => shape}/shape.underline.js (100%) rename src/{ => ui/backend}/ui.backend.file.js (100%) rename src/{ => ui/backend}/ui.backend.firebase.js (100%) rename src/{ => ui/backend}/ui.backend.gdrive.js (100%) rename src/{ => ui/backend}/ui.backend.image.js (100%) rename src/{ => ui/backend}/ui.backend.js (100%) rename src/{ => ui/backend}/ui.backend.local.js (100%) rename src/{ => ui/backend}/ui.backend.webdav.js (100%) rename src/{ => ui}/ui.color.js (100%) rename src/{ => ui}/ui.help.js (100%) rename src/{ => ui}/ui.icon.js (100%) rename src/{ => ui}/ui.io.js (100%) rename src/{ => ui}/ui.js (100%) rename src/{ => ui}/ui.layout.js (100%) rename src/{ => ui}/ui.notes.js (100%) rename src/{ => ui}/ui.shape.js (100%) rename src/{ => ui}/ui.status.js (100%) rename src/{ => ui}/ui.value.js (100%) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d9ea5e3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.js diff --git a/Makefile b/Makefile index 4e5e27ef..df5a8bc1 100644 --- a/Makefile +++ b/Makefile @@ -1,70 +1,25 @@ -SOURCES = src/mm.js \ - src/promise.js \ - src/promise-addons.js \ - src/repo.js \ - src/item.js \ - src/map.js \ - src/keyboard.js \ - src/tip.js \ - src/action.js \ - src/clipboard.js \ - src/menu.js \ - src/command.js \ - src/command.edit.js \ - src/command.select.js \ - src/layout.js \ - src/layout.graph.js \ - src/layout.tree.js \ - src/layout.map.js \ - src/shape.js \ - src/shape.underline.js \ - src/shape.box.js \ - src/shape.ellipse.js \ - src/format.js \ - src/format.json.js \ - src/format.freemind.js \ - src/format.mma.js \ - src/format.mup.js \ - src/format.plaintext.js \ - src/backend.js \ - src/backend.local.js \ - src/backend.webdav.js \ - src/backend.image.js \ - src/backend.file.js \ - src/backend.firebase.js \ - src/backend.gdrive.js \ - src/ui.js \ - src/ui.layout.js \ - src/ui.shape.js \ - src/ui.value.js \ - src/ui.status.js \ - src/ui.color.js \ - src/ui.icon.js \ - src/ui.help.js \ - src/ui.notes.js \ - src/ui.io.js \ - src/ui.backend.js \ - src/ui.backend.file.js \ - src/ui.backend.webdav.js \ - src/ui.backend.image.js \ - src/ui.backend.local.js \ - src/ui.backend.firebase.js \ - src/ui.backend.gdrive.js \ - src/mouse.js \ - src/app.js +MAKEOPTS = "-r" -.PHONY: all push clean +BIN := $(shell npm bin) +TSC := $(BIN)/tsc +LESSC := $(BIN)/lessc +ESBUILD := $(BIN)/esbuild +GCC := $(BIN)/google-closure-compiler -all: my-mind.js +JS := .js +FLAG := $(JS)/.tsflag +APP := my-mind.js -my-mind.js: $(SOURCES) - @echo "/* My Mind web app: all source files combined. */" > $@ - @cat $^ >> $@ +all: $(APP) -push: - @hg bookmark -f master - @hg push ; true - @hg push github ; true +$(APP): $(FLAG) + $(ESBUILD) --bundle $(JS)/$(APP) > $@ + +$(FLAG): $(shell find src -type f) + $(TSC) + touch $@ clean: - @rm my-mind.js + rm -rf $(JS) $(APP) + +.PHONY: all clean diff --git a/my-mind.js b/my-mind.js index fbfe917c..60bc4e7f 100644 --- a/my-mind.js +++ b/my-mind.js @@ -1,5412 +1,4794 @@ -/* 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\(\)\[\]'"])?($|\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); +(() => { + // .js/mm.js + 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))); + }; + }; + } + window.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); + } + }; + + // .js/promise.js + var Promise2 = function(executor) { + this._state = 0; + this._value = null; + this._cb = { + fulfilled: [], + rejected: [] + }; + this._thenPromises = []; + executor && executor(this.fulfill.bind(this), this.reject.bind(this)); + }; + Promise2.resolve = function(value) { + return new Promise2().fulfill(value); + }; + Promise2.reject = function(value) { + return new Promise2().reject(value); + }; + Promise2.prototype.then = function(onFulfilled, onRejected) { + this._cb.fulfilled.push(onFulfilled); + this._cb.rejected.push(onRejected); + var thenPromise = new Promise2(); + this._thenPromises.push(thenPromise); + if (this._state > 0) { + setTimeout(this._processQueue.bind(this), 0); + } + return thenPromise; + }; + Promise2.prototype.fulfill = function(value) { + if (this._state != 0) { + return this; + } + this._state = 1; + this._value = value; + this._processQueue(); + return this; + }; + Promise2.prototype.reject = function(value) { + if (this._state != 0) { + return this; + } + this._state = 2; + this._value = value; + this._processQueue(); + return this; + }; + Promise2.prototype.chain = function(promise) { + return this.then(promise.fulfill.bind(promise), promise.reject.bind(promise)); + }; + Promise2.prototype["catch"] = function(onRejected) { + return this.then(null, onRejected); + }; + Promise2.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); + } + }; + Promise2.prototype._executeCallback = function(cb) { + var thenPromise = this._thenPromises.shift(); + if (typeof cb != "function") { + if (this._state == 1) { + thenPromise.fulfill(this._value); + } else { + thenPromise.reject(this._value); + } + return; + } + try { + var returned = cb(this._value); + if (returned && typeof returned.then == "function") { + var fulfillThenPromise = function(value) { + thenPromise.fulfill(value); + }; + var rejectThenPromise = function(value) { + thenPromise.reject(value); + }; + returned.then(fulfillThenPromise, rejectThenPromise); + } else { + thenPromise.fulfill(returned); + } + } catch (e) { + thenPromise.reject(e); + } + }; + + // .js/promise-addons.js + 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.setTimeout = function(ms) { + var promise = new this(); + setTimeout(function() { + promise.fulfill(); + }, ms); + return promise; + }; + 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.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.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; + }; + + // .js/repo.js + MM.Repo = { + id: "", + 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; + } + }; + + // .js/item.js + 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; + 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); + this._dom.node.appendChild(this._dom.canvas); + this._dom.node.appendChild(this._dom.content); + this._dom.content.appendChild(this._dom.notes); + this._dom.toggle.addEventListener("click", this); + }; + MM.Item.COLOR = "#999"; + 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; + }; + 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) { + this.insertChild(MM.Item.fromJSON(child)); + } else { + var myChild = this._children[index]; + if (myChild.getId() == child.id) { + myChild.mergeWith(child); } else { - window.editor.setContent(''); + this.removeChild(this._children[index]); + this.insertChild(MM.Item.fromJSON(child), index); } - } - 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); + 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(); + MM.publish("item-select", this); + }; + MM.Item.prototype.deselect = function() { + 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) { + 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._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) { + var newChild = false; + if (!child) { + child = new MM.Item(); + newChild = true; + } else if (child.getParent() && child.getParent().removeChild) { + 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(); + 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(); + 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(); + } + break; + case "blur": + 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"); + if (icon) { + this._dom.icon.classList.add("fa"); + this._dom.icon.classList.add(icon); + this._computed.icon = true; } else { - this._dom.notes.classList.remove("notes-indicator-visible"); + this._computed.icon = null; + this._dom.icon.style.display = "none"; } -} - -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