Improved autosave feature
authorAlexander Ebert <ebert@woltlab.com>
Sat, 22 Nov 2014 21:00:36 +0000 (22:00 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 22 Nov 2014 21:01:17 +0000 (22:01 +0100)
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js
wcfsetup/install/files/style/redactor.less
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 668343223d33d28a0241e32ecc2e413c2ba1885a..637fab6549445bf7e67c197ec55cf6f5be1158c4 100644 (file)
@@ -23,7 +23,13 @@ $(function() {
                'wcf.bbcode.quote.insert': '{lang}wcf.bbcode.quote.insert{/lang}',
                'wcf.bbcode.quote.title.clickToSet': '{lang}wcf.bbcode.quote.title.clickToSet{/lang}',
                'wcf.bbcode.quote.title.javascript': '{lang}wcf.bbcode.quote.title.javascript{/lang}',
-               'wcf.global.noSelection': '{lang}wcf.global.noSelection{/lang}'
+               'wcf.global.noSelection': '{lang}wcf.global.noSelection{/lang}',
+               'wcf.message.autosave.restored': '{lang}wcf.message.autosave.restored{/lang}',
+               'wcf.message.autosave.restored.confirm': '{lang}wcf.message.autosave.restored.confirm{/lang}',
+               'wcf.message.autosave.restored.revert': '{lang}wcf.message.autosave.restored.revert{/lang}',
+               'wcf.message.autosave.restored.revert.confirmMessage': '{lang}wcf.message.autosave.restored.revert.confirmMessage{/lang}',
+               'wcf.message.autosave.restored.version': '{lang __literal=true}wcf.message.autosave.restored.version{/lang}',
+               'wcf.message.autosave.saved': '{lang}wcf.message.autosave.saved{/lang}'
        });
        
        var $editorName = '{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}';
@@ -56,6 +62,7 @@ $(function() {
                                autosave: {
                                        active: ($autosave) ? true : false,
                                        key: ($autosave) ? '{@$__wcf->getAutosavePrefix()}_' + $autosave : '',
+                                       prefix: '{@$__wcf->getAutosavePrefix()}',
                                        saveOnInit: {if !$errorField|empty}true{else}false{/if}
                                },
                                originalValue: $textarea.val()
index 15b43ae445ab84573af6e5601668c03db0f0f368..bbf47f3e1b69c67eaf84a05265b4626cccdd7a62 100644 (file)
@@ -10,6 +10,9 @@ if (!RedactorPlugins) var RedactorPlugins = {};
 RedactorPlugins.wutil = function() {
        "use strict";
        
+       var $autosaveLastMessage = '';
+       var $autosaveNotice = null;
+       
        return {
                /**
                 * autosave worker process
@@ -224,6 +227,8 @@ RedactorPlugins.wutil = function() {
                        }
                        
                        if (this.wutil._autosaveWorker === null) {
+                               this.wutil.autosavePurgeOutdated();
+                               
                                this.wutil._autosaveWorker = new WCF.PeriodicalExecuter($.proxy(this.wutil.saveTextToStorage, this), 60 * 1000);
                        }
                        
@@ -234,8 +239,19 @@ RedactorPlugins.wutil = function() {
                 * Saves current editor text to local browser storage.
                 */
                saveTextToStorage: function() {
+                       var $content = this.wutil.getText();
+                       if ($autosaveLastMessage == $content) {
+                               return;
+                       }
+                       
                        try {
-                               localStorage.setItem(this.wutil.getOption('woltlab.autosave').key, this.wutil.getText());
+                               localStorage.setItem(this.wutil.getOption('woltlab.autosave').key, JSON.stringify({
+                                       content: $content,
+                                       timestamp: Date.now()
+                               }));
+                               $autosaveLastMessage = $content;
+                               
+                               this.wutil.autosaveShowNotice('saved');
                        }
                        catch (e) {
                                console.debug("[wutil.saveTextToStorage] Unable to access local storage: " + e.message);
@@ -277,6 +293,8 @@ RedactorPlugins.wutil = function() {
                
                /**
                 * Attempts to restore a saved text.
+                * 
+                * @return      boolean
                 */
                autosaveRestore: function() {
                        var $options = this.wutil.getOption('woltlab.autosave');
@@ -289,18 +307,161 @@ RedactorPlugins.wutil = function() {
                                console.debug("[wutil.autosaveRestore] Unable to access local storage: " + e.message);
                        }
                        
-                       if ($text !== null) {
-                               if (this.wutil.inWysiwygMode()) {
-                                       this.wutil.setOption('woltlab.originalValue', $text);
-                               }
-                               else {
-                                       this.$textarea.val($text);
-                               }
+                       try {
+                               $text = ($text === null) ? null : JSON.parse($text);
+                       }
+                       catch (e) {
+                               $text = null;
+                       }
+                       
+                       if ($text === null || !$text.content) {
+                               return false;
+                       }
+                       
+                       if (this.wutil.inWysiwygMode()) {
+                               this.wutil.setOption('woltlab.originalValue', $text.content);
+                       }
+                       else {
+                               this.$textarea.val($text.content);
+                       }
+                       
+                       this.wutil.autosaveShowNotice('restored', { timestamp: $text.timestamp });
+                       WCF.DOMNodeInsertedHandler.execute();
+                       
+                       return true;
+               },
+               
+               /**
+                * Displays a notice regarding the autosave feature.
+                * 
+                * @param       string          type
+                * @param       object          data
+                */
+               autosaveShowNotice: function(type, data) {
+                       if ($autosaveNotice === null) {
+                               $autosaveNotice = $('<div class="redactorAutosaveNotice"><span class="redactorAutosaveMessage" /></div>');
+                               $autosaveNotice.appendTo(this.$box);
+                               $autosaveNotice.on('transitionend webkitTransitionEnd', (function(event) {
+                                       if (event.originalEvent.propertyName !== 'opacity') {
+                                               return;
+                                       }
+                                       
+                                       if ($autosaveNotice.hasClass('open')) {
+                                               if ($autosaveNotice.data('callbackOpen')) {
+                                                       $autosaveNotice.data('callbackOpen')();
+                                               }
+                                       }
+                                       else {
+                                               if ($autosaveNotice.data('callbackClose')) {
+                                                       $autosaveNotice.data('callbackClose')();
+                                               }
+                                               
+                                               $autosaveNotice.removeData('callbackClose');
+                                               $autosaveNotice.removeData('callbackOpen');
+                                               
+                                               $autosaveNotice.removeClass('redactorAutosaveNoticeRestore');
+                                               $autosaveNotice.empty();
+                                               $('<span class="redactorAutosaveMessage" />').appendTo($autosaveNotice);
+                                       }
+                               }).bind(this));
+                       }
+                       
+                       var $message = '';
+                       switch (type) {
+                               case 'restored':
+                                       $('<span class="icon icon16 fa-info blue jsTooltip" title="' + WCF.Language.get('wcf.message.autosave.restored.version', { date: new Date(data.timestamp).toLocaleString() }) + '"></span>').prependTo($autosaveNotice);
+                                       var $accept = $('<span class="icon icon16 fa-check green pointer jsTooltip" title="' + WCF.Language.get('wcf.message.autosave.restored.confirm') + '"></span>').appendTo($autosaveNotice);
+                                       var $discard = $('<span class="icon icon16 fa-times red pointer jsTooltip" title="' + WCF.Language.get('wcf.message.autosave.restored.revert') + '"></span>').appendTo($autosaveNotice);
+                                       
+                                       $accept.click(function() { $autosaveNotice.removeClass('open'); });
+                                       
+                                       $discard.click((function() {
+                                               WCF.System.Confirmation.show(WCF.Language.get('wcf.message.autosave.restored.revert.confirmMessage'), (function(action) {
+                                                       if (action === 'confirm') {
+                                                               this.wutil.reset();
+                                                               this.wutil.autosavePurge();
+                                                               
+                                                               $autosaveNotice.removeClass('open');
+                                                       }
+                                               }).bind(this));
+                                       }).bind(this));
+                                       
+                                       $message = WCF.Language.get('wcf.message.autosave.restored');
+                                       
+                                       $autosaveNotice.addClass('redactorAutosaveNoticeRestore');
+                               break;
                                
-                               return true;
+                               case 'saved':
+                                       if ($autosaveNotice.hasClass('open')) {
+                                               return;
+                                       }
+                                       
+                                       $autosaveNotice.data('callbackOpen', function() {
+                                               setTimeout(function() {
+                                                       $autosaveNotice.removeClass('open');
+                                               }, 3000);
+                                       });
+                                       
+                                       $message = WCF.Language.get('wcf.message.autosave.saved');
+                               break;
                        }
                        
-                       return false;
+                       $autosaveNotice.children('span.redactorAutosaveMessage').text($message);
+                       $autosaveNotice.addClass('open');
+               },
+               
+               /**
+                * Automatically purges autosaved content older than 7 days.
+                */
+               autosavePurgeOutdated: function() {
+                       var $lastChecked = 0;
+                       var $prefix = this.wutil.getOption('woltlab.autosave').prefix;
+                       var $master = $prefix + '_wcf_master';
+                       
+                       try {
+                               $lastChecked = localStorage.getItem($master);
+                       }
+                       catch (e) {
+                               console.debug("[wutil.autosavePurgeOutdated] Unable to access local storage: " + e.message);
+                       }
+                       
+                       if ($lastChecked === 0) {
+                               // unable to access local storage, skip check
+                               return;
+                       }
+                       
+                       // JavaScript timestamps are in miliseconds
+                       var $oneWeekAgo = Date.now() - (7 * 24 * 3600 * 1000);
+                       if ($lastChecked === null || $lastChecked < $oneWeekAgo) {
+                               var $regExp = new RegExp('^' + $prefix + '_');
+                               for (var $key in localStorage) {
+                                       if ($key.match($regExp)) {
+                                               var $value = localStorage.getItem($key);
+                                               try {
+                                                       $value = JSON.parse($value);
+                                               }
+                                               catch (e) {
+                                                       $value = { timestamp: 0 };
+                                               }
+                                               
+                                               if (!$value.timestamp || $value.timestamp < $oneWeekAgo) {
+                                                       try {
+                                                               localStorage.removeItem($key);
+                                                       }
+                                                       catch (e) {
+                                                               console.debug("[wutil.autosavePurgeOutdated] Unable to access local storage: " + e.message);
+                                                       }
+                                               }
+                                       }
+                               }
+                               
+                               try {
+                                       localStorage.setItem($master, Date.now());
+                               }
+                               catch (e) {
+                                       console.debug("[wutil.autosavePurgeOutdated] Unable to access local storage: " + e.message);
+                               }
+                       }
                },
                
                /**
index 18b9c8e870225f051b2b8e0bec0f8edf364b18db..0e8b735c70bb0c675e7ad37cdf3b9605d901889f 100644 (file)
        > .innerError {
                margin: -1px;
        }
+       
+       > .redactorAutosaveNotice {
+               border: 1px solid @wcfContainerBorderColor;
+               border-width: 1px 0 0 1px;
+               bottom: 0;
+               font-size: 1rem;
+               opacity: 0;
+               padding: @wcfGapSmall;
+               position: absolute;
+               right: 0;
+               transition: visibility 0s linear .3s, opacity .3s linear;
+               visibility: hidden;
+               
+               &.open {
+                       opacity: 1;
+                       visibility: visible;
+                       transition-delay: 0s;
+               }
+               
+               &.redactorAutosaveNoticeRestore > span.fa-check {
+                       margin-right: @wcfGapSmall;
+               }
+               
+               > span.redactorAutosaveMessage {
+                       padding: 0 @wcfGapSmall;
+               }
+       }
 }
 
 .redactor-editor {
index 201ccc02ce9bca3b19494d38bba9cc66f45470d3..a171768f71745d4cd78d00f346d81714a887836e 100644 (file)
@@ -2254,6 +2254,12 @@ Fehler sind beispielsweise:
        </category>
        
        <category name="wcf.message">
+               <item name="wcf.message.autosave.restored"><![CDATA[Entwurf wiederhergestellt]]></item>
+               <item name="wcf.message.autosave.restored.confirm"><![CDATA[Beibehalten]]></item>
+               <item name="wcf.message.autosave.restored.revert"><![CDATA[Verwerfen und Editor leeren]]></item>
+               <item name="wcf.message.autosave.restored.revert.confirmMessage"><![CDATA[Wollen Sie den gespeicherten Entwurf wirklich löschen und den Editor leeren?]]></item>
+               <item name="wcf.message.autosave.restored.version"><![CDATA[Entwurf vom {@$date}]]></item>
+               <item name="wcf.message.autosave.saved"><![CDATA[Entwurf gespeichert]]></item>
                <item name="wcf.message.bbcode.code.copy"><![CDATA[Inhalt kopieren]]></item>
                <item name="wcf.message.quote.insertAllQuotes"><![CDATA[Alle Zitate einfügen]]></item>
                <item name="wcf.message.quote.insertQuote"><![CDATA[Zitat einfügen]]></item>
index 54233653cfed5fe974b984b9b4e51ddf4ff83af8..051d28f2a88eab65271e2e47cec32cfb612a92fb 100644 (file)
@@ -2253,6 +2253,12 @@ Errors are:
        </category>
        
        <category name="wcf.message">
+               <item name="wcf.message.autosave.restored"><![CDATA[Draft restored]]></item>
+               <item name="wcf.message.autosave.restored.confirm"><![CDATA[Keep]]></item>
+               <item name="wcf.message.autosave.restored.revert"><![CDATA[Discard and Clear Editor]]></item>
+               <item name="wcf.message.autosave.restored.revert.confirmMessage"><![CDATA[Do you really want to discard this draft and clear the editor?]]></item>
+               <item name="wcf.message.autosave.restored.version"><![CDATA[Draft as of {@$date}]]></item>
+               <item name="wcf.message.autosave.saved"><![CDATA[Draft saved]]></item>
                <item name="wcf.message.bbcode.code.copy"><![CDATA[Copy Contents]]></item>
                <item name="wcf.message.quote.insertAllQuotes"><![CDATA[Insert All Quotes]]></item>
                <item name="wcf.message.quote.insertQuote"><![CDATA[Insert Quote]]></item>