docs: migrate example to typescript (#5243)

* docs: migrate example to typescript

* fix

* fix sidebar

* fix
This commit is contained in:
Aakansha Doshi 2022-06-14 17:56:05 +05:30 committed by GitHub
parent 5daff2d3cd
commit 6196fba286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 106 deletions

View File

@ -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,7 +211,8 @@ export default function App() {
const updateScene = () => { const updateScene = () => {
const sceneData = { const sceneData = {
elements: [ elements: restoreElements(
[
{ {
type: "rectangle", type: "rectangle",
version: 141, version: 141,
@ -200,13 +233,20 @@ export default function App() {
height: 141.9765625, height: 141.9765625,
seed: 1968410350, seed: 1968410350,
groupIds: [], 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",
elements: initialData.libraryItems[0], id: "1",
created: 1,
elements: initialData.libraryItems[1] as any,
}, },
{ {
status: "unpublished", status: "unpublished",
elements: initialData.libraryItems[1], 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";

View File

@ -1,6 +1,6 @@
import { useState } from "react"; import React, { useState } from "react";
import "./Sidebar.scss"; import "./Sidebar.scss";
export default function Sidebar(props) { export default function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
@ -23,7 +23,7 @@ export default function Sidebar(props) {
> >
Open Sidebar Open Sidebar
</button> </button>
{props.children} {children}
</div> </div>
</> </>
); );

View File

@ -5,7 +5,7 @@ const devConfig = require("./webpack.dev.config");
const devServerConfig = { const devServerConfig = {
entry: { entry: {
bundle: "./example/index.js", bundle: "./example/index.tsx",
}, },
// Server Configuration options // Server Configuration options
devServer: { devServer: {