actionDuplicateSelection.tsx 4.7 KB

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