Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68636236a1 | ||
|
|
59cff0f219 | ||
|
|
81c17a56fb | ||
|
|
802b8c50d5 |
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 };
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class Portal {
|
|||||||
type: sceneType,
|
type: sceneType,
|
||||||
payload: {
|
payload: {
|
||||||
elements: syncableElements,
|
elements: syncableElements,
|
||||||
|
appState: this.app.excalidrawAppState!,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
-9
@@ -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 };
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user