diff --git a/src/packages/excalidraw/example/App.js b/src/packages/excalidraw/example/App.tsx similarity index 74% rename from src/packages/excalidraw/example/App.js rename to src/packages/excalidraw/example/App.tsx index 9cbe95c0..644a596b 100644 --- a/src/packages/excalidraw/example/App.js +++ b/src/packages/excalidraw/example/App.tsx @@ -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" > @@ -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 ( { }; export default function App() { - const appRef = useRef(null); + const appRef = useRef(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(""); + const [canvasUrl, setCanvasUrl] = useState(""); 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(null); - const initialStatePromiseRef = useRef({ promise: null }); + const initialStatePromiseRef = useRef<{ + promise: ResolvablePromise; + }>({ promise: null! }); if (!initialStatePromiseRef.current.promise) { - initialStatePromiseRef.current.promise = resolvablePromise(); + initialStatePromiseRef.current.promise = + resolvablePromise(); } - const [excalidrawAPI, setExcalidrawAPI] = useState(null); + const [excalidrawAPI, setExcalidrawAPI] = + useState(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() { { - excalidrawAPI.setActiveTool({ + excalidrawAPI?.setActiveTool({ type: "custom", customType: "comment", }); @@ -151,7 +183,7 @@ export default function App() { `, )}`; - 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 ( { + 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() { { - excalidrawAPI.resetScene(); + excalidrawAPI?.resetScene(); }} > Reset Scene { - 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() { 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() { { 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() { { 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() { { 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"; diff --git a/src/packages/excalidraw/example/index.js b/src/packages/excalidraw/example/index.tsx similarity index 100% rename from src/packages/excalidraw/example/index.js rename to src/packages/excalidraw/example/index.tsx diff --git a/src/packages/excalidraw/example/sidebar/Sidebar.js b/src/packages/excalidraw/example/sidebar/Sidebar.tsx similarity index 82% rename from src/packages/excalidraw/example/sidebar/Sidebar.js rename to src/packages/excalidraw/example/sidebar/Sidebar.tsx index 329f16b2..3ae40717 100644 --- a/src/packages/excalidraw/example/sidebar/Sidebar.js +++ b/src/packages/excalidraw/example/sidebar/Sidebar.tsx @@ -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 - {props.children} + {children} > ); diff --git a/src/packages/excalidraw/webpack.dev-server.config.js b/src/packages/excalidraw/webpack.dev-server.config.js index 41c40aeb..4e8df899 100644 --- a/src/packages/excalidraw/webpack.dev-server.config.js +++ b/src/packages/excalidraw/webpack.dev-server.config.js @@ -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: {