2020-03-17 20:55:40 +01:00
|
|
|
import {
|
|
|
|
ExcalidrawElement,
|
2021-10-21 22:05:48 +02:00
|
|
|
ExcalidrawImageElement,
|
2020-03-17 20:55:40 +01:00
|
|
|
ExcalidrawTextElement,
|
|
|
|
ExcalidrawLinearElement,
|
|
|
|
ExcalidrawGenericElement,
|
2020-04-08 09:49:52 -07:00
|
|
|
NonDeleted,
|
2020-04-08 21:00:27 +01:00
|
|
|
TextAlign,
|
2020-05-26 13:07:46 -07:00
|
|
|
GroupId,
|
2020-06-25 21:21:27 +02:00
|
|
|
VerticalAlign,
|
2020-12-08 15:02:55 +00:00
|
|
|
Arrowhead,
|
2021-05-09 16:42:10 +01:00
|
|
|
ExcalidrawFreeDrawElement,
|
2021-06-13 21:26:55 +05:30
|
|
|
FontFamilyValues,
|
2022-12-05 21:03:13 +05:30
|
|
|
ExcalidrawTextContainer,
|
2020-03-17 20:55:40 +01:00
|
|
|
} from "../element/types";
|
2021-12-28 17:17:41 +05:30
|
|
|
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
|
2020-03-23 16:38:41 -07:00
|
|
|
import { randomInteger, randomId } from "../random";
|
2021-12-16 21:14:03 +05:30
|
|
|
import { mutateElement, newElementWith } from "./mutateElement";
|
2020-05-26 13:07:46 -07:00
|
|
|
import { getNewGroupIdsForDuplication } from "../groups";
|
2020-05-30 22:48:57 +02:00
|
|
|
import { AppState } from "../types";
|
2020-06-25 21:21:27 +02:00
|
|
|
import { getElementAbsoluteCoords } from ".";
|
|
|
|
import { adjustXYWithRotation } from "../math";
|
|
|
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
2022-09-19 15:30:37 +05:30
|
|
|
import {
|
2022-12-05 21:03:13 +05:30
|
|
|
getBoundTextElementOffset,
|
2022-09-19 15:30:37 +05:30
|
|
|
getContainerDims,
|
|
|
|
getContainerElement,
|
|
|
|
measureText,
|
2022-11-26 23:44:26 +01:00
|
|
|
normalizeText,
|
2022-09-19 15:30:37 +05:30
|
|
|
wrapText,
|
2023-02-22 16:28:12 +05:30
|
|
|
getMaxContainerWidth,
|
2022-09-19 15:30:37 +05:30
|
|
|
} from "./textElement";
|
2023-02-22 16:28:12 +05:30
|
|
|
import { VERTICAL_ALIGN } from "../constants";
|
2022-12-05 21:03:13 +05:30
|
|
|
import { isArrowElement } from "./typeChecks";
|
2020-01-21 00:16:22 +01:00
|
|
|
|
2020-06-06 13:32:43 +02:00
|
|
|
type ElementConstructorOpts = MarkOptional<
|
2021-11-24 18:38:33 +01:00
|
|
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
2020-06-06 13:32:43 +02:00
|
|
|
| "width"
|
|
|
|
| "height"
|
|
|
|
| "angle"
|
|
|
|
| "groupIds"
|
2021-12-14 16:07:01 +01:00
|
|
|
| "boundElements"
|
2020-06-06 13:32:43 +02:00
|
|
|
| "seed"
|
|
|
|
| "version"
|
|
|
|
| "versionNonce"
|
2022-02-03 20:34:59 +05:30
|
|
|
| "link"
|
2020-06-06 13:32:43 +02:00
|
|
|
>;
|
2020-03-17 20:55:40 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
const _newElementBase = <T extends ExcalidrawElement>(
|
2020-03-17 20:55:40 +01:00
|
|
|
type: T["type"],
|
|
|
|
{
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
strokeColor,
|
|
|
|
backgroundColor,
|
|
|
|
fillStyle,
|
|
|
|
strokeWidth,
|
2020-05-14 17:04:33 +02:00
|
|
|
strokeStyle,
|
2020-03-17 20:55:40 +01:00
|
|
|
roughness,
|
|
|
|
opacity,
|
|
|
|
width = 0,
|
|
|
|
height = 0,
|
2020-04-02 17:40:26 +09:00
|
|
|
angle = 0,
|
2020-06-06 13:32:43 +02:00
|
|
|
groupIds = [],
|
2022-12-08 23:48:49 +08:00
|
|
|
roundness = null,
|
2021-12-14 16:07:01 +01:00
|
|
|
boundElements = null,
|
2022-02-03 20:34:59 +05:30
|
|
|
link = null,
|
2022-04-07 12:43:29 +01:00
|
|
|
locked,
|
2020-03-17 20:55:40 +01:00
|
|
|
...rest
|
2020-05-24 21:17:25 +02:00
|
|
|
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
2021-12-16 21:14:03 +05:30
|
|
|
) => {
|
2022-12-08 23:48:49 +08:00
|
|
|
// assign type to guard against excess properties
|
|
|
|
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
2021-12-16 21:14:03 +05:30
|
|
|
id: rest.id || randomId(),
|
|
|
|
type,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
angle,
|
|
|
|
strokeColor,
|
|
|
|
backgroundColor,
|
|
|
|
fillStyle,
|
|
|
|
strokeWidth,
|
|
|
|
strokeStyle,
|
|
|
|
roughness,
|
|
|
|
opacity,
|
|
|
|
groupIds,
|
2022-12-08 23:48:49 +08:00
|
|
|
roundness,
|
2021-12-16 21:14:03 +05:30
|
|
|
seed: rest.seed ?? randomInteger(),
|
|
|
|
version: rest.version || 1,
|
|
|
|
versionNonce: rest.versionNonce ?? 0,
|
|
|
|
isDeleted: false as false,
|
|
|
|
boundElements,
|
|
|
|
updated: getUpdatedTimestamp(),
|
2022-02-03 20:34:59 +05:30
|
|
|
link,
|
2022-04-07 12:43:29 +01:00
|
|
|
locked,
|
2021-12-16 21:14:03 +05:30
|
|
|
};
|
|
|
|
return element;
|
|
|
|
};
|
2020-03-17 20:55:40 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const newElement = (
|
2020-03-17 20:55:40 +01:00
|
|
|
opts: {
|
|
|
|
type: ExcalidrawGenericElement["type"];
|
|
|
|
} & ElementConstructorOpts,
|
2020-05-20 16:21:37 +03:00
|
|
|
): NonDeleted<ExcalidrawGenericElement> =>
|
|
|
|
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
2020-01-08 19:54:42 +01:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
/** computes element x/y offset based on textAlign/verticalAlign */
|
2020-11-06 22:06:39 +02:00
|
|
|
const getTextElementPositionOffsets = (
|
2020-06-25 21:21:27 +02:00
|
|
|
opts: {
|
|
|
|
textAlign: ExcalidrawTextElement["textAlign"];
|
|
|
|
verticalAlign: ExcalidrawTextElement["verticalAlign"];
|
|
|
|
},
|
|
|
|
metrics: {
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
},
|
2020-11-06 22:06:39 +02:00
|
|
|
) => {
|
2020-06-25 21:21:27 +02:00
|
|
|
return {
|
|
|
|
x:
|
|
|
|
opts.textAlign === "center"
|
|
|
|
? metrics.width / 2
|
|
|
|
: opts.textAlign === "right"
|
|
|
|
? metrics.width
|
|
|
|
: 0,
|
|
|
|
y: opts.verticalAlign === "middle" ? metrics.height / 2 : 0,
|
|
|
|
};
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-06-25 21:21:27 +02:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const newTextElement = (
|
2020-03-17 20:55:40 +01:00
|
|
|
opts: {
|
|
|
|
text: string;
|
2020-05-27 15:14:50 +02:00
|
|
|
fontSize: number;
|
2021-06-13 21:26:55 +05:30
|
|
|
fontFamily: FontFamilyValues;
|
2020-04-08 21:00:27 +01:00
|
|
|
textAlign: TextAlign;
|
2020-06-25 21:21:27 +02:00
|
|
|
verticalAlign: VerticalAlign;
|
2022-12-05 21:03:13 +05:30
|
|
|
containerId?: ExcalidrawTextContainer["id"];
|
2020-03-17 20:55:40 +01:00
|
|
|
} & ElementConstructorOpts,
|
2020-05-20 16:21:37 +03:00
|
|
|
): NonDeleted<ExcalidrawTextElement> => {
|
2022-11-26 23:44:26 +01:00
|
|
|
const text = normalizeText(opts.text);
|
|
|
|
const metrics = measureText(text, getFontString(opts));
|
2020-06-25 21:21:27 +02:00
|
|
|
const offsets = getTextElementPositionOffsets(opts, metrics);
|
2020-04-03 14:16:14 +02:00
|
|
|
const textElement = newElementWith(
|
|
|
|
{
|
|
|
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
2022-11-26 23:44:26 +01:00
|
|
|
text,
|
2020-05-27 15:14:50 +02:00
|
|
|
fontSize: opts.fontSize,
|
|
|
|
fontFamily: opts.fontFamily,
|
2020-04-08 21:00:27 +01:00
|
|
|
textAlign: opts.textAlign,
|
2020-06-25 21:21:27 +02:00
|
|
|
verticalAlign: opts.verticalAlign,
|
|
|
|
x: opts.x - offsets.x,
|
|
|
|
y: opts.y - offsets.y,
|
2020-04-03 14:16:14 +02:00
|
|
|
width: metrics.width,
|
|
|
|
height: metrics.height,
|
2021-12-16 21:14:03 +05:30
|
|
|
containerId: opts.containerId || null,
|
2022-11-26 23:44:26 +01:00
|
|
|
originalText: text,
|
2020-04-03 14:16:14 +02:00
|
|
|
},
|
|
|
|
{},
|
|
|
|
);
|
2020-01-21 00:16:22 +01:00
|
|
|
return textElement;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-21 00:16:22 +01:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
const getAdjustedDimensions = (
|
|
|
|
element: ExcalidrawTextElement,
|
|
|
|
nextText: string,
|
|
|
|
): {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
} => {
|
2022-01-12 15:21:36 +01:00
|
|
|
const container = getContainerElement(element);
|
2023-02-23 16:33:10 +05:30
|
|
|
|
|
|
|
const { width: nextWidth, height: nextHeight } = measureText(
|
|
|
|
nextText,
|
|
|
|
getFontString(element),
|
|
|
|
);
|
2020-06-25 21:21:27 +02:00
|
|
|
const { textAlign, verticalAlign } = element;
|
2020-11-06 22:06:30 +02:00
|
|
|
let x: number;
|
|
|
|
let y: number;
|
2021-12-16 21:14:03 +05:30
|
|
|
if (
|
|
|
|
textAlign === "center" &&
|
2022-03-02 20:06:07 +05:30
|
|
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
2021-12-16 21:14:03 +05:30
|
|
|
!element.containerId
|
|
|
|
) {
|
2023-02-23 16:33:10 +05:30
|
|
|
const prevMetrics = measureText(element.text, getFontString(element));
|
2020-06-25 21:21:27 +02:00
|
|
|
const offsets = getTextElementPositionOffsets(element, {
|
|
|
|
width: nextWidth - prevMetrics.width,
|
|
|
|
height: nextHeight - prevMetrics.height,
|
|
|
|
});
|
|
|
|
|
|
|
|
x = element.x - offsets.x;
|
|
|
|
y = element.y - offsets.y;
|
|
|
|
} else {
|
|
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
|
|
|
|
|
|
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
|
|
|
element,
|
|
|
|
nextWidth,
|
|
|
|
nextHeight,
|
2022-08-16 21:51:43 +02:00
|
|
|
false,
|
2020-06-25 21:21:27 +02:00
|
|
|
);
|
|
|
|
const deltaX1 = (x1 - nextX1) / 2;
|
|
|
|
const deltaY1 = (y1 - nextY1) / 2;
|
|
|
|
const deltaX2 = (x2 - nextX2) / 2;
|
|
|
|
const deltaY2 = (y2 - nextY2) / 2;
|
|
|
|
|
|
|
|
[x, y] = adjustXYWithRotation(
|
|
|
|
{
|
|
|
|
s: true,
|
|
|
|
e: textAlign === "center" || textAlign === "left",
|
|
|
|
w: textAlign === "center" || textAlign === "right",
|
|
|
|
},
|
|
|
|
element.x,
|
|
|
|
element.y,
|
|
|
|
element.angle,
|
|
|
|
deltaX1,
|
|
|
|
deltaY1,
|
|
|
|
deltaX2,
|
|
|
|
deltaY2,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
// make sure container dimensions are set properly when
|
|
|
|
// text editor overflows beyond viewport dimensions
|
2022-03-02 17:04:09 +01:00
|
|
|
if (container) {
|
2022-12-05 21:03:13 +05:30
|
|
|
const boundTextElementPadding = getBoundTextElementOffset(element);
|
|
|
|
|
2022-09-19 15:30:37 +05:30
|
|
|
const containerDims = getContainerDims(container);
|
|
|
|
let height = containerDims.height;
|
|
|
|
let width = containerDims.width;
|
2022-12-05 21:03:13 +05:30
|
|
|
if (nextHeight > height - boundTextElementPadding * 2) {
|
|
|
|
height = nextHeight + boundTextElementPadding * 2;
|
2021-12-16 21:14:03 +05:30
|
|
|
}
|
2022-12-05 21:03:13 +05:30
|
|
|
if (nextWidth > width - boundTextElementPadding * 2) {
|
|
|
|
width = nextWidth + boundTextElementPadding * 2;
|
2021-12-16 21:14:03 +05:30
|
|
|
}
|
2022-12-05 21:03:13 +05:30
|
|
|
if (
|
|
|
|
!isArrowElement(container) &&
|
|
|
|
(height !== containerDims.height || width !== containerDims.width)
|
|
|
|
) {
|
2021-12-16 21:14:03 +05:30
|
|
|
mutateElement(container, { height, width });
|
|
|
|
}
|
|
|
|
}
|
2020-06-25 21:21:27 +02:00
|
|
|
return {
|
|
|
|
width: nextWidth,
|
|
|
|
height: nextHeight,
|
|
|
|
x: Number.isFinite(x) ? x : element.x,
|
|
|
|
y: Number.isFinite(y) ? y : element.y,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2022-10-28 23:31:56 +02:00
|
|
|
export const refreshTextDimensions = (
|
|
|
|
textElement: ExcalidrawTextElement,
|
|
|
|
text = textElement.text,
|
|
|
|
) => {
|
|
|
|
const container = getContainerElement(textElement);
|
|
|
|
if (container) {
|
|
|
|
text = wrapText(
|
|
|
|
text,
|
|
|
|
getFontString(textElement),
|
|
|
|
getMaxContainerWidth(container),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const dimensions = getAdjustedDimensions(textElement, text);
|
|
|
|
return { text, ...dimensions };
|
|
|
|
};
|
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
export const updateTextElement = (
|
2022-09-22 15:40:38 +05:30
|
|
|
textElement: ExcalidrawTextElement,
|
2021-12-16 21:14:03 +05:30
|
|
|
{
|
|
|
|
text,
|
|
|
|
isDeleted,
|
|
|
|
originalText,
|
2022-01-17 17:35:35 +05:30
|
|
|
}: {
|
|
|
|
text: string;
|
|
|
|
isDeleted?: boolean;
|
|
|
|
originalText: string;
|
|
|
|
},
|
2020-06-25 21:21:27 +02:00
|
|
|
): ExcalidrawTextElement => {
|
2022-09-22 15:40:38 +05:30
|
|
|
return newElementWith(textElement, {
|
2021-12-16 21:14:03 +05:30
|
|
|
originalText,
|
2022-09-22 15:40:38 +05:30
|
|
|
isDeleted: isDeleted ?? textElement.isDeleted,
|
2022-10-28 23:31:56 +02:00
|
|
|
...refreshTextDimensions(textElement, originalText),
|
2020-06-25 21:21:27 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-05-09 16:42:10 +01:00
|
|
|
export const newFreeDrawElement = (
|
|
|
|
opts: {
|
|
|
|
type: "freedraw";
|
|
|
|
points?: ExcalidrawFreeDrawElement["points"];
|
|
|
|
simulatePressure: boolean;
|
|
|
|
} & ElementConstructorOpts,
|
|
|
|
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
|
|
|
return {
|
|
|
|
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
|
|
|
points: opts.points || [],
|
|
|
|
pressures: [],
|
|
|
|
simulatePressure: opts.simulatePressure,
|
|
|
|
lastCommittedPoint: null,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const newLinearElement = (
|
2020-03-17 20:55:40 +01:00
|
|
|
opts: {
|
2020-03-18 16:43:06 +01:00
|
|
|
type: ExcalidrawLinearElement["type"];
|
2020-12-08 15:02:55 +00:00
|
|
|
startArrowhead: Arrowhead | null;
|
|
|
|
endArrowhead: Arrowhead | null;
|
2020-12-11 13:13:23 +02:00
|
|
|
points?: ExcalidrawLinearElement["points"];
|
2020-03-17 20:55:40 +01:00
|
|
|
} & ElementConstructorOpts,
|
2020-05-20 16:21:37 +03:00
|
|
|
): NonDeleted<ExcalidrawLinearElement> => {
|
2020-03-17 20:55:40 +01:00
|
|
|
return {
|
|
|
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
2020-12-11 13:13:23 +02:00
|
|
|
points: opts.points || [],
|
2020-07-28 23:40:06 +02:00
|
|
|
lastCommittedPoint: null,
|
2020-08-08 21:04:15 -07:00
|
|
|
startBinding: null,
|
|
|
|
endBinding: null,
|
2020-12-08 15:02:55 +00:00
|
|
|
startArrowhead: opts.startArrowhead,
|
|
|
|
endArrowhead: opts.endArrowhead,
|
2020-03-17 20:55:40 +01:00
|
|
|
};
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-17 20:55:40 +01:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
export const newImageElement = (
|
|
|
|
opts: {
|
|
|
|
type: ExcalidrawImageElement["type"];
|
2022-09-18 05:02:13 +08:00
|
|
|
status?: ExcalidrawImageElement["status"];
|
|
|
|
fileId?: ExcalidrawImageElement["fileId"];
|
|
|
|
scale?: ExcalidrawImageElement["scale"];
|
2021-10-21 22:05:48 +02:00
|
|
|
} & ElementConstructorOpts,
|
|
|
|
): NonDeleted<ExcalidrawImageElement> => {
|
|
|
|
return {
|
|
|
|
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
|
|
|
// in the future we'll support changing stroke color for some SVG elements,
|
|
|
|
// and `transparent` will likely mean "use original colors of the image"
|
|
|
|
strokeColor: "transparent",
|
2022-09-18 05:02:13 +08:00
|
|
|
status: opts.status ?? "pending",
|
|
|
|
fileId: opts.fileId ?? null,
|
|
|
|
scale: opts.scale ?? [1, 1],
|
2021-10-21 22:05:48 +02:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-02-19 22:28:11 +01:00
|
|
|
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
2020-11-05 19:06:18 +02:00
|
|
|
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
2020-02-19 22:28:11 +01:00
|
|
|
//
|
|
|
|
// Adapted from https://github.com/lukeed/klona
|
2020-05-23 12:07:11 -07:00
|
|
|
export const deepCopyElement = (val: any, depth: number = 0) => {
|
2020-02-19 22:28:11 +01:00
|
|
|
if (val == null || typeof val !== "object") {
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Object.prototype.toString.call(val) === "[object Object]") {
|
|
|
|
const tmp =
|
|
|
|
typeof val.constructor === "function"
|
|
|
|
? Object.create(Object.getPrototypeOf(val))
|
|
|
|
: {};
|
2020-03-07 10:20:38 -05:00
|
|
|
for (const key in val) {
|
|
|
|
if (val.hasOwnProperty(key)) {
|
2022-12-02 11:36:18 +01:00
|
|
|
// don't copy non-serializable objects like these caches. They'll be
|
|
|
|
// populated when the element is rendered.
|
2020-03-07 10:20:38 -05:00
|
|
|
if (depth === 0 && (key === "shape" || key === "canvas")) {
|
2020-02-19 22:28:11 +01:00
|
|
|
continue;
|
|
|
|
}
|
2020-05-23 12:07:11 -07:00
|
|
|
tmp[key] = deepCopyElement(val[key], depth + 1);
|
2020-02-19 22:28:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return tmp;
|
2020-02-09 23:57:14 +01:00
|
|
|
}
|
|
|
|
|
2020-02-19 22:28:11 +01:00
|
|
|
if (Array.isArray(val)) {
|
|
|
|
let k = val.length;
|
|
|
|
const arr = new Array(k);
|
|
|
|
while (k--) {
|
2020-05-23 12:07:11 -07:00
|
|
|
arr[k] = deepCopyElement(val[k], depth + 1);
|
2020-02-19 22:28:11 +01:00
|
|
|
}
|
|
|
|
return arr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return val;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-02-19 22:28:11 +01:00
|
|
|
|
2020-05-26 13:07:46 -07:00
|
|
|
/**
|
|
|
|
* Duplicate an element, often used in the alt-drag operation.
|
|
|
|
* Note that this method has gotten a bit complicated since the
|
|
|
|
* introduction of gruoping/ungrouping elements.
|
|
|
|
* @param editingGroupId The current group being edited. The new
|
|
|
|
* element will inherit this group and its
|
|
|
|
* parents.
|
|
|
|
* @param groupIdMapForOperation A Map that maps old group IDs to
|
|
|
|
* duplicated ones. If you are duplicating
|
|
|
|
* multiple elements at once, share this map
|
|
|
|
* amongst all of them
|
|
|
|
* @param element Element to duplicate
|
|
|
|
* @param overrides Any element properties to override
|
|
|
|
*/
|
2020-05-20 16:21:37 +03:00
|
|
|
export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
2020-05-30 22:48:57 +02:00
|
|
|
editingGroupId: AppState["editingGroupId"],
|
2020-05-26 13:07:46 -07:00
|
|
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
2020-03-17 20:55:40 +01:00
|
|
|
element: TElement,
|
|
|
|
overrides?: Partial<TElement>,
|
2020-05-20 16:21:37 +03:00
|
|
|
): TElement => {
|
2020-05-23 12:07:11 -07:00
|
|
|
let copy: TElement = deepCopyElement(element);
|
2022-12-01 20:44:33 +05:30
|
|
|
|
2021-12-28 17:17:41 +05:30
|
|
|
if (isTestEnv()) {
|
2021-05-29 21:35:03 +02:00
|
|
|
copy.id = `${copy.id}_copy`;
|
|
|
|
// `window.h` may not be defined in some unit tests
|
|
|
|
if (
|
|
|
|
window.h?.app
|
|
|
|
?.getSceneElementsIncludingDeleted()
|
|
|
|
.find((el) => el.id === copy.id)
|
|
|
|
) {
|
|
|
|
copy.id += "_copy";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
copy.id = randomId();
|
|
|
|
}
|
2022-12-02 11:36:18 +01:00
|
|
|
copy.boundElements = null;
|
2021-11-24 18:38:33 +01:00
|
|
|
copy.updated = getUpdatedTimestamp();
|
2020-03-23 16:38:41 -07:00
|
|
|
copy.seed = randomInteger();
|
2020-05-26 13:07:46 -07:00
|
|
|
copy.groupIds = getNewGroupIdsForDuplication(
|
|
|
|
copy.groupIds,
|
|
|
|
editingGroupId,
|
|
|
|
(groupId) => {
|
|
|
|
if (!groupIdMapForOperation.has(groupId)) {
|
2020-05-28 01:56:18 -07:00
|
|
|
groupIdMapForOperation.set(groupId, randomId());
|
2020-05-26 13:07:46 -07:00
|
|
|
}
|
|
|
|
return groupIdMapForOperation.get(groupId)!;
|
|
|
|
},
|
|
|
|
);
|
2020-03-17 20:55:40 +01:00
|
|
|
if (overrides) {
|
|
|
|
copy = Object.assign(copy, overrides);
|
|
|
|
}
|
2020-01-08 19:54:42 +01:00
|
|
|
return copy;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|