Convert `Ui/Redactor/Autosave` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Tue, 3 Nov 2020 23:44:43 +0000 (00:44 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 3 Nov 2020 23:44:43 +0000 (00:44 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Autosave.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Autosave.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Autosave.ts [new file with mode: 0644]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts

index e2fb5368f9e4af5e2e5d24779d68f38cc3b38708..3a313f801afb781637b49501cc547b3f36ed9b03 100644 (file)
@@ -2,75 +2,57 @@
  * Manages the autosave process storing the current editor message in the local
  * storage to recover it on browser crash or accidental navigation.
  *
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/Redactor/Autosave
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Redactor/Autosave
  */
-define(['Core', 'Devtools', 'EventHandler', 'Language', 'Dom/Traverse', './Metacode'], function (Core, Devtools, EventHandler, Language, DomTraverse, UiRedactorMetacode) {
+define(["require", "exports", "tslib", "../../Core", "../../Devtools", "../../Event/Handler", "../../Language", "./Metacode"], function (require, exports, tslib_1, Core, Devtools_1, EventHandler, Language, UiRedactorMetacode) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            init: function () { },
-            getInitialValue: function () { },
-            getMetaData: function () { },
-            watch: function () { },
-            destroy: function () { },
-            clear: function () { },
-            createOverlay: function () { },
-            hideOverlay: function () { },
-            _saveToStorage: function () { },
-            _cleanup: function () { }
-        };
-        return Fake;
-    }
+    Core = tslib_1.__importStar(Core);
+    Devtools_1 = tslib_1.__importDefault(Devtools_1);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    Language = tslib_1.__importStar(Language);
+    UiRedactorMetacode = tslib_1.__importStar(UiRedactorMetacode);
     // time between save requests in seconds
-    var _frequency = 15;
-    /**
-     * @param       {Element}       element         textarea element
-     * @constructor
-     */
-    function UiRedactorAutosave(element) { this.init(element); }
-    UiRedactorAutosave.prototype = {
+    const _frequency = 15;
+    class UiRedactorAutosave {
         /**
          * Initializes the autosave handler and removes outdated messages from storage.
          *
          * @param       {Element}       element         textarea element
          */
-        init: function (element) {
+        constructor(element) {
             this._container = null;
-            this._metaData = {};
             this._editor = null;
-            this._element = element;
             this._isActive = true;
             this._isPending = false;
-            this._key = Core.getStoragePrefix() + elData(this._element, 'autosave');
-            this._lastMessage = '';
-            this._originalMessage = '';
-            this._overlay = null;
+            this._lastMessage = "";
+            this._metaData = {};
+            this._originalMessage = "";
             this._restored = false;
             this._timer = null;
+            this._element = element;
+            this._key = Core.getStoragePrefix() + this._element.dataset.autosave;
+            //this._overlay = null;
             this._cleanup();
             // remove attribute to prevent Redactor's built-in autosave to kick in
-            this._element.removeAttribute('data-autosave');
-            var form = DomTraverse.parentByTag(this._element, 'FORM');
+            delete this._element.dataset.autosave;
+            const form = this._element.closest("form");
             if (form !== null) {
-                form.addEventListener('submit', this.destroy.bind(this));
+                form.addEventListener("submit", this.destroy.bind(this));
             }
             // export meta data
-            EventHandler.add('com.woltlab.wcf.redactor2', 'getMetaData_' + this._element.id, (function (data) {
-                for (var key in this._metaData) {
-                    if (this._metaData.hasOwnProperty(key)) {
-                        data[key] = this._metaData[key];
-                    }
-                }
-            }).bind(this));
+            EventHandler.add("com.woltlab.wcf.redactor2", `getMetaData_${this._element.id}`, (data) => {
+                Object.entries(this._metaData).forEach(([key, value]) => {
+                    data[key] = value;
+                });
+            });
             // clear editor content on reset
-            EventHandler.add('com.woltlab.wcf.redactor2', 'reset_' + this._element.id, this.hideOverlay.bind(this));
-            document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this));
-        },
-        _onVisibilityChange: function () {
+            EventHandler.add("com.woltlab.wcf.redactor2", `reset_${this._element.id}`, () => this.hideOverlay());
+            document.addEventListener("visibilitychange", () => this._onVisibilityChange());
+        }
+        _onVisibilityChange() {
             if (document.hidden) {
                 this._isActive = false;
                 this._isPending = true;
@@ -79,241 +61,234 @@ define(['Core', 'Devtools', 'EventHandler', 'Language', 'Dom/Traverse', './Metac
                 this._isActive = true;
                 this._isPending = false;
             }
-        },
+        }
         /**
          * Returns the initial value for the textarea, used to inject message
          * from storage into the editor before initialization.
          *
          * @return      {string}        message content
          */
-        getInitialValue: function () {
-            //noinspection JSUnresolvedVariable
-            if (window.ENABLE_DEVELOPER_TOOLS && Devtools._internal_.editorAutosave() === false) {
-                //noinspection JSUnresolvedVariable
+        getInitialValue() {
+            if (window.ENABLE_DEVELOPER_TOOLS && !Devtools_1.default._internal_.editorAutosave()) {
                 return this._element.value;
             }
-            var value = '';
+            let value = "";
             try {
-                value = window.localStorage.getItem(this._key);
+                value = window.localStorage.getItem(this._key) || "";
             }
             catch (e) {
-                window.console.warn("Unable to access local storage: " + e.message);
+                const errorMessage = e.message;
+                window.console.warn(`Unable to access local storage: ${errorMessage}`);
             }
+            let metaData = null;
             try {
-                value = JSON.parse(value);
+                metaData = JSON.parse(value);
             }
             catch (e) {
-                value = '';
+                // We do not care for JSON errors.
             }
             // Check if the storage is outdated.
-            if (value !== null && typeof value === 'object' && value.content) {
-                var lastEditTime = ~~elData(this._element, 'autosave-last-edit-time');
-                if (lastEditTime * 1000 <= value.timestamp) {
+            if (metaData !== null && typeof metaData === "object" && metaData.content) {
+                const lastEditTime = ~~this._element.dataset.autosaveLastEditTime;
+                if (lastEditTime * 1000 <= metaData.timestamp) {
                     // Compare the stored version with the editor content, but only use the `innerText` property
                     // in order to ignore differences in whitespace, e. g. caused by indentation of HTML tags.
-                    var div1 = elCreate('div');
+                    const div1 = document.createElement("div");
                     div1.innerHTML = this._element.value;
-                    var div2 = elCreate('div');
-                    div2.innerHTML = value.content;
+                    const div2 = document.createElement("div");
+                    div2.innerHTML = metaData.content;
                     if (div1.innerText.trim() !== div2.innerText.trim()) {
-                        //noinspection JSUnresolvedVariable
                         this._originalMessage = this._element.value;
                         this._restored = true;
-                        this._metaData = value.meta || {};
-                        return value.content;
+                        this._metaData = metaData.meta || {};
+                        return metaData.content;
                     }
                 }
             }
-            //noinspection JSUnresolvedVariable
             return this._element.value;
-        },
+        }
         /**
          * Returns the stored meta data.
-         *
-         * @return      {Object}
          */
-        getMetaData: function () {
+        getMetaData() {
             return this._metaData;
-        },
+        }
         /**
          * Enables periodical save of editor contents to local storage.
-         *
-         * @param       {$.Redactor}    editor  redactor instance
          */
-        watch: function (editor) {
+        watch(editor) {
             this._editor = editor;
             if (this._timer !== null) {
                 throw new Error("Autosave timer is already active.");
             }
-            this._timer = window.setInterval(this._saveToStorage.bind(this), _frequency * 1000);
+            this._timer = window.setInterval(() => this._saveToStorage(), _frequency * 1000);
             this._saveToStorage();
             this._isPending = false;
-        },
+        }
         /**
          * Disables autosave handler, for use on editor destruction.
          */
-        destroy: function () {
+        destroy() {
             this.clear();
             this._editor = null;
-            window.clearInterval(this._timer);
+            if (this._timer) {
+                window.clearInterval(this._timer);
+            }
             this._timer = null;
             this._isPending = false;
-        },
+        }
         /**
          * Removed the stored message, for use after a message has been submitted.
          */
-        clear: function () {
+        clear() {
             this._metaData = {};
-            this._lastMessage = '';
+            this._lastMessage = "";
             try {
                 window.localStorage.removeItem(this._key);
             }
             catch (e) {
-                window.console.warn("Unable to remove from local storage: " + e.message);
+                const errorMessage = e.message;
+                window.console.warn(`Unable to remove from local storage: ${errorMessage}`);
             }
-        },
+        }
         /**
          * Creates the autosave controls, used to keep or discard the restored draft.
          */
-        createOverlay: function () {
+        createOverlay() {
             if (!this._restored) {
                 return;
             }
-            var container = elCreate('div');
-            container.className = 'redactorAutosaveRestored active';
-            var title = elCreate('span');
-            title.textContent = Language.get('wcf.editor.autosave.restored');
+            const editor = this._editor;
+            const container = document.createElement("div");
+            container.className = "redactorAutosaveRestored active";
+            const title = document.createElement("span");
+            title.textContent = Language.get("wcf.editor.autosave.restored");
             container.appendChild(title);
-            var button = elCreate('a');
-            button.className = 'jsTooltip';
-            button.href = '#';
-            button.title = Language.get('wcf.editor.autosave.keep');
-            button.innerHTML = '<span class="icon icon16 fa-check green"></span>';
-            button.addEventListener('click', (function (event) {
+            const buttonKeep = document.createElement("a");
+            buttonKeep.className = "jsTooltip";
+            buttonKeep.href = "#";
+            buttonKeep.title = Language.get("wcf.editor.autosave.keep");
+            buttonKeep.innerHTML = '<span class="icon icon16 fa-check green"></span>';
+            buttonKeep.addEventListener("click", (event) => {
                 event.preventDefault();
                 this.hideOverlay();
-            }).bind(this));
-            container.appendChild(button);
-            button = elCreate('a');
-            button.className = 'jsTooltip';
-            button.href = '#';
-            button.title = Language.get('wcf.editor.autosave.discard');
-            button.innerHTML = '<span class="icon icon16 fa-times red"></span>';
-            button.addEventListener('click', (function (event) {
+            });
+            container.appendChild(buttonKeep);
+            const buttonDiscard = document.createElement("a");
+            buttonDiscard.className = "jsTooltip";
+            buttonDiscard.href = "#";
+            buttonDiscard.title = Language.get("wcf.editor.autosave.discard");
+            buttonDiscard.innerHTML = '<span class="icon icon16 fa-times red"></span>';
+            buttonDiscard.addEventListener("click", (event) => {
                 event.preventDefault();
                 // remove from storage
                 this.clear();
                 // set code
-                var content = UiRedactorMetacode.convertFromHtml(this._editor.core.element()[0].id, this._originalMessage);
-                this._editor.code.start(content);
+                const content = UiRedactorMetacode.convertFromHtml(editor.core.element()[0].id, this._originalMessage);
+                editor.code.start(content);
                 // set value
-                this._editor.core.textarea().val(this._editor.clean.onSync(this._editor.$editor.html()));
-                this.hideOverlay();
-            }).bind(this));
-            container.appendChild(button);
-            this._editor.core.box()[0].appendChild(container);
-            var callback = (function () {
-                this._editor.core.editor()[0].removeEventListener('click', callback);
+                editor.core.textarea().val(editor.clean.onSync(editor.$editor.html()));
                 this.hideOverlay();
-            }).bind(this);
-            this._editor.core.editor()[0].addEventListener('click', callback);
+            });
+            container.appendChild(buttonDiscard);
+            editor.core.box()[0].appendChild(container);
+            editor.core.editor()[0].addEventListener("click", () => this.hideOverlay(), { once: true });
             this._container = container;
-        },
+        }
         /**
          * Hides the autosave controls.
          */
-        hideOverlay: function () {
+        hideOverlay() {
             if (this._container !== null) {
-                this._container.classList.remove('active');
-                window.setTimeout((function () {
+                this._container.classList.remove("active");
+                window.setTimeout(() => {
                     if (this._container !== null) {
-                        elRemove(this._container);
+                        this._container.remove();
                     }
                     this._container = null;
-                    this._originalMessage = '';
-                }).bind(this), 1000);
+                    this._originalMessage = "";
+                }, 1000);
             }
-        },
+        }
         /**
          * Saves the current message to storage unless there was no change.
-         *
-         * @protected
          */
-        _saveToStorage: function () {
+        _saveToStorage() {
             if (!this._isActive) {
-                if (!this._isPending)
+                if (!this._isPending) {
                     return;
+                }
                 // save one last time before suspending
                 this._isPending = false;
             }
             //noinspection JSUnresolvedVariable
-            if (window.ENABLE_DEVELOPER_TOOLS && Devtools._internal_.editorAutosave() === false) {
-                //noinspection JSUnresolvedVariable
+            if (window.ENABLE_DEVELOPER_TOOLS && !Devtools_1.default._internal_.editorAutosave()) {
                 return;
             }
-            var content = this._editor.code.get();
-            if (this._editor.utils.isEmpty(content)) {
-                content = '';
+            const editor = this._editor;
+            let content = editor.code.get();
+            if (editor.utils.isEmpty(content)) {
+                content = "";
             }
             if (this._lastMessage === content) {
                 // break if content hasn't changed
                 return;
             }
-            if (content === '') {
+            if (content === "") {
                 return this.clear();
             }
             try {
-                EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveMetaData_' + this._element.id, this._metaData);
+                EventHandler.fire("com.woltlab.wcf.redactor2", `autosaveMetaData_${this._element.id}`, this._metaData);
                 window.localStorage.setItem(this._key, JSON.stringify({
                     content: content,
                     meta: this._metaData,
-                    timestamp: Date.now()
+                    timestamp: Date.now(),
                 }));
                 this._lastMessage = content;
             }
             catch (e) {
-                window.console.warn("Unable to write to local storage: " + e.message);
+                const errorMessage = e.message;
+                window.console.warn(`Unable to write to local storage: ${errorMessage}`);
             }
-        },
+        }
         /**
          * Removes stored messages older than one week.
-         *
-         * @protected
          */
-        _cleanup: function () {
-            var oneWeekAgo = Date.now() - (7 * 24 * 3600 * 1000), removeKeys = [];
-            var i, key, length, value;
-            for (i = 0, length = window.localStorage.length; i < length; i++) {
-                key = window.localStorage.key(i);
+        _cleanup() {
+            const oneWeekAgo = Date.now() - 7 * 24 * 3600 * 1000;
+            Object.keys(window.localStorage).forEach((key) => {
                 // check if key matches our prefix
-                if (key.indexOf(Core.getStoragePrefix()) !== 0) {
-                    continue;
+                if (!key.startsWith(Core.getStoragePrefix())) {
+                    return;
                 }
+                let value = "";
                 try {
-                    value = window.localStorage.getItem(key);
+                    value = window.localStorage.getItem(key) || "";
                 }
                 catch (e) {
-                    window.console.warn("Unable to access local storage: " + e.message);
+                    const errorMessage = e.message;
+                    window.console.warn(`Unable to access local storage: ${errorMessage}`);
                 }
+                let timestamp = 0;
                 try {
-                    value = JSON.parse(value);
+                    const content = JSON.parse(value);
+                    timestamp = content.timestamp;
                 }
                 catch (e) {
-                    value = { timestamp: 0 };
+                    // We do not care for JSON errors.
                 }
-                if (!value || value.timestamp < oneWeekAgo) {
-                    removeKeys.push(key);
-                }
-            }
-            for (i = 0, length = removeKeys.length; i < length; i++) {
-                try {
-                    window.localStorage.removeItem(removeKeys[i]);
-                }
-                catch (e) {
-                    window.console.warn("Unable to remove from local storage: " + e.message);
+                if (!value || timestamp < oneWeekAgo) {
+                    try {
+                        window.localStorage.removeItem(key);
+                    }
+                    catch (e) {
+                        const errorMessage = e.message;
+                        window.console.warn(`Unable to remove from local storage: ${errorMessage}`);
+                    }
                 }
-            }
+            });
         }
-    };
+    }
+    Core.enableLegacyInheritance(UiRedactorAutosave);
     return UiRedactorAutosave;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Autosave.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Autosave.js
deleted file mode 100644 (file)
index 6712e0b..0000000
+++ /dev/null
@@ -1,380 +0,0 @@
-/**
- * Manages the autosave process storing the current editor message in the local
- * storage to recover it on browser crash or accidental navigation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/Redactor/Autosave
- */
-define(['Core', 'Devtools', 'EventHandler', 'Language', 'Dom/Traverse', './Metacode'], function(Core, Devtools, EventHandler, Language, DomTraverse, UiRedactorMetacode) {
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               var Fake = function() {};
-               Fake.prototype = {
-                       init: function() {},
-                       getInitialValue: function() {},
-                       getMetaData: function () {},
-                       watch: function() {},
-                       destroy: function() {},
-                       clear: function() {},
-                       createOverlay: function() {},
-                       hideOverlay: function() {},
-                       _saveToStorage: function() {},
-                       _cleanup: function() {}
-               };
-               return Fake;
-       }
-       
-       // time between save requests in seconds
-       var _frequency = 15;
-       
-       /**
-        * @param       {Element}       element         textarea element
-        * @constructor
-        */
-       function UiRedactorAutosave(element) { this.init(element); }
-       UiRedactorAutosave.prototype = {
-               /**
-                * Initializes the autosave handler and removes outdated messages from storage.
-                * 
-                * @param       {Element}       element         textarea element
-                */
-               init: function (element) {
-                       this._container = null;
-                       this._metaData = {};
-                       this._editor = null;
-                       this._element = element;
-                       this._isActive = true;
-                       this._isPending = false;
-                       this._key = Core.getStoragePrefix() + elData(this._element, 'autosave');
-                       this._lastMessage = '';
-                       this._originalMessage = '';
-                       this._overlay = null;
-                       this._restored = false;
-                       this._timer = null;
-                       
-                       this._cleanup();
-                       
-                       // remove attribute to prevent Redactor's built-in autosave to kick in
-                       this._element.removeAttribute('data-autosave');
-                       
-                       var form = DomTraverse.parentByTag(this._element, 'FORM');
-                       if (form !== null) {
-                               form.addEventListener('submit', this.destroy.bind(this));
-                       }
-                       
-                       // export meta data
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'getMetaData_' + this._element.id, (function (data) {
-                               for (var key in this._metaData) {
-                                       if (this._metaData.hasOwnProperty(key)) {
-                                               data[key] = this._metaData[key];
-                                       }
-                               }
-                       }).bind(this));
-                       
-                       // clear editor content on reset
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'reset_' + this._element.id, this.hideOverlay.bind(this));
-                       
-                       document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this));
-               },
-               
-               _onVisibilityChange: function () {
-                       if (document.hidden) {
-                               this._isActive = false;
-                               this._isPending = true;
-                       }
-                       else {
-                               this._isActive = true;
-                               this._isPending = false;
-                       }
-               },
-               
-               /**
-                * Returns the initial value for the textarea, used to inject message
-                * from storage into the editor before initialization.
-                * 
-                * @return      {string}        message content
-                */
-               getInitialValue: function() {
-                       //noinspection JSUnresolvedVariable
-                       if (window.ENABLE_DEVELOPER_TOOLS && Devtools._internal_.editorAutosave() === false) {
-                               //noinspection JSUnresolvedVariable
-                               return this._element.value;
-                       }
-                       
-                       var value = '';
-                       try {
-                               value = window.localStorage.getItem(this._key);
-                       }
-                       catch (e) {
-                               window.console.warn("Unable to access local storage: " + e.message);
-                       }
-                       
-                       try {
-                               value = JSON.parse(value);
-                       }
-                       catch (e) {
-                               value = '';
-                       }
-                       
-                       // Check if the storage is outdated.
-                       if (value !== null && typeof value === 'object' && value.content) {
-                               var lastEditTime = ~~elData(this._element, 'autosave-last-edit-time');
-                               if (lastEditTime * 1000 <= value.timestamp) {
-                                       // Compare the stored version with the editor content, but only use the `innerText` property
-                                       // in order to ignore differences in whitespace, e. g. caused by indentation of HTML tags.
-                                       var div1 = elCreate('div');
-                                       div1.innerHTML = this._element.value;
-                                       var div2 = elCreate('div');
-                                       div2.innerHTML = value.content;
-                                       
-                                       if (div1.innerText.trim() !== div2.innerText.trim()) {
-                                               //noinspection JSUnresolvedVariable
-                                               this._originalMessage = this._element.value;
-                                               this._restored = true;
-                                               
-                                               this._metaData = value.meta || {};
-                                               
-                                               return value.content;
-                                       }
-                               }
-                       }
-                       
-                       //noinspection JSUnresolvedVariable
-                       return this._element.value;
-               },
-               
-               /**
-                * Returns the stored meta data.
-                * 
-                * @return      {Object}
-                */
-               getMetaData: function () {
-                       return this._metaData;
-               },
-               
-               /**
-                * Enables periodical save of editor contents to local storage.
-                * 
-                * @param       {$.Redactor}    editor  redactor instance
-                */
-               watch: function(editor) {
-                       this._editor = editor;
-                       
-                       if (this._timer !== null) {
-                               throw new Error("Autosave timer is already active.");
-                       }
-                       
-                       this._timer = window.setInterval(this._saveToStorage.bind(this), _frequency * 1000);
-                       
-                       this._saveToStorage();
-                       
-                       this._isPending = false;
-               },
-               
-               /**
-                * Disables autosave handler, for use on editor destruction.
-                */
-               destroy: function () {
-                       this.clear();
-                       
-                       this._editor = null;
-                       
-                       window.clearInterval(this._timer);
-                       this._timer = null;
-                       this._isPending = false;
-               },
-               
-               /**
-                * Removed the stored message, for use after a message has been submitted.
-                */
-               clear: function () {
-                       this._metaData = {};
-                       this._lastMessage = '';
-                       
-                       try {
-                               window.localStorage.removeItem(this._key);
-                       }
-                       catch (e) {
-                               window.console.warn("Unable to remove from local storage: " + e.message);
-                       }
-               },
-               
-               /**
-                * Creates the autosave controls, used to keep or discard the restored draft.
-                */
-               createOverlay: function () {
-                       if (!this._restored) {
-                               return;
-                       }
-                       
-                       var container = elCreate('div');
-                       container.className = 'redactorAutosaveRestored active';
-                       
-                       var title = elCreate('span');
-                       title.textContent = Language.get('wcf.editor.autosave.restored');
-                       container.appendChild(title);
-                       
-                       var button = elCreate('a');
-                       button.className = 'jsTooltip';
-                       button.href = '#';
-                       button.title = Language.get('wcf.editor.autosave.keep');
-                       button.innerHTML = '<span class="icon icon16 fa-check green"></span>';
-                       button.addEventListener('click', (function (event) {
-                               event.preventDefault();
-                               
-                               this.hideOverlay();
-                       }).bind(this));
-                       container.appendChild(button);
-                       
-                       button = elCreate('a');
-                       button.className = 'jsTooltip';
-                       button.href = '#';
-                       button.title = Language.get('wcf.editor.autosave.discard');
-                       button.innerHTML = '<span class="icon icon16 fa-times red"></span>';
-                       button.addEventListener('click', (function (event) {
-                               event.preventDefault();
-                               
-                               // remove from storage
-                               this.clear();
-                               
-                               // set code
-                               var content = UiRedactorMetacode.convertFromHtml(this._editor.core.element()[0].id, this._originalMessage);
-                               this._editor.code.start(content);
-                               
-                               // set value
-                               this._editor.core.textarea().val(this._editor.clean.onSync(this._editor.$editor.html()));
-                               
-                               this.hideOverlay();
-                       }).bind(this));
-                       container.appendChild(button);
-                       
-                       this._editor.core.box()[0].appendChild(container);
-                       
-                       var callback = (function () {
-                               this._editor.core.editor()[0].removeEventListener('click', callback);
-                               
-                               this.hideOverlay();
-                       }).bind(this);
-                       this._editor.core.editor()[0].addEventListener('click', callback);
-                       
-                       this._container = container;
-               },
-               
-               /**
-                * Hides the autosave controls.
-                */
-               hideOverlay: function () {
-                       if (this._container !== null) {
-                               this._container.classList.remove('active');
-                               
-                               window.setTimeout((function () {
-                                       if (this._container !== null) {
-                                               elRemove(this._container);
-                                       }
-                                       
-                                       this._container = null;
-                                       this._originalMessage = '';
-                               }).bind(this), 1000);
-                       }
-               },
-               
-               /**
-                * Saves the current message to storage unless there was no change.
-                * 
-                * @protected
-                */
-               _saveToStorage: function() {
-                       if (!this._isActive) {
-                               if (!this._isPending) return;
-                               
-                               // save one last time before suspending
-                               this._isPending = false;
-                       }
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (window.ENABLE_DEVELOPER_TOOLS && Devtools._internal_.editorAutosave() === false) {
-                               //noinspection JSUnresolvedVariable
-                               return;
-                       }
-                       
-                       var content = this._editor.code.get();
-                       if (this._editor.utils.isEmpty(content)) {
-                               content = '';
-                       }
-                       
-                       if (this._lastMessage === content) {
-                               // break if content hasn't changed
-                               return;
-                       }
-                       
-                       if (content === '') {
-                               return this.clear();
-                       }
-                       
-                       try {
-                               EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveMetaData_' + this._element.id, this._metaData);
-                               
-                               window.localStorage.setItem(this._key, JSON.stringify({
-                                       content: content,
-                                       meta: this._metaData,
-                                       timestamp: Date.now()
-                               }));
-                               
-                               this._lastMessage = content;
-                       }
-                       catch (e) {
-                               window.console.warn("Unable to write to local storage: " + e.message);
-                       }
-               },
-               
-               /**
-                * Removes stored messages older than one week.
-                * 
-                * @protected
-                */
-               _cleanup: function () {
-                       var oneWeekAgo = Date.now() - (7 * 24 * 3600 * 1000), removeKeys = [];
-                       var i, key, length, value;
-                       for (i = 0, length = window.localStorage.length; i < length; i++) {
-                               key = window.localStorage.key(i);
-                               
-                               // check if key matches our prefix
-                               if (key.indexOf(Core.getStoragePrefix()) !== 0) {
-                                       continue;
-                               }
-                               
-                               try {
-                                       value = window.localStorage.getItem(key);
-                               }
-                               catch (e) {
-                                       window.console.warn("Unable to access local storage: " + e.message);
-                               }
-                               
-                               try {
-                                       value = JSON.parse(value);
-                               }
-                               catch (e) {
-                                       value = { timestamp: 0 };
-                               }
-                               
-                               if (!value || value.timestamp < oneWeekAgo) {
-                                       removeKeys.push(key);
-                               }
-                       }
-                       
-                       for (i = 0, length = removeKeys.length; i < length; i++) {
-                               try {
-                                       window.localStorage.removeItem(removeKeys[i]);
-                               }
-                               catch (e) {
-                                       window.console.warn("Unable to remove from local storage: " + e.message);
-                               }
-                       }
-               }
-       };
-       
-       return UiRedactorAutosave;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Autosave.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Autosave.ts
new file mode 100644 (file)
index 0000000..f07992f
--- /dev/null
@@ -0,0 +1,363 @@
+/**
+ * Manages the autosave process storing the current editor message in the local
+ * storage to recover it on browser crash or accidental navigation.
+ *
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Redactor/Autosave
+ */
+
+import * as Core from "../../Core";
+import Devtools from "../../Devtools";
+import * as EventHandler from "../../Event/Handler";
+import * as Language from "../../Language";
+import { RedactorEditor } from "./Editor";
+import * as UiRedactorMetacode from "./Metacode";
+
+interface AutosaveMetaData {
+  [key: string]: unknown;
+}
+
+interface AutosaveContent {
+  content: string;
+  meta: AutosaveMetaData;
+  timestamp: number;
+}
+
+// time between save requests in seconds
+const _frequency = 15;
+
+class UiRedactorAutosave {
+  protected _container: HTMLElement | null = null;
+  protected _editor: RedactorEditor | null = null;
+  protected readonly _element: HTMLTextAreaElement;
+  protected _isActive = true;
+  protected _isPending = false;
+  protected readonly _key: string;
+  protected _lastMessage = "";
+  protected _metaData: AutosaveMetaData = {};
+  protected _originalMessage = "";
+  protected _restored = false;
+  protected _timer: number | null = null;
+
+  /**
+   * Initializes the autosave handler and removes outdated messages from storage.
+   *
+   * @param       {Element}       element         textarea element
+   */
+  constructor(element: HTMLTextAreaElement) {
+    this._element = element;
+    this._key = Core.getStoragePrefix() + this._element.dataset.autosave!;
+    //this._overlay = null;
+
+    this._cleanup();
+
+    // remove attribute to prevent Redactor's built-in autosave to kick in
+    delete this._element.dataset.autosave;
+
+    const form = this._element.closest("form");
+    if (form !== null) {
+      form.addEventListener("submit", this.destroy.bind(this));
+    }
+
+    // export meta data
+    EventHandler.add("com.woltlab.wcf.redactor2", `getMetaData_${this._element.id}`, (data: AutosaveMetaData) => {
+      Object.entries(this._metaData).forEach(([key, value]) => {
+        data[key] = value;
+      });
+    });
+
+    // clear editor content on reset
+    EventHandler.add("com.woltlab.wcf.redactor2", `reset_${this._element.id}`, () => this.hideOverlay());
+
+    document.addEventListener("visibilitychange", () => this._onVisibilityChange());
+  }
+
+  protected _onVisibilityChange(): void {
+    if (document.hidden) {
+      this._isActive = false;
+      this._isPending = true;
+    } else {
+      this._isActive = true;
+      this._isPending = false;
+    }
+  }
+
+  /**
+   * Returns the initial value for the textarea, used to inject message
+   * from storage into the editor before initialization.
+   *
+   * @return      {string}        message content
+   */
+  getInitialValue(): string {
+    if (window.ENABLE_DEVELOPER_TOOLS && !Devtools._internal_.editorAutosave()) {
+      return this._element.value;
+    }
+
+    let value = "";
+    try {
+      value = window.localStorage.getItem(this._key) || "";
+    } catch (e) {
+      const errorMessage = (e as Error).message;
+      window.console.warn(`Unable to access local storage: ${errorMessage}`);
+    }
+
+    let metaData: AutosaveContent | null = null;
+    try {
+      metaData = JSON.parse(value);
+    } catch (e) {
+      // We do not care for JSON errors.
+    }
+
+    // Check if the storage is outdated.
+    if (metaData !== null && typeof metaData === "object" && metaData.content) {
+      const lastEditTime = ~~this._element.dataset.autosaveLastEditTime!;
+      if (lastEditTime * 1_000 <= metaData.timestamp) {
+        // Compare the stored version with the editor content, but only use the `innerText` property
+        // in order to ignore differences in whitespace, e. g. caused by indentation of HTML tags.
+        const div1 = document.createElement("div");
+        div1.innerHTML = this._element.value;
+        const div2 = document.createElement("div");
+        div2.innerHTML = metaData.content;
+
+        if (div1.innerText.trim() !== div2.innerText.trim()) {
+          this._originalMessage = this._element.value;
+          this._restored = true;
+
+          this._metaData = metaData.meta || {};
+
+          return metaData.content;
+        }
+      }
+    }
+
+    return this._element.value;
+  }
+
+  /**
+   * Returns the stored meta data.
+   */
+  getMetaData(): AutosaveMetaData {
+    return this._metaData;
+  }
+
+  /**
+   * Enables periodical save of editor contents to local storage.
+   */
+  watch(editor: RedactorEditor): void {
+    this._editor = editor;
+
+    if (this._timer !== null) {
+      throw new Error("Autosave timer is already active.");
+    }
+
+    this._timer = window.setInterval(() => this._saveToStorage(), _frequency * 1_000);
+
+    this._saveToStorage();
+
+    this._isPending = false;
+  }
+
+  /**
+   * Disables autosave handler, for use on editor destruction.
+   */
+  destroy(): void {
+    this.clear();
+
+    this._editor = null;
+
+    if (this._timer) {
+      window.clearInterval(this._timer);
+    }
+
+    this._timer = null;
+    this._isPending = false;
+  }
+
+  /**
+   * Removed the stored message, for use after a message has been submitted.
+   */
+  clear(): void {
+    this._metaData = {};
+    this._lastMessage = "";
+
+    try {
+      window.localStorage.removeItem(this._key);
+    } catch (e) {
+      const errorMessage = (e as Error).message;
+      window.console.warn(`Unable to remove from local storage: ${errorMessage}`);
+    }
+  }
+
+  /**
+   * Creates the autosave controls, used to keep or discard the restored draft.
+   */
+  createOverlay(): void {
+    if (!this._restored) {
+      return;
+    }
+
+    const editor = this._editor!;
+
+    const container = document.createElement("div");
+    container.className = "redactorAutosaveRestored active";
+
+    const title = document.createElement("span");
+    title.textContent = Language.get("wcf.editor.autosave.restored");
+    container.appendChild(title);
+
+    const buttonKeep = document.createElement("a");
+    buttonKeep.className = "jsTooltip";
+    buttonKeep.href = "#";
+    buttonKeep.title = Language.get("wcf.editor.autosave.keep");
+    buttonKeep.innerHTML = '<span class="icon icon16 fa-check green"></span>';
+    buttonKeep.addEventListener("click", (event) => {
+      event.preventDefault();
+
+      this.hideOverlay();
+    });
+    container.appendChild(buttonKeep);
+
+    const buttonDiscard = document.createElement("a");
+    buttonDiscard.className = "jsTooltip";
+    buttonDiscard.href = "#";
+    buttonDiscard.title = Language.get("wcf.editor.autosave.discard");
+    buttonDiscard.innerHTML = '<span class="icon icon16 fa-times red"></span>';
+    buttonDiscard.addEventListener("click", (event) => {
+      event.preventDefault();
+
+      // remove from storage
+      this.clear();
+
+      // set code
+      const content = UiRedactorMetacode.convertFromHtml(editor.core.element()[0].id, this._originalMessage);
+      editor.code.start(content);
+
+      // set value
+      editor.core.textarea().val(editor.clean.onSync(editor.$editor.html()));
+
+      this.hideOverlay();
+    });
+    container.appendChild(buttonDiscard);
+
+    editor.core.box()[0].appendChild(container);
+
+    editor.core.editor()[0].addEventListener("click", () => this.hideOverlay(), { once: true });
+
+    this._container = container;
+  }
+
+  /**
+   * Hides the autosave controls.
+   */
+  hideOverlay(): void {
+    if (this._container !== null) {
+      this._container.classList.remove("active");
+
+      window.setTimeout(() => {
+        if (this._container !== null) {
+          this._container.remove();
+        }
+
+        this._container = null;
+        this._originalMessage = "";
+      }, 1_000);
+    }
+  }
+
+  /**
+   * Saves the current message to storage unless there was no change.
+   */
+  protected _saveToStorage(): void {
+    if (!this._isActive) {
+      if (!this._isPending) {
+        return;
+      }
+
+      // save one last time before suspending
+      this._isPending = false;
+    }
+
+    //noinspection JSUnresolvedVariable
+    if (window.ENABLE_DEVELOPER_TOOLS && !Devtools._internal_.editorAutosave()) {
+      return;
+    }
+
+    const editor = this._editor!;
+    let content = editor.code.get();
+    if (editor.utils.isEmpty(content)) {
+      content = "";
+    }
+
+    if (this._lastMessage === content) {
+      // break if content hasn't changed
+      return;
+    }
+
+    if (content === "") {
+      return this.clear();
+    }
+
+    try {
+      EventHandler.fire("com.woltlab.wcf.redactor2", `autosaveMetaData_${this._element.id}`, this._metaData);
+
+      window.localStorage.setItem(
+        this._key,
+        JSON.stringify({
+          content: content,
+          meta: this._metaData,
+          timestamp: Date.now(),
+        } as AutosaveContent),
+      );
+
+      this._lastMessage = content;
+    } catch (e) {
+      const errorMessage = (e as Error).message;
+      window.console.warn(`Unable to write to local storage: ${errorMessage}`);
+    }
+  }
+
+  /**
+   * Removes stored messages older than one week.
+   */
+  protected _cleanup(): void {
+    const oneWeekAgo = Date.now() - 7 * 24 * 3_600 * 1_000;
+
+    Object.keys(window.localStorage).forEach((key) => {
+      // check if key matches our prefix
+      if (!key.startsWith(Core.getStoragePrefix())) {
+        return;
+      }
+
+      let value = "";
+      try {
+        value = window.localStorage.getItem(key) || "";
+      } catch (e) {
+        const errorMessage = (e as Error).message;
+        window.console.warn(`Unable to access local storage: ${errorMessage}`);
+      }
+
+      let timestamp = 0;
+      try {
+        const content: AutosaveContent = JSON.parse(value);
+        timestamp = content.timestamp;
+      } catch (e) {
+        // We do not care for JSON errors.
+      }
+
+      if (!value || timestamp < oneWeekAgo) {
+        try {
+          window.localStorage.removeItem(key);
+        } catch (e) {
+          const errorMessage = (e as Error).message;
+          window.console.warn(`Unable to remove from local storage: ${errorMessage}`);
+        }
+      }
+    });
+  }
+}
+
+Core.enableLegacyInheritance(UiRedactorAutosave);
+
+export = UiRedactorAutosave;
index e0f968cd8ff688482dc016a9f29b9dee4a5b5900..338f40c154305da4e016744b1bd28b3a9fbfa481 100644 (file)
@@ -1,8 +1,26 @@
 export interface RedactorEditor {
+  $editor: JQuery;
+
   buffer: {
     set: () => void;
   };
+  clean: {
+    onSync: (html: string) => string;
+  };
+  code: {
+    get: () => string;
+    start: (html: string) => void;
+  };
+  core: {
+    box: () => JQuery;
+    editor: () => JQuery;
+    element: () => JQuery;
+    textarea: () => JQuery;
+  };
   insert: {
     text: (text: string) => void;
   };
+  utils: {
+    isEmpty: (html: string) => boolean;
+  };
 }