UNPKG

undoo

Version:
342 lines (302 loc) 8.47 kB
const extend = require('defaulty'); const isEqual = require('fast-deep-equal'); /** * @class */ class Undoo { /** * Create instance * @param [opts] {Object} configuration object * @param [opts.provider=null] {Function} optional function called on save that returns new state for history * @param [opts.maxLength=20] {number} max length history */ constructor(opts) { Object.defineProperties(this, { _opts: { writable: true }, _history: { writable: true }, _position: { writable: true }, _initialState: { writable: true }, _onUpdate: { writable: true, value: ()=>{} }, _onBeforeSave: { writable: true, value: ()=>{} }, _onMaxLength: { writable: true, value: ()=>{} }, _isExceeded: { writable: true, value: false }, _suspendSave: { writable: true, value: false } }); this._opts = extend.copy(opts, { provider: null, maxLength: 20 }); this._initiliaze(); } /** * @ignore * @private */ _initiliaze() { this._initialState = undefined; this._history = []; this._isExceeded = false; this._position = 0; } /** * @ignore * @private */ _checkMaxLength() { if (this._history.length > this._opts.maxLength) { this._history = this._history.slice(1, this._history.length); if (!this._isExceeded) { this._onMaxLength.call(null, this.current(), this.history(), this); this._isExceeded = true; } } else { this._isExceeded = false; } } /** * * @param item * @param beforeSave * @returns {boolean|*} * @private * @ignore */ _rejectSave(item, beforeSave) { return isEqual(item, this.current()) || beforeSave === false || this._suspendSave; } /** * Check if undo is available * @returns {boolean} */ canUndo() { return this._position > 1; } /** * @Check if redo is available * @returns {boolean} */ canRedo() { return this._position < this._history.length; } /** * ignore * @param callback * @private */ static callbackError(callback) { if (typeof callback !== 'function') throw new TypeError('callback must be a function'); } /** * Import external history * @param history {Array} * @returns {Undoo} */ import(history = []) { if(!Array.isArray(history)) throw new TypeError('Items must be an array'); this._initiliaze(); this._history = history; this._position = this._history.length; this._initialState = history[0]; return this; } /** * Get history * @returns {Array} */ history() { return this._history; } /** * Save history * @param [item] {*} * @returns {Undoo} */ save(item) { if (typeof item === 'undefined' && typeof this._opts.provider === 'function') item = this._opts.provider(); let beforeSave = this._onBeforeSave.call(null, item, this); item = beforeSave || item; if (this._rejectSave(item, beforeSave)) return this; if (this._position < this._history.length) this._history = this._history.slice(0, this._position); if (typeof item !== 'undefined') { this._history.push(item); if (this._initialState === undefined) this._initialState = item; } this._checkMaxLength(); this._position = this._history.length; this._onUpdate.call(null, this.current(), 'save', this.history(), this); return this; } /** * Suspend save method * @param [state=true] {boolean} * @returns {Undoo} */ suspendSave(state = true) { this._suspendSave = state; return this; } /** * Check if save is allowed * @returns {boolean} */ allowedSave() { return !this._suspendSave; } /** * Clear history * @returns {Undoo} */ clear() { this._initiliaze(); this._onUpdate.call(null, null, 'clear', this.history(), this); return this; } /** * undo callback * @callback Undoo~undoCallback * @param item {*} current history item */ /** * Undo * @param [callback] {Undoo~undoCallback} callback function * @returns {Undoo} */ undo(callback) { if (this.canUndo()) { this._position--; if (typeof callback === 'function') callback(this.current()); this._onUpdate.call(null, this.current(), 'undo', this.history(), this); } return this; } /** * redo callback * @callback Undoo~redoCallback * @param item {*} current history item */ /** * Redo * @param [callback] {Undoo~redoCallback} callback function * @returns {Undoo} */ redo(callback) { if (this.canRedo()) { this._position++; if (typeof callback === 'function') callback(this.current()); this._onUpdate.call(null, this.current(), 'redo', this.history(), this); } return this; } /** * Get current item in history * @returns {*} */ current() { return this._history.length ? this._history[this._position - 1] : null; } /** * Count history items, the first element is not considered * @returns {number} */ count() { return this._history.length ? this._history.length - 1 : 0; } /** * Get initial state history * @returns {*} */ initialState() { return this._initialState; } /** * onUpdate callback * @callback Undoo~updateCallback * @param item {*} current history item * @param action {string} action that has called update event. Can be: redo, undo, save, clear * @param history {Array} history array * @param istance {Undoo} */ /** * Triggered when history is updated * @param callback {Undoo~updateCallback} callback function * @returns {Undoo} */ onUpdate(callback) { Undoo.callbackError(callback); this._onUpdate = callback; return this; } /** * onMaxLength callback * @callback Undoo~maxLengthCallback * @param item {*} current history item * @param history {Array} history array * @param istance {Undoo} */ /** * Triggered when maxLength is exceeded * @param callback {Undoo~maxLengthCallback} callback function * @returns {Undoo} */ onMaxLength(callback) { Undoo.callbackError(callback); this._onMaxLength = callback; return this; } /** * onBeforeSave callback * @callback Undoo~beforeSaveCallback * @param item {*} current history item * @param istance {Undoo} */ /** * Triggered before save * @param callback {Undoo~beforeSaveCallback} callback function * @returns {Undoo} * @example * // If callback returns `false` the save command will not be executed * myHistory.onBeforeSave(()=>false) * * // You can overwrite item before save * myHistory.onBeforeSave((item)=>{ * return item.toUpperCase(); * }) */ onBeforeSave(callback) { Undoo.callbackError(callback); this._onBeforeSave = callback; return this; } } module.exports = Undoo;