Browse Source

feat: add `props.onDuplicate` (#9117)

* feat: add `props.onDuplicate`

* docs

* clarify docs

* fix docs
David Luzar 5 months ago
parent
commit
c8f4a4cb41

+ 14 - 2
packages/excalidraw/actions/actionDuplicateSelection.tsx

@@ -69,8 +69,20 @@ export const actionDuplicateSelection = register({
       }
     }
 
+    const nextState = duplicateElements(elements, appState);
+
+    if (app.props.onDuplicate && nextState.elements) {
+      const mappedElements = app.props.onDuplicate(
+        nextState.elements,
+        elements,
+      );
+      if (mappedElements) {
+        nextState.elements = mappedElements;
+      }
+    }
+
     return {
-      ...duplicateElements(elements, appState),
+      ...nextState,
       storeAction: StoreAction.CAPTURE,
     };
   },
@@ -92,7 +104,7 @@ export const actionDuplicateSelection = register({
 const duplicateElements = (
   elements: readonly ExcalidrawElement[],
   appState: AppState,
-): Partial<ActionResult> => {
+): Partial<Exclude<ActionResult, false>> => {
   // ---------------------------------------------------------------------------
 
   const groupIdMap = new Map();

+ 19 - 2
packages/excalidraw/components/App.tsx

@@ -3228,7 +3228,14 @@ class App extends React.Component<AppProps, AppState> {
     );
 
     const prevElements = this.scene.getElementsIncludingDeleted();
-    const nextElements = [...prevElements, ...newElements];
+    let nextElements = [...prevElements, ...newElements];
+
+    const mappedNewSceneElements = this.props.onDuplicate?.(
+      nextElements,
+      prevElements,
+    );
+
+    nextElements = mappedNewSceneElements || nextElements;
 
     syncMovedIndices(nextElements, arrayToMap(newElements));
 
@@ -8442,7 +8449,17 @@ class App extends React.Component<AppProps, AppState> {
               }
             }
 
-            const nextSceneElements = [...nextElements, ...elementsToAppend];
+            let nextSceneElements: ExcalidrawElement[] = [
+              ...nextElements,
+              ...elementsToAppend,
+            ];
+
+            const mappedNewSceneElements = this.props.onDuplicate?.(
+              nextSceneElements,
+              elements,
+            );
+
+            nextSceneElements = mappedNewSceneElements || nextSceneElements;
 
             syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
 

+ 2 - 0
packages/excalidraw/index.tsx

@@ -46,6 +46,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
     onPointerDown,
     onPointerUp,
     onScrollChange,
+    onDuplicate,
     children,
     validateEmbeddable,
     renderEmbeddable,
@@ -136,6 +137,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
           onPointerDown={onPointerDown}
           onPointerUp={onPointerUp}
           onScrollChange={onScrollChange}
+          onDuplicate={onDuplicate}
           validateEmbeddable={validateEmbeddable}
           renderEmbeddable={renderEmbeddable}
           aiEnabled={aiEnabled !== false}

+ 16 - 0
packages/excalidraw/types.ts

@@ -512,6 +512,22 @@ export interface ExcalidrawProps {
     data: ClipboardData,
     event: ClipboardEvent | null,
   ) => Promise<boolean> | boolean;
+  /**
+   * Called when element(s) are duplicated so you can listen or modify as
+   * needed.
+   *
+   * Called when duplicating via mouse-drag, keyboard, paste, library insert
+   * etc.
+   *
+   * Returned elements will be used in place of the next elements
+   * (you should return all elements, including deleted, and not mutate
+   * the element if changes are made)
+   */
+  onDuplicate?: (
+    nextElements: readonly ExcalidrawElement[],
+    /** excludes the duplicated elements */
+    prevElements: readonly ExcalidrawElement[],
+  ) => ExcalidrawElement[] | void;
   renderTopRightUI?: (
     isMobile: boolean,
     appState: UIAppState,