Undo
authorRadek Czajka <rczajka@rczajka.pl>
Thu, 20 Jul 2023 15:03:26 +0000 (17:03 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Thu, 20 Jul 2023 15:03:26 +0000 (17:03 +0200)
src/redakcja/settings/__init__.py
src/redakcja/static/js/lib/diff.js [new file with mode: 0644]
src/redakcja/static/js/wiki/base.js
src/redakcja/static/js/wiki/caret.js
src/redakcja/static/js/wiki/loader_readonly.js
src/redakcja/static/js/wiki/undo.js [new file with mode: 0644]
src/redakcja/static/js/wiki/view_editor_wysiwyg.js
src/redakcja/static/js/wiki/view_properties.js
src/redakcja/static/js/wiki/wikiapi.js
src/wiki/templates/wiki/document_details.html
src/wiki/templates/wiki/document_details_base.html

index 45c5e50..ce3289b 100644 (file)
@@ -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 (file)
index 0000000..4dda274
--- /dev/null
@@ -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;
+}
index 4d3a062..d914168 100644 (file)
     $.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();
index 0e34f34..cedf8aa 100644 (file)
@@ -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();
         
index 43b8c9b..2d31123 100644 (file)
@@ -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 (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();
+}
index f39f7a8..decc733 100644 (file)
                                 $origin.html($(element).html());
                             }
                             $overlay.remove();
+                            $.wiki.activePerspective().flush();
                         },
                         error: function(text){
                             alert('Błąd! ' + text);
                 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());
                 });
             });
 
             element.on('click', '.annotation', function(event) {
+                self.flush();
                 event.preventDefault();
                 event.redakcja_caret_ignore = true;
                 $('[x-annotation-box]', $(this).parent()).toggleClass('editing');
 
                     var htmlView = $('#html-view');
                     htmlView.html(element);
+                    if ('PropertiesPerspective' in $.wiki.perspectives)
+                        $.wiki.perspectives.PropertiesPerspective.enable();
 
                     _finalize(success);
                 },
                     _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('<p>Wystąpił błąd:</p><pre>' + text + '</pre>');
+                        }
+                    });
+                }
+            });
+        }
 
         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('<p>Wystąpił błąd:</p><pre>' + text + '</pre>');
-                    _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;
                         success: function(html) {
                             // What if no end?
                             node.insertBefore($(html)[0], end);
+                            self.flush();
                         }
                     });
                 },
index 5c4c0e8..a3fe432 100644 (file)
@@ -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;
                     }
 
                                     let htmlElem = $(html);
                                     self.$edited.replaceWith(htmlElem);
                                     self.edit(htmlElem);
+                                    $.wiki.activePerspective().flush();
                                 }
                             });
                         },
                     let $fg = $(this).closest('.form-group');
                     $('input', $fg).data('edited').remove();
                     self.displayMetaProperty($fg);
+                    $.wiki.perspectives.VisualPerspective.flush();
                     return false;
                 });
 
 
             if (element === null) {
                 self.$edited = null;
+                $("h1", self.$pane).text('');
                 return;
             }
 
             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;
 
index 53b2a9d..1bf6ebb 100644 (file)
 
                    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;
         /*
          * 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
          */
index a4cbe4a..214940e 100644 (file)
   {% include "wiki/tabs/history_view_item.html" %}
 {% endblock %}
 
+{% block tools-menu %}
+  <div class="btn-group mr-auto">
+    <button id="undoBtn" class="btn btn-secondary" title="undo">↺</button>
+    <button id="redoBtn" class="btn btn-secondary" title="redo">↻</button>
+  </div>
+{% endblock %}
+
 {% block tabs-content %}
   {% include "wiki/tabs/summary_view.html" %}
   {% include "wiki/tabs/wysiwyg_editor.html" %}
index 2b3ee91..18290a1 100644 (file)
@@ -43,6 +43,9 @@
       {% block tabs-menu %}{% endblock %}
     </ul>
 
+    {% block tools-menu %}
+    {% endblock %}
+
     <ul class="tabs nav nav-tabs" id="tabs-right">
       {% block tabs-right %}{% endblock %}
     </ul>