* 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;
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;
});
+++ /dev/null
-/**
- * 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;
-});
--- /dev/null
+/**
+ * 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;
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;
+ };
}