diff options
Diffstat (limited to 'examples/with-script-in-browser')
21 files changed, 2545 insertions, 0 deletions
diff --git a/examples/with-script-in-browser/.codesandbox/Dockerfile b/examples/with-script-in-browser/.codesandbox/Dockerfile new file mode 100644 index 0000000..fd5b38d --- /dev/null +++ b/examples/with-script-in-browser/.codesandbox/Dockerfile @@ -0,0 +1,5 @@ +FROM node:18-bullseye + +# Vite wants to open the browser using `open`, so we +# need to install those utils. +RUN apt update -y && apt install -y xdg-utils diff --git a/examples/with-script-in-browser/.codesandbox/tasks.json b/examples/with-script-in-browser/.codesandbox/tasks.json new file mode 100644 index 0000000..990c21a --- /dev/null +++ b/examples/with-script-in-browser/.codesandbox/tasks.json @@ -0,0 +1,35 @@ +{ + // These tasks will run in order when initializing your CodeSandbox project. + "setupTasks": [ + { + "name": "Install Dependencies", + "command": "yarn install" + } + ], + + // These tasks can be run from CodeSandbox. Running one will open a log in the app. + "tasks": { + "build": { + "name": "Build", + "command": "yarn build", + "runAtStart": false + }, + "start": { + "name": "Start Example", + "command": "yarn start", + "runAtStart": true, + "preview": { + "port": 3001 + } + }, + "install-deps": { + "name": "Install Dependencies", + "command": "yarn install", + "restartOn": { + "files": ["yarn.lock"], + "branch": false, + "resume": false + } + } + } +} diff --git a/examples/with-script-in-browser/.gitignore b/examples/with-script-in-browser/.gitignore new file mode 100644 index 0000000..f1b2f5e --- /dev/null +++ b/examples/with-script-in-browser/.gitignore @@ -0,0 +1,2 @@ +# copied assets +public/**/*.woff2
\ No newline at end of file diff --git a/examples/with-script-in-browser/components/CustomFooter.tsx b/examples/with-script-in-browser/components/CustomFooter.tsx new file mode 100644 index 0000000..72fd199 --- /dev/null +++ b/examples/with-script-in-browser/components/CustomFooter.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; + +const COMMENT_SVG = ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + 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> +); + +const CustomFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { Button, MIME_TYPES } = excalidrawLib; + + return ( + <> + <Button + onSelect={() => alert("General Kenobi!")} + style={{ marginLeft: "1rem", width: "auto" }} + title="Hello there!" + > + Hit me + </Button> + <Button + className="custom-element" + onSelect={() => { + excalidrawAPI?.setActiveTool({ + type: "custom", + customType: "comment", + }); + const url = `data:${MIME_TYPES.svg},${encodeURIComponent( + `<svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + class="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>`, + )}`; + excalidrawAPI?.setCursor(`url(${url}), auto`); + }} + title="Comments!" + > + {COMMENT_SVG} + </Button> + </> + ); +}; + +export default CustomFooter; diff --git a/examples/with-script-in-browser/components/ExampleApp.scss b/examples/with-script-in-browser/components/ExampleApp.scss new file mode 100644 index 0000000..e41a77c --- /dev/null +++ b/examples/with-script-in-browser/components/ExampleApp.scss @@ -0,0 +1,92 @@ +.App { + font-family: sans-serif; + text-align: center; + + .comment-avatar { + background: #faa2c1; + border-radius: 66px 67px 67px 0px; + width: 2rem; + height: 2rem; + padding: 4px; + margin: 4px; + img { + width: 100%; + height: 100%; + border-radius: 50%; + } + } + .app-title { + margin-block-start: 0.83em; + margin-block-end: 0.83em; + } +} + +.button-wrapper { + input[type="checkbox"] { + margin: 5px; + } + button { + z-index: 1; + height: 40px; + max-width: 200px; + margin: 10px; + padding: 5px; + } +} + +.excalidraw .App-menu_top .buttonList { + display: flex; +} + +.excalidraw-wrapper { + height: 800px; + margin: 50px; + position: relative; + overflow: hidden; +} + +:root[dir="ltr"] + .excalidraw + .layer-ui__wrapper + .zen-mode-transition.App-menu_bottom--transition-left { + transform: none; +} + +.excalidraw .panelColumn { + text-align: left; +} + +.export-wrapper { + display: flex; + flex-direction: column; + margin: 50px; + + &__checkbox { + display: flex; + } +} + +.excalidraw { + --color-primary: #faa2c1; + --color-primary-darker: #f783ac; + --color-primary-darkest: #e64980; + --color-primary-light: #fcc2d7; + + button.custom-element { + width: 2rem; + height: 2rem; + } + + .custom-footer, + .custom-element { + padding: 0.1rem; + margin: 0 8px; + } + .layer-ui__wrapper__footer.App-menu_bottom { + align-items: stretch; + } + // till its merged in OSS + .App-toolbar-container .mobile-misc-tools-container { + position: absolute; + } +} diff --git a/examples/with-script-in-browser/components/ExampleApp.tsx b/examples/with-script-in-browser/components/ExampleApp.tsx new file mode 100644 index 0000000..08c8032 --- /dev/null +++ b/examples/with-script-in-browser/components/ExampleApp.tsx @@ -0,0 +1,961 @@ +import React, { + useEffect, + useState, + useRef, + useCallback, + Children, + cloneElement, +} from "react"; +import ExampleSidebar from "./sidebar/ExampleSidebar"; + +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +import { nanoid } from "nanoid"; + +import type { ResolvablePromise } from "../utils"; +import { + resolvablePromise, + distance2d, + fileOpen, + withBatchedUpdates, + withBatchedUpdatesThrottled, +} from "../utils"; + +import CustomFooter from "./CustomFooter"; +import MobileFooter from "./MobileFooter"; +import initialData from "../initialData"; + +import type { + AppState, + BinaryFileData, + ExcalidrawImperativeAPI, + ExcalidrawInitialDataState, + Gesture, + LibraryItems, + PointerDownState as ExcalidrawPointerDownState, +} from "@excalidraw/excalidraw/types"; +import type { + NonDeletedExcalidrawElement, + Theme, +} from "@excalidraw/excalidraw/element/types"; +import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types"; + +import "./ExampleApp.scss"; + +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; + }; +}; + +const COMMENT_ICON_DIMENSION = 32; +const COMMENT_INPUT_HEIGHT = 50; +const COMMENT_INPUT_WIDTH = 150; + +export interface AppProps { + appTitle: string; + useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void; + customArgs?: any[]; + children: React.ReactNode; + excalidrawLib: typeof TExcalidraw; +} + +export default function ExampleApp({ + appTitle, + useCustom, + customArgs, + children, + excalidrawLib, +}: AppProps) { + const { + exportToCanvas, + exportToSvg, + exportToBlob, + exportToClipboard, + useHandleLibrary, + MIME_TYPES, + sceneCoordsToViewportCoords, + viewportCoordsToSceneCoords, + restoreElements, + Sidebar, + Footer, + WelcomeScreen, + MainMenu, + LiveCollaborationTrigger, + convertToExcalidrawElements, + TTDDialog, + TTDDialogTrigger, + ROUNDNESS, + loadSceneOrLibraryFromBlob, + } = excalidrawLib; + const appRef = useRef<any>(null); + const [viewModeEnabled, setViewModeEnabled] = useState(false); + const [zenModeEnabled, setZenModeEnabled] = useState(false); + const [gridModeEnabled, setGridModeEnabled] = useState(false); + const [blobUrl, setBlobUrl] = useState<string>(""); + const [canvasUrl, setCanvasUrl] = useState<string>(""); + const [exportWithDarkMode, setExportWithDarkMode] = useState(false); + const [exportEmbedScene, setExportEmbedScene] = useState(false); + const [theme, setTheme] = useState<Theme>("light"); + const [disableImageTool, setDisableImageTool] = useState(false); + const [isCollaborating, setIsCollaborating] = useState(false); + const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>( + {}, + ); + const [comment, setComment] = useState<Comment | null>(null); + + const initialStatePromiseRef = useRef<{ + promise: ResolvablePromise<ExcalidrawInitialDataState | null>; + }>({ promise: null! }); + if (!initialStatePromiseRef.current.promise) { + initialStatePromiseRef.current.promise = + resolvablePromise<ExcalidrawInitialDataState | null>(); + } + + const [excalidrawAPI, setExcalidrawAPI] = + useState<ExcalidrawImperativeAPI | null>(null); + + useCustom(excalidrawAPI, customArgs); + + useHandleLibrary({ excalidrawAPI }); + + useEffect(() => { + if (!excalidrawAPI) { + return; + } + const fetchData = async () => { + const res = await fetch("/images/rocket.jpeg"); + const imageData = await res.blob(); + const reader = new FileReader(); + reader.readAsDataURL(imageData); + + reader.onload = function () { + const imagesArray: BinaryFileData[] = [ + { + id: "rocket" as BinaryFileData["id"], + dataURL: reader.result as BinaryFileData["dataURL"], + mimeType: MIME_TYPES.jpg, + created: 1644915140367, + lastRetrieved: 1644915140367, + }, + ]; + + //@ts-ignore + initialStatePromiseRef.current.promise.resolve({ + ...initialData, + elements: convertToExcalidrawElements(initialData.elements), + }); + excalidrawAPI.addFiles(imagesArray); + }; + }; + fetchData(); + }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]); + + const renderExcalidraw = (children: React.ReactNode) => { + const Excalidraw: any = Children.toArray(children).find( + (child) => + React.isValidElement(child) && + typeof child.type !== "string" && + //@ts-ignore + child.type.displayName === "Excalidraw", + ); + if (!Excalidraw) { + return; + } + const newElement = cloneElement( + Excalidraw, + { + excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api), + initialData: initialStatePromiseRef.current.promise, + onChange: ( + elements: NonDeletedExcalidrawElement[], + state: AppState, + ) => { + console.info("Elements :", elements, "State : ", state); + }, + onPointerUpdate: (payload: { + pointer: { x: number; y: number }; + button: "down" | "up"; + pointersMap: Gesture["pointers"]; + }) => setPointerData(payload), + viewModeEnabled, + zenModeEnabled, + gridModeEnabled, + theme, + name: "Custom name of drawing", + UIOptions: { + canvasActions: { + loadScene: false, + }, + tools: { image: !disableImageTool }, + }, + renderTopRightUI, + onLinkOpen, + onPointerDown, + onScrollChange: rerenderCommentIcons, + validateEmbeddable: true, + }, + <> + {excalidrawAPI && ( + <Footer> + <CustomFooter + excalidrawAPI={excalidrawAPI} + excalidrawLib={excalidrawLib} + /> + </Footer> + )} + <WelcomeScreen /> + <Sidebar name="custom"> + <Sidebar.Tabs> + <Sidebar.Header /> + <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab> + <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab> + <Sidebar.TabTriggers> + <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger> + <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger> + </Sidebar.TabTriggers> + </Sidebar.Tabs> + </Sidebar> + <Sidebar.Trigger + name="custom" + tab="one" + style={{ + position: "absolute", + left: "50%", + transform: "translateX(-50%)", + bottom: "20px", + zIndex: 9999999999999999, + }} + > + Toggle Custom Sidebar + </Sidebar.Trigger> + {renderMenu()} + {excalidrawAPI && ( + <TTDDialogTrigger icon={<span>😀</span>}> + Text to diagram + </TTDDialogTrigger> + )} + <TTDDialog + onTextSubmit={async (_) => { + console.info("submit"); + // sleep for 2s + await new Promise((resolve) => setTimeout(resolve, 2000)); + throw new Error("error, go away now"); + // return "dummy"; + }} + /> + </>, + ); + return newElement; + }; + const renderTopRightUI = (isMobile: boolean) => { + return ( + <> + {!isMobile && ( + <LiveCollaborationTrigger + isCollaborating={isCollaborating} + onSelect={() => { + window.alert("Collab dialog clicked"); + }} + /> + )} + <button + onClick={() => alert("This is an empty top right UI")} + style={{ height: "2.5rem" }} + > + Click me + </button> + </> + ); + }; + + const loadSceneOrLibrary = async () => { + 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 as any); + } else if (contents.type === MIME_TYPES.excalidrawlib) { + excalidrawAPI?.updateLibrary({ + libraryItems: (contents.data as ImportedLibraryData).libraryItems!, + openLibraryMenu: true, + }); + } + }; + + const updateScene = () => { + const sceneData = { + elements: restoreElements( + convertToExcalidrawElements([ + { + type: "rectangle", + id: "rect-1", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + angle: 0, + x: 100.50390625, + y: 93.67578125, + strokeColor: "#c92a2a", + width: 186.47265625, + height: 141.9765625, + seed: 1968410350, + roundness: { + type: ROUNDNESS.ADAPTIVE_RADIUS, + value: 32, + }, + }, + { + type: "arrow", + x: 300, + y: 150, + start: { id: "rect-1" }, + end: { type: "ellipse" }, + }, + { + type: "text", + x: 300, + y: 100, + text: "HELLO WORLD!", + }, + ]), + null, + ), + appState: { + viewBackgroundColor: "#edf2ff", + }, + }; + excalidrawAPI?.updateScene(sceneData); + }; + + const onLinkOpen = useCallback( + ( + element: NonDeletedExcalidrawElement, + event: CustomEvent<{ + nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>; + }>, + ) => { + const link = element.link!; + const { nativeEvent } = event.detail; + const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey; + const isNewWindow = nativeEvent.shiftKey; + const isInternalLink = + link.startsWith("/") || link.includes(window.location.origin); + if (isInternalLink && !isNewTab && !isNewWindow) { + // signal that we're handling the redirect ourselves + event.preventDefault(); + // do a custom redirect, such as passing to react-router + // ... + } + }, + [], + ); + + const onCopy = async (type: "png" | "svg" | "json") => { + if (!excalidrawAPI) { + return false; + } + await exportToClipboard({ + elements: excalidrawAPI.getSceneElements(), + appState: excalidrawAPI.getAppState(), + files: excalidrawAPI.getFiles(), + type, + }); + window.alert(`Copied to clipboard as ${type} successfully`); + }; + + const [pointerData, setPointerData] = useState<{ + pointer: { x: number; y: number }; + button: "down" | "up"; + pointersMap: Gesture["pointers"]; + } | null>(null); + + const onPointerDown = ( + activeTool: AppState["activeTool"], + pointerDownState: ExcalidrawPointerDownState, + ) => { + if (activeTool.type === "custom" && activeTool.customType === "comment") { + const { x, y } = pointerDownState.origin; + setComment({ x, y, value: "" }); + } + }; + + const rerenderCommentIcons = () => { + if (!excalidrawAPI) { + return false; + } + const commentIconsElements = appRef.current.querySelectorAll( + ".comment-icon", + ) as HTMLElement[]; + commentIconsElements.forEach((ele) => { + const id = ele.id; + 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 + }px`; + ele.style.top = `${ + y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop + }px`; + }); + }; + + const onPointerMoveFromPointerDownHandler = ( + pointerDownState: PointerDownState, + ) => { + return withBatchedUpdatesThrottled((event) => { + if (!excalidrawAPI) { + return false; + } + const { x, y } = viewportCoordsToSceneCoords( + { + clientX: event.clientX - pointerDownState.hitElementOffsets.x, + clientY: event.clientY - pointerDownState.hitElementOffsets.y, + }, + excalidrawAPI.getAppState(), + ); + setCommentIcons({ + ...commentIcons, + [pointerDownState.hitElement.id!]: { + ...commentIcons[pointerDownState.hitElement.id!], + x, + y, + }, + }); + }); + }; + const onPointerUpFromPointerDownHandler = ( + pointerDownState: PointerDownState, + ) => { + return withBatchedUpdates((event) => { + window.removeEventListener("pointermove", pointerDownState.onMove); + window.removeEventListener("pointerup", pointerDownState.onUp); + excalidrawAPI?.setActiveTool({ type: "selection" }); + const distance = distance2d( + pointerDownState.x, + pointerDownState.y, + event.clientX, + event.clientY, + ); + if (distance === 0) { + if (!comment) { + setComment({ + x: pointerDownState.hitElement.x + 60, + y: pointerDownState.hitElement.y, + value: pointerDownState.hitElement.value, + id: pointerDownState.hitElement.id, + }); + } else { + setComment(null); + } + } + }); + }; + + const renderCommentIcons = () => { + return Object.values(commentIcons).map((commentIcon) => { + if (!excalidrawAPI) { + return false; + } + const appState = excalidrawAPI.getAppState(); + const { x, y } = sceneCoordsToViewportCoords( + { sceneX: commentIcon.x, sceneY: commentIcon.y }, + 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`, + position: "absolute", + zIndex: 1, + width: `${COMMENT_ICON_DIMENSION}px`, + height: `${COMMENT_ICON_DIMENSION}px`, + cursor: "pointer", + touchAction: "none", + }} + className="comment-icon" + onPointerDown={(event) => { + event.preventDefault(); + if (comment) { + commentIcon.value = comment.value; + saveComment(); + } + const pointerDownState: any = { + x: event.clientX, + y: event.clientY, + hitElement: commentIcon, + hitElementOffsets: { x: event.clientX - x, y: event.clientY - y }, + }; + const onPointerMove = + onPointerMoveFromPointerDownHandler(pointerDownState); + const onPointerUp = + onPointerUpFromPointerDownHandler(pointerDownState); + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); + + pointerDownState.onMove = onPointerMove; + pointerDownState.onUp = onPointerUp; + + excalidrawAPI?.setActiveTool({ + type: "custom", + customType: "comment", + }); + }} + > + <div className="comment-avatar"> + <img src="images/doremon.png" alt="doremon" /> + </div> + </div> + ); + }); + }; + + const saveComment = () => { + if (!comment) { + return; + } + if (!comment.id && !comment.value) { + setComment(null); + return; + } + const id = comment.id || nanoid(); + setCommentIcons({ + ...commentIcons, + [id]: { + x: comment.id ? comment.x - 60 : comment.x, + y: comment.y, + id, + value: comment.value, + }, + }); + setComment(null); + }; + + const renderComment = () => { + if (!comment) { + return null; + } + const appState = excalidrawAPI?.getAppState()!; + const { x, y } = sceneCoordsToViewportCoords( + { sceneX: comment.x, sceneY: comment.y }, + appState, + ); + let top = y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop; + let left = x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft; + + if ( + top + COMMENT_INPUT_HEIGHT < + appState.offsetTop + COMMENT_INPUT_HEIGHT + ) { + top = COMMENT_ICON_DIMENSION / 2; + } + if (top + COMMENT_INPUT_HEIGHT > appState.height) { + top = appState.height - COMMENT_INPUT_HEIGHT - COMMENT_ICON_DIMENSION / 2; + } + if ( + left + COMMENT_INPUT_WIDTH < + appState.offsetLeft + COMMENT_INPUT_WIDTH + ) { + left = COMMENT_ICON_DIMENSION / 2; + } + if (left + COMMENT_INPUT_WIDTH > appState.width) { + left = appState.width - COMMENT_INPUT_WIDTH - COMMENT_ICON_DIMENSION / 2; + } + + return ( + <textarea + className="comment" + style={{ + top: `${top}px`, + left: `${left}px`, + position: "absolute", + zIndex: 1, + height: `${COMMENT_INPUT_HEIGHT}px`, + width: `${COMMENT_INPUT_WIDTH}px`, + }} + ref={(ref) => { + setTimeout(() => ref?.focus()); + }} + placeholder={comment.value ? "Reply" : "Comment"} + value={comment.value} + onChange={(event) => { + setComment({ ...comment, value: event.target.value }); + }} + onBlur={saveComment} + onKeyDown={(event) => { + if (!event.shiftKey && event.key === "Enter") { + event.preventDefault(); + saveComment(); + } + }} + /> + ); + }; + + const renderMenu = () => { + return ( + <MainMenu> + <MainMenu.DefaultItems.SaveAsImage /> + <MainMenu.DefaultItems.Export /> + <MainMenu.Separator /> + <MainMenu.DefaultItems.LiveCollaborationTrigger + isCollaborating={isCollaborating} + onSelect={() => window.alert("You clicked on collab button")} + /> + <MainMenu.Group title="Excalidraw links"> + <MainMenu.DefaultItems.Socials /> + </MainMenu.Group> + <MainMenu.Separator /> + <MainMenu.ItemCustom> + <button + style={{ height: "2rem" }} + onClick={() => window.alert("custom menu item")} + > + custom item + </button> + </MainMenu.ItemCustom> + <MainMenu.DefaultItems.Help /> + + {excalidrawAPI && ( + <MobileFooter + excalidrawLib={excalidrawLib} + excalidrawAPI={excalidrawAPI} + /> + )} + </MainMenu> + ); + }; + + return ( + <div className="App" ref={appRef}> + <h1>{appTitle}</h1> + {/* TODO fix type */} + <ExampleSidebar> + <div className="button-wrapper"> + <button onClick={loadSceneOrLibrary}>Load Scene or Library</button> + <button className="update-scene" onClick={updateScene}> + Update Scene + </button> + <button + className="reset-scene" + onClick={() => { + excalidrawAPI?.resetScene(); + }} + > + Reset Scene + </button> + <button + onClick={() => { + 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, + }); + }} + > + Update Library + </button> + + <label> + <input + type="checkbox" + checked={viewModeEnabled} + onChange={() => setViewModeEnabled(!viewModeEnabled)} + /> + View mode + </label> + <label> + <input + type="checkbox" + checked={zenModeEnabled} + onChange={() => setZenModeEnabled(!zenModeEnabled)} + /> + Zen mode + </label> + <label> + <input + type="checkbox" + checked={gridModeEnabled} + onChange={() => setGridModeEnabled(!gridModeEnabled)} + /> + Grid mode + </label> + <label> + <input + type="checkbox" + checked={theme === "dark"} + onChange={() => { + setTheme(theme === "light" ? "dark" : "light"); + }} + /> + Switch to Dark Theme + </label> + <label> + <input + type="checkbox" + checked={disableImageTool === true} + onChange={() => { + setDisableImageTool(!disableImageTool); + }} + /> + Disable Image Tool + </label> + <label> + <input + type="checkbox" + checked={isCollaborating} + onChange={() => { + if (!isCollaborating) { + const collaborators = new Map(); + collaborators.set("id1", { + username: "Doremon", + avatarUrl: "images/doremon.png", + }); + collaborators.set("id2", { + username: "Excalibot", + avatarUrl: "images/excalibot.png", + }); + collaborators.set("id3", { + username: "Pika", + avatarUrl: "images/pika.jpeg", + }); + collaborators.set("id4", { + username: "fallback", + avatarUrl: "https://example.com", + }); + excalidrawAPI?.updateScene({ collaborators }); + } else { + excalidrawAPI?.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 + style={{ + display: "flex", + gap: "1em", + justifyContent: "center", + marginTop: "1em", + }} + > + <div>x: {pointerData?.pointer.x ?? 0}</div> + <div>y: {pointerData?.pointer.y ?? 0}</div> + </div> + </div> + <div className="excalidraw-wrapper"> + {renderExcalidraw(children)} + {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} + {comment && renderComment()} + </div> + + <div className="export-wrapper button-wrapper"> + <label className="export-wrapper__checkbox"> + <input + type="checkbox" + checked={exportWithDarkMode} + onChange={() => setExportWithDarkMode(!exportWithDarkMode)} + /> + Export with dark mode + </label> + <label className="export-wrapper__checkbox"> + <input + type="checkbox" + checked={exportEmbedScene} + onChange={() => setExportEmbedScene(!exportEmbedScene)} + /> + Export with embed scene + </label> + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const svg = await exportToSvg({ + elements: excalidrawAPI?.getSceneElements(), + appState: { + ...initialData.appState, + exportWithDarkMode, + exportEmbedScene, + width: 300, + height: 100, + }, + files: excalidrawAPI?.getFiles(), + }); + appRef.current.querySelector(".export-svg").innerHTML = + svg.outerHTML; + }} + > + Export to SVG + </button> + <div className="export export-svg"></div> + + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const blob = await exportToBlob({ + elements: excalidrawAPI?.getSceneElements(), + mimeType: "image/png", + appState: { + ...initialData.appState, + exportEmbedScene, + exportWithDarkMode, + }, + files: excalidrawAPI?.getFiles(), + }); + setBlobUrl(window.URL.createObjectURL(blob)); + }} + > + Export to Blob + </button> + <div className="export export-blob"> + <img src={blobUrl} alt="" /> + </div> + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const canvas = await exportToCanvas({ + elements: excalidrawAPI.getSceneElements(), + appState: { + ...initialData.appState, + exportWithDarkMode, + }, + files: excalidrawAPI.getFiles(), + }); + const ctx = canvas.getContext("2d")!; + ctx.font = "30px Excalifont"; + ctx.strokeText("My custom text", 50, 60); + setCanvasUrl(canvas.toDataURL()); + }} + > + Export to Canvas + </button> + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const canvas = await exportToCanvas({ + elements: excalidrawAPI.getSceneElements(), + appState: { + ...initialData.appState, + exportWithDarkMode, + }, + files: excalidrawAPI.getFiles(), + }); + const ctx = canvas.getContext("2d")!; + ctx.font = "30px Excalifont"; + ctx.strokeText("My custom text", 50, 60); + setCanvasUrl(canvas.toDataURL()); + }} + > + Export to Canvas + </button> + <button + type="button" + onClick={() => { + if (!excalidrawAPI) { + return; + } + + const elements = excalidrawAPI.getSceneElements(); + excalidrawAPI.scrollToContent(elements[0], { + fitToViewport: true, + }); + }} + > + Fit to viewport, first element + </button> + <button + type="button" + onClick={() => { + if (!excalidrawAPI) { + return; + } + + const elements = excalidrawAPI.getSceneElements(); + excalidrawAPI.scrollToContent(elements[0], { + fitToContent: true, + }); + + excalidrawAPI.scrollToContent(elements[0], { + fitToContent: true, + }); + }} + > + Fit to content, first element + </button> + <button + type="button" + onClick={() => { + if (!excalidrawAPI) { + return; + } + + const elements = excalidrawAPI.getSceneElements(); + excalidrawAPI.scrollToContent(elements[0], { + fitToContent: true, + }); + + excalidrawAPI.scrollToContent(elements[0]); + }} + > + Scroll to first element, no fitToContent, no fitToViewport + </button> + <div className="export export-canvas"> + <img src={canvasUrl} alt="" /> + </div> + </div> + </ExampleSidebar> + </div> + ); +} diff --git a/examples/with-script-in-browser/components/MobileFooter.tsx b/examples/with-script-in-browser/components/MobileFooter.tsx new file mode 100644 index 0000000..c8fc0f1 --- /dev/null +++ b/examples/with-script-in-browser/components/MobileFooter.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; +import CustomFooter from "./CustomFooter"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +const MobileFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { useDevice, Footer } = excalidrawLib; + + const device = useDevice(); + if (device.editor.isMobile) { + return ( + <Footer> + <CustomFooter + excalidrawAPI={excalidrawAPI} + excalidrawLib={excalidrawLib} + /> + </Footer> + ); + } + return null; +}; +export default MobileFooter; diff --git a/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss new file mode 100644 index 0000000..773a8ff --- /dev/null +++ b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss @@ -0,0 +1,66 @@ +.sidebar { + height: 100%; + width: 0; + position: absolute; + z-index: 1; + top: 0; + left: 0; + background-color: #111; + overflow-x: hidden; + transition: 0.5s; + padding-top: 60px; + + &.open { + width: 300px; + } + + &-links { + display: flex; + flex-direction: column; + padding: 20px; + + button { + padding: 10px; + margin: 10px; + background: #faa2c1; + color: #fff; + border: none; + cursor: pointer; + } + } +} + +.sidebar a { + padding: 8px 8px 8px 32px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; + transition: 0.3s; +} + +.sidebar a:hover { + color: #f1f1f1; +} + +.sidebar .closebtn { + position: absolute; + top: 0; + right: 0; + font-size: 36px; + margin-left: 50px; +} + +.openbtn { + font-size: 20px; + cursor: pointer; + background-color: #111; + color: white; + padding: 10px 15px; + border: none; + display: flex; + margin-left: 50px; +} +.sidebar-open { + margin-left: 300px; +} diff --git a/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx new file mode 100644 index 0000000..1939134 --- /dev/null +++ b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx @@ -0,0 +1,31 @@ +import React, { useState } from "react"; +import "./ExampleSidebar.scss"; + +export default function Sidebar({ children }: { children: React.ReactNode }) { + const [open, setOpen] = useState(false); + + return ( + <> + <div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}> + <button className="closebtn" onClick={() => setOpen(false)}> + x + </button> + <div className="sidebar-links"> + <button>Empty Home</button> + <button>Empty About</button> + </div> + </div> + <div className={`${open ? "sidebar-open" : ""}`}> + <button + className="openbtn" + onClick={() => { + setOpen(!open); + }} + > + Open Sidebar + </button> + {children} + </div> + </> + ); +} diff --git a/examples/with-script-in-browser/index.html b/examples/with-script-in-browser/index.html new file mode 100644 index 0000000..dbbc4ca --- /dev/null +++ b/examples/with-script-in-browser/index.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no" + /> + <meta name="theme-color" content="#000000" /> + + <title>React App</title> + <script> + window.name = "codesandbox"; + window.EXCALIDRAW_ASSET_PATH = + "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/"; + </script> + </head> + + <body> + <noscript> You need to enable JavaScript to run this app. </noscript> + <div id="root"></div> + + <script type="module"> + import * as ExcalidrawLib from "@excalidraw/excalidraw"; + + console.log(ExcalidrawLib); + window.ExcalidrawLib = ExcalidrawLib; + </script> + <script type="module" src="index.tsx"></script> + </body> +</html> diff --git a/examples/with-script-in-browser/index.tsx b/examples/with-script-in-browser/index.tsx new file mode 100644 index 0000000..25ad96e --- /dev/null +++ b/examples/with-script-in-browser/index.tsx @@ -0,0 +1,28 @@ +import App from "./components/ExampleApp"; +import React, { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +import "@excalidraw/excalidraw/index.css"; + +declare global { + interface Window { + ExcalidrawLib: typeof TExcalidraw; + } +} + +const rootElement = document.getElementById("root")!; +const root = createRoot(rootElement); +const { Excalidraw } = window.ExcalidrawLib; +root.render( + <StrictMode> + <App + appTitle={"Excalidraw Example"} + useCustom={(api: any, args?: any[]) => {}} + excalidrawLib={window.ExcalidrawLib} + > + <Excalidraw /> + </App> + </StrictMode>, +); diff --git a/examples/with-script-in-browser/initialData.tsx b/examples/with-script-in-browser/initialData.tsx new file mode 100644 index 0000000..0db23d5 --- /dev/null +++ b/examples/with-script-in-browser/initialData.tsx @@ -0,0 +1,994 @@ +import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform"; +import type { FileId } from "@excalidraw/excalidraw/element/types"; + +const elements: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + id: "1", + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + id: "2", + }, + { + type: "arrow", + x: 100, + y: 200, + label: { text: "HELLO WORLD!!" }, + start: { type: "rectangle" }, + end: { type: "ellipse" }, + }, + { + type: "image", + x: 606.1042326312408, + y: 153.57729779411773, + width: 230, + height: 230, + fileId: "rocket" as FileId, + }, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + }, +]; +export default { + elements, + appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 5 }, + scrollToContent: true, + libraryItems: [ + [ + { + type: "line", + + x: 209.72304760646858, + y: 338.83587294718825, + strokeColor: "#881fa3", + backgroundColor: "#be4bdb", + width: 116.42036295658873, + height: 103.65107323746608, + strokeSharpness: "sharp", + points: [ + [-92.28090097254909, 7.105427357601002e-15], + [-154.72281841151394, 19.199290805487394], + [-155.45758928571422, 79.43840749607878], + [-99.89923520113778, 103.6510732374661], + [-40.317783799181804, 79.1587107641305], + [-39.037226329125524, 21.285677238400705], + [-92.28090097254909, 7.105427357601002e-15], + ], + }, + ], + [ + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + x: -249.48446738689245, + y: 374.851387389359, + strokeColor: "#0a11d3", + backgroundColor: "#228be6", + width: 88.21658171083376, + height: 113.8575037534261, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + points: [ + [-0.22814350714115691, -43.414939319563715], + [0.06274947619197979, 42.63794490105306], + [-0.21453039840335475, 52.43469208825097], + [4.315205554872581, 56.66774540453215], + [20.089784992984285, 60.25027917349701], + [46.7532926683984, 61.365826671969444], + [72.22851104292477, 59.584691681394986], + [85.76368213524371, 55.325139565662596], + [87.67263486434864, 51.7342924478499], + [87.94074036468018, 43.84700272879395], + [87.73030872197806, -36.195582644606276], + [87.2559282533682, -43.758132174307036], + [81.5915337527493, -47.984890854524416], + [69.66352776578219, -50.4328058257654], + [42.481213744224995, -52.49167708145666], + [20.68789182864576, -51.26396751574663], + [3.5475921483286084, -47.099726468136254], + [-0.2758413461535838, -43.46664538034193], + [-0.22814350714115691, -43.414939319563715], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -249.02524930453623, + y: 398.8804363713438, + strokeColor: "#0a11d3", + backgroundColor: "transparent", + width: 88.30808627974527, + height: 9.797916664247975, + seed: 683951089, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + + points: [ + [0, -2.1538602707609424], + [2.326538897826852, 1.751753055375216], + [12.359939318521995, 5.028526743934819], + [25.710950037209347, 7.012921076245119], + [46.6269757640547, 7.193749997581346], + [71.03526003420632, 5.930375670950649], + [85.2899738827162, 1.3342483900732343], + [88.30808627974527, -2.6041666666666288], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -250.11899081659772, + y: 365.80628180927204, + strokeColor: "#0a11d3", + backgroundColor: "transparent", + width: 88.30808627974527, + height: 9.797916664247975, + seed: 1817746897, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + + points: [ + [0, -2.1538602707609424], + [2.326538897826852, 1.751753055375216], + [12.359939318521995, 5.028526743934819], + [25.710950037209347, 7.012921076245119], + [46.6269757640547, 7.193749997581346], + [71.03526003420632, 5.930375670950649], + [85.2899738827162, 1.3342483900732343], + [88.30808627974527, -2.6041666666666288], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -251.23981350275943, + y: 323.4117518426986, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 87.65074610854188, + height: 17.72670397681366, + seed: 1409727409, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 347.98755471983213, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 1073094033, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 378.5900085788926, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 526271345, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 411.8508097533892, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 243707217, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + }, + ], + [ + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 381.22641397493356, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 511870335, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 372.354634046675, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 1283079231, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 359.72407445196296, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 996251633, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 347.1924021546656, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 1764842481, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + ], + [ + { + type: "line", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -471.6208001976387, + y: 520.7681448415112, + strokeColor: "#087f5b", + backgroundColor: "#40c057", + width: 52.317507746132115, + height: 154.56722543646003, + seed: 1424381745, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + + points: [ + [-0.24755378372925183, -40.169554027464216], + [-0.07503751055611152, 76.6515171914404], + [-0.23948042713317108, 89.95108885873196], + [2.446913573036335, 95.69766931810295], + [11.802146636255692, 100.56113713047068], + [27.615140546177496, 102.07554835500338], + [42.72341054254274, 99.65756899883291], + [50.75054563137204, 93.87501510096598], + [51.88266441510958, 89.00026150397161], + [52.04166639997853, 78.29287333983132], + [51.916868330459295, -30.36891819848148], + [51.635533423123285, -40.63545540065934], + [48.27622163143906, -46.37349057843314], + [41.202227904674494, -49.69665692879073], + [25.081551986374073, -52.49167708145666], + [12.15685839679867, -50.825000270901], + [1.9916746648394732, -45.171835889467935], + [-0.2758413461535838, -40.23974757720194], + [-0.24755378372925183, -40.169554027464216], + ], + }, + { + type: "line", + version: 2405, + versionNonce: 2120341087, + isDeleted: false, + id: "TYsYe2VvJ60T_yKa3kyOw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -496.3957643857249, + y: 541.7241190920508, + strokeColor: "#087f5b", + backgroundColor: "transparent", + width: 50.7174766392476, + height: 12.698053371678215, + seed: 726657713, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + + points: [ + [0, -2.0205717204386002], + [1.3361877396713384, 3.0410845646550486], + [7.098613049589299, 7.287767671898479], + [14.766422451441104, 9.859533283467512], + [26.779003528407447, 10.093886705011586], + [40.79727342221974, 8.456559589697127], + [48.98410145879092, 2.500000505196364], + [50.7174766392476, -2.6041666666666288], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -450.969983237283, + y: 542.1789894334747, + strokeColor: "#087f5b", + backgroundColor: "transparent", + width: 50.57247907260371, + height: 10.178760037658167, + seed: 1977326481, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + + points: [ + [0, -2.136356936862347], + [1.332367676378171, 1.9210669226078037], + [7.078318632616268, 5.325208253515953], + [14.724206326638113, 7.386735659885842], + [26.70244431044034, 7.574593370991538], + [40.68063699304561, 6.262111896696538], + [48.84405948536458, 1.4873339211608216], + [50.57247907260371, -2.6041666666666288], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -404.36521010516793, + y: 534.1894365757241, + strokeColor: "#087f5b", + backgroundColor: "#fff", + width: 51.27812853552538, + height: 22.797152568995934, + seed: 1774660383, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "sharp", + boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], + }, + ], + [ + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -393.3000561423187, + y: 338.9742643666818, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 371096063, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -400.8474891780329, + y: 331.95417508096745, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 685932433, + groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -410.24257846374826, + y: 323.7002688309677, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 58634943, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -398.2561518768373, + y: 371.84603609547054, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.57983585730082, + height: 3.249953844290203, + seed: 1673003743, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0.6014697828497827], + [40.42449133807562, 0.7588628355182573], + [46.57983585730082, -2.491091008771946], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -396.400899638823, + y: 340.9822185794818, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 45.567415680676426, + height: 2.8032978840147194, + seed: 1821527807, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [16.832548902953302, -2.8032978840147194], + [45.567415680676426, -0.3275477042019195], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -396.4774991551924, + y: 408.37659284983897, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 48.33668263438425, + height: 4.280657518731036, + seed: 1485707039, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [26.41225578429045, -0.2552319773002338], + [37.62000339651456, 2.3153712935189787], + [48.33668263438425, -1.9652862252120569], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.6615463367227, + y: 419.61974125811776, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 54.40694982784246, + height: 2.9096445412231735, + seed: 1042012991, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [10.166093050596771, -1.166642430373031], + [16.130660965377448, -0.8422655250909383], + [46.26079588567538, 0.6125567455206506], + [54.40694982784246, -2.297087795702523], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.3767034411569, + y: 356.042820132743, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.92865289294453, + height: 2.4757501798128, + seed: 295443295, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [18.193786115221407, -0.5912874140789839], + [46.92865289294453, 1.884462765733816], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.26921524500654, + y: 390.5261491685826, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.92865289294453, + height: 2.4757501798128, + seed: 1734301567, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [8.093938105125233, 1.4279702913643746], + [18.193786115221407, -0.5912874140789839], + [46.92865289294453, 1.884462765733816], + ], + }, + ], + [ + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -593.9896997899341, + y: 343.9798351106279, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 127.88383573213892, + height: 76.53703389977764, + seed: 106569279, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -595.0652975408293, + y: 354.6963695028721, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 128.84193229844433, + height: 0, + seed: 73916127, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + + points: [ + [0, 0], + [128.84193229844433, 0], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -589.5016643209792, + y: 348.2514049106367, + strokeColor: "#000000", + backgroundColor: "#fa5252", + width: 5.001953125, + height: 5.001953125, + seed: 387857791, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -579.2389690084792, + y: 348.2514049106367, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 5.001953125, + height: 5.001953125, + seed: 1486370207, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -568.525552542133, + y: 348.7021260644829, + strokeColor: "#000000", + backgroundColor: "#40c057", + width: 5.001953125, + height: 5.001953125, + seed: 610150847, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -552.4984915525058, + y: 364.75449494249875, + strokeColor: "#000000", + backgroundColor: "#04aaf7", + width: 42.72020253937572, + height: 42.72020253937572, + seed: 144280593, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + x: -530.327851842306, + y: 378.9357912947449, + strokeColor: "#087f5b", + backgroundColor: "#40c057", + width: 28.226201983883442, + height: 24.44112284281997, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + points: [ + [4.907524351775825, 2.043055712211473], + [3.0769604829149455, 1.6284171290602836], + [-2.66472604008681, -4.228569719133945], + [-6.450168189798415, -2.304577297733668], + [-7.704241049212052, 4.416384506147983], + [-6.361372181234263, 8.783101300254884], + [-12.516984713388897, 10.9291595737194], + [-12.295677738198286, 15.686226498407976], + [-7.473371426945252, 15.393030178104425], + [-3.787654025313423, 11.5207568827343], + [1.2873793872375165, 19.910682356036197], + [4.492232250183542, 20.212553123686025], + [1.1302787567009416, 6.843494873631317], + [6.294108177816019, 6.390688722156585], + [8.070028349098962, 7.910451897221202], + [14.143675334886687, 7.910451897221202], + [15.709217270494545, 2.6780252579576427], + [9.128749989671498, 3.1533849725326517], + [10.393751588600717, -3.7167773257046695], + [7.380151667177483, -3.30213874255348], + [4.669824267311791, 1.1200945145694894], + [4.907524351775825, 2.043055712211473], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -551.4394290784783, + y: 385.71736850567976, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 42.095115772272244, + height: 0, + seed: 1443027377, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + + points: [ + [0, 0], + [42.095115772272244, 0], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 90, + angle: 0, + x: -546.3441000487039, + y: 372.6245229061568, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 29.31860660384862, + height: 5.711199931375845, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + points: [ + [0, -2.341683327443203], + [0.7724193963150375, -0.06510358900749044], + [4.103544916365185, 1.84492589414448], + [8.536129150893453, 3.0016281808630056], + [15.480325949120388, 3.1070332647092163], + [23.583965316012858, 2.3706131055211244], + [28.316582284417855, -0.3084668090492442], + [29.31860660384862, -2.6041666666666288], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -538.2701841247845, + y: 363.37196531290607, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 15.528434353116108, + height: 44.82230388130942, + seed: 683572113, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + opacity: 90, + x: -544.828148539078, + y: 402.0199316371545, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 29.31860660384862, + height: 5.896061363392446, + seed: 318798801, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + + points: [ + [0, 0], + [4.103544916365185, -4.322122351104391], + [8.536129150893453, -5.516265043290966], + [15.480325949120388, -5.625081903117008], + [23.583965316012858, -4.8648251269605955], + [28.316582284417855, -2.0990281379671547], + [29.31860660384862, 0.2709794602754383], + ], + }, + ], + [ + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -715.1043446306466, + y: 330.4231266309418, + strokeColor: "#000000", + backgroundColor: "#ced4da", + width: 70.81644178885557, + height: 108.30428902193904, + seed: 1914896753, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -706.996640540555, + y: 338.68030798133873, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 55.801163535143246, + height: 82.83278895375764, + seed: 1306468145, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -684.8099707762028, + y: 425.0579911039235, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 11.427824006438863, + height: 11.427824006438863, + seed: 93422161, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "rectangle", + fillStyle: "cross-hatch", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -698.7169501405845, + y: 349.2244646574789, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 39.2417827352022, + height: 19.889460471185775, + seed: 11646495, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "rectangle", + fillStyle: "cross-hatch", + strokeWidth: 1, + strokeStyle: "solid", + x: -698.7169501405845, + y: 384.7822247024333, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 39.2417827352022, + height: 19.889460471185775, + seed: 291717649, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + ], + ], +}; diff --git a/examples/with-script-in-browser/package.json b/examples/with-script-in-browser/package.json new file mode 100644 index 0000000..3d61f1a --- /dev/null +++ b/examples/with-script-in-browser/package.json @@ -0,0 +1,21 @@ +{ + "name": "with-script-in-browser", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "19.0.0", + "react-dom": "19.0.0", + "@excalidraw/excalidraw": "*", + "browser-fs-access": "0.29.1" + }, + "devDependencies": { + "vite": "5.0.12", + "typescript": "^5" + }, + "scripts": { + "start": "vite", + "build": "vite build", + "build:preview": "yarn build && vite preview --port 5002", + "build:package": "yarn workspace @excalidraw/excalidraw run build:esm" + } +} diff --git a/examples/with-script-in-browser/public/images/doremon.png b/examples/with-script-in-browser/public/images/doremon.png Binary files differnew file mode 100644 index 0000000..36208a4 --- /dev/null +++ b/examples/with-script-in-browser/public/images/doremon.png diff --git a/examples/with-script-in-browser/public/images/excalibot.png b/examples/with-script-in-browser/public/images/excalibot.png Binary files differnew file mode 100644 index 0000000..7928ec3 --- /dev/null +++ b/examples/with-script-in-browser/public/images/excalibot.png diff --git a/examples/with-script-in-browser/public/images/pika.jpeg b/examples/with-script-in-browser/public/images/pika.jpeg Binary files differnew file mode 100644 index 0000000..455ed52 --- /dev/null +++ b/examples/with-script-in-browser/public/images/pika.jpeg diff --git a/examples/with-script-in-browser/public/images/rocket.jpeg b/examples/with-script-in-browser/public/images/rocket.jpeg Binary files differnew file mode 100644 index 0000000..f17a74b --- /dev/null +++ b/examples/with-script-in-browser/public/images/rocket.jpeg diff --git a/examples/with-script-in-browser/tsconfig.json b/examples/with-script-in-browser/tsconfig.json new file mode 100644 index 0000000..be262d1 --- /dev/null +++ b/examples/with-script-in-browser/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "skipLibCheck": true + } +} diff --git a/examples/with-script-in-browser/utils.ts b/examples/with-script-in-browser/utils.ts new file mode 100644 index 0000000..a77b93f --- /dev/null +++ b/examples/with-script-in-browser/utils.ts @@ -0,0 +1,145 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { fileOpen as _fileOpen } from "browser-fs-access"; +import { MIME_TYPES } from "@excalidraw/excalidraw"; + +type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; + +const INPUT_CHANGE_INTERVAL_MS = 500; + +export type ResolvablePromise<T> = Promise<T> & { + resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; + reject: (error: Error) => void; +}; +export const resolvablePromise = <T>() => { + let resolve!: any; + let reject!: any; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + (promise as any).resolve = resolve; + (promise as any).reject = reject; + return promise as ResolvablePromise<T>; +}; + +export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { + const xd = x2 - x1; + const yd = y2 - y1; + return Math.hypot(xd, yd); +}; + +export const fileOpen = <M extends boolean | undefined = false>(opts: { + extensions?: FILE_EXTENSION[]; + description: string; + multiple?: M; +}): Promise<M extends false | undefined ? File : File[]> => { + // an unsafe TS hack, alas not much we can do AFAIK + type RetType = M extends false | undefined ? File : File[]; + + const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { + mimeTypes.push(MIME_TYPES[type]); + + return mimeTypes; + }, [] as string[]); + + const extensions = opts.extensions?.reduce((acc, ext) => { + if (ext === "jpg") { + return acc.concat(".jpg", ".jpeg"); + } + return acc.concat(`.${ext}`); + }, [] as string[]); + + return _fileOpen({ + description: opts.description, + extensions, + mimeTypes, + multiple: opts.multiple ?? false, + legacySetup: (resolve, reject, input) => { + const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); + const focusHandler = () => { + checkForFile(); + document.addEventListener("keyup", scheduleRejection); + document.addEventListener("pointerup", scheduleRejection); + scheduleRejection(); + }; + const checkForFile = () => { + // this hack might not work when expecting multiple files + if (input.files?.length) { + const ret = opts.multiple ? [...input.files] : input.files[0]; + resolve(ret as RetType); + } + }; + requestAnimationFrame(() => { + window.addEventListener("focus", focusHandler); + }); + const interval = window.setInterval(() => { + checkForFile(); + }, INPUT_CHANGE_INTERVAL_MS); + return (rejectPromise) => { + clearInterval(interval); + scheduleRejection.cancel(); + window.removeEventListener("focus", focusHandler); + document.removeEventListener("keyup", scheduleRejection); + document.removeEventListener("pointerup", scheduleRejection); + if (rejectPromise) { + // so that something is shown in console if we need to debug this + console.warn("Opening the file was canceled (legacy-fs)."); + rejectPromise(new Error("Request Aborted")); + } + }; + }, + }) as Promise<RetType>; +}; + +export const debounce = <T extends any[]>( + fn: (...args: T) => void, + timeout: number, +) => { + let handle = 0; + let lastArgs: T | null = null; + const ret = (...args: T) => { + lastArgs = args; + clearTimeout(handle); + handle = window.setTimeout(() => { + lastArgs = null; + fn(...args); + }, timeout); + }; + ret.flush = () => { + clearTimeout(handle); + if (lastArgs) { + const _lastArgs = lastArgs; + lastArgs = null; + fn(..._lastArgs); + } + }; + ret.cancel = () => { + lastArgs = null; + clearTimeout(handle); + }; + return ret; +}; + +export const withBatchedUpdates = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never, +) => + ((event) => { + unstable_batchedUpdates(func as TFunction, event); + }) as TFunction; + +/** + * barches React state updates and throttles the calls to a single call per + * animation frame + */ +export const withBatchedUpdatesThrottled = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF<Parameters<TFunction>>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; diff --git a/examples/with-script-in-browser/vercel.json b/examples/with-script-in-browser/vercel.json new file mode 100644 index 0000000..99a5811 --- /dev/null +++ b/examples/with-script-in-browser/vercel.json @@ -0,0 +1,5 @@ +{ + "outputDirectory": "dist", + "installCommand": "yarn install", + "buildCommand": "yarn build:package && yarn build" +} diff --git a/examples/with-script-in-browser/vite.config.mts b/examples/with-script-in-browser/vite.config.mts new file mode 100644 index 0000000..3ff0ac6 --- /dev/null +++ b/examples/with-script-in-browser/vite.config.mts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3001, + // open the browser + open: true, + }, + publicDir: "public", + optimizeDeps: { + esbuildOptions: { + // Bumping to 2022 due to "Arbitrary module namespace identifier names" not being + // supported in Vite's default browser target https://github.com/vitejs/vite/issues/13556 + target: "es2022", + treeShaking: true, + }, + }, +}); |
