UNPKG

@typescript-package/history

Version:

A lightweight TypeScript package for tracking the history of values.

1,025 lines (1,011 loc) 30.9 kB
import { Data } from '@typescript-package/data'; /** * @description The history storage of specified data. * @export * @abstract * @class HistoryStorage * @template Value * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] */ class HistoryStorage { /** * @description Returns the `string` tag representation of the `HistoryStorage` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return HistoryStorage.name; } /** * @description Returns the data holder of `DataType`. * @public * @readonly * @type {DataType} */ get data() { return this.#data; } /** * @description The length of the history. * @public * @readonly * @type {number} */ get length() { return this.#data.value.length; } /** * @description The data type to store the value. * @type {DataType} */ #data; /** * Creates an instance of `HistoryStorage` child class. * @constructor * @param {readonly Value[]} value The initial array. * @param {?DataConstructorInput<readonly Value[], DataType>} [data] Custom data holder, optionally with params. */ constructor(value, data) { this.#data = new (Array.isArray(data) ? data[0] : data ?? Data)(value, ...Array.isArray(data) ? data.slice(1) : []); } /** * @description Destroys the storage data, by default setting it to `null`. * @public * @returns {this} The `this` current instance. */ destroy() { this.#data.destroy(); return this; } /** * @description Gets the readonly history. * @public * @returns {readonly Value[]} */ get() { return this.#data.value; } /** * @description Checks whether the storage is empty. * @public * @returns {boolean} */ isEmpty() { return this.#data.value.length === 0; } /** * @description Sets the data value. * @protected * @param {readonly Value[]} value The data value of `Value[]` to set. * @returns {this} The `this` current instance. */ set(value) { this.#data.set(value); return this; } } // Abstract. /** * @description The core class for history append and prepend. * @export * @abstract * @class HistoryCore * @template Value * @template {number} [Size=number] * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryStorage<Value, DataType>} */ class HistoryCore extends HistoryStorage { /** * @description Returns the `string` tag representation of the `HistoryCore` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return HistoryCore.name; } /** * @description Gets the history. * @protected * @readonly * @type {readonly Value[]} */ get history() { return super.data.value; } /** * @description The maximum size of the history. * @public * @readonly * @type {Size} */ get size() { return this.#size; } /** * @description The maximum size of the history. * @type {Size} */ #size; /** * Creates an instance of `HistoryCore` child class. * @constructor * @param {Size} size The maximum undo history size. * @param {?DataConstructorInput<readonly Value[], DataType>} [data] Custom data holder. */ constructor(size, data) { super([], data); this.#size = size; } /** * @description Returns the value at the specified index in the history. * @public * @param {number} index The position in the history (0-based index). * @returns {Value | undefined} The value at the specified position. */ at(index) { return this.history[index]; } /** * @description Clears the history. * @public * @returns {this} The `this` current instance. */ clear() { super.set([]); return this; } /** * @description Returns the first value without modifying history. * @public * @abstract * @returns {(Value | undefined)} */ first() { return this.history[0]; } /** * @description Returns the last value without modifying history. * @public * @abstract * @returns {(Value | undefined)} */ last() { return this.history.at(-1); } /** * @description Sets the size for history. * @public * @param {Size} size The maximum size of `Size`. * @returns {this} The `this` current instance. */ setSize(size) { this.#size = size; return this; } //#endregion //#region protected method /** * @description "Removes the last element from an array and returns it. If the array is empty, undefined is returned and the array is not modified." * @protected * @returns {this} The `this` current instance. */ pop() { const history = [...this.history]; if (Array.isArray(history) && history.length > 0) { const last = history.pop(); this.set(history); return last; } return undefined; } /** * @description "Appends new elements to the end of an array, and returns the new length of the array." * @protected * @param {...readonly Value[]} items The items to append. * @returns {number} The new length. */ push(...items) { const history = [...this.history]; const length = history.push(...items); return (this.set(history), length); } /** * @description "Removes the first element from an array and returns it. If the array is empty, undefined is returned and the array is not modified." * @protected * @returns {this} The `this` current instance. */ shift() { const history = [...this.history]; if (Array.isArray(history) && history.length > 0) { const first = history.shift(); this.set(history); return first; } return undefined; } /** * @description The method to trim the history. * @protected * @param {('pop' | 'shift')} method The method `pop` or `shift` to trim the history. * @returns {this} The `this` current instance. */ trim(method) { if (this.#size > 0) { while (this.history.length > this.size) { method === 'pop' ? this.pop() : method === 'shift' && this.shift(); } } return this; } /** * @description "Inserts new elements at the start of an array, and returns the new length of the array." * @protected * @param {...readonly Value[]} items The items to insert. * @returns {number} The new length. */ unshift(...items) { const history = [...this.history]; const length = history.unshift(...items); return (this.set(history), length); } } // Abstract. /** * @description Class extends the `HistoryCore` class to maintain a history of values in a append manner. * This means that new entries are added to the end of the history, and as the history exceeds its size limit, entries from the beginning are removed. * LIFO(Last in, First out): The last value that was added (the most recent one) will be the first one to be removed. * Add: Add to the end of the array (push). * Take: Remove from the end of the array (pop), which is the most recent item. * PeekFirst: Look at the first item in the history (oldest). * PeekLast: Look at the last item in the history (newest). * @export * @abstract * @class HistoryAppend * @template Value * @template {number} [Size=number] * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryCore<Value, Size, DataType>} */ class HistoryAppend extends HistoryCore { /** * @description The default value of maximum history size. * @public * @static * @type {number} */ static size = 10; /** * @description Returns the `string` tag representation of the `HistoryAppend` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return HistoryAppend.name; } /** * Creates an instance of `HistoryAppend` child class. * @constructor * @param {Size} [size=HistoryAppend.size as Size] The maximum history size. * @param {?readonly Value[]} [initialValue] Initial value to add. * @param {?DataConstructorInput<readonly Value[], DataType>} [data] Custom data holder. */ constructor(size = HistoryAppend.size, initialValue, data) { super(size, data); Array.isArray(initialValue) && initialValue.forEach(value => this.add(value)); } /** * @description Adds the value to the history. * - FIFO unshift/queue style * @public * @param {Value} value The value to store. * @returns {this} The `this` current instance. */ add(value) { if (super.size > 0) { super.push(value); super.trim('shift'); } return this; } /** * @description Returns the most recent (last index added) value in the history without modifying it. * @public * @returns {(Value | undefined)} */ newest() { return super.last(); } /** * @description Returns the next value that would be removed (the most recent one) without modifying history. * - LIFO behavior * @public * @returns {(Value | undefined)} The next value in the append manner. */ next() { return super.last(); } /** * @description Returns the first(index 0 the oldest) value in the history without modifying it. * @public * @returns {(Value | undefined)} */ oldest() { return super.first(); } /** * @description Removes and returns the last value in the history. * - LIFO behavior * @public * @returns {(Value | undefined)} */ take() { return super.pop(); } } // Abstract. /** * @description The class represents the current value of the history. * The class is used to: * - store the current value of the history, * - check whether the current value is set, * - update the current value of the history, * - clear the current value of the history, * - get the current value of the history. * @export * @abstract * @class HistoryCurrent * @template Value * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryStorage<Value, DataType>} */ class HistoryCurrent extends HistoryStorage { /** * @description Returns the `string` tag representation of the `HistoryCurrent` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return HistoryCurrent.name; } /** * Creates an instance of `HistoryCurrent` child class. * @constructor * @param {{value?: Value}} [param0={}] * @param {Value} param0.value * @param {?DataConstructor<readonly Value[], DataType>} [data] */ constructor({ value } = {}, data) { super(Object.hasOwn(arguments[0] || {}, 'value') ? [value] : [], data); } /** * @description Clears the `current` history. * @public * @returns {this} The current instance. */ clear() { super.set([]); return this; } } // Abstract. /** * @description Class extends the `HistoryCore` class to maintain a history of values in a prepend manner. * This means that new entries are added to the beginning of the history, and older entries are shifted out as the history size exceeds its limit. * LIFO(Last in, First out): The last value that was added (the most recent one) will be the first one to be removed. * Add: Add to the beginning of the array (unshift). * Take: Remove from the beginning of the array (shift), which is the most recent item. * PeekFirst: Look at the first item in the history (newest). * PeekLast: Look at the last item in the history (oldest). * @export * @abstract * @class HistoryPrepend * @template Value * @template {number} [Size=number] * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryCore<Value, Size, DataType>} */ class HistoryPrepend extends HistoryCore { /** * @description The default value of maximum history size. * @public * @static * @type {number} */ static size = 10; /** * @description Returns the `string` tag representation of the `HistoryPrepend` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return HistoryPrepend.name; } /** * Creates an instance of `HistoryPrepend` child class. * @constructor * @param {Size} [size=HistoryPrepend.size as Size] The maximum history size. * @param {?readonly Value[]} [initialValue] Initial value to add. * @param {?DataConstructorInput<readonly Value[], DataType>} [data] Custom data holder. */ constructor(size = HistoryPrepend.size, initialValue, data) { super(size, data); Array.isArray(initialValue) && initialValue.forEach(value => this.add(value)); } /** * @description Adds the value to the history in a backward manner. * @public * @param {Value} value The value to store. * @returns {this} The `this` current instance. */ add(value) { if (super.size > 0) { super.unshift(value); super.trim('pop'); } return this; } /** * @description Returns the next(index 0) value in the history, the newest value without modifying history. * @public * @returns {Value | undefined} The next redo value. */ next() { return super.first(); } /** * @description Returns the most recent(first index 0) value in the history without modifying it. * @public * @returns {(Value | undefined)} */ newest() { return super.first(); } /** * @description Returns the last value in the history, the oldest value without modifying history. * @public * @returns {Value | undefined} The next redo value. */ oldest() { return super.last(); } /** * @description Removes and returns the first value in the history. * @public * @returns {(Value | undefined)} */ take() { return super.shift(); } } // Abstract. /** * @description * @export * @class CurrentHistory * @template Value * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryCurrent<Value, DataType>} */ class CurrentHistory extends HistoryCurrent { /** * @description Returns the `string` tag representation of the `CurrentHistory` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return CurrentHistory.name; } /** * @description The current history value. * @public * @readonly * @type {Value} */ get value() { return Array.isArray(super.data.value) ? super.data.value[0] : undefined; } /** * Creates an instance of `CurrentHistory`. * @constructor * @param {{value?: Value}} [param0={}] THe object with current `value`. * @param {Value} param0.value The current value inside the object. * @param {?DataConstructorInput<Value, DataType>} [data] Custom data holder. */ constructor({ value } = {}, data) { super(arguments[0], data); } /** * @description Destroys the history of this instance. * @public * @returns {this} The `this` current instance. */ destroy() { super.clear(); super.destroy(); return this; } /** * @description Checks whether the current value is set. * @public * @returns {boolean} Indicates whether instance has set the current value. */ has() { return Array.isArray(super.data.value) && super.data.value.length > 0; } /** * @description Updates a current value. * @public * @param {Value} value The current value. * @returns {this} The `this` current instance. */ update(value) { super.set([value]); return this; } } /** * @description The base abstract class to manage history. * @export * @abstract * @class HistoryBase * @template Value * @template {number} [Size=number] * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] */ class HistoryBase { /** * @description The max size for undo history. * @public * @static * @type {number} */ static size = Infinity; /** * @description Returns the `string` tag representation of the `HistoryBase` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return HistoryBase.name; } /** * @description Gets the current value stored in history. * @public * @readonly * @type {Value} */ get current() { return this.#current.value; } /** * @description Returns the current history of `CurrentHistory`. * @public * @readonly * @type {CurrentType} */ get currentHistory() { return this.#current; } /** * @description * @public * @readonly * @type {{ * current: DataType, * redo: DataType, * undo: DataType * }} */ get data() { return { current: this.#current.data, redo: this.#redo.data, undo: this.#undo.data, }; } /** * @description Returns the redo history. * @public * @readonly * @type {RedoType} */ get redoHistory() { return this.#redo; } /** * @description Returns the undo history. * @public * @readonly * @type {UndoType} */ get undoHistory() { return this.#undo; } /** * @description A private field to store the current value. * @type {CurrentType} */ #current; /** * @description Privately stored callback function invoked on redo. * @type {(value: Value) => void} */ #onRedoCallback; /** * @description Privately stored callback function invoked on undo. * @type {(value: Value) => void} */ #onUndoCallback; /** * @description The class to manage the redo history. * @type {RedoType} */ #redo; /** * @description The class to manage the undo history. * @type {UndoType} */ #undo; /** * Creates an instance of `HistoryBase` child class. * @constructor * @param {{ value?: Value, size?: Size}} [param0={}] * @param {Value} param0.value * @param {Size} param0.size * @param {?DataConstructorInput<readonly Value[], DataType>} [data] * @param {{ * current?: HistoryCurrentConstructor<Value, DataType, CurrentType>, * redo?: HistoryCoreConstructor<Value, Size, DataType, RedoType>, * undo?: HistoryCoreConstructor<Value, Size, DataType, UndoType>, * }} [param1={}] * @param {HistoryCurrentConstructor<Value, DataType, CurrentType>} param1.current * @param {HistoryCoreConstructor<Value, Size, DataType, RedoType>} param1.redo * @param {HistoryCoreConstructor<Value, Size, DataType, UndoType>} param1.undo */ constructor({ value, size } = {}, data, { current, redo, undo } = {}) { this.#current = new (current)(arguments[0], data); this.#redo = new (redo)(size, undefined, data); this.#undo = new (undo)(size, undefined, data); } /** * @description Clears the `current`, `undo` and `redo` history. * @public * @returns {this} The `this` current instance. */ clear() { this.#current.clear(); this.#redo.clear(); this.#undo.clear(); return this; } /** * @description Destroys the history of this instance. * @public * @returns {this} The `this` current instance. */ destroy() { this.clear(); this.#current.destroy(); this.#redo.destroy(); this.#undo.destroy(); return this; } //#region get /** * @description Gets the current, undo and redo history. * @public * @returns {{ current: Readonly<Value>, undo: ImmutableArray<Value>; redo: ImmutableArray<Value> }} */ get() { return { current: this.#current.value, undo: this.#undo.get(), redo: this.#redo.get(), }; } /** * @description The instance method returns read-only redo history. * @public * @template Type * @returns {(ImmutableArray<Value>)} */ getRedo() { return this.#redo.get(); } /** * @description The instance method returns read-only undo history. * @public * @template Type * @returns {(ImmutableArray<Value>)} */ getUndo() { return this.#undo.get(); } //#endregion /** * @description Checks whether the current value is set. * @public * @returns {boolean} */ hasCurrent() { return this.#current.has(); } /** * @description Checks whether the history is enabled by checking undo size. * @public * @returns {boolean} */ isEnabled() { return this.#undo.size > 0 === true; } //#region on /** * @description Sets the callback function invoked on redo. * @public * @param {(value: Value) => void} callbackFn The callback function to invoke. * @returns {this} */ onRedo(callbackFn) { this.#onRedoCallback = callbackFn; return this; } /** * @description Sets the callback function invoked on undo. * @public * @param {(value: Value) => void} callbackFn The callback function to invoke. * @returns {this} */ onUndo(callbackFn) { this.#onUndoCallback = callbackFn; return this; } //#endregion /** * @description Returns the specified by index value from redo history. * @public * @param {number} [index=0] * @returns {(Value | undefined)} */ redoAt(index = 0) { return this.#redo.at(index); } /** * @description Returns the specified by index value from undo history. * @public * @param {number} [index=this.#undo.length - 1] * @returns {(Value | undefined)} */ undoAt(index = this.#undo.length - 1) { return this.#undo.at(index); } //#region first /** * @description Returns the first value that would be redone without modifying history. * @public * @returns {Value | undefined} The first redo value. */ firstRedo() { return this.#redo.first(); } /** * @description Returns the first value that would be undone without modifying history. * @public * @returns {Value | undefined} The first undo value. */ firstUndo() { return this.#undo.first(); } //#endregion //#region last /** * @description Returns the last value that would be redone without modifying history. * @public * @returns {Value | undefined} The last redo value. */ lastRedo() { return this.#redo.last(); } /** * @description Returns the last value that would be undone without modifying history. * @public * @returns {Value | undefined} The last undo value. */ lastUndo() { return this.#undo.last(); } //#endregion //#region next /** * @description Returns the next value that would be redone without modifying history. * @public * @returns {Value | undefined} The next redo value. */ nextRedo() { return this.#redo.next(); } /** * @description Returns the next value that would be undone without modifying history. * @public * @returns {Value | undefined} The next undo value. */ nextUndo() { return this.#undo.next(); } //#endregion /** * @description Pick the current, redo or undo history. * @public * @param {('current' | 'redo' | 'undo')} type * @returns {ImmutableArray<Value>} */ pick(type) { switch (type) { case 'current': return this.#current.get(); case 'redo': return this.#redo.get(); case 'undo': return this.#undo.get(); default: throw new Error(`Invalid type: ${type}. Expected 'current', 'redo', or 'undo'.`); } } /** * @description Redoes the last undone action. * @public * @returns {this} The current instance. */ redo() { const redo = this.#redo; if (redo.get()?.length) { const value = redo.take(); this.#undo.add(this.#current.value); this.#current.update(value); this.#onRedoCallback?.(value); } return this; } /** * @description Sets a new value and updates the undo history. * @public * @param {Value} value * @returns {this} The `this` current instance. */ set(value) { this.#current.has() && this.#undo.add(this.#current.value); this.#current.update(value); this.#redo.clear(); return this; } /** * @description Sets the size of undo history. * @public * @param {Size} size The maximum size for undo history. */ setSize(size) { this.#undo.setSize(size); return this; } /** * @description Undoes the last action and moves it to redo history. * @public * @returns {this} The current instance. */ undo() { const undo = this.#undo; if (undo.get()?.length) { const value = undo.take(); this.#redo.add(this.#current.value); this.#current.update(value); this.#onUndoCallback?.(value); } return this; } } // Abstract. /** * @description Manages the redo history with prepend mechanism. * @export * @class RedoHistory * @template [Value=any] The type of elements stored in the history * @template {number} [Size=number] The maximum size of the history. * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryPrepend<Value, Size, DataType>} */ class RedoHistory extends HistoryPrepend { /** * Creates an instance of `RedoHistory`. * @constructor * @param {Size} [size=RedoHistory.size as Size] * @param {?readonly [Value]} [initialValue] * @param {?DataConstructorInput<readonly Value[], DataType>} [data] */ constructor(size = RedoHistory.size, initialValue, data) { super(size, initialValue, data); } } // Abstract. /** * @description Manages the undo history with append mechanism. * @export * @class UndoHistory * @template [Value=any] The type of elements stored in the history. * @template {number} [Size=number] The maximum size of the history. * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] * @extends {HistoryAppend<Value, Size, DataType>} */ class UndoHistory extends HistoryAppend { /** * Creates an instance of `UndoHistory`. * @constructor * @param {Size} [size=HistoryAppend.size as Size] * @param {?readonly [Value]} [initialValue] * @param {?DataConstructorInput<readonly Value[], DataType>} [data] */ constructor(size = HistoryAppend.size, initialValue, data) { super(size, initialValue, data); } } // Abstract. /** * @description The class to manage the value changes. * @export * @class History * @template Value * @template {number} [Size=number] * @template {DataCore<readonly Value[]>} [DataType=Data<readonly Value[]>] */ class History extends HistoryBase { /** * @description The max size for undo history. * @public * @static * @type {number} */ static size = Infinity; /** * @description Returns the `string` tag representation of the `History` class when used in `Object.prototype.toString.call(instance)`. * @public * @readonly * @type {string} */ get [Symbol.toStringTag]() { return History.name; } /** * Creates an instance of `History`. * @constructor * @param {{ value?: Value, size?: Size}} [param0={}] The optional `value` and maximum undo history `size`. * @param {Value} param0.value The initial value. * @param {Size} param0.size The maximum undo history size. * @param {?DataConstructorInput<readonly Value[], DataType>} [data] The custom data holder for history, current, undo and redo. * @param {{ * current?: HistoryCurrentConstructor<Value, DataType, CurrentType>, * redo?: HistoryCoreConstructor<Value, Size, DataType, RedoType>, * undo?: HistoryCoreConstructor<Value, Size, DataType, UndoType>, * }} [param1={}] * @param {HistoryCurrentConstructor<Value, DataType, CurrentType>} param1.current Custom current history class of `HistoryCurrent`. * @param {HistoryCoreConstructor<Value, Size, DataType, RedoType>} param1.redo Custom redo history class of `HistoryCore`. * @param {HistoryCoreConstructor<Value, Size, DataType, UndoType>} param1.undo Custom undo history class of `HistoryCore`. */ constructor({ value, size } = {}, data, { current, redo, undo } = {}) { super(arguments[0], data, { current: current ?? CurrentHistory, redo: redo ?? RedoHistory, undo: undo ?? UndoHistory }); } } /* * Public API Surface of history */ // Main. /** * Generated bundle index. Do not edit. */ export { CurrentHistory, History, HistoryAppend, HistoryBase, HistoryCore, HistoryCurrent, HistoryPrepend, HistoryStorage, RedoHistory, UndoHistory }; //# sourceMappingURL=typescript-package-history.mjs.map