From: Radek Czajka Date: Thu, 20 Jul 2023 15:03:26 +0000 (+0200) Subject: Undo X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/f8eb70938e6ea3b3c4fac5e2f55df26fcaf21b48?ds=sidebyside Undo --- diff --git a/src/redakcja/settings/__init__.py b/src/redakcja/settings/__init__.py index 45c5e508..ce3289b0 100644 --- a/src/redakcja/settings/__init__.py +++ b/src/redakcja/settings/__init__.py @@ -199,6 +199,8 @@ PIPELINE = { # base UI 'js/wiki/base.js', 'js/wiki/toolbar.js', + 'js/lib/diff.js', + 'js/wiki/undo.js', # dialogs 'js/wiki/dialog_save.js', diff --git a/src/redakcja/static/js/lib/diff.js b/src/redakcja/static/js/lib/diff.js new file mode 100644 index 00000000..4dda2740 --- /dev/null +++ b/src/redakcja/static/js/lib/diff.js @@ -0,0 +1,86 @@ +$.wiki.diff = function(a, b) { + MAXD = 500; + + let VV = new Array(), + N = a.length, + M = b.length, + V, Vp, D, x, y, k; + V = VV[-1] = Array(); + V[1] = 0; + let endD = null; + for (D = 0; D < MAXD && endD === null; D++) { + Vp = V; + V = VV[D] = Array(); + for (k = -D; k <= D; k += 2) { + if (k == -D || (k != D && Vp[k-1] < Vp[k + 1])) { + x = Vp[k + 1]; + } else { + x = Vp[k - 1] + 1; + } + y = x - k; + while (x < N && y < M && a[x] == b[y]) { + x ++; + y ++; + } + V[k] = x; + if (x == N && y == M) { + endD = D; + break; + } + } + } + if (endD === null) { + // Max D limit reached, diff too big. Bail and just return the whole target text. + return b; + } + + // Now go back. + result = [] + let snake, px, py; + for (D = endD; D; --D) { + k = x - y; + V = VV[D - 1]; + if (V[k - 1] === undefined || V[k + 1] > V[k - 1]) { + // move up + k ++; + px = V[k]; + py = px - k; + if (result.length && result[0][0] && result[0][1] == px) { + result[0][2] = b[py] + result[0][2]; + } else { + result.unshift( + [true, px, b[py]] + ) + } + } else { + // move down + k --; + px = V[k]; + py = px - k; + if (result.length && !result[0][0] && result[0][1] == px + 1) { + result[0][1]--; + result[0][2]++; + } else { + result.unshift( + [false, px, 1] + ) + } + } + x = px; + y = py; + } + return result +} + + +$.wiki.patch = function(a, p) { + for (i = p.length - 1; i >= 0; -- i) { + let c = p[i]; + if (c[0]) { + a = a.substr(0, c[1]) + c[2] + a.substr(c[1]); + } else { + a = a.substr(0, c[1]) + a.substr(c[1] + c[2]); + } + } + return a; +} diff --git a/src/redakcja/static/js/wiki/base.js b/src/redakcja/static/js/wiki/base.js index 4d3a062f..d9141687 100644 --- a/src/redakcja/static/js/wiki/base.js +++ b/src/redakcja/static/js/wiki/base.js @@ -103,7 +103,7 @@ $.wiki.exitTab = function(tab){ var self = this; var $tab = $(tab); - if (!('.active', $tab)) return; + if (!('.active', $tab).length) return; $('.active', $tab).removeClass('active'); self.perspectives[$tab.attr('id')].onExit(); $('#' + $tab.attr('data-ui-related')).hide(); diff --git a/src/redakcja/static/js/wiki/caret.js b/src/redakcja/static/js/wiki/caret.js index 0e34f34d..cedf8aa0 100644 --- a/src/redakcja/static/js/wiki/caret.js +++ b/src/redakcja/static/js/wiki/caret.js @@ -37,6 +37,7 @@ class Caret { self.singleClick = true; setTimeout(function() { if (self.singleClick) { + $.wiki.activePerspective().flush(); self.element.insertBefore( anchorNode.splitText( selection.anchorOffset @@ -208,6 +209,8 @@ class Caret { move(opts) { if (!this.attached) return; + + $.wiki.activePerspective().flush(); this.normalize(); diff --git a/src/redakcja/static/js/wiki/loader_readonly.js b/src/redakcja/static/js/wiki/loader_readonly.js index 43b8c9b8..2d311239 100644 --- a/src/redakcja/static/js/wiki/loader_readonly.js +++ b/src/redakcja/static/js/wiki/loader_readonly.js @@ -72,22 +72,8 @@ $(function() /* Load configuration */ $.wiki.loadConfig(); - var initAll = function(a, f) { - if (a.length == 0) return f(); - - $.wiki.initTab({ - tab: a.pop(), - doc: CurrentDocument, - callback: function(){ - initAll(a, f); - } - }); - }; - - - /* - * Initialize all perspectives - */ - initAll( $.makeArray($('#tabs li')), initialize); - console.log(location.hash); + $('.tabs li').each((i, e) => { + $.wiki.initTab({tab: e, doc: CurrentDocument}); + }); + initialize(); }); 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(); +} diff --git a/src/redakcja/static/js/wiki/view_editor_wysiwyg.js b/src/redakcja/static/js/wiki/view_editor_wysiwyg.js index f39f7a87..decc7339 100644 --- a/src/redakcja/static/js/wiki/view_editor_wysiwyg.js +++ b/src/redakcja/static/js/wiki/view_editor_wysiwyg.js @@ -526,6 +526,7 @@ $origin.html($(element).html()); } $overlay.remove(); + $.wiki.activePerspective().flush(); }, error: function(text){ alert('Błąd! ' + text); @@ -639,36 +640,43 @@ self.caret = new Caret(element); $('#insert-reference-button').click(function(){ + self.flush(); self.addReference(); return false; }); $('#insert-annotation-button').click(function(){ + self.flush(); addAnnotation(); return false; }); $('#insert-theme-button').click(function(){ + self.flush(); addTheme(); return false; }); $(".insert-inline-tag").click(function() { + self.flush(); self.insertInlineTag($(this).attr('data-tag')); return false; }); $(".insert-char").click(function() { + self.flush(); addSymbol(caret=self.caret); return false; }); $(document).on('click', '.edit-button', function(event){ + self.flush(); event.preventDefault(); openForEdit($(this).parent()); }); $(document).on('click', '.uwaga-button', function(event){ + self.flush(); event.preventDefault(); createUwagaBefore($(this).parent()); }); @@ -679,6 +687,7 @@ }); element.on('click', '.annotation', function(event) { + self.flush(); event.preventDefault(); event.redakcja_caret_ignore = true; $('[x-annotation-box]', $(this).parent()).toggleClass('editing'); @@ -706,6 +715,8 @@ var htmlView = $('#html-view'); htmlView.html(element); + if ('PropertiesPerspective' in $.wiki.perspectives) + $.wiki.perspectives.PropertiesPerspective.enable(); _finalize(success); }, @@ -717,44 +728,51 @@ _finalize(failure); } }); - }; + } + + flush() { + let self = this; + return new Promise((resolve, reject) => { + if ($('#html-view .error').length > 0) { + reject() + } else { + //return _finalize(failure); + html2text({ + element: $('#html-view').get(0), + stripOuter: true, + success: (text) => { + self.doc.setText(text); + resolve(); + }, + error: (text) => { + reject(text); + //$('#source-editor').html('

Wystąpił błąd:

' + text + '
'); + } + }); + } + }); + } onExit(success, failure) { var self = this; self.caret.detach(); - $.wiki.exitTab('#PropertiesPerspective'); - - $.blockUI({ - message: 'Zapisywanie widoku...' - }); - - function _finalize(callback){ - $.unblockUI(); - if (callback) - callback(); - } - - if ($('#html-view .error').length > 0) - return _finalize(failure); + if ('PropertiesPerspective' in $.wiki.perspectives) + $.wiki.perspectives.PropertiesPerspective.disable(); - html2text({ - element: $('#html-view').get(0), - stripOuter: true, - success: function(text){ - self.doc.setText(text); - _finalize(success); - }, - error: function(text){ - $('#source-editor').html('

Wystąpił błąd:

' + text + '
'); - _finalize(failure); - } + self.flush().then(() => { + success && success(); + }).catch((e) => { + // TODO report + console.log('REJECTED!', e); + failure && failure(); }); }; insertInlineTag(tag) { this.caret.detach(); + let self = this; let selection = window.getSelection(); var n = selection.rangeCount; @@ -824,6 +842,7 @@ success: function(html) { // What if no end? node.insertBefore($(html)[0], end); + self.flush(); } }); }, diff --git a/src/redakcja/static/js/wiki/view_properties.js b/src/redakcja/static/js/wiki/view_properties.js index 5c4c0e8b..a3fe4323 100644 --- a/src/redakcja/static/js/wiki/view_properties.js +++ b/src/redakcja/static/js/wiki/view_properties.js @@ -40,6 +40,7 @@ class PropertiesPerspective extends $.wiki.SidebarPerspective { vsplitbar = 'WŁAŚCIWOŚCI'; + $edited = null; constructor(options) { super(options); @@ -78,6 +79,7 @@ } else { $input.data("edited").text(inputval); } + $.wiki.perspectives.VisualPerspective.flush(); return; } @@ -99,6 +101,7 @@ let htmlElem = $(html); self.$edited.replaceWith(htmlElem); self.edit(htmlElem); + $.wiki.activePerspective().flush(); } }); }, @@ -148,6 +151,7 @@ let $fg = $(this).closest('.form-group'); $('input', $fg).data('edited').remove(); self.displayMetaProperty($fg); + $.wiki.perspectives.VisualPerspective.flush(); return false; }); @@ -192,6 +196,7 @@ if (element === null) { self.$edited = null; + $("h1", self.$pane).text(''); return; } @@ -401,10 +406,17 @@ if ($.wiki.activePerspective() != 'VisualPerspective') $.wiki.switchToTab('#VisualPerspective'); - if (self.$edited === null) { - self.edit($('[x-node="utwor"]')[0]); + this.enable(); + } + + enable() { + if (this.$edited === null) { + this.edit($('[x-node="utwor"]')[0]); } } + disable() { + this.edit(null); + } } $.wiki.PropertiesPerspective = PropertiesPerspective; diff --git a/src/redakcja/static/js/wiki/wikiapi.js b/src/redakcja/static/js/wiki/wikiapi.js index 53b2a9da..1bf6ebb1 100644 --- a/src/redakcja/static/js/wiki/wikiapi.js +++ b/src/redakcja/static/js/wiki/wikiapi.js @@ -150,6 +150,7 @@ if (self.text === null || self.revision !== data.revision) { self.text = data.text; + $.wiki.undo.push(data.text); self.revision = data.revision; self.gallery = data.gallery; changed = true; @@ -288,12 +289,32 @@ /* * Set document's text */ - setText(text) { + setText(text, silent=false) { if (text == this.text) return; + if (!silent) { + $.wiki.undo.push(text); + } this.text = text; this.has_local_changes = true; } + undo() { + let ctx = $.wiki.exitContext(); + this.setText( + $.wiki.undo.undo(), + true + ); + $.wiki.enterContext(ctx); + } + redo() { + let ctx = $.wiki.exitContext(); + this.setText( + $.wiki.undo.redo(), + true + ); + $.wiki.enterContext(ctx); + } + /* * Save text back to the server */ diff --git a/src/wiki/templates/wiki/document_details.html b/src/wiki/templates/wiki/document_details.html index a4cbe4a9..214940e0 100644 --- a/src/wiki/templates/wiki/document_details.html +++ b/src/wiki/templates/wiki/document_details.html @@ -21,6 +21,13 @@ {% include "wiki/tabs/history_view_item.html" %} {% endblock %} +{% block tools-menu %} +
+ + +
+{% endblock %} + {% block tabs-content %} {% include "wiki/tabs/summary_view.html" %} {% include "wiki/tabs/wysiwyg_editor.html" %} diff --git a/src/wiki/templates/wiki/document_details_base.html b/src/wiki/templates/wiki/document_details_base.html index 2b3ee91a..18290a12 100644 --- a/src/wiki/templates/wiki/document_details_base.html +++ b/src/wiki/templates/wiki/document_details_base.html @@ -43,6 +43,9 @@ {% block tabs-menu %}{% endblock %} + {% block tools-menu %} + {% endblock %} +