excalidraw/src/packages/excalidraw/index.tsx
Aakansha Doshi 3ea07076ad
feat: support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically (#6546)
* feat: support creating text containers programatically

* fix

* fix

* fix

* fix

* update api to use label

* fix api and support individual shapes and text element

* update test case in package example

* support creating arrows and line

* support labelled arrows

* add in package example

* fix alignment

* better types

* fix

* keep element as is unless we support prog api

* fix tests

* fix lint

* ignore

* support arrow bindings via start and end in api

* fix lint

* fix coords

* support id as well for elements

* preserve bindings if present and fix testcases

* preserve bindings for labelled arrows

* support ids, clean up code and move the api related stuff to transform.ts

* allow multiple arrows to bind to single element

* fix singular elements

* fix single text element, unique id and tests

* fix lint

* fix

* support binding arrow to text element

* fix creation of regular text

* use same stroke color as parent for text containers and height 0 for linear element by default

* fix types

* fix

* remove more ts ignore

* remove ts ignore

* remove

* Add coverage script

* Add tests

* fix tests

* make type optional when id present

* remove type when id provided in tests

* Add more tests

* tweak

* let host call convertToExcalidrawElements when using programmatic API

* remove convertToExcalidrawElements call from restore

* lint

* update snaps

* Add new type excalidraw-api/clipboard for programmatic api

* cleanup

* rename tweak

* tweak

* make image attributes optional and better ts check

* support image via programmatic API

* fix lint

* more types

* make fileId mandatory for image and export convertToExcalidrawElements

* fix

* small tweaks

* update snaps

* fix

* use Object.assign instead of mutateElement

* lint

* preserve z-index by pushing all elements first and then add bindings

* instantiate instead of closure for storing elements

* use element API to create regular text, diamond, ellipse and rectangle

* fix snaps

* udpdate api

* ts fixes

* make `convertToExcalidrawElements` more typesafe

* update snaps

* refactor the approach so that order of elements doesn't matter

* Revert "update snaps"

This reverts commit 621dfadccf.

* review fixes

* rename ExcalidrawProgrammaticElement -> ExcalidrawELementSkeleton

* Add tests

* give preference to first element when duplicate ids found

* use console.error

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-08-09 16:41:15 +05:30

256 lines
7.1 KiB
TypeScript

import React, { useEffect, forwardRef } from "react";
import { InitializeApp } from "../../components/InitializeApp";
import App from "../../components/App";
import { isShallowEqual } from "../../utils";
import "../../css/app.scss";
import "../../css/styles.scss";
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
import { defaultLang } from "../../i18n";
import { DEFAULT_UI_OPTIONS } from "../../constants";
import { Provider } from "jotai";
import { jotaiScope, jotaiStore } from "../../jotai";
import Footer from "../../components/footer/FooterCenter";
import MainMenu from "../../components/main-menu/MainMenu";
import WelcomeScreen from "../../components/welcome-screen/WelcomeScreen";
import LiveCollaborationTrigger from "../../components/live-collaboration/LiveCollaborationTrigger";
const ExcalidrawBase = (props: ExcalidrawProps) => {
const {
onChange,
initialData,
excalidrawRef,
isCollaborating = false,
onPointerUpdate,
renderTopRightUI,
langCode = defaultLang.code,
viewModeEnabled,
zenModeEnabled,
gridModeEnabled,
libraryReturnUrl,
theme,
name,
renderCustomStats,
onPaste,
detectScroll = true,
handleKeyboardGlobally = false,
onLibraryChange,
autoFocus = false,
generateIdForFile,
onLinkOpen,
onPointerDown,
onScrollChange,
children,
validateEmbeddable,
renderEmbeddable,
} = props;
const canvasActions = props.UIOptions?.canvasActions;
// FIXME normalize/set defaults in parent component so that the memo resolver
// compares the same values
const UIOptions: AppProps["UIOptions"] = {
...props.UIOptions,
canvasActions: {
...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions,
},
};
if (canvasActions?.export) {
UIOptions.canvasActions.export.saveFileToDisk =
canvasActions.export?.saveFileToDisk ??
DEFAULT_UI_OPTIONS.canvasActions.export.saveFileToDisk;
}
if (
UIOptions.canvasActions.toggleTheme === null &&
typeof theme === "undefined"
) {
UIOptions.canvasActions.toggleTheme = true;
}
useEffect(() => {
// Block pinch-zooming on iOS outside of the content area
const handleTouchMove = (event: TouchEvent) => {
// @ts-ignore
if (typeof event.scale === "number" && event.scale !== 1) {
event.preventDefault();
}
};
document.addEventListener("touchmove", handleTouchMove, {
passive: false,
});
return () => {
document.removeEventListener("touchmove", handleTouchMove);
};
}, []);
return (
<Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
<InitializeApp langCode={langCode} theme={theme}>
<App
onChange={onChange}
initialData={initialData}
excalidrawRef={excalidrawRef}
isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate}
renderTopRightUI={renderTopRightUI}
langCode={langCode}
viewModeEnabled={viewModeEnabled}
zenModeEnabled={zenModeEnabled}
gridModeEnabled={gridModeEnabled}
libraryReturnUrl={libraryReturnUrl}
theme={theme}
name={name}
renderCustomStats={renderCustomStats}
UIOptions={UIOptions}
onPaste={onPaste}
detectScroll={detectScroll}
handleKeyboardGlobally={handleKeyboardGlobally}
onLibraryChange={onLibraryChange}
autoFocus={autoFocus}
generateIdForFile={generateIdForFile}
onLinkOpen={onLinkOpen}
onPointerDown={onPointerDown}
onScrollChange={onScrollChange}
validateEmbeddable={validateEmbeddable}
renderEmbeddable={renderEmbeddable}
>
{children}
</App>
</InitializeApp>
</Provider>
);
};
type PublicExcalidrawProps = Omit<ExcalidrawProps, "forwardedRef">;
const areEqual = (
prevProps: PublicExcalidrawProps,
nextProps: PublicExcalidrawProps,
) => {
// short-circuit early
if (prevProps.children !== nextProps.children) {
return false;
}
const {
initialData: prevInitialData,
UIOptions: prevUIOptions = {},
...prev
} = prevProps;
const {
initialData: nextInitialData,
UIOptions: nextUIOptions = {},
...next
} = nextProps;
// comparing UIOptions
const prevUIOptionsKeys = Object.keys(prevUIOptions) as (keyof Partial<
typeof DEFAULT_UI_OPTIONS
>)[];
const nextUIOptionsKeys = Object.keys(nextUIOptions) as (keyof Partial<
typeof DEFAULT_UI_OPTIONS
>)[];
if (prevUIOptionsKeys.length !== nextUIOptionsKeys.length) {
return false;
}
const isUIOptionsSame = prevUIOptionsKeys.every((key) => {
if (key === "canvasActions") {
const canvasOptionKeys = Object.keys(
prevUIOptions.canvasActions!,
) as (keyof Partial<typeof DEFAULT_UI_OPTIONS.canvasActions>)[];
return canvasOptionKeys.every((key) => {
if (
key === "export" &&
prevUIOptions?.canvasActions?.export &&
nextUIOptions?.canvasActions?.export
) {
return (
prevUIOptions.canvasActions.export.saveFileToDisk ===
nextUIOptions.canvasActions.export.saveFileToDisk
);
}
return (
prevUIOptions?.canvasActions?.[key] ===
nextUIOptions?.canvasActions?.[key]
);
});
}
return prevUIOptions[key] === nextUIOptions[key];
});
return isUIOptionsSame && isShallowEqual(prev, next);
};
const forwardedRefComp = forwardRef<
ExcalidrawAPIRefValue,
PublicExcalidrawProps
>((props, ref) => <ExcalidrawBase {...props} excalidrawRef={ref} />);
export const Excalidraw = React.memo(forwardedRefComp, areEqual);
Excalidraw.displayName = "Excalidraw";
export {
getSceneVersion,
isInvisiblySmallElement,
getNonDeletedElements,
} from "../../element";
export { defaultLang, useI18n, languages } from "../../i18n";
export {
restore,
restoreAppState,
restoreElements,
restoreLibraryItems,
} from "../../data/restore";
export {
exportToCanvas,
exportToBlob,
exportToSvg,
serializeAsJSON,
serializeLibraryAsJSON,
loadLibraryFromBlob,
loadFromBlob,
loadSceneOrLibraryFromBlob,
getFreeDrawSvgPath,
exportToClipboard,
mergeLibraryItems,
} from "../../packages/utils";
export { isLinearElement } from "../../element/typeChecks";
export { FONT_FAMILY, THEME, MIME_TYPES } from "../../constants";
export {
mutateElement,
newElementWith,
bumpVersion,
} from "../../element/mutateElement";
export {
parseLibraryTokensFromUrl,
useHandleLibrary,
} from "../../data/library";
export {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
} from "../../utils";
export { Sidebar } from "../../components/Sidebar/Sidebar";
export { Button } from "../../components/Button";
export { Footer };
export { MainMenu };
export { useDevice } from "../../components/App";
export { WelcomeScreen };
export { LiveCollaborationTrigger };
export { DefaultSidebar } from "../../components/DefaultSidebar";
export { normalizeLink } from "../../data/url";
export { convertToExcalidrawElements } from "../../data/transform";