X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/573b9004c7bc2a6a2b29335155b4ff55d142c6b6..f8eb70938e6ea3b3c4fac5e2f55df26fcaf21b48:/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 index 00000000..848085fa --- /dev/null +++ b/src/redakcja/static/js/wiki/undo.js @@ -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(); +}