actionDuplicateSelection.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import {
  2. DEFAULT_GRID_SIZE,
  3. KEYS,
  4. arrayToMap,
  5. getShortcutKey,
  6. } from "@excalidraw/common";
  7. import { getNonDeletedElements } from "@excalidraw/element";
  8. import { LinearElementEditor } from "@excalidraw/element";
  9. import {
  10. getSelectedElements,
  11. getSelectionStateForElements,
  12. } from "@excalidraw/element";
  13. import { syncMovedIndices } from "@excalidraw/element";
  14. import { duplicateElements } from "@excalidraw/element";
  15. import { CaptureUpdateAction } from "@excalidraw/element";
  16. import { ToolButton } from "../components/ToolButton";
  17. import { DuplicateIcon } from "../components/icons";
  18. import { t } from "../i18n";
  19. import { isSomeElementSelected } from "../scene";
  20. import { register } from "./register";
  21. export const actionDuplicateSelection = register({
  22. name: "duplicateSelection",
  23. label: "labels.duplicateSelection",
  24. icon: DuplicateIcon,
  25. trackEvent: { category: "element" },
  26. perform: (elements, appState, formData, app) => {
  27. if (appState.selectedElementsAreBeingDragged) {
  28. return false;
  29. }
  30. // duplicate selected point(s) if editing a line
  31. if (appState.selectedLinearElement?.isEditing) {
  32. // TODO: Invariants should be checked here instead of duplicateSelectedPoints()
  33. try {
  34. const newAppState = LinearElementEditor.duplicateSelectedPoints(
  35. appState,
  36. app.scene,
  37. );
  38. return {
  39. elements,
  40. appState: newAppState,
  41. captureUpdate: CaptureUpdateAction.IMMEDIATELY,
  42. };
  43. } catch {
  44. return false;
  45. }
  46. }
  47. let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
  48. type: "in-place",
  49. elements,
  50. idsOfElementsToDuplicate: arrayToMap(
  51. getSelectedElements(elements, appState, {
  52. includeBoundTextElement: true,
  53. includeElementsInFrames: true,
  54. }),
  55. ),
  56. appState,
  57. randomizeSeed: true,
  58. overrides: ({ origElement, origIdToDuplicateId }) => {
  59. const duplicateFrameId =
  60. origElement.frameId && origIdToDuplicateId.get(origElement.frameId);
  61. return {
  62. x: origElement.x + DEFAULT_GRID_SIZE / 2,
  63. y: origElement.y + DEFAULT_GRID_SIZE / 2,
  64. frameId: duplicateFrameId ?? origElement.frameId,
  65. };
  66. },
  67. });
  68. if (app.props.onDuplicate && elementsWithDuplicates) {
  69. const mappedElements = app.props.onDuplicate(
  70. elementsWithDuplicates,
  71. elements,
  72. );
  73. if (mappedElements) {
  74. elementsWithDuplicates = mappedElements;
  75. }
  76. }
  77. return {
  78. elements: syncMovedIndices(
  79. elementsWithDuplicates,
  80. arrayToMap(duplicatedElements),
  81. ),
  82. appState: {
  83. ...appState,
  84. ...getSelectionStateForElements(
  85. duplicatedElements,
  86. getNonDeletedElements(elementsWithDuplicates),
  87. appState,
  88. ),
  89. },
  90. captureUpdate: CaptureUpdateAction.IMMEDIATELY,
  91. };
  92. },
  93. keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
  94. PanelComponent: ({ elements, appState, updateData }) => (
  95. <ToolButton
  96. type="button"
  97. icon={DuplicateIcon}
  98. title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
  99. "CtrlOrCmd+D",
  100. )}`}
  101. aria-label={t("labels.duplicateSelection")}
  102. onClick={() => updateData(null)}
  103. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  104. />
  105. ),
  106. });