import { DEFAULT_GRID_SIZE, KEYS, arrayToMap, getShortcutKey, } from "@excalidraw/common"; import { getNonDeletedElements } from "@excalidraw/element"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { getSelectedElements, getSelectionStateForElements, } from "@excalidraw/element/selection"; import { syncMovedIndices } from "@excalidraw/element/fractionalIndex"; import { duplicateElements } from "@excalidraw/element/duplicate"; import { ToolButton } from "../components/ToolButton"; import { DuplicateIcon } from "../components/icons"; import { t } from "../i18n"; import { isSomeElementSelected } from "../scene"; import { CaptureUpdateAction } from "../store"; import { register } from "./register"; export const actionDuplicateSelection = register({ name: "duplicateSelection", label: "labels.duplicateSelection", icon: DuplicateIcon, trackEvent: { category: "element" }, perform: (elements, appState, formData, app) => { if (appState.selectedElementsAreBeingDragged) { return false; } // duplicate selected point(s) if editing a line if (appState.editingLinearElement) { // TODO: Invariants should be checked here instead of duplicateSelectedPoints() try { const newAppState = LinearElementEditor.duplicateSelectedPoints( appState, app.scene.getNonDeletedElementsMap(), ); return { elements, appState: newAppState, captureUpdate: CaptureUpdateAction.IMMEDIATELY, }; } catch { return false; } } let { duplicatedElements, elementsWithDuplicates } = duplicateElements({ type: "in-place", elements, idsOfElementsToDuplicate: arrayToMap( getSelectedElements(elements, appState, { includeBoundTextElement: true, includeElementsInFrames: true, }), ), appState, randomizeSeed: true, overrides: ({ origElement, origIdToDuplicateId }) => { const duplicateFrameId = origElement.frameId && origIdToDuplicateId.get(origElement.frameId); return { x: origElement.x + DEFAULT_GRID_SIZE / 2, y: origElement.y + DEFAULT_GRID_SIZE / 2, frameId: duplicateFrameId ?? origElement.frameId, }; }, }); if (app.props.onDuplicate && elementsWithDuplicates) { const mappedElements = app.props.onDuplicate( elementsWithDuplicates, elements, ); if (mappedElements) { elementsWithDuplicates = mappedElements; } } return { elements: syncMovedIndices( elementsWithDuplicates, arrayToMap(duplicatedElements), ), appState: { ...appState, ...getSelectionStateForElements( duplicatedElements, getNonDeletedElements(elementsWithDuplicates), appState, ), }, captureUpdate: CaptureUpdateAction.IMMEDIATELY, }; }, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D, PanelComponent: ({ elements, appState, updateData }) => ( updateData(null)} visible={isSomeElementSelected(getNonDeletedElements(elements), appState)} /> ), });