Compare commits
28 Commits
master
...
aakansha-c
Author | SHA1 | Date | |
---|---|---|---|
|
c4b951a0c5 | ||
|
c93d8f4bd0 | ||
|
645f9a5dc0 | ||
|
128b7741c1 | ||
|
1edde7291c | ||
|
1ca56204b1 | ||
|
f3ae7a8506 | ||
|
5f57daa132 | ||
|
db9c9eb3d2 | ||
|
2e8c4d25f2 | ||
|
4953828d86 | ||
|
6eb0cf6a10 | ||
|
ba48aa24a0 | ||
|
4e75f10b2c | ||
|
d2d3599661 | ||
|
ed3eda3401 | ||
|
d27b32dd2c | ||
|
2337842f57 | ||
|
5b78f50fe3 | ||
|
a4a95a591a | ||
|
3d459076fb | ||
|
14a23c6c50 | ||
|
5f4a5b1789 | ||
|
47498796e0 | ||
|
8706277d14 | ||
|
3d0a1106ff | ||
|
61699ff3c2 | ||
|
39d0084a5e |
@ -304,21 +304,42 @@ export const actionErase = register({
|
|||||||
name: "eraser",
|
name: "eraser",
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
|
const activeTool: any = { ...appState.activeTool };
|
||||||
|
|
||||||
|
if (appState.activeTool.type !== "eraser") {
|
||||||
|
if (appState.activeTool.type === "custom") {
|
||||||
|
activeTool.lastActiveToolBeforeEraser = {
|
||||||
|
type: "custom",
|
||||||
|
customType: appState.activeTool.customType,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
activeTool.lastActiveToolBeforeEraser = appState.activeTool.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isEraserActive(appState)) {
|
||||||
|
if (appState.activeTool.lastActiveToolBeforeEraser) {
|
||||||
|
if (
|
||||||
|
typeof appState.activeTool.lastActiveToolBeforeEraser === "object" &&
|
||||||
|
appState.activeTool.lastActiveToolBeforeEraser?.type === "custom"
|
||||||
|
) {
|
||||||
|
activeTool.type = "custom";
|
||||||
|
activeTool.customType =
|
||||||
|
appState.activeTool.lastActiveToolBeforeEraser.customType;
|
||||||
|
} else {
|
||||||
|
activeTool.type = appState.activeTool.lastActiveToolBeforeEraser;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeTool.type = "selection";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeTool.type = "eraser";
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
activeTool: {
|
activeTool,
|
||||||
...appState.activeTool,
|
|
||||||
type: isEraserActive(appState)
|
|
||||||
? appState.activeTool.lastActiveToolBeforeEraser ?? "selection"
|
|
||||||
: "eraser",
|
|
||||||
lastActiveToolBeforeEraser:
|
|
||||||
appState.activeTool.type === "eraser" //node throws incorrect type error when using isEraserActive()
|
|
||||||
? null
|
|
||||||
: appState.activeTool.type,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
@ -136,7 +136,21 @@ export const actionFinalize = register({
|
|||||||
) {
|
) {
|
||||||
resetCursor(canvas);
|
resetCursor(canvas);
|
||||||
}
|
}
|
||||||
|
const activeTool: any = { ...appState.activeTool };
|
||||||
|
if (appState.activeTool.lastActiveToolBeforeEraser) {
|
||||||
|
if (
|
||||||
|
typeof appState.activeTool.lastActiveToolBeforeEraser === "object" &&
|
||||||
|
appState.activeTool.lastActiveToolBeforeEraser.type === "custom"
|
||||||
|
) {
|
||||||
|
activeTool.type = appState.activeTool.lastActiveToolBeforeEraser.type;
|
||||||
|
activeTool.customType =
|
||||||
|
appState.activeTool.lastActiveToolBeforeEraser.customType;
|
||||||
|
} else {
|
||||||
|
activeTool.type = appState.activeTool.lastActiveToolBeforeEraser;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeTool.type = "selection";
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
appState: {
|
appState: {
|
||||||
@ -147,14 +161,7 @@ export const actionFinalize = register({
|
|||||||
appState.activeTool.type === "freedraw") &&
|
appState.activeTool.type === "freedraw") &&
|
||||||
multiPointElement
|
multiPointElement
|
||||||
? appState.activeTool
|
? appState.activeTool
|
||||||
: {
|
: activeTool,
|
||||||
...appState.activeTool,
|
|
||||||
type:
|
|
||||||
appState.activeTool.type === "eraser" &&
|
|
||||||
appState.activeTool.lastActiveToolBeforeEraser
|
|
||||||
? appState.activeTool.lastActiveToolBeforeEraser
|
|
||||||
: "selection",
|
|
||||||
},
|
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
|
@ -119,12 +119,17 @@ 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,
|
||||||
isBindingElementType,
|
isBindingElementType,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
|
isCustomElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@ -380,6 +385,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);
|
||||||
@ -394,7 +400,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene(this);
|
||||||
this.library = new Library(this);
|
this.library = new Library(this);
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
this.actionManager = new ActionManager(
|
this.actionManager = new ActionManager(
|
||||||
@ -409,6 +415,59 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.actionManager.registerAction(createRedoAction(this.history));
|
this.actionManager.registerAction(createRedoAction(this.history));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomType = (customType: string) => {
|
||||||
|
this.setState({
|
||||||
|
activeTool: { ...this.state.activeTool, type: "custom", customType },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderCustomElement = (coords: { x: number; y: number }) => {
|
||||||
|
if (this.state.activeTool.type !== "custom") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config =
|
||||||
|
this.props.customElementsConfig?.[this.state.activeTool.customType];
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
coords.x,
|
||||||
|
coords.y,
|
||||||
|
this.state.gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
const width = config.width || 40;
|
||||||
|
const height = config.height || 40;
|
||||||
|
const customElement = newCustomElement(this.state.activeTool.customType, {
|
||||||
|
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,
|
||||||
|
locked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.replaceAllElements([
|
||||||
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
|
customElement,
|
||||||
|
]);
|
||||||
|
const customElementConfig =
|
||||||
|
this.props.customElementsConfig?.[customElement.customType];
|
||||||
|
|
||||||
|
if (customElementConfig && customElementConfig.onCreate) {
|
||||||
|
customElementConfig.onCreate(customElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private renderCanvas() {
|
private renderCanvas() {
|
||||||
const canvasScale = window.devicePixelRatio;
|
const canvasScale = window.devicePixelRatio;
|
||||||
const {
|
const {
|
||||||
@ -532,6 +591,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" />
|
||||||
@ -1234,6 +1294,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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1591,14 +1652,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
|
const activeTool: any = {
|
||||||
|
...prevState.activeTool,
|
||||||
|
locked: !prevState.activeTool.locked,
|
||||||
|
type: prevState.activeTool.locked
|
||||||
|
? "selection"
|
||||||
|
: prevState.activeTool.type,
|
||||||
|
};
|
||||||
|
if (prevState.activeTool.type === "custom") {
|
||||||
|
activeTool.customType = prevState.activeTool.customType;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
activeTool: {
|
activeTool,
|
||||||
...prevState.activeTool,
|
|
||||||
locked: !prevState.activeTool.locked,
|
|
||||||
type: prevState.activeTool.locked
|
|
||||||
? "selection"
|
|
||||||
: prevState.activeTool.type,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -2829,6 +2894,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
)) &&
|
)) &&
|
||||||
!hitElement?.locked
|
!hitElement?.locked
|
||||||
) {
|
) {
|
||||||
|
if (hitElement && isCustomElement(hitElement)) {
|
||||||
|
const config =
|
||||||
|
this.props.customElementsConfig?.[hitElement.customType];
|
||||||
|
|
||||||
|
if (!config?.transformHandles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||||
@ -3051,6 +3124,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
|
} else if (this.state.activeTool.type === "custom") {
|
||||||
|
setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR);
|
||||||
|
this.renderCustomElement({
|
||||||
|
x: pointerDownState.origin.x,
|
||||||
|
y: pointerDownState.origin.y,
|
||||||
|
});
|
||||||
} else if (this.state.activeTool.type === "freedraw") {
|
} else if (this.state.activeTool.type === "freedraw") {
|
||||||
this.handleFreeDrawElementOnPointerDown(
|
this.handleFreeDrawElementOnPointerDown(
|
||||||
event,
|
event,
|
||||||
@ -3091,15 +3170,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
this.lastPointerUp = event;
|
this.lastPointerUp = event;
|
||||||
if (this.deviceType.isTouchScreen) {
|
let hitElement;
|
||||||
|
if (this.deviceType.isTouchScreen || this.props.onElementClick) {
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
{ clientX: event.clientX, clientY: event.clientY },
|
{ clientX: event.clientX, clientY: event.clientY },
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const hitElement = this.getElementAtPosition(
|
hitElement = this.getElementAtPosition(scenePointer.x, scenePointer.y);
|
||||||
scenePointer.x,
|
|
||||||
scenePointer.y,
|
|
||||||
);
|
|
||||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||||
scenePointer,
|
scenePointer,
|
||||||
hitElement,
|
hitElement,
|
||||||
@ -3112,6 +3189,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.redirectToLink(event, this.deviceType.isTouchScreen);
|
this.redirectToLink(event, this.deviceType.isTouchScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.button !== POINTER_BUTTON.SECONDARY &&
|
||||||
|
this.state.activeTool.type === "selection" &&
|
||||||
|
this.props.onElementClick &&
|
||||||
|
hitElement
|
||||||
|
) {
|
||||||
|
const threshold = 5;
|
||||||
|
const isSinglePointClick =
|
||||||
|
distance2d(
|
||||||
|
this.lastPointerDown!.clientX,
|
||||||
|
this.lastPointerDown!.clientY,
|
||||||
|
this.lastPointerUp!.clientX,
|
||||||
|
this.lastPointerUp!.clientY,
|
||||||
|
) <= threshold;
|
||||||
|
if (isSinglePointClick) {
|
||||||
|
this.props.onElementClick(hitElement, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.removePointer(event);
|
this.removePointer(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -4220,6 +4315,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elementsWithinSelection = getElementsWithinSelection(
|
const elementsWithinSelection = getElementsWithinSelection(
|
||||||
elements,
|
elements,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
|
this.props.customElementsConfig,
|
||||||
);
|
);
|
||||||
this.setState((prevState) =>
|
this.setState((prevState) =>
|
||||||
selectGroupsForSelectedElements(
|
selectGroupsForSelectedElements(
|
||||||
@ -4513,6 +4609,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// Code below handles selection when element(s) weren't
|
// Code below handles selection when element(s) weren't
|
||||||
// drag or added to selection on pointer down phase.
|
// drag or added to selection on pointer down phase.
|
||||||
const hitElement = pointerDownState.hit.element;
|
const hitElement = pointerDownState.hit.element;
|
||||||
|
|
||||||
if (isEraserActive(this.state)) {
|
if (isEraserActive(this.state)) {
|
||||||
const draggedDistance = distance2d(
|
const draggedDistance = distance2d(
|
||||||
this.lastPointerDown!.clientX,
|
this.lastPointerDown!.clientX,
|
||||||
@ -4546,7 +4643,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} else if (Object.keys(pointerDownState.elementIdsToErase).length) {
|
} else if (Object.keys(pointerDownState.elementIdsToErase).length) {
|
||||||
this.restoreReadyToEraseElements(pointerDownState);
|
this.restoreReadyToEraseElements(pointerDownState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
hitElement &&
|
||||||
!pointerDownState.drag.hasOccurred &&
|
!pointerDownState.drag.hasOccurred &&
|
||||||
@ -5347,7 +5443,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(event.nativeEvent.pointerType === "touch" ||
|
(event.nativeEvent.pointerType === "touch" ||
|
||||||
(event.nativeEvent.pointerType === "pen" &&
|
(event.nativeEvent.pointerType === "pen" &&
|
||||||
@ -5364,6 +5459,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
includeLockedElements: true,
|
includeLockedElements: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let disableContextMenu = false;
|
||||||
|
if (element && isCustomElement(element)) {
|
||||||
|
const config = this.props.customElementsConfig?.[element.customType];
|
||||||
|
|
||||||
|
disableContextMenu = !!config?.disableContextMenu;
|
||||||
|
}
|
||||||
|
if (disableContextMenu) {
|
||||||
|
this.contextMenuOpen = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const type = element ? "element" : "canvas";
|
const type = element ? "element" : "canvas";
|
||||||
|
|
||||||
const container = this.excalidrawContainerRef.current!;
|
const container = this.excalidrawContainerRef.current!;
|
||||||
|
@ -68,6 +68,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 = ({
|
||||||
@ -95,6 +96,7 @@ const LayerUI = ({
|
|||||||
library,
|
library,
|
||||||
id,
|
id,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
|
renderCustomElementWidget,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const deviceType = useDeviceType();
|
const deviceType = useDeviceType();
|
||||||
|
|
||||||
@ -439,6 +441,8 @@ const LayerUI = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{actionManager.renderAction("eraser", { size: "small" })}
|
{actionManager.renderAction("eraser", { size: "small" })}
|
||||||
|
{renderCustomElementWidget &&
|
||||||
|
renderCustomElementWidget(appState)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import cssVariables from "./css/variables.module.scss";
|
import cssVariables from "./css/variables.module.scss";
|
||||||
import { AppProps } from "./types";
|
import { AppProps, CustomElementConfig } from "./types";
|
||||||
import { FontFamilyValues } from "./element/types";
|
import { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
@ -155,6 +155,17 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_CUSTOM_ELEMENT_CONFIG: Required<CustomElementConfig> = {
|
||||||
|
type: "custom",
|
||||||
|
customType: "custom",
|
||||||
|
transformHandles: true,
|
||||||
|
displayData: { content: "", type: "svg" },
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
stackedOnTop: false,
|
||||||
|
onCreate: () => {},
|
||||||
|
disableContextMenu: false,
|
||||||
|
};
|
||||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||||
|
@ -48,6 +48,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||||||
arrow: true,
|
arrow: true,
|
||||||
freedraw: true,
|
freedraw: true,
|
||||||
eraser: false,
|
eraser: false,
|
||||||
|
custom: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RestoredDataState = {
|
export type RestoredDataState = {
|
||||||
@ -198,6 +199,10 @@ const restoreElement = (
|
|||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
case "custom":
|
||||||
|
return restoreElementWithProperties(element, {
|
||||||
|
customType: element.customType || "custom",
|
||||||
|
});
|
||||||
// generic elements
|
// generic elements
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
@ -255,6 +260,19 @@ export const restoreAppState = (
|
|||||||
? localValue
|
? localValue
|
||||||
: defaultValue;
|
: defaultValue;
|
||||||
}
|
}
|
||||||
|
const activeTool: any = {
|
||||||
|
lastActiveToolBeforeEraser: null,
|
||||||
|
locked: nextAppState.activeTool.locked ?? false,
|
||||||
|
type: "selection",
|
||||||
|
};
|
||||||
|
if (AllowedExcalidrawActiveTools[nextAppState.activeTool.type]) {
|
||||||
|
if (nextAppState.activeTool.type === "custom") {
|
||||||
|
activeTool.type = "custom";
|
||||||
|
activeTool.customType = nextAppState.activeTool.customType ?? "custom";
|
||||||
|
} else {
|
||||||
|
activeTool.type = nextAppState.activeTool.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
cursorButton: localAppState?.cursorButton || "up",
|
cursorButton: localAppState?.cursorButton || "up",
|
||||||
@ -262,13 +280,7 @@ export const restoreAppState = (
|
|||||||
penDetected:
|
penDetected:
|
||||||
localAppState?.penDetected ??
|
localAppState?.penDetected ??
|
||||||
(appState.penMode ? appState.penDetected ?? false : false),
|
(appState.penMode ? appState.penDetected ?? false : false),
|
||||||
activeTool: {
|
activeTool,
|
||||||
lastActiveToolBeforeEraser: null,
|
|
||||||
locked: nextAppState.activeTool.locked ?? false,
|
|
||||||
type: AllowedExcalidrawActiveTools[nextAppState.activeTool.type]
|
|
||||||
? nextAppState.activeTool.type ?? "selection"
|
|
||||||
: "selection",
|
|
||||||
},
|
|
||||||
// Migrates from previous version where appState.zoom was a number
|
// Migrates from previous version where appState.zoom was a number
|
||||||
zoom:
|
zoom:
|
||||||
typeof appState.zoom === "number"
|
typeof appState.zoom === "number"
|
||||||
|
@ -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";
|
||||||
@ -32,13 +33,20 @@ import { Point } from "../types";
|
|||||||
import { Drawable } from "roughjs/bin/core";
|
import { Drawable } from "roughjs/bin/core";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isCustomElement,
|
||||||
|
isImageElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
|
|
||||||
const isElementDraggableFromInside = (
|
const isElementDraggableFromInside = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
if (isCustomElement(element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -166,6 +174,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 +208,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 +238,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 +515,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 +548,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 +599,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 +633,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 +643,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 +786,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";
|
||||||
@ -320,6 +321,17 @@ export const newImageElement = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const newCustomElement = (
|
||||||
|
customType: string,
|
||||||
|
opts: {
|
||||||
|
type: ExcalidrawCustomElement["type"];
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): NonDeleted<ExcalidrawCustomElement> => {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawCustomElement>("custom", opts),
|
||||||
|
customType,
|
||||||
|
};
|
||||||
|
};
|
||||||
// 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.)
|
||||||
//
|
//
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
|
ExcalidrawCustomElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isGenericElement = (
|
export const isGenericElement = (
|
||||||
@ -142,3 +143,7 @@ export const isBoundToContainer = (
|
|||||||
element !== null && isTextElement(element) && element.containerId !== null
|
element !== null && isTextElement(element) && element.containerId !== null
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isCustomElement = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawCustomElement => element && element.type === "custom";
|
||||||
|
@ -84,6 +84,9 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
|||||||
scale: [number, number];
|
scale: [number, number];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawCustomElement = _ExcalidrawElementBase &
|
||||||
|
Readonly<{ type: "custom"; customType: string }>;
|
||||||
|
|
||||||
export type InitializedExcalidrawImageElement = MarkNonNullable<
|
export type InitializedExcalidrawImageElement = MarkNonNullable<
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
"fileId"
|
"fileId"
|
||||||
@ -108,7 +111,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;
|
||||||
@ -134,7 +138,8 @@ export type ExcalidrawBindableElement =
|
|||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement
|
| ExcalidrawEllipseElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawImageElement;
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawCustomElement;
|
||||||
|
|
||||||
export type ExcalidrawTextContainer =
|
export type ExcalidrawTextContainer =
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
|
@ -8,7 +8,6 @@ import initialData from "./initialData";
|
|||||||
|
|
||||||
// This is so that we use the bundled excalidraw.development.js file instead
|
// This is so that we use the bundled excalidraw.development.js file instead
|
||||||
// of the actual source code
|
// of the actual source code
|
||||||
|
|
||||||
const {
|
const {
|
||||||
exportToCanvas,
|
exportToCanvas,
|
||||||
exportToSvg,
|
exportToSvg,
|
||||||
@ -16,8 +15,27 @@ const {
|
|||||||
exportToClipboard,
|
exportToClipboard,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
} = window.ExcalidrawLib;
|
} = window.ExcalidrawLib;
|
||||||
|
|
||||||
|
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 THUMBS_UP_SVG = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<path d="M96 191.1H32c-17.67 0-32 14.33-32 31.1v223.1c0 17.67 14.33 31.1 32 31.1h64c17.67 0 32-14.33 32-31.1V223.1C128 206.3 113.7 191.1 96 191.1zM512 227c0-36.89-30.05-66.92-66.97-66.92h-99.86C354.7 135.1 360 113.5 360 100.8c0-33.8-26.2-68.78-70.06-68.78c-46.61 0-59.36 32.44-69.61 58.5c-31.66 80.5-60.33 66.39-60.33 93.47c0 12.84 10.36 23.99 24.02 23.99c5.256 0 10.55-1.721 14.97-5.26c76.76-61.37 57.97-122.7 90.95-122.7c16.08 0 22.06 12.75 22.06 20.79c0 7.404-7.594 39.55-25.55 71.59c-2.046 3.646-3.066 7.686-3.066 11.72c0 13.92 11.43 23.1 24 23.1h137.6C455.5 208.1 464 216.6 464 227c0 9.809-7.766 18.03-17.67 18.71c-12.66 .8593-22.36 11.4-22.36 23.94c0 15.47 11.39 15.95 11.39 28.91c0 25.37-35.03 12.34-35.03 42.15c0 11.22 6.392 13.03 6.392 22.25c0 22.66-29.77 13.76-29.77 40.64c0 4.515 1.11 5.961 1.11 9.456c0 10.45-8.516 18.95-18.97 18.95h-52.53c-25.62 0-51.02-8.466-71.5-23.81l-36.66-27.51c-4.315-3.245-9.37-4.811-14.38-4.811c-13.85 0-24.03 11.38-24.03 24.04c0 7.287 3.312 14.42 9.596 19.13l36.67 27.52C235 468.1 270.6 480 306.6 480h52.53c35.33 0 64.36-27.49 66.8-62.2c17.77-12.23 28.83-32.51 28.83-54.83c0-3.046-.2187-6.107-.6406-9.122c17.84-12.15 29.28-32.58 29.28-55.28c0-5.311-.6406-10.54-1.875-15.64C499.9 270.1 512 250.2 512 227z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const resolvablePromise = () => {
|
const resolvablePromise = () => {
|
||||||
let resolve;
|
let resolve;
|
||||||
let reject;
|
let reject;
|
||||||
@ -53,7 +71,7 @@ const renderFooter = () => {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const excalidrawRef = useRef(null);
|
const excalidrawRef = useRef(null);
|
||||||
|
const excalidrawWrapperRef = useRef(null);
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
@ -151,6 +169,135 @@ 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>
|
||||||
|
<button
|
||||||
|
className="custom-element"
|
||||||
|
onClick={() => {
|
||||||
|
excalidrawRef.current.setCustomType("thumbsup");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{THUMBS_UP_SVG}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = (element) => {
|
||||||
|
setTimeout(() => addTextArea(element), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCustomElementsConfig = () => {
|
||||||
|
return {
|
||||||
|
star: {
|
||||||
|
type: "custom",
|
||||||
|
customType: "star",
|
||||||
|
displayData: {
|
||||||
|
type: "svg",
|
||||||
|
content: `<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,
|
||||||
|
disableContextMenu: true,
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: "custom",
|
||||||
|
customType: "comment",
|
||||||
|
displayData: {
|
||||||
|
type: "svg",
|
||||||
|
content: () =>
|
||||||
|
`<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>`,
|
||||||
|
},
|
||||||
|
transformHandles: false,
|
||||||
|
stackedOnTop: true,
|
||||||
|
onCreate,
|
||||||
|
disableContextMenu: true,
|
||||||
|
},
|
||||||
|
thumbsup: {
|
||||||
|
type: "custom",
|
||||||
|
customType: "thumbsup",
|
||||||
|
displayData: {
|
||||||
|
type: "dataURL",
|
||||||
|
content: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.crossOrigin = "Anonymous";
|
||||||
|
image.src =
|
||||||
|
"https://upload.wikimedia.org/wikipedia/commons/1/1f/SMirC-thumbsup.svg";
|
||||||
|
|
||||||
|
image.onload = function () {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = 30 * window.devicePixelRatio;
|
||||||
|
canvas.height = 30 * window.devicePixelRatio;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
context.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||||
|
context.drawImage(image, 5, 5, 20, 20);
|
||||||
|
resolve(canvas.toDataURL());
|
||||||
|
};
|
||||||
|
image.onerror = (err) => reject(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTextArea = (element) => {
|
||||||
|
const { x: viewPortX, y: viewPortY } = sceneCoordsToViewportCoords(
|
||||||
|
{
|
||||||
|
sceneX: element.x,
|
||||||
|
sceneY: element.y,
|
||||||
|
},
|
||||||
|
excalidrawRef.current.getAppState(),
|
||||||
|
);
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
Object.assign(textarea.style, {
|
||||||
|
position: "absolute",
|
||||||
|
display: "inline-block",
|
||||||
|
left: `${viewPortX + element.width / 2}px`,
|
||||||
|
top: `${viewPortY + element.height / 2}px`,
|
||||||
|
height: `${100}px`,
|
||||||
|
width: `${100}px`,
|
||||||
|
zIndex: 10,
|
||||||
|
className: "comment-textarea",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
fontSize: "13px",
|
||||||
|
});
|
||||||
|
textarea.placeholder = "Start typing your comments";
|
||||||
|
|
||||||
|
textarea.onblur = () => {
|
||||||
|
textarea.remove();
|
||||||
|
};
|
||||||
|
excalidrawWrapperRef.current.querySelector(".excalidraw").append(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onElementClick = (element) => {
|
||||||
|
if (element.type === "custom" && element.customType === "comment") {
|
||||||
|
addTextArea(element);
|
||||||
|
}
|
||||||
|
};
|
||||||
const onCopy = async (type) => {
|
const onCopy = async (type) => {
|
||||||
await exportToClipboard({
|
await exportToClipboard({
|
||||||
elements: excalidrawRef.current.getSceneElements(),
|
elements: excalidrawRef.current.getSceneElements(),
|
||||||
@ -160,6 +307,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<h1> Excalidraw Example</h1>
|
<h1> Excalidraw Example</h1>
|
||||||
@ -275,14 +423,14 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="excalidraw-wrapper">
|
<div className="excalidraw-wrapper" ref={excalidrawWrapperRef}>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={excalidrawRef}
|
ref={excalidrawRef}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
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")
|
||||||
}
|
}
|
||||||
@ -295,6 +443,9 @@ export default function App() {
|
|||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderFooter={renderFooter}
|
renderFooter={renderFooter}
|
||||||
onLinkOpen={onLinkOpen}
|
onLinkOpen={onLinkOpen}
|
||||||
|
renderCustomElementWidget={renderCustomElementWidget}
|
||||||
|
customElementsConfig={getCustomElementsConfig()}
|
||||||
|
onElementClick={onElementClick}
|
||||||
/>
|
/>
|
||||||
</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;
|
||||||
|
}
|
||||||
|
@ -69,6 +69,32 @@ export default {
|
|||||||
status: "pending",
|
status: "pending",
|
||||||
scale: [1, 1],
|
scale: [1, 1],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "z35XgE9DvTXlG1OzXmp2x",
|
||||||
|
type: "custom",
|
||||||
|
x: 147.13928993437958,
|
||||||
|
y: 328.8974609375,
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
angle: 0,
|
||||||
|
strokeColor: "#000000",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
fillStyle: "hachure",
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
strokeSharpness: "round",
|
||||||
|
seed: 1483808630,
|
||||||
|
version: 79,
|
||||||
|
versionNonce: 861014250,
|
||||||
|
isDeleted: false,
|
||||||
|
boundElements: null,
|
||||||
|
updated: 1648630123004,
|
||||||
|
link: null,
|
||||||
|
customType: "star",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
|
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
|
||||||
scrollToContent: true,
|
scrollToContent: true,
|
||||||
|
@ -8,7 +8,10 @@ import "../../css/styles.scss";
|
|||||||
|
|
||||||
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||||
import { defaultLang } from "../../i18n";
|
import { defaultLang } from "../../i18n";
|
||||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
import {
|
||||||
|
DEFAULT_UI_OPTIONS,
|
||||||
|
DEFAULT_CUSTOM_ELEMENT_CONFIG,
|
||||||
|
} from "../../constants";
|
||||||
import { Provider } from "jotai";
|
import { Provider } from "jotai";
|
||||||
import { jotaiScope, jotaiStore } from "../../jotai";
|
import { jotaiScope, jotaiStore } from "../../jotai";
|
||||||
|
|
||||||
@ -37,6 +40,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
generateIdForFile,
|
generateIdForFile,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
|
renderCustomElementWidget,
|
||||||
|
onElementClick,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
@ -47,6 +52,10 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
...canvasActions,
|
...canvasActions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const customElementsConfig = {} as AppProps["customElementsConfig"];
|
||||||
|
Object.entries(props.customElementsConfig || {}).forEach(([key, value]) => {
|
||||||
|
customElementsConfig![key] = { ...DEFAULT_CUSTOM_ELEMENT_CONFIG, ...value };
|
||||||
|
});
|
||||||
|
|
||||||
if (canvasActions?.export) {
|
if (canvasActions?.export) {
|
||||||
UIOptions.canvasActions.export.saveFileToDisk =
|
UIOptions.canvasActions.export.saveFileToDisk =
|
||||||
@ -100,6 +109,9 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
generateIdForFile={generateIdForFile}
|
generateIdForFile={generateIdForFile}
|
||||||
onLinkOpen={onLinkOpen}
|
onLinkOpen={onLinkOpen}
|
||||||
|
renderCustomElementWidget={renderCustomElementWidget}
|
||||||
|
customElementsConfig={customElementsConfig}
|
||||||
|
onElementClick={onElementClick}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</InitializeApp>
|
</InitializeApp>
|
||||||
@ -209,3 +221,5 @@ export {
|
|||||||
newElementWith,
|
newElementWith,
|
||||||
bumpVersion,
|
bumpVersion,
|
||||||
} from "../../element/mutateElement";
|
} from "../../element/mutateElement";
|
||||||
|
|
||||||
|
export { sceneCoordsToViewportCoords } from "../../utils";
|
||||||
|
@ -9,7 +9,7 @@ const devServerConfig = {
|
|||||||
},
|
},
|
||||||
// Server Configuration options
|
// Server Configuration options
|
||||||
devServer: {
|
devServer: {
|
||||||
port: 3001,
|
//port: 3001,
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
hot: true,
|
hot: true,
|
||||||
compress: true,
|
compress: true,
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
|
ExcalidrawCustomElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
@ -189,6 +190,9 @@ const drawImagePlaceholder = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customElementImgCache: {
|
||||||
|
[key: ExcalidrawCustomElement["customType"]]: HTMLImageElement;
|
||||||
|
} = {};
|
||||||
const drawElementOnCanvas = (
|
const drawElementOnCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
@ -250,6 +254,54 @@ const drawElementOnCanvas = (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "custom": {
|
||||||
|
const config = renderConfig.customElementsConfig?.[element.customType];
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const cacheImage = (data: string, type: "svg" | "dataURL") => {
|
||||||
|
if (!customElementImgCache[element.id]) {
|
||||||
|
let url: string;
|
||||||
|
if (type === "svg") {
|
||||||
|
url = `data:${MIME_TYPES.svg}, ${encodeURIComponent(data)}`;
|
||||||
|
} else {
|
||||||
|
url = data;
|
||||||
|
}
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = url;
|
||||||
|
img.id = element.id;
|
||||||
|
customElementImgCache[element.id] = img;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const { type, content } = config.displayData;
|
||||||
|
if (typeof content === "string") {
|
||||||
|
cacheImage(content, type);
|
||||||
|
} else {
|
||||||
|
const contentData = content(element);
|
||||||
|
if (contentData instanceof Promise) {
|
||||||
|
contentData.then(
|
||||||
|
(res) => {
|
||||||
|
cacheImage(res, type);
|
||||||
|
},
|
||||||
|
(err) => console.error(err),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cacheImage(contentData, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (customElementImgCache[element.id]) {
|
||||||
|
context.drawImage(
|
||||||
|
customElementImgCache[element.id],
|
||||||
|
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 +831,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 +862,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,
|
||||||
@ -305,24 +304,32 @@ export const renderScene = (
|
|||||||
!appState.editingLinearElement
|
!appState.editingLinearElement
|
||||||
) {
|
) {
|
||||||
const selections = elements.reduce((acc, element) => {
|
const selections = elements.reduce((acc, element) => {
|
||||||
|
const isCustom = element.type === "custom";
|
||||||
|
let config;
|
||||||
const selectionColors = [];
|
const selectionColors = [];
|
||||||
// local user
|
|
||||||
if (
|
if (element.type === "custom") {
|
||||||
appState.selectedElementIds[element.id] &&
|
config = renderConfig.customElementsConfig?.[element.customType];
|
||||||
!isSelectedViaGroup(appState, element)
|
|
||||||
) {
|
|
||||||
selectionColors.push(oc.black);
|
|
||||||
}
|
}
|
||||||
// remote users
|
if (!isCustom || (isCustom && config && config.transformHandles)) {
|
||||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
// local user
|
||||||
selectionColors.push(
|
if (
|
||||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
appState.selectedElementIds[element.id] &&
|
||||||
(socketId) => {
|
!isSelectedViaGroup(appState, element)
|
||||||
const { background } = getClientColors(socketId, appState);
|
) {
|
||||||
return background;
|
selectionColors.push(oc.black);
|
||||||
},
|
}
|
||||||
),
|
// remote users
|
||||||
);
|
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||||
|
selectionColors.push(
|
||||||
|
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||||
|
(socketId) => {
|
||||||
|
const { background } = getClientColors(socketId, appState);
|
||||||
|
return background;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (selectionColors.length) {
|
if (selectionColors.length) {
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
@ -352,7 +359,6 @@ export const renderScene = (
|
|||||||
selectionColors: [oc.black],
|
selectionColors: [oc.black],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const groupId of getSelectedGroupIds(appState)) {
|
for (const groupId of getSelectedGroupIds(appState)) {
|
||||||
// TODO: support multiplayer selected group IDs
|
// TODO: support multiplayer selected group IDs
|
||||||
addSelectionForGroupId(groupId);
|
addSelectionForGroupId(groupId);
|
||||||
@ -372,19 +378,33 @@ export const renderScene = (
|
|||||||
context.save();
|
context.save();
|
||||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||||
if (locallySelectedElements.length === 1) {
|
if (locallySelectedElements.length === 1) {
|
||||||
context.fillStyle = oc.white;
|
let showTransformHandles = true;
|
||||||
const transformHandles = getTransformHandles(
|
if (locallySelectedElements[0].type === "custom") {
|
||||||
locallySelectedElements[0],
|
const config =
|
||||||
renderConfig.zoom,
|
renderConfig.customElementsConfig?.[
|
||||||
"mouse", // when we render we don't know which pointer type so use mouse
|
locallySelectedElements[0].customType
|
||||||
);
|
];
|
||||||
if (!appState.viewModeEnabled) {
|
|
||||||
renderTransformHandles(
|
if (!config || !config.transformHandles) {
|
||||||
context,
|
showTransformHandles = false;
|
||||||
renderConfig,
|
}
|
||||||
transformHandles,
|
}
|
||||||
locallySelectedElements[0].angle,
|
if (showTransformHandles) {
|
||||||
|
context.fillStyle = oc.white;
|
||||||
|
const transformHandles = getTransformHandles(
|
||||||
|
locallySelectedElements[0],
|
||||||
|
renderConfig.zoom,
|
||||||
|
"mouse", // when we render we don't know which pointer type so use mouse
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!appState.viewModeEnabled) {
|
||||||
|
renderTransformHandles(
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
transformHandles,
|
||||||
|
locallySelectedElements[0].angle,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
||||||
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
||||||
@ -573,6 +593,7 @@ const renderTransformHandles = (
|
|||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig,
|
||||||
transformHandles: TransformHandles,
|
transformHandles: TransformHandles,
|
||||||
angle: number,
|
angle: number,
|
||||||
|
name?: string,
|
||||||
): void => {
|
): void => {
|
||||||
Object.keys(transformHandles).forEach((key) => {
|
Object.keys(transformHandles).forEach((key) => {
|
||||||
const transformHandle = transformHandles[key as TransformHandleType];
|
const transformHandle = transformHandles[key as TransformHandleType];
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import App from "../components/App";
|
||||||
|
import { isCustomElement } from "../element/typeChecks";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
@ -26,7 +28,11 @@ class Scene {
|
|||||||
|
|
||||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
||||||
private static sceneMapById = new Map<string, Scene>();
|
private static sceneMapById = new Map<string, Scene>();
|
||||||
|
private app: App;
|
||||||
|
|
||||||
|
constructor(app: App) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
||||||
if (isIdKey(elementKey)) {
|
if (isIdKey(elementKey)) {
|
||||||
this.sceneMapById.set(elementKey, scene);
|
this.sceneMapById.set(elementKey, scene);
|
||||||
@ -91,12 +97,28 @@ class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
||||||
this.elements = nextElements;
|
this.elements = [];
|
||||||
|
const elements: ExcalidrawElement[] = [];
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
|
const elementsToBeStackedOnTop: ExcalidrawElement[] = [];
|
||||||
nextElements.forEach((element) => {
|
nextElements.forEach((element) => {
|
||||||
|
if (isCustomElement(element)) {
|
||||||
|
const config =
|
||||||
|
this.app.props.customElementsConfig?.[element.customType];
|
||||||
|
|
||||||
|
if (config?.stackedOnTop) {
|
||||||
|
elementsToBeStackedOnTop.push(element);
|
||||||
|
} else {
|
||||||
|
elements.push(element);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.push(element);
|
||||||
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
Scene.mapElementToScene(element, this);
|
||||||
});
|
});
|
||||||
|
elementsToBeStackedOnTop.forEach((ele) => elements.push(ele));
|
||||||
|
this.elements = elements;
|
||||||
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
this.informMutation();
|
this.informMutation();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
|
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
import { getElementAbsoluteCoords } from "../element";
|
||||||
import { isTextBindableContainer } from "../element/typeChecks";
|
import { isTextBindableContainer } from "../element/typeChecks";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const hasBackground = (type: string) =>
|
export const hasBackground = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
@ -31,7 +32,7 @@ export const hasStrokeStyle = (type: string) =>
|
|||||||
type === "arrow" ||
|
type === "arrow" ||
|
||||||
type === "line";
|
type === "line";
|
||||||
|
|
||||||
export const canChangeSharpness = (type: string) =>
|
export const canChangeSharpness = (type: AppState["activeTool"]["type"]) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "arrow" ||
|
type === "arrow" ||
|
||||||
type === "line" ||
|
type === "line" ||
|
||||||
|
@ -3,20 +3,25 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppProps, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
|
|
||||||
export const getElementsWithinSelection = (
|
export const getElementsWithinSelection = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
selection: NonDeletedExcalidrawElement,
|
selection: NonDeletedExcalidrawElement,
|
||||||
|
customElementConfig: AppProps["customElementsConfig"],
|
||||||
) => {
|
) => {
|
||||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||||
getElementAbsoluteCoords(selection);
|
getElementAbsoluteCoords(selection);
|
||||||
return elements.filter((element) => {
|
return elements.filter((element) => {
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
getElementBounds(element);
|
getElementBounds(element);
|
||||||
|
const isCustom = element.type === "custom";
|
||||||
|
const allowSelection = isCustom
|
||||||
|
? customElementConfig?.[element.customType]?.transformHandles
|
||||||
|
: true;
|
||||||
return (
|
return (
|
||||||
|
allowSelection &&
|
||||||
element.locked === false &&
|
element.locked === false &&
|
||||||
element.type !== "selection" &&
|
element.type !== "selection" &&
|
||||||
!isBoundToContainer(element) &&
|
!isBoundToContainer(element) &&
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ExcalidrawTextElement } from "../element/types";
|
import { ExcalidrawTextElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppProps, AppState } 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?: AppProps["customElementsConfig"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SceneScroll = {
|
export type SceneScroll = {
|
||||||
|
49
src/types.ts
49
src/types.ts
@ -66,6 +66,14 @@ export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
|
|||||||
|
|
||||||
export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
||||||
|
|
||||||
|
export type LastActiveToolBeforeEraser =
|
||||||
|
| typeof SHAPES[number]["value"]
|
||||||
|
| {
|
||||||
|
type: "custom";
|
||||||
|
customType: string;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
|
||||||
export type AppState = {
|
export type AppState = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
@ -80,11 +88,18 @@ 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;
|
||||||
activeTool: {
|
activeTool:
|
||||||
type: typeof SHAPES[number]["value"] | "eraser";
|
| {
|
||||||
lastActiveToolBeforeEraser: typeof SHAPES[number]["value"] | null;
|
type: typeof SHAPES[number]["value"] | "eraser";
|
||||||
locked: boolean;
|
lastActiveToolBeforeEraser: LastActiveToolBeforeEraser;
|
||||||
};
|
locked: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "custom";
|
||||||
|
customType: string;
|
||||||
|
lastActiveToolBeforeEraser: LastActiveToolBeforeEraser;
|
||||||
|
locked: boolean;
|
||||||
|
};
|
||||||
penMode: boolean;
|
penMode: boolean;
|
||||||
penDetected: boolean;
|
penDetected: boolean;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
@ -212,6 +227,22 @@ export type ExcalidrawAPIRefValue =
|
|||||||
ready?: false;
|
ready?: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CustomElementConfig = {
|
||||||
|
type: "custom";
|
||||||
|
customType: string;
|
||||||
|
transformHandles?: boolean;
|
||||||
|
displayData: {
|
||||||
|
type: "svg" | "dataURL";
|
||||||
|
content:
|
||||||
|
| string
|
||||||
|
| ((element?: ExcalidrawElement) => string | Promise<string>);
|
||||||
|
};
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
stackedOnTop: boolean;
|
||||||
|
onCreate?: (element: ExcalidrawElement) => void;
|
||||||
|
disableContextMenu: boolean;
|
||||||
|
};
|
||||||
export type ExcalidrawInitialDataState = Merge<
|
export type ExcalidrawInitialDataState = Merge<
|
||||||
ImportedDataState,
|
ImportedDataState,
|
||||||
{
|
{
|
||||||
@ -271,6 +302,12 @@ export interface ExcalidrawProps {
|
|||||||
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
|
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
|
||||||
}>,
|
}>,
|
||||||
) => void;
|
) => void;
|
||||||
|
renderCustomElementWidget?: (appState: AppState) => void;
|
||||||
|
customElementsConfig?: Record<string, CustomElementConfig>;
|
||||||
|
onElementClick?: (
|
||||||
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SceneData = {
|
export type SceneData = {
|
||||||
@ -324,6 +361,7 @@ export type AppProps = ExcalidrawProps & {
|
|||||||
detectScroll: boolean;
|
detectScroll: boolean;
|
||||||
handleKeyboardGlobally: boolean;
|
handleKeyboardGlobally: boolean;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
|
customElementsConfig: Required<CustomElementConfig>[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A subset of App class properties that we need to use elsewhere
|
/** A subset of App class properties that we need to use elsewhere
|
||||||
@ -431,6 +469,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