UNPKG

@progress/kendo-angular-grid

Version:

Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.

239 lines (238 loc) 7.07 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { isPresent } from "@progress/kendo-angular-common"; /** * @hidden * A linked-list based implementation of an undo-redo stack. * Maintains a chain of states that can be navigated forward and backward. */ export class UndoRedoStack { maxSize; /** The current active node in the undo-redo history */ currentNode = null; /** The root node of the stack (first state) */ rootNode = null; /** Track the size of the stack */ _size = 0; /** * Creates a new UndoRedoStack. * @param maxSize Optional maximum number of states to maintain (unlimited if not provided) */ constructor(maxSize = -1) { this.maxSize = maxSize; } /** * Gets the current number of states in the stack */ get size() { return this._size; } /** * Gets the current active state */ get current() { return this.currentNode ? this.currentNode.state : null; } /** * Checks if undo is available (if there's a previous state) */ get canUndo() { return isPresent(this.currentNode?.previous); } /** * Checks if redo is available (if there's a next state) */ get canRedo() { return isPresent(this.currentNode?.next); } /** * Adds a new state to the undo-redo stack * @param state The state to add * @param id Optional identifier for the state * @returns The newly created node */ add(state, id) { const newNode = { state, previous: this.currentNode, next: null, id }; // If we have a current node, update its next reference if (this.currentNode) { // If we're adding after a node that already had a "next", // we need to discard that branch of history if (this.currentNode.next) { this.truncateForward(this.currentNode); } this.currentNode.next = newNode; } else { // This is the first node this.rootNode = newNode; } this.currentNode = newNode; this._size++; // If we've exceeded the max size, remove oldest nodes this.enforceMaxSize(); return newNode; } /** * Finds a node by its identifier * @param id The identifier to search for * @returns The found node or null if not found */ find(id) { if (!this.rootNode) { return null; } let node = this.rootNode; while (node) { if (node.id === id) { return node; } node = node.next; } return null; } /** * Removes a node by its identifier * @param id The identifier of the node to remove * @returns True if the node was found and removed, false otherwise */ remove(id) { const nodeToRemove = this.find(id); if (!nodeToRemove) { return false; } // Handle removal of current node if (nodeToRemove === this.currentNode) { this.currentNode = nodeToRemove.previous || nodeToRemove.next; } // Connect previous and next nodes if (nodeToRemove.previous) { nodeToRemove.previous.next = nodeToRemove.next; } else { // Removing the root node this.rootNode = nodeToRemove.next; } if (nodeToRemove.next) { nodeToRemove.next.previous = nodeToRemove.previous; } // Clean up references to help garbage collection nodeToRemove.previous = null; nodeToRemove.next = null; this._size--; return true; } /** * Performs an undo operation, moving to the previous state * @returns The previous state or null if can't undo */ undo() { if (!this.canUndo) { return null; } this.currentNode = this.currentNode.previous; return this.currentNode.state; } peekNext() { return this.currentNode.next?.state || null; } peekPrev() { return this.currentNode.previous?.state || null; } /** * Performs a redo operation, moving to the next state * @returns The next state or null if can't redo */ redo() { if (!this.canRedo) { return null; } this.currentNode = this.currentNode.next; return this.currentNode.state; } /** * Clears all history */ clear() { this.currentNode = null; this.rootNode = null; this._size = 0; } /** * Removes all states after the specified node * @param node The node to truncate from * @returns The number of nodes removed */ truncateForward(node) { if (!node.next) { return 0; } let removedCount = 0; let currentNext = node.next; while (currentNext) { const temp = currentNext.next; // Clean up references for garbage collection currentNext.previous = null; currentNext.next = null; currentNext = temp; removedCount++; } // Update the node's next pointer node.next = null; this._size -= removedCount; return removedCount; } /** * Ensures the stack doesn't exceed the maximum size by removing oldest nodes */ enforceMaxSize() { if (this.maxSize <= 0 || this._size <= this.maxSize) { return; } let nodesToRemove = this._size - this.maxSize; let currentNode = this.rootNode; // Find the new root node while (nodesToRemove > 0 && currentNode) { currentNode = currentNode.next; nodesToRemove--; } if (currentNode) { // Disconnect from previous history currentNode.previous = null; // Update root node this.rootNode = currentNode; // Update size this._size = this.maxSize; } } /** * Gets all states in the stack as an array (from oldest to newest) */ toArray() { const result = []; let node = this.rootNode; while (node) { result.push(node.state); node = node.next; } return result; } /** * Gets the history nodes as an array (useful for debugging) */ getNodes() { const result = []; let node = this.rootNode; while (node) { result.push(node); node = node.next; } return result; } }