feat: Support custom elements in @excalidraw/excalidraw
This commit is contained in:
parent
2209e2c1e8
commit
39d0084a5e
@ -119,7 +119,11 @@ import {
|
||||
} from "../element/binding";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||
import {
|
||||
deepCopyElement,
|
||||
newCustomElement,
|
||||
newFreeDrawElement,
|
||||
} from "../element/newElement";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBindingElement,
|
||||
@ -327,6 +331,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||
contextMenuOpen: boolean = false;
|
||||
lastScenePointer: { x: number; y: number } | null = null;
|
||||
customElementName: string | null = null;
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
@ -378,6 +383,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
importLibrary: this.importLibraryFromUrl,
|
||||
setToastMessage: this.setToastMessage,
|
||||
id: this.id,
|
||||
setCustomType: this.setCustomType,
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@ -407,6 +413,48 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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() {
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
const {
|
||||
@ -530,6 +578,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
library={this.library}
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
renderCustomElementWidget={this.props.renderCustomElementWidget}
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
@ -1224,6 +1273,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageCache: this.imageCache,
|
||||
isExporting: false,
|
||||
renderScrollbars: !this.deviceType.isMobile,
|
||||
customElementsConfig: this.props.customElementsConfig,
|
||||
},
|
||||
);
|
||||
|
||||
@ -2986,6 +3036,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x,
|
||||
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") {
|
||||
this.handleFreeDrawElementOnPointerDown(
|
||||
event,
|
||||
|
@ -67,6 +67,7 @@ interface LayerUIProps {
|
||||
library: Library;
|
||||
id: string;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderCustomElementWidget?: (appState: AppState) => void;
|
||||
}
|
||||
|
||||
const LayerUI = ({
|
||||
@ -94,6 +95,7 @@ const LayerUI = ({
|
||||
library,
|
||||
id,
|
||||
onImageAction,
|
||||
renderCustomElementWidget,
|
||||
}: LayerUIProps) => {
|
||||
const deviceType = useDeviceType();
|
||||
|
||||
@ -437,6 +439,8 @@ const LayerUI = ({
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("eraser", { size: "small" })}
|
||||
{renderCustomElementWidget &&
|
||||
renderCustomElementWidget(appState)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -44,6 +44,7 @@ export const AllowedExcalidrawElementTypes: Record<
|
||||
arrow: true,
|
||||
freedraw: true,
|
||||
eraser: false,
|
||||
custom: true,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
@ -193,6 +194,8 @@ const restoreElement = (
|
||||
y,
|
||||
});
|
||||
}
|
||||
case "custom":
|
||||
return restoreElementWithProperties(element, { name: "custom" });
|
||||
// generic elements
|
||||
case "ellipse":
|
||||
return restoreElementWithProperties(element, {});
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawCustomElement,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
||||
@ -166,6 +167,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "ellipse":
|
||||
case "custom":
|
||||
const distance = distanceToBindableElement(args.element, args.point);
|
||||
return args.check(distance, args.threshold);
|
||||
case "freedraw": {
|
||||
@ -199,6 +201,7 @@ export const distanceToBindableElement = (
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "custom":
|
||||
return distanceToRectangle(element, point);
|
||||
case "diamond":
|
||||
return distanceToDiamond(element, point);
|
||||
@ -228,7 +231,8 @@ const distanceToRectangle = (
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement,
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawCustomElement,
|
||||
point: Point,
|
||||
): number => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||
@ -504,6 +508,7 @@ export const determineFocusDistance = (
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "custom":
|
||||
return c / (hwidth * (nabs + q * mabs));
|
||||
case "diamond":
|
||||
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||
@ -536,6 +541,7 @@ export const determineFocusPoint = (
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "custom":
|
||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||
break;
|
||||
case "ellipse":
|
||||
@ -586,6 +592,7 @@ const getSortedElementLineIntersections = (
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "custom":
|
||||
const corners = getCorners(element);
|
||||
intersections = corners
|
||||
.flatMap((point, i) => {
|
||||
@ -619,7 +626,8 @@ const getCorners = (
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement,
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawCustomElement,
|
||||
scale: number = 1,
|
||||
): GA.Point[] => {
|
||||
const hx = (scale * element.width) / 2;
|
||||
@ -628,6 +636,7 @@ const getCorners = (
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "custom":
|
||||
return [
|
||||
GA.point(hx, hy),
|
||||
GA.point(hx, -hy),
|
||||
@ -770,7 +779,8 @@ export const findFocusPointForRectangulars = (
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement,
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawCustomElement,
|
||||
// Between -1 and 1 for how far away should the focus point be relative
|
||||
// to the size of the element. Sign determines orientation.
|
||||
relativeDistance: number,
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
FontFamilyValues,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawCustomElement,
|
||||
} from "../element/types";
|
||||
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
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
|
||||
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||
//
|
||||
|
@ -83,6 +83,9 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||
scale: [number, number];
|
||||
}>;
|
||||
|
||||
export type ExcalidrawCustomElement = _ExcalidrawElementBase &
|
||||
Readonly<{ type: "custom"; name: string }>;
|
||||
|
||||
export type InitializedExcalidrawImageElement = MarkNonNullable<
|
||||
ExcalidrawImageElement,
|
||||
"fileId"
|
||||
@ -107,7 +110,8 @@ export type ExcalidrawElement =
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement;
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawCustomElement;
|
||||
|
||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||
isDeleted: boolean;
|
||||
@ -133,7 +137,8 @@ export type ExcalidrawBindableElement =
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawImageElement;
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawCustomElement;
|
||||
|
||||
export type ExcalidrawTextContainer =
|
||||
| ExcalidrawRectangleElement
|
||||
|
@ -12,6 +12,18 @@ import { MIME_TYPES } from "../../../constants";
|
||||
const { exportToCanvas, exportToSvg, exportToBlob } = window.Excalidraw;
|
||||
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 = () => {
|
||||
let resolve;
|
||||
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 (
|
||||
<div className="App">
|
||||
<h1> Excalidraw Example</h1>
|
||||
@ -220,7 +279,7 @@ export default function App() {
|
||||
onChange={(elements, state) =>
|
||||
console.info("Elements :", elements, "State : ", state)
|
||||
}
|
||||
onPointerUpdate={(payload) => console.info(payload)}
|
||||
//onPointerUpdate={(payload) => console.info(payload)}
|
||||
onCollabButtonClick={() =>
|
||||
window.alert("You clicked on collab button")
|
||||
}
|
||||
@ -233,6 +292,8 @@ export default function App() {
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderFooter={renderFooter}
|
||||
onLinkOpen={onLinkOpen}
|
||||
renderCustomElementWidget={renderCustomElementWidget}
|
||||
customElementsConfig={getCustomElementsConfig()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
.button-wrapper button {
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
max-width: 200px;
|
||||
max-width: 250px;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 800px;
|
||||
height: 600px;
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
@ -47,3 +47,8 @@
|
||||
--color-primary-darkest: #e64980;
|
||||
--color-primary-light: #fcc2d7;
|
||||
}
|
||||
|
||||
.custom-element {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
autoFocus = false,
|
||||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
renderCustomElementWidget,
|
||||
customElementsConfig,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
@ -98,6 +100,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
autoFocus={autoFocus}
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
renderCustomElementWidget={renderCustomElementWidget}
|
||||
customElementsConfig={customElementsConfig}
|
||||
/>
|
||||
</InitializeApp>
|
||||
);
|
||||
|
@ -250,6 +250,16 @@ const drawElementOnCanvas = (
|
||||
}
|
||||
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: {
|
||||
if (isTextElement(element)) {
|
||||
const rtl = isRTL(element.text);
|
||||
@ -779,7 +789,8 @@ export const renderElement = (
|
||||
case "line":
|
||||
case "arrow":
|
||||
case "image":
|
||||
case "text": {
|
||||
case "text":
|
||||
case "custom": {
|
||||
generateElementShape(element, generator);
|
||||
if (renderConfig.isExporting) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
@ -809,6 +820,7 @@ export const renderElement = (
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// @ts-ignore
|
||||
throw new Error(`Unimplemented type ${element.type}`);
|
||||
|
@ -190,7 +190,6 @@ export const renderScene = (
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false };
|
||||
}
|
||||
|
||||
const {
|
||||
renderScrollbars = true,
|
||||
renderSelection = true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ExcalidrawTextElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { AppClassProperties, AppState, ExcalidrawProps } from "../types";
|
||||
|
||||
export type RenderConfig = {
|
||||
// AppState values
|
||||
@ -27,6 +27,7 @@ export type RenderConfig = {
|
||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||
CSS filters), and we disable render optimizations for best output */
|
||||
isExporting: boolean;
|
||||
customElementsConfig?: ExcalidrawProps["customElementsConfig"];
|
||||
};
|
||||
|
||||
export type SceneScroll = {
|
||||
|
14
src/types.ts
14
src/types.ts
@ -77,7 +77,7 @@ export type AppState = {
|
||||
// (e.g. text element when typing into the input)
|
||||
editingElement: NonDeletedExcalidrawElement | null;
|
||||
editingLinearElement: LinearElementEditor | null;
|
||||
elementType: typeof SHAPES[number]["value"] | "eraser";
|
||||
elementType: typeof SHAPES[number]["value"] | "eraser" | "custom";
|
||||
elementLocked: boolean;
|
||||
penMode: boolean;
|
||||
penDetected: boolean;
|
||||
@ -206,6 +206,15 @@ export type ExcalidrawAPIRefValue =
|
||||
ready?: false;
|
||||
};
|
||||
|
||||
type CustomElementConfig = {
|
||||
type: "custom";
|
||||
name: string;
|
||||
resize?: boolean;
|
||||
rotate?: boolean;
|
||||
svg: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
export interface ExcalidrawProps {
|
||||
onChange?: (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -253,6 +262,8 @@ export interface ExcalidrawProps {
|
||||
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
|
||||
}>,
|
||||
) => void;
|
||||
renderCustomElementWidget?: (appState: AppState) => void;
|
||||
customElementsConfig?: CustomElementConfig[];
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
@ -412,6 +423,7 @@ export type ExcalidrawImperativeAPI = {
|
||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
||||
ready: true;
|
||||
id: string;
|
||||
setCustomType: InstanceType<typeof App>["setCustomType"];
|
||||
};
|
||||
|
||||
export type DeviceType = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user