diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index 80a9eedaac..512aec733b 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -51,7 +51,7 @@ import { register } from "./register"; import type { AppState, Offsets } from "../types"; -export const actionChangeViewBackgroundColor = register({ +export const actionChangeViewBackgroundColor = register>({ name: "changeViewBackgroundColor", label: "labels.canvasBackground", trackEvent: false, @@ -64,7 +64,7 @@ export const actionChangeViewBackgroundColor = register({ perform: (_, appState, value) => { return { appState: { ...appState, ...value }, - captureUpdate: !!value.viewBackgroundColor + captureUpdate: !!value?.viewBackgroundColor ? CaptureUpdateAction.IMMEDIATELY : CaptureUpdateAction.EVENTUALLY, }; @@ -463,7 +463,7 @@ export const actionZoomToFit = register({ !event[KEYS.CTRL_OR_CMD], }); -export const actionToggleTheme = register({ +export const actionToggleTheme = register({ name: "toggleTheme", label: (_, appState) => { return appState.theme === THEME.DARK diff --git a/packages/excalidraw/actions/actionClipboard.tsx b/packages/excalidraw/actions/actionClipboard.tsx index d9b011d2bc..8d5ed2a30a 100644 --- a/packages/excalidraw/actions/actionClipboard.tsx +++ b/packages/excalidraw/actions/actionClipboard.tsx @@ -20,12 +20,12 @@ import { t } from "../i18n"; import { actionDeleteSelected } from "./actionDeleteSelected"; import { register } from "./register"; -export const actionCopy = register({ +export const actionCopy = register({ name: "copy", label: "labels.copy", icon: DuplicateIcon, trackEvent: { category: "element" }, - perform: async (elements, appState, event: ClipboardEvent | null, app) => { + perform: async (elements, appState, event, app) => { const elementsToCopy = app.scene.getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: true, @@ -109,12 +109,12 @@ export const actionPaste = register({ keyTest: undefined, }); -export const actionCut = register({ +export const actionCut = register({ name: "cut", label: "labels.cut", icon: cutIcon, trackEvent: { category: "element" }, - perform: (elements, appState, event: ClipboardEvent | null, app) => { + perform: (elements, appState, event, app) => { actionCopy.perform(elements, appState, event, app); return actionDeleteSelected.perform(elements, appState, null, app); }, diff --git a/packages/excalidraw/actions/actionExport.tsx b/packages/excalidraw/actions/actionExport.tsx index 908e2463e9..1604d3849c 100644 --- a/packages/excalidraw/actions/actionExport.tsx +++ b/packages/excalidraw/actions/actionExport.tsx @@ -31,7 +31,9 @@ import "../components/ToolIcon.scss"; import { register } from "./register"; -export const actionChangeProjectName = register({ +import type { AppState } from "../types"; + +export const actionChangeProjectName = register({ name: "changeProjectName", label: "labels.fileTitle", trackEvent: false, @@ -51,7 +53,7 @@ export const actionChangeProjectName = register({ ), }); -export const actionChangeExportScale = register({ +export const actionChangeExportScale = register({ name: "changeExportScale", label: "imageExportDialog.scale", trackEvent: { category: "export", action: "scale" }, @@ -101,7 +103,9 @@ export const actionChangeExportScale = register({ }, }); -export const actionChangeExportBackground = register({ +export const actionChangeExportBackground = register< + AppState["exportBackground"] +>({ name: "changeExportBackground", label: "imageExportDialog.label.withBackground", trackEvent: { category: "export", action: "toggleBackground" }, @@ -121,7 +125,9 @@ export const actionChangeExportBackground = register({ ), }); -export const actionChangeExportEmbedScene = register({ +export const actionChangeExportEmbedScene = register< + AppState["exportEmbedScene"] +>({ name: "changeExportEmbedScene", label: "imageExportDialog.tooltip.embedScene", trackEvent: { category: "export", action: "embedScene" }, @@ -288,7 +294,9 @@ export const actionLoadScene = register({ keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O, }); -export const actionExportWithDarkMode = register({ +export const actionExportWithDarkMode = register< + AppState["exportWithDarkMode"] +>({ name: "exportWithDarkMode", label: "imageExportDialog.label.darkMode", trackEvent: { category: "export", action: "toggleTheme" }, diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 0c597f80d8..9730c022f9 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -43,7 +43,12 @@ import { register } from "./register"; import type { AppState } from "../types"; -export const actionFinalize = register({ +type FormData = { + event: PointerEvent; + sceneCoords: { x: number; y: number }; +}; + +export const actionFinalize = register({ name: "finalize", label: "", trackEvent: false, diff --git a/packages/excalidraw/actions/actionNavigate.tsx b/packages/excalidraw/actions/actionNavigate.tsx index 27f0d6024c..02dcecef50 100644 --- a/packages/excalidraw/actions/actionNavigate.tsx +++ b/packages/excalidraw/actions/actionNavigate.tsx @@ -2,6 +2,8 @@ import clsx from "clsx"; import { CaptureUpdateAction } from "@excalidraw/element"; +import { invariant } from "@excalidraw/common"; + import { getClientColor } from "../clients"; import { Avatar } from "../components/Avatar"; import { @@ -16,12 +18,17 @@ import { register } from "./register"; import type { GoToCollaboratorComponentProps } from "../components/UserList"; import type { Collaborator } from "../types"; -export const actionGoToCollaborator = register({ +export const actionGoToCollaborator = register({ name: "goToCollaborator", label: "Go to a collaborator", viewMode: true, trackEvent: { category: "collab" }, - perform: (_elements, appState, collaborator: Collaborator) => { + perform: (_elements, appState, collaborator) => { + invariant( + collaborator, + "actionGoToCollaborator: collaborator should be defined when actionGoToCollaborator is called", + ); + if ( !collaborator.socketId || appState.userToFollow?.socketId === collaborator.socketId || diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 7502404d70..75a7bbd8a1 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1,4 +1,5 @@ import { pointFrom } from "@excalidraw/math"; + import { useEffect, useMemo, useRef, useState } from "react"; import { @@ -21,6 +22,7 @@ import { getLineHeight, isTransparent, reduceToCommonValue, + invariant, } from "@excalidraw/common"; import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element"; @@ -292,13 +294,15 @@ const changeFontSize = ( // ----------------------------------------------------------------------------- -export const actionChangeStrokeColor = register({ +export const actionChangeStrokeColor = register< + Pick +>({ name: "changeStrokeColor", label: "labels.stroke", trackEvent: false, perform: (elements, appState, value) => { return { - ...(value.currentItemStrokeColor && { + ...(value?.currentItemStrokeColor && { elements: changeProperty( elements, appState, @@ -316,7 +320,7 @@ export const actionChangeStrokeColor = register({ ...appState, ...value, }, - captureUpdate: !!value.currentItemStrokeColor + captureUpdate: !!value?.currentItemStrokeColor ? CaptureUpdateAction.IMMEDIATELY : CaptureUpdateAction.EVENTUALLY, }; @@ -346,12 +350,14 @@ export const actionChangeStrokeColor = register({ ), }); -export const actionChangeBackgroundColor = register({ +export const actionChangeBackgroundColor = register< + Pick +>({ name: "changeBackgroundColor", label: "labels.changeBackground", trackEvent: false, perform: (elements, appState, value, app) => { - if (!value.currentItemBackgroundColor) { + if (!value?.currentItemBackgroundColor) { return { appState: { ...appState, @@ -423,7 +429,7 @@ export const actionChangeBackgroundColor = register({ ), }); -export const actionChangeFillStyle = register({ +export const actionChangeFillStyle = register({ name: "changeFillStyle", label: "labels.fill", trackEvent: false, @@ -503,7 +509,9 @@ export const actionChangeFillStyle = register({ }, }); -export const actionChangeStrokeWidth = register({ +export const actionChangeStrokeWidth = register< + ExcalidrawElement["strokeWidth"] +>({ name: "changeStrokeWidth", label: "labels.strokeWidth", trackEvent: false, @@ -559,7 +567,7 @@ export const actionChangeStrokeWidth = register({ ), }); -export const actionChangeSloppiness = register({ +export const actionChangeSloppiness = register({ name: "changeSloppiness", label: "labels.sloppiness", trackEvent: false, @@ -613,7 +621,9 @@ export const actionChangeSloppiness = register({ ), }); -export const actionChangeStrokeStyle = register({ +export const actionChangeStrokeStyle = register< + ExcalidrawElement["strokeStyle"] +>({ name: "changeStrokeStyle", label: "labels.strokeStyle", trackEvent: false, @@ -666,7 +676,7 @@ export const actionChangeStrokeStyle = register({ ), }); -export const actionChangeOpacity = register({ +export const actionChangeOpacity = register({ name: "changeOpacity", label: "labels.opacity", trackEvent: false, @@ -690,78 +700,89 @@ export const actionChangeOpacity = register({ ), }); -export const actionChangeFontSize = register({ - name: "changeFontSize", - label: "labels.fontSize", - trackEvent: false, - perform: (elements, appState, value, app) => { - return changeFontSize(elements, appState, app, () => value, value); +export const actionChangeFontSize = register( + { + name: "changeFontSize", + label: "labels.fontSize", + trackEvent: false, + perform: (elements, appState, value, app) => { + return changeFontSize( + elements, + appState, + app, + () => { + invariant(value, "actionChangeFontSize: Expected a font size value"); + return value; + }, + value, + ); + }, + PanelComponent: ({ elements, appState, updateData, app }) => ( +
+ {t("labels.fontSize")} +
+ { + if (isTextElement(element)) { + return element.fontSize; + } + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); + if (boundTextElement) { + return boundTextElement.fontSize; + } + return null; + }, + (element) => + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, + (hasSelection) => + hasSelection + ? null + : appState.currentItemFontSize || DEFAULT_FONT_SIZE, + )} + onChange={(value) => updateData(value)} + /> +
+
+ ), }, - PanelComponent: ({ elements, appState, updateData, app }) => ( -
- {t("labels.fontSize")} -
- { - if (isTextElement(element)) { - return element.fontSize; - } - const boundTextElement = getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ); - if (boundTextElement) { - return boundTextElement.fontSize; - } - return null; - }, - (element) => - isTextElement(element) || - getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ) !== null, - (hasSelection) => - hasSelection - ? null - : appState.currentItemFontSize || DEFAULT_FONT_SIZE, - )} - onChange={(value) => updateData(value)} - /> -
-
- ), -}); +); export const actionDecreaseFontSize = register({ name: "decreaseFontSize", @@ -821,7 +842,10 @@ type ChangeFontFamilyData = Partial< resetContainers?: true; }; -export const actionChangeFontFamily = register({ +export const actionChangeFontFamily = register<{ + currentItemFontFamily: any; + currentHoveredFontFamily: any; +}>({ name: "changeFontFamily", label: "labels.fontFamily", trackEvent: false, @@ -858,6 +882,8 @@ export const actionChangeFontFamily = register({ }; } + invariant(value, "actionChangeFontFamily: value must be defined"); + const { currentItemFontFamily, currentHoveredFontFamily } = value; let nextCaptureUpdateAction: CaptureUpdateActionType = @@ -1191,7 +1217,7 @@ export const actionChangeFontFamily = register({ }, }); -export const actionChangeTextAlign = register({ +export const actionChangeTextAlign = register({ name: "changeTextAlign", label: "Change text alignment", trackEvent: false, @@ -1283,7 +1309,7 @@ export const actionChangeTextAlign = register({ }, }); -export const actionChangeVerticalAlign = register({ +export const actionChangeVerticalAlign = register({ name: "changeVerticalAlign", label: "Change vertical alignment", trackEvent: { category: "element" }, @@ -1375,7 +1401,7 @@ export const actionChangeVerticalAlign = register({ }, }); -export const actionChangeRoundness = register({ +export const actionChangeRoundness = register<"sharp" | "round">({ name: "changeRoundness", label: "Change edge roundness", trackEvent: false, @@ -1532,15 +1558,16 @@ const getArrowheadOptions = (flip: boolean) => { ] as const; }; -export const actionChangeArrowhead = register({ +export const actionChangeArrowhead = register<{ + position: "start" | "end"; + type: Arrowhead; +}>({ name: "changeArrowhead", label: "Change arrowheads", trackEvent: false, - perform: ( - elements, - appState, - value: { position: "start" | "end"; type: Arrowhead }, - ) => { + perform: (elements, appState, value) => { + invariant(value, "actionChangeArrowhead: value must be defined"); + return { elements: changeProperty(elements, appState, (el) => { if (isLinearElement(el)) { @@ -1616,7 +1643,7 @@ export const actionChangeArrowhead = register({ }, }); -export const actionChangeArrowType = register({ +export const actionChangeArrowType = register({ name: "changeArrowType", label: "Change arrow types", trackEvent: false, diff --git a/packages/excalidraw/actions/register.ts b/packages/excalidraw/actions/register.ts index 7c841e3aee..8f22810393 100644 --- a/packages/excalidraw/actions/register.ts +++ b/packages/excalidraw/actions/register.ts @@ -2,7 +2,12 @@ import type { Action } from "./types"; export let actions: readonly Action[] = []; -export const register = (action: T) => { +export const register = < + TData extends any, + T extends Action = Action, +>( + action: T, +) => { actions = actions.concat(action); return action as T & { keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"]; diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index e6f3631263..0a91bc625a 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -32,10 +32,10 @@ export type ActionResult = } | false; -type ActionFn = ( +type ActionFn = ( elements: readonly OrderedExcalidrawElement[], appState: Readonly, - formData: any, + formData: TData | undefined, app: AppClassProperties, ) => ActionResult | Promise; @@ -158,7 +158,7 @@ export type PanelComponentProps = { ) => React.JSX.Element | null; }; -export interface Action { +export interface Action { name: ActionName; label: | string @@ -175,7 +175,7 @@ export interface Action { elements: readonly ExcalidrawElement[], ) => React.ReactNode); PanelComponent?: React.FC; - perform: ActionFn; + perform: ActionFn; keyPriority?: number; keyTest?: ( event: React.KeyboardEvent | KeyboardEvent, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 7faf954799..aa908a6ba7 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -8009,7 +8009,11 @@ class App extends React.Component { ) < LINE_CONFIRM_THRESHOLD) ) { this.actionManager.executeAction(actionFinalize, "ui", { - event, + event: event.nativeEvent, + sceneCoords: { + x: pointerDownState.origin.x, + y: pointerDownState.origin.y, + }, }); return; }