From d8266e29be2d9914d4a61e8cd17faaf522d70f3f Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Thu, 25 Sep 2014 21:42:51 +0200 Subject: [PATCH 1/7] prepare for paste --- my-mind.js | 90 ++++++++++++++++++++++++++++++++++++++--------- src/action.js | 15 ++++++++ src/app.js | 43 ++++++++++++++++------ src/item.js | 17 +++++++-- src/keyboard.js | 1 + src/mouse.js | 3 +- src/ui.backend.js | 3 +- src/ui.io.js | 1 + src/ui.js | 7 ++-- 9 files changed, 146 insertions(+), 34 deletions(-) diff --git a/my-mind.js b/my-mind.js index 5ef1f6d..a862a86 100644 --- a/my-mind.js +++ b/my-mind.js @@ -462,6 +462,19 @@ MM.Item.prototype.clone = function() { return this.constructor.fromJSON(data); } +MM.Item.prototype.focus = function() { + /* going to mode 2c */ + this._dom.node.classList.add("current"); + this.getMap().ensureItemVisibility(this); + MM.publish("item-focus", this); +} + +MM.Item.prototype.blur = function() { + /* we were in 2b; finish that via 4b */ + 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; } @@ -685,7 +698,7 @@ MM.Item.prototype.removeChild = function(child) { MM.Item.prototype.startEditing = function() { this._oldText = this.getText(); this._dom.text.contentEditable = true; - this._dom.text.focus(); + this._dom.text.focus(); /* switch to 2b */ document.execCommand("styleWithCSS", null, false); this._dom.text.addEventListener("input", this); @@ -720,7 +733,7 @@ MM.Item.prototype.handleEvent = function(e) { if (e.keyCode == 9) { e.preventDefault(); } /* TAB has a special meaning in this app, do not use it to change focus */ break; - case "blur": + case "blur": /* 4d */ MM.Command.Finish.execute(); break; @@ -1133,6 +1146,7 @@ MM.Keyboard.init = function() { } 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; } @@ -1194,6 +1208,21 @@ 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; @@ -3502,15 +3531,15 @@ MM.UI = function() { this._value = new MM.UI.Value(); this._status = new MM.UI.Status(); + MM.subscribe("item-focus", this); MM.subscribe("item-change", this); - MM.subscribe("item-select", this); this.toggle(); } MM.UI.prototype.handleMessage = function(message, publisher) { switch (message) { - case "item-select": + case "item-focus": this._update(); break; @@ -3521,7 +3550,8 @@ MM.UI.prototype.handleMessage = function(message, publisher) { } MM.UI.prototype.handleEvent = function(e) { - /* blur to return focus back to app commands */ + /* blur to return focus back to app commands (mode 2c) */ + /* FIXME 1) re-select current node, 2) do it also after any select changes */ if (e.target.nodeName.toLowerCase() != "select") { e.target.blur(); } if (e.target == this._toggle) { @@ -3876,6 +3906,7 @@ MM.UI.IO.prototype.show = function(mode) { MM.UI.IO.prototype.hide = function() { if (!this._node.classList.contains("visible")) { return; } this._node.classList.remove("visible"); + /* FIXME instead of blurring, just re-select current node => switch to 2c */ document.activeElement && document.activeElement.blur(); window.removeEventListener("keydown", this); } @@ -3990,7 +4021,8 @@ MM.UI.Backend.show = function(mode) { 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(); } @@ -4596,7 +4628,8 @@ MM.Mouse.handleEvent = function(e) { } if (item == MM.App.current && MM.App.editing) { return; } - document.activeElement && document.activeElement.blur(); + /* if we are editing another item, end that by blurring it */ + document.activeElement && document.activeElement.blur(); this._startDrag(e, item); break; @@ -4805,6 +4838,37 @@ MM.Mouse._visualizeDragState = function(state) { node.style.boxShadow = (x*offset) + "px " + (y*offset) + "px 2px " + spread + "px #000"; } } +/* + * 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 focus belongs the the currently selected item. + * + * In 2a, we try to return focus (re-select, 2c) as soon as possible + * (after clicking, after changing select's value). + * + * 3) After selecting an item, we switch to 2c. In 2c, the current item + * focuses its invisible "paste" node to listen for ctrl+v data. + * + * 4) Editing mode (2b) can be ended by multiple ways: + * 4a) By calling current.stopEditing(); + * this shall be followed by some resolution. + * 4b) By executing MM.Command.{Finish,Cancel}; + * these call 4a internally. + * 4c) By blurring the item itself (by selecting another); + * this calls MM.Command.Finish (4b). + * 4b) By blurring the currentElement; + * this calls MM.Command.Finish (4b). + * + */ MM.App = { keyboard: null, current: null, @@ -4848,17 +4912,9 @@ MM.App = { }, select: function(item) { - if (item == this.current) { return; } - - if (this.editing) { MM.Command.Finish.execute(); } - - if (this.current) { - this.current.getDOM().node.classList.remove("current"); - } + if (this.current && this.current != item) { this.current.blur(); } this.current = item; - this.current.getDOM().node.classList.add("current"); - this.map.ensureItemVisibility(item); - MM.publish("item-select", item); + this.current.focus(); }, adjustFontSize: function(diff) { diff --git a/src/action.js b/src/action.js index bb75433..9880886 100644 --- a/src/action.js +++ b/src/action.js @@ -2,6 +2,21 @@ 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; diff --git a/src/app.js b/src/app.js index 5153e9e..71acaf1 100644 --- a/src/app.js +++ b/src/app.js @@ -1,3 +1,34 @@ +/* + * 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 focus belongs the the currently selected item. + * + * In 2a, we try to return focus (re-select, 2c) as soon as possible + * (after clicking, after changing select's value). + * + * 3) After selecting an item, we switch to 2c. In 2c, the current item + * focuses its invisible "paste" node to listen for ctrl+v data. + * + * 4) Editing mode (2b) can be ended by multiple ways: + * 4a) By calling current.stopEditing(); + * this shall be followed by some resolution. + * 4b) By executing MM.Command.{Finish,Cancel}; + * these call 4a internally. + * 4c) By blurring the item itself (by selecting another); + * this calls MM.Command.Finish (4b). + * 4b) By blurring the currentElement; + * this calls MM.Command.Finish (4b). + * + */ MM.App = { keyboard: null, current: null, @@ -41,17 +72,9 @@ MM.App = { }, select: function(item) { - if (item == this.current) { return; } - - if (this.editing) { MM.Command.Finish.execute(); } - - if (this.current) { - this.current.getDOM().node.classList.remove("current"); - } + if (this.current && this.current != item) { this.current.blur(); } this.current = item; - this.current.getDOM().node.classList.add("current"); - this.map.ensureItemVisibility(item); - MM.publish("item-select", item); + this.current.focus(); }, adjustFontSize: function(diff) { diff --git a/src/item.js b/src/item.js index c76e8a5..2222576 100644 --- a/src/item.js +++ b/src/item.js @@ -173,6 +173,19 @@ MM.Item.prototype.clone = function() { return this.constructor.fromJSON(data); } +MM.Item.prototype.focus = function() { + /* going to mode 2c */ + this._dom.node.classList.add("current"); + this.getMap().ensureItemVisibility(this); + MM.publish("item-focus", this); +} + +MM.Item.prototype.blur = function() { + /* we were in 2b; finish that via 4b */ + 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; } @@ -396,7 +409,7 @@ MM.Item.prototype.removeChild = function(child) { MM.Item.prototype.startEditing = function() { this._oldText = this.getText(); this._dom.text.contentEditable = true; - this._dom.text.focus(); + this._dom.text.focus(); /* switch to 2b */ document.execCommand("styleWithCSS", null, false); this._dom.text.addEventListener("input", this); @@ -431,7 +444,7 @@ MM.Item.prototype.handleEvent = function(e) { if (e.keyCode == 9) { e.preventDefault(); } /* TAB has a special meaning in this app, do not use it to change focus */ break; - case "blur": + case "blur": /* 4d */ MM.Command.Finish.execute(); break; diff --git a/src/keyboard.js b/src/keyboard.js index d668c1b..9495e1c 100644 --- a/src/keyboard.js +++ b/src/keyboard.js @@ -5,6 +5,7 @@ MM.Keyboard.init = function() { } 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; } diff --git a/src/mouse.js b/src/mouse.js index d580a17..a1ee65e 100644 --- a/src/mouse.js +++ b/src/mouse.js @@ -58,7 +58,8 @@ MM.Mouse.handleEvent = function(e) { } if (item == MM.App.current && MM.App.editing) { return; } - document.activeElement && document.activeElement.blur(); + /* if we are editing another item, end that by blurring it */ + document.activeElement && document.activeElement.blur(); this._startDrag(e, item); break; diff --git a/src/ui.backend.js b/src/ui.backend.js index feb709e..e1e5465 100644 --- a/src/ui.backend.js +++ b/src/ui.backend.js @@ -55,7 +55,8 @@ MM.UI.Backend.show = function(mode) { 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(); } diff --git a/src/ui.io.js b/src/ui.io.js index 185440a..982ce85 100644 --- a/src/ui.io.js +++ b/src/ui.io.js @@ -84,6 +84,7 @@ MM.UI.IO.prototype.show = function(mode) { MM.UI.IO.prototype.hide = function() { if (!this._node.classList.contains("visible")) { return; } this._node.classList.remove("visible"); + /* FIXME instead of blurring, just re-select current node => switch to 2c */ document.activeElement && document.activeElement.blur(); window.removeEventListener("keydown", this); } diff --git a/src/ui.js b/src/ui.js index b7ca2a0..fa14d60 100644 --- a/src/ui.js +++ b/src/ui.js @@ -10,15 +10,15 @@ MM.UI = function() { this._value = new MM.UI.Value(); this._status = new MM.UI.Status(); + MM.subscribe("item-focus", this); MM.subscribe("item-change", this); - MM.subscribe("item-select", this); this.toggle(); } MM.UI.prototype.handleMessage = function(message, publisher) { switch (message) { - case "item-select": + case "item-focus": this._update(); break; @@ -29,7 +29,8 @@ MM.UI.prototype.handleMessage = function(message, publisher) { } MM.UI.prototype.handleEvent = function(e) { - /* blur to return focus back to app commands */ + /* blur to return focus back to app commands (mode 2c) */ + /* FIXME 1) re-select current node, 2) do it also after any select changes */ if (e.target.nodeName.toLowerCase() != "select") { e.target.blur(); } if (e.target == this._toggle) { From 9c1ebeef2f6f7b244a55e6e13713633ce812297c Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Fri, 26 Sep 2014 14:23:47 +0200 Subject: [PATCH 2/7] clipboard preparations --- my-mind.js | 141 +++++++++++++++++++++++++++++++---------------- src/app.js | 38 +++++++------ src/clipboard.js | 36 +++++++++++- src/command.js | 4 ++ src/item.js | 12 ++-- src/keyboard.js | 2 +- src/ui.io.js | 3 +- src/ui.js | 46 +++++++++------- 8 files changed, 188 insertions(+), 94 deletions(-) diff --git a/my-mind.js b/my-mind.js index a862a86..a9c1ecb 100644 --- a/my-mind.js +++ b/my-mind.js @@ -462,15 +462,15 @@ MM.Item.prototype.clone = function() { return this.constructor.fromJSON(data); } -MM.Item.prototype.focus = function() { - /* going to mode 2c */ +MM.Item.prototype.select = function() { this._dom.node.classList.add("current"); this.getMap().ensureItemVisibility(this); - MM.publish("item-focus", this); + MM.Clipboard.focus(); /* going to mode 2c */ + MM.publish("item-select", this); } -MM.Item.prototype.blur = function() { - /* we were in 2b; finish that via 4b */ +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"); } @@ -733,7 +733,7 @@ MM.Item.prototype.handleEvent = function(e) { if (e.keyCode == 9) { e.preventDefault(); } /* TAB has a special meaning in this app, do not use it to change focus */ break; - case "blur": /* 4d */ + case "blur": /* 3d */ MM.Command.Finish.execute(); break; @@ -1160,7 +1160,7 @@ MM.Keyboard.handleEvent = function(e) { var keys = command.keys; for (var j=0;j")) + + return lines.join("\n") + (depth ? "" : "\n"); +} MM.Menu = { _dom: {}, _port: null, @@ -1528,6 +1560,7 @@ MM.Menu = { MM.Command = Object.create(MM.Repo, { keys: {value: []}, editMode: {value: false}, + prevent: {value: true}, /* prevent default keyboard action? */ label: {value: ""} }); @@ -1771,6 +1804,7 @@ MM.Command.Pan.handleEvent = function(e) { MM.Command.Copy = Object.create(MM.Command, { label: {value: "Copy"}, + prevent: {value: false}, keys: {value: [{keyCode: "C".charCodeAt(0), ctrlKey:true}]} }); MM.Command.Copy.execute = function() { @@ -1779,6 +1813,7 @@ MM.Command.Copy.execute = function() { MM.Command.Cut = Object.create(MM.Command, { label: {value: "Cut"}, + prevent: {value: false}, keys: {value: [{keyCode: "X".charCodeAt(0), ctrlKey:true}]} }); MM.Command.Cut.execute = function() { @@ -1787,6 +1822,7 @@ MM.Command.Cut.execute = function() { MM.Command.Paste = Object.create(MM.Command, { label: {value: "Paste"}, + prevent: {value: false}, keys: {value: [{keyCode: "V".charCodeAt(0), ctrlKey:true}]} }); MM.Command.Paste.execute = function() { @@ -3521,7 +3557,6 @@ MM.Backend.GDrive._auth = function(forceUI) { } MM.UI = function() { this._node = document.querySelector(".ui"); - this._node.addEventListener("click", this); this._toggle = this._node.querySelector("#toggle"); @@ -3531,15 +3566,18 @@ MM.UI = function() { this._value = new MM.UI.Value(); this._status = new MM.UI.Status(); - MM.subscribe("item-focus", this); + 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-focus": + case "item-select": this._update(); break; @@ -3550,23 +3588,29 @@ MM.UI.prototype.handleMessage = function(message, publisher) { } MM.UI.prototype.handleEvent = function(e) { - /* blur to return focus back to app commands (mode 2c) */ - /* FIXME 1) re-select current node, 2) do it also after any select changes */ - if (e.target.nodeName.toLowerCase() != "select") { e.target.blur(); } + 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; + 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; } } @@ -3906,8 +3950,7 @@ MM.UI.IO.prototype.show = function(mode) { MM.UI.IO.prototype.hide = function() { if (!this._node.classList.contains("visible")) { return; } this._node.classList.remove("visible"); - /* FIXME instead of blurring, just re-select current node => switch to 2c */ - document.activeElement && document.activeElement.blur(); + MM.Clipboard.focus(); window.removeEventListener("keydown", this); } @@ -4838,6 +4881,12 @@ MM.Mouse._visualizeDragState = function(state) { 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. * ============================================================== @@ -4850,23 +4899,20 @@ MM.Mouse._visualizeDragState = function(state) { * Keyboard shortcuts are disabled. * 2b) Current item is being edited. It is contentEditable and focused. * Blurring ends the edit mode. - * 2c) ELSE the focus belongs the the currently selected item. + * 2c) ELSE the Clipboard is focused (its invisible textarea) * - * In 2a, we try to return focus (re-select, 2c) as soon as possible - * (after clicking, after changing select's value). + * In 2a, we try to lose focus as soon as possible + * (after clicking, after changing select's value), switching to 2c. * - * 3) After selecting an item, we switch to 2c. In 2c, the current item - * focuses its invisible "paste" node to listen for ctrl+v data. - * - * 4) Editing mode (2b) can be ended by multiple ways: - * 4a) By calling current.stopEditing(); + * 3) Editing mode (2b) can be ended by multiple ways: + * 3a) By calling current.stopEditing(); * this shall be followed by some resolution. - * 4b) By executing MM.Command.{Finish,Cancel}; - * these call 4a internally. - * 4c) By blurring the item itself (by selecting another); - * this calls MM.Command.Finish (4b). - * 4b) By blurring the currentElement; - * this calls MM.Command.Finish (4b). + * 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 = { @@ -4912,11 +4958,11 @@ MM.App = { }, select: function(item) { - if (this.current && this.current != item) { this.current.blur(); } + if (this.current && this.current != item) { this.current.deselect(); } this.current = item; - this.current.focus(); + this.current.select(); }, - + adjustFontSize: function(diff) { this._fontSize = Math.max(30, this._fontSize + 10*diff); this._port.style.fontSize = this._fontSize + "%"; @@ -4966,6 +5012,7 @@ MM.App = { MM.Keyboard.init(); MM.Menu.init(this._port); MM.Mouse.init(this._port); + MM.Clipboard.init(); window.addEventListener("resize", this); window.addEventListener("beforeunload", this); diff --git a/src/app.js b/src/app.js index 71acaf1..3e3b313 100644 --- a/src/app.js +++ b/src/app.js @@ -1,3 +1,9 @@ + +setInterval(function() { + console.log(document.activeElement); +}, 1000); + + /* * Notes regarding app state/modes, activeElements, focusing etc. * ============================================================== @@ -10,23 +16,20 @@ * Keyboard shortcuts are disabled. * 2b) Current item is being edited. It is contentEditable and focused. * Blurring ends the edit mode. - * 2c) ELSE the focus belongs the the currently selected item. + * 2c) ELSE the Clipboard is focused (its invisible textarea) * - * In 2a, we try to return focus (re-select, 2c) as soon as possible - * (after clicking, after changing select's value). + * In 2a, we try to lose focus as soon as possible + * (after clicking, after changing select's value), switching to 2c. * - * 3) After selecting an item, we switch to 2c. In 2c, the current item - * focuses its invisible "paste" node to listen for ctrl+v data. - * - * 4) Editing mode (2b) can be ended by multiple ways: - * 4a) By calling current.stopEditing(); + * 3) Editing mode (2b) can be ended by multiple ways: + * 3a) By calling current.stopEditing(); * this shall be followed by some resolution. - * 4b) By executing MM.Command.{Finish,Cancel}; - * these call 4a internally. - * 4c) By blurring the item itself (by selecting another); - * this calls MM.Command.Finish (4b). - * 4b) By blurring the currentElement; - * this calls MM.Command.Finish (4b). + * 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 = { @@ -72,11 +75,11 @@ MM.App = { }, select: function(item) { - if (this.current && this.current != item) { this.current.blur(); } + if (this.current && this.current != item) { this.current.deselect(); } this.current = item; - this.current.focus(); + this.current.select(); }, - + adjustFontSize: function(diff) { this._fontSize = Math.max(30, this._fontSize + 10*diff); this._port.style.fontSize = this._fontSize + "%"; @@ -126,6 +129,7 @@ MM.App = { MM.Keyboard.init(); MM.Menu.init(this._port); MM.Mouse.init(this._port); + MM.Clipboard.init(); window.addEventListener("resize", this); window.addEventListener("beforeunload", this); diff --git a/src/clipboard.js b/src/clipboard.js index 99cf625..b98bf23 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -1,13 +1,32 @@ MM.Clipboard = { _data: null, - _mode: "" + _mode: "", + _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(); +} + MM.Clipboard.copy = function(sourceItem) { this._endCut(); - this._data = sourceItem.clone(); this._mode = "copy"; + + var plaintext = this._itemToPlaintext(sourceItem); + this._node.value = plaintext; + this._node.selectionStart = 0; + this._node.selectionEnd = this._node.value.length; + setTimeout(function() { this._node.value = ""; }.bind(this), 0); } MM.Clipboard.paste = function(targetItem) { @@ -59,3 +78,16 @@ MM.Clipboard._endCut = function() { this._data = null; this._mode = ""; } + +MM.Clipboard._itemToPlaintext = function(item, depth) { + depth = depth || 0; + + var lines = item.getChildren().map(function(child) { + return this._itemToPlaintext(child, depth+1); + }, this); + + var prefix = new Array(depth+1).join("\t"); + lines.unshift(prefix + item.getText().replace(/\n/g, "
")) + + return lines.join("\n") + (depth ? "" : "\n"); +} diff --git a/src/command.js b/src/command.js index 663a146..6cd8545 100644 --- a/src/command.js +++ b/src/command.js @@ -1,6 +1,7 @@ MM.Command = Object.create(MM.Repo, { keys: {value: []}, editMode: {value: false}, + prevent: {value: true}, /* prevent default keyboard action? */ label: {value: ""} }); @@ -244,6 +245,7 @@ MM.Command.Pan.handleEvent = function(e) { MM.Command.Copy = Object.create(MM.Command, { label: {value: "Copy"}, + prevent: {value: false}, keys: {value: [{keyCode: "C".charCodeAt(0), ctrlKey:true}]} }); MM.Command.Copy.execute = function() { @@ -252,6 +254,7 @@ MM.Command.Copy.execute = function() { MM.Command.Cut = Object.create(MM.Command, { label: {value: "Cut"}, + prevent: {value: false}, keys: {value: [{keyCode: "X".charCodeAt(0), ctrlKey:true}]} }); MM.Command.Cut.execute = function() { @@ -260,6 +263,7 @@ MM.Command.Cut.execute = function() { MM.Command.Paste = Object.create(MM.Command, { label: {value: "Paste"}, + prevent: {value: false}, keys: {value: [{keyCode: "V".charCodeAt(0), ctrlKey:true}]} }); MM.Command.Paste.execute = function() { diff --git a/src/item.js b/src/item.js index 2222576..938fac8 100644 --- a/src/item.js +++ b/src/item.js @@ -173,15 +173,15 @@ MM.Item.prototype.clone = function() { return this.constructor.fromJSON(data); } -MM.Item.prototype.focus = function() { - /* going to mode 2c */ +MM.Item.prototype.select = function() { this._dom.node.classList.add("current"); this.getMap().ensureItemVisibility(this); - MM.publish("item-focus", this); + MM.Clipboard.focus(); /* going to mode 2c */ + MM.publish("item-select", this); } -MM.Item.prototype.blur = function() { - /* we were in 2b; finish that via 4b */ +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"); } @@ -444,7 +444,7 @@ MM.Item.prototype.handleEvent = function(e) { if (e.keyCode == 9) { e.preventDefault(); } /* TAB has a special meaning in this app, do not use it to change focus */ break; - case "blur": /* 4d */ + case "blur": /* 3d */ MM.Command.Finish.execute(); break; diff --git a/src/keyboard.js b/src/keyboard.js index 9495e1c..475c15a 100644 --- a/src/keyboard.js +++ b/src/keyboard.js @@ -19,7 +19,7 @@ MM.Keyboard.handleEvent = function(e) { var keys = command.keys; for (var j=0;j switch to 2c */ - document.activeElement && document.activeElement.blur(); + MM.Clipboard.focus(); window.removeEventListener("keydown", this); } diff --git a/src/ui.js b/src/ui.js index fa14d60..c649d7c 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,6 +1,5 @@ MM.UI = function() { this._node = document.querySelector(".ui"); - this._node.addEventListener("click", this); this._toggle = this._node.querySelector("#toggle"); @@ -10,15 +9,18 @@ MM.UI = function() { this._value = new MM.UI.Value(); this._status = new MM.UI.Status(); - MM.subscribe("item-focus", this); + 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-focus": + case "item-select": this._update(); break; @@ -29,23 +31,29 @@ MM.UI.prototype.handleMessage = function(message, publisher) { } MM.UI.prototype.handleEvent = function(e) { - /* blur to return focus back to app commands (mode 2c) */ - /* FIXME 1) re-select current node, 2) do it also after any select changes */ - if (e.target.nodeName.toLowerCase() != "select") { e.target.blur(); } + 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; + 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; } } From a36eae944b01872bc50e774da4ee132ad8d3e140 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Fri, 26 Sep 2014 16:11:34 +0200 Subject: [PATCH 3/7] pasting external data --- my-mind.js | 130 +++++++++++++++++++++++++++++++++++++++-------- src/app.js | 4 +- src/clipboard.js | 126 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 218 insertions(+), 42 deletions(-) diff --git a/my-mind.js b/my-mind.js index a9c1ecb..bad1fb0 100644 --- a/my-mind.js +++ b/my-mind.js @@ -1408,8 +1408,9 @@ MM.Action.SetStatus.prototype.undo = function() { this._item.getMap().update(); } MM.Clipboard = { - _data: null, + _item: null, _mode: "", + _delay: 0, _node: document.createElement("textarea") }; @@ -1428,40 +1429,72 @@ MM.Clipboard.focus = function() { MM.Clipboard.copy = function(sourceItem) { this._endCut(); - this._data = sourceItem.clone(); + this._item = sourceItem.clone(); this._mode = "copy"; - var plaintext = this._itemToPlaintext(sourceItem); - this._node.value = plaintext; - this._node.selectionStart = 0; - this._node.selectionEnd = this._node.value.length; - setTimeout(function() { this._node.value = ""; }.bind(this), 0); + this._expose(); } MM.Clipboard.paste = function(targetItem) { - if (!this._data) { return; } + setTimeout(function() { + var pasted = this._node.value; + this._node.value = ""; + if (!pasted) { return; } /* nothing */ + + if (this._item && pasted == this._itemToPlaintext(this._item)) { /* 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 (this._data == targetItem || this._data.getParent() == targetItem) { /* abort by pasting on the same node or the parent */ + 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 == this._data) { return; } /* moving to a child => forbidden */ + if (item == sourceItem) { return; } /* moving to a child => forbidden */ item = item.getParent(); } - var action = new MM.Action.MoveItem(this._data, targetItem); + var action = new MM.Action.MoveItem(sourceItem, targetItem); MM.App.action(action); this._endCut(); break; case "copy": - var action = new MM.Action.AppendItem(targetItem, this._data.clone()); + var action = new MM.Action.AppendItem(targetItem, sourceItem.clone()); + MM.App.action(action); + break; + } +} + +MM.Clipboard._pastePlaintext = function(plaintext, targetItem) { + var items = this._parsePlaintext(plaintext); + + if (this._mode == "cut") { this._endCut(); } /* external paste => abort cutting */ + + switch (items.length) { + case 0: return; + + case 1: + var action = new MM.Action.AppendItem(targetItem, items[0]); + MM.App.action(action); + break; + + default: + var actions = items.map(function(item) { + return new MM.Action.AppendItem(targetItem, item); + }); + var action = new MM.Action.Multi(actions); MM.App.action(action); break; } @@ -1471,20 +1504,29 @@ MM.Clipboard.paste = function(targetItem) { MM.Clipboard.cut = function(sourceItem) { this._endCut(); - this._data = sourceItem; + this._item = sourceItem; + this._item.getDOM().node.classList.add("cut"); this._mode = "cut"; - var node = this._data.getDOM().node; - node.classList.add("cut"); + this._expose(); +} + +/** + * Expose plaintext data to the textarea to be copied to system clipboard. Clear afterwards. + */ +MM.Clipboard._expose = function() { + var plaintext = this._itemToPlaintext(this._item); + this._node.value = plaintext; + this._node.selectionStart = 0; + this._node.selectionEnd = this._node.value.length; + setTimeout(function() { this._node.value = ""; }.bind(this), this._delay); } MM.Clipboard._endCut = function() { if (this._mode != "cut") { return; } - var node = this._data.getDOM().node; - node.classList.remove("cut"); - - this._data = null; + this._item.getDOM().node.classList.remove("cut"); + this._item = null; this._mode = ""; } @@ -1500,6 +1542,52 @@ MM.Clipboard._itemToPlaintext = function(item, depth) { return lines.join("\n") + (depth ? "" : "\n"); } + +MM.Clipboard._parsePlaintext = function(plaintext) { + var lines = plaintext.split("\n").filter(function(line) { + return line.match(/\S/); + }); + + return this._parsePlaintextItems(lines); +} + +MM.Clipboard._parsePlaintextItems = function(lines) { + var items = []; + if (!lines.length) { return items; } + var firstPrefix = this._getPlaintextPrefix(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; } + this._parsePlaintextItems(childLines).forEach(function(child) { + currentItem.insertChild(child); + }); + childLines = []; + } + + lines.forEach(function(line, index) { + if (this._getPlaintextPrefix(line) == firstPrefix) { /* new top-level item! */ + convertChildLinesToChildren.call(this); /* finalize previous item */ + + var json = {text:line.match(/^\s*(.*)/)[1]}; + currentItem = MM.Item.fromJSON(json); + items.push(currentItem); + } else { /* prepare as a future child */ + childLines.push(line); + } + }, this); + + convertChildLinesToChildren.call(this); + + return items; +} + +MM.Clipboard._getPlaintextPrefix = function(line) { + return line.match(/^\s*/)[0]; +} MM.Menu = { _dom: {}, _port: null, @@ -4881,11 +4969,11 @@ MM.Mouse._visualizeDragState = function(state) { 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. diff --git a/src/app.js b/src/app.js index 3e3b313..328bfcb 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,8 @@ - +/* setInterval(function() { console.log(document.activeElement); }, 1000); - +*/ /* * Notes regarding app state/modes, activeElements, focusing etc. diff --git a/src/clipboard.js b/src/clipboard.js index b98bf23..da7eb8e 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -1,6 +1,7 @@ MM.Clipboard = { - _data: null, + _item: null, _mode: "", + _delay: 0, _node: document.createElement("textarea") }; @@ -19,40 +20,72 @@ MM.Clipboard.focus = function() { MM.Clipboard.copy = function(sourceItem) { this._endCut(); - this._data = sourceItem.clone(); + this._item = sourceItem.clone(); this._mode = "copy"; - var plaintext = this._itemToPlaintext(sourceItem); - this._node.value = plaintext; - this._node.selectionStart = 0; - this._node.selectionEnd = this._node.value.length; - setTimeout(function() { this._node.value = ""; }.bind(this), 0); + this._expose(); } MM.Clipboard.paste = function(targetItem) { - if (!this._data) { return; } + setTimeout(function() { + var pasted = this._node.value; + this._node.value = ""; + if (!pasted) { return; } /* nothing */ + + if (this._item && pasted == this._itemToPlaintext(this._item)) { /* 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 (this._data == targetItem || this._data.getParent() == targetItem) { /* abort by pasting on the same node or the parent */ + 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 == this._data) { return; } /* moving to a child => forbidden */ + if (item == sourceItem) { return; } /* moving to a child => forbidden */ item = item.getParent(); } - var action = new MM.Action.MoveItem(this._data, targetItem); + var action = new MM.Action.MoveItem(sourceItem, targetItem); MM.App.action(action); this._endCut(); break; case "copy": - var action = new MM.Action.AppendItem(targetItem, this._data.clone()); + var action = new MM.Action.AppendItem(targetItem, sourceItem.clone()); + MM.App.action(action); + break; + } +} + +MM.Clipboard._pastePlaintext = function(plaintext, targetItem) { + var items = this._parsePlaintext(plaintext); + + if (this._mode == "cut") { this._endCut(); } /* external paste => abort cutting */ + + switch (items.length) { + case 0: return; + + case 1: + var action = new MM.Action.AppendItem(targetItem, items[0]); + MM.App.action(action); + break; + + default: + var actions = items.map(function(item) { + return new MM.Action.AppendItem(targetItem, item); + }); + var action = new MM.Action.Multi(actions); MM.App.action(action); break; } @@ -62,20 +95,29 @@ MM.Clipboard.paste = function(targetItem) { MM.Clipboard.cut = function(sourceItem) { this._endCut(); - this._data = sourceItem; + this._item = sourceItem; + this._item.getDOM().node.classList.add("cut"); this._mode = "cut"; - var node = this._data.getDOM().node; - node.classList.add("cut"); + this._expose(); +} + +/** + * Expose plaintext data to the textarea to be copied to system clipboard. Clear afterwards. + */ +MM.Clipboard._expose = function() { + var plaintext = this._itemToPlaintext(this._item); + this._node.value = plaintext; + this._node.selectionStart = 0; + this._node.selectionEnd = this._node.value.length; + setTimeout(function() { this._node.value = ""; }.bind(this), this._delay); } MM.Clipboard._endCut = function() { if (this._mode != "cut") { return; } - var node = this._data.getDOM().node; - node.classList.remove("cut"); - - this._data = null; + this._item.getDOM().node.classList.remove("cut"); + this._item = null; this._mode = ""; } @@ -91,3 +133,49 @@ MM.Clipboard._itemToPlaintext = function(item, depth) { return lines.join("\n") + (depth ? "" : "\n"); } + +MM.Clipboard._parsePlaintext = function(plaintext) { + var lines = plaintext.split("\n").filter(function(line) { + return line.match(/\S/); + }); + + return this._parsePlaintextItems(lines); +} + +MM.Clipboard._parsePlaintextItems = function(lines) { + var items = []; + if (!lines.length) { return items; } + var firstPrefix = this._getPlaintextPrefix(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; } + this._parsePlaintextItems(childLines).forEach(function(child) { + currentItem.insertChild(child); + }); + childLines = []; + } + + lines.forEach(function(line, index) { + if (this._getPlaintextPrefix(line) == firstPrefix) { /* new top-level item! */ + convertChildLinesToChildren.call(this); /* finalize previous item */ + + var json = {text:line.match(/^\s*(.*)/)[1]}; + currentItem = MM.Item.fromJSON(json); + items.push(currentItem); + } else { /* prepare as a future child */ + childLines.push(line); + } + }, this); + + convertChildLinesToChildren.call(this); + + return items; +} + +MM.Clipboard._getPlaintextPrefix = function(line) { + return line.match(/^\s*/)[0]; +} From fca2362a9b8d128f8ac7340aa2843f40cf5cf406 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Mon, 29 Sep 2014 08:15:20 +0200 Subject: [PATCH 4/7] plaintext as a dedicated format --- Makefile | 1 + my-mind.js | 181 ++++++++++++++++++++++----------------- src/clipboard.js | 93 ++++---------------- src/format.plaintext.js | 86 +++++++++++++++++++ src/ui.backend.file.js | 1 + src/ui.backend.gdrive.js | 1 + 6 files changed, 207 insertions(+), 156 deletions(-) create mode 100644 src/format.plaintext.js diff --git a/Makefile b/Makefile index 70021af..00f5ac5 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ SOURCES = src/mm.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 \ diff --git a/my-mind.js b/my-mind.js index bad1fb0..def175e 100644 --- a/my-mind.js +++ b/my-mind.js @@ -1441,7 +1441,7 @@ MM.Clipboard.paste = function(targetItem) { this._node.value = ""; if (!pasted) { return; } /* nothing */ - if (this._item && pasted == this._itemToPlaintext(this._item)) { /* pasted a previously copied/cut item */ + 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); @@ -1478,27 +1478,22 @@ MM.Clipboard._pasteItem = function(sourceItem, targetItem) { } MM.Clipboard._pastePlaintext = function(plaintext, targetItem) { - var items = this._parsePlaintext(plaintext); - if (this._mode == "cut") { this._endCut(); } /* external paste => abort cutting */ - switch (items.length) { - case 0: return; - - case 1: - var action = new MM.Action.AppendItem(targetItem, items[0]); - MM.App.action(action); - break; + var json = MM.Format.Plaintext.from(plaintext); + var map = MM.Map.fromJSON(json); + var root = map.getRoot(); - default: - var actions = items.map(function(item) { - return new MM.Action.AppendItem(targetItem, item); - }); - var action = new MM.Action.Multi(actions); - MM.App.action(action); - break; + 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) { @@ -1515,7 +1510,8 @@ MM.Clipboard.cut = function(sourceItem) { * Expose plaintext data to the textarea to be copied to system clipboard. Clear afterwards. */ MM.Clipboard._expose = function() { - var plaintext = this._itemToPlaintext(this._item); + 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; @@ -1529,65 +1525,6 @@ MM.Clipboard._endCut = function() { this._item = null; this._mode = ""; } - -MM.Clipboard._itemToPlaintext = function(item, depth) { - depth = depth || 0; - - var lines = item.getChildren().map(function(child) { - return this._itemToPlaintext(child, depth+1); - }, this); - - var prefix = new Array(depth+1).join("\t"); - lines.unshift(prefix + item.getText().replace(/\n/g, "
")) - - return lines.join("\n") + (depth ? "" : "\n"); -} - -MM.Clipboard._parsePlaintext = function(plaintext) { - var lines = plaintext.split("\n").filter(function(line) { - return line.match(/\S/); - }); - - return this._parsePlaintextItems(lines); -} - -MM.Clipboard._parsePlaintextItems = function(lines) { - var items = []; - if (!lines.length) { return items; } - var firstPrefix = this._getPlaintextPrefix(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; } - this._parsePlaintextItems(childLines).forEach(function(child) { - currentItem.insertChild(child); - }); - childLines = []; - } - - lines.forEach(function(line, index) { - if (this._getPlaintextPrefix(line) == firstPrefix) { /* new top-level item! */ - convertChildLinesToChildren.call(this); /* finalize previous item */ - - var json = {text:line.match(/^\s*(.*)/)[1]}; - currentItem = MM.Item.fromJSON(json); - items.push(currentItem); - } else { /* prepare as a future child */ - childLines.push(line); - } - }, this); - - convertChildLinesToChildren.call(this); - - return items; -} - -MM.Clipboard._getPlaintextPrefix = function(line) { - return line.match(/^\s*/)[0]; -} MM.Menu = { _dom: {}, _port: null, @@ -3120,6 +3057,92 @@ MM.Format.Mup._MMtoMup = function(item, side) { 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); /** @@ -4219,6 +4242,7 @@ MM.UI.Backend.File.init = function(select) { 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; } @@ -4632,6 +4656,7 @@ MM.UI.Backend.GDrive.init = function(select) { 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; } diff --git a/src/clipboard.js b/src/clipboard.js index da7eb8e..66fa996 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -32,7 +32,7 @@ MM.Clipboard.paste = function(targetItem) { this._node.value = ""; if (!pasted) { return; } /* nothing */ - if (this._item && pasted == this._itemToPlaintext(this._item)) { /* pasted a previously copied/cut item */ + 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); @@ -69,27 +69,22 @@ MM.Clipboard._pasteItem = function(sourceItem, targetItem) { } MM.Clipboard._pastePlaintext = function(plaintext, targetItem) { - var items = this._parsePlaintext(plaintext); - if (this._mode == "cut") { this._endCut(); } /* external paste => abort cutting */ - switch (items.length) { - case 0: return; - - case 1: - var action = new MM.Action.AppendItem(targetItem, items[0]); - MM.App.action(action); - break; + var json = MM.Format.Plaintext.from(plaintext); + var map = MM.Map.fromJSON(json); + var root = map.getRoot(); - default: - var actions = items.map(function(item) { - return new MM.Action.AppendItem(targetItem, item); - }); - var action = new MM.Action.Multi(actions); - MM.App.action(action); - break; + 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) { @@ -106,7 +101,8 @@ MM.Clipboard.cut = function(sourceItem) { * Expose plaintext data to the textarea to be copied to system clipboard. Clear afterwards. */ MM.Clipboard._expose = function() { - var plaintext = this._itemToPlaintext(this._item); + 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; @@ -120,62 +116,3 @@ MM.Clipboard._endCut = function() { this._item = null; this._mode = ""; } - -MM.Clipboard._itemToPlaintext = function(item, depth) { - depth = depth || 0; - - var lines = item.getChildren().map(function(child) { - return this._itemToPlaintext(child, depth+1); - }, this); - - var prefix = new Array(depth+1).join("\t"); - lines.unshift(prefix + item.getText().replace(/\n/g, "
")) - - return lines.join("\n") + (depth ? "" : "\n"); -} - -MM.Clipboard._parsePlaintext = function(plaintext) { - var lines = plaintext.split("\n").filter(function(line) { - return line.match(/\S/); - }); - - return this._parsePlaintextItems(lines); -} - -MM.Clipboard._parsePlaintextItems = function(lines) { - var items = []; - if (!lines.length) { return items; } - var firstPrefix = this._getPlaintextPrefix(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; } - this._parsePlaintextItems(childLines).forEach(function(child) { - currentItem.insertChild(child); - }); - childLines = []; - } - - lines.forEach(function(line, index) { - if (this._getPlaintextPrefix(line) == firstPrefix) { /* new top-level item! */ - convertChildLinesToChildren.call(this); /* finalize previous item */ - - var json = {text:line.match(/^\s*(.*)/)[1]}; - currentItem = MM.Item.fromJSON(json); - items.push(currentItem); - } else { /* prepare as a future child */ - childLines.push(line); - } - }, this); - - convertChildLinesToChildren.call(this); - - return items; -} - -MM.Clipboard._getPlaintextPrefix = function(line) { - return line.match(/^\s*/)[0]; -} diff --git a/src/format.plaintext.js b/src/format.plaintext.js new file mode 100644 index 0000000..6f993e8 --- /dev/null +++ b/src/format.plaintext.js @@ -0,0 +1,86 @@ +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]; +} diff --git a/src/ui.backend.file.js b/src/ui.backend.file.js index 34806c4..cb066f5 100644 --- a/src/ui.backend.file.js +++ b/src/ui.backend.file.js @@ -10,6 +10,7 @@ MM.UI.Backend.File.init = function(select) { 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; } diff --git a/src/ui.backend.gdrive.js b/src/ui.backend.gdrive.js index dec7941..533fd9d 100644 --- a/src/ui.backend.gdrive.js +++ b/src/ui.backend.gdrive.js @@ -10,6 +10,7 @@ MM.UI.Backend.GDrive.init = function(select) { 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; } From b6c0dc099e1c0e2a4f5ab45558d36c54b56072b7 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Mon, 29 Sep 2014 10:19:38 +0200 Subject: [PATCH 5/7] proper newline/br handling --- examples/features.mymind | 2 +- my-mind.js | 30 +++++++++++++++++++----------- src/format.freemind.js | 4 ++-- src/format.js | 8 ++++++++ src/format.json.js | 2 +- src/format.mma.js | 4 ++-- src/format.mup.js | 4 ++-- src/format.plaintext.js | 2 +- src/item.js | 4 ++-- src/map.js | 2 +- 10 files changed, 39 insertions(+), 23 deletions(-) diff --git a/examples/features.mymind b/examples/features.mymind index e4349e2..c0325fa 100644 --- a/examples/features.mymind +++ b/examples/features.mymind @@ -1,7 +1,7 @@ { "root": { "id": "ujfdpxoz", - "text": "My Mind\nFeatures", + "text": "My Mind
Features", "layout": "map", "children": [ { diff --git a/my-mind.js b/my-mind.js index def175e..dd19fe7 100644 --- a/my-mind.js +++ b/my-mind.js @@ -510,7 +510,7 @@ MM.Item.prototype.updateSubtree = function(isSubChild) { } MM.Item.prototype.setText = function(text) { - this._dom.text.innerHTML = text.replace(/\n/g, "
"); + this._dom.text.innerHTML = text; this._findLinks(this._dom.text); return this.update(); } @@ -520,7 +520,7 @@ MM.Item.prototype.getId = function() { } MM.Item.prototype.getText = function() { - return this._dom.text.innerHTML.replace(//g, "\n"); + return this._dom.text.innerHTML; } MM.Item.prototype.collapse = function() { @@ -1069,7 +1069,7 @@ MM.Map.prototype.getRoot = function() { MM.Map.prototype.getName = function() { var name = this._root.getText(); - return name.replace(/\n/g, " ").replace(/<.*?>/g, "").trim(); + return MM.Format.br2nl(name).replace(/\n/g, " ").replace(/<.*?>/g, "").trim(); } MM.Map.prototype.getId = function() { @@ -2790,6 +2790,14 @@ MM.Format.getByMime = function(mime) { MM.Format.to = function(data) {} MM.Format.from = function(data) {} + +MM.Format.nl2br = function(str) { + return str.replace(/\n/g, "
"); +} + +MM.Format.br2nl = function(str) { + return str.replace(//g, "\n"); +} MM.Format.JSON = Object.create(MM.Format, { id: {value: "json"}, label: {value: "Native (JSON)"}, @@ -2798,7 +2806,7 @@ MM.Format.JSON = Object.create(MM.Format, { }); MM.Format.JSON.to = function(data) { - return JSON.stringify(data, null, 2) + "\n"; + return JSON.stringify(data, null, "\t") + "\n"; } MM.Format.JSON.from = function(data) { @@ -2852,7 +2860,7 @@ MM.Format.FreeMind._serializeItem = function(doc, json) { MM.Format.FreeMind._serializeAttributes = function(doc, json) { var elm = doc.createElement("node"); - elm.setAttribute("TEXT", json.text); + elm.setAttribute("TEXT", MM.Format.br2nl(json.text)); elm.setAttribute("ID", json.id); if (json.side) { elm.setAttribute("POSITION", json.side); } @@ -2878,7 +2886,7 @@ MM.Format.FreeMind._parseNode = function(node, parent) { MM.Format.FreeMind._parseAttributes = function(node, parent) { var json = { children: [], - text: node.getAttribute("TEXT") || "", + text: MM.Format.nl2br(node.getAttribute("TEXT") || ""), id: node.getAttribute("ID") }; @@ -2924,7 +2932,7 @@ MM.Format.MMA = Object.create(MM.Format.FreeMind, { MM.Format.MMA._parseAttributes = function(node, parent) { var json = { children: [], - text: node.getAttribute("title") || "", + text: MM.Format.nl2br(node.getAttribute("title") || ""), shape: "box" }; @@ -2954,7 +2962,7 @@ MM.Format.MMA._parseAttributes = function(node, parent) { MM.Format.MMA._serializeAttributes = function(doc, json) { var elm = doc.createElement("node"); - elm.setAttribute("title", json.text); + 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"); } @@ -2993,7 +3001,7 @@ MM.Format.Mup.from = function(data) { MM.Format.Mup._MupToMM = function(item) { var json = { - text: item.title, + text: MM.Format.nl2br(item.title), id: item.id, shape: "box" } @@ -3031,7 +3039,7 @@ MM.Format.Mup._MupToMM = function(item) { MM.Format.Mup._MMtoMup = function(item, side) { var result = { id: item.id, - title: item.text, + title: MM.Format.br2nl(item.text), attr: {} } if (item.color) { @@ -3103,7 +3111,7 @@ MM.Format.Plaintext._serializeItem = function(item, depth) { }, this); var prefix = new Array(depth+1).join("\t"); - lines.unshift(prefix + item.text.replace(/\n/g, "
")) + lines.unshift(prefix + item.text.replace(/\n/g, "")); return lines.join("\n") + (depth ? "" : "\n"); } diff --git a/src/format.freemind.js b/src/format.freemind.js index 52c037f..8c7ad1c 100644 --- a/src/format.freemind.js +++ b/src/format.freemind.js @@ -46,7 +46,7 @@ MM.Format.FreeMind._serializeItem = function(doc, json) { MM.Format.FreeMind._serializeAttributes = function(doc, json) { var elm = doc.createElement("node"); - elm.setAttribute("TEXT", json.text); + elm.setAttribute("TEXT", MM.Format.br2nl(json.text)); elm.setAttribute("ID", json.id); if (json.side) { elm.setAttribute("POSITION", json.side); } @@ -72,7 +72,7 @@ MM.Format.FreeMind._parseNode = function(node, parent) { MM.Format.FreeMind._parseAttributes = function(node, parent) { var json = { children: [], - text: node.getAttribute("TEXT") || "", + text: MM.Format.nl2br(node.getAttribute("TEXT") || ""), id: node.getAttribute("ID") }; diff --git a/src/format.js b/src/format.js index cd1a764..3f40063 100644 --- a/src/format.js +++ b/src/format.js @@ -16,3 +16,11 @@ MM.Format.getByMime = function(mime) { MM.Format.to = function(data) {} MM.Format.from = function(data) {} + +MM.Format.nl2br = function(str) { + return str.replace(/\n/g, "
"); +} + +MM.Format.br2nl = function(str) { + return str.replace(//g, "\n"); +} diff --git a/src/format.json.js b/src/format.json.js index 3bab0f5..3b1b9a3 100644 --- a/src/format.json.js +++ b/src/format.json.js @@ -6,7 +6,7 @@ MM.Format.JSON = Object.create(MM.Format, { }); MM.Format.JSON.to = function(data) { - return JSON.stringify(data, null, 2) + "\n"; + return JSON.stringify(data, null, "\t") + "\n"; } MM.Format.JSON.from = function(data) { diff --git a/src/format.mma.js b/src/format.mma.js index 1bba5f3..08e3d9d 100644 --- a/src/format.mma.js +++ b/src/format.mma.js @@ -7,7 +7,7 @@ MM.Format.MMA = Object.create(MM.Format.FreeMind, { MM.Format.MMA._parseAttributes = function(node, parent) { var json = { children: [], - text: node.getAttribute("title") || "", + text: MM.Format.nl2br(node.getAttribute("title") || ""), shape: "box" }; @@ -37,7 +37,7 @@ MM.Format.MMA._parseAttributes = function(node, parent) { MM.Format.MMA._serializeAttributes = function(doc, json) { var elm = doc.createElement("node"); - elm.setAttribute("title", json.text); + 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"); } diff --git a/src/format.mup.js b/src/format.mup.js index e32fb52..68d04e8 100644 --- a/src/format.mup.js +++ b/src/format.mup.js @@ -23,7 +23,7 @@ MM.Format.Mup.from = function(data) { MM.Format.Mup._MupToMM = function(item) { var json = { - text: item.title, + text: MM.Format.nl2br(item.title), id: item.id, shape: "box" } @@ -61,7 +61,7 @@ MM.Format.Mup._MupToMM = function(item) { MM.Format.Mup._MMtoMup = function(item, side) { var result = { id: item.id, - title: item.text, + title: MM.Format.br2nl(item.text), attr: {} } if (item.color) { diff --git a/src/format.plaintext.js b/src/format.plaintext.js index 6f993e8..7499486 100644 --- a/src/format.plaintext.js +++ b/src/format.plaintext.js @@ -44,7 +44,7 @@ MM.Format.Plaintext._serializeItem = function(item, depth) { }, this); var prefix = new Array(depth+1).join("\t"); - lines.unshift(prefix + item.text.replace(/\n/g, "
")) + lines.unshift(prefix + item.text.replace(/\n/g, "")); return lines.join("\n") + (depth ? "" : "\n"); } diff --git a/src/item.js b/src/item.js index 938fac8..addadbe 100644 --- a/src/item.js +++ b/src/item.js @@ -221,7 +221,7 @@ MM.Item.prototype.updateSubtree = function(isSubChild) { } MM.Item.prototype.setText = function(text) { - this._dom.text.innerHTML = text.replace(/\n/g, "
"); + this._dom.text.innerHTML = text; this._findLinks(this._dom.text); return this.update(); } @@ -231,7 +231,7 @@ MM.Item.prototype.getId = function() { } MM.Item.prototype.getText = function() { - return this._dom.text.innerHTML.replace(//g, "\n"); + return this._dom.text.innerHTML; } MM.Item.prototype.collapse = function() { diff --git a/src/map.js b/src/map.js index 81c38b5..e554002 100644 --- a/src/map.js +++ b/src/map.js @@ -195,7 +195,7 @@ MM.Map.prototype.getRoot = function() { MM.Map.prototype.getName = function() { var name = this._root.getText(); - return name.replace(/\n/g, " ").replace(/<.*?>/g, "").trim(); + return MM.Format.br2nl(name).replace(/\n/g, " ").replace(/<.*?>/g, "").trim(); } MM.Map.prototype.getId = function() { From 86fe5db7eeddcfb72a6ae42203665426557001e4 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Mon, 29 Sep 2014 12:53:13 +0200 Subject: [PATCH 6/7] better clipboard blur protection --- my-mind.js | 22 ++++++++++++++-------- src/app.js | 4 ++-- src/item.js | 5 ++++- src/mouse.js | 13 ++++++++----- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/my-mind.js b/my-mind.js index dd19fe7..4a4953b 100644 --- a/my-mind.js +++ b/my-mind.js @@ -660,7 +660,7 @@ MM.Item.prototype.insertChild = function(child, index) { if (!child) { child = new MM.Item(); newChild = true; - } else if (child.getParent()) { + } else if (child.getParent() && child.getParent().removeChild) { /* only when the child has non-map parent */ child.getParent().removeChild(child); } @@ -719,6 +719,9 @@ MM.Item.prototype.stopEditing = function() { this._oldText = ""; this.update(); /* text changed */ + + MM.Clipboard.focus(); + return result; } @@ -4769,11 +4772,11 @@ MM.Mouse.handleEvent = function(e) { case "contextmenu": this._endDrag(); + e.preventDefault(); var item = MM.App.map.getItemFor(e.target); - if (item) { MM.App.select(item); } + item && MM.App.select(item); - e.preventDefault(); MM.Menu.open(e.clientX, e.clientY); break; @@ -4782,6 +4785,7 @@ MM.Mouse.handleEvent = function(e) { e.clientX = e.touches[0].clientX; e.clientY = e.touches[0].clientY; case "mousedown": + if (e.type == "mousedown") { e.preventDefault(); } /* to prevent blurring the clipboard node */ var item = MM.App.map.getItemFor(e.target); if (e.type == "touchstart") { /* context menu here, after we have the item */ @@ -4791,9 +4795,11 @@ MM.Mouse.handleEvent = function(e) { }, this.TOUCH_DELAY); } - if (item == MM.App.current && MM.App.editing) { return; } - /* if we are editing another item, end that by blurring it */ - document.activeElement && document.activeElement.blur(); + if (MM.App.editing) { + if (item == MM.App.current) { return; } /* ignore dnd on edited node */ + MM.Command.Finish.execute(); /* clicked elsewhere => finalize edit */ + } + this._startDrag(e, item); break; @@ -5002,11 +5008,11 @@ MM.Mouse._visualizeDragState = function(state) { 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. diff --git a/src/app.js b/src/app.js index 328bfcb..3e3b313 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,8 @@ -/* + setInterval(function() { console.log(document.activeElement); }, 1000); -*/ + /* * Notes regarding app state/modes, activeElements, focusing etc. diff --git a/src/item.js b/src/item.js index addadbe..e6ab595 100644 --- a/src/item.js +++ b/src/item.js @@ -371,7 +371,7 @@ MM.Item.prototype.insertChild = function(child, index) { if (!child) { child = new MM.Item(); newChild = true; - } else if (child.getParent()) { + } else if (child.getParent() && child.getParent().removeChild) { /* only when the child has non-map parent */ child.getParent().removeChild(child); } @@ -430,6 +430,9 @@ MM.Item.prototype.stopEditing = function() { this._oldText = ""; this.update(); /* text changed */ + + MM.Clipboard.focus(); + return result; } diff --git a/src/mouse.js b/src/mouse.js index a1ee65e..281c726 100644 --- a/src/mouse.js +++ b/src/mouse.js @@ -35,11 +35,11 @@ MM.Mouse.handleEvent = function(e) { case "contextmenu": this._endDrag(); + e.preventDefault(); var item = MM.App.map.getItemFor(e.target); - if (item) { MM.App.select(item); } + item && MM.App.select(item); - e.preventDefault(); MM.Menu.open(e.clientX, e.clientY); break; @@ -48,6 +48,7 @@ MM.Mouse.handleEvent = function(e) { e.clientX = e.touches[0].clientX; e.clientY = e.touches[0].clientY; case "mousedown": + if (e.type == "mousedown") { e.preventDefault(); } /* to prevent blurring the clipboard node */ var item = MM.App.map.getItemFor(e.target); if (e.type == "touchstart") { /* context menu here, after we have the item */ @@ -57,9 +58,11 @@ MM.Mouse.handleEvent = function(e) { }, this.TOUCH_DELAY); } - if (item == MM.App.current && MM.App.editing) { return; } - /* if we are editing another item, end that by blurring it */ - document.activeElement && document.activeElement.blur(); + if (MM.App.editing) { + if (item == MM.App.current) { return; } /* ignore dnd on edited node */ + MM.Command.Finish.execute(); /* clicked elsewhere => finalize edit */ + } + this._startDrag(e, item); break; From 4641b350a32cdaa6b932537bce0b0fed6970ec83 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Thu, 2 Oct 2014 16:46:19 +0200 Subject: [PATCH 7/7] clipboard fixes --- my-mind.js | 37 +++++++++++++++++++++++++++---------- src/app.js | 4 ++-- src/clipboard.js | 14 +++++++++++--- src/command.edit.js | 4 ++-- src/command.js | 15 ++++++++++++--- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/my-mind.js b/my-mind.js index 4a4953b..a74b096 100644 --- a/my-mind.js +++ b/my-mind.js @@ -1413,7 +1413,7 @@ MM.Action.SetStatus.prototype.undo = function() { MM.Clipboard = { _item: null, _mode: "", - _delay: 0, + _delay: 50, _node: document.createElement("textarea") }; @@ -1428,6 +1428,7 @@ MM.Clipboard.init = function() { MM.Clipboard.focus = function() { this._node.focus(); + this._empty(); } MM.Clipboard.copy = function(sourceItem) { @@ -1441,7 +1442,7 @@ MM.Clipboard.copy = function(sourceItem) { MM.Clipboard.paste = function(targetItem) { setTimeout(function() { var pasted = this._node.value; - 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 */ @@ -1518,7 +1519,14 @@ MM.Clipboard._expose = function() { this._node.value = plaintext; this._node.selectionStart = 0; this._node.selectionEnd = this._node.value.length; - setTimeout(function() { this._node.value = ""; }.bind(this), this._delay); + 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() { @@ -1833,7 +1841,10 @@ MM.Command.Pan.handleEvent = function(e) { MM.Command.Copy = Object.create(MM.Command, { label: {value: "Copy"}, prevent: {value: false}, - keys: {value: [{keyCode: "C".charCodeAt(0), ctrlKey:true}]} + 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); @@ -1842,7 +1853,10 @@ MM.Command.Copy.execute = function() { MM.Command.Cut = Object.create(MM.Command, { label: {value: "Cut"}, prevent: {value: false}, - keys: {value: [{keyCode: "X".charCodeAt(0), ctrlKey:true}]} + 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); @@ -1851,7 +1865,10 @@ MM.Command.Cut.execute = function() { MM.Command.Paste = Object.create(MM.Command, { label: {value: "Paste"}, prevent: {value: false}, - keys: {value: [{keyCode: "V".charCodeAt(0), ctrlKey:true}]} + 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); @@ -1969,7 +1986,7 @@ MM.Command.Strikethrough = Object.create(MM.Command.Style, { MM.Command.Value = Object.create(MM.Command, { label: {value: "Set value"}, - keys: {value: [{charCode: "v".charCodeAt(0), ctrlKey:false}]} + keys: {value: [{charCode: "v".charCodeAt(0), ctrlKey:false, metaKey:false}]} }); MM.Command.Value.execute = function() { var item = MM.App.current; @@ -2008,7 +2025,7 @@ MM.Command.No.execute = function() { MM.Command.Computed = Object.create(MM.Command, { label: {value: "Computed"}, - keys: {value: [{charCode: "c".charCodeAt(0), ctrlKey:false}]} + keys: {value: [{charCode: "c".charCodeAt(0), ctrlKey:false, metaKey:false}]} }); MM.Command.Computed.execute = function() { var item = MM.App.current; @@ -5008,11 +5025,11 @@ MM.Mouse._visualizeDragState = function(state) { 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. diff --git a/src/app.js b/src/app.js index 3e3b313..328bfcb 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,8 @@ - +/* setInterval(function() { console.log(document.activeElement); }, 1000); - +*/ /* * Notes regarding app state/modes, activeElements, focusing etc. diff --git a/src/clipboard.js b/src/clipboard.js index 66fa996..a871e22 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -1,7 +1,7 @@ MM.Clipboard = { _item: null, _mode: "", - _delay: 0, + _delay: 50, _node: document.createElement("textarea") }; @@ -16,6 +16,7 @@ MM.Clipboard.init = function() { MM.Clipboard.focus = function() { this._node.focus(); + this._empty(); } MM.Clipboard.copy = function(sourceItem) { @@ -29,7 +30,7 @@ MM.Clipboard.copy = function(sourceItem) { MM.Clipboard.paste = function(targetItem) { setTimeout(function() { var pasted = this._node.value; - 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 */ @@ -106,7 +107,14 @@ MM.Clipboard._expose = function() { this._node.value = plaintext; this._node.selectionStart = 0; this._node.selectionEnd = this._node.value.length; - setTimeout(function() { this._node.value = ""; }.bind(this), this._delay); + 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() { diff --git a/src/command.edit.js b/src/command.edit.js index 2976623..94af92b 100644 --- a/src/command.edit.js +++ b/src/command.edit.js @@ -101,7 +101,7 @@ MM.Command.Strikethrough = Object.create(MM.Command.Style, { MM.Command.Value = Object.create(MM.Command, { label: {value: "Set value"}, - keys: {value: [{charCode: "v".charCodeAt(0), ctrlKey:false}]} + keys: {value: [{charCode: "v".charCodeAt(0), ctrlKey:false, metaKey:false}]} }); MM.Command.Value.execute = function() { var item = MM.App.current; @@ -140,7 +140,7 @@ MM.Command.No.execute = function() { MM.Command.Computed = Object.create(MM.Command, { label: {value: "Computed"}, - keys: {value: [{charCode: "c".charCodeAt(0), ctrlKey:false}]} + keys: {value: [{charCode: "c".charCodeAt(0), ctrlKey:false, metaKey:false}]} }); MM.Command.Computed.execute = function() { var item = MM.App.current; diff --git a/src/command.js b/src/command.js index 6cd8545..792415d 100644 --- a/src/command.js +++ b/src/command.js @@ -246,7 +246,10 @@ MM.Command.Pan.handleEvent = function(e) { MM.Command.Copy = Object.create(MM.Command, { label: {value: "Copy"}, prevent: {value: false}, - keys: {value: [{keyCode: "C".charCodeAt(0), ctrlKey:true}]} + 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); @@ -255,7 +258,10 @@ MM.Command.Copy.execute = function() { MM.Command.Cut = Object.create(MM.Command, { label: {value: "Cut"}, prevent: {value: false}, - keys: {value: [{keyCode: "X".charCodeAt(0), ctrlKey:true}]} + 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); @@ -264,7 +270,10 @@ MM.Command.Cut.execute = function() { MM.Command.Paste = Object.create(MM.Command, { label: {value: "Paste"}, prevent: {value: false}, - keys: {value: [{keyCode: "V".charCodeAt(0), ctrlKey:true}]} + 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);