Undo
[redakcja.git] / src / redakcja / static / js / wiki / undo.js
1 {
2
3     class Undo {
4         maxItems = 100;
5         stack = [];
6         position = 0;
7
8         stats = {size: 0};
9
10         constructor() {
11             $(() => {
12                 this.$undo = $("#undoBtn");
13                 this.$undo.on('click', () => {CurrentDocument.undo();})
14                 this.$redo = $("#redoBtn");
15                 this.$redo.on('click', () => {CurrentDocument.redo();})
16                 this.$stats = $("#undoStats");
17             })
18         }
19
20         refresh() {
21             this.$undo.prop('disabled', !this.canUndo);
22             this.$redo.prop('disabled', !this.canRedo);
23             this.$undo.attr('title', 'undo\n\n' + this.renderStats())
24         }
25
26         renderStats() {
27             return this.stats.size / 1e6;
28         }
29
30         push(state) {
31             // Has the state actually changed?
32             if (state == this.materialize(this.position))
33                 return;
34
35             while (this.position) {
36                 this.pop();
37                 --this.position;
38             }
39
40             this.put(state);
41             this.trim();
42             this.refresh();
43         }
44         pop() {
45             this.stats.size -= this.stack[0].length;
46             return this.stack.shift()
47         }
48         put(state) {
49             this.stack.unshift(state);
50             this.stats.size += state.length;
51         }
52         trim() {
53             while (this.stack.length > this.maxItems) {
54                 this.stats.size -= this.stack.pop().length;
55             }
56         }
57         materialize(n) {
58             return this.stack[n];
59         }
60
61         undo() {
62             if (!this.canUndo) return;
63             let val = this.materialize(++this.position);
64             this.refresh();
65             return val;
66         }
67
68         redo() {
69             if (!this.canRedo) return;
70             let val = this.materialize(--this.position);
71             this.refresh();
72             return val;
73         }
74
75         get canUndo() {
76             return this.stack.length > this.position + 1;
77         }
78
79         get canRedo() {
80             return this.position > 0;
81         }
82     }
83
84
85     class TextUndo extends Undo {
86         stats = {
87             Items: 0,
88             Size: 0,
89             textSize: 0,
90             textItems: 0,
91             diffSize: 0,
92             diffItems: 0,
93             diffChanges: 0,
94         }
95
96         statsFor(item) {
97             if (Array.isArray(item)) {
98                 return {
99                     diffItems: 1,
100                     diffChanges: item.length,
101                     diffSize: JSON.stringify(item).length
102                 }
103             } else {
104                 return {
105                     textItems: 1,
106                     textSize: item.length
107                 }
108             }
109         }
110         addStats(stats) {
111             for (let i in stats) {
112                 this.stats[i] += stats[i]
113             }
114         }
115         subStats(stats) {
116             for (let i in stats) {
117                 this.stats[i] -= stats[i]
118             }
119         }
120         renderStats() {
121             this.stats['Items'] = this.stats['textItems'] + this.stats['diffItems'];
122             this.stats['Size'] = this.stats['textSize'] + this.stats['diffSize'];
123             let stats = '', v;
124             for (let k in this.stats) {
125                 v = this.stats[k];
126                 if (k.endsWith('Size')) {
127                     let level = 0;
128                     while (v > 1000) {
129                         v /= 1000;
130                         level++;
131                     }
132                     v = Math.round(v)
133                     v += ['B', 'kB', 'MB', 'GB'][level];
134                 }
135                 stats += k + ': ' + v + '\n';
136             }
137             return stats;
138         }
139
140
141         put(state) {
142             if (this.stack.length) {
143                 let tip = this.materialize(0);
144                 this.subStats(this.statsFor(this.stack[0]))
145                 this.stack[0] = $.wiki.diff(state, tip);
146                 this.addStats(this.statsFor(this.stack[0]))
147             }
148             this.stack.unshift(state);
149             this.addStats(this.statsFor(state));
150         }
151         pop() {
152             if (this.stack.length > 1) {
153                 this.subStats(this.statsFor(this.stack[1]))
154                 this.stack[1] = this.materialize(1);
155                 this.addStats(this.statsFor(this.stack[1]))
156             }
157             this.subStats(this.statsFor(this.stack[0]))
158             return this.stack.shift();
159         }
160         trim() {
161             while (this.stack.length > this.maxItems) {
162                 this.subStats(this.statsFor(
163                     this.stack.pop()
164                 ));
165             }
166         }
167
168         materialize(n) {
169             if (n >= this.stack.length) return;
170             let state, base_i, i;
171             for (i = 0; i <= n; ++i) {
172                 if (!Array.isArray(this.stack[i])) {
173                     base_i = i;
174                 }
175             }
176             state = this.stack[base_i];
177             for (i = base_i + 1; i <= n; ++i) {
178                 state = $.wiki.patch(state, this.stack[i]);
179             }
180             return state;
181         }
182     }
183
184     $.wiki.undo = new TextUndo();
185 }