feat: Support custom elements in @excalidraw/excalidraw

This commit is contained in:
ad1992 2022-03-23 19:04:00 +05:30
parent 2209e2c1e8
commit 39d0084a5e
13 changed files with 202 additions and 13 deletions

View File

@ -119,7 +119,11 @@ import {
} from "../element/binding"; } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
import { deepCopyElement, newFreeDrawElement } from "../element/newElement"; import {
deepCopyElement,
newCustomElement,
newFreeDrawElement,
} from "../element/newElement";
import { import {
hasBoundTextElement, hasBoundTextElement,
isBindingElement, isBindingElement,
@ -327,6 +331,7 @@ class App extends React.Component<AppProps, AppState> {
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null; lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
contextMenuOpen: boolean = false; contextMenuOpen: boolean = false;
lastScenePointer: { x: number; y: number } | null = null; lastScenePointer: { x: number; y: number } | null = null;
customElementName: string | null = null;
constructor(props: AppProps) { constructor(props: AppProps) {
super(props); super(props);
@ -378,6 +383,7 @@ class App extends React.Component<AppProps, AppState> {
importLibrary: this.importLibraryFromUrl, importLibrary: this.importLibraryFromUrl,
setToastMessage: this.setToastMessage, setToastMessage: this.setToastMessage,
id: this.id, id: this.id,
setCustomType: this.setCustomType,
} as const; } as const;
if (typeof excalidrawRef === "function") { if (typeof excalidrawRef === "function") {
excalidrawRef(api); excalidrawRef(api);
@ -407,6 +413,48 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.registerAction(createRedoAction(this.history)); this.actionManager.registerAction(createRedoAction(this.history));
} }
setCustomType = (name: string) => {
this.setState({ elementType: "custom" });
this.customElementName = name;
};
renderCustomElement = (
coords: { x: number; y: number },
name: string = "",
) => {
const config = this.props.customElementsConfig!.find(
(config) => config.name === name,
)!;
const [gridX, gridY] = getGridPoint(
coords.x,
coords.y,
this.state.gridSize,
);
const width = config.width || 40;
const height = config.height || 40;
const customElement = newCustomElement(name, {
type: "custom",
x: gridX - width / 2,
y: gridY - height / 2,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
width,
height,
});
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
customElement,
]);
};
private renderCanvas() { private renderCanvas() {
const canvasScale = window.devicePixelRatio; const canvasScale = window.devicePixelRatio;
const { const {
@ -530,6 +578,7 @@ class App extends React.Component<AppProps, AppState> {
library={this.library} library={this.library}
id={this.id} id={this.id}
onImageAction={this.onImageAction} onImageAction={this.onImageAction}
renderCustomElementWidget={this.props.renderCustomElementWidget}
/> />
<div className="excalidraw-textEditorContainer" /> <div className="excalidraw-textEditorContainer" />
<div className="excalidraw-contextMenuContainer" /> <div className="excalidraw-contextMenuContainer" />
@ -1224,6 +1273,7 @@ class App extends React.Component<AppProps, AppState> {
imageCache: this.imageCache, imageCache: this.imageCache,
isExporting: false, isExporting: false,
renderScrollbars: !this.deviceType.isMobile, renderScrollbars: !this.deviceType.isMobile,
customElementsConfig: this.props.customElementsConfig,
}, },
); );
@ -2986,6 +3036,17 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
}); });
} else if (this.state.elementType === "custom") {
if (this.customElementName) {
setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR);
this.renderCustomElement(
{
x: pointerDownState.origin.x,
y: pointerDownState.origin.y,
},
this.customElementName,
);
}
} else if (this.state.elementType === "freedraw") { } else if (this.state.elementType === "freedraw") {
this.handleFreeDrawElementOnPointerDown( this.handleFreeDrawElementOnPointerDown(
event, event,

View File

@ -67,6 +67,7 @@ interface LayerUIProps {
library: Library; library: Library;
id: string; id: string;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderCustomElementWidget?: (appState: AppState) => void;
} }
const LayerUI = ({ const LayerUI = ({
@ -94,6 +95,7 @@ const LayerUI = ({
library, library,
id, id,
onImageAction, onImageAction,
renderCustomElementWidget,
}: LayerUIProps) => { }: LayerUIProps) => {
const deviceType = useDeviceType(); const deviceType = useDeviceType();
@ -437,6 +439,8 @@ const LayerUI = ({
})} })}
> >
{actionManager.renderAction("eraser", { size: "small" })} {actionManager.renderAction("eraser", { size: "small" })}
{renderCustomElementWidget &&
renderCustomElementWidget(appState)}
</div> </div>
</> </>
)} )}

