Undo
[redakcja.git] / src / redakcja / static / js / wiki / undo.js
diff --git a/src/redakcja/static/js/wiki/undo.js b/src/redakcja/static/js/wiki/undo.js
new file mode 100644 (file)
index 0000000..848085f
--- /dev/null
@@ -0,0 +1,185 @@
+{
+
+    class Undo {
+        maxItems = 100;
+        stack = [];
+        position = 0;
+
+        stats = {size: 0};
+
+        constructor() {
+            $(() => {
+                this.$undo = $("#undoBtn");
+                this.$undo.on('click', () => {CurrentDocument.undo();})
+                this.$redo = $("#redoBtn");
+                this.$redo.on('click', () => {CurrentDocument.redo();})
+                this.$stats = $("#undoStats");
+            })
+        }
+
+        refresh() {
+            this.$undo.prop('disabled', !this.canUndo);
+            this.$redo.prop('disabled', !this.canRedo);
+            this.$undo.attr('title', 'undo\n\n' + this.renderStats())
+        }
+
+        renderStats() {
+            return this.stats.size / 1e6;
+        }
+
+        push(state) {
+            // Has the state actually changed?
+            if (state == this.materialize(this.position))
+                return;
+
+            while (this.position) {
+                this.pop();
+                --this.position;
+            }
+
+            this.put(state);
+            this.trim();
+            this.refresh();
+        }
+        pop() {
+            this.stats.size -= this.stack[0].length;
+            return this.stack.shift()
+        }
+        put(state) {
+            this.stack.unshift(state);
+            this.stats.size += state.length;
+        }
+        trim() {
+            while (this.stack.length > this.maxItems) {
+                this.stats.size -= this.stack.pop().length;
+            }
+        }
+        materialize(n) {
+            return this.stack[n];
+        }
+
+        undo() {
+            if (!this.canUndo) return;
+            let val = this.materialize(++this.position);
+            this.refresh();
+            return val;
+        }
+
+        redo() {
+            if (!this.canRedo) return;
+            let val = this.materialize(--this.position);
+            this.refresh();
+            return val;
+        }
+
+        get canUndo() {
+            return this.stack.length > this.position + 1;
+        }
+
+        get canRedo() {
+            return this.position > 0;
+        }
+    }
+
+
+    class TextUndo extends Undo {
+        stats = {
+            Items: 0,
+            Size: 0,
+            textSize: 0,
+            textItems: 0,
+            diffSize: 0,
+            diffItems: 0,
+            diffChanges: 0,
+        }
+
+        statsFor(item) {
+            if (Array.isArray(item)) {
+                return {
+                    diffItems: 1,
+                    diffChanges: item.length,
+                    diffSize: JSON.stringify(item).length
+                }
+            } else {
+                return {
+                    textItems: 1,
+                    textSize: item.length
+                }
+            }
+        }
+        addStats(stats) {
+            for (let i in stats) {
+                this.stats[i] += stats[i]
+            }
+        }
+        subStats(stats) {
+            for (let i in stats) {
+                this.stats[i] -= stats[i]
+            }
+        }
+        renderStats() {
+            this.stats['Items'] = this.stats['textItems'] + this.stats['diffItems'];
+            this.stats['Size'] = this.stats['textSize'] + this.stats['diffSize'];
+            let stats = '', v;
+            for (let k in this.stats) {
+                v = this.stats[k];
+                if (k.endsWith('Size')) {
+                    let level = 0;
+                    while (v > 1000) {
+                        v /= 1000;
+                        level++;
+                    }
+                    v = Math.round(v)
+                    v += ['B', 'kB', 'MB', 'GB'][level];
+                }
+                stats += k + ': ' + v + '\n';
+            }
+            return stats;
+        }
+
+
+        put(state) {
+            if (this.stack.length) {
+                let tip = this.materialize(0);
+                this.subStats(this.statsFor(this.stack[0]))
+                this.stack[0] = $.wiki.diff(state, tip);
+                this.addStats(this.statsFor(this.stack[0]))
+            }
+            this.stack.unshift(state);
+            this.addStats(this.statsFor(state));
+        }
+        pop() {
+            if (this.stack.length > 1) {
+                this.subStats(this.statsFor(this.stack[1]))
+                this.stack[1] = this.materialize(1);
+                this.addStats(this.statsFor(this.stack[1]))
+            }
+            this.subStats(this.statsFor(this.stack[0]))
+            return this.stack.shift();
+        }
+        trim() {
+            while (this.stack.length > this.maxItems) {
+                this.subStats(this.statsFor(
+                    this.stack.pop()
+                ));
+            }
+        }
+
+        materialize(n) {
+            if (n >= this.stack.length) return;
+            let state, base_i, i;
+            for (i = 0; i <= n; ++i) {
+                if (!Array.isArray(this.stack[i])) {
+                    base_i = i;
+                }
+            }
+            state = this.stack[base_i];
+            for (i = base_i + 1; i <= n; ++i) {
+                state = $.wiki.patch(state, this.stack[i]);
+            }
+            return state;
+        }
+    }
+
+    $.wiki.undo = new TextUndo();
+}