Compare commits

..
Author SHA1 Message Date
Aakansha Doshi 68636236a1 feat: sync backgroud color in collab mode 2020-12-13 21:33:27 +05:30
Aakansha DoshiandGitHub 59cff0f219 ci: Add github action to make sure changelog for @excalidraw/excalidraw is updated (#2518)
Add guidelines for changelog and group the commits
update the changelog with the latest commits since the last release
Co-authored-by: Lipis <lipiridis@gmail.com>
2020-12-13 18:53:14 +05:30
LipisandGitHub 81c17a56fb RTL support for the stats dialog (#2530) 2020-12-13 13:39:45 +01:00
802b8c50d5 Insert Library items in the middle of the screen (#2527)
Co-authored-by: Zen Tang <zen@wayve.ai>
2020-12-13 13:46:42 +02:00
18 changed files with 1226 additions and 1028 deletions
+26
View File
@@ -0,0 +1,26 @@
name: Changelog in sync for packages
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Setup Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install and run changelog check
run: |
npm ci
npm run changelog:check
env:
CI: true
+2 -1
View File
@@ -100,6 +100,7 @@
"test:other": "npm run prettier -- --list-different", "test:other": "npm run prettier -- --list-different",
"test:typecheck": "tsc", "test:typecheck": "tsc",
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false", "test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
"test": "npm run test:app" "test": "npm run test:app",
"changelog:check": "node ./scripts/changelog-check.js"
} }
} }
+34
View File
@@ -0,0 +1,34 @@
const { exec } = require("child_process");
const changeLogCheck = () => {
exec(
"git diff origin/master --cached --name-only",
(error, stdout, stderr) => {
if (error || stderr) {
process.exit(1);
}
if (!stdout || stdout.includes("packages/excalidraw/CHANGELOG.MD")) {
process.exit(0);
}
const onlyNonSrcFilesUpdated = stdout.indexOf("src") < 0;
if (onlyNonSrcFilesUpdated) {
process.exit(0);
}
const changedFiles = stdout.trim().split("\n");
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/;
const excalidrawPackageFiles = changedFiles.filter((file) => {
return file.indexOf("src") >= 0 && !filesToIgnoreRegex.test(file);
});
if (excalidrawPackageFiles.length) {
process.exit(1);
}
process.exit(0);
},
);
};
changeLogCheck();
+28 -128
View File
@@ -1,110 +1,28 @@
import { import {
isTextElement, isTextElement,
isExcalidrawElement,
redrawTextBoundingBox, redrawTextBoundingBox,
getNonDeletedElements,
} from "../element"; } from "../element";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
import { import {
ExcalidrawElement, DEFAULT_FONT_SIZE,
ExcalidrawElementPossibleProps, DEFAULT_FONT_FAMILY,
} from "../element/types"; DEFAULT_TEXT_ALIGN,
import { AppState } from "../types"; } from "../constants";
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. // `copiedStyles` is exported only for tests.
let COPIED_STYLES: { export let copiedStyles: string = "{}";
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({ export const actionCopyStyles = register({
name: "copyStyles", name: "copyStyles",
perform: (elements, appState) => { perform: (elements, appState) => {
COPIED_STYLES = getCommonStyleProps( const element = elements.find((el) => appState.selectedElementIds[el.id]);
getSelectedElements(getNonDeletedElements(elements), appState), if (element) {
); copiedStyles = JSON.stringify(element);
}
return { return {
appState: {
...appState,
...COPIED_STYLES.appStateStyles,
},
commitToHistory: false, commitToHistory: false,
}; };
}, },
@@ -117,49 +35,31 @@ export const actionCopyStyles = register({
export const actionPasteStyles = register({ export const actionPasteStyles = register({
name: "pasteStyles", name: "pasteStyles",
perform: (elements, appState) => { perform: (elements, appState) => {
if (!COPIED_STYLES) { const pastedElement = JSON.parse(copiedStyles);
if (!isExcalidrawElement(pastedElement)) {
return { elements, commitToHistory: false }; 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 { return {
elements: elements.map((element) => { elements: elements.map((element) => {
if (appState.selectedElementIds[element.id]) { if (appState.selectedElementIds[element.id]) {
const commonProps = { const newElement = newElementWith(element, {
backgroundColor: getStyle(element, "backgroundColor"), backgroundColor: pastedElement?.backgroundColor,
strokeWidth: getStyle(element, "strokeWidth"), strokeWidth: pastedElement?.strokeWidth,
strokeColor: getStyle(element, "strokeColor"), strokeColor: pastedElement?.strokeColor,
strokeStyle: getStyle(element, "strokeStyle"), strokeStyle: pastedElement?.strokeStyle,
fillStyle: getStyle(element, "fillStyle"), fillStyle: pastedElement?.fillStyle,
opacity: getStyle(element, "opacity"), opacity: pastedElement?.opacity,
roughness: getStyle(element, "roughness"), roughness: pastedElement?.roughness,
strokeSharpness: getStyle(element, "strokeSharpness"), });
}; if (isTextElement(newElement)) {
if (isTextElement(element)) { mutateElement(newElement, {
const newElement = newElementWith(element, { fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
...commonProps, fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
fontSize: getStyle(element, "fontSize"), textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
fontFamily: getStyle(element, "fontFamily"),
textAlign: getStyle(element, "textAlign"),
}); });
redrawTextBoundingBox(newElement); redrawTextBoundingBox(newElement);
return newElement;
} else if (isLinearElement(element)) {
return newElementWith(element, {
...commonProps,
startArrowhead: getStyle(element, "startArrowhead"),
endArrowhead: getStyle(element, "endArrowhead"),
});
} }
return newElementWith(element, commonProps); return newElement;
} }
return element; return element;
}), }),
+8 -1
View File
@@ -351,6 +351,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const canvasWidth = canvasDOMWidth * canvasScale; const canvasWidth = canvasDOMWidth * canvasScale;
const canvasHeight = canvasDOMHeight * canvasScale; const canvasHeight = canvasDOMHeight * canvasScale;
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
return ( return (
<div <div
className="excalidraw" className="excalidraw"
@@ -371,7 +374,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
onCollabButtonClick={onCollabButtonClick} onCollabButtonClick={onCollabButtonClick}
onLockToggle={this.toggleLock} onLockToggle={this.toggleLock}
onInsertShape={(elements) => onInsertShape={(elements) =>
this.addElementsFromPasteOrLibrary(elements) this.addElementsFromPasteOrLibrary(
elements,
DEFAULT_PASTE_X,
DEFAULT_PASTE_Y,
)
} }
zenModeEnabled={zenModeEnabled} zenModeEnabled={zenModeEnabled}
toggleZenMode={this.toggleZenMode} toggleZenMode={this.toggleZenMode}
+15 -1
View File
@@ -6,8 +6,10 @@
right: 12px; right: 12px;
font-size: 12px; font-size: 12px;
z-index: 999; z-index: 999;
h3 { h3 {
margin: 0 24px 8px 0; margin: 0 24px 8px 0;
white-space: nowrap;
} }
.close { .close {
@@ -29,9 +31,21 @@
} }
tr { tr {
td:nth-child(2) { td:nth-child(2) {
min-width: 48px; min-width: 24px;
text-align: right; text-align: right;
} }
} }
} }
:root[dir="rtl"] & {
left: 12px;
right: initial;
h3 {
margin: 0 0 8px 24px;
}
.close {
float: left;
}
}
} }
+7 -10
View File
@@ -24,13 +24,13 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// (see https://github.com/microsoft/TypeScript/issues/21732) // (see https://github.com/microsoft/TypeScript/issues/21732)
const { points } = updates as any; const { points } = updates as any;
if (points !== undefined) { if (typeof points !== "undefined") {
updates = { ...getSizeFromPoints(points), ...updates }; updates = { ...getSizeFromPoints(points), ...updates };
} }
for (const key in updates) { for (const key in updates) {
const value = (updates as any)[key]; const value = (updates as any)[key];
if (value !== undefined) { if (typeof value !== "undefined") {
if ( if (
(element as any)[key] === value && (element as any)[key] === value &&
// if object, always update in case its deep prop was mutated // if object, always update in case its deep prop was mutated
@@ -72,9 +72,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
} }
if ( if (
updates.height !== undefined || typeof updates.height !== "undefined" ||
updates.width !== undefined || typeof updates.width !== "undefined" ||
points !== undefined typeof points !== "undefined"
) { ) {
invalidateShapeForElement(element); invalidateShapeForElement(element);
} }
@@ -84,12 +84,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
Scene.getScene(element)?.informMutation(); Scene.getScene(element)?.informMutation();
}; };
export const newElementWith = < export const newElementWith = <TElement extends ExcalidrawElement>(
TElement extends ExcalidrawElement,
K extends keyof Omit<TElement, "id" | "version" | "versionNonce">
>(
element: TElement, element: TElement,
updates: Pick<TElement, K>, updates: ElementUpdate<TElement>,
): TElement => ({ ): TElement => ({
...element, ...element,
...updates, ...updates,
+1 -14
View File
@@ -21,7 +21,7 @@ type _ExcalidrawElementBase = Readonly<{
strokeWidth: number; strokeWidth: number;
strokeStyle: StrokeStyle; strokeStyle: StrokeStyle;
strokeSharpness: StrokeSharpness; strokeSharpness: StrokeSharpness;
roughness: 0 | 1 | 2; roughness: number;
opacity: number; opacity: number;
width: number; width: number;
height: number; height: number;
@@ -110,16 +110,3 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
startArrowhead: Arrowhead | null; startArrowhead: Arrowhead | null;
endArrowhead: 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 };
+18 -11
View File
@@ -52,7 +52,7 @@ export interface CollabAPI {
onPointerUpdate: CollabInstance["onPointerUpdate"]; onPointerUpdate: CollabInstance["onPointerUpdate"];
initializeSocketClient: CollabInstance["initializeSocketClient"]; initializeSocketClient: CollabInstance["initializeSocketClient"];
onCollabButtonClick: CollabInstance["onCollabButtonClick"]; onCollabButtonClick: CollabInstance["onCollabButtonClick"];
broadcastElements: CollabInstance["broadcastElements"]; broadcastScene: CollabInstance["broadcastScene"];
} }
type ReconciledElements = readonly ExcalidrawElement[] & { type ReconciledElements = readonly ExcalidrawElement[] & {
@@ -239,11 +239,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return; return;
case SCENE.INIT: { case SCENE.INIT: {
if (!this.portal.socketInitialized) { if (!this.portal.socketInitialized) {
const remoteElements = decryptedData.payload.elements; const { elements, appState } = decryptedData.payload;
const reconciledElements = this.reconcileElements( const reconciledElements = this.reconcileElements(elements);
remoteElements, this.handleRemoteSceneUpdate(reconciledElements, appState, {
);
this.handleRemoteSceneUpdate(reconciledElements, {
init: true, init: true,
}); });
this.initializeSocket(); this.initializeSocket();
@@ -254,6 +252,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
case SCENE.UPDATE: case SCENE.UPDATE:
this.handleRemoteSceneUpdate( this.handleRemoteSceneUpdate(
this.reconcileElements(decryptedData.payload.elements), this.reconcileElements(decryptedData.payload.elements),
decryptedData.payload.appState,
); );
break; break;
case "MOUSE_LOCATION": { case "MOUSE_LOCATION": {
@@ -323,6 +322,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private handleRemoteSceneUpdate = ( private handleRemoteSceneUpdate = (
elements: ReconciledElements, elements: ReconciledElements,
appState: AppState,
{ {
init = false, init = false,
initFromSnapshot = false, initFromSnapshot = false,
@@ -334,6 +334,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.excalidrawRef.current!.updateScene({ this.excalidrawRef.current!.updateScene({
elements, elements,
appState,
commitToHistory: !!init, commitToHistory: !!init,
}); });
@@ -383,22 +384,28 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.portal.broadcastMouseLocation(payload); this.portal.broadcastMouseLocation(payload);
}; };
broadcastElements = ( broadcastScene = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
state: AppState, state: AppState,
) => { ) => {
const didBackgroundUpdate =
this.excalidrawAppState?.viewBackgroundColor !==
state.viewBackgroundColor;
this.excalidrawAppState = state; this.excalidrawAppState = state;
if ( if (
getSceneVersion(elements) > getSceneVersion(elements) >
this.getLastBroadcastedOrReceivedSceneVersion() this.getLastBroadcastedOrReceivedSceneVersion() ||
didBackgroundUpdate
) { ) {
this.portal.broadcastScene( this.portal.broadcastScene(
SCENE.UPDATE, SCENE.UPDATE,
getSyncableElements(elements), getSyncableElements(elements),
false, false,
); );
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements); if (!didBackgroundUpdate) {
this.queueBroadcastAllElements(); this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
this.queueBroadcastAllElements();
}
} }
}; };
@@ -466,7 +473,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
onPointerUpdate: this.onPointerUpdate, onPointerUpdate: this.onPointerUpdate,
initializeSocketClient: this.initializeSocketClient, initializeSocketClient: this.initializeSocketClient,
onCollabButtonClick: this.onCollabButtonClick, onCollabButtonClick: this.onCollabButtonClick,
broadcastElements: this.broadcastElements, broadcastScene: this.broadcastScene,
})} })}
</> </>
); );
+1
View File
@@ -111,6 +111,7 @@ class Portal {
type: sceneType, type: sceneType,
payload: { payload: {
elements: syncableElements, elements: syncableElements,
appState: this.app.excalidrawAppState!,
}, },
}; };
+2
View File
@@ -40,12 +40,14 @@ export type SocketUpdateDataSource = {
type: "SCENE_INIT"; type: "SCENE_INIT";
payload: { payload: {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: AppState;
}; };
}; };
SCENE_UPDATE: { SCENE_UPDATE: {
type: "SCENE_UPDATE"; type: "SCENE_UPDATE";
payload: { payload: {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: AppState;
}; };
}; };
MOUSE_LOCATION: { MOUSE_LOCATION: {
+1 -1
View File
@@ -256,7 +256,7 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
) => { ) => {
saveDebounced(elements, appState); saveDebounced(elements, appState);
if (collab.isCollaborating) { if (collab.isCollaborating) {
collab.broadcastElements(elements, appState); collab.broadcastScene(elements, appState);
} }
}; };
-9
View File
@@ -46,15 +46,6 @@ type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> & type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
Required<Pick<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 // PNG encoding/decoding
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type TEXtChunk = { name: "tEXt"; data: Uint8Array }; type TEXtChunk = { name: "tEXt"; data: Uint8Array };
+33 -2
View File
@@ -1,10 +1,41 @@
# Changelog # Changelog
<!--
Guidelines for changelog:
The change should be grouped under one of the below section and must contain PR link.
- Features: For new features.
- Fixes: For bug fixes.
- Chore: Changes for non src files example package.json.
- Improvements: For any improvements.
- Refactor: For any refactoring.
-->
## 0.1.0 ## [Unreleased]
First release of `@excalidraw/excalidraw` ### Features
- Insert Library items in the middle of the screen [#2527](https://github.com/excalidraw/excalidraw/pull/2527)
- Show shortcut context menu [#2501](https://github.com/excalidraw/excalidraw/pull/2501)
- Aligns arrowhead schemas [#2517](https://github.com/excalidraw/excalidraw/pull/2517)
- Add Cut to menus [#2511](https://github.com/excalidraw/excalidraw/pull/2511)
- More Arrowheads: dot, bar [#2486](https://github.com/excalidraw/excalidraw/pull/2486)
- Support CSV graphs and improve the look and feel [#2495](https://github.com/excalidraw/excalidraw/pull/2495)
### Fixes
- Fix Library Menu Layout [#2502](https://github.com/excalidraw/excalidraw/pull/2502)
### Improvements
- RTL support for the stats dialog [#2530](https://github.com/excalidraw/excalidraw/pull/2530)
- Expand canvas padding based on zoom. [#2515](https://github.com/excalidraw/excalidraw/pull/2515)
- Hide shortcuts on pickers for mobile [#2508](https://github.com/excalidraw/excalidraw/pull/2508)
- Hide stats and scrollToContent-button when mobile menus open [#2509](https://github.com/excalidraw/excalidraw/pull/2509)
### Chore
- Bump ini from 1.3.5 to 1.3.7 in /src/packages/excalidraw [#2500](https://github.com/excalidraw/excalidraw/pull/2500)
## 0.1.1 ## 0.1.1
#### Fix #### Fix
- Update the homepage URL so it redirects to correct readme [#2498](https://github.com/excalidraw/excalidraw/pull/2498) - Update the homepage URL so it redirects to correct readme [#2498](https://github.com/excalidraw/excalidraw/pull/2498)
## 0.1.0
First release of `@excalidraw/excalidraw`
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -81,12 +81,6 @@ export class API {
verticalAlign?: T extends "text" verticalAlign?: T extends "text"
? ExcalidrawTextElement["verticalAlign"] ? ExcalidrawTextElement["verticalAlign"]
: never; : never;
startArrowhead?: T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement["startArrowhead"]
: never;
endArrowhead?: T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement["endArrowhead"]
: never;
}): T extends "arrow" | "line" | "draw" }): T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement ? ExcalidrawLinearElement
: T extends "text" : T extends "text"
@@ -136,8 +130,8 @@ export class API {
case "draw": case "draw":
element = newLinearElement({ element = newLinearElement({
type: type as "arrow" | "line" | "draw", type: type as "arrow" | "line" | "draw",
startArrowhead: rest.startArrowhead ?? null, startArrowhead: null,
endArrowhead: rest.endArrowhead ?? null, endArrowhead: null,
...base, ...base,
}); });
break; break;
+56 -198
View File
@@ -1,7 +1,7 @@
import { queryByText } from "@testing-library/react"; import { queryByText } from "@testing-library/react";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { getDefaultAppState } from "../appState"; import { copiedStyles } from "../actions/actionStyles";
import { ShortcutName } from "../actions/shortcuts"; import { ShortcutName } from "../actions/shortcuts";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { setLanguage } from "../i18n"; import { setLanguage } from "../i18n";
@@ -775,224 +775,82 @@ describe("regression tests", () => {
}); });
}); });
it("copy-styles updates appState defaults", () => { it("selecting 'Copy styles' in context menu copies styles", () => {
h.app.updateScene({ UI.clickTool("rectangle");
elements: [ mouse.down(10, 10);
API.createElement({ mouse.up(20, 20);
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, { fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2, button: 2,
clientX: 1, clientX: 1,
clientY: 1, clientY: 1,
}); });
const contextMenu = document.querySelector(".context-menu"); const contextMenu = document.querySelector(".context-menu");
expect(copiedStyles).toBe("{}");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!); fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
expect(copiedStyles).not.toBe("{}");
expect(h.state).toEqual( const element = JSON.parse(copiedStyles);
expect.objectContaining({ expect(element).toEqual(API.getSelectedElement());
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("paste-styles action", () => { it("selecting 'Paste styles' in context menu pastes styles", () => {
h.app.updateScene({ UI.clickTool("rectangle");
elements: [ mouse.down(10, 10);
API.createElement({ mouse.up(20, 20);
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({ UI.clickTool("rectangle");
selectedElementIds: { A: true, B: true, C: true }, 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" },
}); });
mouse.reset();
// Copy styles of second rectangle
fireEvent.contextMenu(GlobalTestState.canvas, { fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2, button: 2,
clientX: 1, clientX: 40,
clientY: 1, clientY: 40,
}); });
fireEvent.click( let contextMenu = document.querySelector(".context-menu");
queryByText( fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
document.querySelector(".context-menu") as HTMLElement, const secondRect = JSON.parse(copiedStyles);
"Copy styles", expect(secondRect.id).toBe(h.elements[1].id);
)!,
);
h.app.setState({ mouse.reset();
selectedElementIds: { D: true, E: true, F: true }, // Paste styles to first rectangle
});
fireEvent.contextMenu(GlobalTestState.canvas, { fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2, button: 2,
clientX: 201, clientX: 10,
clientY: 201, clientY: 10,
}); });
fireEvent.click( contextMenu = document.querySelector(".context-menu");
queryByText( fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
document.querySelector(".context-menu") as HTMLElement,
"Paste styles",
)!,
);
const defaultAppState = getDefaultAppState(); const firstRect = API.getSelectedElement();
expect(firstRect.id).toBe(h.elements[0].id);
expect(h.elements.find((element) => element.id === "D")).toEqual( expect(firstRect.strokeColor).toBe("#c92a2a");
expect.objectContaining({ expect(firstRect.backgroundColor).toBe("#e64980");
opacity: 90, expect(firstRect.fillStyle).toBe("cross-hatch");
strokeColor: "#FF0000", expect(firstRect.strokeWidth).toBe(2); // Bold: 2
strokeStyle: "solid", expect(firstRect.strokeStyle).toBe("dotted");
strokeWidth: 10, expect(firstRect.roughness).toBe(2); // Cartoonist: 2
roughness: 2, expect(firstRect.opacity).toBe(60);
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", () => { it("selecting 'Delete' in context menu deletes element", () => {
+1 -1
View File
@@ -55,7 +55,7 @@ export type AppState = {
currentItemFillStyle: ExcalidrawElement["fillStyle"]; currentItemFillStyle: ExcalidrawElement["fillStyle"];
currentItemStrokeWidth: number; currentItemStrokeWidth: number;
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"]; currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
currentItemRoughness: ExcalidrawElement["roughness"]; currentItemRoughness: number;
currentItemOpacity: number; currentItemOpacity: number;
currentItemFontFamily: FontFamily; currentItemFontFamily: FontFamily;
currentItemFontSize: number; currentItemFontSize: number;