|
|
@ -1,12 +1,13 @@
|
|
|
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
|
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
import InitialData from "./initialData";
|
|
|
|
|
|
|
|
import Sidebar from "./sidebar/Sidebar";
|
|
|
|
import Sidebar from "./sidebar/Sidebar";
|
|
|
|
|
|
|
|
|
|
|
|
import "./App.scss";
|
|
|
|
import "./App.scss";
|
|
|
|
import initialData from "./initialData";
|
|
|
|
import initialData from "./initialData";
|
|
|
|
import { nanoid } from "nanoid";
|
|
|
|
import { nanoid } from "nanoid";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
|
|
|
|
resolvablePromise,
|
|
|
|
|
|
|
|
ResolvablePromise,
|
|
|
|
withBatchedUpdates,
|
|
|
|
withBatchedUpdates,
|
|
|
|
withBatchedUpdatesThrottled,
|
|
|
|
withBatchedUpdatesThrottled,
|
|
|
|
} from "../../../utils";
|
|
|
|
} from "../../../utils";
|
|
|
@ -14,7 +15,42 @@ import { EVENT } from "../../../constants";
|
|
|
|
import { distance2d } from "../../../math";
|
|
|
|
import { distance2d } from "../../../math";
|
|
|
|
import { fileOpen } from "../../../data/filesystem";
|
|
|
|
import { fileOpen } from "../../../data/filesystem";
|
|
|
|
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
|
|
|
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
|
|
AppState,
|
|
|
|
|
|
|
|
BinaryFileData,
|
|
|
|
|
|
|
|
ExcalidrawImperativeAPI,
|
|
|
|
|
|
|
|
ExcalidrawInitialDataState,
|
|
|
|
|
|
|
|
Gesture,
|
|
|
|
|
|
|
|
LibraryItems,
|
|
|
|
|
|
|
|
PointerDownState as ExcalidrawPointerDownState,
|
|
|
|
|
|
|
|
} from "../../../types";
|
|
|
|
|
|
|
|
import { ExcalidrawElement } from "../../../element/types";
|
|
|
|
|
|
|
|
import { ImportedLibraryData } from "../../../data/types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
|
|
|
interface Window {
|
|
|
|
|
|
|
|
ExcalidrawLib: any;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type Comment = {
|
|
|
|
|
|
|
|
x: number;
|
|
|
|
|
|
|
|
y: number;
|
|
|
|
|
|
|
|
value: string;
|
|
|
|
|
|
|
|
id?: string;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type PointerDownState = {
|
|
|
|
|
|
|
|
x: number;
|
|
|
|
|
|
|
|
y: number;
|
|
|
|
|
|
|
|
hitElement: Comment;
|
|
|
|
|
|
|
|
onMove: any;
|
|
|
|
|
|
|
|
onUp: any;
|
|
|
|
|
|
|
|
hitElementOffsets: {
|
|
|
|
|
|
|
|
x: number;
|
|
|
|
|
|
|
|
y: number;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
};
|
|
|
|
// This is so that we use the bundled excalidraw.development.js file instead
|
|
|
|
// This is so that we use the bundled excalidraw.development.js file instead
|
|
|
|
// of the actual source code
|
|
|
|
// of the actual source code
|
|
|
|
|
|
|
|
|
|
|
@ -28,6 +64,7 @@ const {
|
|
|
|
MIME_TYPES,
|
|
|
|
MIME_TYPES,
|
|
|
|
sceneCoordsToViewportCoords,
|
|
|
|
sceneCoordsToViewportCoords,
|
|
|
|
viewportCoordsToSceneCoords,
|
|
|
|
viewportCoordsToSceneCoords,
|
|
|
|
|
|
|
|
restoreElements,
|
|
|
|
} = window.ExcalidrawLib;
|
|
|
|
} = window.ExcalidrawLib;
|
|
|
|
|
|
|
|
|
|
|
|
const COMMENT_SVG = (
|
|
|
|
const COMMENT_SVG = (
|
|
|
@ -41,7 +78,7 @@ const COMMENT_SVG = (
|
|
|
|
stroke-width="2"
|
|
|
|
stroke-width="2"
|
|
|
|
stroke-linecap="round"
|
|
|
|
stroke-linecap="round"
|
|
|
|
stroke-linejoin="round"
|
|
|
|
stroke-linejoin="round"
|
|
|
|
class="feather feather-message-circle"
|
|
|
|
className="feather feather-message-circle"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
|
|
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
|
|
|
</svg>
|
|
|
|
</svg>
|
|
|
@ -50,18 +87,6 @@ const COMMENT_ICON_DIMENSION = 32;
|
|
|
|
const COMMENT_INPUT_HEIGHT = 50;
|
|
|
|
const COMMENT_INPUT_HEIGHT = 50;
|
|
|
|
const COMMENT_INPUT_WIDTH = 150;
|
|
|
|
const COMMENT_INPUT_WIDTH = 150;
|
|
|
|
|
|
|
|
|
|
|
|
const resolvablePromise = () => {
|
|
|
|
|
|
|
|
let resolve;
|
|
|
|
|
|
|
|
let reject;
|
|
|
|
|
|
|
|
const promise = new Promise((_resolve, _reject) => {
|
|
|
|
|
|
|
|
resolve = _resolve;
|
|
|
|
|
|
|
|
reject = _reject;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
promise.resolve = resolve;
|
|
|
|
|
|
|
|
promise.reject = reject;
|
|
|
|
|
|
|
|
return promise;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderTopRightUI = () => {
|
|
|
|
const renderTopRightUI = () => {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
<button
|
|
|
@ -75,25 +100,31 @@ const renderTopRightUI = () => {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
export default function App() {
|
|
|
|
const appRef = useRef(null);
|
|
|
|
const appRef = useRef<any>(null);
|
|
|
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
|
|
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
|
|
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
|
|
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
|
|
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
|
|
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
|
|
|
const [blobUrl, setBlobUrl] = useState(null);
|
|
|
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
|
|
|
const [canvasUrl, setCanvasUrl] = useState(null);
|
|
|
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
|
|
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
|
|
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
|
|
|
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
|
|
|
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
|
|
|
const [theme, setTheme] = useState("light");
|
|
|
|
const [theme, setTheme] = useState("light");
|
|
|
|
const [isCollaborating, setIsCollaborating] = useState(false);
|
|
|
|
const [isCollaborating, setIsCollaborating] = useState(false);
|
|
|
|
const [commentIcons, setCommentIcons] = useState({});
|
|
|
|
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
|
|
|
const [comment, setComment] = useState(null);
|
|
|
|
{},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
const [comment, setComment] = useState<Comment | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const initialStatePromiseRef = useRef({ promise: null });
|
|
|
|
const initialStatePromiseRef = useRef<{
|
|
|
|
|
|
|
|
promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
|
|
|
|
|
|
|
|
}>({ promise: null! });
|
|
|
|
if (!initialStatePromiseRef.current.promise) {
|
|
|
|
if (!initialStatePromiseRef.current.promise) {
|
|
|
|
initialStatePromiseRef.current.promise = resolvablePromise();
|
|
|
|
initialStatePromiseRef.current.promise =
|
|
|
|
|
|
|
|
resolvablePromise<ExcalidrawInitialDataState | null>();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
|
|
|
const [excalidrawAPI, setExcalidrawAPI] =
|
|
|
|
|
|
|
|
useState<ExcalidrawImperativeAPI | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
useHandleLibrary({ excalidrawAPI });
|
|
|
|
useHandleLibrary({ excalidrawAPI });
|
|
|
|
|
|
|
|
|
|
|
@ -108,16 +139,17 @@ export default function App() {
|
|
|
|
reader.readAsDataURL(imageData);
|
|
|
|
reader.readAsDataURL(imageData);
|
|
|
|
|
|
|
|
|
|
|
|
reader.onload = function () {
|
|
|
|
reader.onload = function () {
|
|
|
|
const imagesArray = [
|
|
|
|
const imagesArray: BinaryFileData[] = [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: "rocket",
|
|
|
|
id: "rocket" as BinaryFileData["id"],
|
|
|
|
dataURL: reader.result,
|
|
|
|
dataURL: reader.result as BinaryFileData["dataURL"],
|
|
|
|
mimeType: MIME_TYPES.jpg,
|
|
|
|
mimeType: MIME_TYPES.jpg,
|
|
|
|
created: 1644915140367,
|
|
|
|
created: 1644915140367,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
initialStatePromiseRef.current.promise.resolve(InitialData);
|
|
|
|
//@ts-ignore
|
|
|
|
|
|
|
|
initialStatePromiseRef.current.promise.resolve(initialData);
|
|
|
|
excalidrawAPI.addFiles(imagesArray);
|
|
|
|
excalidrawAPI.addFiles(imagesArray);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -131,7 +163,7 @@ export default function App() {
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="custom-element"
|
|
|
|
className="custom-element"
|
|
|
|
onClick={() => {
|
|
|
|
onClick={() => {
|
|
|
|
excalidrawAPI.setActiveTool({
|
|
|
|
excalidrawAPI?.setActiveTool({
|
|
|
|
type: "custom",
|
|
|
|
type: "custom",
|
|
|
|
customType: "comment",
|
|
|
|
customType: "comment",
|
|
|
|
});
|
|
|
|
});
|
|
|
@ -151,7 +183,7 @@ export default function App() {
|
|
|
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
|
|
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
|
|
|
</svg>`,
|
|
|
|
</svg>`,
|
|
|
|
)}`;
|
|
|
|
)}`;
|
|
|
|
excalidrawAPI.setCursor(`url(${url}), auto`);
|
|
|
|
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{COMMENT_SVG}
|
|
|
|
{COMMENT_SVG}
|
|
|
@ -168,10 +200,10 @@ export default function App() {
|
|
|
|
const file = await fileOpen({ description: "Excalidraw or library file" });
|
|
|
|
const file = await fileOpen({ description: "Excalidraw or library file" });
|
|
|
|
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
|
|
|
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
|
|
|
if (contents.type === MIME_TYPES.excalidraw) {
|
|
|
|
if (contents.type === MIME_TYPES.excalidraw) {
|
|
|
|
excalidrawAPI.updateScene(contents.data);
|
|
|
|
excalidrawAPI?.updateScene(contents.data as any);
|
|
|
|
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
|
|
|
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
|
|
|
excalidrawAPI.updateLibrary({
|
|
|
|
excalidrawAPI?.updateLibrary({
|
|
|
|
libraryItems: contents.data.libraryItems,
|
|
|
|
libraryItems: (contents.data as ImportedLibraryData).libraryItems!,
|
|
|
|
openLibraryMenu: true,
|
|
|
|
openLibraryMenu: true,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -179,34 +211,42 @@ export default function App() {
|
|
|
|
|
|
|
|
|
|
|
|
const updateScene = () => {
|
|
|
|
const updateScene = () => {
|
|
|
|
const sceneData = {
|
|
|
|
const sceneData = {
|
|
|
|
elements: [
|
|
|
|
elements: restoreElements(
|
|
|
|
{
|
|
|
|
[
|
|
|
|
type: "rectangle",
|
|
|
|
{
|
|
|
|
version: 141,
|
|
|
|
type: "rectangle",
|
|
|
|
versionNonce: 361174001,
|
|
|
|
version: 141,
|
|
|
|
isDeleted: false,
|
|
|
|
versionNonce: 361174001,
|
|
|
|
id: "oDVXy8D6rom3H1-LLH2-f",
|
|
|
|
isDeleted: false,
|
|
|
|
fillStyle: "hachure",
|
|
|
|
id: "oDVXy8D6rom3H1-LLH2-f",
|
|
|
|
strokeWidth: 1,
|
|
|
|
fillStyle: "hachure",
|
|
|
|
strokeStyle: "solid",
|
|
|
|
strokeWidth: 1,
|
|
|
|
roughness: 1,
|
|
|
|
strokeStyle: "solid",
|
|
|
|
opacity: 100,
|
|
|
|
roughness: 1,
|
|
|
|
angle: 0,
|
|
|
|
opacity: 100,
|
|
|
|
x: 100.50390625,
|
|
|
|
angle: 0,
|
|
|
|
y: 93.67578125,
|
|
|
|
x: 100.50390625,
|
|
|
|
strokeColor: "#c92a2a",
|
|
|
|
y: 93.67578125,
|
|
|
|
backgroundColor: "transparent",
|
|
|
|
strokeColor: "#c92a2a",
|
|
|
|
width: 186.47265625,
|
|
|
|
backgroundColor: "transparent",
|
|
|
|
height: 141.9765625,
|
|
|
|
width: 186.47265625,
|
|
|
|
seed: 1968410350,
|
|
|
|
height: 141.9765625,
|
|
|
|
groupIds: [],
|
|
|
|
seed: 1968410350,
|
|
|
|
},
|
|
|
|
groupIds: [],
|
|
|
|
],
|
|
|
|
boundElements: null,
|
|
|
|
|
|
|
|
locked: false,
|
|
|
|
|
|
|
|
link: null,
|
|
|
|
|
|
|
|
updated: 1,
|
|
|
|
|
|
|
|
strokeSharpness: "round",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
null,
|
|
|
|
|
|
|
|
),
|
|
|
|
appState: {
|
|
|
|
appState: {
|
|
|
|
viewBackgroundColor: "#edf2ff",
|
|
|
|
viewBackgroundColor: "#edf2ff",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
excalidrawAPI.updateScene(sceneData);
|
|
|
|
excalidrawAPI?.updateScene(sceneData);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onLinkOpen = useCallback((element, event) => {
|
|
|
|
const onLinkOpen = useCallback((element, event) => {
|
|
|
@ -224,19 +264,26 @@ export default function App() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const onCopy = async (type) => {
|
|
|
|
const onCopy = async (type: string) => {
|
|
|
|
await exportToClipboard({
|
|
|
|
await exportToClipboard({
|
|
|
|
elements: excalidrawAPI.getSceneElements(),
|
|
|
|
elements: excalidrawAPI?.getSceneElements(),
|
|
|
|
appState: excalidrawAPI.getAppState(),
|
|
|
|
appState: excalidrawAPI?.getAppState(),
|
|
|
|
files: excalidrawAPI.getFiles(),
|
|
|
|
files: excalidrawAPI?.getFiles(),
|
|
|
|
type,
|
|
|
|
type,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
|
|
|
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const [pointerData, setPointerData] = useState(null);
|
|
|
|
const [pointerData, setPointerData] = useState<{
|
|
|
|
|
|
|
|
pointer: { x: number; y: number };
|
|
|
|
|
|
|
|
button: "down" | "up";
|
|
|
|
|
|
|
|
pointersMap: Gesture["pointers"];
|
|
|
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const onPointerDown = (activeTool, pointerDownState) => {
|
|
|
|
const onPointerDown = (
|
|
|
|
|
|
|
|
activeTool: AppState["activeTool"],
|
|
|
|
|
|
|
|
pointerDownState: ExcalidrawPointerDownState,
|
|
|
|
|
|
|
|
) => {
|
|
|
|
if (activeTool.type === "custom" && activeTool.customType === "comment") {
|
|
|
|
if (activeTool.type === "custom" && activeTool.customType === "comment") {
|
|
|
|
const { x, y } = pointerDownState.origin;
|
|
|
|
const { x, y } = pointerDownState.origin;
|
|
|
|
setComment({ x, y, value: "" });
|
|
|
|
setComment({ x, y, value: "" });
|
|
|
@ -244,48 +291,53 @@ export default function App() {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const rerenderCommentIcons = () => {
|
|
|
|
const rerenderCommentIcons = () => {
|
|
|
|
const commentIconsElements =
|
|
|
|
const commentIconsElements = appRef.current.querySelectorAll(
|
|
|
|
appRef.current.querySelectorAll(".comment-icon");
|
|
|
|
".comment-icon",
|
|
|
|
|
|
|
|
) as HTMLElement[];
|
|
|
|
commentIconsElements.forEach((ele) => {
|
|
|
|
commentIconsElements.forEach((ele) => {
|
|
|
|
const id = ele.id;
|
|
|
|
const id = ele.id;
|
|
|
|
const appstate = excalidrawAPI.getAppState();
|
|
|
|
const appstate = excalidrawAPI?.getAppState();
|
|
|
|
const { x, y } = sceneCoordsToViewportCoords(
|
|
|
|
const { x, y } = sceneCoordsToViewportCoords(
|
|
|
|
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
|
|
|
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
|
|
|
appstate,
|
|
|
|
appstate,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
ele.style.left = `${
|
|
|
|
ele.style.left = `${
|
|
|
|
x - COMMENT_ICON_DIMENSION / 2 - appstate.offsetLeft
|
|
|
|
x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft
|
|
|
|
}px`;
|
|
|
|
}px`;
|
|
|
|
ele.style.top = `${
|
|
|
|
ele.style.top = `${
|
|
|
|
y - COMMENT_ICON_DIMENSION / 2 - appstate.offsetTop
|
|
|
|
y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop
|
|
|
|
}px`;
|
|
|
|
}px`;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onPointerMoveFromPointerDownHandler = (pointerDownState) => {
|
|
|
|
const onPointerMoveFromPointerDownHandler = (
|
|
|
|
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
|
|
|
|
) => {
|
|
|
|
return withBatchedUpdatesThrottled((event) => {
|
|
|
|
return withBatchedUpdatesThrottled((event) => {
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
|
|
{
|
|
|
|
{
|
|
|
|
clientX: event.clientX - pointerDownState.hitElementOffsets.x,
|
|
|
|
clientX: event.clientX - pointerDownState.hitElementOffsets.x,
|
|
|
|
clientY: event.clientY - pointerDownState.hitElementOffsets.y,
|
|
|
|
clientY: event.clientY - pointerDownState.hitElementOffsets.y,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
excalidrawAPI.getAppState(),
|
|
|
|
excalidrawAPI?.getAppState(),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
setCommentIcons({
|
|
|
|
setCommentIcons({
|
|
|
|
...commentIcons,
|
|
|
|
...commentIcons,
|
|
|
|
[pointerDownState.hitElement.id]: {
|
|
|
|
[pointerDownState.hitElement.id!]: {
|
|
|
|
...commentIcons[pointerDownState.hitElement.id],
|
|
|
|
...commentIcons[pointerDownState.hitElement.id!],
|
|
|
|
x,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
y,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const onPointerUpFromPointerDownHandler = (pointerDownState) => {
|
|
|
|
const onPointerUpFromPointerDownHandler = (
|
|
|
|
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
|
|
|
|
) => {
|
|
|
|
return withBatchedUpdates((event) => {
|
|
|
|
return withBatchedUpdates((event) => {
|
|
|
|
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
|
|
|
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
|
|
|
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
|
|
|
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
|
|
|
excalidrawAPI.setActiveTool({ type: "selection" });
|
|
|
|
excalidrawAPI?.setActiveTool({ type: "selection" });
|
|
|
|
const distance = distance2d(
|
|
|
|
const distance = distance2d(
|
|
|
|
pointerDownState.x,
|
|
|
|
pointerDownState.x,
|
|
|
|
pointerDownState.y,
|
|
|
|
pointerDownState.y,
|
|
|
@ -308,18 +360,18 @@ export default function App() {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const renderCommentIcons = () => {
|
|
|
|
const renderCommentIcons = () => {
|
|
|
|
return Object.values(commentIcons).map((commentIcon) => {
|
|
|
|
return Object.values(commentIcons).map((commentIcon) => {
|
|
|
|
const appState = excalidrawAPI.getAppState();
|
|
|
|
const appState = excalidrawAPI?.getAppState();
|
|
|
|
const { x, y } = sceneCoordsToViewportCoords(
|
|
|
|
const { x, y } = sceneCoordsToViewportCoords(
|
|
|
|
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
|
|
|
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
|
|
|
excalidrawAPI.getAppState(),
|
|
|
|
excalidrawAPI?.getAppState(),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
id={commentIcon.id}
|
|
|
|
id={commentIcon.id}
|
|
|
|
key={commentIcon.id}
|
|
|
|
key={commentIcon.id}
|
|
|
|
style={{
|
|
|
|
style={{
|
|
|
|
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop}px`,
|
|
|
|
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,
|
|
|
|
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft}px`,
|
|
|
|
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,
|
|
|
|
position: "absolute",
|
|
|
|
position: "absolute",
|
|
|
|
zIndex: 1,
|
|
|
|
zIndex: 1,
|
|
|
|
width: `${COMMENT_ICON_DIMENSION}px`,
|
|
|
|
width: `${COMMENT_ICON_DIMENSION}px`,
|
|
|
@ -334,7 +386,7 @@ export default function App() {
|
|
|
|
commentIcon.value = comment.value;
|
|
|
|
commentIcon.value = comment.value;
|
|
|
|
saveComment();
|
|
|
|
saveComment();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const pointerDownState = {
|
|
|
|
const pointerDownState: any = {
|
|
|
|
x: event.clientX,
|
|
|
|
x: event.clientX,
|
|
|
|
y: event.clientY,
|
|
|
|
y: event.clientY,
|
|
|
|
hitElement: commentIcon,
|
|
|
|
hitElement: commentIcon,
|
|
|
@ -350,7 +402,7 @@ export default function App() {
|
|
|
|
pointerDownState.onMove = onPointerMove;
|
|
|
|
pointerDownState.onMove = onPointerMove;
|
|
|
|
pointerDownState.onUp = onPointerUp;
|
|
|
|
pointerDownState.onUp = onPointerUp;
|
|
|
|
|
|
|
|
|
|
|
|
excalidrawAPI.setActiveTool({
|
|
|
|
excalidrawAPI?.setActiveTool({
|
|
|
|
type: "custom",
|
|
|
|
type: "custom",
|
|
|
|
customType: "comment",
|
|
|
|
customType: "comment",
|
|
|
|
});
|
|
|
|
});
|
|
|
@ -365,6 +417,9 @@ export default function App() {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const saveComment = () => {
|
|
|
|
const saveComment = () => {
|
|
|
|
|
|
|
|
if (!comment) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
if (!comment.id && !comment.value) {
|
|
|
|
if (!comment.id && !comment.value) {
|
|
|
|
setComment(null);
|
|
|
|
setComment(null);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
@ -383,7 +438,10 @@ export default function App() {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderComment = () => {
|
|
|
|
const renderComment = () => {
|
|
|
|
const appState = excalidrawAPI.getAppState();
|
|
|
|
if (!comment) {
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const appState = excalidrawAPI?.getAppState()!;
|
|
|
|
const { x, y } = sceneCoordsToViewportCoords(
|
|
|
|
const { x, y } = sceneCoordsToViewportCoords(
|
|
|
|
{ sceneX: comment.x, sceneY: comment.y },
|
|
|
|
{ sceneX: comment.x, sceneY: comment.y },
|
|
|
|
appState,
|
|
|
|
appState,
|
|
|
@ -450,24 +508,29 @@ export default function App() {
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="reset-scene"
|
|
|
|
className="reset-scene"
|
|
|
|
onClick={() => {
|
|
|
|
onClick={() => {
|
|
|
|
excalidrawAPI.resetScene();
|
|
|
|
excalidrawAPI?.resetScene();
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
Reset Scene
|
|
|
|
Reset Scene
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={() => {
|
|
|
|
onClick={() => {
|
|
|
|
excalidrawAPI.updateLibrary({
|
|
|
|
const libraryItems: LibraryItems = [
|
|
|
|
libraryItems: [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
status: "published",
|
|
|
|
status: "published",
|
|
|
|
id: "1",
|
|
|
|
elements: initialData.libraryItems[0],
|
|
|
|
created: 1,
|
|
|
|
},
|
|
|
|
elements: initialData.libraryItems[1] as any,
|
|
|
|
{
|
|
|
|
},
|
|
|
|
status: "unpublished",
|
|
|
|
{
|
|
|
|
elements: initialData.libraryItems[1],
|
|
|
|
status: "unpublished",
|
|
|
|
},
|
|
|
|
id: "2",
|
|
|
|
],
|
|
|
|
created: 2,
|
|
|
|
|
|
|
|
elements: initialData.libraryItems[1] as any,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
excalidrawAPI?.updateLibrary({
|
|
|
|
|
|
|
|
libraryItems,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
@ -535,9 +598,9 @@ export default function App() {
|
|
|
|
username: "fallback",
|
|
|
|
username: "fallback",
|
|
|
|
avatarUrl: "https://example.com",
|
|
|
|
avatarUrl: "https://example.com",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
excalidrawAPI.updateScene({ collaborators });
|
|
|
|
excalidrawAPI?.updateScene({ collaborators });
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
excalidrawAPI.updateScene({
|
|
|
|
excalidrawAPI?.updateScene({
|
|
|
|
collaborators: new Map(),
|
|
|
|
collaborators: new Map(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -571,12 +634,16 @@ export default function App() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="excalidraw-wrapper">
|
|
|
|
<div className="excalidraw-wrapper">
|
|
|
|
<Excalidraw
|
|
|
|
<Excalidraw
|
|
|
|
ref={(api) => setExcalidrawAPI(api)}
|
|
|
|
ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)}
|
|
|
|
initialData={initialStatePromiseRef.current.promise}
|
|
|
|
initialData={initialStatePromiseRef.current.promise}
|
|
|
|
onChange={(elements, state) => {
|
|
|
|
onChange={(elements: ExcalidrawElement[], state: AppState) => {
|
|
|
|
console.info("Elements :", elements, "State : ", state);
|
|
|
|
console.info("Elements :", elements, "State : ", state);
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
onPointerUpdate={(payload) => setPointerData(payload)}
|
|
|
|
onPointerUpdate={(payload: {
|
|
|
|
|
|
|
|
pointer: { x: number; y: number };
|
|
|
|
|
|
|
|
button: "down" | "up";
|
|
|
|
|
|
|
|
pointersMap: Gesture["pointers"];
|
|
|
|
|
|
|
|
}) => setPointerData(payload)}
|
|
|
|
onCollabButtonClick={() =>
|
|
|
|
onCollabButtonClick={() =>
|
|
|
|
window.alert("You clicked on collab button")
|
|
|
|
window.alert("You clicked on collab button")
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -616,7 +683,7 @@ export default function App() {
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={async () => {
|
|
|
|
onClick={async () => {
|
|
|
|
const svg = await exportToSvg({
|
|
|
|
const svg = await exportToSvg({
|
|
|
|
elements: excalidrawAPI.getSceneElements(),
|
|
|
|
elements: excalidrawAPI?.getSceneElements(),
|
|
|
|
appState: {
|
|
|
|
appState: {
|
|
|
|
...initialData.appState,
|
|
|
|
...initialData.appState,
|
|
|
|
exportWithDarkMode,
|
|
|
|
exportWithDarkMode,
|
|
|
@ -625,7 +692,7 @@ export default function App() {
|
|
|
|
height: 100,
|
|
|
|
height: 100,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
embedScene: true,
|
|
|
|
embedScene: true,
|
|
|
|
files: excalidrawAPI.getFiles(),
|
|
|
|
files: excalidrawAPI?.getFiles(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
appRef.current.querySelector(".export-svg").innerHTML =
|
|
|
|
appRef.current.querySelector(".export-svg").innerHTML =
|
|
|
|
svg.outerHTML;
|
|
|
|
svg.outerHTML;
|
|
|
@ -638,14 +705,14 @@ export default function App() {
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={async () => {
|
|
|
|
onClick={async () => {
|
|
|
|
const blob = await exportToBlob({
|
|
|
|
const blob = await exportToBlob({
|
|
|
|
elements: excalidrawAPI.getSceneElements(),
|
|
|
|
elements: excalidrawAPI?.getSceneElements(),
|
|
|
|
mimeType: "image/png",
|
|
|
|
mimeType: "image/png",
|
|
|
|
appState: {
|
|
|
|
appState: {
|
|
|
|
...initialData.appState,
|
|
|
|
...initialData.appState,
|
|
|
|
exportEmbedScene,
|
|
|
|
exportEmbedScene,
|
|
|
|
exportWithDarkMode,
|
|
|
|
exportWithDarkMode,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
files: excalidrawAPI.getFiles(),
|
|
|
|
files: excalidrawAPI?.getFiles(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
setBlobUrl(window.URL.createObjectURL(blob));
|
|
|
|
setBlobUrl(window.URL.createObjectURL(blob));
|
|
|
|
}}
|
|
|
|
}}
|
|
|
@ -659,12 +726,12 @@ export default function App() {
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={async () => {
|
|
|
|
onClick={async () => {
|
|
|
|
const canvas = await exportToCanvas({
|
|
|
|
const canvas = await exportToCanvas({
|
|
|
|
elements: excalidrawAPI.getSceneElements(),
|
|
|
|
elements: excalidrawAPI?.getSceneElements(),
|
|
|
|
appState: {
|
|
|
|
appState: {
|
|
|
|
...initialData.appState,
|
|
|
|
...initialData.appState,
|
|
|
|
exportWithDarkMode,
|
|
|
|
exportWithDarkMode,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
files: excalidrawAPI.getFiles(),
|
|
|
|
files: excalidrawAPI?.getFiles(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
ctx.font = "30px Virgil";
|
|
|
|
ctx.font = "30px Virgil";
|