more styling
[roojs1] / Roo / lib / UndoManager.js
1 /**
2  * Originally based of this code... - refactored for Roo...
3  * https://github.com/aaalsaleh/undo-manager
4  
5  * undo-manager.js
6  * @author  Abdulrahman Alsaleh 
7  * @copyright 2015 Abdulrahman Alsaleh 
8  * @license  MIT License (c) 
9  *
10  * Hackily modifyed by alan@roojs.com
11  *
12  *
13  *  
14  *
15  *  TOTALLY UNTESTED...
16  *
17  *  Documentation to be done....
18  */
19  
20
21 /**
22 * @class Roo.lib.UndoManager
23 * An undo manager implementation in JavaScript. It follows the W3C UndoManager and DOM Transaction
24 * Draft and the undocumented and disabled Mozilla Firefox's UndoManager implementation.
25
26  * Usage:
27  * <pre><code>
28
29
30 editor.undoManager = new Roo.lib.UndoManager(1000, editor);
31  
32 </code></pre>
33
34 * For more information see this blog post with examples:
35 *  <a href="http://www.cnitblog.com/seeyeah/archive/2011/12/30/38728.html/">DomHelper
36      - Create Elements using DOM, HTML fragments and Templates</a>. 
37 * @constructor
38 * @param {Number} limit how far back to go ... use 1000?
39 * @param {Object} scope usually use document..
40 */
41
42 Roo.lib.UndoManager = function (limit, undoScopeHost)
43 {
44     this.stack = [];
45     this.limit = limit;
46     this.scope = undoScopeHost;
47     this.fireEvent = typeof CustomEvent != 'undefined' && undoScopeHost && undoScopeHost.dispatchEvent;
48     if (this.fireEvent) {
49         this.bindEvents();
50     }
51     this.reset();
52     
53 };
54         
55 Roo.lib.UndoManager.prototype = {
56     
57     limit : false,
58     stack : false,
59     scope :  false,
60     fireEvent : false,
61     position : 0,
62     length : 0,
63     
64     
65      /**
66      * To push and execute a transaction, the method undoManager.transact
67      * must be called by passing a transaction object as the first argument, and a merge
68      * flag as the second argument. A transaction object has the following properties:
69      *
70      * Usage:
71 <pre><code>
72 undoManager.transact({
73     label: 'Typing',
74     execute: function() { ... },
75     undo: function() { ... },
76     // redo same as execute
77     redo: function() { this.execute(); }
78 }, false);
79
80 // merge transaction
81 undoManager.transact({
82     label: 'Typing',
83     execute: function() { ... },  // this will be run...
84     undo: function() { ... }, // what to do when undo is run.
85     // redo same as execute
86     redo: function() { this.execute(); }
87 }, true); 
88 </code></pre> 
89      *
90      * 
91      * @param {Object} transaction The transaction to add to the stack.
92      * @return {String} The HTML fragment
93      */
94     
95     
96     transact : function (transaction, merge)
97     {
98         if (arguments.length < 2) {
99             throw new TypeError('Not enough arguments to UndoManager.transact.');
100         }
101
102         transaction.execute();
103
104         this.stack.splice(0, this.position);
105         if (merge && this.length) {
106             this.stack[0].push(transaction);
107         } else {
108             this.stack.unshift([transaction]);
109         }
110     
111         this.position = 0;
112
113         if (this.limit && this.stack.length > this.limit) {
114             this.length = this.stack.length = this.limit;
115         } else {
116             this.length = this.stack.length;
117         }
118
119         if (this.fireEvent) {
120             this.scope.dispatchEvent(
121                 new CustomEvent('DOMTransaction', {
122                     detail: {
123                         transactions: this.stack[0].slice()
124                     },
125                     bubbles: true,
126                     cancelable: false
127                 })
128             );
129         }
130         
131         //Roo.log("transaction: pos:" + this.position + " len: " + this.length + " slen:" + this.stack.length);
132       
133         
134     },
135
136     undo : function ()
137     {
138         //Roo.log("undo: pos:" + this.position + " len: " + this.length + " slen:" + this.stack.length);
139         
140         if (this.position < this.length) {
141             for (var i = this.stack[this.position].length - 1; i >= 0; i--) {
142                 this.stack[this.position][i].undo();
143             }
144             this.position++;
145
146             if (this.fireEvent) {
147                 this.scope.dispatchEvent(
148                     new CustomEvent('undo', {
149                         detail: {
150                             transactions: this.stack[this.position - 1].slice()
151                         },
152                         bubbles: true,
153                         cancelable: false
154                     })
155                 );
156             }
157         }
158     },
159
160     redo : function ()
161     {
162         if (this.position > 0) {
163             for (var i = 0, n = this.stack[this.position - 1].length; i < n; i++) {
164                 this.stack[this.position - 1][i].redo();
165             }
166             this.position--;
167
168             if (this.fireEvent) {
169                 this.scope.dispatchEvent(
170                     new CustomEvent('redo', {
171                         detail: {
172                             transactions: this.stack[this.position].slice()
173                         },
174                         bubbles: true,
175                         cancelable: false
176                     })
177                 );
178             }
179         }
180     },
181
182     item : function (index)
183     {
184         if (index >= 0 && index < this.length) {
185             return this.stack[index].slice();
186         }
187         return null;
188     },
189
190     clearUndo : function () {
191         this.stack.length = this.length = this.position;
192     },
193
194     clearRedo : function () {
195         this.stack.splice(0, this.position);
196         this.position = 0;
197         this.length = this.stack.length;
198     },
199     /**
200      * Reset the undo - probaly done on load to clear all history.
201      */
202     reset : function()
203     {
204         this.stack = [];
205         this.position = 0;
206         this.length = 0;
207         this.current_html = this.scope.innerHTML;
208         if (this.timer !== false) {
209             clearTimeout(this.timer);
210         }
211         this.timer = false;
212         this.merge = false;
213         this.addEvent();
214         
215     },
216     current_html : '',
217     timer : false,
218     merge : false,
219     
220     
221     // this will handle the undo/redo on the element.?
222     bindEvents : function()
223     {
224         var el  = this.scope;
225         el.undoManager = this;
226         
227         
228         this.scope.addEventListener('keydown', function(e) {
229             if ((e.ctrlKey || e.metaKey) && e.keyCode === 90) {
230                 if (e.shiftKey) {
231                     el.undoManager.redo(); // Ctrl/Command + Shift + Z
232                 } else {
233                     el.undoManager.undo(); // Ctrl/Command + Z
234                 }
235         
236                 e.preventDefault();
237             }
238         });
239         /// ignore keyup..
240         this.scope.addEventListener('keyup', function(e) {
241             if ((e.ctrlKey || e.metaKey) && e.keyCode === 90) {
242                 e.preventDefault();
243             }
244         });
245         
246         
247         
248         var t = this;
249         
250         el.addEventListener('input', function(e) {
251             if(el.innerHTML == t.current_html) {
252                 return;
253             }
254             // only record events every second.
255             if (t.timer !== false) {
256                clearTimeout(t.timer);
257                t.timer = false;
258             }
259             t.timer = setTimeout(function() { t.merge = false; }, 1000);
260             
261             t.addEvent(t.merge);
262             t.merge = true; // ignore changes happening every second..
263         });
264         },
265     /**
266      * Manually add an event.
267      * Normall called without arguements - and it will just get added to the stack.
268      * 
269      */
270     
271     addEvent : function(merge)
272     {
273         //Roo.log("undomanager +" + (merge ? 'Y':'n'));
274         // not sure if this should clear the timer 
275         merge = typeof(merge) == 'undefined' ? false : merge; 
276         
277         this.scope.undoManager.transact({
278             scope : this.scope,
279             oldHTML: this.current_html,
280             newHTML: this.scope.innerHTML,
281             // nothing to execute (content already changed when input is fired)
282             execute: function() { },
283             undo: function() {
284                 this.scope.innerHTML = this.current_html = this.oldHTML;
285             },
286             redo: function() {
287                 this.scope.innerHTML = this.current_html = this.newHTML;
288             }
289         }, false); //merge);
290         
291         this.merge = merge;
292         
293         this.current_html = this.scope.innerHTML;
294     }
295     
296     
297      
298     
299     
300     
301 };