View File

@ -44,6 +44,7 @@ export const AllowedExcalidrawElementTypes: Record<
arrow: true, arrow: true,
freedraw: true, freedraw: true,
eraser: false, eraser: false,
custom: true,
}; };
export type RestoredDataState = { export type RestoredDataState = {
@ -193,6 +194,8 @@ const restoreElement = (
y, y,
}); });
} }
case "custom":
return restoreElementWithProperties(element, { name: "custom" });
// generic elements // generic elements
case "ellipse": case "ellipse":
return restoreElementWithProperties(element, {}); return restoreElementWithProperties(element, {});

View File

@ -25,6 +25,7 @@ import {
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawCustomElement,
} from "./types"; } from "./types";
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds"; import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
@ -166,6 +167,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
case "text": case "text":
case "diamond": case "diamond":
case "ellipse": case "ellipse":
case "custom":
const distance = distanceToBindableElement(args.element, args.point); const distance = distanceToBindableElement(args.element, args.point);
return args.check(distance, args.threshold); return args.check(distance, args.threshold);
case "freedraw": { case "freedraw": {
@ -199,6 +201,7 @@ export const distanceToBindableElement = (
case "rectangle": case "rectangle":
case "image": case "image":
case "text": case "text":
case "custom":
return distanceToRectangle(element, point); return distanceToRectangle(element, point);
case "diamond": case "diamond":
return distanceToDiamond(element, point); return distanceToDiamond(element, point);
@ -228,7 +231,8 @@ const distanceToRectangle = (
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawFreeDrawElement | ExcalidrawFreeDrawElement
| ExcalidrawImageElement, | ExcalidrawImageElement
| ExcalidrawCustomElement,
point: Point, point: Point,
): number => { ): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
@ -504,6 +508,7 @@ export const determineFocusDistance = (
case "rectangle": case "rectangle":
case "image": case "image":
case "text": case "text":
case "custom":
return c / (hwidth * (nabs + q * mabs)); return c / (hwidth * (nabs + q * mabs));
case "diamond": case "diamond":
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight); return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
@ -536,6 +541,7 @@ export const determineFocusPoint = (
case "image": case "image":
case "text": case "text":
case "diamond": case "diamond":
case "custom":
point = findFocusPointForRectangulars(element, focus, adjecentPointRel); point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
break; break;
case "ellipse": case "ellipse":
@ -586,6 +592,7 @@ const getSortedElementLineIntersections = (
case "image": case "image":
case "text": case "text":
case "diamond": case "diamond":
case "custom":
const corners = getCorners(element); const corners = getCorners(element);
intersections = corners intersections = corners
.flatMap((point, i) => { .flatMap((point, i) => {
@ -619,7 +626,8 @@ const getCorners = (
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
| ExcalidrawImageElement | ExcalidrawImageElement
| ExcalidrawDiamondElement | ExcalidrawDiamondElement
| ExcalidrawTextElement, | ExcalidrawTextElement
| ExcalidrawCustomElement,
scale: number = 1, scale: number = 1,
): GA.Point[] => { ): GA.Point[] => {
const hx = (scale * element.width) / 2; const hx = (scale * element.width) / 2;
@ -628,6 +636,7 @@ const getCorners = (
case "rectangle": case "rectangle":
case "image": case "image":
case "text": case "text":
case "custom":
return [ return [
GA.point(hx, hy), GA.point(hx, hy),
GA.point(hx, -hy), GA.point(hx, -hy),
@ -770,7 +779,8 @@ export const findFocusPointForRectangulars = (
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
| ExcalidrawImageElement | ExcalidrawImageElement
| ExcalidrawDiamondElement | ExcalidrawDiamondElement
| ExcalidrawTextElement, | ExcalidrawTextElement
| ExcalidrawCustomElement,
// Between -1 and 1 for how far away should the focus point be relative // Between -1 and 1 for how far away should the focus point be relative
// to the size of the element. Sign determines orientation. // to the size of the element. Sign determines orientation.
relativeDistance: number, relativeDistance: number,

View File

@ -12,6 +12,7 @@ import {
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
FontFamilyValues, FontFamilyValues,
ExcalidrawRectangleElement, ExcalidrawRectangleElement,
ExcalidrawCustomElement,
} from "../element/types"; } from "../element/types";
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils"; import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
@ -318,6 +319,17 @@ export const newImageElement = (
}; };
}; };
export const newCustomElement = (
name: string,
opts: {
type: ExcalidrawCustomElement["type"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawCustomElement> => {
return {
..._newElementBase<ExcalidrawCustomElement>("custom", opts),
name,
};
};
// Simplified deep clone for the purpose of cloning ExcalidrawElement only // Simplified deep clone for the purpose of cloning ExcalidrawElement only
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.) // (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
// //

View File

@ -83,6 +83,9 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
scale: [number, number]; scale: [number, number];
}>; }>;
export type ExcalidrawCustomElement = _ExcalidrawElementBase &
Readonly<{ type: "custom"; name: string }>;
export type InitializedExcalidrawImageElement = MarkNonNullable< export type InitializedExcalidrawImageElement = MarkNonNullable<
ExcalidrawImageElement, ExcalidrawImageElement,
"fileId" "fileId"
@ -107,7 +110,8 @@ export type ExcalidrawElement =
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawLinearElement | ExcalidrawLinearElement
| ExcalidrawFreeDrawElement | ExcalidrawFreeDrawElement
| ExcalidrawImageElement; | ExcalidrawImageElement
| ExcalidrawCustomElement;
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & { export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
isDeleted: boolean; isDeleted: boolean;
@ -133,7 +137,8 @@ export type ExcalidrawBindableElement =
| ExcalidrawDiamondElement | ExcalidrawDiamondElement
| ExcalidrawEllipseElement | ExcalidrawEllipseElement
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawImageElement; | ExcalidrawImageElement
| ExcalidrawCustomElement;
export type ExcalidrawTextContainer = export type ExcalidrawTextContainer =
| ExcalidrawRectangleElement | ExcalidrawRectangleElement

View File

@ -12,6 +12,18 @@ import { MIME_TYPES } from "../../../constants";
const { exportToCanvas, exportToSvg, exportToBlob } = window.Excalidraw; const { exportToCanvas, exportToSvg, exportToBlob } = window.Excalidraw;
const Excalidraw = window.Excalidraw.default; const Excalidraw = window.Excalidraw.default;
const STAR_SVG = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M287.9 0C297.1 0 305.5 5.25 309.5 13.52L378.1 154.8L531.4 177.5C540.4 178.8 547.8 185.1 550.7 193.7C553.5 202.4 551.2 211.9 544.8 218.2L433.6 328.4L459.9 483.9C461.4 492.9 457.7 502.1 450.2 507.4C442.8 512.7 432.1 513.4 424.9 509.1L287.9 435.9L150.1 509.1C142.9 513.4 133.1 512.7 125.6 507.4C118.2 502.1 114.5 492.9 115.1 483.9L142.2 328.4L31.11 218.2C24.65 211.9 22.36 202.4 25.2 193.7C28.03 185.1 35.5 178.8 44.49 177.5L197.7 154.8L266.3 13.52C270.4 5.249 278.7 0 287.9 0L287.9 0zM287.9 78.95L235.4 187.2C231.9 194.3 225.1 199.3 217.3 200.5L98.98 217.9L184.9 303C190.4 308.5 192.9 316.4 191.6 324.1L171.4 443.7L276.6 387.5C283.7 383.7 292.2 383.7 299.2 387.5L404.4 443.7L384.2 324.1C382.9 316.4 385.5 308.5 391 303L476.9 217.9L358.6 200.5C350.7 199.3 343.9 194.3 340.5 187.2L287.9 78.95z" />
</svg>
);
const COMMENT_SVG = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M256 32C114.6 32 .0272 125.1 .0272 240c0 47.63 19.91 91.25 52.91 126.2c-14.88 39.5-45.87 72.88-46.37 73.25c-6.625 7-8.375 17.25-4.625 26C5.818 474.2 14.38 480 24 480c61.5 0 109.1-25.75 139.1-46.25C191.1 442.8 223.3 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32zM256.1 400c-26.75 0-53.12-4.125-78.38-12.12l-22.75-7.125l-19.5 13.75c-14.25 10.12-33.88 21.38-57.5 29c7.375-12.12 14.37-25.75 19.88-40.25l10.62-28l-20.62-21.87C69.82 314.1 48.07 282.2 48.07 240c0-88.25 93.25-160 208-160s208 71.75 208 160S370.8 400 256.1 400z" />
</svg>
);
const resolvablePromise = () => { const resolvablePromise = () => {
let resolve; let resolve;
let reject; let reject;
@ -140,6 +152,53 @@ export default function App() {
} }
}, []); }, []);
const renderCustomElementWidget = () => {
return (
<>
<button
className="custom-element"
onClick={() => {
excalidrawRef.current.setCustomType("star");
}}
>
{STAR_SVG}
</button>
<button
className="custom-element"
onClick={() => {
excalidrawRef.current.setCustomType("comment");
}}
>
{COMMENT_SVG}
</button>
</>
);
};
const getCustomElementsConfig = () => {
return [
{
type: "custom",
name: "star",
svg: `data:${
MIME_TYPES.svg
}, ${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M287.9 0C297.1 0 305.5 5.25 309.5 13.52L378.1 154.8L531.4 177.5C540.4 178.8 547.8 185.1 550.7 193.7C553.5 202.4 551.2 211.9 544.8 218.2L433.6 328.4L459.9 483.9C461.4 492.9 457.7 502.1 450.2 507.4C442.8 512.7 432.1 513.4 424.9 509.1L287.9 435.9L150.1 509.1C142.9 513.4 133.1 512.7 125.6 507.4C118.2 502.1 114.5 492.9 115.1 483.9L142.2 328.4L31.11 218.2C24.65 211.9 22.36 202.4 25.2 193.7C28.03 185.1 35.5 178.8 44.49 177.5L197.7 154.8L266.3 13.52C270.4 5.249 278.7 0 287.9 0L287.9 0zM287.9 78.95L235.4 187.2C231.9 194.3 225.1 199.3 217.3 200.5L98.98 217.9L184.9 303C190.4 308.5 192.9 316.4 191.6 324.1L171.4 443.7L276.6 387.5C283.7 383.7 292.2 383.7 299.2 387.5L404.4 443.7L384.2 324.1C382.9 316.4 385.5 308.5 391 303L476.9 217.9L358.6 200.5C350.7 199.3 343.9 194.3 340.5 187.2L287.9 78.95z" />
</svg>`)}`,
width: 60,
height: 60,
},
{
type: "custom",
name: "comment",
svg: `data:${
MIME_TYPES.svg
}, ${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M256 32C114.6 32 .0272 125.1 .0272 240c0 47.63 19.91 91.25 52.91 126.2c-14.88 39.5-45.87 72.88-46.37 73.25c-6.625 7-8.375 17.25-4.625 26C5.818 474.2 14.38 480 24 480c61.5 0 109.1-25.75 139.1-46.25C191.1 442.8 223.3 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32zM256.1 400c-26.75 0-53.12-4.125-78.38-12.12l-22.75-7.125l-19.5 13.75c-14.25 10.12-33.88 21.38-57.5 29c7.375-12.12 14.37-25.75 19.88-40.25l10.62-28l-20.62-21.87C69.82 314.1 48.07 282.2 48.07 240c0-88.25 93.25-160 208-160s208 71.75 208 160S370.8 400 256.1 400z" />
</svg>`)}`,
},
];
};
return ( return (
<div className="App"> <div className="App">
<h1> Excalidraw Example</h1> <h1> Excalidraw Example</h1>
@ -220,7 +279,7 @@ export default function App() {
onChange={(elements, state) => onChange={(elements, state) =>
console.info("Elements :", elements, "State : ", state) console.info("Elements :", elements, "State : ", state)
} }
onPointerUpdate={(payload) => console.info(payload)} //onPointerUpdate={(payload) => console.info(payload)}
onCollabButtonClick={() => onCollabButtonClick={() =>
window.alert("You clicked on collab button") window.alert("You clicked on collab button")
} }
@ -233,6 +292,8 @@ export default function App() {
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderFooter={renderFooter} renderFooter={renderFooter}
onLinkOpen={onLinkOpen} onLinkOpen={onLinkOpen}
renderCustomElementWidget={renderCustomElementWidget}
customElementsConfig={getCustomElementsConfig()}
/> />
</div> </div>

View File

@ -6,7 +6,7 @@
.button-wrapper button { .button-wrapper button {
z-index: 1; z-index: 1;
height: 40px; height: 40px;
max-width: 200px; max-width: 250px;
margin: 10px; margin: 10px;
padding: 5px; padding: 5px;
} }
@ -16,7 +16,7 @@
} }
.excalidraw-wrapper { .excalidraw-wrapper {
height: 800px; height: 600px;
margin: 50px; margin: 50px;
} }
@ -47,3 +47,8 @@
--color-primary-darkest: #e64980; --color-primary-darkest: #e64980;
--color-primary-light: #fcc2d7; --color-primary-light: #fcc2d7;
} }
.custom-element {
width: 2rem;
height: 2rem;
}

View File

@ -36,6 +36,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
autoFocus = false, autoFocus = false,
generateIdForFile, generateIdForFile,
onLinkOpen, onLinkOpen,
renderCustomElementWidget,
customElementsConfig,
} = props; } = props;
const canvasActions = props.UIOptions?.canvasActions; const canvasActions = props.UIOptions?.canvasActions;
@ -98,6 +100,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
autoFocus={autoFocus} autoFocus={autoFocus}
generateIdForFile={generateIdForFile} generateIdForFile={generateIdForFile}
onLinkOpen={onLinkOpen} onLinkOpen={onLinkOpen}
renderCustomElementWidget={renderCustomElementWidget}
customElementsConfig={customElementsConfig}
/> />
</InitializeApp> </InitializeApp>
); );

View File

@ -250,6 +250,16 @@ const drawElementOnCanvas = (
} }
break; break;
} }
case "custom": {
const config = renderConfig.customElementsConfig?.find(
(config) => config.name === element.name,
);
const img = document.createElement("img");
img.src = config!.svg;
context.drawImage(img, 0, 0, element.width, element.height);
break;
}
default: { default: {
if (isTextElement(element)) { if (isTextElement(element)) {
const rtl = isRTL(element.text); const rtl = isRTL(element.text);
@ -779,7 +789,8 @@ export const renderElement = (
case "line": case "line":
case "arrow": case "arrow":
case "image": case "image":
case "text": { case "text":
case "custom": {
generateElementShape(element, generator); generateElementShape(element, generator);
if (renderConfig.isExporting) { if (renderConfig.isExporting) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
@ -809,6 +820,7 @@ export const renderElement = (
} }
break; break;
} }
default: { default: {
// @ts-ignore // @ts-ignore
throw new Error(`Unimplemented type ${element.type}`); throw new Error(`Unimplemented type ${element.type}`);

View File

@ -190,7 +190,6 @@ export const renderScene = (
if (canvas === null) { if (canvas === null) {
return { atLeastOneVisibleElement: false }; return { atLeastOneVisibleElement: false };
} }
const { const {
renderScrollbars = true, renderScrollbars = true,
renderSelection = true, renderSelection = true,

View File

@ -1,5 +1,5 @@
import { ExcalidrawTextElement } from "../element/types"; import { ExcalidrawTextElement } from "../element/types";
import { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState, ExcalidrawProps } from "../types";
export type RenderConfig = { export type RenderConfig = {
// AppState values // AppState values
@ -27,6 +27,7 @@ export type RenderConfig = {
/** when exporting the behavior is slightly different (e.g. we can't use /** when exporting the behavior is slightly different (e.g. we can't use
CSS filters), and we disable render optimizations for best output */ CSS filters), and we disable render optimizations for best output */
isExporting: boolean; isExporting: boolean;
customElementsConfig?: ExcalidrawProps["customElementsConfig"];
}; };
export type SceneScroll = { export type SceneScroll = {

View File

@ -77,7 +77,7 @@ export type AppState = {
// (e.g. text element when typing into the input) // (e.g. text element when typing into the input)
editingElement: NonDeletedExcalidrawElement | null; editingElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null; editingLinearElement: LinearElementEditor | null;
elementType: typeof SHAPES[number]["value"] | "eraser"; elementType: typeof SHAPES[number]["value"] | "eraser" | "custom";
elementLocked: boolean; elementLocked: boolean;
penMode: boolean; penMode: boolean;
penDetected: boolean; penDetected: boolean;
@ -206,6 +206,15 @@ export type ExcalidrawAPIRefValue =
ready?: false; ready?: false;
}; };
type CustomElementConfig = {
type: "custom";
name: string;
resize?: boolean;
rotate?: boolean;
svg: string;
width?: number;
height?: number;
};
export interface ExcalidrawProps { export interface ExcalidrawProps {
onChange?: ( onChange?: (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@ -253,6 +262,8 @@ export interface ExcalidrawProps {
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>; nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
}>, }>,
) => void; ) => void;
renderCustomElementWidget?: (appState: AppState) => void;
customElementsConfig?: CustomElementConfig[];
} }
export type SceneData = { export type SceneData = {
@ -412,6 +423,7 @@ export type ExcalidrawImperativeAPI = {
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>; readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true; ready: true;
id: string; id: string;
setCustomType: InstanceType<typeof App>["setCustomType"];
}; };
export type DeviceType = { export type DeviceType = {