@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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;
}
}