mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
tweak API of duplicateElements to clarify usage
This commit is contained in:
parent
679103e2f8
commit
029aa3456f
6 changed files with 105 additions and 48 deletions
|
@ -53,7 +53,9 @@ export const actionDuplicateSelection = register({
|
||||||
}
|
}
|
||||||
|
|
||||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
||||||
duplicateElements(elements, {
|
duplicateElements({
|
||||||
|
type: "in-place",
|
||||||
|
elements,
|
||||||
idsOfElementsToDuplicate: arrayToMap(
|
idsOfElementsToDuplicate: arrayToMap(
|
||||||
getSelectedElements(elements, appState, {
|
getSelectedElements(elements, appState, {
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
|
@ -66,6 +68,7 @@ export const actionDuplicateSelection = register({
|
||||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
x: element.x + DEFAULT_GRID_SIZE / 2,
|
||||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
y: element.y + DEFAULT_GRID_SIZE / 2,
|
||||||
}),
|
}),
|
||||||
|
reverseOrder: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (app.props.onDuplicate && nextElements) {
|
if (app.props.onDuplicate && nextElements) {
|
||||||
|
|
|
@ -3219,17 +3219,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
||||||
|
|
||||||
const { newElements } = duplicateElements(
|
const { newElements } = duplicateElements({
|
||||||
elements.map((element) => {
|
type: "everything",
|
||||||
|
elements: elements.map((element) => {
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
x: element.x + gridX - minX,
|
x: element.x + gridX - minX,
|
||||||
y: element.y + gridY - minY,
|
y: element.y + gridY - minY,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
{
|
randomizeSeed: !opts.retainSeed,
|
||||||
randomizeSeed: !opts.retainSeed,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||||
let nextElements = [...prevElements, ...newElements];
|
let nextElements = [...prevElements, ...newElements];
|
||||||
|
@ -8434,7 +8433,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newElements: clonedElements, elementsWithClones } =
|
const { newElements: clonedElements, elementsWithClones } =
|
||||||
duplicateElements(elements, {
|
duplicateElements({
|
||||||
|
type: "in-place",
|
||||||
|
elements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
idsOfElementsToDuplicate: new Map(
|
idsOfElementsToDuplicate: new Map(
|
||||||
|
|
|
@ -162,8 +162,11 @@ export default function LibraryMenuItems({
|
||||||
...item,
|
...item,
|
||||||
// duplicate each library item before inserting on canvas to confine
|
// duplicate each library item before inserting on canvas to confine
|
||||||
// ids and bindings to each library item. See #6465
|
// ids and bindings to each library item. See #6465
|
||||||
elements: duplicateElements(item.elements, { randomizeSeed: true })
|
elements: duplicateElements({
|
||||||
.newElements,
|
type: "everything",
|
||||||
|
elements: item.elements,
|
||||||
|
randomizeSeed: true,
|
||||||
|
}).newElements,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,12 @@ import { FONT_FAMILY, ORIG_ID, ROUNDNESS } from "../constants";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { isPrimitive } from "../utils";
|
import { isPrimitive } from "../utils";
|
||||||
|
|
||||||
import { act, assertElements, render } from "../tests/test-utils";
|
import {
|
||||||
|
act,
|
||||||
|
assertElements,
|
||||||
|
getCloneByOrigId,
|
||||||
|
render,
|
||||||
|
} from "../tests/test-utils";
|
||||||
import { Excalidraw } from "..";
|
import { Excalidraw } from "..";
|
||||||
import { actionDuplicateSelection } from "../actions";
|
import { actionDuplicateSelection } from "../actions";
|
||||||
|
|
||||||
|
@ -162,7 +167,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements(origElements);
|
const { newElements: clonedElements } = duplicateElements({
|
||||||
|
type: "everything",
|
||||||
|
elements: origElements,
|
||||||
|
});
|
||||||
|
|
||||||
// generic id in-equality checks
|
// generic id in-equality checks
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
@ -313,9 +321,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements(
|
const { newElements: clonedElements } = duplicateElements({
|
||||||
origElements,
|
type: "everything",
|
||||||
) as any as { newElements: typeof origElements };
|
elements: origElements,
|
||||||
|
}) as any as { newElements: typeof origElements };
|
||||||
|
|
||||||
const [
|
const [
|
||||||
clonedRectangle,
|
clonedRectangle,
|
||||||
|
@ -359,9 +368,10 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements(
|
const { newElements: clonedElements } = duplicateElements({
|
||||||
origElements,
|
type: "everything",
|
||||||
) as any as { newElements: typeof origElements };
|
elements: origElements,
|
||||||
|
}) as any as { newElements: typeof origElements };
|
||||||
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
||||||
clonedElements;
|
clonedElements;
|
||||||
|
|
||||||
|
@ -384,7 +394,7 @@ describe("duplicating multiple elements", () => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
newElements: [clonedRectangle1],
|
newElements: [clonedRectangle1],
|
||||||
} = duplicateElements([rectangle1]);
|
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
||||||
|
|
||||||
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
||||||
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||||
|
|
|
@ -96,20 +96,62 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
};
|
};
|
||||||
|
|
||||||
export const duplicateElements = (
|
export const duplicateElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
opts: {
|
||||||
opts?: {
|
elements: readonly ExcalidrawElement[];
|
||||||
idsOfElementsToDuplicate?: Map<ExcalidrawElement["id"], ExcalidrawElement>;
|
randomizeSeed?: boolean;
|
||||||
appState?: {
|
|
||||||
editingGroupId: AppState["editingGroupId"];
|
|
||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
|
||||||
};
|
|
||||||
overrides?: (
|
overrides?: (
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
) => Partial<ExcalidrawElement>;
|
) => Partial<ExcalidrawElement>;
|
||||||
randomizeSeed?: boolean;
|
} & (
|
||||||
reverseOrder?: boolean;
|
| {
|
||||||
},
|
/**
|
||||||
|
* Duplicates all elements in array.
|
||||||
|
*
|
||||||
|
* Use this when programmaticaly duplicating elements, without direct
|
||||||
|
* user interaction.
|
||||||
|
*/
|
||||||
|
type: "everything";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* Duplicates specified elements and inserts them back into the array
|
||||||
|
* in specified order.
|
||||||
|
*
|
||||||
|
* Use this when duplicating Scene elements, during user interaction
|
||||||
|
* such as alt-drag or on duplicate action.
|
||||||
|
*/
|
||||||
|
type: "in-place";
|
||||||
|
idsOfElementsToDuplicate: Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>;
|
||||||
|
appState: {
|
||||||
|
editingGroupId: AppState["editingGroupId"];
|
||||||
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* If true, duplicated elements are inserted _before_ specified
|
||||||
|
* elements. Case: alt-dragging elements to duplicate them.
|
||||||
|
*
|
||||||
|
* TODO: remove this once (if) we stop replacing the original element
|
||||||
|
* with the duplicated one in the scene array.
|
||||||
|
*/
|
||||||
|
reverseOrder: boolean;
|
||||||
|
}
|
||||||
|
),
|
||||||
) => {
|
) => {
|
||||||
|
let { elements } = opts;
|
||||||
|
|
||||||
|
const appState =
|
||||||
|
"appState" in opts
|
||||||
|
? opts.appState
|
||||||
|
: ({
|
||||||
|
editingGroupId: null,
|
||||||
|
selectedGroupIds: {},
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
||||||
|
|
||||||
// Ids of elements that have already been processed so we don't push them
|
// Ids of elements that have already been processed so we don't push them
|
||||||
// into the array twice if we end up backtracking when retrieving
|
// into the array twice if we end up backtracking when retrieving
|
||||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
@ -128,11 +170,12 @@ export const duplicateElements = (
|
||||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
||||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||||
const _idsOfElementsToDuplicate =
|
const _idsOfElementsToDuplicate =
|
||||||
opts?.idsOfElementsToDuplicate ??
|
opts.type === "in-place"
|
||||||
new Map(elements.map((el) => [el.id, el]));
|
? opts.idsOfElementsToDuplicate
|
||||||
|
: new Map(elements.map((el) => [el.id, el]));
|
||||||
|
|
||||||
// For sanity
|
// For sanity
|
||||||
if (opts?.appState?.selectedGroupIds) {
|
if (opts.type === "in-place") {
|
||||||
for (const groupId of Object.keys(opts.appState.selectedGroupIds)) {
|
for (const groupId of Object.keys(opts.appState.selectedGroupIds)) {
|
||||||
elements
|
elements
|
||||||
.filter((el) => el.groupIds?.includes(groupId))
|
.filter((el) => el.groupIds?.includes(groupId))
|
||||||
|
@ -165,11 +208,11 @@ export const duplicateElements = (
|
||||||
processedIds.set(element.id, true);
|
processedIds.set(element.id, true);
|
||||||
|
|
||||||
const newElement = duplicateElement(
|
const newElement = duplicateElement(
|
||||||
opts?.appState?.editingGroupId ?? null,
|
appState.editingGroupId,
|
||||||
groupIdMap,
|
groupIdMap,
|
||||||
element,
|
element,
|
||||||
opts?.overrides?.(element),
|
opts.overrides?.(element),
|
||||||
opts?.randomizeSeed,
|
opts.randomizeSeed,
|
||||||
);
|
);
|
||||||
|
|
||||||
processedIds.set(newElement.id, true);
|
processedIds.set(newElement.id, true);
|
||||||
|
@ -204,18 +247,18 @@ export const duplicateElements = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts?.reverseOrder && index < 1) {
|
if (reverseOrder && index < 1) {
|
||||||
elementsWithClones.unshift(...castArray(elements));
|
elementsWithClones.unshift(...castArray(elements));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts?.reverseOrder && index > elementsWithClones.length - 1) {
|
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
||||||
elementsWithClones.push(...castArray(elements));
|
elementsWithClones.push(...castArray(elements));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elementsWithClones.splice(
|
elementsWithClones.splice(
|
||||||
index + (opts?.reverseOrder ? 0 : 1),
|
index + (reverseOrder ? 0 : 1),
|
||||||
0,
|
0,
|
||||||
...castArray(elements),
|
...castArray(elements),
|
||||||
);
|
);
|
||||||
|
@ -241,13 +284,7 @@ export const duplicateElements = (
|
||||||
// groups
|
// groups
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const groupId = getSelectedGroupForElement(
|
const groupId = getSelectedGroupForElement(appState, element);
|
||||||
(opts?.appState ?? {
|
|
||||||
editingGroupId: null,
|
|
||||||
selectedGroupIds: {},
|
|
||||||
}) as AppState,
|
|
||||||
element,
|
|
||||||
);
|
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
const groupElements = getElementsInGroup(elements, groupId).flatMap(
|
const groupElements = getElementsInGroup(elements, groupId).flatMap(
|
||||||
(element) =>
|
(element) =>
|
||||||
|
@ -256,7 +293,7 @@ export const duplicateElements = (
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetIndex = opts?.reverseOrder
|
const targetIndex = reverseOrder
|
||||||
? elementsWithClones.findIndex((el) => {
|
? elementsWithClones.findIndex((el) => {
|
||||||
return el.groupIds?.includes(groupId);
|
return el.groupIds?.includes(groupId);
|
||||||
})
|
})
|
||||||
|
@ -306,7 +343,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
targetIndex + (opts?.reverseOrder ? -1 : 0),
|
targetIndex + (reverseOrder ? -1 : 0),
|
||||||
copyElements([element, boundTextElement]),
|
copyElements([element, boundTextElement]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -214,7 +214,10 @@ export const isSelectedViaGroup = (
|
||||||
) => getSelectedGroupForElement(appState, element) != null;
|
) => getSelectedGroupForElement(appState, element) != null;
|
||||||
|
|
||||||
export const getSelectedGroupForElement = (
|
export const getSelectedGroupForElement = (
|
||||||
appState: InteractiveCanvasAppState,
|
appState: Pick<
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
"editingGroupId" | "selectedGroupIds"
|
||||||
|
>,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) =>
|
) =>
|
||||||
element.groupIds
|
element.groupIds
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue