feat: Support custom elements in @excalidraw/excalidraw
This commit is contained in:
parent
2209e2c1e8
commit
39d0084a5e
@ -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,
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -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, {});
|
||||||
|
@ -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,
|
||||||
|
@ -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.)
|
||||||
//
|
//
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}`);
|
||||||
|
@ -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,
|
||||||
|
@ -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 = {
|
||||||
|
14
src/types.ts
14
src/types.ts
@ -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 = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user