feats: 为 snow-shot 提供自定义功能

This commit is contained in:
chao 2025-04-18 01:07:56 +08:00
parent 58f7d33d80
commit 27e0350fa7
21 changed files with 940 additions and 478 deletions

View file

@ -848,6 +848,13 @@ const ExcalidrawWrapper = () => {
handleKeyboardGlobally={true} handleKeyboardGlobally={true}
autoFocus={true} autoFocus={true}
theme={editorTheme} theme={editorTheme}
customOptions={{
disableKeyEvents: true,
// hideMainToolbar: true,
// hideMenu: true,
// hideFooter: true,
hideContextMenu: true,
}}
renderTopRightUI={(isMobile) => { renderTopRightUI={(isMobile) => {
if (isMobile || !collabAPI || isCollabDisabled) { if (isMobile || !collabAPI || isCollabDisabled) {
return null; return null;

View file

@ -78,8 +78,8 @@
"autorelease": "node scripts/autorelease.js", "autorelease": "node scripts/autorelease.js",
"prerelease:excalidraw": "node scripts/prerelease.js", "prerelease:excalidraw": "node scripts/prerelease.js",
"release:excalidraw": "node scripts/release.js", "release:excalidraw": "node scripts/release.js",
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/{build,dist}", "rm:build": "rimraf -rfexcalidraw-app/{build,dist,dev-dist} && rimraf -rfpackages/*/{dist,build} && rimraf -rfexamples/*/{build,dist}",
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules", "rm:node_modules": "rimraf -rfnode_modules && rimraf -rfexcalidraw-app/node_modules && rimraf -rfpackages/*/node_modules",
"clean-install": "yarn rm:node_modules && yarn install" "clean-install": "yarn rm:node_modules && yarn install"
}, },
"resolutions": { "resolutions": {

View file

@ -50,7 +50,7 @@
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
"scripts": { "scripts": {
"gen:types": "rm -rf types && tsc", "gen:types": "rimraf -rf types && tsc",
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types" "build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
} }
} }

View file

@ -1,7 +1,10 @@
import type { KeyboardModifiersObject } from "@excalidraw/excalidraw/types";
import { isDarwin } from "./constants"; import { isDarwin } from "./constants";
import type { ValueOf } from "./utility-types"; import type { ValueOf } from "./utility-types";
export const CODES = { export const CODES = {
EQUAL: "Equal", EQUAL: "Equal",
MINUS: "Minus", MINUS: "Minus",
@ -140,12 +143,60 @@ export const isArrowKey = (key: string) =>
key === KEYS.ARROW_DOWN || key === KEYS.ARROW_DOWN ||
key === KEYS.ARROW_UP; key === KEYS.ARROW_UP;
export const shouldResizeFromCenter = (event: MouseEvent | KeyboardEvent) => const shouldResizeFromCenterDefault = (event: MouseEvent | KeyboardEvent) =>
event.altKey; event.altKey;
export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) => const shouldMaintainAspectRatioDefault = (event: MouseEvent | KeyboardEvent) =>
event.shiftKey; event.shiftKey;
const shouldRotateWithDiscreteAngleDefault = (
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
) => event.shiftKey;
const shouldSnappingDefault = (event: KeyboardModifiersObject) =>
event[KEYS.CTRL_OR_CMD];
let shouldResizeFromCenterFunction = shouldResizeFromCenterDefault;
let shouldMaintainAspectRatioFunction = shouldMaintainAspectRatioDefault;
let shouldRotateWithDiscreteAngleFunction =
shouldRotateWithDiscreteAngleDefault;
let shouldSnappingFunction = shouldSnappingDefault;
export const setShouldResizeFromCenter = (
shouldResizeFromCenter: (event: MouseEvent | KeyboardEvent) => boolean,
) => {
shouldResizeFromCenterFunction = shouldResizeFromCenter;
};
export const setShouldMaintainAspectRatio = (
shouldMaintainAspectRatio: (event: MouseEvent | KeyboardEvent) => boolean,
) => {
shouldMaintainAspectRatioFunction = shouldMaintainAspectRatio;
};
export const setShouldRotateWithDiscreteAngle = (
shouldRotateWithDiscreteAngle: (
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
) => boolean,
) => {
shouldRotateWithDiscreteAngleFunction = shouldRotateWithDiscreteAngle;
};
export const setShouldSnapping = (
shouldSnapping: (event: KeyboardModifiersObject) => boolean,
) => {
shouldSnappingFunction = shouldSnapping;
};
export const shouldResizeFromCenter = (event: MouseEvent | KeyboardEvent) =>
shouldResizeFromCenterFunction(event);
export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
shouldMaintainAspectRatioFunction(event);
export const shouldRotateWithDiscreteAngle = ( export const shouldRotateWithDiscreteAngle = (
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>, event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
) => event.shiftKey; ) => shouldRotateWithDiscreteAngleFunction(event);
export const shouldSnapping = (event: KeyboardModifiersObject) =>
shouldSnappingFunction(event);

View file

@ -50,7 +50,7 @@
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
"scripts": { "scripts": {
"gen:types": "rm -rf types && tsc", "gen:types": "rimraf -rf types && tsc",
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types" "build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
} }
} }

View file

@ -1,5 +1,5 @@
import { pointFrom } from "@excalidraw/math"; import { pointFrom } from "@excalidraw/math";
import { useEffect, useMemo, useRef, useState } from "react"; import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
@ -137,7 +137,12 @@ import { CaptureUpdateAction } from "../store";
import { register } from "./register"; import { register } from "./register";
import type { CaptureUpdateActionType } from "../store"; import type { CaptureUpdateActionType } from "../store";
import type { AppClassProperties, AppState, Primitive } from "../types"; import {
ExcalidrawPropsCustomOptionsContext,
type AppClassProperties,
type AppState,
type Primitive,
} from "../types";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@ -322,11 +327,17 @@ export const actionChangeStrokeColor = register({
: CaptureUpdateAction.EVENTUALLY, : CaptureUpdateAction.EVENTUALLY,
}; };
}, },
PanelComponent: ({ elements, appState, updateData, appProps }) => ( PanelComponent: ({ elements, appState, updateData, appProps }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
return (
<> <>
<h3 aria-hidden="true">{t("labels.stroke")}</h3> <h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker <ColorPicker
topPicks={DEFAULT_ELEMENT_STROKE_PICKS} topPicks={
customOptions?.pickerRenders?.elementStrokeColors ??
DEFAULT_ELEMENT_STROKE_PICKS
}
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE} palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
type="elementStroke" type="elementStroke"
label={t("labels.stroke")} label={t("labels.stroke")}
@ -343,7 +354,8 @@ export const actionChangeStrokeColor = register({
updateData={updateData} updateData={updateData}
/> />
</> </>
), );
},
}); });
export const actionChangeBackgroundColor = register({ export const actionChangeBackgroundColor = register({
@ -368,11 +380,17 @@ export const actionChangeBackgroundColor = register({
: CaptureUpdateAction.EVENTUALLY, : CaptureUpdateAction.EVENTUALLY,
}; };
}, },
PanelComponent: ({ elements, appState, updateData, appProps }) => ( PanelComponent: ({ elements, appState, updateData, appProps }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
return (
<> <>
<h3 aria-hidden="true">{t("labels.background")}</h3> <h3 aria-hidden="true">{t("labels.background")}</h3>
<ColorPicker <ColorPicker
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS} topPicks={
customOptions?.pickerRenders?.elementBackgroundColors ??
DEFAULT_ELEMENT_BACKGROUND_PICKS
}
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE} palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
type="elementBackground" type="elementBackground"
label={t("labels.background")} label={t("labels.background")}
@ -383,13 +401,16 @@ export const actionChangeBackgroundColor = register({
true, true,
appState.currentItemBackgroundColor, appState.currentItemBackgroundColor,
)} )}
onChange={(color) => updateData({ currentItemBackgroundColor: color })} onChange={(color) =>
updateData({ currentItemBackgroundColor: color })
}
elements={elements} elements={elements}
appState={appState} appState={appState}
updateData={updateData} updateData={updateData}
/> />
</> </>
), );
},
}); });
export const actionChangeFillStyle = register({ export const actionChangeFillStyle = register({

View file

@ -7,6 +7,8 @@ import {
moveAllRight, moveAllRight,
} from "@excalidraw/element/zindex"; } from "@excalidraw/element/zindex";
import { useContext } from "react";
import { import {
BringForwardIcon, BringForwardIcon,
BringToFrontIcon, BringToFrontIcon,
@ -16,6 +18,8 @@ import {
import { t } from "../i18n"; import { t } from "../i18n";
import { CaptureUpdateAction } from "../store"; import { CaptureUpdateAction } from "../store";
import { ExcalidrawPropsCustomOptionsContext } from "../types";
import { register } from "./register"; import { register } from "./register";
export const actionSendBackward = register({ export const actionSendBackward = register({
@ -36,7 +40,19 @@ export const actionSendBackward = register({
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
!event.shiftKey && !event.shiftKey &&
event.code === CODES.BRACKET_LEFT, event.code === CODES.BRACKET_LEFT,
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData, appState }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
if (customOptions?.pickerRenders?.layerButtonRender) {
return customOptions.pickerRenders.layerButtonRender({
onClick: () => updateData(null),
title: `${t("labels.sendBackward")}`,
children: SendBackwardIcon,
name: "sendBackward",
});
}
return (
<button <button
type="button" type="button"
className="zIndexButton" className="zIndexButton"
@ -45,7 +61,8 @@ export const actionSendBackward = register({
> >
{SendBackwardIcon} {SendBackwardIcon}
</button> </button>
), );
},
}); });
export const actionBringForward = register({ export const actionBringForward = register({
@ -66,7 +83,19 @@ export const actionBringForward = register({
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
!event.shiftKey && !event.shiftKey &&
event.code === CODES.BRACKET_RIGHT, event.code === CODES.BRACKET_RIGHT,
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData, appState }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
if (customOptions?.pickerRenders?.layerButtonRender) {
return customOptions.pickerRenders.layerButtonRender({
onClick: () => updateData(null),
title: `${t("labels.bringForward")}`,
children: BringForwardIcon,
name: "bringForward",
});
}
return (
<button <button
type="button" type="button"
className="zIndexButton" className="zIndexButton"
@ -75,7 +104,8 @@ export const actionBringForward = register({
> >
{BringForwardIcon} {BringForwardIcon}
</button> </button>
), );
},
}); });
export const actionSendToBack = register({ export const actionSendToBack = register({
@ -99,7 +129,19 @@ export const actionSendToBack = register({
: event[KEYS.CTRL_OR_CMD] && : event[KEYS.CTRL_OR_CMD] &&
event.shiftKey && event.shiftKey &&
event.code === CODES.BRACKET_LEFT, event.code === CODES.BRACKET_LEFT,
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData, appState }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
if (customOptions?.pickerRenders?.layerButtonRender) {
return customOptions.pickerRenders.layerButtonRender({
onClick: () => updateData(null),
title: `${t("labels.sendToBack")}`,
children: SendToBackIcon,
name: "sendToBack",
});
}
return (
<button <button
type="button" type="button"
className="zIndexButton" className="zIndexButton"
@ -112,7 +154,8 @@ export const actionSendToBack = register({
> >
{SendToBackIcon} {SendToBackIcon}
</button> </button>
), );
},
}); });
export const actionBringToFront = register({ export const actionBringToFront = register({
@ -137,7 +180,19 @@ export const actionBringToFront = register({
: event[KEYS.CTRL_OR_CMD] && : event[KEYS.CTRL_OR_CMD] &&
event.shiftKey && event.shiftKey &&
event.code === CODES.BRACKET_RIGHT, event.code === CODES.BRACKET_RIGHT,
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData, appState }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
if (customOptions?.pickerRenders?.layerButtonRender) {
return customOptions.pickerRenders.layerButtonRender({
onClick: () => updateData(null),
title: `${t("labels.bringToFront")}`,
children: BringToFrontIcon,
name: "bringToFront",
});
}
return (
<button <button
type="button" type="button"
className="zIndexButton" className="zIndexButton"
@ -150,5 +205,6 @@ export const actionBringToFront = register({
> >
{BringToFrontIcon} {BringToFrontIcon}
</button> </button>
), );
},
}); });

View file

@ -160,7 +160,6 @@ export class ActionManager {
const appState = this.getAppState(); const appState = this.getAppState();
const updateData = (formState?: any) => { const updateData = (formState?: any) => {
trackAction(action, "ui", appState, elements, this.app, formState); trackAction(action, "ui", appState, elements, this.app, formState);
this.updater( this.updater(
action.perform( action.perform(
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),

View file

@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import { useState } from "react"; import { useState, useContext } from "react";
import { import {
CLASSES, CLASSES,
@ -46,6 +46,8 @@ import {
hasStrokeWidth, hasStrokeWidth,
} from "../scene"; } from "../scene";
import { ExcalidrawPropsCustomOptionsContext } from "../types";
import { SHAPES } from "./shapes"; import { SHAPES } from "./shapes";
import "./Actions.scss"; import "./Actions.scss";
@ -112,6 +114,8 @@ export const SelectedShapeActions = ({
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
app: AppClassProperties; app: AppClassProperties;
}) => { }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
const targetElements = getTargetElements(elementsMap, appState); const targetElements = getTargetElements(elementsMap, appState);
let isSingleElementBoundContainer = false; let isSingleElementBoundContainer = false;
@ -212,12 +216,22 @@ export const SelectedShapeActions = ({
<fieldset> <fieldset>
<legend>{t("labels.layers")}</legend> <legend>{t("labels.layers")}</legend>
<div className="buttonList"> {!customOptions?.pickerRenders?.ButtonList && (
<div className={"buttonList"}>
{renderAction("sendToBack")} {renderAction("sendToBack")}
{renderAction("sendBackward")} {renderAction("sendBackward")}
{renderAction("bringForward")} {renderAction("bringForward")}
{renderAction("bringToFront")} {renderAction("bringToFront")}
</div> </div>
)}
{customOptions?.pickerRenders?.ButtonList && (
<customOptions.pickerRenders.ButtonList>
{renderAction("sendToBack")}
{renderAction("sendBackward")}
{renderAction("bringForward")}
{renderAction("bringToFront")}
</customOptions.pickerRenders.ButtonList>
)}
</fieldset> </fieldset>
{showAlignActions && !isSingleElementBoundContainer && ( {showAlignActions && !isSingleElementBoundContainer && (

View file

@ -100,6 +100,10 @@ import {
arrayToMap, arrayToMap,
type EXPORT_IMAGE_TYPES, type EXPORT_IMAGE_TYPES,
randomInteger, randomInteger,
setShouldResizeFromCenter,
setShouldMaintainAspectRatio,
setShouldRotateWithDiscreteAngle,
setShouldSnapping,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@ -810,6 +814,26 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.registerAction( this.actionManager.registerAction(
createRedoAction(this.history, this.store), createRedoAction(this.history, this.store),
); );
// 初始化一些配置
if (this.props.customOptions?.shouldResizeFromCenter) {
setShouldResizeFromCenter(
this.props.customOptions.shouldResizeFromCenter,
);
}
if (this.props.customOptions?.shouldMaintainAspectRatio) {
setShouldMaintainAspectRatio(
this.props.customOptions.shouldMaintainAspectRatio,
);
}
if (this.props.customOptions?.shouldRotateWithDiscreteAngle) {
setShouldRotateWithDiscreteAngle(
this.props.customOptions.shouldRotateWithDiscreteAngle,
);
}
if (this.props.customOptions?.shouldSnapping) {
setShouldSnapping(this.props.customOptions.shouldSnapping);
}
} }
private onWindowMessage(event: MessageEvent) { private onWindowMessage(event: MessageEvent) {
@ -1648,10 +1672,12 @@ class App extends React.Component<AppProps, AppState> {
generateLinkForSelection={ generateLinkForSelection={
this.props.generateLinkForSelection this.props.generateLinkForSelection
} }
customOptions={this.props.customOptions}
> >
{this.props.children} {this.props.children}
</LayerUI> </LayerUI>
<div className="excalidraw-container-inner">
<div className="excalidraw-textEditorContainer" /> <div className="excalidraw-textEditorContainer" />
<div className="excalidraw-contextMenuContainer" /> <div className="excalidraw-contextMenuContainer" />
<div className="excalidraw-eye-dropper-container" /> <div className="excalidraw-eye-dropper-container" />
@ -1757,7 +1783,8 @@ class App extends React.Component<AppProps, AppState> {
closable={this.state.toast.closable} closable={this.state.toast.closable}
/> />
)} )}
{this.state.contextMenu && ( {this.state.contextMenu &&
!this.props.customOptions?.hideContextMenu && (
<ContextMenu <ContextMenu
items={this.state.contextMenu.items} items={this.state.contextMenu.items}
top={this.state.contextMenu.top} top={this.state.contextMenu.top}
@ -1789,8 +1816,10 @@ class App extends React.Component<AppProps, AppState> {
renderGrid: isGridModeEnabled(this), renderGrid: isGridModeEnabled(this),
canvasBackgroundColor: canvasBackgroundColor:
this.state.viewBackgroundColor, this.state.viewBackgroundColor,
embedsValidationStatus: this.embedsValidationStatus, embedsValidationStatus:
elementsPendingErasure: this.elementsPendingErasure, this.embedsValidationStatus,
elementsPendingErasure:
this.elementsPendingErasure,
pendingFlowchartNodes: pendingFlowchartNodes:
this.flowChartCreator.pendingNodes, this.flowChartCreator.pendingNodes,
}} }}
@ -1851,6 +1880,7 @@ class App extends React.Component<AppProps, AppState> {
/> />
)} )}
{this.renderFrameNames()} {this.renderFrameNames()}
</div>
</ExcalidrawActionManagerContext.Provider> </ExcalidrawActionManagerContext.Provider>
{this.renderEmbeddables()} {this.renderEmbeddables()}
</ExcalidrawElementsContext.Provider> </ExcalidrawElementsContext.Provider>
@ -2242,7 +2272,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevAppState) => { this.setState((prevAppState) => {
const actionAppState = actionResult.appState || {}; const actionAppState = actionResult.appState || {};
return { const res = {
...prevAppState, ...prevAppState,
...actionAppState, ...actionAppState,
// NOTE this will prevent opening context menu using an action // NOTE this will prevent opening context menu using an action
@ -2256,6 +2286,21 @@ class App extends React.Component<AppProps, AppState> {
name, name,
errorMessage, errorMessage,
}; };
// Print differences between prevAppState and res
const differences = Object.keys(res).filter((key) => {
return (prevAppState as any)[key] !== (res as any)[key];
});
console.log(
"State differences:",
differences.map((key) => ({
key,
prev: (prevAppState as any)[key],
new: (res as any)[key],
})),
);
return res;
}); });
didUpdate = true; didUpdate = true;
@ -2964,6 +3009,10 @@ class App extends React.Component<AppProps, AppState> {
// Copy/paste // Copy/paste
private onCut = withBatchedUpdates((event: ClipboardEvent) => { private onCut = withBatchedUpdates((event: ClipboardEvent) => {
if (this.props.customOptions?.disableKeyEvents) {
return;
}
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains( const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
document.activeElement, document.activeElement,
); );
@ -4089,6 +4138,10 @@ class App extends React.Component<AppProps, AppState> {
// Input handling // Input handling
private onKeyDown = withBatchedUpdates( private onKeyDown = withBatchedUpdates(
(event: React.KeyboardEvent | KeyboardEvent) => { (event: React.KeyboardEvent | KeyboardEvent) => {
if (this.props.customOptions?.disableKeyEvents) {
return;
}
// normalize `event.key` when CapsLock is pressed #2372 // normalize `event.key` when CapsLock is pressed #2372
if ( if (
@ -5958,7 +6011,12 @@ class App extends React.Component<AppProps, AppState> {
let dxFromLastCommitted = gridX - rx - lastCommittedX; let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY; let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) { if (
(
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
shouldRotateWithDiscreteAngle
)(event)
) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } = ({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
getLockedLinearCursorAlignSize( getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point // actual coordinate of the last committed point
@ -8560,7 +8618,13 @@ class App extends React.Component<AppProps, AppState> {
let dx = gridX - newElement.x; let dx = gridX - newElement.x;
let dy = gridY - newElement.y; let dy = gridY - newElement.y;
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) { if (
(
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
shouldRotateWithDiscreteAngle
)(event) &&
points.length === 2
) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize( ({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
newElement.x, newElement.x,
newElement.y, newElement.y,
@ -10451,7 +10515,7 @@ class App extends React.Component<AppProps, AppState> {
width: distance(pointerDownState.origin.x, pointerCoords.x), width: distance(pointerDownState.origin.x, pointerCoords.x),
height: distance(pointerDownState.origin.y, pointerCoords.y), height: distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
shouldResizeFromCenter: false, shouldResizeFromCenter: shouldResizeFromCenter(event),
zoom: this.state.zoom.value, zoom: this.state.zoom.value,
informMutation, informMutation,
}); });
@ -10744,11 +10808,16 @@ class App extends React.Component<AppProps, AppState> {
selectedElements, selectedElements,
this.scene.getElementsMapIncludingDeleted(), this.scene.getElementsMapIncludingDeleted(),
this.scene, this.scene,
shouldRotateWithDiscreteAngle(event), (
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
shouldRotateWithDiscreteAngle
)(event),
shouldResizeFromCenter(event), shouldResizeFromCenter(event),
selectedElements.some((element) => isImageElement(element)) selectedElements.some((element) =>
isImageElement(element)
? !shouldMaintainAspectRatio(event) ? !shouldMaintainAspectRatio(event)
: shouldMaintainAspectRatio(event), : shouldMaintainAspectRatio(event),
),
resizeX, resizeX,
resizeY, resizeY,
pointerDownState.resize.center.x, pointerDownState.resize.center.x,

View file

@ -1,12 +1,12 @@
import clsx from "clsx"; import clsx from "clsx";
import { useContext, type JSX } from "react";
import { ExcalidrawPropsCustomOptionsContext } from "../types";
import { ButtonIcon } from "./ButtonIcon"; import { ButtonIcon } from "./ButtonIcon";
import type { JSX } from "react"; export type ButtonIconSelectProps<T> = {
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
export const ButtonIconSelect = <T extends Object>(
props: {
options: { options: {
value: T; value: T;
text: string; text: string;
@ -26,11 +26,38 @@ export const ButtonIconSelect = <T extends Object>(
event: React.MouseEvent<HTMLButtonElement, MouseEvent>, event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => void; ) => void;
} }
), );
) => (
<div className="buttonList"> // TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
{props.options.map((option) => export const ButtonIconSelect = <T extends Object>(
props.type === "button" ? ( props: ButtonIconSelectProps<T>,
) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
if (customOptions?.pickerRenders?.buttonIconSelectRender) {
return customOptions.pickerRenders.buttonIconSelectRender(props);
}
const renderButtonIcon = (
option: ButtonIconSelectProps<T>["options"][number],
) => {
if (props.type !== "button") {
return null;
}
if (customOptions?.pickerRenders?.CustomButtonIcon) {
return (
<customOptions.pickerRenders.CustomButtonIcon
key={option.text}
icon={option.icon}
title={option.text}
testId={option.testId}
active={option.active ?? props.value === option.value}
onClick={(event) => props.onClick(option.value, event)}
/>
);
}
return (
<ButtonIcon <ButtonIcon
key={option.text} key={option.text}
icon={option.icon} icon={option.icon}
@ -39,7 +66,31 @@ export const ButtonIconSelect = <T extends Object>(
active={option.active ?? props.value === option.value} active={option.active ?? props.value === option.value}
onClick={(event) => props.onClick(option.value, event)} onClick={(event) => props.onClick(option.value, event)}
/> />
) : ( );
};
const renderRadioButtonIcon = (
option: ButtonIconSelectProps<T>["options"][number],
) => {
if (props.type === "button") {
return null;
}
if (customOptions?.pickerRenders?.buttonIconSelectRadioRender) {
return customOptions.pickerRenders.buttonIconSelectRadioRender({
key: option.text,
active: props.value === option.value,
title: option.text,
name: props.group,
onChange: () => props.onChange(option.value),
checked: props.value === option.value,
dataTestid: option.testId ?? "",
children: option.icon,
value: option.value,
});
}
return (
<label <label
key={option.text} key={option.text}
className={clsx({ active: props.value === option.value })} className={clsx({ active: props.value === option.value })}
@ -54,7 +105,16 @@ export const ButtonIconSelect = <T extends Object>(
/> />
{option.icon} {option.icon}
</label> </label>
), );
};
return (
<div className="buttonList">
{props.options.map((option) =>
props.type === "button"
? renderButtonIcon(option)
: renderRadioButtonIcon(option),
)} )}
</div> </div>
); );
};

View file

@ -1,6 +1,6 @@
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx"; import clsx from "clsx";
import { useRef } from "react"; import { useContext, useRef } from "react";
import { import {
COLOR_OUTLINE_CONTRAST_THRESHOLD, COLOR_OUTLINE_CONTRAST_THRESHOLD,
@ -19,6 +19,11 @@ import { ButtonSeparator } from "../ButtonSeparator";
import { activeEyeDropperAtom } from "../EyeDropper"; import { activeEyeDropperAtom } from "../EyeDropper";
import { PropertiesPopover } from "../PropertiesPopover"; import { PropertiesPopover } from "../PropertiesPopover";
import {
ExcalidrawPropsCustomOptionsContext,
type AppState,
} from "../../types";
import { ColorInput } from "./ColorInput"; import { ColorInput } from "./ColorInput";
import { Picker } from "./Picker"; import { Picker } from "./Picker";
import PickerHeading from "./PickerHeading"; import PickerHeading from "./PickerHeading";
@ -29,8 +34,6 @@ import "./ColorPicker.scss";
import type { ColorPickerType } from "./colorPickerUtils"; import type { ColorPickerType } from "./colorPickerUtils";
import type { AppState } from "../../types";
const isValidColor = (color: string) => { const isValidColor = (color: string) => {
const style = new Option().style; const style = new Option().style;
style.color = color; style.color = color;
@ -220,16 +223,22 @@ export const ColorPicker = ({
updateData, updateData,
appState, appState,
}: ColorPickerProps) => { }: ColorPickerProps) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
const renderPopover = () => {
if (customOptions?.pickerRenders?.colorPickerPopoverRender) {
return customOptions.pickerRenders.colorPickerPopoverRender({
color,
label,
type,
onChange,
elements,
palette,
updateData,
});
}
return ( return (
<div>
<div role="dialog" aria-modal="true" className="color-picker-container">
<TopPicks
activeColor={color}
onChange={onChange}
type={type}
topPicks={topPicks}
/>
<ButtonSeparator />
<Popover.Root <Popover.Root
open={appState.openPopup === type} open={appState.openPopup === type}
onOpenChange={(open) => { onOpenChange={(open) => {
@ -251,6 +260,20 @@ export const ColorPicker = ({
/> />
)} )}
</Popover.Root> </Popover.Root>
);
};
return (
<div>
<div role="dialog" aria-modal="true" className="color-picker-container">
<TopPicks
activeColor={color}
onChange={onChange}
type={type}
topPicks={topPicks}
/>
<ButtonSeparator />
{renderPopover()}
</div> </div>
</div> </div>
); );

View file

@ -7,10 +7,13 @@ import {
DEFAULT_ELEMENT_STROKE_PICKS, DEFAULT_ELEMENT_STROKE_PICKS,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { useContext } from "react";
import { ExcalidrawPropsCustomOptionsContext } from "@excalidraw/excalidraw/types";
import { isColorDark } from "./colorPickerUtils"; import { isColorDark } from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils"; import type { ColorPickerType } from "./colorPickerUtils";
interface TopPicksProps { interface TopPicksProps {
onChange: (color: string) => void; onChange: (color: string) => void;
type: ColorPickerType; type: ColorPickerType;
@ -24,13 +27,19 @@ export const TopPicks = ({
activeColor, activeColor,
topPicks, topPicks,
}: TopPicksProps) => { }: TopPicksProps) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
let colors; let colors;
if (type === "elementStroke") { if (type === "elementStroke") {
colors = DEFAULT_ELEMENT_STROKE_PICKS; colors =
customOptions?.pickerRenders?.elementStrokeColors ??
DEFAULT_ELEMENT_STROKE_PICKS;
} }
if (type === "elementBackground") { if (type === "elementBackground") {
colors = DEFAULT_ELEMENT_BACKGROUND_PICKS; colors =
customOptions?.pickerRenders?.elementBackgroundColors ??
DEFAULT_ELEMENT_BACKGROUND_PICKS;
} }
if (type === "canvasBackground") { if (type === "canvasBackground") {
@ -49,7 +58,21 @@ export const TopPicks = ({
return ( return (
<div className="color-picker__top-picks"> <div className="color-picker__top-picks">
{colors.map((color: string) => ( {colors.map((color: string) => {
if (customOptions?.pickerRenders?.colorPickerTopPickesButtonRender) {
return customOptions.pickerRenders.colorPickerTopPickesButtonRender({
active: color === activeColor,
color,
isTransparent: color === "transparent" || !color,
hasOutline: !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
onClick: () => onChange(color),
dataTestid: `color-top-pick-${color}`,
children: <div className="color-picker__button-outline" />,
key: color,
});
}
return (
<button <button
className={clsx("color-picker__button", { className={clsx("color-picker__button", {
active: color === activeColor, active: color === activeColor,
@ -68,7 +91,8 @@ export const TopPicks = ({
> >
<div className="color-picker__button-outline" /> <div className="color-picker__button-outline" />
</button> </button>
))} );
})}
</div> </div>
); );
}; };

View file

@ -72,6 +72,7 @@ import type {
BinaryFiles, BinaryFiles,
UIAppState, UIAppState,
AppClassProperties, AppClassProperties,
ExcalidrawPropsCustomOptions,
} from "../types"; } from "../types";
interface LayerUIProps { interface LayerUIProps {
@ -95,6 +96,7 @@ interface LayerUIProps {
app: AppClassProperties; app: AppClassProperties;
isCollaborating: boolean; isCollaborating: boolean;
generateLinkForSelection?: AppProps["generateLinkForSelection"]; generateLinkForSelection?: AppProps["generateLinkForSelection"];
customOptions?: ExcalidrawPropsCustomOptions;
} }
const DefaultMainMenu: React.FC<{ const DefaultMainMenu: React.FC<{
@ -153,6 +155,7 @@ const LayerUI = ({
app, app,
isCollaborating, isCollaborating,
generateLinkForSelection, generateLinkForSelection,
customOptions,
}: LayerUIProps) => { }: LayerUIProps) => {
const device = useDevice(); const device = useDevice();
const tunnels = useInitializeTunnels(); const tunnels = useInitializeTunnels();
@ -209,13 +212,8 @@ const LayerUI = ({
</div> </div>
); );
const renderSelectedShapeActions = () => ( const renderSelectedShapeActions = () => {
<Section const children = (
heading="selectedShapeActions"
className={clsx("selected-shape-actions zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
})}
>
<Island <Island
className={CLASSES.SHAPE_ACTIONS_MENU} className={CLASSES.SHAPE_ACTIONS_MENU}
padding={2} padding={2}
@ -232,8 +230,19 @@ const LayerUI = ({
app={app} app={app}
/> />
</Island> </Island>
);
return (
<Section
heading="selectedShapeActions"
className={clsx("selected-shape-actions zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
})}
>
{customOptions?.layoutRenders?.menuRender?.({ children }) ?? children}
</Section> </Section>
); );
};
const renderFixedSideContainer = () => { const renderFixedSideContainer = () => {
const shouldRenderSelectedShapeActions = showSelectedShapeActions( const shouldRenderSelectedShapeActions = showSelectedShapeActions(
@ -249,7 +258,10 @@ const LayerUI = ({
return ( return (
<FixedSideContainer side="top"> <FixedSideContainer side="top">
<div className="App-menu App-menu_top"> <div
className="App-menu App-menu_top"
style={{ display: customOptions?.hideMenu ? "none" : undefined }}
>
<Stack.Col gap={6} className={clsx("App-menu_top__left")}> <Stack.Col gap={6} className={clsx("App-menu_top__left")}>
{renderCanvasActions()} {renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
@ -274,6 +286,11 @@ const LayerUI = ({
className={clsx("App-toolbar", { className={clsx("App-toolbar", {
"zen-mode": appState.zenModeEnabled, "zen-mode": appState.zenModeEnabled,
})} })}
style={{
display: customOptions?.hideMainToolbar
? "none"
: undefined,
}}
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
@ -547,12 +564,14 @@ const LayerUI = ({
> >
{renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />} {renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
{renderFixedSideContainer()} {renderFixedSideContainer()}
{!customOptions?.hideFooter && (
<Footer <Footer
appState={appState} appState={appState}
actionManager={actionManager} actionManager={actionManager}
showExitZenModeBtn={showExitZenModeBtn} showExitZenModeBtn={showExitZenModeBtn}
renderWelcomeScreen={renderWelcomeScreen} renderWelcomeScreen={renderWelcomeScreen}
/> />
)}
{appState.scrolledOutside && ( {appState.scrolledOutside && (
<button <button
type="button" type="button"

View file

@ -1,7 +1,8 @@
import React, { useEffect } from "react"; import React, { useCallback, useContext, useEffect } from "react";
import { getFormValue } from "../actions/actionProperties"; import { getFormValue } from "../actions/actionProperties";
import { t } from "../i18n"; import { t } from "../i18n";
import { ExcalidrawPropsCustomOptionsContext } from "../types";
import "./Range.scss"; import "./Range.scss";
@ -40,9 +41,22 @@ export const Range = ({
} }
}, [value]); }, [value]);
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
return ( return (
<label className="control-label"> <label className="control-label">
{t("labels.opacity")} {t("labels.opacity")}
{customOptions?.pickerRenders?.rangeRender ? (
customOptions?.pickerRenders?.rangeRender({
value,
onChange: (value: number) => {
updateData(value);
},
step: 10,
min: 0,
max: 100,
})
) : (
<div className="range-wrapper"> <div className="range-wrapper">
<input <input
ref={rangeRef} ref={rangeRef}
@ -62,6 +76,7 @@ export const Range = ({
</div> </div>
<div className="zero-label">0</div> <div className="zero-label">0</div>
</div> </div>
)}
</label> </label>
); );
}; };

View file

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React, { useEffect, useImperativeHandle, useRef } from "react";
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common"; import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
@ -16,7 +16,8 @@ import "./css/app.scss";
import "./css/styles.scss"; import "./css/styles.scss";
import "./fonts/fonts.css"; import "./fonts/fonts.css";
import type { AppProps, ExcalidrawProps } from "./types"; import { ExcalidrawPropsCustomOptionsContext, type AppProps, type ExcalidrawProps } from "./types";
import type { ActionResult } from "./actions/types";
polyfill(); polyfill();
@ -53,6 +54,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
renderEmbeddable, renderEmbeddable,
aiEnabled, aiEnabled,
showDeprecatedFonts, showDeprecatedFonts,
customOptions,
actionRef,
} = props; } = props;
const canvasActions = props.UIOptions?.canvasActions; const canvasActions = props.UIOptions?.canvasActions;
@ -108,10 +111,23 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
}; };
}, []); }, []);
const appRef = useRef<App>(null);
useImperativeHandle(
actionRef,
() => ({
syncActionResult: (actionResult: ActionResult) => {
appRef.current?.syncActionResult(actionResult);
},
}),
[],
);
return ( return (
<ExcalidrawPropsCustomOptionsContext.Provider value={customOptions}>
<EditorJotaiProvider store={editorJotaiStore}> <EditorJotaiProvider store={editorJotaiStore}>
<InitializeApp langCode={langCode} theme={theme}> <InitializeApp langCode={langCode} theme={theme}>
<App <App
ref={appRef}
onChange={onChange} onChange={onChange}
initialData={initialData} initialData={initialData}
excalidrawAPI={excalidrawAPI} excalidrawAPI={excalidrawAPI}
@ -143,11 +159,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
renderEmbeddable={renderEmbeddable} renderEmbeddable={renderEmbeddable}
aiEnabled={aiEnabled !== false} aiEnabled={aiEnabled !== false}
showDeprecatedFonts={showDeprecatedFonts} showDeprecatedFonts={showDeprecatedFonts}
customOptions={customOptions}
> >
{children} {children}
</App> </App>
</InitializeApp> </InitializeApp>
</EditorJotaiProvider> </EditorJotaiProvider>
</ExcalidrawPropsCustomOptionsContext.Provider>
); );
}; };

View file

@ -129,7 +129,7 @@
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw", "homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
"scripts": { "scripts": {
"gen:types": "rm -rf types && tsc", "gen:types": "rimraf -rf types && tsc",
"build:esm": "rm -rf dist && node ../../scripts/buildPackage.js && yarn gen:types" "build:esm": "rimraf -rf dist && node ../../scripts/buildPackage.js && yarn gen:types"
} }
} }

View file

@ -7,7 +7,7 @@ import {
type GlobalPoint, type GlobalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { TOOL_TYPE, KEYS } from "@excalidraw/common"; import { TOOL_TYPE, KEYS, shouldSnapping } from "@excalidraw/common";
import { import {
getCommonBounds, getCommonBounds,
getDraggedElementsBounds, getDraggedElementsBounds,
@ -173,9 +173,9 @@ export const isSnappingEnabled = ({
}) => { }) => {
if (event) { if (event) {
return ( return (
(app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) || (app.state.objectsSnapModeEnabled && !shouldSnapping(event)) ||
(!app.state.objectsSnapModeEnabled && (!app.state.objectsSnapModeEnabled &&
event[KEYS.CTRL_OR_CMD] && shouldSnapping(event) &&
!isGridModeEnabled(app)) !isGridModeEnabled(app))
); );
} }

View file

@ -1,8 +1,12 @@
import { createContext, type JSX } from "react";
import type { import type {
IMAGE_MIME_TYPES, IMAGE_MIME_TYPES,
UserIdleState, UserIdleState,
throttleRAF, throttleRAF,
MIME_TYPES, MIME_TYPES,
ColorPaletteCustom,
ColorTuple,
} from "@excalidraw/common"; } from "@excalidraw/common";
import type { SuggestedBinding } from "@excalidraw/element/binding"; import type { SuggestedBinding } from "@excalidraw/element/binding";
@ -43,7 +47,9 @@ import type {
MakeBrand, MakeBrand,
} from "@excalidraw/common/utility-types"; } from "@excalidraw/common/utility-types";
import type { Action } from "./actions/types"; import type { ColorPickerType } from "./components/ColorPicker/colorPickerUtils";
import type { Action, ActionResult } from "./actions/types";
import type { Spreadsheet } from "./charts"; import type { Spreadsheet } from "./charts";
import type { ClipboardData } from "./clipboard"; import type { ClipboardData } from "./clipboard";
import type App from "./components/App"; import type App from "./components/App";
@ -57,7 +63,8 @@ import type { ImportedDataState } from "./data/types";
import type { Language } from "./i18n"; import type { Language } from "./i18n";
import type { isOverScrollBars } from "./scene/scrollbars"; import type { isOverScrollBars } from "./scene/scrollbars";
import type React from "react"; import type React from "react";
import type { JSX } from "react"; import type { ButtonIconSelectProps } from "./components/ButtonIconSelect";
import type { ButtonIcon } from "./components/ButtonIcon";
export type SocketId = string & { _brand: "SocketId" }; export type SocketId = string & { _brand: "SocketId" };
@ -512,6 +519,83 @@ export type OnUserFollowedPayload = {
action: "FOLLOW" | "UNFOLLOW"; action: "FOLLOW" | "UNFOLLOW";
}; };
export interface ExcalidrawPropsCustomOptions {
disableKeyEvents?: boolean;
hideMainToolbar?: boolean;
hideMenu?: boolean;
hideFooter?: boolean;
hideContextMenu?: boolean;
shouldResizeFromCenter?: (event: MouseEvent | KeyboardEvent) => boolean;
shouldMaintainAspectRatio?: (event: MouseEvent | KeyboardEvent) => boolean;
shouldRotateWithDiscreteAngle?: (
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
) => boolean;
shouldSnapping?: (event: KeyboardModifiersObject) => boolean;
layoutRenders?: {
menuRender?: (props: { children: React.ReactNode }) => React.ReactNode;
};
pickerRenders?: {
ButtonList?: React.ComponentType<{ children: React.ReactNode }>;
elementStrokeColors?: ColorTuple;
elementBackgroundColors?: ColorTuple;
buttonIconSelectRender?: <T extends Object>(
props: ButtonIconSelectProps<T>,
) => JSX.Element;
buttonIconSelectRadioRender?: (props: {
key: string;
active: boolean;
title: string;
name: string;
onChange: () => void;
checked: boolean;
dataTestid?: string;
children: React.ReactNode;
value: any;
}) => JSX.Element;
CustomButtonIcon?: typeof ButtonIcon;
layerButtonRender?: (props: {
onClick: () => void;
title: string;
children: React.ReactNode;
name: string;
}) => JSX.Element;
rangeRender?: (props: {
value: number;
onChange: (value: number) => void;
min: number;
max: number;
step: number;
}) => React.ReactNode;
colorPickerTopPickesButtonRender?: (props: {
active: boolean;
color: string;
isTransparent: boolean;
hasOutline: boolean;
onClick: () => void;
dataTestid: string;
children: React.ReactNode;
key: string;
}) => React.ReactNode;
colorPickerPopoverRender?: (props: {
color: string;
label: string;
type: ColorPickerType;
onChange: (color: string) => void;
elements: readonly ExcalidrawElement[];
palette: ColorPaletteCustom | null;
updateData: (formData?: any) => void;
}) => React.ReactNode;
};
}
export const ExcalidrawPropsCustomOptionsContext = createContext<
ExcalidrawPropsCustomOptions | undefined
>({});
export interface ExcalidrawActionType {
syncActionResult: (actionResult: ActionResult) => void;
}
export interface ExcalidrawProps { export interface ExcalidrawProps {
onChange?: ( onChange?: (
elements: readonly OrderedExcalidrawElement[], elements: readonly OrderedExcalidrawElement[],
@ -601,6 +685,8 @@ export interface ExcalidrawProps {
) => JSX.Element | null; ) => JSX.Element | null;
aiEnabled?: boolean; aiEnabled?: boolean;
showDeprecatedFonts?: boolean; showDeprecatedFonts?: boolean;
customOptions?: ExcalidrawPropsCustomOptions;
actionRef?: React.RefObject<ExcalidrawActionType | undefined>;
} }
export type SceneData = { export type SceneData = {

View file

@ -54,7 +54,7 @@
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
"scripts": { "scripts": {
"gen:types": "rm -rf types && tsc", "gen:types": "rimraf -rf types && tsc",
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types" "build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
} }
} }

View file

@ -69,7 +69,7 @@
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
"scripts": { "scripts": {
"gen:types": "rm -rf types && tsc", "gen:types": "rimraf -rf types && tsc",
"build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types" "build:esm": "rimraf -rf dist && node ../../scripts/buildUtils.js && yarn gen:types"
} }
} }