diff options
Diffstat (limited to 'examples/with-script-in-browser/components/ExampleApp.tsx')
| -rw-r--r-- | examples/with-script-in-browser/components/ExampleApp.tsx | 961 |
1 files changed, 961 insertions, 0 deletions
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> + ); +} |
