Загрузка данных
import { BehaviorSubject, Observable } from 'rxjs';
import {
ChartSeriesType,
Intervals,
SymbolInfo,
TimeFormat,
Timeframes,
} from '@src/types';
import { DateFormat } from '@src/utils';
export type UndoKey = keyof UndoConfig;
export interface UndoRedoState {
canUndo: boolean;
canRedo: boolean;
}
interface UndoConfig {
timeframe: (value: Timeframes) => void;
seriesSelected: (value: ChartSeriesType) => void;
symbolInfo: (value: SymbolInfo) => void;
timeFormat: (value: TimeFormat) => void;
dateFormat: (value: DateFormat) => void;
interval: (value: Intervals | null) => void;
}
interface HistoryItem {
type: 'item';
key: UndoKey;
previousValue: unknown;
nextValue: unknown;
}
interface HistoryCommand {
type: 'command';
undo: () => void;
redo: () => void;
}
interface HistoryGroup {
type: 'group';
entries: HistoryEntry[];
}
type HistoryEntry =
| HistoryItem
| HistoryCommand
| HistoryGroup;
interface PushCommandParams {
undo: () => void;
redo: () => void;
}
export class UndoRedo {
private undoStack: HistoryEntry[] = [];
private redoStack: HistoryEntry[] = [];
private groupStack: HistoryEntry[][] = [];
private state$ = new BehaviorSubject<UndoRedoState>({
canUndo: false,
canRedo: false,
});
constructor(private readonly config: UndoConfig) {}
public push(
key: UndoKey,
previousValue: unknown,
nextValue: unknown,
): void {
if (Object.is(previousValue, nextValue)) {
return;
}
this.pushEntry({
type: 'item',
key,
previousValue,
nextValue,
});
}
public pushCommand({
undo,
redo,
}: PushCommandParams): void {
this.pushEntry({
type: 'command',
undo,
redo,
});
}
public group<T>(callback: () => T): T {
this.groupStack.push([]);
try {
return callback();
} finally {
this.completeGroup();
}
}
public undo = (): void => {
const entry = this.undoStack.pop();
if (!entry) {
return;
}
this.applyEntry(entry, 'undo');
this.redoStack.push(entry);
this.updateState();
};
public redo = (): void => {
const entry = this.redoStack.pop();
if (!entry) {
return;
}
this.applyEntry(entry, 'redo');
this.undoStack.push(entry);
this.updateState();
};
public canUndo(): boolean {
return this.undoStack.length > 0;
}
public canRedo(): boolean {
return this.redoStack.length > 0;
}
public getState(): Observable<UndoRedoState> {
return this.state$.asObservable();
}
public clear(): void {
this.undoStack = [];
this.redoStack = [];
this.groupStack = [];
this.updateState();
}
private completeGroup(): void {
const entries = this.groupStack.pop();
if (!entries?.length) {
return;
}
this.pushEntry({
type: 'group',
entries,
});
}
private pushEntry(entry: HistoryEntry): void {
if (this.redoStack.length > 0) {
this.redoStack = [];
}
const currentGroup =
this.groupStack[this.groupStack.length - 1];
if (currentGroup) {
currentGroup.push(entry);
return;
}
this.undoStack.push(entry);
this.updateState();
}
private applyEntry(
entry: HistoryEntry,
direction: 'undo' | 'redo',
): void {
if (entry.type === 'group') {
this.applyGroup(entry, direction);
return;
}
if (entry.type === 'command') {
entry[direction]();
return;
}
const apply = this.config[entry.key] as (
value: unknown,
) => void;
apply(
direction === 'undo'
? entry.previousValue
: entry.nextValue,
);
}
private applyGroup(
group: HistoryGroup,
direction: 'undo' | 'redo',
): void {
if (direction === 'undo') {
for (
let index = group.entries.length - 1;
index >= 0;
index -= 1
) {
this.applyEntry(
group.entries[index],
direction,
);
}
return;
}
group.entries.forEach((entry) => {
this.applyEntry(entry, direction);
});
}
private updateState(): void {
const nextState: UndoRedoState = {
canUndo: this.canUndo(),
canRedo: this.canRedo(),
};
const currentState = this.state$.value;
if (
currentState.canUndo === nextState.canUndo &&
currentState.canRedo === nextState.canRedo
) {
return;
}
this.state$.next(nextState);
}
}