mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			v0.17.4
			...
			improve_co
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ce4b64b2a3 | ||
| 
						 | 
					ef82e15ee8 | ||
| 
						 | 
					9f6e3c5a9d | ||
| 
						 | 
					8a106dde57 | ||
| 
						 | 
					2dc84f04be | 
@@ -1,28 +1,110 @@
 | 
			
		||||
import {
 | 
			
		||||
  isTextElement,
 | 
			
		||||
  isExcalidrawElement,
 | 
			
		||||
  redrawTextBoundingBox,
 | 
			
		||||
  getNonDeletedElements,
 | 
			
		||||
} from "../element";
 | 
			
		||||
import { CODES, KEYS } from "../keys";
 | 
			
		||||
import { register } from "./register";
 | 
			
		||||
import { mutateElement, newElementWith } from "../element/mutateElement";
 | 
			
		||||
import { newElementWith } from "../element/mutateElement";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_FONT_SIZE,
 | 
			
		||||
  DEFAULT_FONT_FAMILY,
 | 
			
		||||
  DEFAULT_TEXT_ALIGN,
 | 
			
		||||
} from "../constants";
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawElementPossibleProps,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  canChangeSharpness,
 | 
			
		||||
  getSelectedElements,
 | 
			
		||||
  hasBackground,
 | 
			
		||||
  hasStroke,
 | 
			
		||||
  hasText,
 | 
			
		||||
} from "../scene";
 | 
			
		||||
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
 | 
			
		||||
 | 
			
		||||
