You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
970 lines
28 KiB
970 lines
28 KiB
import { nanoid } from "nanoid"; |
|
import React, { |
|
useEffect, |
|
useState, |
|
useRef, |
|
useCallback, |
|
Children, |
|
cloneElement, |
|
} from "react"; |
|
|
|
import type * as TExcalidraw from "@excalidraw/excalidraw"; |
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types"; |
|
import type { |
|
NonDeletedExcalidrawElement, |
|
Theme, |
|
} from "@excalidraw/excalidraw/element/types"; |
|
import type { |
|
AppState, |
|
BinaryFileData, |
|
ExcalidrawImperativeAPI, |
|
ExcalidrawInitialDataState, |
|
Gesture, |
|
LibraryItems, |
|
PointerDownState as ExcalidrawPointerDownState, |
|
} from "@excalidraw/excalidraw/types"; |
|
|
|
import initialData from "../initialData"; |
|
import { |
|
resolvablePromise, |
|
distance2d, |
|
fileOpen, |
|
withBatchedUpdates, |
|
withBatchedUpdatesThrottled, |
|
} from "../utils"; |
|
|
|
import CustomFooter from "./CustomFooter"; |
|
import MobileFooter from "./MobileFooter"; |
|
import ExampleSidebar from "./sidebar/ExampleSidebar"; |
|
|
|
import "./ExampleApp.scss"; |
|
|
|
import type { ResolvablePromise } from "../utils"; |
|
|
|
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 [renderScrollbars, setRenderScrollbars] = 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, |
|
renderScrollbars, |
|
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={renderScrollbars} |
|
onChange={() => setRenderScrollbars(!renderScrollbars)} |
|
/> |
|
Render scrollbars |
|
</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> |
|
); |
|
}
|
|
|