@progress/kendo-angular-grid
Version:
Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.
250 lines (249 loc) • 11 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 { Directive, EventEmitter, Input, Output } from '@angular/core';
import { UndoRedoStack } from './undo-redo.stack';
import { GridComponent } from '../grid.component';
import { UndoRedoEvent } from './grid-state.models';
import { Subscription } from 'rxjs';
import { EditService } from '../editing/edit.service';
import { filter } from 'rxjs/operators';
import { UndoRedoService } from './undo-redo.service';
import { hasObservers, isPresent } from '@progress/kendo-angular-common';
import { ChangeNotificationService } from '../data/change-notification.service';
import { ContextService } from '../common/provider.service';
import { LocalDataChangesService } from '../editing/local-data-changes.service';
import { recursiveFlatMap } from '../utils';
import * as i0 from "@angular/core";
import * as i1 from "../grid.component";
import * as i2 from "../editing/edit.service";
import * as i3 from "./undo-redo.service";
import * as i4 from "../data/change-notification.service";
import * as i5 from "../common/provider.service";
import * as i6 from "../editing/local-data-changes.service";
/**
* Represents the directive that manages undo-redo operations in the Grid.
* Use this directive to enable undo and redo functionality for user actions in the Grid.
*
* @example
* ```html
* <kendo-grid [data]="data" kendoGridUndoRedo></kendo-grid>
* ```
* @remarks
* Applied to: {@link GridComponent}.
*/
export class UndoRedoDirective {
host;
editService;
undoRedoService;
changeNotification;
ctx;
localDataChangesService;
/**
* Sets the maximum number of actions to keep in the undo-redo stack.
* @default 10
*/
maxStoredStates = 10;
/**
* Defines the property name of the data item unique key that will be used to identify the items when performing undo-redo actions.
*/
itemIdKey;
/**
* Fires when you perform the undo action. Provides the Grid state to apply.
*/
onUndo = new EventEmitter();
/**
* Fires when you perform the redo action. Provides the Grid state to apply.
*/
onRedo = new EventEmitter();
/**
* Returns all undo-redo actions currently in the stack.
*/
get undoRedoItems() {
return this.stack.toArray();
}
stack;
subs = new Subscription();
addToState = true;
constructor(host, editService, undoRedoService, changeNotification, ctx, localDataChangesService) {
this.host = host;
this.editService = editService;
this.undoRedoService = undoRedoService;
this.changeNotification = changeNotification;
this.ctx = ctx;
this.localDataChangesService = localDataChangesService;
this.host.undoRedoService = this.undoRedoService;
}
ngOnInit() {
this.stack = new UndoRedoStack(this.maxStoredStates);
this.subs = this.host.gridStateChange.subscribe((state) => {
if (this.addToState) {
this.stack.add({
originalEvent: {
skip: state.skip,
take: state.take,
sort: state.sort,
filter: state.filter,
group: state.group
},
gridState: state
});
}
let stackEndPointReached;
if (this.stack.canUndo) {
stackEndPointReached = this.stack.canRedo ? false : 'end';
}
else {
stackEndPointReached = 'start';
}
this.undoRedoService.stackEndReached.next(stackEndPointReached);
});
this.subs.add(this.editService.changes
.pipe(filter((event) => event.action === 'save' || event.action === 'remove'))
.subscribe(event => {
this.stack.add({
originalEvent: { ...event, dataItem: structuredClone(event.dataItem) },
gridState: this.host.currentState
});
this.addToState = false;
this.host.gridStateChange.emit(this.stack.current.gridState);
this.addToState = true;
this.updateUndoRedoDisabled();
}));
this.subs.add(this.changeNotification.changes.subscribe(() => {
if (!this.ctx.dataBindingDirective) {
this.stack.current.gridState = this.host.currentState;
}
}));
['Undo', 'Redo'].forEach((action) => {
this.subs.add(this.undoRedoService[`on${action}`].subscribe(() => {
if (!this.stack[`can${action}`]) {
return;
}
let eventData;
if (action === 'Undo') {
const isSaveOrRemove = this.stack.current.originalEvent.action === 'save' || this.stack.current.originalEvent.action === 'remove';
eventData = isSaveOrRemove ? this.stack.current : this.stack.peekPrev();
}
else {
eventData = this.stack.peekNext();
}
const event = new UndoRedoEvent(eventData);
if (hasObservers(this[`on${action}`])) {
this[`on${action}`].emit(event);
if (event.isDefaultPrevented()) {
return;
}
}
this.stack[`${action.toLowerCase()}`]();
this.updateUndoRedoDisabled();
const originalAction = event.originalEvent.action;
const isLocalData = isPresent(this.ctx?.dataBindingDirective);
if (!isLocalData) {
return;
}
const isSaveOrRemove = originalAction === 'save' || originalAction === 'remove';
if (isSaveOrRemove) {
if (originalAction === 'save') {
const stateItem = this.getGridDataItems(this.stack.current.gridState.currentData).find(item => item[this.itemIdKey] === event.originalEvent.dataItem[this.itemIdKey]);
this.localDataChangesService?.data.splice(event.originalEvent.rowIndex, 1, stateItem);
}
else if (action === 'Undo') {
this.localDataChangesService?.data.splice(event.originalEvent.rowIndex, 0, event.originalEvent.dataItem);
}
else {
this.localDataChangesService?.data.splice(event.originalEvent.rowIndex, 1);
}
this.localDataChangesService?.changes.emit();
}
else {
this.host.loadState({ ...this.stack.current.gridState, currentData: null });
if (this.isDataStateChangeEvent(event.originalEvent)) {
const { skip, take, sort, filter, group } = event.gridState;
this.host.dataStateChange.emit({ skip, take, sort, filter, group });
}
}
}));
});
}
ngAfterViewInit() {
this.stack.add({
originalEvent: {
skip: this.host.skip,
take: this.host.pageSize,
sort: this.host.sort,
filter: this.host.filter,
group: this.host.group
}, gridState: this.host.currentState
});
}
ngOnDestroy() {
this.stack.clear();
this.stack = null;
this.subs.unsubscribe();
}
/**
* Re-applies the last action that you reverted with the `undo` method.
*/
redo() {
if (this.stack.canRedo) {
this.stack.redo();
this.host.loadState(this.stack.current.gridState);
if (!this.stack.canRedo) {
this.undoRedoService.stackEndReached.next('end');
}
}
}
/**
* Reverts the last user action that you performed.
*/
undo() {
if (this.stack.canUndo) {
this.stack.undo();
this.host.loadState(this.stack.current.gridState);
if (!this.stack.canUndo) {
this.undoRedoService.stackEndReached.next('start');
}
}
}
updateUndoRedoDisabled() {
if (!this.stack.canRedo) {
this.undoRedoService.stackEndReached.next('end');
return;
}
if (!this.stack.canUndo) {
this.undoRedoService.stackEndReached.next('start');
return;
}
this.undoRedoService.stackEndReached.next(false);
}
getGridDataItems(data) {
return Array.isArray(data) ? data.flatMap(recursiveFlatMap) :
data.data.flatMap(recursiveFlatMap);
}
isDataStateChangeEvent(event) {
return event && ['skip', 'take', 'sort', 'filter', 'group'].some(prop => prop in event);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UndoRedoDirective, deps: [{ token: i1.GridComponent }, { token: i2.EditService }, { token: i3.UndoRedoService }, { token: i4.ChangeNotificationService }, { token: i5.ContextService }, { token: i6.LocalDataChangesService }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: UndoRedoDirective, isStandalone: true, selector: "[kendoGridUndoRedo]", inputs: { maxStoredStates: "maxStoredStates", itemIdKey: "itemIdKey" }, outputs: { onUndo: "undo", onRedo: "redo" }, providers: [UndoRedoService], exportAs: ["kendoGridUndoRedo"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UndoRedoDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoGridUndoRedo]',
standalone: true,
exportAs: 'kendoGridUndoRedo',
providers: [UndoRedoService]
}]
}], ctorParameters: () => [{ type: i1.GridComponent }, { type: i2.EditService }, { type: i3.UndoRedoService }, { type: i4.ChangeNotificationService }, { type: i5.ContextService }, { type: i6.LocalDataChangesService }], propDecorators: { maxStoredStates: [{
type: Input
}], itemIdKey: [{
type: Input
}], onUndo: [{
type: Output,
args: ['undo']
}], onRedo: [{
type: Output,
args: ['redo']
}] } });