actionHistory.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  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 History, { HistoryEntry } from "../history";
  7. import { ExcalidrawElement } from "../element/types";
  8. import { AppState } from "../types";
  9. import { isWindows, 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. type ActionCreator = (history: History) => Action;
  58. export const createUndoAction: ActionCreator = (history) => ({
  59. name: "undo",
  60. perform: (elements, appState) =>
  61. writeData(elements, appState, () => history.undoOnce()),
  62. keyTest: (event) =>
  63. event[KEYS.CTRL_OR_CMD] &&
  64. event.key.toLowerCase() === KEYS.Z &&
  65. !event.shiftKey,
  66. PanelComponent: ({ updateData }) => (
  67. <ToolButton
  68. type="button"
  69. icon={undo}
  70. aria-label={t("buttons.undo")}
  71. onClick={updateData}
  72. />
  73. ),
  74. commitToHistory: () => false,
  75. });
  76. export const createRedoAction: ActionCreator = (history) => ({
  77. name: "redo",
  78. perform: (elements, appState) =>
  79. writeData(elements, appState, () => history.redoOnce()),
  80. keyTest: (event) =>
  81. (event[KEYS.CTRL_OR_CMD] &&
  82. event.shiftKey &&
  83. event.key.toLowerCase() === KEYS.Z) ||
  84. (isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
  85. PanelComponent: ({ updateData }) => (
  86. <ToolButton
  87. type="button"
  88. icon={redo}
  89. aria-label={t("buttons.redo")}
  90. onClick={updateData}
  91. />
  92. ),
  93. commitToHistory: () => false,
  94. });