@typescript-package/history
Version:
A lightweight TypeScript package for tracking the history of values.
934 lines (920 loc) • 25.8 kB
JavaScript
import { Data } from '@typescript-package/data';
/**
* @description The history storage of specified data.
* @export
* @abstract
* @class HistoryStorage
* @template Value
* @template {DataCore<Value[]>} [DataType=Data<Value[]>]
* @extends {DataCore<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
* @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 {Value[]} value
* @param {?DataConstructor<Value, DataType>} [data]
*/
constructor(value, data) {
this.#data = data ? new data(value) : new Data(value);
}
/**
* @description Destroys the storage data by setting it to `null`.
* @public
* @returns {this} Returns the 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 {Value[]} value The data of `Type[]` to set.
* @returns {this} Returns `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<Value[]>} [DataType=Data<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 {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 {number}
*/
#size;
/**
* Creates an instance of `HistoryCore` child class.
* @constructor
* @param {Size} size
* @param {?DataConstructor<Value, DataType>} [data]
*/
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 current instance.
*/
clear() {
const history = this.history;
history.length = 0;
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
* @returns {this} The `this` current instance.
*/
setSize(size) {
this.#size = size;
return this;
}
/**
* @description The method to trim the history.
* @protected
* @param {('pop' | 'shift')} method The method `pop` or `shift` to trim the history.
* @returns {this}
*/
trim(method) {
if (this.#size > 0) {
while (this.history.length > this.size) {
method === 'pop' ? this.history.pop() : method === 'shift' && this.history.shift();
}
}
return this;
}
}
// 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<Value[]>} [DataType=Data<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]
* @param {?DataConstructor<Value, DataType>} [data]
*/
constructor(size = HistoryAppend.size, data) {
super(size, data);
}
/**
* @description Adds the value to the history.
* - FIFO unshift/queue style
* @public
* @param {Value} value The value to store.
* @returns {this} The current instance.
*/
add(value) {
if (super.size > 0) {
super.history.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 this.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.history.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<Value[]>} [DataType=Data<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<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<Value[]>} [DataType=Data<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]
* @param {?DataConstructor<Value, DataType>} [data]
*/
constructor(size = HistoryPrepend.size, data) {
super(size, data);
}
/**
* @description Adds the value to the history in a backward manner.
* @public
* @param {Value} value The value to store.
* @returns {this} The current instance.
*/
add(value) {
if (super.size > 0) {
super.history.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 this.first();
}
/**
* @description Returns the most recent(first index 0) value in the history without modifying it.
* @public
* @returns {(Value | undefined)}
*/
newest() {
return this.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 this.last();
}
/**
* @description Removes and returns the first value in the history.
* @public
* @returns {(Value | undefined)}
*/
take() {
return super.history.shift();
}
}
// Abstract.
/**
* @description
* @export
* @class CurrentHistory
* @template Value
* @template {DataCore<Value[]>} [DataType=Data<Value[]>]
* @extends {HistoryStorage<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
* @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={}]
* @param {Value} param0.value
* @param {?DataConstructor<Value, DataType>} [data]
*/
constructor({ value } = {}, data) {
super(arguments[0], data);
}
/**
* @description Destroys the history of this instance.
* @public
* @returns {this} The current instance.
*/
destroy() {
super.clear();
super.destroy();
return this;
}
/**
* @description Checks whether the current value is set.
* @public
* @returns {boolean}
*/
has() {
return Array.isArray(super.data.value) && super.data.value.length > 0;
}
/**
* @description Updates a current value.
* @public
* @param {Value} value
* @returns {this}
*/
update(value) {
super.set([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<Value[]>} [DataType=Data<Value[]>]
* @extends {HistoryPrepend<Value, Size, DataType>}
*/
class RedoHistory extends HistoryPrepend {
}
;
// 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<Value[]>} [DataType=Data<Value[]>]
* @extends {HistoryAppend<Value, Size, DataType>}
*/
class UndoHistory extends HistoryAppend {
}
;
// Class.
/**
* @description The base abstract class to manage history.
* @export
* @abstract
* @class HistoryBase
* @template Value
* @template {number} [Size=number]
* @template {DataCore<Value[]>} [DataType=Data<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 {CurrentHistory<Value, DataType>}
*/
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 {HistoryCore<Value, Size, DataType>}
*/
get redoHistory() {
return this.#redo;
}
/**
* @description Returns the undo history.
* @public
* @readonly
* @type {HistoryCore<Value, Size, DataType>}
*/
get undoHistory() {
return this.#undo;
}
/**
* @description A private field to store the current value.
* @type {CurrentHistory<Value, DataType>}
*/
#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 {RedoHistory}
*/
#redo;
/**
* @description The class to manage the undo history.
* @type {UndoHistory}
*/
#undo;
/**
* Creates an instance of `HistoryBase` child class.
* @constructor
* @param {{ value?: Value, size?: Size}} [param0={}]
* @param {Value} param0.value
* @param {Size} param0.size
* @param {?DataConstructor<Value, DataType>} [data]
* @param {{
* current?: HistoryCurrentConstructor<Value, DataType>,
* redo?: HistoryCoreConstructor<Value, Size, DataType>,
* undo?: HistoryCoreConstructor<Value, Size, DataType>,
* }} [param1={}]
* @param {HistoryCurrentConstructor<Value, DataType>} param1.current
* @param {HistoryCoreConstructor<Value, Size, DataType>} param1.redo
* @param {HistoryCoreConstructor<Value, Size, DataType>} param1.undo
*/
constructor({ value, size } = {}, data, { current, redo, undo } = {}) {
this.#current = new (current || CurrentHistory)(arguments[0], data);
this.#redo = new (redo || RedoHistory)(size || Infinity, data);
this.#undo = new (undo || UndoHistory)(size, data);
}
/**
* @description Clears the `current`, `undo` and `redo` history.
* @public
* @returns {this} The 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 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: Readonly<Value[]>; redo: Readonly<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 {(Readonly<Value[]> | undefined)}
*/
getRedo() {
return this.#redo.get();
}
/**
* @description The instance method returns read-only undo history.
* @public
* @template Type
* @returns {(Readonly<Value[]> | undefined)}
*/
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 {Readonly<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 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 The class to manage the value changes.
* @export
* @class History
* @template Value
* @template {number} [Size=number]
* @template {DataCore<Value[]>} [DataType=Data<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;
}
}
/*
* Public API Surface of history
*/
/**
* Generated bundle index. Do not edit.
*/
export { History, HistoryAppend, HistoryBase, HistoryCore, HistoryCurrent, HistoryPrepend, HistoryStorage };
//# sourceMappingURL=typescript-package-history.mjs.map