Skip to content
Snippets Groups Projects
manageddata.ts 7.24 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";
    
    
    type SavePoint = {
        description: string;
        data: string;
        id: number;
    };
    
    
    /**
     * 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: SavePoint[]; // 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;
        }
    
    
        /**
         * @returns SavePoint of current history position. Gives access to meta data of current data.
         */
        public get currentSavePoint(): SavePoint {
            return this.history[this.historyPosition];
        }
    
    
        /**
         * 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 current state of data to a given savepoint that is stored in the history.
         * @param savePointId Id of desired savepoint.
         * @returns True, if successful.
         */
        public goToSavePoint(savePointId: number): boolean {
            // Iterate overhistory and find position with same savepoint id
            for (let i = 0; i < this.history.length; i++) {
                if (this.history[i].id === savePointId) {
                    return this.setHistoryPosition(i);
                }
            }
    
            return false; // Not found
    
        /**
         * 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;
            }
    
    
            return this.setHistoryPosition(newHistoryPosition);
        }
    
        /**
         * Loads a given history index into the current data object and sets historyPosition accordingly.
         * @param position Position (Index) of history point to load.
         * @returns True, if successful.
         */
        private setHistoryPosition(position: number): boolean {
            if (position < 0 || position >= this.history.length) {
                return false;
            }
    
            this.historyPosition = position;
    
            const savePointData = JSON.parse(
                this.history[this.historyPosition].data
            );
            this.data = this.restoreData(savePointData);
    
    
            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;
        }
    
    
        /**
         * Restores proper format of data.
         * @param data New data to restore.
         * @returns Formatted data ready to use.
         */
        protected restoreData(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();
        }
    }