Newer
Older
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 {

Maximilian Giller
committed
if (this.history[this.historyPosition] === undefined) {
return this.data !== undefined;
}
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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;

Maximilian Giller
committed
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;
}

Maximilian Giller
committed
/**
* Restores proper format of data.
* @param data New data to restore.
* @returns Formatted data ready to use.
*/
protected restoreData(data: any): any {
return data;
}
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/**
* 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();
}
}