mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-23 06:05:37 +01:00
Compare commits
51 Commits
export-deb
...
aakansha-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b951a0c5 | ||
|
|
c93d8f4bd0 | ||
|
|
645f9a5dc0 | ||
|
|
b30066ca19 | ||
|
|
aae8e2fa5d | ||
|
|
9e6d5fdbcb | ||
|
|
22b2e10ddb | ||
|
|
d53ac2a61e | ||
|
|
6a0f800716 | ||
|
|
aee1e2451e | ||
|
|
da94eb1284 | ||
|
|
ea51251fe6 | ||
|
|
399ce1e01a | ||
|
|
7df8302ba2 | ||
|
|
af8c59b5bb | ||
|
|
cf0f00285b | ||
|
|
b5c67a384c | ||
|
|
af93cedc08 | ||
|
|
128b7741c1 | ||
|
|
b6a6f2d465 | ||
|
|
6bcbf8b50a | ||
|
|
666516d7e9 | ||
|
|
b941c5b661 | ||
|
|
8f8c85c64e | ||
|
|
116b0c48da | ||
|
|
aa2971e8c5 | ||
|
|
5656ac1e3e | ||
|
|
1edde7291c | ||
|
|
1ca56204b1 | ||
|
|
f3ae7a8506 | ||
|
|
5f57daa132 | ||
|
|
db9c9eb3d2 | ||
|
|
2e8c4d25f2 | ||
|
|
4953828d86 | ||
|
|
6eb0cf6a10 | ||
|
|
ba48aa24a0 | ||
|
|
4e75f10b2c | ||
|
|
d2d3599661 | ||
|
|
ed3eda3401 | ||
|
|
d27b32dd2c | ||
|
|
2337842f57 | ||
|
|
5b78f50fe3 | ||
|
|
a4a95a591a | ||
|
|
3d459076fb | ||
|
|
14a23c6c50 | ||
|
|
5f4a5b1789 | ||
|
|
47498796e0 | ||
|
|
8706277d14 | ||
|
|
3d0a1106ff | ||
|
|
61699ff3c2 | ||
|
|
39d0084a5e |
@@ -29,7 +29,7 @@
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"browser-fs-access": "0.24.1",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"clsx": "1.1.1",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"firebase": "8.3.3",
|
||||
@@ -38,7 +38,7 @@
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "1.6.4",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "3.1.32",
|
||||
"nanoid": "3.3.3",
|
||||
"open-color": "1.9.1",
|
||||
"pako": "1.0.11",
|
||||
"perfect-freehand": "1.0.16",
|
||||
|
||||
@@ -25,9 +25,9 @@ export const actionAddToLibrary = register({
|
||||
}
|
||||
|
||||
return app.library
|
||||
.loadLibrary()
|
||||
.getLatestLibrary()
|
||||
.then((items) => {
|
||||
return app.library.saveLibrary([
|
||||
return app.library.setLibrary([
|
||||
{
|
||||
id: randomId(),
|
||||
status: "unpublished",
|
||||
|
||||
@@ -304,21 +304,42 @@ export const actionErase = register({
|
||||
name: "eraser",
|
||||
trackEvent: { category: "toolbar" },
|
||||
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 {
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
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,
|
||||
},
|
||||
activeTool,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,9 @@ export const actionCopy = register({
|
||||
name: "copy",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
copyToClipboard(getNonDeletedElements(elements), appState, app.files);
|
||||
const selectedElements = getSelectedElements(elements, appState, true);
|
||||
|
||||
copyToClipboard(selectedElements, appState, app.files);
|
||||
|
||||
return {
|
||||
commitToHistory: false,
|
||||
|
||||
@@ -136,7 +136,21 @@ export const actionFinalize = register({
|
||||
) {
|
||||
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 {
|
||||
elements: newElements,
|
||||
appState: {
|
||||
@@ -147,14 +161,7 @@ export const actionFinalize = register({
|
||||
appState.activeTool.type === "freedraw") &&
|
||||
multiPointElement
|
||||
? appState.activeTool
|
||||
: {
|
||||
...appState.activeTool,
|
||||
type:
|
||||
appState.activeTool.type === "eraser" &&
|
||||
appState.activeTool.lastActiveToolBeforeEraser
|
||||
? appState.activeTool.lastActiveToolBeforeEraser
|
||||
: "selection",
|
||||
},
|
||||
: activeTool,
|
||||
draggingElement: null,
|
||||
multiElement: null,
|
||||
editingElement: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getClientColors, getClientInitials } from "../clients";
|
||||
import { getClientColors } from "../clients";
|
||||
import { Avatar } from "../components/Avatar";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { Collaborator } from "../types";
|
||||
@@ -43,16 +43,15 @@ export const actionGoToCollaborator = register({
|
||||
}
|
||||
|
||||
const { background, stroke } = getClientColors(clientId, appState);
|
||||
const shortName = getClientInitials(collaborator.username);
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
color={background}
|
||||
border={stroke}
|
||||
onClick={() => updateData(collaborator.pointer)}
|
||||
>
|
||||
{shortName}
|
||||
</Avatar>
|
||||
name={collaborator.username || ""}
|
||||
src={collaborator.src}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -503,20 +503,6 @@ export const actionChangeOpacity = register({
|
||||
max="100"
|
||||
step="10"
|
||||
onChange={(event) => updateData(+event.target.value)}
|
||||
onWheel={(event) => {
|
||||
event.stopPropagation();
|
||||
const target = event.target as HTMLInputElement;
|
||||
const STEP = 10;
|
||||
const MAX = 100;
|
||||
const MIN = 0;
|
||||
const value = +target.value;
|
||||
|
||||
if (event.deltaY < 0 && value < MAX) {
|
||||
updateData(value + STEP);
|
||||
} else if (event.deltaY > 0 && value > MIN) {
|
||||
updateData(value - STEP);
|
||||
}
|
||||
}}
|
||||
value={
|
||||
getFormValue(
|
||||
elements,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { getSelectedElements } from "./scene";
|
||||
import { AppState, BinaryFiles } from "./types";
|
||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
@@ -12,7 +11,7 @@ import { isPromiseLike } from "./utils";
|
||||
|
||||
type ElementsClipboard = {
|
||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||
elements: ExcalidrawElement[];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
files: BinaryFiles | undefined;
|
||||
};
|
||||
|
||||
@@ -57,19 +56,20 @@ const clipboardContainsElements = (
|
||||
export const copyToClipboard = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
files: BinaryFiles | null,
|
||||
) => {
|
||||
// select binded text elements when copying
|
||||
const selectedElements = getSelectedElements(elements, appState, true);
|
||||
const contents: ElementsClipboard = {
|
||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||
elements: selectedElements,
|
||||
files: selectedElements.reduce((acc, element) => {
|
||||
if (isInitializedImageElement(element) && files[element.fileId]) {
|
||||
acc[element.fileId] = files[element.fileId];
|
||||
}
|
||||
return acc;
|
||||
}, {} as BinaryFiles),
|
||||
elements,
|
||||
files: files
|
||||
? elements.reduce((acc, element) => {
|
||||
if (isInitializedImageElement(element) && files[element.fileId]) {
|
||||
acc[element.fileId] = files[element.fileId];
|
||||
}
|
||||
return acc;
|
||||
}, {} as BinaryFiles)
|
||||
: undefined,
|
||||
};
|
||||
const json = JSON.stringify(contents);
|
||||
CLIPBOARD = json;
|
||||
|
||||
@@ -119,12 +119,17 @@ import {
|
||||
} from "../element/binding";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||
import {
|
||||
deepCopyElement,
|
||||
newCustomElement,
|
||||
newFreeDrawElement,
|
||||
} from "../element/newElement";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBindingElement,
|
||||
isBindingElementType,
|
||||
isBoundToContainer,
|
||||
isCustomElement,
|
||||
isImageElement,
|
||||
isInitializedImageElement,
|
||||
isLinearElement,
|
||||
@@ -257,6 +262,7 @@ import {
|
||||
isPointHittingLinkIcon,
|
||||
isLocalLink,
|
||||
} from "../element/Hyperlink";
|
||||
import { AbortError } from "../errors";
|
||||
|
||||
const defaultDeviceTypeContext: DeviceType = {
|
||||
isMobile: false,
|
||||
@@ -379,6 +385,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
importLibrary: this.importLibraryFromUrl,
|
||||
setToastMessage: this.setToastMessage,
|
||||
id: this.id,
|
||||
setCustomType: this.setCustomType,
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@@ -393,7 +400,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
id: this.id,
|
||||
};
|
||||
|
||||
this.scene = new Scene();
|
||||
this.scene = new Scene(this);
|
||||
this.library = new Library(this);
|
||||
this.history = new History();
|
||||
this.actionManager = new ActionManager(
|
||||
@@ -408,6 +415,59 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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() {
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
const {
|
||||
@@ -531,6 +591,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
library={this.library}
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
renderCustomElementWidget={this.props.renderCustomElementWidget}
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
@@ -703,21 +764,35 @@ class App extends React.Component<AppProps, AppState> {
|
||||
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
|
||||
}
|
||||
|
||||
const defaultStatus = "published";
|
||||
|
||||
this.setState({ isLibraryOpen: true });
|
||||
|
||||
try {
|
||||
const request = await fetch(decodeURIComponent(url));
|
||||
const blob = await request.blob();
|
||||
const defaultStatus = "published";
|
||||
const libraryItems = await loadLibraryFromBlob(blob, defaultStatus);
|
||||
if (
|
||||
token === this.id ||
|
||||
window.confirm(
|
||||
t("alerts.confirmAddLibrary", {
|
||||
numShapes: libraryItems.length,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
await this.library.importLibrary(libraryItems, defaultStatus);
|
||||
}
|
||||
await this.library.importLibrary(
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
const request = await fetch(decodeURIComponent(url));
|
||||
const blob = await request.blob();
|
||||
const libraryItems = await loadLibraryFromBlob(blob, defaultStatus);
|
||||
|
||||
if (
|
||||
token === this.id ||
|
||||
window.confirm(
|
||||
t("alerts.confirmAddLibrary", {
|
||||
numShapes: libraryItems.length,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
resolve(libraryItems);
|
||||
} else {
|
||||
reject(new AbortError());
|
||||
}
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: t("errors.importLibraryError") });
|
||||
@@ -1219,6 +1294,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageCache: this.imageCache,
|
||||
isExporting: false,
|
||||
renderScrollbars: !this.deviceType.isMobile,
|
||||
customElementsConfig: this.props.customElementsConfig,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1576,14 +1652,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
}
|
||||
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 {
|
||||
activeTool: {
|
||||
...prevState.activeTool,
|
||||
locked: !prevState.activeTool.locked,
|
||||
type: prevState.activeTool.locked
|
||||
? "selection"
|
||||
: prevState.activeTool.type,
|
||||
},
|
||||
activeTool,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1674,6 +1754,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
collaborators?: SceneData["collaborators"];
|
||||
commitToHistory?: SceneData["commitToHistory"];
|
||||
libraryItems?:
|
||||
| ((
|
||||
currentLibraryItems: LibraryItems,
|
||||
) =>
|
||||
| Required<SceneData>["libraryItems"]
|
||||
| Promise<Required<SceneData>["libraryItems"]>)
|
||||
| Required<SceneData>["libraryItems"]
|
||||
| Promise<Required<SceneData>["libraryItems"]>;
|
||||
}) => {
|
||||
@@ -1694,20 +1779,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (sceneData.libraryItems) {
|
||||
this.library.saveLibrary(
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
this.library.setLibrary((currentLibraryItems) => {
|
||||
const nextItems =
|
||||
typeof sceneData.libraryItems === "function"
|
||||
? sceneData.libraryItems(currentLibraryItems)
|
||||
: sceneData.libraryItems;
|
||||
|
||||
return new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
resolve(
|
||||
restoreLibraryItems(
|
||||
await sceneData.libraryItems,
|
||||
"unpublished",
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
reject(new Error(t("errors.importLibraryError")));
|
||||
resolve(restoreLibraryItems(await nextItems, "unpublished"));
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -2239,8 +2324,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isTextElement(selectedElements[0])) {
|
||||
existingTextElement = selectedElements[0];
|
||||
} else if (isTextBindableContainer(selectedElements[0], false)) {
|
||||
container = selectedElements[0];
|
||||
existingTextElement = getBoundTextElement(container);
|
||||
existingTextElement = getBoundTextElement(selectedElements[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2810,6 +2894,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
)) &&
|
||||
!hitElement?.locked
|
||||
) {
|
||||
if (hitElement && isCustomElement(hitElement)) {
|
||||
const config =
|
||||
this.props.customElementsConfig?.[hitElement.customType];
|
||||
|
||||
if (!config?.transformHandles) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
@@ -3032,6 +3124,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x,
|
||||
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") {
|
||||
this.handleFreeDrawElementOnPointerDown(
|
||||
event,
|
||||
@@ -3072,15 +3170,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
this.lastPointerUp = event;
|
||||
if (this.deviceType.isTouchScreen) {
|
||||
let hitElement;
|
||||
if (this.deviceType.isTouchScreen || this.props.onElementClick) {
|
||||
const scenePointer = viewportCoordsToSceneCoords(
|
||||
{ clientX: event.clientX, clientY: event.clientY },
|
||||
this.state,
|
||||
);
|
||||
const hitElement = this.getElementAtPosition(
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
hitElement = this.getElementAtPosition(scenePointer.x, scenePointer.y);
|
||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||
scenePointer,
|
||||
hitElement,
|
||||
@@ -3093,6 +3189,24 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -4201,6 +4315,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const elementsWithinSelection = getElementsWithinSelection(
|
||||
elements,
|
||||
draggingElement,
|
||||
this.props.customElementsConfig,
|
||||
);
|
||||
this.setState((prevState) =>
|
||||
selectGroupsForSelectedElements(
|
||||
@@ -4494,6 +4609,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// Code below handles selection when element(s) weren't
|
||||
// drag or added to selection on pointer down phase.
|
||||
const hitElement = pointerDownState.hit.element;
|
||||
|
||||
if (isEraserActive(this.state)) {
|
||||
const draggedDistance = distance2d(
|
||||
this.lastPointerDown!.clientX,
|
||||
@@ -4527,7 +4643,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
} else if (Object.keys(pointerDownState.elementIdsToErase).length) {
|
||||
this.restoreReadyToEraseElements(pointerDownState);
|
||||
}
|
||||
|
||||
if (
|
||||
hitElement &&
|
||||
!pointerDownState.drag.hasOccurred &&
|
||||
@@ -5281,11 +5396,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
file?.type === MIME_TYPES.excalidrawlib ||
|
||||
file?.name?.endsWith(".excalidrawlib")
|
||||
) {
|
||||
this.library
|
||||
.importLibrary(file)
|
||||
.catch((error) =>
|
||||
this.setState({ isLoading: false, errorMessage: error.message }),
|
||||
);
|
||||
this.setState({ isLibraryOpen: true });
|
||||
this.library.importLibrary(file).catch((error) => {
|
||||
console.error(error);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMessage: t("errors.importLibraryError"),
|
||||
});
|
||||
});
|
||||
// default: assume an Excalidraw file regardless of extension/MimeType
|
||||
} else if (file) {
|
||||
this.setState({ isLoading: true });
|
||||
@@ -5325,7 +5443,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
(event.nativeEvent.pointerType === "touch" ||
|
||||
(event.nativeEvent.pointerType === "pen" &&
|
||||
@@ -5342,6 +5459,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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 container = this.excalidrawContainerRef.current!;
|
||||
|
||||
@@ -12,5 +12,11 @@
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
|
||||
&-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import "./Avatar.scss";
|
||||
|
||||
import React from "react";
|
||||
import { getClientInitials } from "../clients";
|
||||
|
||||
type AvatarProps = {
|
||||
children: string;
|
||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
color: string;
|
||||
border: string;
|
||||
name: string;
|
||||
src?: string;
|
||||
};
|
||||
|
||||
export const Avatar = ({ children, color, border, onClick }: AvatarProps) => (
|
||||
<div
|
||||
className="Avatar"
|
||||
style={{ background: color, border: `1px solid ${border}` }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => {
|
||||
const shortName = getClientInitials(name);
|
||||
const style = src
|
||||
? undefined
|
||||
: { background: color, border: `1px solid ${border}` };
|
||||
return (
|
||||
<div className="Avatar" style={style} onClick={onClick}>
|
||||
{src ? (
|
||||
<img className="Avatar-img" src={src} alt={shortName} />
|
||||
) : (
|
||||
shortName
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ interface LayerUIProps {
|
||||
library: Library;
|
||||
id: string;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderCustomElementWidget?: (appState: AppState) => void;
|
||||
}
|
||||
|
||||
const LayerUI = ({
|
||||
@@ -95,6 +96,7 @@ const LayerUI = ({
|
||||
library,
|
||||
id,
|
||||
onImageAction,
|
||||
renderCustomElementWidget,
|
||||
}: LayerUIProps) => {
|
||||
const deviceType = useDeviceType();
|
||||
|
||||
@@ -439,6 +441,8 @@ const LayerUI = ({
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("eraser", { size: "small" })}
|
||||
{renderCustomElementWidget &&
|
||||
renderCustomElementWidget(appState)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
|
||||
.Spinner {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
// 2px from the left to account for focus border of left-most button
|
||||
margin: 0 2px;
|
||||
|
||||
@@ -139,7 +139,7 @@ export const LibraryMenu = ({
|
||||
const nextItems = libraryItems.filter(
|
||||
(item) => !selectedItems.includes(item.id),
|
||||
);
|
||||
library.saveLibrary(nextItems).catch(() => {
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||
});
|
||||
setSelectedItems([]);
|
||||
@@ -170,7 +170,7 @@ export const LibraryMenu = ({
|
||||
...libraryItems,
|
||||
];
|
||||
onAddToLibrary();
|
||||
library.saveLibrary(nextItems).catch(() => {
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||
});
|
||||
},
|
||||
@@ -220,7 +220,7 @@ export const LibraryMenu = ({
|
||||
libItem.status = "published";
|
||||
}
|
||||
});
|
||||
library.saveLibrary(nextLibItems);
|
||||
library.setLibrary(nextLibItems);
|
||||
},
|
||||
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
||||
);
|
||||
@@ -229,7 +229,10 @@ export const LibraryMenu = ({
|
||||
LibraryItem["id"] | null
|
||||
>(null);
|
||||
|
||||
if (libraryItemsData.status === "loading") {
|
||||
if (
|
||||
libraryItemsData.status === "loading" &&
|
||||
!libraryItemsData.isInitialized
|
||||
) {
|
||||
return (
|
||||
<LibraryMenuWrapper ref={ref}>
|
||||
<div className="layer-ui__library-message">
|
||||
@@ -255,7 +258,7 @@ export const LibraryMenu = ({
|
||||
}
|
||||
onError={(error) => window.alert(error)}
|
||||
updateItemsInStorage={() =>
|
||||
library.saveLibrary(libraryItemsData.libraryItems)
|
||||
library.setLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
onRemove={(id: string) =>
|
||||
setSelectedItems(selectedItems.filter((_id) => _id !== id))
|
||||
@@ -264,6 +267,7 @@ export const LibraryMenu = ({
|
||||
)}
|
||||
{publishLibSuccess && renderPublishSuccess()}
|
||||
<LibraryMenuItems
|
||||
isLoading={libraryItemsData.status === "loading"}
|
||||
libraryItems={libraryItemsData.libraryItems}
|
||||
onRemoveFromLibrary={() =>
|
||||
removeFromLibrary(libraryItemsData.libraryItems)
|
||||
|
||||
@@ -22,8 +22,10 @@ import { Tooltip } from "./Tooltip";
|
||||
|
||||
import "./LibraryMenuItems.scss";
|
||||
import { VERSIONS } from "../constants";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
const LibraryMenuItems = ({
|
||||
isLoading,
|
||||
libraryItems,
|
||||
onRemoveFromLibrary,
|
||||
onAddToLibrary,
|
||||
@@ -40,6 +42,7 @@ const LibraryMenuItems = ({
|
||||
onPublish,
|
||||
resetLibrary,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
libraryItems: LibraryItems;
|
||||
pendingElements: LibraryItem["elements"];
|
||||
onRemoveFromLibrary: () => void;
|
||||
@@ -108,7 +111,8 @@ const LibraryMenuItems = ({
|
||||
importLibraryFromJSON(library)
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
console.error(error);
|
||||
setAppState({ errorMessage: t("errors.importLibraryError") });
|
||||
});
|
||||
}}
|
||||
className="library-actions--load"
|
||||
@@ -125,7 +129,7 @@ const LibraryMenuItems = ({
|
||||
onClick={async () => {
|
||||
const libraryItems = itemsSelected
|
||||
? items
|
||||
: await library.loadLibrary();
|
||||
: await library.getLatestLibrary();
|
||||
saveLibraryAsJSON(libraryItems)
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
@@ -284,16 +288,20 @@ const LibraryMenuItems = ({
|
||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||
<div className="layer-ui__library-header" key="library-header">
|
||||
{renderLibraryActions()}
|
||||
<a
|
||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||
window.name || "_blank"
|
||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||
VERSIONS.excalidrawLibrary
|
||||
}`}
|
||||
target="_excalidraw_libraries"
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<a
|
||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||
window.name || "_blank"
|
||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||
VERSIONS.excalidrawLibrary
|
||||
}`}
|
||||
target="_excalidraw_libraries"
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Stack.Col
|
||||
className="library-menu-items-container__items"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// container in body where the actual tooltip is appended to
|
||||
.excalidraw-tooltip {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cssVariables from "./css/variables.module.scss";
|
||||
import { AppProps } from "./types";
|
||||
import { AppProps, CustomElementConfig } from "./types";
|
||||
import { FontFamilyValues } from "./element/types";
|
||||
|
||||
export const APP_NAME = "Excalidraw";
|
||||
@@ -108,7 +108,8 @@ export const EXPORT_DATA_TYPES = {
|
||||
excalidrawLibrary: "excalidrawlib",
|
||||
} as const;
|
||||
|
||||
export const EXPORT_SOURCE = window.location.origin;
|
||||
export const EXPORT_SOURCE =
|
||||
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
||||
|
||||
// time in milliseconds
|
||||
export const IMAGE_RENDER_TIMEOUT = 500;
|
||||
@@ -154,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_LANDSCAPE = 1000;
|
||||
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||
|
||||
@@ -5,13 +5,12 @@ import type App from "../components/App";
|
||||
import { ImportedDataState } from "./types";
|
||||
import { atom } from "jotai";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { isPromiseLike } from "../utils";
|
||||
import { t } from "../i18n";
|
||||
|
||||
export const libraryItemsAtom = atom<
|
||||
| { status: "loading"; libraryItems: null; promise: Promise<LibraryItems> }
|
||||
| { status: "loaded"; libraryItems: LibraryItems }
|
||||
>({ status: "loaded", libraryItems: [] });
|
||||
export const libraryItemsAtom = atom<{
|
||||
status: "loading" | "loaded";
|
||||
isInitialized: boolean;
|
||||
libraryItems: LibraryItems;
|
||||
}>({ status: "loaded", isInitialized: true, libraryItems: [] });
|
||||
|
||||
const cloneLibraryItems = (libraryItems: LibraryItems): LibraryItems =>
|
||||
JSON.parse(JSON.stringify(libraryItems));
|
||||
@@ -40,12 +39,28 @@ const isUniqueItem = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Merges otherItems into localItems. Unique items in otherItems array are
|
||||
sorted first. */
|
||||
export const mergeLibraryItems = (
|
||||
localItems: LibraryItems,
|
||||
otherItems: LibraryItems,
|
||||
): LibraryItems => {
|
||||
const newItems = [];
|
||||
for (const item of otherItems) {
|
||||
if (isUniqueItem(localItems, item)) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [...newItems, ...localItems];
|
||||
};
|
||||
|
||||
class Library {
|
||||
/** cache for currently active promise when initializing/updating libaries
|
||||
asynchronously */
|
||||
private libraryItemsPromise: Promise<LibraryItems> | null = null;
|
||||
/** last resolved libraryItems */
|
||||
/** latest libraryItems */
|
||||
private lastLibraryItems: LibraryItems = [];
|
||||
/** indicates whether library is initialized with library items (has gone
|
||||
* though at least one update) */
|
||||
private isInitialized = false;
|
||||
|
||||
private app: App;
|
||||
|
||||
@@ -53,95 +68,138 @@ class Library {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
resetLibrary = async () => {
|
||||
this.saveLibrary([]);
|
||||
private updateQueue: Promise<LibraryItems>[] = [];
|
||||
|
||||
private getLastUpdateTask = (): Promise<LibraryItems> | undefined => {
|
||||
return this.updateQueue[this.updateQueue.length - 1];
|
||||
};
|
||||
|
||||
/** imports library (currently merges, removing duplicates) */
|
||||
async importLibrary(
|
||||
private notifyListeners = () => {
|
||||
if (this.updateQueue.length > 0) {
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
status: "loading",
|
||||
libraryItems: this.lastLibraryItems,
|
||||
isInitialized: this.isInitialized,
|
||||
});
|
||||
} else {
|
||||
this.isInitialized = true;
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
status: "loaded",
|
||||
libraryItems: this.lastLibraryItems,
|
||||
isInitialized: this.isInitialized,
|
||||
});
|
||||
try {
|
||||
this.app.props.onLibraryChange?.(
|
||||
cloneLibraryItems(this.lastLibraryItems),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
resetLibrary = () => {
|
||||
return this.setLibrary([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* imports library (from blob or libraryItems), merging with current library
|
||||
* (attempting to remove duplicates)
|
||||
*/
|
||||
importLibrary(
|
||||
library:
|
||||
| Blob
|
||||
| Required<ImportedDataState>["libraryItems"]
|
||||
| Promise<Required<ImportedDataState>["libraryItems"]>,
|
||||
defaultStatus: LibraryItem["status"] = "unpublished",
|
||||
) {
|
||||
return this.saveLibrary(
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
let libraryItems: LibraryItems;
|
||||
if (library instanceof Blob) {
|
||||
libraryItems = await loadLibraryFromBlob(library, defaultStatus);
|
||||
} else {
|
||||
libraryItems = restoreLibraryItems(await library, defaultStatus);
|
||||
}
|
||||
|
||||
const existingLibraryItems = this.lastLibraryItems;
|
||||
|
||||
const filteredItems = [];
|
||||
for (const item of libraryItems) {
|
||||
if (isUniqueItem(existingLibraryItems, item)) {
|
||||
filteredItems.push(item);
|
||||
): Promise<LibraryItems> {
|
||||
return this.setLibrary(
|
||||
() =>
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
let libraryItems: LibraryItems;
|
||||
if (library instanceof Blob) {
|
||||
libraryItems = await loadLibraryFromBlob(library, defaultStatus);
|
||||
} else {
|
||||
libraryItems = restoreLibraryItems(await library, defaultStatus);
|
||||
}
|
||||
}
|
||||
|
||||
resolve([...filteredItems, ...existingLibraryItems]);
|
||||
} catch (error) {
|
||||
reject(new Error(t("errors.importLibraryError")));
|
||||
}
|
||||
}),
|
||||
resolve(mergeLibraryItems(this.lastLibraryItems, libraryItems));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
loadLibrary = (): Promise<LibraryItems> => {
|
||||
/**
|
||||
* @returns latest cloned libraryItems. Awaits all in-progress updates first.
|
||||
*/
|
||||
getLatestLibrary = (): Promise<LibraryItems> => {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
resolve(
|
||||
cloneLibraryItems(
|
||||
await (this.libraryItemsPromise || this.lastLibraryItems),
|
||||
),
|
||||
);
|
||||
const libraryItems = await (this.getLastUpdateTask() ||
|
||||
this.lastLibraryItems);
|
||||
if (this.updateQueue.length > 0) {
|
||||
resolve(this.getLatestLibrary());
|
||||
} else {
|
||||
resolve(cloneLibraryItems(libraryItems));
|
||||
}
|
||||
} catch (error) {
|
||||
return resolve(this.lastLibraryItems);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
saveLibrary = async (items: LibraryItems | Promise<LibraryItems>) => {
|
||||
const prevLibraryItems = this.lastLibraryItems;
|
||||
try {
|
||||
let nextLibraryItems;
|
||||
if (isPromiseLike(items)) {
|
||||
const promise = items.then((items) => cloneLibraryItems(items));
|
||||
this.libraryItemsPromise = promise;
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
status: "loading",
|
||||
promise,
|
||||
libraryItems: null,
|
||||
});
|
||||
nextLibraryItems = await promise;
|
||||
} else {
|
||||
nextLibraryItems = cloneLibraryItems(items);
|
||||
setLibrary = (
|
||||
/**
|
||||
* LibraryItems that will replace current items. Can be a function which
|
||||
* will be invoked after all previous tasks are resolved
|
||||
* (this is the prefered way to update the library to avoid race conditions,
|
||||
* but you'll want to manually merge the library items in the callback
|
||||
* - which is what we're doing in Library.importLibrary()).
|
||||
*
|
||||
* If supplied promise is rejected with AbortError, we swallow it and
|
||||
* do not update the library.
|
||||
*/
|
||||
libraryItems:
|
||||
| LibraryItems
|
||||
| Promise<LibraryItems>
|
||||
| ((
|
||||
latestLibraryItems: LibraryItems,
|
||||
) => LibraryItems | Promise<LibraryItems>),
|
||||
): Promise<LibraryItems> => {
|
||||
const task = new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
await this.getLastUpdateTask();
|
||||
|
||||
if (typeof libraryItems === "function") {
|
||||
libraryItems = libraryItems(this.lastLibraryItems);
|
||||
}
|
||||
|
||||
this.lastLibraryItems = cloneLibraryItems(await libraryItems);
|
||||
|
||||
resolve(this.lastLibraryItems);
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
|
||||
this.lastLibraryItems = nextLibraryItems;
|
||||
this.libraryItemsPromise = null;
|
||||
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
status: "loaded",
|
||||
libraryItems: nextLibraryItems,
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === "AbortError") {
|
||||
console.warn("Library update aborted by user");
|
||||
return this.lastLibraryItems;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
this.updateQueue = this.updateQueue.filter((_task) => _task !== task);
|
||||
this.notifyListeners();
|
||||
});
|
||||
await this.app.props.onLibraryChange?.(
|
||||
cloneLibraryItems(nextLibraryItems),
|
||||
);
|
||||
} catch (error: any) {
|
||||
this.lastLibraryItems = prevLibraryItems;
|
||||
this.libraryItemsPromise = null;
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
status: "loaded",
|
||||
libraryItems: prevLibraryItems,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.updateQueue.push(task);
|
||||
this.notifyListeners();
|
||||
|
||||
return task;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||
arrow: true,
|
||||
freedraw: true,
|
||||
eraser: false,
|
||||
custom: true,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
@@ -198,6 +199,10 @@ const restoreElement = (
|
||||
y,
|
||||
});
|
||||
}
|
||||
case "custom":
|
||||
return restoreElementWithProperties(element, {
|
||||
customType: element.customType || "custom",
|
||||
});
|
||||
// generic elements
|
||||
case "ellipse":
|
||||
return restoreElementWithProperties(element, {});
|
||||
@@ -255,6 +260,19 @@ export const restoreAppState = (
|
||||
? localValue
|
||||
: 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 {
|
||||
...nextAppState,
|
||||
cursorButton: localAppState?.cursorButton || "up",
|
||||
@@ -262,13 +280,7 @@ export const restoreAppState = (
|
||||
penDetected:
|
||||
localAppState?.penDetected ??
|
||||
(appState.penMode ? appState.penDetected ?? false : false),
|
||||
activeTool: {
|
||||
lastActiveToolBeforeEraser: null,
|
||||
locked: nextAppState.activeTool.locked ?? false,
|
||||
type: AllowedExcalidrawActiveTools[nextAppState.activeTool.type]
|
||||
? nextAppState.activeTool.type ?? "selection"
|
||||
: "selection",
|
||||
},
|
||||
activeTool,
|
||||
// Migrates from previous version where appState.zoom was a number
|
||||
zoom:
|
||||
typeof appState.zoom === "number"
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawCustomElement,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
||||
@@ -32,13 +33,20 @@ import { Point } from "../types";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { AppState } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isCustomElement,
|
||||
isImageElement,
|
||||
} from "./typeChecks";
|
||||
import { isTextElement } from ".";
|
||||
import { isTransparent } from "../utils";
|
||||
|
||||
const isElementDraggableFromInside = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
): boolean => {
|
||||
if (isCustomElement(element)) {
|
||||
return true;
|
||||
}
|
||||
if (element.type === "arrow") {
|
||||
return false;
|
||||
}
|
||||
@@ -166,6 +174,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "ellipse":
|
||||
case "custom":
|
||||
const distance = distanceToBindableElement(args.element, args.point);
|
||||
return args.check(distance, args.threshold);
|
||||
case "freedraw": {
|
||||
@@ -199,6 +208,7 @@ export const distanceToBindableElement = (
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "custom":
|
||||
return distanceToRectangle(element, point);
|
||||
case "diamond":
|
||||
return distanceToDiamond(element, point);
|
||||
@@ -228,7 +238,8 @@ const distanceToRectangle = (
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement,
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawCustomElement,
|
||||
point: Point,
|
||||
): number => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||
@@ -504,6 +515,7 @@ export const determineFocusDistance = (
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "custom":
|
||||
return c / (hwidth * (nabs + q * mabs));
|
||||
case "diamond":
|
||||
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||
@@ -536,6 +548,7 @@ export const determineFocusPoint = (
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "custom":
|
||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||
break;
|
||||
case "ellipse":
|
||||
@@ -586,6 +599,7 @@ const getSortedElementLineIntersections = (
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "custom":
|
||||
const corners = getCorners(element);
|
||||
intersections = corners
|
||||
.flatMap((point, i) => {
|
||||
@@ -619,7 +633,8 @@ const getCorners = (
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement,
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawCustomElement,
|
||||
scale: number = 1,
|
||||
): GA.Point[] => {
|
||||
const hx = (scale * element.width) / 2;
|
||||
@@ -628,6 +643,7 @@ const getCorners = (
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "custom":
|
||||
return [
|
||||
GA.point(hx, hy),
|
||||
GA.point(hx, -hy),
|
||||
@@ -770,7 +786,8 @@ export const findFocusPointForRectangulars = (
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement,
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawCustomElement,
|
||||
// Between -1 and 1 for how far away should the focus point be relative
|
||||
// to the size of the element. Sign determines orientation.
|
||||
relativeDistance: number,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
FontFamilyValues,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawCustomElement,
|
||||
} from "../element/types";
|
||||
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
@@ -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
|
||||
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||
//
|
||||
|
||||
@@ -544,6 +544,29 @@ describe("textWysiwyg", () => {
|
||||
expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
|
||||
});
|
||||
|
||||
it("should'nt bind text to container when not double clicked on center", async () => {
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(rectangle.id);
|
||||
|
||||
// clicking somewhere on top left
|
||||
mouse.doubleClickAt(rectangle.x + 20, rectangle.y + 20);
|
||||
expect(h.elements.length).toBe(2);
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(null);
|
||||
mouse.down();
|
||||
const editor = document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toBe(null);
|
||||
});
|
||||
|
||||
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
||||
expect(h.elements.length).toBe(1);
|
||||
|
||||
|
||||
@@ -283,7 +283,14 @@ export const textWysiwyg = ({
|
||||
// using scrollHeight here since we need to calculate
|
||||
// number of lines so cannot use editable.style.height
|
||||
// as that gets updated below
|
||||
const lines = editable.scrollHeight / getApproxLineHeight(font);
|
||||
// Rounding here so that the lines calculated is more accurate in all browsers.
|
||||
// The scrollHeight and approxLineHeight differs in diff browsers
|
||||
// eg it gives 1.05 in firefox for handewritten small font due to which
|
||||
// height gets updated as lines > 1 and leads to jumping text for first line in bound container
|
||||
// hence rounding here to avoid that
|
||||
const lines = Math.round(
|
||||
editable.scrollHeight / getApproxLineHeight(font),
|
||||
);
|
||||
// auto increase height only when lines > 1 so its
|
||||
// measured correctly and vertically aligns for
|
||||
// first line as well as setting height to "auto"
|
||||
@@ -298,7 +305,6 @@ export const textWysiwyg = ({
|
||||
font,
|
||||
container!.width,
|
||||
).split("\n").length;
|
||||
|
||||
// This is browser behaviour when setting height to "auto"
|
||||
// It sets the height needed for 2 lines even if actual
|
||||
// line count is 1 as mentioned above as well
|
||||
@@ -316,8 +322,6 @@ export const textWysiwyg = ({
|
||||
}
|
||||
|
||||
editable.onkeydown = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!event.shiftKey && actionZoomIn.keyTest(event)) {
|
||||
event.preventDefault();
|
||||
app.actionManager.executeAction(actionZoomIn);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawTextContainer,
|
||||
ExcalidrawCustomElement,
|
||||
} from "./types";
|
||||
|
||||
export const isGenericElement = (
|
||||
@@ -142,3 +143,7 @@ export const isBoundToContainer = (
|
||||
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];
|
||||
}>;
|
||||
|
||||
export type ExcalidrawCustomElement = _ExcalidrawElementBase &
|
||||
Readonly<{ type: "custom"; customType: string }>;
|
||||
|
||||
export type InitializedExcalidrawImageElement = MarkNonNullable<
|
||||
ExcalidrawImageElement,
|
||||
"fileId"
|
||||
@@ -108,7 +111,8 @@ export type ExcalidrawElement =
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement;
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawCustomElement;
|
||||
|
||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||
isDeleted: boolean;
|
||||
@@ -134,7 +138,8 @@ export type ExcalidrawBindableElement =
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawImageElement;
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawCustomElement;
|
||||
|
||||
export type ExcalidrawTextContainer =
|
||||
| ExcalidrawRectangleElement
|
||||
|
||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -13,6 +13,7 @@ interface Window {
|
||||
ClipboardItem: any;
|
||||
__EXCALIDRAW_SHA__: string | undefined;
|
||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||
gtag: Function;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,18 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
#### Features
|
||||
|
||||
- Export [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) supported by Excalidraw [#5135](https://github.com/excalidraw/excalidraw/pull/5135).
|
||||
- Support [`src`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L50) for collaborators. Now onwards host can pass `src` to render the customized avatar for collaborators [#5114](https://github.com/excalidraw/excalidraw/pull/5114).
|
||||
- Support `libraryItems` argument in `initialData.libraryItems` and `updateScene({ libraryItems })` to be a Promise resolving to `LibraryItems`, and support functional update of `libraryItems` in [`updateScene({ libraryItems })`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene). [#5101](https://github.com/excalidraw/excalidraw/pull/5101).
|
||||
- Expose util [`mergeLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#mergeLibraryItems) [#5101](https://github.com/excalidraw/excalidraw/pull/5101).
|
||||
- Expose util [`exportToClipboard`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToClipboard) which allows to copy the scene contents to clipboard as `svg`, `png` or `json` [#5103](https://github.com/excalidraw/excalidraw/pull/5103).
|
||||
- Expose `window.EXCALIDRAW_EXPORT_SOURCE` which you can use to overwrite the `source` field in exported data [#5095](https://github.com/excalidraw/excalidraw/pull/5095).
|
||||
- The `exportToBlob` utility now supports the `exportEmbedScene` option when generating a png image [#5047](https://github.com/excalidraw/excalidraw/pull/5047).
|
||||
- Exported [`restoreLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreLibraryItems) API [#4995](https://github.com/excalidraw/excalidraw/pull/4995).
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Use `window.EXCALIDRAW_ASSET_PATH` for fonts when exporting to svg [#5065](https://github.com/excalidraw/excalidraw/pull/5065).
|
||||
- Library menu now properly rerenders if open when library is updated using `updateScene({ libraryItems })` [#4995](https://github.com/excalidraw/excalidraw/pull/4995).
|
||||
|
||||
#### Refactor
|
||||
|
||||
@@ -436,7 +436,7 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro
|
||||
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | The elements with which Excalidraw should be mounted. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | The App state with which Excalidraw should be mounted. |
|
||||
| `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L151) | This library items with which Excalidraw should be mounted. |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which Excalidraw should be mounted. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. |
|
||||
|
||||
```json
|
||||
@@ -512,9 +512,9 @@ You can use this function to update the scene with the sceneData. It accepts the
|
||||
| --- | --- | --- |
|
||||
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17) | The `elements` to be updated in the scene |
|
||||
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
|
||||
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L29">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
|
||||
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L35">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
|
||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | ((currentItems: [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) => [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. |
|
||||
|
||||
### `addFiles`
|
||||
|
||||
@@ -857,7 +857,7 @@ This function returns the canvas with the exported elements, appState and dimens
|
||||
|
||||
<pre>
|
||||
exportToBlob(
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L10">ExportOpts</a> & {
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
|
||||
mimeType?: string,
|
||||
quality?: number;
|
||||
})
|
||||
@@ -900,6 +900,34 @@ exportToSvg({
|
||||
|
||||
This function returns a promise which resolves to svg of the exported drawing.
|
||||
|
||||
#### `exportToClipboard`
|
||||
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToClipboard(
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
|
||||
mimeType?: string,
|
||||
quality?: number;
|
||||
type: 'png' | 'svg' |'json'
|
||||
})
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| opts | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas). |
|
||||
| mimeType | string | "image/png" | Indicates the image format, this will be used when exporting as `png`. |
|
||||
| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. This will be used when exporting as `png`. |
|
||||
| type | 'png' | 'svg' | 'json' | | This determines the format to which the scene data should be exported. |
|
||||
|
||||
**How to use**
|
||||
|
||||
```js
|
||||
import { exportToClipboard } from "@excalidraw/excalidraw-next";
|
||||
```
|
||||
|
||||
Copies the scene data in the specified format (determined by `type`) to clipboard.
|
||||
|
||||
##### Additional attributes of appState for `export\*` APIs
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
@@ -924,17 +952,21 @@ serializeAsJSON({
|
||||
|
||||
Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details).
|
||||
|
||||
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
|
||||
|
||||
#### `serializeLibraryAsJSON`
|
||||
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
serializeLibraryAsJSON({
|
||||
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L191">LibraryItems[]</a>,
|
||||
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>,
|
||||
</pre>
|
||||
|
||||
Takes the library items and returns a JSON string.
|
||||
|
||||
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
|
||||
|
||||
#### `getSceneVersion`
|
||||
|
||||
**How to use**
|
||||
@@ -1040,6 +1072,20 @@ getNonDeletedElements(elements: <a href="https://github.com/excalidraw/excalidra
|
||||
|
||||
This function returns an array of deleted elements.
|
||||
|
||||
#### `mergeLibraryItems`
|
||||
|
||||
```js
|
||||
import { mergeLibraryItems } from "@excalidraw/excalidraw-next";
|
||||
```
|
||||
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>, otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>
|
||||
</pre>
|
||||
|
||||
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
|
||||
|
||||
### Exported constants
|
||||
|
||||
#### `FONT_FAMILY`
|
||||
@@ -1077,6 +1123,16 @@ import { THEME } from "@excalidraw/excalidraw-next";
|
||||
|
||||
Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
|
||||
|
||||
### `MIME_TYPES`
|
||||
|
||||
**How to use **
|
||||
|
||||
```js
|
||||
import { MIME_TYPES } from "@excalidraw/excalidraw-next";
|
||||
```
|
||||
|
||||
[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) contains all the mime types supported by `Excalidraw`.
|
||||
|
||||
## Need help?
|
||||
|
||||
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
const dotenv = require("dotenv");
|
||||
const { readFileSync } = require("fs");
|
||||
|
||||
const pkg = require("./package.json");
|
||||
const parseEnvVariables = (filepath) => {
|
||||
return Object.entries(dotenv.parse(readFileSync(filepath))).reduce(
|
||||
const envVars = Object.entries(dotenv.parse(readFileSync(filepath))).reduce(
|
||||
(env, [key, value]) => {
|
||||
env[key] = JSON.stringify(value);
|
||||
return env;
|
||||
},
|
||||
{},
|
||||
);
|
||||
envVars.PKG_NAME = JSON.stringify(pkg.name);
|
||||
envVars.PKG_VERSION = JSON.stringify(pkg.version);
|
||||
envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
||||
return envVars;
|
||||
};
|
||||
|
||||
module.exports = { parseEnvVariables };
|
||||
|
||||
@@ -5,13 +5,37 @@ import Sidebar from "./sidebar/Sidebar";
|
||||
|
||||
import "./App.scss";
|
||||
import initialData from "./initialData";
|
||||
import { MIME_TYPES } from "../../../constants";
|
||||
|
||||
// This is so that we use the bundled excalidraw.development.js file instead
|
||||
// of the actual source code
|
||||
const {
|
||||
exportToCanvas,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
exportToClipboard,
|
||||
Excalidraw,
|
||||
MIME_TYPES,
|
||||
sceneCoordsToViewportCoords,
|
||||
} = 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 { exportToCanvas, exportToSvg, exportToBlob, Excalidraw } =
|
||||
window.ExcalidrawLib;
|
||||
const resolvablePromise = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
@@ -26,7 +50,10 @@ const resolvablePromise = () => {
|
||||
|
||||
const renderTopRightUI = () => {
|
||||
return (
|
||||
<button onClick={() => alert("This is dummy top right UI")}>
|
||||
<button
|
||||
onClick={() => alert("This is dummy top right UI")}
|
||||
style={{ height: "2.5rem" }}
|
||||
>
|
||||
{" "}
|
||||
Click me{" "}
|
||||
</button>
|
||||
@@ -44,7 +71,7 @@ const renderFooter = () => {
|
||||
|
||||
export default function App() {
|
||||
const excalidrawRef = useRef(null);
|
||||
|
||||
const excalidrawWrapperRef = useRef(null);
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
@@ -53,6 +80,7 @@ export default function App() {
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState("light");
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
|
||||
const initialStatePromiseRef = useRef({ promise: null });
|
||||
if (!initialStatePromiseRef.current.promise) {
|
||||
@@ -141,6 +169,145 @@ 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) => {
|
||||
await exportToClipboard({
|
||||
elements: excalidrawRef.current.getSceneElements(),
|
||||
appState: excalidrawRef.current.getAppState(),
|
||||
files: excalidrawRef.current.getFiles(),
|
||||
type,
|
||||
});
|
||||
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1> Excalidraw Example</h1>
|
||||
@@ -175,6 +342,7 @@ export default function App() {
|
||||
>
|
||||
Update Library
|
||||
</button>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -213,15 +381,56 @@ export default function App() {
|
||||
/>
|
||||
Switch to Dark Theme
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isCollaborating}
|
||||
onChange={() => {
|
||||
if (!isCollaborating) {
|
||||
const collaborators = new Map();
|
||||
collaborators.set("id1", {
|
||||
username: "Doremon",
|
||||
src: "doremon.png",
|
||||
});
|
||||
collaborators.set("id2", {
|
||||
username: "Excalibot",
|
||||
src: "https://avatars.githubusercontent.com/excalibot",
|
||||
});
|
||||
collaborators.set("id3", {
|
||||
username: "Pika",
|
||||
src: "pika.jpeg",
|
||||
});
|
||||
excalidrawRef.current.updateScene({ collaborators });
|
||||
} else {
|
||||
excalidrawRef.current.updateScene({
|
||||
collaborators: new Map(),
|
||||
});
|
||||
}
|
||||
setIsCollaborating(!isCollaborating);
|
||||
}}
|
||||
/>
|
||||
Show collaborators
|
||||
</label>
|
||||
<div>
|
||||
<button onClick={onCopy.bind(null, "png")}>
|
||||
Copy to Clipboard as PNG
|
||||
</button>
|
||||
<button onClick={onCopy.bind(null, "svg")}>
|
||||
Copy to Clipboard as SVG
|
||||
</button>
|
||||
<button onClick={onCopy.bind(null, "json")}>
|
||||
Copy to Clipboard as JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<div className="excalidraw-wrapper" ref={excalidrawWrapperRef}>
|
||||
<Excalidraw
|
||||
ref={excalidrawRef}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onChange={(elements, state) =>
|
||||
console.info("Elements :", elements, "State : ", state)
|
||||
}
|
||||
onPointerUpdate={(payload) => console.info(payload)}
|
||||
//onPointerUpdate={(payload) => console.info(payload)}
|
||||
onCollabButtonClick={() =>
|
||||
window.alert("You clicked on collab button")
|
||||
}
|
||||
@@ -234,6 +443,9 @@ export default function App() {
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderFooter={renderFooter}
|
||||
onLinkOpen={onLinkOpen}
|
||||
renderCustomElementWidget={renderCustomElementWidget}
|
||||
customElementsConfig={getCustomElementsConfig()}
|
||||
onElementClick={onElementClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.button-wrapper button {
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
max-width: 200px;
|
||||
max-width: 250px;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 800px;
|
||||
height: 600px;
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
@@ -47,3 +47,8 @@
|
||||
--color-primary-darkest: #e64980;
|
||||
--color-primary-light: #fcc2d7;
|
||||
}
|
||||
|
||||
.custom-element {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,32 @@ export default {
|
||||
status: "pending",
|
||||
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 },
|
||||
scrollToContent: true,
|
||||
|
||||
BIN
src/packages/excalidraw/example/public/doremon.png
Normal file
BIN
src/packages/excalidraw/example/public/doremon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
BIN
src/packages/excalidraw/example/public/pika.jpeg
Normal file
BIN
src/packages/excalidraw/example/public/pika.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -8,7 +8,10 @@ import "../../css/styles.scss";
|
||||
|
||||
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||
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 { jotaiScope, jotaiStore } from "../../jotai";
|
||||
|
||||
@@ -37,6 +40,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
autoFocus = false,
|
||||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
renderCustomElementWidget,
|
||||
onElementClick,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
@@ -47,6 +52,10 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
...canvasActions,
|
||||
},
|
||||
};
|
||||
const customElementsConfig = {} as AppProps["customElementsConfig"];
|
||||
Object.entries(props.customElementsConfig || {}).forEach(([key, value]) => {
|
||||
customElementsConfig![key] = { ...DEFAULT_CUSTOM_ELEMENT_CONFIG, ...value };
|
||||
});
|
||||
|
||||
if (canvasActions?.export) {
|
||||
UIOptions.canvasActions.export.saveFileToDisk =
|
||||
@@ -100,6 +109,9 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
autoFocus={autoFocus}
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
renderCustomElementWidget={renderCustomElementWidget}
|
||||
customElementsConfig={customElementsConfig}
|
||||
onElementClick={onElementClick}
|
||||
/>
|
||||
</Provider>
|
||||
</InitializeApp>
|
||||
@@ -197,14 +209,17 @@ export {
|
||||
loadLibraryFromBlob,
|
||||
loadFromBlob,
|
||||
getFreeDrawSvgPath,
|
||||
exportToClipboard,
|
||||
mergeLibraryItems,
|
||||
} from "../../packages/utils";
|
||||
export { isLinearElement } from "../../element/typeChecks";
|
||||
export { debounce } from "../../utils";
|
||||
|
||||
export { FONT_FAMILY, THEME } from "../../constants";
|
||||
export { FONT_FAMILY, THEME, MIME_TYPES } from "../../constants";
|
||||
|
||||
export {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
bumpVersion,
|
||||
} from "../../element/mutateElement";
|
||||
|
||||
export { sceneCoordsToViewportCoords } from "../../utils";
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
"@babel/core": "7.17.0",
|
||||
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
||||
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
||||
"@babel/plugin-transform-runtime": "7.16.8",
|
||||
"@babel/plugin-transform-runtime": "7.17.10",
|
||||
"@babel/plugin-transform-typescript": "7.16.1",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"autoprefixer": "10.4.2",
|
||||
"autoprefixer": "10.4.5",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -63,7 +63,7 @@
|
||||
"terser-webpack-plugin": "5.3.1",
|
||||
"ts-loader": "9.2.8",
|
||||
"typescript": "4.5.4",
|
||||
"webpack": "5.65.0",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-bundle-analyzer": "4.5.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.7.4",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ENV } from "../../constants";
|
||||
import pkg from "./package.json";
|
||||
if (process.env.NODE_ENV !== ENV.TEST) {
|
||||
/* eslint-disable */
|
||||
/* global __webpack_public_path__:writable */
|
||||
__webpack_public_path__ =
|
||||
window.EXCALIDRAW_ASSET_PATH ||
|
||||
`https://unpkg.com/${pkg.name}@${pkg.version}/dist/`;
|
||||
`https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const devServerConfig = {
|
||||
},
|
||||
// Server Configuration options
|
||||
devServer: {
|
||||
port: 3001,
|
||||
//port: 3001,
|
||||
host: "localhost",
|
||||
hot: true,
|
||||
compress: true,
|
||||
|
||||
@@ -862,10 +862,10 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.16.7"
|
||||
|
||||
"@babel/plugin-transform-runtime@7.16.8":
|
||||
version "7.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.8.tgz#3339368701103edae708f0fba9e4bfb70a3e5872"
|
||||
integrity sha512-6Kg2XHPFnIarNweZxmzbgYnnWsXxkx9WQUVk2sksBRL80lBC1RAQV3wQagWxdCHiYHqPN+oenwNIuttlYgIbQQ==
|
||||
"@babel/plugin-transform-runtime@7.17.10":
|
||||
version "7.17.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz#b89d821c55d61b5e3d3c3d1d636d8d5a81040ae1"
|
||||
integrity sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.16.7"
|
||||
"@babel/helper-plugin-utils" "^7.16.7"
|
||||
@@ -1174,10 +1174,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/eslint-scope@^3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86"
|
||||
integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==
|
||||
"@types/eslint-scope@^3.7.3":
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
|
||||
integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==
|
||||
dependencies:
|
||||
"@types/eslint" "*"
|
||||
"@types/estree" "*"
|
||||
@@ -1190,10 +1190,10 @@
|
||||
"@types/estree" "*"
|
||||
"@types/json-schema" "*"
|
||||
|
||||
"@types/estree@*", "@types/estree@^0.0.50":
|
||||
version "0.0.50"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
|
||||
integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
|
||||
"@types/estree@*", "@types/estree@^0.0.51":
|
||||
version "0.0.51"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
||||
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
||||
|
||||
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
|
||||
version "4.17.27"
|
||||
@@ -1571,20 +1571,20 @@ array-union@^2.1.0:
|
||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||
|
||||
async@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
autoprefixer@10.4.2:
|
||||
version "10.4.2"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
|
||||
integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==
|
||||
autoprefixer@10.4.5:
|
||||
version "10.4.5"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.5.tgz#662193c744094b53d3637f39be477e07bd904998"
|
||||
integrity sha512-Fvd8yCoA7lNX/OUllvS+aS1I7WRBclGXsepbvT8ZaPgrH24rgXpZzF0/6Hh3ZEkwg+0AES/Osd196VZmYoEFtw==
|
||||
dependencies:
|
||||
browserslist "^4.19.1"
|
||||
caniuse-lite "^1.0.30001297"
|
||||
fraction.js "^4.1.2"
|
||||
browserslist "^4.20.2"
|
||||
caniuse-lite "^1.0.30001332"
|
||||
fraction.js "^4.2.0"
|
||||
normalize-range "^0.1.2"
|
||||
picocolors "^1.0.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
@@ -1800,15 +1800,15 @@ braces@^3.0.1, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.17.6, browserslist@^4.19.1:
|
||||
version "4.19.1"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
|
||||
integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==
|
||||
browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.17.6, browserslist@^4.19.1, browserslist@^4.20.2:
|
||||
version "4.20.2"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88"
|
||||
integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001286"
|
||||
electron-to-chromium "^1.4.17"
|
||||
caniuse-lite "^1.0.30001317"
|
||||
electron-to-chromium "^1.4.84"
|
||||
escalade "^3.1.1"
|
||||
node-releases "^2.0.1"
|
||||
node-releases "^2.0.2"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
@@ -1844,10 +1844,10 @@ callsites@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297:
|
||||
version "1.0.30001298"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz#0e690039f62e91c3ea581673d716890512e7ec52"
|
||||
integrity sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==
|
||||
caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001332:
|
||||
version "1.0.30001332"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd"
|
||||
integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==
|
||||
|
||||
chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
@@ -2211,10 +2211,10 @@ ee-first@1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
electron-to-chromium@^1.4.17:
|
||||
version "1.4.38"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.38.tgz#10ea58d73d36b13e78d5024f3b74a352d3958d01"
|
||||
integrity sha512-WhHt3sZazKj0KK/UpgsbGQnUUoFeAHVishzHFExMxagpZgjiGYSC9S0ZlbhCfSH2L2i+2A1yyqOIliTctMx7KQ==
|
||||
electron-to-chromium@^1.4.84:
|
||||
version "1.4.118"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.118.tgz#2d917c71712dac9652cc01af46c7d0bd51552974"
|
||||
integrity sha512-maZIKjnYDvF7Fs35nvVcyr44UcKNwybr93Oba2n3HkKDFAtk0svERkLN/HyczJDS3Fo4wU9th9fUQd09ZLtj1w==
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -2226,10 +2226,10 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||
|
||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0"
|
||||
integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==
|
||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.2:
|
||||
version "5.9.2"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9"
|
||||
integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
@@ -2449,10 +2449,10 @@ forwarded@0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||
|
||||
fraction.js@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8"
|
||||
integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==
|
||||
fraction.js@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
||||
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
@@ -2544,10 +2544,10 @@ globby@^11.0.1:
|
||||
merge2 "^1.3.0"
|
||||
slash "^3.0.0"
|
||||
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
|
||||
version "4.2.10"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -3044,24 +3044,12 @@ micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.2.3"
|
||||
|
||||
mime-db@1.45.0:
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
|
||||
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
|
||||
|
||||
mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
|
||||
version "1.51.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
|
||||
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
|
||||
|
||||
mime-types@^2.1.27:
|
||||
version "2.1.28"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
|
||||
integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
|
||||
dependencies:
|
||||
mime-db "1.45.0"
|
||||
|
||||
mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24:
|
||||
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24:
|
||||
version "2.1.34"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
|
||||
integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
|
||||
@@ -3103,9 +3091,9 @@ minimatch@^3.0.4:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@^0.5.5:
|
||||
version "0.5.5"
|
||||
@@ -3158,14 +3146,14 @@ neo-async@^2.6.2:
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
node-forge@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c"
|
||||
integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
|
||||
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
|
||||
|
||||
node-releases@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5"
|
||||
integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
|
||||
node-releases@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
|
||||
integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -4180,18 +4168,18 @@ webpack-merge@5.8.0, webpack-merge@^5.7.3:
|
||||
clone-deep "^4.0.1"
|
||||
wildcard "^2.0.0"
|
||||
|
||||
webpack-sources@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260"
|
||||
integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==
|
||||
webpack-sources@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
webpack@5.65.0:
|
||||
version "5.65.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.65.0.tgz#ed2891d9145ba1f0d318e4ea4f89c3fa18e6f9be"
|
||||
integrity sha512-Q5or2o6EKs7+oKmJo7LaqZaMOlDWQse9Tm5l1WAfU/ujLGN5Pb0SqGeVkN/4bpPmEqEP5RnVhiqsOtWtUVwGRw==
|
||||
webpack@5.72.0:
|
||||
version "5.72.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.72.0.tgz#f8bc40d9c6bb489a4b7a8a685101d6022b8b6e28"
|
||||
integrity sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.0"
|
||||
"@types/estree" "^0.0.50"
|
||||
"@types/eslint-scope" "^3.7.3"
|
||||
"@types/estree" "^0.0.51"
|
||||
"@webassemblyjs/ast" "1.11.1"
|
||||
"@webassemblyjs/wasm-edit" "1.11.1"
|
||||
"@webassemblyjs/wasm-parser" "1.11.1"
|
||||
@@ -4199,12 +4187,12 @@ webpack@5.65.0:
|
||||
acorn-import-assertions "^1.7.6"
|
||||
browserslist "^4.14.5"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.8.3"
|
||||
enhanced-resolve "^5.9.2"
|
||||
es-module-lexer "^0.9.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.2.4"
|
||||
graceful-fs "^4.2.9"
|
||||
json-parse-better-errors "^1.0.2"
|
||||
loader-runner "^4.2.0"
|
||||
mime-types "^2.1.27"
|
||||
@@ -4213,7 +4201,7 @@ webpack@5.65.0:
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.1.3"
|
||||
watchpack "^2.3.1"
|
||||
webpack-sources "^3.2.2"
|
||||
webpack-sources "^3.2.3"
|
||||
|
||||
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
|
||||
version "0.7.4"
|
||||
|
||||
@@ -10,6 +10,11 @@ import { restore } from "../data/restore";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { encodePngMetadata } from "../data/image";
|
||||
import { serializeAsJSON } from "../data/json";
|
||||
import {
|
||||
copyBlobToClipboardAsPng,
|
||||
copyTextToSystemClipboard,
|
||||
copyToClipboard,
|
||||
} from "../clipboard";
|
||||
|
||||
type ExportOpts = {
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[];
|
||||
@@ -81,7 +86,7 @@ export const exportToBlob = async (
|
||||
mimeType?: string;
|
||||
quality?: number;
|
||||
},
|
||||
): Promise<Blob | null> => {
|
||||
): Promise<Blob> => {
|
||||
let { mimeType = MIME_TYPES.png, quality } = opts;
|
||||
|
||||
if (mimeType === MIME_TYPES.png && typeof quality === "number") {
|
||||
@@ -107,9 +112,12 @@ export const exportToBlob = async (
|
||||
|
||||
quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
async (blob: Blob | null) => {
|
||||
async (blob) => {
|
||||
if (!blob) {
|
||||
return reject(new Error("couldn't export to blob"));
|
||||
}
|
||||
if (
|
||||
blob &&
|
||||
mimeType === MIME_TYPES.png &&
|
||||
@@ -156,6 +164,34 @@ export const exportToSvg = async ({
|
||||
);
|
||||
};
|
||||
|
||||
export const exportToClipboard = async (
|
||||
opts: ExportOpts & {
|
||||
mimeType?: string;
|
||||
quality?: number;
|
||||
type: "png" | "svg" | "json";
|
||||
},
|
||||
) => {
|
||||
if (opts.type === "svg") {
|
||||
const svg = await exportToSvg(opts);
|
||||
await copyTextToSystemClipboard(svg.outerHTML);
|
||||
} else if (opts.type === "png") {
|
||||
await copyBlobToClipboardAsPng(exportToBlob(opts));
|
||||
} else if (opts.type === "json") {
|
||||
const appState = {
|
||||
offsetTop: 0,
|
||||
offsetLeft: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
...getDefaultAppState(),
|
||||
...opts.appState,
|
||||
};
|
||||
await copyToClipboard(opts.elements, appState, opts.files);
|
||||
} else {
|
||||
throw new Error("Invalid export type");
|
||||
}
|
||||
};
|
||||
|
||||
export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json";
|
||||
export { loadFromBlob, loadLibraryFromBlob } from "../data/blob";
|
||||
export { getFreeDrawSvgPath } from "../renderer/renderElement";
|
||||
export { mergeLibraryItems } from "../data/library";
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@babel/plugin-transform-typescript": "7.16.1",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
|
||||
@@ -1320,13 +1320,13 @@ babel-helper-get-function-arity@^6.24.1:
|
||||
babel-runtime "^6.22.0"
|
||||
babel-types "^6.24.1"
|
||||
|
||||
babel-loader@8.2.3:
|
||||
version "8.2.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d"
|
||||
integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==
|
||||
babel-loader@8.2.5:
|
||||
version "8.2.5"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e"
|
||||
integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==
|
||||
dependencies:
|
||||
find-cache-dir "^3.3.1"
|
||||
loader-utils "^1.4.0"
|
||||
loader-utils "^2.0.0"
|
||||
make-dir "^3.1.0"
|
||||
schema-utils "^2.6.5"
|
||||
|
||||
@@ -2002,13 +2002,6 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json5@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
json5@^2.1.2:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
|
||||
@@ -2031,15 +2024,6 @@ loader-runner@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
|
||||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||
|
||||
loader-utils@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
|
||||
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
json5 "^1.0.1"
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
|
||||
@@ -2122,10 +2106,10 @@ mimic-fn@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
minimist@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
NonDeletedExcalidrawElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawCustomElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
isTextElement,
|
||||
@@ -189,6 +190,9 @@ const drawImagePlaceholder = (
|
||||
);
|
||||
};
|
||||
|
||||
const customElementImgCache: {
|
||||
[key: ExcalidrawCustomElement["customType"]]: HTMLImageElement;
|
||||
} = {};
|
||||
const drawElementOnCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
@@ -250,6 +254,54 @@ const drawElementOnCanvas = (
|
||||
}
|
||||
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: {
|
||||
if (isTextElement(element)) {
|
||||
const rtl = isRTL(element.text);
|
||||
@@ -779,7 +831,8 @@ export const renderElement = (
|
||||
case "line":
|
||||
case "arrow":
|
||||
case "image":
|
||||
case "text": {
|
||||
case "text":
|
||||
case "custom": {
|
||||
generateElementShape(element, generator);
|
||||
if (renderConfig.isExporting) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
@@ -809,6 +862,7 @@ export const renderElement = (
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// @ts-ignore
|
||||
throw new Error(`Unimplemented type ${element.type}`);
|
||||
|
||||
@@ -190,7 +190,6 @@ export const renderScene = (
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false };
|
||||
}
|
||||
|
||||
const {
|
||||
renderScrollbars = true,
|
||||
renderSelection = true,
|
||||
@@ -305,24 +304,32 @@ export const renderScene = (
|
||||
!appState.editingLinearElement
|
||||
) {
|
||||
const selections = elements.reduce((acc, element) => {
|
||||
const isCustom = element.type === "custom";
|
||||
let config;
|
||||
const selectionColors = [];
|
||||
// local user
|
||||
if (
|
||||
appState.selectedElementIds[element.id] &&
|
||||
!isSelectedViaGroup(appState, element)
|
||||
) {
|
||||
selectionColors.push(oc.black);
|
||||
|
||||
if (element.type === "custom") {
|
||||
config = renderConfig.customElementsConfig?.[element.customType];
|
||||
}
|
||||
// remote users
|
||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||
selectionColors.push(
|
||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||
(socketId) => {
|
||||
const { background } = getClientColors(socketId, appState);
|
||||
return background;
|
||||
},
|
||||
),
|
||||
);
|
||||
if (!isCustom || (isCustom && config && config.transformHandles)) {
|
||||
// local user
|
||||
if (
|
||||
appState.selectedElementIds[element.id] &&
|
||||
!isSelectedViaGroup(appState, element)
|
||||
) {
|
||||
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) {
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
@@ -352,7 +359,6 @@ export const renderScene = (
|
||||
selectionColors: [oc.black],
|
||||
});
|
||||
};
|
||||
|
||||
for (const groupId of getSelectedGroupIds(appState)) {
|
||||
// TODO: support multiplayer selected group IDs
|
||||
addSelectionForGroupId(groupId);
|
||||
@@ -372,19 +378,33 @@ export const renderScene = (
|
||||
context.save();
|
||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||
if (locallySelectedElements.length === 1) {
|
||||
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,
|
||||
let showTransformHandles = true;
|
||||
if (locallySelectedElements[0].type === "custom") {
|
||||
const config =
|
||||
renderConfig.customElementsConfig?.[
|
||||
locallySelectedElements[0].customType
|
||||
];
|
||||
|
||||
if (!config || !config.transformHandles) {
|
||||
showTransformHandles = false;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
||||
@@ -573,6 +593,7 @@ const renderTransformHandles = (
|
||||
renderConfig: RenderConfig,
|
||||
transformHandles: TransformHandles,
|
||||
angle: number,
|
||||
name?: string,
|
||||
): void => {
|
||||
Object.keys(transformHandles).forEach((key) => {
|
||||
const transformHandle = transformHandles[key as TransformHandleType];
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
} from "../element/types";
|
||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import App from "../components/App";
|
||||
import { isCustomElement } from "../element/typeChecks";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
@@ -26,7 +28,11 @@ class Scene {
|
||||
|
||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
||||
private static sceneMapById = new Map<string, Scene>();
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
||||
if (isIdKey(elementKey)) {
|
||||
this.sceneMapById.set(elementKey, scene);
|
||||
@@ -91,12 +97,28 @@ class Scene {
|
||||
}
|
||||
|
||||
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
||||
this.elements = nextElements;
|
||||
this.elements = [];
|
||||
const elements: ExcalidrawElement[] = [];
|
||||
this.elementsMap.clear();
|
||||
const elementsToBeStackedOnTop: ExcalidrawElement[] = [];
|
||||
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);
|
||||
Scene.mapElementToScene(element, this);
|
||||
});
|
||||
elementsToBeStackedOnTop.forEach((ele) => elements.push(ele));
|
||||
this.elements = elements;
|
||||
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
this.informMutation();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
|
||||
import { getElementAbsoluteCoords } from "../element";
|
||||
import { isTextBindableContainer } from "../element/typeChecks";
|
||||
import { AppState } from "../types";
|
||||
|
||||
export const hasBackground = (type: string) =>
|
||||
type === "rectangle" ||
|
||||
@@ -31,7 +32,7 @@ export const hasStrokeStyle = (type: string) =>
|
||||
type === "arrow" ||
|
||||
type === "line";
|
||||
|
||||
export const canChangeSharpness = (type: string) =>
|
||||
export const canChangeSharpness = (type: AppState["activeTool"]["type"]) =>
|
||||
type === "rectangle" ||
|
||||
type === "arrow" ||
|
||||
type === "line" ||
|
||||
|
||||
@@ -115,6 +115,19 @@ export const exportToSvg = async (
|
||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||
}
|
||||
|
||||
let assetPath = "https://excalidraw.com/";
|
||||
|
||||
// Asset path needs to be determined only when using package
|
||||
if (process.env.IS_EXCALIDRAW_NPM_PACKAGE) {
|
||||
assetPath =
|
||||
window.EXCALIDRAW_ASSET_PATH ||
|
||||
`https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}`;
|
||||
|
||||
if (assetPath?.startsWith("/")) {
|
||||
assetPath = assetPath.replace("/", `${window.location.origin}/`);
|
||||
}
|
||||
assetPath = `${assetPath}/dist/excalidraw-assets/`;
|
||||
}
|
||||
svgRoot.innerHTML = `
|
||||
${SVG_EXPORT_TAG}
|
||||
${metadata}
|
||||
@@ -122,16 +135,15 @@ export const exportToSvg = async (
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://excalidraw.com/Virgil.woff2");
|
||||
src: url("${assetPath}Virgil.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
||||
src: url("${assetPath}Cascadia.woff2");
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
`;
|
||||
|
||||
// render background rect
|
||||
if (appState.exportBackground && viewBackgroundColor) {
|
||||
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
|
||||
|
||||
@@ -3,20 +3,25 @@ import {
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||
import { AppState } from "../types";
|
||||
import { AppProps, AppState } from "../types";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
|
||||
export const getElementsWithinSelection = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
selection: NonDeletedExcalidrawElement,
|
||||
customElementConfig: AppProps["customElementsConfig"],
|
||||
) => {
|
||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||
getElementAbsoluteCoords(selection);
|
||||
return elements.filter((element) => {
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getElementBounds(element);
|
||||
|
||||
const isCustom = element.type === "custom";
|
||||
const allowSelection = isCustom
|
||||
? customElementConfig?.[element.customType]?.transformHandles
|
||||
: true;
|
||||
return (
|
||||
allowSelection &&
|
||||
element.locked === false &&
|
||||
element.type !== "selection" &&
|
||||
!isBoundToContainer(element) &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExcalidrawTextElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { AppClassProperties, AppProps, AppState } from "../types";
|
||||
|
||||
export type RenderConfig = {
|
||||
// AppState values
|
||||
@@ -27,6 +27,7 @@ export type RenderConfig = {
|
||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||
CSS filters), and we disable render optimizations for best output */
|
||||
isExporting: boolean;
|
||||
customElementsConfig?: AppProps["customElementsConfig"];
|
||||
};
|
||||
|
||||
export type SceneScroll = {
|
||||
|
||||
@@ -14,12 +14,12 @@ describe("library", () => {
|
||||
});
|
||||
|
||||
it("import library via drag&drop", async () => {
|
||||
expect(await h.app.library.loadLibrary()).toEqual([]);
|
||||
expect(await h.app.library.getLatestLibrary()).toEqual([]);
|
||||
await API.drop(
|
||||
await API.loadFile("./fixtures/fixture_library.excalidrawlib"),
|
||||
);
|
||||
await waitFor(async () => {
|
||||
expect(await h.app.library.loadLibrary()).toEqual([
|
||||
expect(await h.app.library.getLatestLibrary()).toEqual([
|
||||
{
|
||||
status: "unpublished",
|
||||
elements: [expect.objectContaining({ id: "A" })],
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../fixtures/elementFixture";
|
||||
|
||||
describe("exportToSvg", () => {
|
||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||
const ELEMENT_HEIGHT = 100;
|
||||
const ELEMENT_WIDTH = 100;
|
||||
const ELEMENTS = [
|
||||
|
||||
52
src/types.ts
52
src/types.ts
@@ -45,6 +45,9 @@ export type Collaborator = {
|
||||
background: string;
|
||||
stroke: string;
|
||||
};
|
||||
// The url of the collaborator's avatar, defaults to username intials
|
||||
// if not present
|
||||
src?: string;
|
||||
};
|
||||
|
||||
export type DataURL = string & { _brand: "DataURL" };
|
||||
@@ -63,6 +66,14 @@ export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
|
||||
|
||||
export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
||||
|
||||
export type LastActiveToolBeforeEraser =
|
||||
| typeof SHAPES[number]["value"]
|
||||
| {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
}
|
||||
| null;
|
||||
|
||||
export type AppState = {
|
||||
isLoading: boolean;
|
||||
errorMessage: string | null;
|
||||
@@ -77,11 +88,18 @@ export type AppState = {
|
||||
// (e.g. text element when typing into the input)
|
||||
editingElement: NonDeletedExcalidrawElement | null;
|
||||
editingLinearElement: LinearElementEditor | null;
|
||||
activeTool: {
|
||||
type: typeof SHAPES[number]["value"] | "eraser";
|
||||
lastActiveToolBeforeEraser: typeof SHAPES[number]["value"] | null;
|
||||
locked: boolean;
|
||||
};
|
||||
activeTool:
|
||||
| {
|
||||
type: typeof SHAPES[number]["value"] | "eraser";
|
||||
lastActiveToolBeforeEraser: LastActiveToolBeforeEraser;
|
||||
locked: boolean;
|
||||
}
|
||||
| {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
lastActiveToolBeforeEraser: LastActiveToolBeforeEraser;
|
||||
locked: boolean;
|
||||
};
|
||||
penMode: boolean;
|
||||
penDetected: boolean;
|
||||
exportBackground: boolean;
|
||||
@@ -209,6 +227,22 @@ export type ExcalidrawAPIRefValue =
|
||||
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<
|
||||
ImportedDataState,
|
||||
{
|
||||
@@ -268,6 +302,12 @@ export interface ExcalidrawProps {
|
||||
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
|
||||
}>,
|
||||
) => void;
|
||||
renderCustomElementWidget?: (appState: AppState) => void;
|
||||
customElementsConfig?: Record<string, CustomElementConfig>;
|
||||
onElementClick?: (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
@@ -321,6 +361,7 @@ export type AppProps = ExcalidrawProps & {
|
||||
detectScroll: boolean;
|
||||
handleKeyboardGlobally: boolean;
|
||||
isCollaborating: boolean;
|
||||
customElementsConfig: Required<CustomElementConfig>[] | undefined;
|
||||
};
|
||||
|
||||
/** A subset of App class properties that we need to use elsewhere
|
||||
@@ -428,6 +469,7 @@ export type ExcalidrawImperativeAPI = {
|
||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
||||
ready: true;
|
||||
id: string;
|
||||
setCustomType: InstanceType<typeof App>["setCustomType"];
|
||||
};
|
||||
|
||||
export type DeviceType = {
|
||||
|
||||
@@ -575,6 +575,9 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||
export const isTestEnv = () =>
|
||||
typeof process !== "undefined" && process.env?.NODE_ENV === "test";
|
||||
|
||||
export const isProdEnv = () =>
|
||||
typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
||||
|
||||
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
|
||||
return new CustomEvent(name, {
|
||||
detail: {
|
||||
|
||||
28
yarn.lock
28
yarn.lock
@@ -2971,9 +2971,9 @@ async-limiter@~1.0.0:
|
||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||
|
||||
async@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
@@ -3394,10 +3394,10 @@ brorand@^1.0.1, brorand@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz"
|
||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||
|
||||
browser-fs-access@0.24.1:
|
||||
version "0.24.1"
|
||||
resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.24.1.tgz#29b915fdcd2ef0972ebabc2b7685c6893d288a72"
|
||||
integrity sha512-fRcwhfNej0h2Jy+Uodxjbc5PQvNkZyG9fXu3S6Mcv0kigKath5sL54GXfMtOay/A/ULMa956eQZ9lMWVPiLtMA==
|
||||
browser-fs-access@0.29.1:
|
||||
version "0.29.1"
|
||||
resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.29.1.tgz#8a9794c73cf86b9aec74201829999c597128379c"
|
||||
integrity sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==
|
||||
|
||||
browser-process-hrtime@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -8369,9 +8369,9 @@ minimatch@3.0.4, minimatch@^3.0.4:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
minipass-collect@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -8510,10 +8510,10 @@ nan@^2.12.1:
|
||||
resolved "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz"
|
||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||
|
||||
nanoid@3.1.32, nanoid@^3.1.20:
|
||||
version "3.1.32"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.32.tgz#8f96069e6239cc0a9ae8c0d3b41a3b4933a88c0a"
|
||||
integrity sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==
|
||||
nanoid@3.3.3, nanoid@^3.1.20:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
|
||||
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
|
||||
Reference in New Issue
Block a user