type AppStateStyles = {
 | 
			
		||||
  [K in AssertSubset<
 | 
			
		||||
    keyof AppState,
 | 
			
		||||
    typeof copyableStyles[number][0]
 | 
			
		||||
  >]: AppState[K];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ElementStyles = {
 | 
			
		||||
  [K in AssertSubset<
 | 
			
		||||
    keyof ExcalidrawElementPossibleProps,
 | 
			
		||||
    typeof copyableStyles[number][1]
 | 
			
		||||
  >]: ExcalidrawElementPossibleProps[K];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ElemelementStylesByType = Record<ExcalidrawElement["type"], ElementStyles>;
 | 
			
		||||
 | 
			
		||||
// `copiedStyles` is exported only for tests.
 | 
			
		||||
export let copiedStyles: string = "{}";
 | 
			
		||||
let COPIED_STYLES: {
 | 
			
		||||
  appStateStyles: Partial<AppStateStyles>;
 | 
			
		||||
  elementStyles: Partial<ElementStyles>;
 | 
			
		||||
  elementStylesByType: Partial<ElemelementStylesByType>;
 | 
			
		||||
} | null = null;
 | 
			
		||||
 | 
			
		||||
/* [AppState prop, ExcalidrawElement prop, predicate] */
 | 
			
		||||
const copyableStyles = [
 | 
			
		||||
  ["currentItemOpacity", "opacity", () => true],
 | 
			
		||||
  ["currentItemStrokeColor", "strokeColor", () => true],
 | 
			
		||||
  ["currentItemStrokeStyle", "strokeStyle", hasStroke],
 | 
			
		||||
  ["currentItemStrokeWidth", "strokeWidth", hasStroke],
 | 
			
		||||
  ["currentItemRoughness", "roughness", hasStroke],
 | 
			
		||||
  ["currentItemBackgroundColor", "backgroundColor", hasBackground],
 | 
			
		||||
  ["currentItemFillStyle", "fillStyle", hasBackground],
 | 
			
		||||
  ["currentItemStrokeSharpness", "strokeSharpness", canChangeSharpness],
 | 
			
		||||
  ["currentItemLinearStrokeSharpness", "strokeSharpness", isLinearElementType],
 | 
			
		||||
  ["currentItemStartArrowhead", "startArrowhead", isLinearElementType],
 | 
			
		||||
  ["currentItemEndArrowhead", "endArrowhead", isLinearElementType],
 | 
			
		||||
  ["currentItemFontFamily", "fontFamily", hasText],
 | 
			
		||||
  ["currentItemFontSize", "fontSize", hasText],
 | 
			
		||||
  ["currentItemTextAlign", "textAlign", hasText],
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
const getCommonStyleProps = (
 | 
			
		||||
  elements: readonly ExcalidrawElement[],
 | 
			
		||||
): Exclude<typeof COPIED_STYLES, null> => {
 | 
			
		||||
  const appStateStyles = {} as AppStateStyles;
 | 
			
		||||
  const elementStyles = {} as ElementStyles;
 | 
			
		||||
 | 
			
		||||
  const elementStylesByType = elements.reduce((acc, element) => {
 | 
			
		||||
    // only use the first element of given type
 | 
			
		||||
    if (!acc[element.type]) {
 | 
			
		||||
      acc[element.type] = {} as ElementStyles;
 | 
			
		||||
      copyableStyles.forEach(([appStateProp, prop, predicate]) => {
 | 
			
		||||
        const value = (element as any)[prop];
 | 
			
		||||
        if (value !== undefined && predicate(element.type)) {
 | 
			
		||||
          if (appStateStyles[appStateProp] === undefined) {
 | 
			
		||||
            (appStateStyles as any)[appStateProp] = value;
 | 
			
		||||
          }
 | 
			
		||||
          if (elementStyles[prop] === undefined) {
 | 
			
		||||
            (elementStyles as any)[prop] = value;
 | 
			
		||||
          }
 | 
			
		||||
          (acc as any)[element.type][prop] = value;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return acc;
 | 
			
		||||
  }, {} as ElemelementStylesByType);
 | 
			
		||||
 | 
			
		||||
  // clone in case we ever make some of the props into non-primitives
 | 
			
		||||
  return JSON.parse(
 | 
			
		||||
    JSON.stringify({ appStateStyles, elementStyles, elementStylesByType }),
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionCopyStyles = register({
 | 
			
		||||
  name: "copyStyles",
 | 
			
		||||
  perform: (elements, appState) => {
 | 
			
		||||
    const element = elements.find((el) => appState.selectedElementIds[el.id]);
 | 
			
		||||
    if (element) {
 | 
			
		||||
      copiedStyles = JSON.stringify(element);
 | 
			
		||||
    }
 | 
			
		||||
    COPIED_STYLES = getCommonStyleProps(
 | 
			
		||||
      getSelectedElements(getNonDeletedElements(elements), appState),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      appState: {
 | 
			
		||||
        ...appState,
 | 
			
		||||
        ...COPIED_STYLES.appStateStyles,
 | 
			
		||||
      },
 | 
			
		||||
      commitToHistory: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
@@ -35,31 +117,49 @@ export const actionCopyStyles = register({
 | 
			
		||||
export const actionPasteStyles = register({
 | 
			
		||||
  name: "pasteStyles",
 | 
			
		||||
  perform: (elements, appState) => {
 | 
			
		||||
    const pastedElement = JSON.parse(copiedStyles);
 | 
			
		||||
    if (!isExcalidrawElement(pastedElement)) {
 | 
			
		||||
    if (!COPIED_STYLES) {
 | 
			
		||||
      return { elements, commitToHistory: false };
 | 
			
		||||
    }
 | 
			
		||||
    const getStyle = <T extends ExcalidrawElement, K extends keyof T>(
 | 
			
		||||
      element: T,
 | 
			
		||||
      prop: K,
 | 
			
		||||
    ) => {
 | 
			
		||||
      return (COPIED_STYLES?.elementStylesByType[element.type]?.[
 | 
			
		||||
        prop as keyof ElementStyles
 | 
			
		||||
      ] ??
 | 
			
		||||
        COPIED_STYLES?.elementStyles[prop as keyof ElementStyles] ??
 | 
			
		||||
        element[prop]) as T[K];
 | 
			
		||||
    };
 | 
			
		||||
    return {
 | 
			
		||||
      elements: elements.map((element) => {
 | 
			
		||||
        if (appState.selectedElementIds[element.id]) {
 | 
			
		||||
          const newElement = newElementWith(element, {
 | 
			
		||||
            backgroundColor: pastedElement?.backgroundColor,
 | 
			
		||||
            strokeWidth: pastedElement?.strokeWidth,
 | 
			
		||||
            strokeColor: pastedElement?.strokeColor,
 | 
			
		||||
            strokeStyle: pastedElement?.strokeStyle,
 | 
			
		||||
            fillStyle: pastedElement?.fillStyle,
 | 
			
		||||
            opacity: pastedElement?.opacity,
 | 
			
		||||
            roughness: pastedElement?.roughness,
 | 
			
		||||
          });
 | 
			
		||||
          if (isTextElement(newElement)) {
 | 
			
		||||
            mutateElement(newElement, {
 | 
			
		||||
              fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
 | 
			
		||||
              fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
 | 
			
		||||
              textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
 | 
			
		||||
          const commonProps = {
 | 
			
		||||
            backgroundColor: getStyle(element, "backgroundColor"),
 | 
			
		||||
            strokeWidth: getStyle(element, "strokeWidth"),
 | 
			
		||||
            strokeColor: getStyle(element, "strokeColor"),
 | 
			
		||||
            strokeStyle: getStyle(element, "strokeStyle"),
 | 
			
		||||
            fillStyle: getStyle(element, "fillStyle"),
 | 
			
		||||
            opacity: getStyle(element, "opacity"),
 | 
			
		||||
            roughness: getStyle(element, "roughness"),
 | 
			
		||||
            strokeSharpness: getStyle(element, "strokeSharpness"),
 | 
			
		||||
          };
 | 
			
		||||
          if (isTextElement(element)) {
 | 
			
		||||
            const newElement = newElementWith(element, {
 | 
			
		||||
              ...commonProps,
 | 
			
		||||
              fontSize: getStyle(element, "fontSize"),
 | 
			
		||||
              fontFamily: getStyle(element, "fontFamily"),
 | 
			
		||||
              textAlign: getStyle(element, "textAlign"),
 | 
			
		||||
            });
 | 
			
		||||
            redrawTextBoundingBox(newElement);
 | 
			
		||||
            return newElement;
 | 
			
		||||
          } else if (isLinearElement(element)) {
 | 
			
		||||
            return newElementWith(element, {
 | 
			
		||||
              ...commonProps,
 | 
			
		||||
              startArrowhead: getStyle(element, "startArrowhead"),
 | 
			
		||||
              endArrowhead: getStyle(element, "endArrowhead"),
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          return newElement;
 | 
			
		||||
          return newElementWith(element, commonProps);
 | 
			
		||||
        }
 | 
			
		||||
        return element;
 | 
			
		||||
      }),
 | 
			
		||||
 
 | 
			
		||||
@@ -24,13 +24,13 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
 | 
			
		||||
  // (see https://github.com/microsoft/TypeScript/issues/21732)
 | 
			
		||||
  const { points } = updates as any;
 | 
			
		||||
 | 
			
		||||
  if (typeof points !== "undefined") {
 | 
			
		||||
  if (points !== undefined) {
 | 
			
		||||
    updates = { ...getSizeFromPoints(points), ...updates };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const key in updates) {
 | 
			
		||||
    const value = (updates as any)[key];
 | 
			
		||||
    if (typeof value !== "undefined") {
 | 
			
		||||
    if (value !== undefined) {
 | 
			
		||||
      if (
 | 
			
		||||
        (element as any)[key] === value &&
 | 
			
		||||
        // if object, always update in case its deep prop was mutated
 | 
			
		||||
@@ -72,9 +72,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    typeof updates.height !== "undefined" ||
 | 
			
		||||
    typeof updates.width !== "undefined" ||
 | 
			
		||||
    typeof points !== "undefined"
 | 
			
		||||
    updates.height !== undefined ||
 | 
			
		||||
    updates.width !== undefined ||
 | 
			
		||||
    points !== undefined
 | 
			
		||||
  ) {
 | 
			
		||||
    invalidateShapeForElement(element);
 | 
			
		||||
  }
 | 
			
		||||
@@ -84,9 +84,12 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
 | 
			
		||||
  Scene.getScene(element)?.informMutation();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const newElementWith = <TElement extends ExcalidrawElement>(
 | 
			
		||||
export const newElementWith = <
 | 
			
		||||
  TElement extends ExcalidrawElement,
 | 
			
		||||
  K extends keyof Omit<TElement, "id" | "version" | "versionNonce">
 | 
			
		||||
>(
 | 
			
		||||
  element: TElement,
 | 
			
		||||
  updates: ElementUpdate<TElement>,
 | 
			
		||||
  updates: Pick<TElement, K>,
 | 
			
		||||
): TElement => ({
 | 
			
		||||
  ...element,
 | 
			
		||||
  ...updates,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ type _ExcalidrawElementBase = Readonly<{
 | 
			
		||||
  strokeWidth: number;
 | 
			
		||||
  strokeStyle: StrokeStyle;
 | 
			
		||||
  strokeSharpness: StrokeSharpness;
 | 
			
		||||
  roughness: number;
 | 
			
		||||
  roughness: 0 | 1 | 2;
 | 
			
		||||
  opacity: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
@@ -110,3 +110,16 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
 | 
			
		||||
    startArrowhead: Arrowhead | null;
 | 
			
		||||
    endArrowhead: Arrowhead | null;
 | 
			
		||||
  }>;
 | 
			
		||||
 | 
			
		||||
export type ExcalidrawElementTypes = Pick<ExcalidrawElement, "type">["type"];
 | 
			
		||||
 | 
			
		||||
/** @private */
 | 
			
		||||
type __ExcalidrawElementPossibleProps_withoutType<T> = T extends any
 | 
			
		||||
  ? { [K in keyof Omit<T, "type">]: T[K] }
 | 
			
		||||
  : never;
 | 
			
		||||
 | 
			
		||||
/** Do not use for anything unless you really need it for some abstract
 | 
			
		||||
    API types */
 | 
			
		||||
export type ExcalidrawElementPossibleProps = UnionToIntersection<
 | 
			
		||||
  __ExcalidrawElementPossibleProps_withoutType<ExcalidrawElement>
 | 
			
		||||
> & { type: ExcalidrawElementTypes };
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -46,6 +46,15 @@ type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
 | 
			
		||||
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
 | 
			
		||||
  Required<Pick<T, RK>>;
 | 
			
		||||
 | 
			
		||||
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
 | 
			
		||||
  x: infer R,
 | 
			
		||||
) => any
 | 
			
		||||
  ? R
 | 
			
		||||
  : never;
 | 
			
		||||
 | 
			
		||||
/** Assert K is a subset of T, and returns K */
 | 
			
		||||
type AssertSubset<T, K extends T> = K;
 | 
			
		||||
 | 
			
		||||
// PNG encoding/decoding
 | 
			
		||||
// -----------------------------------------------------------------------------
 | 
			
		||||
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -81,6 +81,12 @@ export class API {
 | 
			
		||||
    verticalAlign?: T extends "text"
 | 
			
		||||
      ? ExcalidrawTextElement["verticalAlign"]
 | 
			
		||||
      : never;
 | 
			
		||||
    startArrowhead?: T extends "arrow" | "line" | "draw"
 | 
			
		||||
      ? ExcalidrawLinearElement["startArrowhead"]
 | 
			
		||||
      : never;
 | 
			
		||||
    endArrowhead?: T extends "arrow" | "line" | "draw"
 | 
			
		||||
      ? ExcalidrawLinearElement["endArrowhead"]
 | 
			
		||||
      : never;
 | 
			
		||||
  }): T extends "arrow" | "line" | "draw"
 | 
			
		||||
    ? ExcalidrawLinearElement
 | 
			
		||||
    : T extends "text"
 | 
			
		||||
@@ -130,8 +136,8 @@ export class API {
 | 
			
		||||
      case "draw":
 | 
			
		||||
        element = newLinearElement({
 | 
			
		||||
          type: type as "arrow" | "line" | "draw",
 | 
			
		||||
          startArrowhead: null,
 | 
			
		||||
          endArrowhead: null,
 | 
			
		||||
          startArrowhead: rest.startArrowhead ?? null,
 | 
			
		||||
          endArrowhead: rest.endArrowhead ?? null,
 | 
			
		||||
          ...base,
 | 
			
		||||
        });
 | 
			
		||||
        break;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { queryByText } from "@testing-library/react";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import ReactDOM from "react-dom";
 | 
			
		||||
import { copiedStyles } from "../actions/actionStyles";
 | 
			
		||||
import { getDefaultAppState } from "../appState";
 | 
			
		||||
import { ShortcutName } from "../actions/shortcuts";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
import { setLanguage } from "../i18n";
 | 
			
		||||
@@ -775,82 +775,224 @@ describe("regression tests", () => {
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("selecting 'Copy styles' in context menu copies styles", () => {
 | 
			
		||||
    UI.clickTool("rectangle");
 | 
			
		||||
    mouse.down(10, 10);
 | 
			
		||||
    mouse.up(20, 20);
 | 
			
		||||
  it("copy-styles updates appState defaults", () => {
 | 
			
		||||
    h.app.updateScene({
 | 
			
		||||
      elements: [
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "rectangle",
 | 
			
		||||
          id: "A",
 | 
			
		||||
          x: 0,
 | 
			
		||||
          y: 0,
 | 
			
		||||
          opacity: 90,
 | 
			
		||||
          strokeColor: "#FF0000",
 | 
			
		||||
          strokeStyle: "solid",
 | 
			
		||||
          strokeWidth: 10,
 | 
			
		||||
          roughness: 2,
 | 
			
		||||
          backgroundColor: "#00FF00",
 | 
			
		||||
          fillStyle: "solid",
 | 
			
		||||
          strokeSharpness: "sharp",
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "arrow",
 | 
			
		||||
          id: "B",
 | 
			
		||||
          x: 200,
 | 
			
		||||
          y: 200,
 | 
			
		||||
          startArrowhead: "bar",
 | 
			
		||||
          endArrowhead: "bar",
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          id: "C",
 | 
			
		||||
          x: 200,
 | 
			
		||||
          y: 200,
 | 
			
		||||
          fontFamily: 3,
 | 
			
		||||
          fontSize: 200,
 | 
			
		||||
          textAlign: "center",
 | 
			
		||||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    h.app.setState({
 | 
			
		||||
      selectedElementIds: { A: true, B: true, C: true },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const defaultAppState = getDefaultAppState();
 | 
			
		||||
 | 
			
		||||
    expect(h.state).toEqual(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        currentItemOpacity: defaultAppState.currentItemOpacity,
 | 
			
		||||
        currentItemStrokeColor: defaultAppState.currentItemStrokeColor,
 | 
			
		||||
        currentItemStrokeStyle: defaultAppState.currentItemStrokeStyle,
 | 
			
		||||
        currentItemStrokeWidth: defaultAppState.currentItemStrokeWidth,
 | 
			
		||||
        currentItemRoughness: defaultAppState.currentItemRoughness,
 | 
			
		||||
        currentItemBackgroundColor: defaultAppState.currentItemBackgroundColor,
 | 
			
		||||
        currentItemFillStyle: defaultAppState.currentItemFillStyle,
 | 
			
		||||
        currentItemStrokeSharpness: defaultAppState.currentItemStrokeSharpness,
 | 
			
		||||
        currentItemStartArrowhead: defaultAppState.currentItemStartArrowhead,
 | 
			
		||||
        currentItemEndArrowhead: defaultAppState.currentItemEndArrowhead,
 | 
			
		||||
        currentItemFontFamily: defaultAppState.currentItemFontFamily,
 | 
			
		||||
        currentItemFontSize: defaultAppState.currentItemFontSize,
 | 
			
		||||
        currentItemTextAlign: defaultAppState.currentItemTextAlign,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    fireEvent.contextMenu(GlobalTestState.canvas, {
 | 
			
		||||
      button: 2,
 | 
			
		||||
      clientX: 1,
 | 
			
		||||
      clientY: 1,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const contextMenu = document.querySelector(".context-menu");
 | 
			
		||||
    expect(copiedStyles).toBe("{}");
 | 
			
		||||
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
 | 
			
		||||
    expect(copiedStyles).not.toBe("{}");
 | 
			
		||||
    const element = JSON.parse(copiedStyles);
 | 
			
		||||
    expect(element).toEqual(API.getSelectedElement());
 | 
			
		||||
 | 
			
		||||
    expect(h.state).toEqual(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        currentItemOpacity: 90,
 | 
			
		||||
        currentItemStrokeColor: "#FF0000",
 | 
			
		||||
        currentItemStrokeStyle: "solid",
 | 
			
		||||
        currentItemStrokeWidth: 10,
 | 
			
		||||
        currentItemRoughness: 2,
 | 
			
		||||
        currentItemBackgroundColor: "#00FF00",
 | 
			
		||||
        currentItemFillStyle: "solid",
 | 
			
		||||
        currentItemStrokeSharpness: "sharp",
 | 
			
		||||
        currentItemStartArrowhead: "bar",
 | 
			
		||||
        currentItemEndArrowhead: "bar",
 | 
			
		||||
        currentItemFontFamily: 3,
 | 
			
		||||
        currentItemFontSize: 200,
 | 
			
		||||
        currentItemTextAlign: "center",
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("selecting 'Paste styles' in context menu pastes styles", () => {
 | 
			
		||||
    UI.clickTool("rectangle");
 | 
			
		||||
    mouse.down(10, 10);
 | 
			
		||||
    mouse.up(20, 20);
 | 
			
		||||
 | 
			
		||||
    UI.clickTool("rectangle");
 | 
			
		||||
    mouse.down(10, 10);
 | 
			
		||||
    mouse.up(20, 20);
 | 
			
		||||
 | 
			
		||||
    // Change some styles of second rectangle
 | 
			
		||||
    clickLabeledElement("Stroke");
 | 
			
		||||
    clickLabeledElement("#c92a2a");
 | 
			
		||||
    clickLabeledElement("Background");
 | 
			
		||||
    clickLabeledElement("#e64980");
 | 
			
		||||
    // Fill style
 | 
			
		||||
    fireEvent.click(screen.getByTitle("Cross-hatch"));
 | 
			
		||||
    // Stroke width
 | 
			
		||||
    fireEvent.click(screen.getByTitle("Bold"));
 | 
			
		||||
    // Stroke style
 | 
			
		||||
    fireEvent.click(screen.getByTitle("Dotted"));
 | 
			
		||||
    // Roughness
 | 
			
		||||
    fireEvent.click(screen.getByTitle("Cartoonist"));
 | 
			
		||||
    // Opacity
 | 
			
		||||
    fireEvent.change(screen.getByLabelText("Opacity"), {
 | 
			
		||||
      target: { value: "60" },
 | 
			
		||||
  it("paste-styles action", () => {
 | 
			
		||||
    h.app.updateScene({
 | 
			
		||||
      elements: [
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "rectangle",
 | 
			
		||||
          id: "A",
 | 
			
		||||
          x: 0,
 | 
			
		||||
          y: 0,
 | 
			
		||||
          opacity: 90,
 | 
			
		||||
          strokeColor: "#FF0000",
 | 
			
		||||
          strokeStyle: "solid",
 | 
			
		||||
          strokeWidth: 10,
 | 
			
		||||
          roughness: 2,
 | 
			
		||||
          backgroundColor: "#00FF00",
 | 
			
		||||
          fillStyle: "solid",
 | 
			
		||||
          strokeSharpness: "sharp",
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "arrow",
 | 
			
		||||
          id: "B",
 | 
			
		||||
          x: 0,
 | 
			
		||||
          y: 0,
 | 
			
		||||
          startArrowhead: "bar",
 | 
			
		||||
          endArrowhead: "bar",
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          id: "C",
 | 
			
		||||
          x: 0,
 | 
			
		||||
          y: 0,
 | 
			
		||||
          fontFamily: 3,
 | 
			
		||||
          fontSize: 200,
 | 
			
		||||
          textAlign: "center",
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "rectangle",
 | 
			
		||||
          id: "D",
 | 
			
		||||
          x: 200,
 | 
			
		||||
          y: 200,
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "arrow",
 | 
			
		||||
          id: "E",
 | 
			
		||||
          x: 200,
 | 
			
		||||
          y: 200,
 | 
			
		||||
        }),
 | 
			
		||||
        API.createElement({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          id: "F",
 | 
			
		||||
          x: 200,
 | 
			
		||||
          y: 200,
 | 
			
		||||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    h.app.setState({
 | 
			
		||||
      selectedElementIds: { A: true, B: true, C: true },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    mouse.reset();
 | 
			
		||||
    // Copy styles of second rectangle
 | 
			
		||||
    fireEvent.contextMenu(GlobalTestState.canvas, {
 | 
			
		||||
      button: 2,
 | 
			
		||||
      clientX: 40,
 | 
			
		||||
      clientY: 40,
 | 
			
		||||
      clientX: 1,
 | 
			
		||||
      clientY: 1,
 | 
			
		||||
    });
 | 
			
		||||
    let contextMenu = document.querySelector(".context-menu");
 | 
			
		||||
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
 | 
			
		||||
    const secondRect = JSON.parse(copiedStyles);
 | 
			
		||||
    expect(secondRect.id).toBe(h.elements[1].id);
 | 
			
		||||
    fireEvent.click(
 | 
			
		||||
      queryByText(
 | 
			
		||||
        document.querySelector(".context-menu") as HTMLElement,
 | 
			
		||||
        "Copy styles",
 | 
			
		||||
      )!,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    mouse.reset();
 | 
			
		||||
    // Paste styles to first rectangle
 | 
			
		||||
    h.app.setState({
 | 
			
		||||
      selectedElementIds: { D: true, E: true, F: true },
 | 
			
		||||
    });
 | 
			
		||||
    fireEvent.contextMenu(GlobalTestState.canvas, {
 | 
			
		||||
      button: 2,
 | 
			
		||||
      clientX: 10,
 | 
			
		||||
      clientY: 10,
 | 
			
		||||
      clientX: 201,
 | 
			
		||||
      clientY: 201,
 | 
			
		||||
    });
 | 
			
		||||
    contextMenu = document.querySelector(".context-menu");
 | 
			
		||||
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
 | 
			
		||||
    fireEvent.click(
 | 
			
		||||
      queryByText(
 | 
			
		||||
        document.querySelector(".context-menu") as HTMLElement,
 | 
			
		||||
        "Paste styles",
 | 
			
		||||
      )!,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const firstRect = API.getSelectedElement();
 | 
			
		||||
    expect(firstRect.id).toBe(h.elements[0].id);
 | 
			
		||||
    expect(firstRect.strokeColor).toBe("#c92a2a");
 | 
			
		||||
    expect(firstRect.backgroundColor).toBe("#e64980");
 | 
			
		||||
    expect(firstRect.fillStyle).toBe("cross-hatch");
 | 
			
		||||
    expect(firstRect.strokeWidth).toBe(2); // Bold: 2
 | 
			
		||||
    expect(firstRect.strokeStyle).toBe("dotted");
 | 
			
		||||
    expect(firstRect.roughness).toBe(2); // Cartoonist: 2
 | 
			
		||||
    expect(firstRect.opacity).toBe(60);
 | 
			
		||||
    const defaultAppState = getDefaultAppState();
 | 
			
		||||
 | 
			
		||||
    expect(h.elements.find((element) => element.id === "D")).toEqual(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        opacity: 90,
 | 
			
		||||
        strokeColor: "#FF0000",
 | 
			
		||||
        strokeStyle: "solid",
 | 
			
		||||
        strokeWidth: 10,
 | 
			
		||||
        roughness: 2,
 | 
			
		||||
        backgroundColor: "#00FF00",
 | 
			
		||||
        fillStyle: "solid",
 | 
			
		||||
        strokeSharpness: "sharp",
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    expect(h.elements.find((element) => element.id === "E")).toEqual(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        opacity: defaultAppState.currentItemOpacity,
 | 
			
		||||
        strokeColor: defaultAppState.currentItemStrokeColor,
 | 
			
		||||
        strokeStyle: defaultAppState.currentItemStrokeStyle,
 | 
			
		||||
        strokeWidth: defaultAppState.currentItemStrokeWidth,
 | 
			
		||||
        roughness: defaultAppState.currentItemRoughness,
 | 
			
		||||
        backgroundColor: "#00FF00",
 | 
			
		||||
        fillStyle: "solid",
 | 
			
		||||
        strokeSharpness: "sharp",
 | 
			
		||||
        startArrowhead: "bar",
 | 
			
		||||
        endArrowhead: "bar",
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    expect(h.elements.find((element) => element.id === "F")).toEqual(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        opacity: defaultAppState.currentItemOpacity,
 | 
			
		||||
        strokeColor: defaultAppState.currentItemStrokeColor,
 | 
			
		||||
        strokeStyle: defaultAppState.currentItemStrokeStyle,
 | 
			
		||||
        strokeWidth: 10,
 | 
			
		||||
        roughness: 2,
 | 
			
		||||
        backgroundColor: "#00FF00",
 | 
			
		||||
        fillStyle: "solid",
 | 
			
		||||
        strokeSharpness: "sharp",
 | 
			
		||||
        fontFamily: 3,
 | 
			
		||||
        fontSize: 200,
 | 
			
		||||
        textAlign: "center",
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("selecting 'Delete' in context menu deletes element", () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ export type AppState = {
 | 
			
		||||
  currentItemFillStyle: ExcalidrawElement["fillStyle"];
 | 
			
		||||
  currentItemStrokeWidth: number;
 | 
			
		||||
  currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
 | 
			
		||||
  currentItemRoughness: number;
 | 
			
		||||
  currentItemRoughness: ExcalidrawElement["roughness"];
 | 
			
		||||
  currentItemOpacity: number;
 | 
			
		||||
  currentItemFontFamily: FontFamily;
 | 
			
		||||
  currentItemFontSize: number;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user