history.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { AppState } from "./types";
  2. import { ExcalidrawElement } from "./element/types";
  3. import { clearAppStatePropertiesForHistory } from "./appState";
  4. class SceneHistory {
  5. private recording: boolean = true;
  6. private stateHistory: string[] = [];
  7. private redoStack: string[] = [];
  8. private generateEntry(
  9. appState: AppState,
  10. elements: readonly ExcalidrawElement[],
  11. ) {
  12. return JSON.stringify({
  13. appState: clearAppStatePropertiesForHistory(appState),
  14. elements: elements.map(({ shape, canvas, ...element }) => ({
  15. ...element,
  16. shape: null,
  17. canvas: null,
  18. points:
  19. appState.multiElement && appState.multiElement.id === element.id
  20. ? element.points.slice(0, -1)
  21. : element.points,
  22. })),
  23. });
  24. }
  25. pushEntry(appState: AppState, elements: readonly ExcalidrawElement[]) {
  26. const newEntry = this.generateEntry(appState, elements);
  27. if (
  28. this.stateHistory.length > 0 &&
  29. this.stateHistory[this.stateHistory.length - 1] === newEntry
  30. ) {
  31. // If the last entry is the same as this one, ignore it
  32. return;
  33. }
  34. this.stateHistory.push(newEntry);
  35. // As a new entry was pushed, we invalidate the redo stack
  36. this.clearRedoStack();
  37. }
  38. restoreEntry(entry: string) {
  39. try {
  40. return JSON.parse(entry);
  41. } catch {
  42. return null;
  43. }
  44. }
  45. clearRedoStack() {
  46. this.redoStack.splice(0, this.redoStack.length);
  47. }
  48. redoOnce() {
  49. if (this.redoStack.length === 0) {
  50. return null;
  51. }
  52. const entryToRestore = this.redoStack.pop();
  53. if (entryToRestore !== undefined) {
  54. this.stateHistory.push(entryToRestore);
  55. return this.restoreEntry(entryToRestore);
  56. }
  57. return null;
  58. }
  59. undoOnce() {
  60. if (this.stateHistory.length === 0) {
  61. return null;
  62. }
  63. const currentEntry = this.stateHistory.pop();
  64. const entryToRestore = this.stateHistory[this.stateHistory.length - 1];
  65. if (currentEntry !== undefined) {
  66. this.redoStack.push(currentEntry);
  67. return this.restoreEntry(entryToRestore);
  68. }
  69. return null;
  70. }
  71. isRecording() {
  72. return this.recording;
  73. }
  74. skipRecording() {
  75. this.recording = false;
  76. }
  77. resumeRecording() {
  78. this.recording = true;
  79. }
  80. }
  81. export const createHistory: () => { history: SceneHistory } = () => {
  82. const history = new SceneHistory();
  83. return { history };
  84. };