import {SerializableItem} from "./helper/serializableitem"; import jQuery from "jquery"; const SAVE_BUTTON_ID = "div#ks-editor #toolbar-save"; /** * Allows objects to have undo/redo functionality in their data and custom save points. */ export default class ManagedData extends SerializableItem { public data: any; // The data to be stored in a history. public history: any[]; // All save points of the data. public historyPosition: number; // Currently selected save point in history. Latest always at index 0. private savedHistoryId: number; // Id of save point that is considered saved. private storingEnabled: boolean; // To internally disable saving of objects on save call. /** * Sets initial states. * @param data Initial state of data to be stored. */ constructor(data: any) { super(); this.data = data; this.history = []; // Newest state is always at 0 this.historyPosition = 0; this.savedHistoryId = 0; this.storingEnabled = true; this.storeCurrentData("Initial state", false); } /** * If the data has unsaved changes, this will subscribe to the tab-closing event to warn about losing unsaved changes before closing. * @private */ private updateUnsavedChangesHandler() { if (this.hasUnsavedChanges()) { jQuery(SAVE_BUTTON_ID).removeClass("hidden"); window.addEventListener("beforeunload", this.handleBeforeUnload); } else { jQuery(SAVE_BUTTON_ID).addClass("hidden"); window.removeEventListener("beforeunload", this.handleBeforeUnload); } } /** * Called on the tab-closing event to trigger a warning, to avoid losing unsaved changes. * @param e Event. * @private */ private handleBeforeUnload(e: any) { const confirmationMessage = "If you leave before saving, unsaved changes will be lost."; (e || window.event).returnValue = confirmationMessage; //Gecko + IE return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. } /** * Returns true, if data has unsaved changes. */ public hasUnsavedChanges(): boolean { return this.history[this.historyPosition].id !== this.savedHistoryId; } /** * Internally marks the current save point as saved. */ public markChangesAsSaved() { this.savedHistoryId = this.history[this.historyPosition].id; this.updateUnsavedChangesHandler(); } /** * Setter to disable storing save points. */ public disableStoring() { this.storingEnabled = false; } /** * Setter to enable storing save points. */ public enableStoring() { this.storingEnabled = true; } /** * Event triggered after undo. */ protected onUndo() { // No base implementation. } /** * Event triggered after redo. */ protected onRedo() { // No base implementation. } /** * Go to one step back in the stored history, if available. * @returns True, if successful. */ public undo(): boolean { if (this.step(1)) { this.updateUnsavedChangesHandler(); this.onUndo(); return true; } else { return false; } } /** * Go one step forward in the stored history, if available. * @returns True, if successful. */ public redo(): boolean { if (this.step(-1)) { this.updateUnsavedChangesHandler(); this.onRedo(); return true; } else { return false; } } /** * Moves the history pointer to the desired position and adjusts the data object. * @param direction How many steps to take in the history. Positive for going back in time, negative for going forward. * @returns True, if successful. * @private */ private step(direction = 1): boolean { const newHistoryPosition = this.historyPosition + Math.sign(direction); if ( newHistoryPosition >= this.history.length || newHistoryPosition < 0 ) { return false; } this.historyPosition = newHistoryPosition; this.data = JSON.parse(this.history[this.historyPosition].data); return true; } /** * Formats the data to the desired stored format. * @param data The raw data. * @returns The formatted, cleaned up data to be stored. */ protected storableData(data: any): any { return data; } /** * Creates a save point. * @param description Description of the current save point. Could describe the difference to the previous save point. * @param relevantChanges Indicates major or minor changes. Major changes get a new id to indicate an actual changed state. Should usually be true. */ public storeCurrentData(description: string, relevantChanges = true) { if (this.storingEnabled === false) { return; } const formattedData = this.storableData(this.data); let nextId = 0; if (this.history.length > 0) { nextId = this.history[0].id; // Keep same as previous id, if nothing relevant changed // Otherwise, increase by one if (relevantChanges) { nextId++; } } // Forget about the currently stored potential future this.history.splice(0, this.historyPosition); this.historyPosition = 0; this.history.unshift({ description: description, data: JSON.stringify(formattedData), // Creating a deep copy id: nextId, }); this.updateUnsavedChangesHandler(); } }