mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			v0.11.0
			...
			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