docs: migrate example to typescript (#5243)
* docs: migrate example to typescript * fix * fix sidebar * fix
This commit is contained in:
parent
5daff2d3cd
commit
6196fba286
@ -1,12 +1,13 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
import InitialData from "./initialData";
|
||||
import Sidebar from "./sidebar/Sidebar";
|
||||
|
||||
import "./App.scss";
|
||||
import initialData from "./initialData";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
resolvablePromise,
|
||||
ResolvablePromise,
|
||||
withBatchedUpdates,
|
||||
withBatchedUpdatesThrottled,
|
||||
} from "../../../utils";
|
||||
@ -14,7 +15,42 @@ import { EVENT } from "../../../constants";
|
||||
import { distance2d } from "../../../math";
|
||||
import { fileOpen } from "../../../data/filesystem";
|
||||
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
|
||||
// of the actual source code
|
||||
|
||||
@ -28,6 +64,7 @@ const {
|
||||
MIME_TYPES,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
restoreElements,
|
||||
} = window.ExcalidrawLib;
|
||||
|
||||
const COMMENT_SVG = (
|
||||
@ -41,7 +78,7 @@ const COMMENT_SVG = (
|
||||
stroke-width="2"
|
||||
stroke-linecap="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>
|
||||
</svg>
|
||||
@ -50,18 +87,6 @@ const COMMENT_ICON_DIMENSION = 32;
|
||||
const COMMENT_INPUT_HEIGHT = 50;
|
||||
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 = () => {
|
||||
return (
|
||||
<button
|
||||
@ -75,25 +100,31 @@ const renderTopRightUI = () => {
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const appRef = useRef(null);
|
||||
const appRef = useRef<any>(null);
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
const [blobUrl, setBlobUrl] = useState(null);
|
||||
const [canvasUrl, setCanvasUrl] = useState(null);
|
||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState("light");
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
const [commentIcons, setCommentIcons] = useState({});
|
||||
const [comment, setComment] = useState(null);
|
||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||
{},
|
||||
);
|
||||
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) {
|
||||
initialStatePromiseRef.current.promise = resolvablePromise();
|
||||
initialStatePromiseRef.current.promise =
|
||||
resolvablePromise<ExcalidrawInitialDataState | null>();
|
||||
}
|
||||
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||
const [excalidrawAPI, setExcalidrawAPI] =
|
||||
useState<ExcalidrawImperativeAPI | null>(null);
|
||||
|
||||
useHandleLibrary({ excalidrawAPI });
|
||||
|
||||
@ -108,16 +139,17 @@ export default function App() {
|
||||
reader.readAsDataURL(imageData);
|
||||
|
||||
reader.onload = function () {
|
||||
const imagesArray = [
|
||||
const imagesArray: BinaryFileData[] = [
|
||||
{
|
||||
id: "rocket",
|
||||
dataURL: reader.result,
|
||||
id: "rocket" as BinaryFileData["id"],
|
||||
dataURL: reader.result as BinaryFileData["dataURL"],
|
||||
mimeType: MIME_TYPES.jpg,
|
||||
created: 1644915140367,
|
||||
},
|
||||
];
|
||||
|
||||
initialStatePromiseRef.current.promise.resolve(InitialData);
|
||||
//@ts-ignore
|
||||
initialStatePromiseRef.current.promise.resolve(initialData);
|
||||
excalidrawAPI.addFiles(imagesArray);
|
||||
};
|
||||
};
|
||||
@ -131,7 +163,7 @@ export default function App() {
|
||||
<button
|
||||
className="custom-element"
|
||||
onClick={() => {
|
||||
excalidrawAPI.setActiveTool({
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
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>
|
||||
</svg>`,
|
||||
)}`;
|
||||
excalidrawAPI.setCursor(`url(${url}), auto`);
|
||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||
}}
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
@ -168,10 +200,10 @@ export default function App() {
|
||||
const file = await fileOpen({ description: "Excalidraw or library file" });
|
||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||
if (contents.type === MIME_TYPES.excalidraw) {
|
||||
excalidrawAPI.updateScene(contents.data);
|
||||
excalidrawAPI?.updateScene(contents.data as any);
|
||||
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: contents.data.libraryItems,
|
||||
excalidrawAPI?.updateLibrary({
|
||||
libraryItems: (contents.data as ImportedLibraryData).libraryItems!,
|
||||
openLibraryMenu: true,
|
||||
});
|
||||
}
|
||||
@ -179,34 +211,42 @@ export default function App() {
|
||||
|
||||
const updateScene = () => {
|
||||
const sceneData = {
|
||||
elements: [
|
||||
{
|
||||
type: "rectangle",
|
||||
version: 141,
|
||||
versionNonce: 361174001,
|
||||
isDeleted: false,
|
||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: 100.50390625,
|
||||
y: 93.67578125,
|
||||
strokeColor: "#c92a2a",
|
||||
backgroundColor: "transparent",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
seed: 1968410350,
|
||||
groupIds: [],
|
||||
},
|
||||
],
|
||||
elements: restoreElements(
|
||||
[
|
||||
{
|
||||
type: "rectangle",
|
||||
version: 141,
|
||||
versionNonce: 361174001,
|
||||
isDeleted: false,
|
||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: 100.50390625,
|
||||
y: 93.67578125,
|
||||
strokeColor: "#c92a2a",
|
||||
backgroundColor: "transparent",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
seed: 1968410350,
|
||||
groupIds: [],
|
||||
boundElements: null,
|
||||
locked: false,
|
||||
link: null,
|
||||
updated: 1,
|
||||
strokeSharpness: "round",
|
||||
},
|
||||
],
|
||||
null,
|
||||
),
|
||||
appState: {
|
||||
viewBackgroundColor: "#edf2ff",
|
||||
},
|
||||
};
|
||||
excalidrawAPI.updateScene(sceneData);
|
||||
excalidrawAPI?.updateScene(sceneData);
|
||||
};
|
||||
|
||||
const onLinkOpen = useCallback((element, event) => {
|
||||
@ -224,19 +264,26 @@ export default function App() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onCopy = async (type) => {
|
||||
const onCopy = async (type: string) => {
|
||||
await exportToClipboard({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: excalidrawAPI.getAppState(),
|
||||
files: excalidrawAPI.getFiles(),
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
appState: excalidrawAPI?.getAppState(),
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
type,
|
||||
});
|
||||
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") {
|
||||
const { x, y } = pointerDownState.origin;
|
||||
setComment({ x, y, value: "" });
|
||||
@ -244,48 +291,53 @@ export default function App() {
|
||||
};
|
||||
|
||||
const rerenderCommentIcons = () => {
|
||||
const commentIconsElements =
|
||||
appRef.current.querySelectorAll(".comment-icon");
|
||||
const commentIconsElements = appRef.current.querySelectorAll(
|
||||
".comment-icon",
|
||||
) as HTMLElement[];
|
||||
commentIconsElements.forEach((ele) => {
|
||||
const id = ele.id;
|
||||
const appstate = excalidrawAPI.getAppState();
|
||||
const appstate = excalidrawAPI?.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
||||
appstate,
|
||||
);
|
||||
ele.style.left = `${
|
||||
x - COMMENT_ICON_DIMENSION / 2 - appstate.offsetLeft
|
||||
x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft
|
||||
}px`;
|
||||
ele.style.top = `${
|
||||
y - COMMENT_ICON_DIMENSION / 2 - appstate.offsetTop
|
||||
y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop
|
||||
}px`;
|
||||
});
|
||||
};
|
||||
|
||||
const onPointerMoveFromPointerDownHandler = (pointerDownState) => {
|
||||
const onPointerMoveFromPointerDownHandler = (
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
return withBatchedUpdatesThrottled((event) => {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: event.clientX - pointerDownState.hitElementOffsets.x,
|
||||
clientY: event.clientY - pointerDownState.hitElementOffsets.y,
|
||||
},
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI?.getAppState(),
|
||||
);
|
||||
setCommentIcons({
|
||||
...commentIcons,
|
||||
[pointerDownState.hitElement.id]: {
|
||||
...commentIcons[pointerDownState.hitElement.id],
|
||||
[pointerDownState.hitElement.id!]: {
|
||||
...commentIcons[pointerDownState.hitElement.id!],
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
const onPointerUpFromPointerDownHandler = (pointerDownState) => {
|
||||
const onPointerUpFromPointerDownHandler = (
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
return withBatchedUpdates((event) => {
|
||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
||||
excalidrawAPI.setActiveTool({ type: "selection" });
|
||||
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||
const distance = distance2d(
|
||||
pointerDownState.x,
|
||||
pointerDownState.y,
|
||||
@ -308,18 +360,18 @@ export default function App() {
|
||||
};
|
||||
const renderCommentIcons = () => {
|
||||
return Object.values(commentIcons).map((commentIcon) => {
|
||||
const appState = excalidrawAPI.getAppState();
|
||||
const appState = excalidrawAPI?.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI?.getAppState(),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
id={commentIcon.id}
|
||||
key={commentIcon.id}
|
||||
style={{
|
||||
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop}px`,
|
||||
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft}px`,
|
||||
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,
|
||||
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
width: `${COMMENT_ICON_DIMENSION}px`,
|
||||
@ -334,7 +386,7 @@ export default function App() {
|
||||
commentIcon.value = comment.value;
|
||||
saveComment();
|
||||
}
|
||||
const pointerDownState = {
|
||||
const pointerDownState: any = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
hitElement: commentIcon,
|
||||
@ -350,7 +402,7 @@ export default function App() {
|
||||
pointerDownState.onMove = onPointerMove;
|
||||
pointerDownState.onUp = onPointerUp;
|
||||
|
||||
excalidrawAPI.setActiveTool({
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
});
|
||||
@ -365,6 +417,9 @@ export default function App() {
|
||||
};
|
||||
|
||||
const saveComment = () => {
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
if (!comment.id && !comment.value) {
|
||||
setComment(null);
|
||||
return;
|
||||
@ -383,7 +438,10 @@ export default function App() {
|
||||
};
|
||||
|
||||
const renderComment = () => {
|
||||
const appState = excalidrawAPI.getAppState();
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
const appState = excalidrawAPI?.getAppState()!;
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: comment.x, sceneY: comment.y },
|
||||
appState,
|
||||
@ -450,24 +508,29 @@ export default function App() {
|
||||
<button
|
||||
className="reset-scene"
|
||||
onClick={() => {
|
||||
excalidrawAPI.resetScene();
|
||||
excalidrawAPI?.resetScene();
|
||||
}}
|
||||
>
|
||||
Reset Scene
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: [
|
||||
{
|
||||
status: "published",
|
||||
elements: initialData.libraryItems[0],
|
||||
},
|
||||
{
|
||||
status: "unpublished",
|
||||
elements: initialData.libraryItems[1],
|
||||
},
|
||||
],
|
||||
const libraryItems: LibraryItems = [
|
||||
{
|
||||
status: "published",
|
||||
id: "1",
|
||||
created: 1,
|
||||
elements: initialData.libraryItems[1] as any,
|
||||
},
|
||||
{
|
||||
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",
|
||||
avatarUrl: "https://example.com",
|
||||
});
|
||||
excalidrawAPI.updateScene({ collaborators });
|
||||
excalidrawAPI?.updateScene({ collaborators });
|
||||
} else {
|
||||
excalidrawAPI.updateScene({
|
||||
excalidrawAPI?.updateScene({
|
||||
collaborators: new Map(),
|
||||
});
|
||||
}
|
||||
@ -571,12 +634,16 @@ export default function App() {
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<Excalidraw
|
||||
ref={(api) => setExcalidrawAPI(api)}
|
||||
ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onChange={(elements, state) => {
|
||||
onChange={(elements: ExcalidrawElement[], state: AppState) => {
|
||||
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={() =>
|
||||
window.alert("You clicked on collab button")
|
||||
}
|
||||
@ -616,7 +683,7 @@ export default function App() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const svg = await exportToSvg({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
@ -625,7 +692,7 @@ export default function App() {
|
||||
height: 100,
|
||||
},
|
||||
embedScene: true,
|
||||
files: excalidrawAPI.getFiles(),
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
});
|
||||
appRef.current.querySelector(".export-svg").innerHTML =
|
||||
svg.outerHTML;
|
||||
@ -638,14 +705,14 @@ export default function App() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const blob = await exportToBlob({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
mimeType: "image/png",
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportEmbedScene,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
});
|
||||
setBlobUrl(window.URL.createObjectURL(blob));
|
||||
}}
|
||||
@ -659,12 +726,12 @@ export default function App() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const canvas = await exportToCanvas({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
});
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = "30px Virgil";
|
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import "./Sidebar.scss";
|
||||
export default function Sidebar(props) {
|
||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@ -23,7 +23,7 @@ export default function Sidebar(props) {
|
||||
>
|
||||
Open Sidebar
|
||||
</button>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
@ -5,7 +5,7 @@ const devConfig = require("./webpack.dev.config");
|
||||
|
||||
const devServerConfig = {
|
||||
entry: {
|
||||
bundle: "./example/index.js",
|
||||
bundle: "./example/index.tsx",
|
||||
},
|
||||
// Server Configuration options
|
||||
devServer: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user