actionHistory.tsx 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import { Action, ActionResult } from "./types";
  2. import React from "react";
  3. import { undo, redo } from "../components/icons";
  4. import { ToolButton } from "../components/ToolButton";
  5. import { t } from "../i18n";
  6. import { SceneHistory, HistoryEntry } from "../history";
  7. import { ExcalidrawElement } from "../element/types";
  8. import { AppState } from "../types";
  9. import { KEYS } from "../keys";
  10. import { getElementMap } from "../element";
  11. import { newElementWith } from "../element/mutateElement";
  12. import { fixBindingsAfterDeletion } from "../element/binding";
  13. const writeData = (
  14. prevElements: readonly ExcalidrawElement[],
  15. appState: AppState,
  16. updater: () => HistoryEntry | null,
  17. ): ActionResult => {
  18. const commitToHistory = false;
  19. if (
  20. !appState.multiElement &&
  21. !appState.resizingElement &&
  22. !appState.editingElement &&
  23. !appState.draggingElement
  24. ) {
  25. const data = updater();
  26. if (data === null) {
  27. return { commitToHistory };
  28. }
  29. const prevElementMap = getElementMap(prevElements);
  30. const nextElements = data.elements;
  31. const nextElementMap = getElementMap(nextElements);
  32. const deletedElements = prevElements.filter(
  33. (prevElement) => !nextElementMap.hasOwnProperty(prevElement.id),
  34. );
  35. const elements = nextElements
  36. .map((nextElement) =>
  37. newElementWith(
  38. prevElementMap[nextElement.id] || nextElement,
  39. nextElement,
  40. ),
  41. )
  42. .concat(
  43. deletedElements.map((prevElement) =>
  44. newElementWith(prevElement, { isDeleted: true }),
  45. ),
  46. );
  47. fixBindingsAfterDeletion(elements, deletedElements);
  48. return {
  49. elements,
  50. appState: { ...appState, ...data.appState },
  51. commitToHistory,
  52. syncHistory: true,
  53. };
  54. }
  55. return { commitToHistory };
  56. };
  57. const testUndo = (shift: boolean) => (event: KeyboardEvent) =>
  58. event[KEYS.CTRL_OR_CMD] && /z/i.test(event.key) && event.shiftKey === shift;
  59. type ActionCreator = (history: SceneHistory) => Action;
  60. export const createUndoAction: ActionCreator = (history) => ({
  61. name: "undo",
  62. perform: (elements, appState) =>
  63. writeData(elements, appState, () => history.undoOnce()),
  64. keyTest: testUndo(false),
  65. PanelComponent: ({ updateData }) => (
  66. <ToolButton
  67. type="button"
  68. icon={undo}
  69. aria-label={t("buttons.undo")}
  70. onClick={updateData}
  71. />
  72. ),
  73. commitToHistory: () => false,
  74. });
  75. export const createRedoAction: ActionCreator = (history) => ({
  76. name: "redo",
  77. perform: (elements, appState) =>
  78. writeData(elements, appState, () => history.redoOnce()),
  79. keyTest: testUndo(true),
  80. PanelComponent: ({ updateData }) => (
  81. <ToolButton
  82. type="button"
  83. icon={redo}
  84. aria-label={t("buttons.redo")}
  85. onClick={updateData}
  86. />
  87. ),
  88. commitToHistory: () => false,
  89. });