actionDuplicateSelection.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import { KEYS } from "../keys";
  2. import { register } from "./register";
  3. import { ExcalidrawElement } from "../element/types";
  4. import { duplicateElement, getNonDeletedElements } from "../element";
  5. import { getSelectedElements, isSomeElementSelected } from "../scene";
  6. import { ToolButton } from "../components/ToolButton";
  7. import { clone } from "../components/icons";
  8. import { t } from "../i18n";
  9. import { arrayToMap, getShortcutKey } from "../utils";
  10. import { LinearElementEditor } from "../element/linearElementEditor";
  11. import {
  12. selectGroupsForSelectedElements,
  13. getSelectedGroupForElement,
  14. getElementsInGroup,
  15. } from "../groups";
  16. import { AppState } from "../types";
  17. import { fixBindingsAfterDuplication } from "../element/binding";
  18. import { ActionResult } from "./types";
  19. import { GRID_SIZE } from "../constants";
  20. import { bindTextToShapeAfterDuplication } from "../element/textElement";
  21. import { isBoundToContainer } from "../element/typeChecks";
  22. export const actionDuplicateSelection = register({
  23. name: "duplicateSelection",
  24. trackEvent: { category: "element" },
  25. perform: (elements, appState) => {
  26. // duplicate selected point(s) if editing a line
  27. if (appState.editingLinearElement) {
  28. const ret = LinearElementEditor.duplicateSelectedPoints(appState);
  29. if (!ret) {
  30. return false;
  31. }
  32. return {
  33. elements,
  34. appState: ret.appState,
  35. commitToHistory: true,
  36. };
  37. }
  38. return {
  39. ...duplicateElements(elements, appState),
  40. commitToHistory: true,
  41. };
  42. },
  43. contextItemLabel: "labels.duplicateSelection",
  44. keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
  45. PanelComponent: ({ elements, appState, updateData }) => (
  46. <ToolButton
  47. type="button"
  48. icon={clone}
  49. title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
  50. "CtrlOrCmd+D",
  51. )}`}
  52. aria-label={t("labels.duplicateSelection")}
  53. onClick={() => updateData(null)}
  54. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  55. />
  56. ),
  57. });
  58. const duplicateElements = (
  59. elements: readonly ExcalidrawElement[],
  60. appState: AppState,
  61. ): Partial<ActionResult> => {
  62. const groupIdMap = new Map();
  63. const newElements: ExcalidrawElement[] = [];
  64. const oldElements: ExcalidrawElement[] = [];
  65. const oldIdToDuplicatedId = new Map();
  66. const duplicateAndOffsetElement = (element: ExcalidrawElement) => {
  67. const newElement = duplicateElement(
  68. appState.editingGroupId,
  69. groupIdMap,
  70. element,
  71. {
  72. x: element.x + GRID_SIZE / 2,
  73. y: element.y + GRID_SIZE / 2,
  74. },
  75. );
  76. oldIdToDuplicatedId.set(element.id, newElement.id);
  77. oldElements.push(element);
  78. newElements.push(newElement);
  79. return newElement;
  80. };
  81. const finalElements: ExcalidrawElement[] = [];
  82. let index = 0;
  83. const selectedElementIds = arrayToMap(
  84. getSelectedElements(elements, appState, true),
  85. );
  86. while (index < elements.length) {
  87. const element = elements[index];
  88. if (selectedElementIds.get(element.id)) {
  89. if (element.groupIds.length) {
  90. const groupId = getSelectedGroupForElement(appState, element);
  91. // if group selected, duplicate it atomically
  92. if (groupId) {
  93. const groupElements = getElementsInGroup(elements, groupId);
  94. finalElements.push(
  95. ...groupElements,
  96. ...groupElements.map((element) =>
  97. duplicateAndOffsetElement(element),
  98. ),
  99. );
  100. index = index + groupElements.length;
  101. continue;
  102. }
  103. }
  104. finalElements.push(element, duplicateAndOffsetElement(element));
  105. } else {
  106. finalElements.push(element);
  107. }
  108. index++;
  109. }
  110. bindTextToShapeAfterDuplication(
  111. finalElements,
  112. oldElements,
  113. oldIdToDuplicatedId,
  114. );
  115. fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
  116. return {
  117. elements: finalElements,
  118. appState: selectGroupsForSelectedElements(
  119. {
  120. ...appState,
  121. selectedGroupIds: {},
  122. selectedElementIds: newElements.reduce((acc, element) => {
  123. if (!isBoundToContainer(element)) {
  124. acc[element.id] = true;
  125. }
  126. return acc;
  127. }, {} as any),
  128. },
  129. getNonDeletedElements(finalElements),
  130. ),
  131. };
  132. };