2 * Undo Manager for wysihtml5
3 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
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>',
17 function cleanTempElements(doc) {
19 while (tempElement = doc.querySelector("._wysihtml5-temp")) {
20 tempElement.parentNode.removeChild(tempElement);
24 wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
25 /** @scope wysihtml5.UndoManager.prototype */ {
26 constructor: function(editor) {
28 this.composer = editor.composer;
29 this.element = this.composer.element;
40 _observe: function() {
42 doc = this.composer.sandbox.getDocument(),
45 // Catch CTRL+Z and CTRL+Y
46 dom.observe(this.element, "keydown", function(event) {
47 if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
51 var keyCode = event.keyCode,
52 isUndo = keyCode === Z_KEY && !event.shiftKey,
53 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
57 event.preventDefault();
60 event.preventDefault();
64 // Catch delete and backspace
65 dom.observe(this.element, "keydown", function(event) {
66 var keyCode = event.keyCode;
67 if (keyCode === lastKey) {
73 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
79 .on("newword:composer", function() {
83 .on("beforecommand:composer", function() {
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;
94 if (currentHtml === previousHtml) {
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();
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;
113 if (node.nodeType === wysihtml5.ELEMENT_NODE) {
116 element = node.parentNode;
117 position = this.getChildNodeIndex(element, node);
120 element.setAttribute(DATA_ATTR_OFFSET, offset);
121 if (typeof(position) !== "undefined") {
122 element.setAttribute(DATA_ATTR_NODE, position);
126 var clone = this.element.cloneNode(!!currentHtml);
127 this.historyDom.push(clone);
128 this.historyStr.push(currentHtml);
131 element.removeAttribute(DATA_ATTR_OFFSET);
132 element.removeAttribute(DATA_ATTR_NODE);
140 if (!this.undoPossible()) {
144 this.set(this.historyDom[--this.position - 1]);
145 this.editor.fire("undo:composer");
149 if (!this.redoPossible()) {
153 this.set(this.historyDom[++this.position - 1]);
154 this.editor.fire("redo:composer");
157 undoPossible: function() {
158 return this.position > 1;
161 redoPossible: function() {
162 return this.position < this.historyStr.length;
165 set: function(historyEntry) {
166 this.element.innerHTML = "";
169 childNodes = historyEntry.childNodes,
170 length = historyEntry.childNodes.length;
172 for (; i<length; i++) {
173 this.element.appendChild(childNodes[i].cloneNode(true));
181 if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
182 offset = historyEntry.getAttribute(DATA_ATTR_OFFSET);
183 position = historyEntry.getAttribute(DATA_ATTR_NODE);
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);
193 if (position !== null) {
194 node = this.getChildNodeByIndex(node, +position);
197 this.composer.selection.set(node, offset);
200 getChildNodeIndex: function(parent, child) {
202 childNodes = parent.childNodes,
203 length = childNodes.length;
204 for (; i<length; i++) {
205 if (childNodes[i] === child) {
211 getChildNodeByIndex: function(parent, index) {
212 return parent.childNodes[index];