From fe12682039debe41b66dedc046b424f89558fcc2 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 29 Jul 2016 14:23:11 +0200 Subject: [PATCH] Added autosave feature --- com.woltlab.wcf/templates/wysiwyg.tpl | 9 +- .../redactor2/plugins/WoltLabAlignment.js | 2 +- .../redactor2/plugins/WoltLabAutosave.js | 32 +++ .../js/WoltLab/WCF/Ui/Message/InlineEditor.js | 4 + .../files/js/WoltLab/WCF/Ui/Message/Reply.js | 4 +- .../js/WoltLab/WCF/Ui/Redactor/Autosave.js | 195 ++++++++++++++++++ 6 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAutosave.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js diff --git a/com.woltlab.wcf/templates/wysiwyg.tpl b/com.woltlab.wcf/templates/wysiwyg.tpl index fd5ff2922e..e7bb151a13 100644 --- a/com.woltlab.wcf/templates/wysiwyg.tpl +++ b/com.woltlab.wcf/templates/wysiwyg.tpl @@ -20,6 +20,7 @@ {* WoltLab *} '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabAlignment.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabAttachment.js?v={@LAST_UPDATE_TIME}', + '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabAutosave.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabBlock.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabButton.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabCode.js?v={@LAST_UPDATE_TIME}', @@ -47,7 +48,7 @@ {event name='redactorJavaScript'} ], function () { - require(['Language', 'WoltLab/WCF/Ui/Redactor/Metacode'], function(Language, UiRedactorMetacode) { + require(['Language', 'WoltLab/WCF/Ui/Redactor/Autosave', 'WoltLab/WCF/Ui/Redactor/Metacode'], function(Language, UiRedactorAutosave, UiRedactorMetacode) { Language.addObject({ 'wcf.editor.code.edit': '{lang}wcf.editor.code.edit{/lang}', 'wcf.editor.code.file': '{lang}wcf.editor.code.file{/lang}', @@ -98,9 +99,10 @@ var element = elById('{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}'); UiRedactorMetacode.convert(element); - var autosave = elData(element, 'autosave') || ''; + var autosave = elData(element, 'autosave') || null; if (autosave) { - element.removeAttribute('data-autosave'); + autosave = new UiRedactorAutosave(element); + element.value = autosave.getInitialValue(); } var config = { @@ -159,6 +161,7 @@ // WoltLab core 'WoltLabAlignment', 'WoltLabAttachment', + 'WoltLabAutosave', 'WoltLabCode', 'WoltLabColor', 'WoltLabDropdown', diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAlignment.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAlignment.js index ee709f0ecd..72a9401da9 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAlignment.js +++ b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAlignment.js @@ -1,4 +1,4 @@ -$.Redactor.prototype.WoltLabAlignment = function() { +$.Redactor.prototype.WoltLabAlignment = function() { "use strict"; return { diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAutosave.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAutosave.js new file mode 100644 index 0000000000..2c9762a26b --- /dev/null +++ b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabAutosave.js @@ -0,0 +1,32 @@ +$.Redactor.prototype.WoltLabAutosave = function() { + "use strict"; + + return { + init: function () { + //noinspection JSUnresolvedVariable + if (this.opts.woltlab.autosave) { + //noinspection JSUnresolvedVariable + this.opts.woltlab.autosave.watch(this); + + WCF.System.Event.addListener('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this.$element[0].id, this.WoltLabAutosave.destroy.bind(this)); + WCF.System.Event.addListener('com.woltlab.wcf.redactor2', 'autosaveReset_' + this.$element[0].id, this.WoltLabAutosave.reset.bind(this)); + } + }, + + destroy: function () { + //noinspection JSUnresolvedVariable + if (this.opts.woltlab.autosave) { + //noinspection JSUnresolvedVariable + this.opts.woltlab.autosave.destroy(); + } + }, + + reset: function () { + //noinspection JSUnresolvedVariable + if (this.opts.woltlab.autosave) { + //noinspection JSUnresolvedVariable + this.opts.woltlab.autosave.clear(); + } + } + }; +}; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js index 54acb6904e..68cef5c936 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js @@ -508,6 +508,7 @@ define( */ _showMessage: function(data) { var activeElement = this._activeElement; + var editorId = this._getEditorId(); var elementData = this._elements.get(activeElement); var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter); @@ -553,6 +554,8 @@ define( this._updateHistory(this._getHash(this._getObjectId(activeElement))); + EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId); + UiNotification.show(); if (this._options.quoteManager) { @@ -595,6 +598,7 @@ define( * @protected */ _destroyEditor: function() { + EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId()); EventHandler.fire('com.woltlab.wcf.redactor', 'destroy_' + this._getEditorId()); }, diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js index 06f98c6d2f..8a73c4b7eb 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js @@ -260,7 +260,7 @@ define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/U * @protected */ _insertMessage: function(data) { - // TODO: clear autosave content and disable it + this._getEditor().WoltLabAutosave.reset(); // redirect to new page //noinspection JSUnresolvedVariable @@ -295,8 +295,6 @@ define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/U UiNotification.show(Language.get(this._options.successMessage)); - // TODO: resume autosave - if (this._options.quoteManager) { this._options.quoteManager.countQuotes(); } diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js new file mode 100644 index 0000000000..94fffd21a9 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js @@ -0,0 +1,195 @@ +/** + * 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-2016 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/Ui/Redactor/Autosave + */ +define([], function() { + "use strict"; + + // time between save requests in seconds + var _frequency = 15; + + //noinspection JSUnresolvedVariable + var _prefix = 'wsc' + window.WCF_PATH.hashCode() + '-'; + + /** + * @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._editor = null; + this._element = element; + this._key = _prefix + elData(this._element, 'autosave'); + this._lastMessage = ''; + this._timer = null; + + this._cleanup(); + + // remove attribute to prevent Redactor's built-in autosave to kick in + this._element.removeAttribute('data-autosave'); + }, + + /** + * Returns the initial value for the textarea, used to inject message + * from storage into the editor before initialization. + * + * @return {string} message content + */ + getInitialValue: function () { + 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 storage is outdated + if (value !== null && typeof value === 'object') { + var lastEditTime = ~~elData(this._element, 'autosave-last-edit-time'); + if (lastEditTime * 1000 > value.timestamp) { + //noinspection JSUnresolvedVariable + return this._element.value; + } + + return value.content; + } + + //noinspection JSUnresolvedVariable + return this._element.value; + }, + + /** + * 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(); + }, + + /** + * Disables autosave handler, for use on editor destruction. + */ + destroy: function () { + this.clear(); + + this._editor = null; + + window.clearInterval(this._timer); + this._timer = null; + }, + + /** + * Removed the stored message, for use after a message has been submitted. + */ + clear: function () { + this._lastMessage = ''; + + try { + window.localStorage.removeItem(this._key); + } + catch (e) { + window.console.warn("Unable to remove from local storage: " + e.message); + } + }, + + /** + * Saves the current message to storage unless there was no change. + * + * @protected + */ + _saveToStorage: function() { + var content = this._editor.code.get(); + if (this._editor.utils.isEmpty(content)) { + content = ''; + } + + if (this._lastMessage === content) { + // break if content hasn't changed + return; + } + + try { + window.localStorage.setItem(this._key, JSON.stringify({ + content: content, + 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); + var key, value; + for (var i = 0, length = window.localStorage.length; i < length; i++) { + key = window.localStorage.key(i); + + // check if key matches our prefix + if (key.indexOf(_prefix) !== 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) { + try { + window.localStorage.removeItem(key); + } + catch (e) { + window.console.warn("Unable to remove from local storage: " + e.message); + } + } + } + } + }; + + return UiRedactorAutosave; +}); -- 2.20.1