WIP: Note editing, markdown to html
[oweals/karmaworld.git] / karmaworld / apps / wysihtml5 / static / wysihtml5 / wysihtml-0.4.17 / src / undo_manager.js
1 /**
2  * Undo Manager for wysihtml5
3  * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
4  */
5 (function(wysihtml5) {
6   var Z_KEY               = 90,
7       Y_KEY               = 89,
8       BACKSPACE_KEY       = 8,
9       DELETE_KEY          = 46,
10       MAX_HISTORY_ENTRIES = 25,
11       DATA_ATTR_NODE      = "data-wysihtml5-selection-node",
12       DATA_ATTR_OFFSET    = "data-wysihtml5-selection-offset",
13       UNDO_HTML           = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
14       REDO_HTML           = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
15       dom                 = wysihtml5.dom;
16
17   function cleanTempElements(doc) {
18     var tempElement;
19     while (tempElement = doc.querySelector("._wysihtml5-temp")) {
20       tempElement.parentNode.removeChild(tempElement);
21     }
22   }
23
24   wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
25     /** @scope wysihtml5.UndoManager.prototype */ {
26     constructor: function(editor) {
27       this.editor = editor;
28       this.composer = editor.composer;
29       this.element = this.composer.element;
30
31       this.position = 0;
32       this.historyStr = [];
33       this.historyDom = [];
34
35       this.transact();
36
37       this._observe();
38     },
39
40     _observe: function() {
41       var that      = this,
42           doc       = this.composer.sandbox.getDocument(),
43           lastKey;
44
45       // Catch CTRL+Z and CTRL+Y
46       dom.observe(this.element, "keydown", function(event) {
47         if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
48           return;
49         }
50
51         var keyCode = event.keyCode,
52             isUndo = keyCode === Z_KEY && !event.shiftKey,
53             isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
54
55         if (isUndo) {
56           that.undo();
57           event.preventDefault();
58         } else if (isRedo) {
59           that.redo();
60           event.preventDefault();
61         }
62       });
63
64       // Catch delete and backspace
65       dom.observe(this.element, "keydown", function(event) {
66         var keyCode = event.keyCode;
67         if (keyCode === lastKey) {
68           return;
69         }
70
71         lastKey = keyCode;
72
73         if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
74           that.transact();
75         }
76       });
77
78       this.editor
79         .on("newword:composer", function() {
80           that.transact();
81         })
82
83         .on("beforecommand:composer", function() {
84           that.transact();
85         });
86     },
87
88     transact: function() {
89       var previousHtml      = this.historyStr[this.position - 1],
90           currentHtml       = this.composer.getValue(false, false),
91           composerIsVisible   = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
92           range, node, offset, element, position;
93
94       if (currentHtml === previousHtml) {
95         return;
96       }
97
98       var length = this.historyStr.length = this.historyDom.length = this.position;
99       if (length > MAX_HISTORY_ENTRIES) {
100         this.historyStr.shift();
101         this.historyDom.shift();
102         this.position--;
103       }
104
105       this.position++;
106
107       if (composerIsVisible) {
108         // Do not start saving selection if composer is not visible
109         range   = this.composer.selection.getRange();
110         node    = (range && range.startContainer) ? range.startContainer : this.element;
111         offset  = (range && range.startOffset) ? range.startOffset : 0;
112
113         if (node.nodeType === wysihtml5.ELEMENT_NODE) {
114           element = node;
115         } else {
116           element  = node.parentNode;
117           position = this.getChildNodeIndex(element, node);
118         }
119
120         element.setAttribute(DATA_ATTR_OFFSET, offset);
121         if (typeof(position) !== "undefined") {
122           element.setAttribute(DATA_ATTR_NODE, position);
123         }
124       }
125
126       var clone = this.element.cloneNode(!!currentHtml);
127       this.historyDom.push(clone);
128       this.historyStr.push(currentHtml);
129
130       if (element) {
131         element.removeAttribute(DATA_ATTR_OFFSET);
132         element.removeAttribute(DATA_ATTR_NODE);
133       }
134
135     },
136
137     undo: function() {
138       this.transact();
139
140       if (!this.undoPossible()) {
141         return;
142       }
143
144       this.set(this.historyDom[--this.position - 1]);
145       this.editor.fire("undo:composer");
146     },
147
148     redo: function() {
149       if (!this.redoPossible()) {
150         return;
151       }
152
153       this.set(this.historyDom[++this.position - 1]);
154       this.editor.fire("redo:composer");
155     },
156
157     undoPossible: function() {
158       return this.position > 1;
159     },
160
161     redoPossible: function() {
162       return this.position < this.historyStr.length;
163     },
164
165     set: function(historyEntry) {
166       this.element.innerHTML = "";
167
168       var i = 0,
169           childNodes = historyEntry.childNodes,
170           length = historyEntry.childNodes.length;
171
172       for (; i<length; i++) {
173         this.element.appendChild(childNodes[i].cloneNode(true));
174       }
175
176       // Restore selection
177       var offset,
178           node,
179           position;
180
181       if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
182         offset    = historyEntry.getAttribute(DATA_ATTR_OFFSET);
183         position  = historyEntry.getAttribute(DATA_ATTR_NODE);
184         node      = this.element;
185       } else {
186         node      = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element;
187         offset    = node.getAttribute(DATA_ATTR_OFFSET);
188         position  = node.getAttribute(DATA_ATTR_NODE);
189         node.removeAttribute(DATA_ATTR_OFFSET);
190         node.removeAttribute(DATA_ATTR_NODE);
191       }
192
193       if (position !== null) {
194         node = this.getChildNodeByIndex(node, +position);
195       }
196
197       this.composer.selection.set(node, offset);
198     },
199
200     getChildNodeIndex: function(parent, child) {
201       var i           = 0,
202           childNodes  = parent.childNodes,
203           length      = childNodes.length;
204       for (; i<length; i++) {
205         if (childNodes[i] === child) {
206           return i;
207         }
208       }
209     },
210
211     getChildNodeByIndex: function(parent, index) {
212       return parent.childNodes[index];
213     }
214   });
215 })(wysihtml5);