Skip to content
Snippets Groups Projects
manageddata.ts 5.74 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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();
        }
    }