aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/actions/actionStyles.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/actions/actionStyles.ts')
-rw-r--r--packages/excalidraw/actions/actionStyles.ts167
1 files changed, 167 insertions, 0 deletions
diff --git a/packages/excalidraw/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts
new file mode 100644
index 0000000..01f8040
--- /dev/null
+++ b/packages/excalidraw/actions/actionStyles.ts
@@ -0,0 +1,167 @@
+import {
+ isTextElement,
+ isExcalidrawElement,
+ redrawTextBoundingBox,
+} from "../element";
+import { CODES, KEYS } from "../keys";
+import { t } from "../i18n";
+import { register } from "./register";
+import { newElementWith } from "../element/mutateElement";
+import {
+ DEFAULT_FONT_SIZE,
+ DEFAULT_FONT_FAMILY,
+ DEFAULT_TEXT_ALIGN,
+} from "../constants";
+import { getBoundTextElement } from "../element/textElement";
+import {
+ hasBoundTextElement,
+ canApplyRoundnessTypeToElement,
+ getDefaultRoundnessTypeForElement,
+ isFrameLikeElement,
+ isArrowElement,
+} from "../element/typeChecks";
+import { getSelectedElements } from "../scene";
+import type { ExcalidrawTextElement } from "../element/types";
+import { paintIcon } from "../components/icons";
+import { CaptureUpdateAction } from "../store";
+import { getLineHeight } from "../fonts";
+
+// `copiedStyles` is exported only for tests.
+export let copiedStyles: string = "{}";
+
+export const actionCopyStyles = register({
+ name: "copyStyles",
+ label: "labels.copyStyles",
+ icon: paintIcon,
+ trackEvent: { category: "element" },
+ perform: (elements, appState, formData, app) => {
+ const elementsCopied = [];
+ const element = elements.find((el) => appState.selectedElementIds[el.id]);
+ elementsCopied.push(element);
+ if (element && hasBoundTextElement(element)) {
+ const boundTextElement = getBoundTextElement(
+ element,
+ app.scene.getNonDeletedElementsMap(),
+ );
+ elementsCopied.push(boundTextElement);
+ }
+ if (element) {
+ copiedStyles = JSON.stringify(elementsCopied);
+ }
+ return {
+ appState: {
+ ...appState,
+ toast: { message: t("toast.copyStyles") },
+ },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+ keyTest: (event) =>
+ event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C,
+});
+
+export const actionPasteStyles = register({
+ name: "pasteStyles",
+ label: "labels.pasteStyles",
+ icon: paintIcon,
+ trackEvent: { category: "element" },
+ perform: (elements, appState, formData, app) => {
+ const elementsCopied = JSON.parse(copiedStyles);
+ const pastedElement = elementsCopied[0];
+ const boundTextElement = elementsCopied[1];
+ if (!isExcalidrawElement(pastedElement)) {
+ return { elements, captureUpdate: CaptureUpdateAction.EVENTUALLY };
+ }
+
+ const selectedElements = getSelectedElements(elements, appState, {
+ includeBoundTextElement: true,
+ });
+ const selectedElementIds = selectedElements.map((element) => element.id);
+ return {
+ elements: elements.map((element) => {
+ if (selectedElementIds.includes(element.id)) {
+ let elementStylesToCopyFrom = pastedElement;
+ if (isTextElement(element) && element.containerId) {
+ elementStylesToCopyFrom = boundTextElement;
+ }
+ if (!elementStylesToCopyFrom) {
+ return element;
+ }
+ let newElement = newElementWith(element, {
+ backgroundColor: elementStylesToCopyFrom?.backgroundColor,
+ strokeWidth: elementStylesToCopyFrom?.strokeWidth,
+ strokeColor: elementStylesToCopyFrom?.strokeColor,
+ strokeStyle: elementStylesToCopyFrom?.strokeStyle,
+ fillStyle: elementStylesToCopyFrom?.fillStyle,
+ opacity: elementStylesToCopyFrom?.opacity,
+ roughness: elementStylesToCopyFrom?.roughness,
+ roundness: elementStylesToCopyFrom.roundness
+ ? canApplyRoundnessTypeToElement(
+ elementStylesToCopyFrom.roundness.type,
+ element,
+ )
+ ? elementStylesToCopyFrom.roundness
+ : getDefaultRoundnessTypeForElement(element)
+ : null,
+ });
+
+ if (isTextElement(newElement)) {
+ const fontSize =
+ (elementStylesToCopyFrom as ExcalidrawTextElement).fontSize ||
+ DEFAULT_FONT_SIZE;
+ const fontFamily =
+ (elementStylesToCopyFrom as ExcalidrawTextElement).fontFamily ||
+ DEFAULT_FONT_FAMILY;
+ newElement = newElementWith(newElement, {
+ fontSize,
+ fontFamily,
+ textAlign:
+ (elementStylesToCopyFrom as ExcalidrawTextElement).textAlign ||
+ DEFAULT_TEXT_ALIGN,
+ lineHeight:
+ (elementStylesToCopyFrom as ExcalidrawTextElement).lineHeight ||
+ getLineHeight(fontFamily),
+ });
+ let container = null;
+ if (newElement.containerId) {
+ container =
+ selectedElements.find(
+ (element) =>
+ isTextElement(newElement) &&
+ element.id === newElement.containerId,
+ ) || null;
+ }
+ redrawTextBoundingBox(
+ newElement,
+ container,
+ app.scene.getNonDeletedElementsMap(),
+ );
+ }
+
+ if (
+ newElement.type === "arrow" &&
+ isArrowElement(elementStylesToCopyFrom)
+ ) {
+ newElement = newElementWith(newElement, {
+ startArrowhead: elementStylesToCopyFrom.startArrowhead,
+ endArrowhead: elementStylesToCopyFrom.endArrowhead,
+ });
+ }
+
+ if (isFrameLikeElement(element)) {
+ newElement = newElementWith(newElement, {
+ roundness: null,
+ backgroundColor: "transparent",
+ });
+ }
+
+ return newElement;
+ }
+ return element;
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ keyTest: (event) =>
+ event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
+});