Compare commits
29 Commits
dwelle/red
...
zsviczian-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c3dffb082 | ||
|
|
0e0f34edd8 | ||
|
|
4888d9d355 | ||
|
|
1c39bd5781 | ||
|
|
90ad885446 | ||
|
|
1741c234a6 | ||
|
|
63b50b3586 | ||
|
|
e0fefa8025 | ||
|
|
d426cc968d | ||
|
|
2409c091ff | ||
|
|
626fe252ab | ||
|
|
10bd08ef19 | ||
|
|
2789d08154 | ||
|
|
678bb2b819 | ||
|
|
966f9aead9 | ||
|
|
4f0a2a9593 | ||
|
|
f3f8217125 | ||
|
|
89bd6181f2 | ||
|
|
c6fdac131b | ||
|
|
0415c616b1 | ||
|
|
740a165452 | ||
|
|
4997624a3a | ||
|
|
b66daae1f3 | ||
|
|
1e7df58b5b | ||
|
|
46da032626 | ||
|
|
3b0593baa7 | ||
|
|
dd530737a2 | ||
|
|
a4e5e46dd1 | ||
|
|
0fa5f5de4c |
@@ -7,9 +7,6 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
|
||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||
|
||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
||||
VITE_APP_PORTAL_URL=
|
||||
|
||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||
|
||||
|
||||
@@ -4,16 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
|
||||
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
|
||||
|
||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||
|
||||
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
||||
|
||||
# Fill to set socket server URL used for collaboration.
|
||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
||||
VITE_APP_WS_SERVER_URL=
|
||||
# socket server URL used for collaboration
|
||||
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
||||
|
||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||
|
||||
|
||||
2
.github/workflows/autorelease-excalidraw.yml
vendored
@@ -23,5 +23,5 @@ jobs:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Auto release
|
||||
run: |
|
||||
yarn add @actions/core
|
||||
yarn add @actions/core -W
|
||||
yarn autorelease
|
||||
|
||||
1
.gitignore
vendored
@@ -25,3 +25,4 @@ packages/excalidraw/types
|
||||
coverage
|
||||
dev-dist
|
||||
html
|
||||
examples/**/bundle.*
|
||||
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
|
||||
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
|
||||
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
|
||||
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
|
||||
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
|
||||
| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
|
||||
| [onChange](#onChange) | `function` | Subscribes to change events |
|
||||
| [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
|
||||
| [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
|
||||
|
||||
@@ -23,7 +23,7 @@ All `props` are _optional_.
|
||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
||||
| [`name`](#name) | `string` | | Name of the drawing |
|
||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
|
||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
|
||||
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
|
||||
|
||||
@@ -32,15 +32,9 @@ function App() {
|
||||
|
||||
### Next.js
|
||||
|
||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||
Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`.
|
||||
|
||||
Here are two ways on how you can render **Excalidraw** on **Next.js**.
|
||||
|
||||
|
||||
|
||||
1. Using **Next.js Dynamic** import [Recommended].
|
||||
|
||||
Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`.
|
||||
If you want to only import `Excalidraw` component you can do :point_down:
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -55,25 +49,88 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
|
||||
However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically.
|
||||
|
||||
If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down:
|
||||
|
||||
2. Importing Excalidraw once **client** is rendered.
|
||||
<Tabs>
|
||||
<TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
|
||||
|
||||
```jsx showLineNumbers
|
||||
import { useState, useEffect } from "react";
|
||||
export default function App() {
|
||||
const [Excalidraw, setExcalidraw] = useState(null);
|
||||
useEffect(() => {
|
||||
import("@excalidraw/excalidraw").then((comp) =>
|
||||
setExcalidraw(comp.Excalidraw),
|
||||
```jsx showLineNumbers
|
||||
"use client";
|
||||
import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
console.info(convertToExcalidrawElements([{
|
||||
type: "rectangle",
|
||||
id: "rect-1",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
},]));
|
||||
return (
|
||||
<div style={{height:"500px", width:"500px"}}
|
||||
<Excalidraw />
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
return <>{Excalidraw && <Excalidraw />}</>;
|
||||
}
|
||||
```
|
||||
};
|
||||
export default ExcalidrawWrapper;
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pages" label="Pages router">
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ExcalidrawWrapper />
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="app" label="App router">
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ExcalidrawWrapper />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/).
|
||||
|
||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
|
||||
|
||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||
|
||||
|
||||
@@ -15,14 +15,23 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.app-title {
|
||||
margin-block-start: 0.83em;
|
||||
margin-block-end: 0.83em;
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrapper button {
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
max-width: 200px;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
.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 {
|
||||
@@ -1,15 +1,30 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
Children,
|
||||
cloneElement,
|
||||
} from "react";
|
||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import type * as TExcalidraw from "../index";
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
import "./App.scss";
|
||||
import initialData from "./initialData";
|
||||
import { nanoid } from "nanoid";
|
||||
import { resolvablePromise, ResolvablePromise } from "../utils";
|
||||
import { EVENT, ROUNDNESS } from "../constants";
|
||||
import { distance2d } from "../math";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
||||
|
||||
import {
|
||||
resolvablePromise,
|
||||
ResolvablePromise,
|
||||
distance2d,
|
||||
fileOpen,
|
||||
withBatchedUpdates,
|
||||
withBatchedUpdatesThrottled,
|
||||
} from "../utils";
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import initialData from "../initialData";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
@@ -18,19 +33,14 @@ import type {
|
||||
Gesture,
|
||||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} from "../types";
|
||||
import type { NonDeletedExcalidrawElement, Theme } from "../element/types";
|
||||
import { ImportedLibraryData } from "../data/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import { KEYS } from "../keys";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
} from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
}
|
||||
import "./App.scss";
|
||||
|
||||
type Comment = {
|
||||
x: number;
|
||||
@@ -51,31 +61,6 @@ type PointerDownState = {
|
||||
};
|
||||
};
|
||||
|
||||
const { useEffect, useState, useRef, useCallback } = window.React;
|
||||
|
||||
// This is so that we use the bundled excalidraw.development.js file instead
|
||||
// of the actual source code
|
||||
const {
|
||||
exportToCanvas,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
exportToClipboard,
|
||||
Excalidraw,
|
||||
useHandleLibrary,
|
||||
MIME_TYPES,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
restoreElements,
|
||||
Sidebar,
|
||||
Footer,
|
||||
WelcomeScreen,
|
||||
MainMenu,
|
||||
LiveCollaborationTrigger,
|
||||
convertToExcalidrawElements,
|
||||
TTDDialog,
|
||||
TTDDialogTrigger,
|
||||
} = window.ExcalidrawLib;
|
||||
|
||||
const COMMENT_ICON_DIMENSION = 32;
|
||||
const COMMENT_INPUT_HEIGHT = 50;
|
||||
const COMMENT_INPUT_WIDTH = 150;
|
||||
@@ -84,8 +69,38 @@ export interface AppProps {
|
||||
appTitle: string;
|
||||
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
||||
customArgs?: any[];
|
||||
children: React.ReactNode;
|
||||
excalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
|
||||
export default function App({
|
||||
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);
|
||||
@@ -147,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
};
|
||||
};
|
||||
fetchData();
|
||||
}, [excalidrawAPI]);
|
||||
}, [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 (
|
||||
<>
|
||||
@@ -332,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
return withBatchedUpdates((event) => {
|
||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
||||
window.removeEventListener("pointermove", pointerDownState.onMove);
|
||||
window.removeEventListener("pointerup", pointerDownState.onUp);
|
||||
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||
const distance = distance2d(
|
||||
pointerDownState.x,
|
||||
@@ -397,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||
const onPointerUp =
|
||||
onPointerUpFromPointerDownHandler(pointerDownState);
|
||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
|
||||
pointerDownState.onMove = onPointerMove;
|
||||
pointerDownState.onUp = onPointerUp;
|
||||
@@ -490,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
}}
|
||||
onBlur={saveComment}
|
||||
onKeyDown={(event) => {
|
||||
if (!event.shiftKey && event.key === KEYS.ENTER) {
|
||||
if (!event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveComment();
|
||||
}
|
||||
@@ -523,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
</MainMenu.ItemCustom>
|
||||
<MainMenu.DefaultItems.Help />
|
||||
|
||||
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
||||
{excalidrawAPI && (
|
||||
<MobileFooter
|
||||
excalidrawLib={excalidrawLib}
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
/>
|
||||
)}
|
||||
</MainMenu>
|
||||
);
|
||||
};
|
||||
@@ -672,83 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
|
||||
setExcalidrawAPI(api)
|
||||
}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onChange={(elements, state) => {
|
||||
// console.info("Elements :", elements, "State : ", state);
|
||||
}}
|
||||
onPointerUpdate={(payload: {
|
||||
pointer: { x: number; y: number };
|
||||
button: "down" | "up";
|
||||
pointersMap: Gesture["pointers"];
|
||||
}) => setPointerData(payload)}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
theme={theme}
|
||||
name="Custom name of drawing"
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={rerenderCommentIcons}
|
||||
// allow all urls
|
||||
validateEmbeddable={true}
|
||||
>
|
||||
{excalidrawAPI && (
|
||||
<Footer>
|
||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
||||
</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";
|
||||
}}
|
||||
/>
|
||||
</Excalidraw>
|
||||
{renderExcalidraw(children)}
|
||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||
{comment && renderComment()}
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExcalidrawImperativeAPI } from "../types";
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
|
||||
const { Button, MIME_TYPES } = window.ExcalidrawLib;
|
||||
const COMMENT_SVG = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -17,24 +17,28 @@ const COMMENT_SVG = (
|
||||
<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!")}
|
||||
className="you are a bold one"
|
||||
style={{ marginLeft: "1rem" }}
|
||||
style={{ marginLeft: "1rem", width: "auto" }}
|
||||
title="Hello there!"
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
Hit me
|
||||
</Button>
|
||||
<button
|
||||
<Button
|
||||
className="custom-element"
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
@@ -57,15 +61,10 @@ const CustomFooter = ({
|
||||
)}`;
|
||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||
}}
|
||||
title="Comments!"
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
</button>
|
||||
<button
|
||||
className="custom-footer"
|
||||
onClick={() => alert("This is dummy footer")}
|
||||
>
|
||||
custom footer
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
27
examples/excalidraw/components/MobileFooter.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/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;
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import "./ExampleSidebar.scss";
|
||||
|
||||
const React = window.React;
|
||||
|
||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
||||
import type { FileId } from "../element/types";
|
||||
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
|
||||
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
||||
|
||||
const elements: ExcalidrawElementSkeleton[] = [
|
||||
{
|
||||
13
examples/excalidraw/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@excalidraw/excalidraw": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
3
examples/excalidraw/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig"
|
||||
}
|
||||
146
examples/excalidraw/utils.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||
import type { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||
import { AbortError } from "../../packages/excalidraw/errors";
|
||||
|
||||
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 AbortError());
|
||||
}
|
||||
};
|
||||
},
|
||||
}) 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);
|
||||
};
|
||||
36
examples/excalidraw/with-nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
examples/excalidraw/with-nextjs/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3005) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
12
examples/excalidraw/with-nextjs/next.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
distDir: "build",
|
||||
typescript: {
|
||||
// The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
// This is needed as in pages router the code for importing types throws error as its outside next js app
|
||||
transpilePackages: ["../"],
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
25
examples/excalidraw/with-nextjs/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "with-nextjs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
|
||||
"dev": "yarn build:workspace && next dev -p 3005",
|
||||
"build": "yarn build:workspace && next build",
|
||||
"start": "next start -p 3006",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "*",
|
||||
"next": "14.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"path2d-polyfill": "2.0.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
BIN
examples/excalidraw/with-nextjs/src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
11
examples/excalidraw/with-nextjs/src/app/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
23
examples/excalidraw/with-nextjs/src/app/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
// with ssr false
|
||||
const ExcalidrawWithClientOnly = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<a href="/excalidraw-in-pages">Switch to Pages router</a>
|
||||
<h1 className="page-title">App Router</h1>
|
||||
|
||||
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||
<ExcalidrawWithClientOnly />
|
||||
</>
|
||||
);
|
||||
}
|
||||
15
examples/excalidraw/with-nextjs/src/common.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1c7ed6;
|
||||
font-size: 20px;
|
||||
text-decoration: none;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
}
|
||||
22
examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import App from "../../components/App";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<App
|
||||
appTitle={"Excalidraw with Nextjs Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
excalidrawLib={excalidrawLib}
|
||||
>
|
||||
<Excalidraw />
|
||||
</App>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcalidrawWrapper;
|
||||
@@ -0,0 +1,22 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
// with ssr false
|
||||
const Excalidraw = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<a href="/">Switch to App router</a>
|
||||
<h1 className="page-title">Pages Router</h1>
|
||||
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||
<Excalidraw />
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
examples/excalidraw/with-nextjs/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
3
examples/excalidraw/with-nextjs/vercel.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"outputDirectory": "build"
|
||||
}
|
||||
252
examples/excalidraw/with-nextjs/yarn.lock
Normal file
@@ -0,0 +1,252 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@excalidraw/excalidraw@workspace:^":
|
||||
version "0.17.2"
|
||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96"
|
||||
integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ==
|
||||
|
||||
"@next/env@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
|
||||
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
|
||||
|
||||
"@next/swc-darwin-arm64@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
|
||||
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
|
||||
|
||||
"@next/swc-darwin-x64@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
|
||||
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
|
||||
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
|
||||
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
|
||||
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
|
||||
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
|
||||
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
|
||||
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
|
||||
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
|
||||
|
||||
"@swc/helpers@0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/node@^20":
|
||||
version "20.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
|
||||
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
|
||||
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||
|
||||
"@types/react-dom@^18":
|
||||
version "18.2.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
|
||||
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18":
|
||||
version "18.2.47"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40"
|
||||
integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
|
||||
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
|
||||
|
||||
busboy@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||
dependencies:
|
||||
streamsearch "^1.1.0"
|
||||
|
||||
caniuse-lite@^1.0.30001406:
|
||||
version "1.0.30001576"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4"
|
||||
integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==
|
||||
|
||||
client-only@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
|
||||
glob-to-regexp@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.11:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
loose-envify@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
nanoid@^3.3.6:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
next@14.0.4:
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
|
||||
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
|
||||
dependencies:
|
||||
"@next/env" "14.0.4"
|
||||
"@swc/helpers" "0.5.2"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001406"
|
||||
graceful-fs "^4.2.11"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.1"
|
||||
watchpack "2.4.0"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "14.0.4"
|
||||
"@next/swc-darwin-x64" "14.0.4"
|
||||
"@next/swc-linux-arm64-gnu" "14.0.4"
|
||||
"@next/swc-linux-arm64-musl" "14.0.4"
|
||||
"@next/swc-linux-x64-gnu" "14.0.4"
|
||||
"@next/swc-linux-x64-musl" "14.0.4"
|
||||
"@next/swc-win32-arm64-msvc" "14.0.4"
|
||||
"@next/swc-win32-ia32-msvc" "14.0.4"
|
||||
"@next/swc-win32-x64-msvc" "14.0.4"
|
||||
|
||||
path2d-polyfill@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391"
|
||||
integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
postcss@8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
react-dom@^18:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react@^18:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
streamsearch@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
styled-jsx@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
|
||||
integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
|
||||
dependencies:
|
||||
client-only "0.0.1"
|
||||
|
||||
tslib@^2.4.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
typescript@^5:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
watchpack@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||
dependencies:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
||||
@@ -13,20 +13,20 @@
|
||||
window.name = "codesandbox";
|
||||
</script>
|
||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||
<link rel="stylesheet" href="bundle.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
<div id="root"></div>
|
||||
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
|
||||
|
||||
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
||||
of the actual source code -->
|
||||
<script type="module">
|
||||
import * as ExcalidrawLib from "/dist/browser/dev/index.js";
|
||||
import * as ExcalidrawLib from "@excalidraw/excalidraw";
|
||||
|
||||
console.log(ExcalidrawLib);
|
||||
window.ExcalidrawLib = ExcalidrawLib;
|
||||
</script>
|
||||
<script type="module" src="bundle.js"></script>
|
||||
<script type="module" src="index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
28
examples/excalidraw/with-script-in-browser/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import App from "../components/App";
|
||||
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>,
|
||||
);
|
||||
19
examples/excalidraw/with-script-in-browser/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "with-script-in-browser",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@excalidraw/excalidraw": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "5.0.12",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
|
||||
"build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
|
||||
"build:preview": "yarn build && vite preview --port 5002"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"outputDirectory": "example/public",
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "yarn install"
|
||||
}
|
||||
11
examples/excalidraw/with-script-in-browser/vite.config.mts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3001,
|
||||
// open the browser
|
||||
open: true,
|
||||
},
|
||||
publicDir: "public",
|
||||
});
|
||||
313
examples/excalidraw/yarn.lock
Normal file
@@ -0,0 +1,313 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@esbuild/aix-ppc64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3"
|
||||
integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==
|
||||
|
||||
"@esbuild/android-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220"
|
||||
integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==
|
||||
|
||||
"@esbuild/android-arm@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c"
|
||||
integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==
|
||||
|
||||
"@esbuild/android-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2"
|
||||
integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==
|
||||
|
||||
"@esbuild/darwin-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf"
|
||||
integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==
|
||||
|
||||
"@esbuild/darwin-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e"
|
||||
integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a"
|
||||
integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==
|
||||
|
||||
"@esbuild/freebsd-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2"
|
||||
integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==
|
||||
|
||||
"@esbuild/linux-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545"
|
||||
integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==
|
||||
|
||||
"@esbuild/linux-arm@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3"
|
||||
integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==
|
||||
|
||||
"@esbuild/linux-ia32@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4"
|
||||
integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==
|
||||
|
||||
"@esbuild/linux-loong64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121"
|
||||
integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==
|
||||
|
||||
"@esbuild/linux-mips64el@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9"
|
||||
integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912"
|
||||
integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==
|
||||
|
||||
"@esbuild/linux-riscv64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916"
|
||||
integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==
|
||||
|
||||
"@esbuild/linux-s390x@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8"
|
||||
integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==
|
||||
|
||||
"@esbuild/linux-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766"
|
||||
integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==
|
||||
|
||||
"@esbuild/netbsd-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d"
|
||||
integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2"
|
||||
integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==
|
||||
|
||||
"@esbuild/sunos-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767"
|
||||
integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==
|
||||
|
||||
"@esbuild/win32-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee"
|
||||
integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c"
|
||||
integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==
|
||||
|
||||
"@esbuild/win32-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04"
|
||||
integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
|
||||
integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a"
|
||||
integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb"
|
||||
integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1"
|
||||
integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560"
|
||||
integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114"
|
||||
integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632"
|
||||
integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad"
|
||||
integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
|
||||
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4"
|
||||
integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a"
|
||||
integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85"
|
||||
integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
|
||||
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
|
||||
|
||||
"@types/estree@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
esbuild@^0.19.3:
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96"
|
||||
integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.19.11"
|
||||
"@esbuild/android-arm" "0.19.11"
|
||||
"@esbuild/android-arm64" "0.19.11"
|
||||
"@esbuild/android-x64" "0.19.11"
|
||||
"@esbuild/darwin-arm64" "0.19.11"
|
||||
"@esbuild/darwin-x64" "0.19.11"
|
||||
"@esbuild/freebsd-arm64" "0.19.11"
|
||||
"@esbuild/freebsd-x64" "0.19.11"
|
||||
"@esbuild/linux-arm" "0.19.11"
|
||||
"@esbuild/linux-arm64" "0.19.11"
|
||||
"@esbuild/linux-ia32" "0.19.11"
|
||||
"@esbuild/linux-loong64" "0.19.11"
|
||||
"@esbuild/linux-mips64el" "0.19.11"
|
||||
"@esbuild/linux-ppc64" "0.19.11"
|
||||
"@esbuild/linux-riscv64" "0.19.11"
|
||||
"@esbuild/linux-s390x" "0.19.11"
|
||||
"@esbuild/linux-x64" "0.19.11"
|
||||
"@esbuild/netbsd-x64" "0.19.11"
|
||||
"@esbuild/openbsd-x64" "0.19.11"
|
||||
"@esbuild/sunos-x64" "0.19.11"
|
||||
"@esbuild/win32-arm64" "0.19.11"
|
||||
"@esbuild/win32-ia32" "0.19.11"
|
||||
"@esbuild/win32-x64" "0.19.11"
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
loose-envify@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
postcss@^8.4.32:
|
||||
version "8.4.33"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
|
||||
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
rollup@^4.2.0:
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05"
|
||||
integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.5"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.9.5"
|
||||
"@rollup/rollup-android-arm64" "4.9.5"
|
||||
"@rollup/rollup-darwin-arm64" "4.9.5"
|
||||
"@rollup/rollup-darwin-x64" "4.9.5"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.9.5"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.9.5"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.9.5"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.9.5"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.9.5"
|
||||
"@rollup/rollup-linux-x64-musl" "4.9.5"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.9.5"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.9.5"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.9.5"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
vite@5.0.6:
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c"
|
||||
integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==
|
||||
dependencies:
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.32"
|
||||
rollup "^4.2.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
import {
|
||||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
getCollabServer,
|
||||
getSyncableElements,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
@@ -452,13 +451,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||
|
||||
try {
|
||||
const socketServerData = await getCollabServer();
|
||||
|
||||
this.portal.socket = this.portal.open(
|
||||
socketIOClient(socketServerData.url, {
|
||||
transports: socketServerData.polling
|
||||
? ["websocket", "polling"]
|
||||
: ["websocket"],
|
||||
socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
||||
transports: ["websocket", "polling"],
|
||||
}),
|
||||
roomId,
|
||||
roomKey,
|
||||
|
||||
@@ -65,35 +65,6 @@ const generateRoomId = async () => {
|
||||
return bytesToHexString(buffer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Right now the reason why we resolve connection params (url, polling...)
|
||||
* from upstream is to allow changing the params immediately when needed without
|
||||
* having to wait for clients to update the SW.
|
||||
*
|
||||
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
|
||||
*/
|
||||
export const getCollabServer = async (): Promise<{
|
||||
url: string;
|
||||
polling: boolean;
|
||||
}> => {
|
||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
||||
return {
|
||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
||||
polling: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
||||
);
|
||||
return await resp.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(t("errors.cannotResolveCollabServer"));
|
||||
}
|
||||
};
|
||||
|
||||
export type EncryptedData = {
|
||||
data: ArrayBuffer;
|
||||
iv: Uint8Array;
|
||||
|
||||
@@ -20,17 +20,6 @@ Object.defineProperty(window, "crypto", {
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => {
|
||||
const module = (await importActual()) as any;
|
||||
return {
|
||||
__esmodule: true,
|
||||
...module,
|
||||
getCollabServer: vi.fn(() => ({
|
||||
url: /* doesn't really matter */ "http://localhost:3002",
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
||||
const loadFromFirebase = async () => null;
|
||||
const saveToFirebase = () => {};
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"workspaces": [
|
||||
"excalidraw-app",
|
||||
"packages/excalidraw",
|
||||
"packages/utils"
|
||||
"packages/utils",
|
||||
"examples/excalidraw",
|
||||
"examples/excalidraw/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
@@ -43,7 +45,7 @@
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "6.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "5.0.6",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-checker": "0.6.1",
|
||||
"vite-plugin-ejs": "1.7.0",
|
||||
"vite-plugin-pwa": "0.17.4",
|
||||
|
||||
2
packages/excalidraw/.gitignore
vendored
@@ -1,4 +1,2 @@
|
||||
node_modules
|
||||
types
|
||||
bundle.js
|
||||
bundle.css
|
||||
|
||||
@@ -13,8 +13,11 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Features
|
||||
|
||||
- Add `onPointerUp` prop [#7638](https://github.com/excalidraw/excalidraw/pull/7638).
|
||||
|
||||
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
|
||||
- Remove `ExcalidrawEmbeddableElement.validated` attribute. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
||||
@@ -40,8 +40,13 @@ const alignSelectedElements = (
|
||||
alignment: Alignment,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const elementsMap = arrayToMap(elements);
|
||||
|
||||
const updatedElements = alignElements(selectedElements, alignment);
|
||||
const updatedElements = alignElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
alignment,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
getOriginalContainerHeightFromCache,
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "../element/textWysiwyg";
|
||||
} from "../element/containerCache";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isTextBindableContainer,
|
||||
@@ -45,8 +45,9 @@ export const actionUnbindText = register({
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
const { width, height, baseline } = measureText(
|
||||
boundTextElement.originalText,
|
||||
@@ -106,7 +107,10 @@ export const actionBindText = register({
|
||||
if (
|
||||
textElement &&
|
||||
bindingContainer &&
|
||||
getBoundTextElement(bindingContainer) === null
|
||||
getBoundTextElement(
|
||||
bindingContainer,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) === null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,11 @@ const distributeSelectedElements = (
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
|
||||
const updatedElements = distributeElements(selectedElements, distribution);
|
||||
const updatedElements = distributeElements(
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
distribution,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ const duplicateElements = (
|
||||
continue;
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
|
||||
const isElementAFrameLike = isFrameLikeElement(element);
|
||||
|
||||
if (idsOfElementsToDuplicate.get(element.id)) {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { register } from "./register";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { resizeMultipleElements } from "../element/resizeElements";
|
||||
import { AppState, PointerDownState } from "../types";
|
||||
import { AppState } from "../types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getCommonBoundingBox } from "../element/bounds";
|
||||
@@ -20,7 +25,12 @@ export const actionFlipHorizontal = register({
|
||||
perform: (elements, appState, _, app) => {
|
||||
return {
|
||||
elements: updateFrameMembershipOfSelectedElements(
|
||||
flipSelectedElements(elements, appState, "horizontal"),
|
||||
flipSelectedElements(
|
||||
elements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
"horizontal",
|
||||
),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
@@ -38,7 +48,12 @@ export const actionFlipVertical = register({
|
||||
perform: (elements, appState, _, app) => {
|
||||
return {
|
||||
elements: updateFrameMembershipOfSelectedElements(
|
||||
flipSelectedElements(elements, appState, "vertical"),
|
||||
flipSelectedElements(
|
||||
elements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
"vertical",
|
||||
),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
@@ -53,6 +68,7 @@ export const actionFlipVertical = register({
|
||||
|
||||
const flipSelectedElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||
appState: Readonly<AppState>,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
) => {
|
||||
@@ -67,6 +83,7 @@ const flipSelectedElements = (
|
||||
|
||||
const updatedElements = flipElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
appState,
|
||||
flipDirection,
|
||||
);
|
||||
@@ -79,15 +96,17 @@ const flipSelectedElements = (
|
||||
};
|
||||
|
||||
const flipElements = (
|
||||
elements: NonDeleted<ExcalidrawElement>[],
|
||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||
appState: AppState,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
): ExcalidrawElement[] => {
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
||||
|
||||
resizeMultipleElements(
|
||||
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
||||
elements,
|
||||
elementsMap,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
"nw",
|
||||
true,
|
||||
flipDirection === "horizontal" ? maxX : minX,
|
||||
@@ -96,7 +115,7 @@ const flipElements = (
|
||||
|
||||
(isBindingEnabled(appState)
|
||||
? bindOrUnbindSelectedElements
|
||||
: unbindLinearElements)(elements);
|
||||
: unbindLinearElements)(selectedElements);
|
||||
|
||||
return elements;
|
||||
return selectedElements;
|
||||
};
|
||||
|
||||
@@ -63,11 +63,7 @@ export const actionRemoveAllElementsFromFrame = register({
|
||||
|
||||
if (isFrameLikeElement(selectedElement)) {
|
||||
return {
|
||||
elements: removeAllElementsFromFrame(
|
||||
elements,
|
||||
selectedElement,
|
||||
appState,
|
||||
),
|
||||
elements: removeAllElementsFromFrame(elements, selectedElement),
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {
|
||||
|
||||
@@ -105,10 +105,9 @@ export const actionGroup = register({
|
||||
const frameElementsMap = groupByFrameLikes(selectedElements);
|
||||
|
||||
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
||||
nextElements = removeElementsFromFrame(
|
||||
nextElements,
|
||||
removeElementsFromFrame(
|
||||
elementsInFrame,
|
||||
appState,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -229,7 +228,7 @@ export const actionUngroup = register({
|
||||
nextElements,
|
||||
getElementsInResizingFrame(nextElements, frame, appState),
|
||||
frame,
|
||||
appState,
|
||||
app,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,7 +57,9 @@ export const actionGoToCollaborator = register({
|
||||
isBeingFollowed={isBeingFollowed}
|
||||
isCurrentUser={collaborator.isCurrentUser === true}
|
||||
/>
|
||||
{collaborator.username}
|
||||
<div className="UserList__collaborator-name">
|
||||
{collaborator.username}
|
||||
</div>
|
||||
<div
|
||||
className="UserList__collaborator-follow-status-icon"
|
||||
style={{ visibility: isBeingFollowed ? "visible" : "hidden" }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppState, Primitive } from "../types";
|
||||
import { AppClassProperties, AppState, Primitive } from "../types";
|
||||
import {
|
||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||
@@ -66,7 +66,6 @@ import {
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
getDefaultLineHeight,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
@@ -189,6 +188,7 @@ const offsetElementAfterFontResize = (
|
||||
const changeFontSize = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
||||
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
||||
) => {
|
||||
@@ -206,7 +206,10 @@ const changeFontSize = (
|
||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
|
||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||
|
||||
@@ -600,10 +603,10 @@ export const actionChangeOpacity = register({
|
||||
export const actionChangeFontSize = register({
|
||||
name: "changeFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return changeFontSize(elements, appState, () => value, value);
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, () => value, value);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<ButtonIconSelect
|
||||
@@ -641,14 +644,21 @@ export const actionChangeFontSize = register({
|
||||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
@@ -663,8 +673,8 @@ export const actionChangeFontSize = register({
|
||||
export const actionDecreaseFontSize = register({
|
||||
name: "decreaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return changeFontSize(elements, appState, (element) =>
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(
|
||||
// get previous value before relative increase (doesn't work fully
|
||||
// due to rounding and float precision issues)
|
||||
@@ -685,8 +695,8 @@ export const actionDecreaseFontSize = register({
|
||||
export const actionIncreaseFontSize = register({
|
||||
name: "increaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return changeFontSize(elements, appState, (element) =>
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
||||
);
|
||||
},
|
||||
@@ -703,7 +713,7 @@ export const actionIncreaseFontSize = register({
|
||||
export const actionChangeFontFamily = register({
|
||||
name: "changeFontFamily",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
@@ -717,7 +727,10 @@ export const actionChangeFontFamily = register({
|
||||
lineHeight: getDefaultLineHeight(value),
|
||||
},
|
||||
);
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
@@ -732,7 +745,7 @@ export const actionChangeFontFamily = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||
const options: {
|
||||
value: FontFamilyValues;
|
||||
text: string;
|
||||
@@ -772,14 +785,21 @@ export const actionChangeFontFamily = register({
|
||||
if (isTextElement(element)) {
|
||||
return element.fontFamily;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontFamily;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
@@ -795,7 +815,7 @@ export const actionChangeFontFamily = register({
|
||||
export const actionChangeTextAlign = register({
|
||||
name: "changeTextAlign",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
@@ -806,7 +826,10 @@ export const actionChangeTextAlign = register({
|
||||
oldElement,
|
||||
{ textAlign: value },
|
||||
);
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
@@ -821,7 +844,8 @@ export const actionChangeTextAlign = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.textAlign")}</legend>
|
||||
@@ -854,14 +878,18 @@ export const actionChangeTextAlign = register({
|
||||
if (isTextElement(element)) {
|
||||
return element.textAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.textAlign;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(element, elementsMap) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemTextAlign,
|
||||
)}
|
||||
@@ -875,7 +903,7 @@ export const actionChangeTextAlign = register({
|
||||
export const actionChangeVerticalAlign = register({
|
||||
name: "changeVerticalAlign",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
@@ -887,7 +915,10 @@ export const actionChangeVerticalAlign = register({
|
||||
{ verticalAlign: value },
|
||||
);
|
||||
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
@@ -901,7 +932,7 @@ export const actionChangeVerticalAlign = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||
return (
|
||||
<fieldset>
|
||||
<ButtonIconSelect<VerticalAlign | false>
|
||||
@@ -933,14 +964,21 @@ export const actionChangeVerticalAlign = register({
|
||||
if (isTextElement(element) && element.containerId) {
|
||||
return element.verticalAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.verticalAlign;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
|
||||
@@ -32,12 +32,15 @@ export let copiedStyles: string = "{}";
|
||||
export const actionCopyStyles = register({
|
||||
name: "copyStyles",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState) => {
|
||||
perform: (elements, appState, formData, app) => {
|
||||
const elementsCopied = [];
|
||||
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
||||
elementsCopied.push(element);
|
||||
if (element && hasBoundTextElement(element)) {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
elementsCopied.push(boundTextElement);
|
||||
}
|
||||
if (element) {
|
||||
@@ -59,7 +62,7 @@ export const actionCopyStyles = register({
|
||||
export const actionPasteStyles = register({
|
||||
name: "pasteStyles",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState) => {
|
||||
perform: (elements, appState, formData, app) => {
|
||||
const elementsCopied = JSON.parse(copiedStyles);
|
||||
const pastedElement = elementsCopied[0];
|
||||
const boundTextElement = elementsCopied[1];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||
import { newElementWith } from "./element/mutateElement";
|
||||
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
@@ -10,10 +10,13 @@ export interface Alignment {
|
||||
|
||||
export const alignElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
alignment: Alignment,
|
||||
): ExcalidrawElement[] => {
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
|
||||
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
);
|
||||
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
|
||||
|
||||
return groups.flatMap((group) => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElementType,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useDevice } from "./App";
|
||||
import {
|
||||
@@ -44,17 +47,14 @@ import { useTunnels } from "../context/tunnels";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
appState,
|
||||
elements,
|
||||
elementsMap,
|
||||
renderAction,
|
||||
}: {
|
||||
appState: UIAppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||
renderAction: ActionManager["renderAction"];
|
||||
}) => {
|
||||
const targetElements = getTargetElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
const targetElements = getTargetElements(elementsMap, appState);
|
||||
|
||||
let isSingleElementBoundContainer = false;
|
||||
if (
|
||||
@@ -137,12 +137,12 @@ export const SelectedShapeActions = ({
|
||||
{renderAction("changeFontFamily")}
|
||||
|
||||
{(appState.activeTool.type === "text" ||
|
||||
suppportsHorizontalAlign(targetElements)) &&
|
||||
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
||||
renderAction("changeTextAlign")}
|
||||
</>
|
||||
)}
|
||||
|
||||
{shouldAllowVerticalAlign(targetElements) &&
|
||||
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
||||
renderAction("changeVerticalAlign")}
|
||||
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||
|
||||
@@ -115,7 +115,6 @@ import {
|
||||
newLinearElement,
|
||||
newTextElement,
|
||||
newImageElement,
|
||||
textWysiwyg,
|
||||
transformElements,
|
||||
updateTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@@ -217,7 +216,6 @@ import {
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
hasBackground,
|
||||
isOverScrollBars,
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
@@ -348,6 +346,8 @@ import {
|
||||
updateFrameMembershipOfSelectedElements,
|
||||
isElementInFrame,
|
||||
getFrameLikeTitle,
|
||||
getElementsOverlappingFrame,
|
||||
filterElementsEligibleAsFrameChildren,
|
||||
} from "../frame";
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
@@ -395,7 +395,7 @@ import {
|
||||
import { Emitter } from "../emitter";
|
||||
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
|
||||
import { MagicCacheData, diagramToHTML } from "../data/magic";
|
||||
import { elementsOverlappingBBox, exportToBlob } from "../../utils/export";
|
||||
import { exportToBlob } from "../../utils/export";
|
||||
import { COLOR_PALETTE } from "../colors";
|
||||
import { ElementCanvasButton } from "./MagicButton";
|
||||
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
||||
@@ -407,6 +407,8 @@ import { AnimatedTrail } from "../animated-trail";
|
||||
import { LaserTrails } from "../laser-trails";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
import { getRenderOpacity } from "../renderer/renderElement";
|
||||
import { textWysiwyg } from "../element/textWysiwyg";
|
||||
import { isOverScrollBars } from "../scene/scrollbars";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@@ -1297,10 +1299,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const FRAME_NAME_EDIT_PADDING = 6;
|
||||
|
||||
const reset = () => {
|
||||
if (f.name?.trim() === "") {
|
||||
mutateElement(f, { name: null });
|
||||
}
|
||||
|
||||
mutateElement(f, { name: f.name?.trim() || null });
|
||||
this.setState({ editingFrame: null });
|
||||
};
|
||||
|
||||
@@ -1323,6 +1322,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onBlur={() => reset()}
|
||||
onKeyDown={(event) => {
|
||||
// for some inexplicable reason, `onBlur` triggered on ESC
|
||||
@@ -1415,7 +1415,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||
|
||||
const versionNonce = this.scene.getVersionNonce();
|
||||
const { canvasElements, visibleElements } =
|
||||
const { elementsMap, visibleElements } =
|
||||
this.renderer.getRenderableElements({
|
||||
versionNonce,
|
||||
zoom: this.state.zoom,
|
||||
@@ -1429,6 +1429,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pendingImageElementId: this.state.pendingImageElementId,
|
||||
});
|
||||
|
||||
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
const shouldBlockPointerEvents =
|
||||
!(
|
||||
this.state.editingElement && isLinearElement(this.state.editingElement)
|
||||
@@ -1625,7 +1627,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<StaticCanvas
|
||||
canvas={this.canvas}
|
||||
rc={this.rc}
|
||||
elements={canvasElements}
|
||||
elementsMap={elementsMap}
|
||||
allElementsMap={allElementsMap}
|
||||
visibleElements={visibleElements}
|
||||
versionNonce={versionNonce}
|
||||
selectionNonce={
|
||||
@@ -1646,7 +1649,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<InteractiveCanvas
|
||||
containerRef={this.excalidrawContainerRef}
|
||||
canvas={this.interactiveCanvas}
|
||||
elements={canvasElements}
|
||||
elementsMap={elementsMap}
|
||||
visibleElements={visibleElements}
|
||||
selectedElements={selectedElements}
|
||||
versionNonce={versionNonce}
|
||||
@@ -1803,11 +1806,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const magicFrameChildren = elementsOverlappingBBox({
|
||||
elements: this.scene.getNonDeletedElements(),
|
||||
bounds: magicFrame,
|
||||
type: "overlap",
|
||||
}).filter((el) => !isMagicFrameElement(el));
|
||||
const magicFrameChildren = getElementsOverlappingFrame(
|
||||
this.scene.getNonDeletedElements(),
|
||||
magicFrame,
|
||||
).filter((el) => !isMagicFrameElement(el));
|
||||
|
||||
if (!magicFrameChildren.length) {
|
||||
if (source === "button") {
|
||||
@@ -2779,7 +2781,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private renderInteractiveSceneCallback = ({
|
||||
atLeastOneVisibleElement,
|
||||
scrollBars,
|
||||
elements,
|
||||
elementsMap,
|
||||
}: RenderInteractiveSceneCallback) => {
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
@@ -2788,7 +2790,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// hide when editing text
|
||||
isTextElement(this.state.editingElement)
|
||||
? false
|
||||
: !atLeastOneVisibleElement && elements.length > 0;
|
||||
: !atLeastOneVisibleElement && elementsMap.size > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside });
|
||||
}
|
||||
@@ -3099,16 +3101,29 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
);
|
||||
|
||||
const nextElements = [
|
||||
const allElements = [
|
||||
...this.scene.getElementsIncludingDeleted(),
|
||||
...newElements,
|
||||
];
|
||||
|
||||
this.scene.replaceAllElements(nextElements);
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
if (topLayerFrame) {
|
||||
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
||||
newElements,
|
||||
topLayerFrame,
|
||||
);
|
||||
addElementsToFrame(allElements, eligibleElements, topLayerFrame);
|
||||
}
|
||||
|
||||
this.scene.replaceAllElements(allElements);
|
||||
|
||||
newElements.forEach((newElement) => {
|
||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||
const container = getContainerElement(newElement);
|
||||
const container = getContainerElement(
|
||||
newElement,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
redrawTextBoundingBox(newElement, container);
|
||||
}
|
||||
});
|
||||
@@ -3855,7 +3870,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (!isTextElement(selectedElement)) {
|
||||
container = selectedElement as ExcalidrawTextContainer;
|
||||
}
|
||||
const midPoint = getContainerCenter(selectedElement, this.state);
|
||||
const midPoint = getContainerCenter(
|
||||
selectedElement,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const sceneX = midPoint.x;
|
||||
const sceneY = midPoint.y;
|
||||
this.startTextEditing({
|
||||
@@ -4172,11 +4191,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.replaceAllElements([
|
||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||
if (_element.id === element.id && isTextElement(_element)) {
|
||||
return updateTextElement(_element, {
|
||||
text,
|
||||
isDeleted,
|
||||
originalText,
|
||||
});
|
||||
return updateTextElement(
|
||||
_element,
|
||||
getContainerElement(
|
||||
_element,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
{
|
||||
text,
|
||||
isDeleted,
|
||||
originalText,
|
||||
},
|
||||
);
|
||||
}
|
||||
return _element;
|
||||
}),
|
||||
@@ -4312,6 +4338,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
? allHitElements[allHitElements.length - 2]
|
||||
: elementWithHighestZIndex;
|
||||
@@ -4341,7 +4368,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
return getElementsAtPosition(elements, (element) =>
|
||||
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
|
||||
hitTest(
|
||||
element,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
).filter((element) => {
|
||||
// hitting a frame's element from outside the frame is not considered a hit
|
||||
const containingFrame = getContainingFrame(element);
|
||||
@@ -4378,7 +4412,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
container,
|
||||
);
|
||||
if (container && parentCenterPosition) {
|
||||
const boundTextElementToContainer = getBoundTextElement(container);
|
||||
const boundTextElementToContainer = getBoundTextElement(
|
||||
container,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (!boundTextElementToContainer) {
|
||||
shouldBindToContainer = true;
|
||||
}
|
||||
@@ -4391,7 +4428,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isTextElement(selectedElements[0])) {
|
||||
existingTextElement = selectedElements[0];
|
||||
} else if (container) {
|
||||
existingTextElement = getBoundTextElement(selectedElements[0]);
|
||||
existingTextElement = getBoundTextElement(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
} else {
|
||||
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||
}
|
||||
@@ -4600,7 +4640,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
[sceneX, sceneY],
|
||||
)
|
||||
) {
|
||||
const midPoint = getContainerCenter(container, this.state);
|
||||
const midPoint = getContainerCenter(
|
||||
container,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
sceneX = midPoint.x;
|
||||
sceneY = midPoint.y;
|
||||
@@ -5236,8 +5280,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
@@ -5264,6 +5308,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
linearElementEditor,
|
||||
{ x: scenePointerX, y: scenePointerY },
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||
@@ -5279,6 +5324,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.frameNameBoundsCache,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
elementsMap,
|
||||
)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
@@ -5290,6 +5336,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.frameNameBoundsCache,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
@@ -5759,7 +5806,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event.preventDefault();
|
||||
|
||||
let nextPastePrevented = false;
|
||||
const isLinux = /Linux/.test(window.navigator.platform);
|
||||
const isLinux =
|
||||
typeof window === undefined
|
||||
? false
|
||||
: /Linux/.test(window.navigator.platform);
|
||||
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING);
|
||||
let { clientX: lastX, clientY: lastY } = event;
|
||||
@@ -6036,6 +6086,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.history,
|
||||
pointerDownState.origin,
|
||||
linearElementEditor,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (ret.hitElement) {
|
||||
pointerDownState.hit.element = ret.hitElement;
|
||||
@@ -6450,8 +6501,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (embedLink.warning) {
|
||||
this.setToast({ message: embedLink.warning, closable: true });
|
||||
if (embedLink.error instanceof URIError) {
|
||||
this.setToast({
|
||||
message: t("toast.unrecognizedLinkFormat"),
|
||||
closable: true,
|
||||
});
|
||||
}
|
||||
|
||||
const element = newEmbeddableElement({
|
||||
@@ -6971,6 +7025,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
},
|
||||
linearElementEditor,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (didDrag) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
@@ -7512,6 +7567,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ pendingImageElementId: null });
|
||||
}
|
||||
|
||||
this.props?.onPointerUp?.(activeTool, pointerDownState);
|
||||
this.onPointerUpEmitter.trigger(
|
||||
this.state.activeTool,
|
||||
pointerDownState,
|
||||
@@ -7689,13 +7745,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
groupIds: [],
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements(
|
||||
removeElementsFromFrame(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
[linearElement],
|
||||
this.state,
|
||||
),
|
||||
removeElementsFromFrame(
|
||||
[linearElement],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
this.scene.informMutation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7705,7 +7760,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
let nextElements = this.scene.getElementsIncludingDeleted();
|
||||
let nextElements = this.scene.getElementsMapIncludingDeleted();
|
||||
|
||||
const updateGroupIdsAfterEditingGroup = (
|
||||
elements: ExcalidrawElement[],
|
||||
@@ -7798,7 +7853,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
this.scene.replaceAllElements(
|
||||
addElementsToFrame(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
elementsInsideFrame,
|
||||
draggingElement,
|
||||
),
|
||||
@@ -7846,7 +7901,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state,
|
||||
),
|
||||
frame,
|
||||
this.state,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8074,6 +8129,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.frameNameBoundsCache,
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)) ||
|
||||
(!hitElement &&
|
||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
|
||||
@@ -9126,10 +9182,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (
|
||||
transformElements(
|
||||
pointerDownState,
|
||||
pointerDownState.originalElements,
|
||||
transformHandleType,
|
||||
selectedElements,
|
||||
pointerDownState.resize.arrowDirection,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
shouldResizeFromCenter(event),
|
||||
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
||||
@@ -9139,7 +9195,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
resizeY,
|
||||
pointerDownState.resize.center.x,
|
||||
pointerDownState.resize.center.y,
|
||||
this.state,
|
||||
)
|
||||
) {
|
||||
this.maybeSuggestBindingForAll(selectedElements);
|
||||
@@ -9316,7 +9371,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
let elementCenterX = container.x + container.width / 2;
|
||||
let elementCenterY = container.y + container.height / 2;
|
||||
|
||||
const elementCenter = getContainerCenter(container, appState);
|
||||
const elementCenter = getContainerCenter(
|
||||
container,
|
||||
appState,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (elementCenter) {
|
||||
elementCenterX = elementCenter.x;
|
||||
elementCenterY = elementCenter.y;
|
||||
|
||||
@@ -16,25 +16,20 @@ const FollowMode = ({
|
||||
onDisconnect,
|
||||
}: FollowModeProps) => {
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<div className="follow-mode" style={{ width, height }}>
|
||||
<div className="follow-mode__badge">
|
||||
<div className="follow-mode__badge__label">
|
||||
Following{" "}
|
||||
<span
|
||||
className="follow-mode__badge__username"
|
||||
title={userToFollow.username}
|
||||
>
|
||||
{userToFollow.username}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="follow-mode__disconnect-btn"
|
||||
<div className="follow-mode" style={{ width, height }}>
|
||||
<div className="follow-mode__badge">
|
||||
<div className="follow-mode__badge__label">
|
||||
Following{" "}
|
||||
<span
|
||||
className="follow-mode__badge__username"
|
||||
title={userToFollow.username}
|
||||
>
|
||||
{CloseIcon}
|
||||
</button>
|
||||
{userToFollow.username}
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
||||
{CloseIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -226,7 +226,7 @@ const LayerUI = ({
|
||||
>
|
||||
<SelectedShapeActions
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
renderAction={actionManager.renderAction}
|
||||
/>
|
||||
</Island>
|
||||
|
||||
@@ -183,7 +183,7 @@ export const MobileMenu = ({
|
||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
renderAction={actionManager.renderAction}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
.default-sidebar-trigger .sidebar-trigger__label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
.UserList__collaborator-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.UserList__collaborator-follow-status-icon {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { DOMAttributes } from "react";
|
||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "../../scene/types";
|
||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||
@@ -15,7 +16,7 @@ import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||
type InteractiveCanvasProps = {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||
versionNonce: number | undefined;
|
||||
@@ -113,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
renderInteractiveScene(
|
||||
{
|
||||
canvas: props.canvas,
|
||||
elements: props.elements,
|
||||
elementsMap: props.elementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
selectedElements: props.selectedElements,
|
||||
scale: window.devicePixelRatio,
|
||||
@@ -201,10 +202,10 @@ const areEqual = (
|
||||
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||
prevProps.scale !== nextProps.scale ||
|
||||
// we need to memoize on element arrays because they may have renewed
|
||||
// we need to memoize on elementsMap because they may have renewed
|
||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||
// on appState)
|
||||
prevProps.elements !== nextProps.elements ||
|
||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||
prevProps.selectedElements !== nextProps.selectedElements
|
||||
) {
|
||||
|
||||
@@ -3,14 +3,21 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { renderStaticScene } from "../../renderer/renderScene";
|
||||
import { isShallowEqual } from "../../utils";
|
||||
import type { AppState, StaticCanvasAppState } from "../../types";
|
||||
import type { StaticCanvasRenderConfig } from "../../scene/types";
|
||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||
import type {
|
||||
RenderableElementsMap,
|
||||
StaticCanvasRenderConfig,
|
||||
} from "../../scene/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../../element/types";
|
||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||
|
||||
type StaticCanvasProps = {
|
||||
canvas: HTMLCanvasElement;
|
||||
rc: RoughCanvas;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
allElementsMap: NonDeletedSceneElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
versionNonce: number | undefined;
|
||||
selectionNonce: number | undefined;
|
||||
@@ -63,7 +70,8 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
||||
canvas,
|
||||
rc: props.rc,
|
||||
scale: props.scale,
|
||||
elements: props.elements,
|
||||
elementsMap: props.elementsMap,
|
||||
allElementsMap: props.allElementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
appState: props.appState,
|
||||
renderConfig: props.renderConfig,
|
||||
@@ -106,10 +114,10 @@ const areEqual = (
|
||||
if (
|
||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||
prevProps.scale !== nextProps.scale ||
|
||||
// we need to memoize on element arrays because they may have renewed
|
||||
// we need to memoize on elementsMap because they may have renewed
|
||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||
// on appState)
|
||||
prevProps.elements !== nextProps.elements ||
|
||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||
prevProps.visibleElements !== nextProps.visibleElements
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,6 @@ import cssVariables from "./css/variables.module.scss";
|
||||
import { AppProps } from "./types";
|
||||
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
||||
import { COLOR_PALETTE } from "./colors";
|
||||
|
||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
export const isWindows = /^Win/.test(navigator.platform);
|
||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||
@@ -143,6 +142,7 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
||||
export const DEFAULT_TEXT_ALIGN = "left";
|
||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||
export const DEFAULT_VERSION = "{version}";
|
||||
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
||||
|
||||
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { elementsOverlappingBBox } from "../../utils/export";
|
||||
import { isSomeElementSelected, getSelectedElements } from "../scene";
|
||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||
import { ExportType } from "../scene/types";
|
||||
@@ -20,6 +19,7 @@ import { cloneJSON } from "../utils";
|
||||
import { canvasToBlob } from "./blob";
|
||||
import { fileSave, FileSystemHandle } from "./filesystem";
|
||||
import { serializeAsJSON } from "./json";
|
||||
import { getElementsOverlappingFrame } from "../frame";
|
||||
|
||||
export { loadFromBlob } from "./blob";
|
||||
export { loadFromJSON, saveAsJSON } from "./json";
|
||||
@@ -56,11 +56,7 @@ export const prepareElementsForExport = (
|
||||
isFrameLikeElement(exportedElements[0])
|
||||
) {
|
||||
exportingFrame = exportedElements[0];
|
||||
exportedElements = elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: exportingFrame,
|
||||
type: "overlap",
|
||||
});
|
||||
exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
|
||||
} else if (exportedElements.length > 1) {
|
||||
exportedElements = getSelectedElements(
|
||||
elements,
|
||||
|
||||
@@ -40,6 +40,7 @@ import { arrayToMap } from "../utils";
|
||||
import { MarkOptional, Mutable } from "../utility-types";
|
||||
import {
|
||||
detectLineHeight,
|
||||
getContainerElement,
|
||||
getDefaultLineHeight,
|
||||
measureBaseline,
|
||||
} from "../element/textElement";
|
||||
@@ -179,7 +180,6 @@ const restoreElementWithProperties = <
|
||||
|
||||
const restoreElement = (
|
||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||
refreshDimensions = false,
|
||||
): typeof element | null => {
|
||||
switch (element.type) {
|
||||
case "text":
|
||||
@@ -232,10 +232,6 @@ const restoreElement = (
|
||||
element = bumpVersion(element);
|
||||
}
|
||||
|
||||
if (refreshDimensions) {
|
||||
element = { ...element, ...refreshTextDimensions(element) };
|
||||
}
|
||||
|
||||
return element;
|
||||
case "freedraw": {
|
||||
return restoreElementWithProperties(element, {
|
||||
@@ -426,10 +422,7 @@ export const restoreElements = (
|
||||
// filtering out selection, which is legacy, no longer kept in elements,
|
||||
// and causing issues if retained
|
||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||
element,
|
||||
opts?.refreshDimensions,
|
||||
);
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
||||
if (migratedElement) {
|
||||
const localElement = localElementsMap?.get(element.id);
|
||||
if (localElement && localElement.version > migratedElement.version) {
|
||||
@@ -462,6 +455,16 @@ export const restoreElements = (
|
||||
} else if (element.boundElements) {
|
||||
repairContainerElement(element, restoredElementsMap);
|
||||
}
|
||||
|
||||
if (opts.refreshDimensions && isTextElement(element)) {
|
||||
Object.assign(
|
||||
element,
|
||||
refreshTextDimensions(
|
||||
element,
|
||||
getContainerElement(element, restoredElementsMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return restoredElements;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
normalizeText,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
VerticalAlign,
|
||||
} from "../element/types";
|
||||
import { MarkOptional } from "../utility-types";
|
||||
import { assertNever, cloneJSON, getFontString } from "../utils";
|
||||
import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { randomId } from "../random";
|
||||
|
||||
@@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100;
|
||||
const bindTextToContainer = (
|
||||
container: ExcalidrawElement,
|
||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const textElement: ExcalidrawTextElement = newTextElement({
|
||||
x: 0,
|
||||
@@ -623,6 +625,7 @@ export const convertToExcalidrawElements = (
|
||||
let [container, text] = bindTextToContainer(
|
||||
excalidrawElement,
|
||||
element?.label,
|
||||
arrayToMap(elementStore.getElements()),
|
||||
);
|
||||
elementStore.add(container);
|
||||
elementStore.add(text);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { newElementWith } from "./element/mutateElement";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
import { getCommonBoundingBox } from "./element/bounds";
|
||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||
|
||||
export interface Distribution {
|
||||
space: "between";
|
||||
@@ -10,6 +10,7 @@ export interface Distribution {
|
||||
|
||||
export const distributeElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
distribution: Distribution,
|
||||
): ExcalidrawElement[] => {
|
||||
const [start, mid, end, extent] =
|
||||
@@ -18,7 +19,7 @@ export const distributeElements = (
|
||||
: (["minY", "midY", "maxY", "height"] as const);
|
||||
|
||||
const bounds = getCommonBoundingBox(selectedElements);
|
||||
const groups = getMaximumGroups(selectedElements)
|
||||
const groups = getMaximumGroups(selectedElements, elementsMap)
|
||||
.map((group) => [group, getCommonBoundingBox(group)] as const)
|
||||
.sort((a, b) => a[1][mid] - b[1][mid]);
|
||||
|
||||
|
||||
@@ -120,8 +120,11 @@ export const Hyperlink = ({
|
||||
} else {
|
||||
const { width, height } = element;
|
||||
const embedLink = getEmbedLink(link);
|
||||
if (embedLink?.warning) {
|
||||
setToast({ message: embedLink.warning, closable: true });
|
||||
if (embedLink?.error instanceof URIError) {
|
||||
setToast({
|
||||
message: t("toast.unrecognizedLinkFormat"),
|
||||
closable: true,
|
||||
});
|
||||
}
|
||||
const ar = embedLink
|
||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
||||
|
||||
@@ -321,9 +321,9 @@ export const updateBoundElements = (
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
||||
const scene = Scene.getScene(changedElement)!;
|
||||
getNonDeletedElements(
|
||||
Scene.getScene(changedElement)!,
|
||||
scene,
|
||||
boundLinearElements.map((el) => el.id),
|
||||
).forEach((element) => {
|
||||
if (!isLinearElement(element)) {
|
||||
@@ -362,9 +362,12 @@ export const updateBoundElements = (
|
||||
endBinding,
|
||||
changedElement as ExcalidrawBindableElement,
|
||||
);
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundText) {
|
||||
handleBindTextResize(element, false);
|
||||
handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMapOrArray,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import { distance2d, rotate, rotatePoint } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
@@ -73,13 +75,16 @@ export class ElementBounds {
|
||||
) {
|
||||
return cachedBounds.bounds;
|
||||
}
|
||||
|
||||
const bounds = ElementBounds.calculateBounds(element);
|
||||
const scene = Scene.getScene(element);
|
||||
const bounds = ElementBounds.calculateBounds(
|
||||
element,
|
||||
scene?.getNonDeletedElementsMap() || new Map(),
|
||||
);
|
||||
|
||||
// hack to ensure that downstream checks could retrieve element Scene
|
||||
// so as to have correctly calculated bounds
|
||||
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
|
||||
const shouldCache = Scene.getScene(element);
|
||||
const shouldCache = !!scene;
|
||||
|
||||
if (shouldCache) {
|
||||
ElementBounds.boundsCache.set(element, {
|
||||
@@ -91,7 +96,10 @@ export class ElementBounds {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
||||
private static calculateBounds(
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): Bounds {
|
||||
let bounds: Bounds;
|
||||
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||
@@ -110,7 +118,7 @@ export class ElementBounds {
|
||||
maxY + element.y,
|
||||
];
|
||||
} else if (isLinearElement(element)) {
|
||||
bounds = getLinearElementRotatedBounds(element, cx, cy);
|
||||
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||
} else if (element.type === "diamond") {
|
||||
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
||||
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
||||
@@ -153,15 +161,20 @@ export const getElementAbsoluteCoords = (
|
||||
element: ExcalidrawElement,
|
||||
includeBoundText: boolean = false,
|
||||
): [number, number, number, number, number, number] => {
|
||||
const elementsMap =
|
||||
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
|
||||
if (isFreeDrawElement(element)) {
|
||||
return getFreeDrawElementAbsoluteCoords(element);
|
||||
} else if (isLinearElement(element)) {
|
||||
return LinearElementEditor.getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
includeBoundText,
|
||||
);
|
||||
} else if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
const container = elementsMap
|
||||
? getContainerElement(element, elementsMap)
|
||||
: null;
|
||||
if (isArrowElement(container)) {
|
||||
const coords = LinearElementEditor.getBoundTextElementPosition(
|
||||
container,
|
||||
@@ -672,7 +685,10 @@ const getLinearElementRotatedBounds = (
|
||||
element: ExcalidrawLinearElement,
|
||||
cx: number,
|
||||
cy: number,
|
||||
elementsMap: ElementsMap,
|
||||
): Bounds => {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (element.points.length < 2) {
|
||||
const [pointX, pointY] = element.points[0];
|
||||
const [x, y] = rotate(
|
||||
@@ -684,7 +700,6 @@ const getLinearElementRotatedBounds = (
|
||||
);
|
||||
|
||||
let coords: Bounds = [x, y, x, y];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
@@ -709,7 +724,6 @@ const getLinearElementRotatedBounds = (
|
||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
@@ -729,10 +743,8 @@ const getLinearElementRotatedBounds = (
|
||||
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
||||
return ElementBounds.getBounds(element);
|
||||
};
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): Bounds => {
|
||||
if (!elements.length) {
|
||||
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
|
||||
if ("size" in elements ? !elements.size : !elements.length) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
StrokeRoundness,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import {
|
||||
@@ -78,6 +79,7 @@ export const hitTest = (
|
||||
frameNameBoundsCache: FrameNameBoundsCache,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
// How many pixels off the shape boundary we still consider a hit
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
@@ -95,7 +97,7 @@ export const hitTest = (
|
||||
);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
const isHittingBoundTextElement = hitTest(
|
||||
boundTextElement,
|
||||
@@ -103,6 +105,7 @@ export const hitTest = (
|
||||
frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
elementsMap,
|
||||
);
|
||||
if (isHittingBoundTextElement) {
|
||||
return true;
|
||||
@@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
|
||||
frameNameBoundsCache: FrameNameBoundsCache,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
|
||||
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
|
||||
// eg for linear elements text can be outside the element bounding box
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (
|
||||
boundTextElement &&
|
||||
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
|
||||
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
33
packages/excalidraw/element/containerCache.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ExcalidrawTextContainer } from "./types";
|
||||
|
||||
export const originalContainerCache: {
|
||||
[id: ExcalidrawTextContainer["id"]]:
|
||||
| {
|
||||
height: ExcalidrawTextContainer["height"];
|
||||
}
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
export const updateOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
height: ExcalidrawTextContainer["height"],
|
||||
) => {
|
||||
const data =
|
||||
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
||||
data.height = height;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const resetOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
if (originalContainerCache[id]) {
|
||||
delete originalContainerCache[id];
|
||||
}
|
||||
};
|
||||
|
||||
export const getOriginalContainerHeightFromCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
return originalContainerCache[id]?.height ?? null;
|
||||
};
|
||||
@@ -5,14 +5,9 @@ import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
import { AppState, PointerDownState } from "../types";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { isSelectedViaGroup } from "../groups";
|
||||
import { getGridPoint } from "../math";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isFrameLikeElement,
|
||||
} from "./typeChecks";
|
||||
import { isArrowElement, isFrameLikeElement } from "./typeChecks";
|
||||
|
||||
export const dragSelectedElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
@@ -37,13 +32,11 @@ export const dragSelectedElements = (
|
||||
.map((f) => f.id);
|
||||
|
||||
if (frames.length > 0) {
|
||||
const elementsInFrames = scene
|
||||
.getNonDeletedElements()
|
||||
.filter((e) => !isBoundToContainer(e))
|
||||
.filter((e) => e.frameId !== null)
|
||||
.filter((e) => frames.includes(e.frameId!));
|
||||
|
||||
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
||||
for (const element of scene.getNonDeletedElements()) {
|
||||
if (element.frameId !== null && frames.includes(element.frameId)) {
|
||||
elementsToUpdate.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commonBounds = getCommonBounds(
|
||||
@@ -60,18 +53,14 @@ export const dragSelectedElements = (
|
||||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||
// update coords of bound text only if we're dragging the container directly
|
||||
// (we don't drag the group that it's part of)
|
||||
if (
|
||||
// Don't update coords of arrow label since we calculate its position during render
|
||||
!isArrowElement(element) &&
|
||||
// container isn't part of any group
|
||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
||||
(!element.groupIds.length ||
|
||||
// container is part of a group, but we're dragging the container directly
|
||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
|
||||
// skip arrow labels since we calculate its position during render
|
||||
!isArrowElement(element)
|
||||
) {
|
||||
const textElement = getBoundTextElement(element);
|
||||
const textElement = getBoundTextElement(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { register } from "../actions/register";
|
||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { ExcalidrawProps } from "../types";
|
||||
import { getFontString, updateActiveTool } from "../utils";
|
||||
import { setCursorForShape } from "../cursor";
|
||||
import { newTextElement } from "./newElement";
|
||||
import { getContainerElement, wrapText } from "./textElement";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
isIframeElement,
|
||||
isIframeLikeElement,
|
||||
} from "./typeChecks";
|
||||
import { wrapText } from "./textElement";
|
||||
import { isIframeElement } from "./typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
IframeData,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
const embeddedLinkCache = new Map<string, IframeData>();
|
||||
@@ -112,8 +106,8 @@ export const getEmbedLink = (
|
||||
const vimeoLink = link.match(RE_VIMEO);
|
||||
if (vimeoLink?.[1]) {
|
||||
const target = vimeoLink?.[1];
|
||||
const warning = !/^\d+$/.test(target)
|
||||
? t("toast.unrecognizedLinkFormat")
|
||||
const error = !/^\d+$/.test(target)
|
||||
? new URIError("Invalid embed link format")
|
||||
: undefined;
|
||||
type = "video";
|
||||
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||
@@ -125,7 +119,7 @@ export const getEmbedLink = (
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type, warning };
|
||||
return { link, intrinsicSize: aspectRatio, type, error };
|
||||
}
|
||||
|
||||
const figmaLink = link.match(RE_FIGMA);
|
||||
@@ -217,21 +211,6 @@ export const getEmbedLink = (
|
||||
return { link, intrinsicSize: aspectRatio, type };
|
||||
};
|
||||
|
||||
export const isIframeLikeOrItsLabel = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
): Boolean => {
|
||||
if (isIframeLikeElement(element)) {
|
||||
return true;
|
||||
}
|
||||
if (element.type === "text") {
|
||||
const container = getContainerElement(element);
|
||||
if (container && isFrameLikeElement(container)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const createPlaceholderEmbeddableLabel = (
|
||||
element: ExcalidrawIframeLikeElement,
|
||||
): ExcalidrawElement => {
|
||||
|
||||
@@ -50,7 +50,6 @@ export {
|
||||
dragNewElement,
|
||||
} from "./dragElements";
|
||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||
export { textWysiwyg } from "./textWysiwyg";
|
||||
export { redrawTextBoundingBox } from "./textElement";
|
||||
export {
|
||||
getPerfectElementSize,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
PointBinding,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import {
|
||||
distance2d,
|
||||
@@ -193,6 +194,7 @@ export class LinearElementEditor {
|
||||
pointSceneCoords: { x: number; y: number }[],
|
||||
) => void,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean {
|
||||
if (!linearElementEditor) {
|
||||
return false;
|
||||
@@ -272,9 +274,9 @@ export class LinearElementEditor {
|
||||
);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, false);
|
||||
handleBindTextResize(element, elementsMap, false);
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
@@ -404,9 +406,10 @@ export class LinearElementEditor {
|
||||
|
||||
static getEditorMidPoints = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): typeof editorMidPointsCache["points"] => {
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
|
||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||
if (
|
||||
@@ -465,6 +468,7 @@ export class LinearElementEditor {
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scenePointer: { x: number; y: number },
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
@@ -503,7 +507,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
let index = 0;
|
||||
const midPoints: typeof editorMidPointsCache["points"] =
|
||||
LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
const distance = distance2d(
|
||||
@@ -581,6 +585,7 @@ export class LinearElementEditor {
|
||||
linearElementEditor: LinearElementEditor,
|
||||
appState: AppState,
|
||||
midPoint: Point,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
@@ -588,7 +593,11 @@ export class LinearElementEditor {
|
||||
if (!element) {
|
||||
return -1;
|
||||
}
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
);
|
||||
let index = 0;
|
||||
while (index < midPoints.length) {
|
||||
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
|
||||
@@ -605,6 +614,7 @@ export class LinearElementEditor {
|
||||
history: History,
|
||||
scenePointer: { x: number; y: number },
|
||||
linearElementEditor: LinearElementEditor,
|
||||
elementsMap: ElementsMap,
|
||||
): {
|
||||
didAddPoint: boolean;
|
||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||
@@ -630,6 +640,7 @@ export class LinearElementEditor {
|
||||
linearElementEditor,
|
||||
scenePointer,
|
||||
appState,
|
||||
elementsMap,
|
||||
);
|
||||
let segmentMidpointIndex = null;
|
||||
if (segmentMidpoint) {
|
||||
@@ -637,6 +648,7 @@ export class LinearElementEditor {
|
||||
linearElementEditor,
|
||||
appState,
|
||||
segmentMidpoint,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
@@ -1418,6 +1430,7 @@ export class LinearElementEditor {
|
||||
|
||||
static getElementAbsoluteCoords = (
|
||||
element: ExcalidrawLinearElement,
|
||||
elementsMap: ElementsMap,
|
||||
includeBoundText: boolean = false,
|
||||
): [number, number, number, number, number, number] => {
|
||||
let coords: [number, number, number, number, number, number];
|
||||
@@ -1462,7 +1475,7 @@ export class LinearElementEditor {
|
||||
if (!includeBoundText) {
|
||||
return coords;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
coords = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
|
||||
@@ -31,7 +31,6 @@ import { getElementAbsoluteCoords } from ".";
|
||||
import { adjustXYWithRotation } from "../math";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import {
|
||||
getContainerElement,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
@@ -333,17 +332,17 @@ const getAdjustedDimensions = (
|
||||
|
||||
export const refreshTextDimensions = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
text = textElement.text,
|
||||
) => {
|
||||
if (textElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
text = wrapText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
getBoundTextMaxWidth(container),
|
||||
getBoundTextMaxWidth(container, textElement),
|
||||
);
|
||||
}
|
||||
const dimensions = getAdjustedDimensions(textElement, text);
|
||||
@@ -352,6 +351,7 @@ export const refreshTextDimensions = (
|
||||
|
||||
export const updateTextElement = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
{
|
||||
text,
|
||||
isDeleted,
|
||||
@@ -365,7 +365,7 @@ export const updateTextElement = (
|
||||
return newElementWith(textElement, {
|
||||
originalText,
|
||||
isDeleted: isDeleted ?? textElement.isDeleted,
|
||||
...refreshTextDimensions(textElement, originalText),
|
||||
...refreshTextDimensions(textElement, container, originalText),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import {
|
||||
@@ -41,7 +42,7 @@ import {
|
||||
MaybeTransformHandleType,
|
||||
TransformHandleDirection,
|
||||
} from "./transformHandles";
|
||||
import { AppState, Point, PointerDownState } from "../types";
|
||||
import { Point, PointerDownState } from "../types";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
getApproxMinLineWidth,
|
||||
@@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => {
|
||||
|
||||
// Returns true when transform (resizing/rotation) happened
|
||||
export const transformElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
resizeArrowDirection: "origin" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
@@ -79,7 +80,6 @@ export const transformElements = (
|
||||
pointerY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
@@ -89,7 +89,6 @@ export const transformElements = (
|
||||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
pointerDownState.originalElements,
|
||||
);
|
||||
updateBoundElements(element);
|
||||
} else if (
|
||||
@@ -101,6 +100,7 @@ export const transformElements = (
|
||||
) {
|
||||
resizeSingleTextElement(
|
||||
element,
|
||||
elementsMap,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
@@ -109,9 +109,10 @@ export const transformElements = (
|
||||
updateBoundElements(element);
|
||||
} else if (transformHandleType) {
|
||||
resizeSingleElement(
|
||||
pointerDownState.originalElements,
|
||||
originalElements,
|
||||
shouldMaintainAspectRatio,
|
||||
element,
|
||||
elementsMap,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
@@ -123,8 +124,9 @@ export const transformElements = (
|
||||
} else if (selectedElements.length > 1) {
|
||||
if (transformHandleType === "rotation") {
|
||||
rotateMultipleElements(
|
||||
pointerDownState,
|
||||
originalElements,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
@@ -139,8 +141,9 @@ export const transformElements = (
|
||||
transformHandleType === "se"
|
||||
) {
|
||||
resizeMultipleElements(
|
||||
pointerDownState,
|
||||
originalElements,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
@@ -157,7 +160,6 @@ const rotateSingleElement = (
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
@@ -207,6 +209,7 @@ const rescalePointsInElement = (
|
||||
|
||||
const measureFontSizeFromWidth = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
elementsMap: ElementsMap,
|
||||
nextWidth: number,
|
||||
nextHeight: number,
|
||||
): { size: number; baseline: number } | null => {
|
||||
@@ -215,9 +218,9 @@ const measureFontSizeFromWidth = (
|
||||
|
||||
const hasContainer = isBoundToContainer(element);
|
||||
if (hasContainer) {
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (container) {
|
||||
width = getBoundTextMaxWidth(container);
|
||||
width = getBoundTextMaxWidth(container, element);
|
||||
}
|
||||
}
|
||||
const nextFontSize = element.fontSize * (nextWidth / width);
|
||||
@@ -257,6 +260,7 @@ const getSidesForTransformHandle = (
|
||||
|
||||
const resizeSingleTextElement = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
@@ -303,7 +307,12 @@ const resizeSingleTextElement = (
|
||||
if (scale > 0) {
|
||||
const nextWidth = element.width * scale;
|
||||
const nextHeight = element.height * scale;
|
||||
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
element,
|
||||
elementsMap,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
);
|
||||
if (metrics === null) {
|
||||
return;
|
||||
}
|
||||
@@ -342,6 +351,7 @@ export const resizeSingleElement = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleDirection: TransformHandleDirection,
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
@@ -385,7 +395,7 @@ export const resizeSingleElement = (
|
||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||
|
||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
||||
@@ -448,7 +458,8 @@ export const resizeSingleElement = (
|
||||
|
||||
const nextFont = measureFontSizeFromWidth(
|
||||
boundTextElement,
|
||||
getBoundTextMaxWidth(updatedElement),
|
||||
elementsMap,
|
||||
getBoundTextMaxWidth(updatedElement, boundTextElement),
|
||||
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
||||
);
|
||||
if (nextFont === null) {
|
||||
@@ -630,6 +641,7 @@ export const resizeSingleElement = (
|
||||
}
|
||||
handleBindTextResize(
|
||||
element,
|
||||
elementsMap,
|
||||
transformHandleDirection,
|
||||
shouldMaintainAspectRatio,
|
||||
);
|
||||
@@ -637,8 +649,9 @@ export const resizeSingleElement = (
|
||||
};
|
||||
|
||||
export const resizeMultipleElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
@@ -658,7 +671,7 @@ export const resizeMultipleElements = (
|
||||
}[],
|
||||
element,
|
||||
) => {
|
||||
const origElement = pointerDownState.originalElements.get(element.id);
|
||||
const origElement = originalElements.get(element.id);
|
||||
if (origElement) {
|
||||
acc.push({ orig: origElement, latest: element });
|
||||
}
|
||||
@@ -679,7 +692,7 @@ export const resizeMultipleElements = (
|
||||
if (!textId) {
|
||||
return acc;
|
||||
}
|
||||
const text = pointerDownState.originalElements.get(textId) ?? null;
|
||||
const text = originalElements.get(textId) ?? null;
|
||||
if (!isBoundToContainer(text)) {
|
||||
return acc;
|
||||
}
|
||||
@@ -825,7 +838,12 @@ export const resizeMultipleElements = (
|
||||
}
|
||||
|
||||
if (isTextElement(orig)) {
|
||||
const metrics = measureFontSizeFromWidth(orig, width, height);
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
orig,
|
||||
elementsMap,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
if (!metrics) {
|
||||
return;
|
||||
}
|
||||
@@ -833,7 +851,7 @@ export const resizeMultipleElements = (
|
||||
update.baseline = metrics.baseline;
|
||||
}
|
||||
|
||||
const boundTextElement = pointerDownState.originalElements.get(
|
||||
const boundTextElement = originalElements.get(
|
||||
getBoundTextElementId(orig) ?? "",
|
||||
) as ExcalidrawTextElementWithContainer | undefined;
|
||||
|
||||
@@ -866,7 +884,7 @@ export const resizeMultipleElements = (
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && boundTextFontSize) {
|
||||
mutateElement(
|
||||
boundTextElement,
|
||||
@@ -876,7 +894,7 @@ export const resizeMultipleElements = (
|
||||
},
|
||||
false,
|
||||
);
|
||||
handleBindTextResize(element, transformHandleType, true);
|
||||
handleBindTextResize(element, elementsMap, transformHandleType, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,8 +902,9 @@ export const resizeMultipleElements = (
|
||||
};
|
||||
|
||||
const rotateMultipleElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
@@ -906,8 +925,7 @@ const rotateMultipleElements = (
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const origAngle =
|
||||
pointerDownState.originalElements.get(element.id)?.angle ??
|
||||
element.angle;
|
||||
originalElements.get(element.id)?.angle ?? element.angle;
|
||||
const [rotatedCX, rotatedCY] = rotate(
|
||||
cx,
|
||||
cy,
|
||||
@@ -926,7 +944,7 @@ const rotateMultipleElements = (
|
||||
);
|
||||
updateBoundElements(element, { simultaneouslyUpdated: elements });
|
||||
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !isArrowElement(element)) {
|
||||
mutateElement(
|
||||
boundText,
|
||||
|
||||
@@ -319,17 +319,17 @@ describe("Test measureText", () => {
|
||||
|
||||
it("should return max width when container is rectangle", () => {
|
||||
const container = API.createElement({ type: "rectangle", ...params });
|
||||
expect(getBoundTextMaxWidth(container)).toBe(168);
|
||||
expect(getBoundTextMaxWidth(container, null)).toBe(168);
|
||||
});
|
||||
|
||||
it("should return max width when container is ellipse", () => {
|
||||
const container = API.createElement({ type: "ellipse", ...params });
|
||||
expect(getBoundTextMaxWidth(container)).toBe(116);
|
||||
expect(getBoundTextMaxWidth(container, null)).toBe(116);
|
||||
});
|
||||
|
||||
it("should return max width when container is diamond", () => {
|
||||
const container = API.createElement({ type: "diamond", ...params });
|
||||
expect(getBoundTextMaxWidth(container)).toBe(79);
|
||||
expect(getBoundTextMaxWidth(container, null)).toBe(79);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
||||
import {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementType,
|
||||
ExcalidrawTextContainer,
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { MaybeTransformHandleType } from "./transformHandles";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isTextElement } from ".";
|
||||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
@@ -31,11 +31,12 @@ import { isTextBindableContainer } from "./typeChecks";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
import {
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./textWysiwyg";
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
} from "./containerCache";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
@@ -88,7 +89,7 @@ export const redrawTextBoundingBox = (
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container, textElement);
|
||||
|
||||
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
||||
const nextHeight = computeContainerDimensionForBoundText(
|
||||
@@ -161,6 +162,7 @@ export const bindTextToShapeAfterDuplication = (
|
||||
|
||||
export const handleBindTextResize = (
|
||||
container: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
shouldMaintainAspectRatio = false,
|
||||
) => {
|
||||
@@ -169,25 +171,17 @@ export const handleBindTextResize = (
|
||||
return;
|
||||
}
|
||||
resetOriginalContainerCache(container.id);
|
||||
let textElement = Scene.getScene(container)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElement;
|
||||
const textElement = getBoundTextElement(container, elementsMap);
|
||||
if (textElement && textElement.text) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
textElement = Scene.getScene(container)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElement;
|
||||
let text = textElement.text;
|
||||
let nextHeight = textElement.height;
|
||||
let nextWidth = textElement.width;
|
||||
const maxWidth = getBoundTextMaxWidth(container);
|
||||
const maxHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
const maxWidth = getBoundTextMaxWidth(container, textElement);
|
||||
const maxHeight = getBoundTextMaxHeight(container, textElement);
|
||||
let containerHeight = container.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (
|
||||
@@ -242,10 +236,7 @@ export const handleBindTextResize = (
|
||||
if (!isArrowElement(container)) {
|
||||
mutateElement(
|
||||
textElement,
|
||||
computeBoundTextPosition(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
),
|
||||
computeBoundTextPosition(container, textElement),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +254,7 @@ export const computeBoundTextPosition = (
|
||||
}
|
||||
const containerCoords = getContainerCoords(container);
|
||||
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement);
|
||||
|
||||
let x;
|
||||
let y;
|
||||
@@ -666,33 +657,32 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||
: null;
|
||||
};
|
||||
|
||||
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
||||
export const getBoundTextElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
|
||||
if (boundTextElementId) {
|
||||
return (
|
||||
(Scene.getScene(element)?.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElementWithContainer) || null
|
||||
);
|
||||
return (elementsMap.get(boundTextElementId) ||
|
||||
null) as ExcalidrawTextElementWithContainer | null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getContainerElement = (
|
||||
element:
|
||||
| (ExcalidrawElement & {
|
||||
containerId: ExcalidrawElement["id"] | null;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
element: ExcalidrawTextElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawTextContainer | null => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.containerId) {
|
||||
return Scene.getScene(element)?.getElement(element.containerId) || null;
|
||||
return (elementsMap.get(element.containerId) ||
|
||||
null) as ExcalidrawTextContainer | null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -700,6 +690,7 @@ export const getContainerElement = (
|
||||
export const getContainerCenter = (
|
||||
container: ExcalidrawElement,
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
if (!isArrowElement(container)) {
|
||||
return {
|
||||
@@ -719,6 +710,7 @@ export const getContainerCenter = (
|
||||
const index = container.points.length / 2 - 1;
|
||||
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
|
||||
container,
|
||||
elementsMap,
|
||||
appState,
|
||||
)[index];
|
||||
if (!midSegmentMidpoint) {
|
||||
@@ -752,28 +744,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||
const container = getContainerElement(textElement);
|
||||
export const getTextElementAngle = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
) => {
|
||||
if (!container || isArrowElement(container)) {
|
||||
return textElement.angle;
|
||||
}
|
||||
return container.angle;
|
||||
};
|
||||
|
||||
export const getBoundTextElementOffset = (
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const container = getContainerElement(boundTextElement);
|
||||
if (!container || !boundTextElement) {
|
||||
return 0;
|
||||
}
|
||||
if (isArrowElement(container)) {
|
||||
return BOUND_TEXT_PADDING * 8;
|
||||
}
|
||||
|
||||
return BOUND_TEXT_PADDING;
|
||||
};
|
||||
|
||||
export const getBoundTextElementPosition = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
@@ -788,12 +768,12 @@ export const getBoundTextElementPosition = (
|
||||
|
||||
export const shouldAllowVerticalAlign = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return selectedElements.some((element) => {
|
||||
const hasBoundContainer = isBoundToContainer(element);
|
||||
if (hasBoundContainer) {
|
||||
const container = getContainerElement(element);
|
||||
if (isTextElement(element) && isArrowElement(container)) {
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -804,12 +784,12 @@ export const shouldAllowVerticalAlign = (
|
||||
|
||||
export const suppportsHorizontalAlign = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return selectedElements.some((element) => {
|
||||
const hasBoundContainer = isBoundToContainer(element);
|
||||
if (hasBoundContainer) {
|
||||
const container = getContainerElement(element);
|
||||
if (isTextElement(element) && isArrowElement(container)) {
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -890,9 +870,7 @@ export const computeContainerDimensionForBoundText = (
|
||||
|
||||
export const getBoundTextMaxWidth = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
|
||||
container,
|
||||
),
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const { width } = container;
|
||||
if (isArrowElement(container)) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "./types";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextContainer,
|
||||
} from "./types";
|
||||
import { AppState } from "../types";
|
||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
computeContainerDimensionForBoundText,
|
||||
detectLineHeight,
|
||||
computeBoundTextPosition,
|
||||
getBoundTextElement,
|
||||
} from "./textElement";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
@@ -43,6 +43,10 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||
import App from "../components/App";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import {
|
||||
originalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
|
||||
const getTransform = (
|
||||
width: number,
|
||||
@@ -65,38 +69,6 @@ const getTransform = (
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
};
|
||||
|
||||
const originalContainerCache: {
|
||||
[id: ExcalidrawTextContainer["id"]]:
|
||||
| {
|
||||
height: ExcalidrawTextContainer["height"];
|
||||
}
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
export const updateOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
height: ExcalidrawTextContainer["height"],
|
||||
) => {
|
||||
const data =
|
||||
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
||||
data.height = height;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const resetOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
if (originalContainerCache[id]) {
|
||||
delete originalContainerCache[id];
|
||||
}
|
||||
};
|
||||
|
||||
export const getOriginalContainerHeightFromCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
return originalContainerCache[id]?.height ?? null;
|
||||
};
|
||||
|
||||
export const textWysiwyg = ({
|
||||
id,
|
||||
onChange,
|
||||
@@ -153,7 +125,10 @@ export const textWysiwyg = ({
|
||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||
let coordX = updatedTextElement.x;
|
||||
let coordY = updatedTextElement.y;
|
||||
const container = getContainerElement(updatedTextElement);
|
||||
const container = getContainerElement(
|
||||
updatedTextElement,
|
||||
app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
let maxWidth = updatedTextElement.width;
|
||||
|
||||
let maxHeight = updatedTextElement.height;
|
||||
@@ -193,7 +168,8 @@ export const textWysiwyg = ({
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = getBoundTextMaxWidth(container);
|
||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||
|
||||
maxHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||
@@ -277,7 +253,7 @@ export const textWysiwyg = ({
|
||||
transform: getTransform(
|
||||
textElementWidth,
|
||||
textElementHeight,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
getTextElementAngle(updatedTextElement, container),
|
||||
appState,
|
||||
maxWidth,
|
||||
editorMaxHeight,
|
||||
@@ -348,17 +324,24 @@ export const textWysiwyg = ({
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(
|
||||
element,
|
||||
app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
|
||||
const font = getFontString({
|
||||
fontSize: app.state.currentItemFontSize,
|
||||
fontFamily: app.state.currentItemFontFamily,
|
||||
});
|
||||
if (container) {
|
||||
const boundTextElement = getBoundTextElement(
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const wrappedText = wrapText(
|
||||
`${editable.value}${data}`,
|
||||
font,
|
||||
getBoundTextMaxWidth(container),
|
||||
getBoundTextMaxWidth(container, boundTextElement),
|
||||
);
|
||||
const width = getTextWidth(wrappedText, font);
|
||||
editable.style.width = `${width}px`;
|
||||
@@ -528,7 +511,10 @@ export const textWysiwyg = ({
|
||||
return;
|
||||
}
|
||||
let text = editable.value;
|
||||
const container = getContainerElement(updateElement);
|
||||
const container = getContainerElement(
|
||||
updateElement,
|
||||
app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
|
||||
if (container) {
|
||||
text = updateElement.text;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { rotate } from "../math";
|
||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
| "n"
|
||||
@@ -106,7 +106,8 @@ export const getTransformHandlesFromCoords = (
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
const dashedLineMargin = margin / zoom.value;
|
||||
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
||||
const centeringOffset =
|
||||
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
|
||||
|
||||
const transformHandles: TransformHandles = {
|
||||
nw: omitSides.nw
|
||||
@@ -263,8 +264,8 @@ export const getTransformHandles = (
|
||||
};
|
||||
}
|
||||
const dashedLineMargin = isLinearElement(element)
|
||||
? DEFAULT_SPACING + 8
|
||||
: DEFAULT_SPACING;
|
||||
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
|
||||
: DEFAULT_TRANSFORM_HANDLE_SPACING;
|
||||
return getTransformHandlesFromCoords(
|
||||
getElementAbsoluteCoords(element, true),
|
||||
element.angle,
|
||||
|
||||
@@ -214,7 +214,10 @@ export const isBoundToContainer = (
|
||||
};
|
||||
|
||||
export const isUsingAdaptiveRadius = (type: string) =>
|
||||
type === "rectangle" || type === "embeddable" || type === "iframe";
|
||||
type === "rectangle" ||
|
||||
type === "embeddable" ||
|
||||
type === "iframe" ||
|
||||
type === "image";
|
||||
|
||||
export const isUsingProportionalRadius = (type: string) =>
|
||||
type === "line" || type === "arrow" || type === "diamond";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
THEME,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||
import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types";
|
||||
import { MagicCacheData } from "../data/magic";
|
||||
|
||||
export type ChartType = "bar" | "line";
|
||||
@@ -104,7 +104,7 @@ export type ExcalidrawIframeLikeElement =
|
||||
export type IframeData =
|
||||
| {
|
||||
intrinsicSize: { w: number; h: number };
|
||||
warning?: string;
|
||||
error?: Error;
|
||||
} & (
|
||||
| { type: "video" | "generic"; link: string }
|
||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||
@@ -254,3 +254,41 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||
export type FileId = string & { _brand: "FileId" };
|
||||
|
||||
export type ExcalidrawElementType = ExcalidrawElement["type"];
|
||||
|
||||
/**
|
||||
* Map of excalidraw elements.
|
||||
* Unspecified whether deleted or non-deleted.
|
||||
* Can be a subset of Scene elements.
|
||||
*/
|
||||
export type ElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement>;
|
||||
|
||||
/**
|
||||
* Map of non-deleted elements.
|
||||
* Can be a subset of Scene elements.
|
||||
*/
|
||||
export type NonDeletedElementsMap = Map<
|
||||
ExcalidrawElement["id"],
|
||||
NonDeletedExcalidrawElement
|
||||
> &
|
||||
MakeBrand<"NonDeletedElementsMap">;
|
||||
|
||||
/**
|
||||
* Map of all excalidraw Scene elements, including deleted.
|
||||
* Not a subset. Use this type when you need access to current Scene elements.
|
||||
*/
|
||||
export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
|
||||
MakeBrand<"SceneElementsMap">;
|
||||
|
||||
/**
|
||||
* Map of all non-deleted Scene elements.
|
||||
* Not a subset. Use this type when you need access to current Scene elements.
|
||||
*/
|
||||
export type NonDeletedSceneElementsMap = Map<
|
||||
ExcalidrawElement["id"],
|
||||
NonDeletedExcalidrawElement
|
||||
> &
|
||||
MakeBrand<"NonDeletedSceneElementsMap">;
|
||||
|
||||
export type ElementsMapOrArray =
|
||||
| readonly ExcalidrawElement[]
|
||||
| Readonly<ElementsMap>;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { ExcalidrawImperativeAPI } from "../types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
const { useDevice, Footer } = window.ExcalidrawLib;
|
||||
|
||||
const MobileFooter = ({
|
||||
excalidrawAPI,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
if (device.editor.isMobile) {
|
||||
return (
|
||||
<Footer>
|
||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
||||
</Footer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export default MobileFooter;
|
||||
@@ -1,17 +0,0 @@
|
||||
import App from "./App";
|
||||
|
||||
const { StrictMode } = window.React;
|
||||
//@ts-ignore
|
||||
const { createRoot } = window.ReactDOM;
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App
|
||||
appTitle={"Excalidraw Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
/>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
isTextElement,
|
||||
} from "./element";
|
||||
import {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeleted,
|
||||
@@ -21,8 +23,12 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||
import { getElementLineSegments } from "./element/bounds";
|
||||
import { doLineSegmentsIntersect } from "../utils/export";
|
||||
import {
|
||||
doLineSegmentsIntersect,
|
||||
elementsOverlappingBBox,
|
||||
} from "../utils/export";
|
||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
||||
import { ReadonlySetLike } from "./utility-types";
|
||||
|
||||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
@@ -104,17 +110,16 @@ export const elementsAreInFrameBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||
getElementAbsoluteCoords(frame);
|
||||
const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame);
|
||||
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getCommonBounds(elements);
|
||||
|
||||
return (
|
||||
selectionX1 <= elementX1 &&
|
||||
selectionY1 <= elementY1 &&
|
||||
selectionX2 >= elementX2 &&
|
||||
selectionY2 >= elementY2
|
||||
frameX1 <= elementX1 &&
|
||||
frameY1 <= elementY1 &&
|
||||
frameX2 >= elementX2 &&
|
||||
frameY2 >= elementY2
|
||||
);
|
||||
};
|
||||
|
||||
@@ -209,9 +214,17 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => {
|
||||
};
|
||||
|
||||
export const getFrameChildren = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
allElements: ElementsMapOrArray,
|
||||
frameId: string,
|
||||
) => allElements.filter((element) => element.frameId === frameId);
|
||||
) => {
|
||||
const frameChildren: ExcalidrawElement[] = [];
|
||||
for (const element of allElements.values()) {
|
||||
if (element.frameId === frameId) {
|
||||
frameChildren.push(element);
|
||||
}
|
||||
}
|
||||
return frameChildren;
|
||||
};
|
||||
|
||||
export const getFrameLikeElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
@@ -369,43 +382,107 @@ export const getContainingFrame = (
|
||||
|
||||
// --------------------------- Frame Operations -------------------------------
|
||||
|
||||
/** */
|
||||
export const filterElementsEligibleAsFrameChildren = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
|
||||
|
||||
elements = omitGroupsContainingFrameLikes(elements);
|
||||
|
||||
for (const element of elements) {
|
||||
if (isFrameLikeElement(element) && element.id !== frame.id) {
|
||||
otherFrames.add(element.id);
|
||||
}
|
||||
}
|
||||
|
||||
const processedGroups = new Set<ExcalidrawElement["id"]>();
|
||||
|
||||
const eligibleElements: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
// don't add frames or their children
|
||||
if (
|
||||
isFrameLikeElement(element) ||
|
||||
(element.frameId && otherFrames.has(element.frameId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.groupIds.length) {
|
||||
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||
if (!processedGroups.has(shallowestGroupId)) {
|
||||
processedGroups.add(shallowestGroupId);
|
||||
const groupElements = getElementsInGroup(elements, shallowestGroupId);
|
||||
if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) {
|
||||
for (const child of groupElements) {
|
||||
eligibleElements.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const overlaps = elementOverlapsWithFrame(element, frame);
|
||||
if (overlaps) {
|
||||
eligibleElements.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eligibleElements;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retains (or repairs for target frame) the ordering invriant where children
|
||||
* elements come right before the parent frame:
|
||||
* [el, el, child, child, frame, el]
|
||||
*
|
||||
* @returns mutated allElements (same data structure)
|
||||
*/
|
||||
export const addElementsToFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
||||
allElements: T,
|
||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
const { currTargetFrameChildrenMap } = allElements.reduce(
|
||||
(acc, element, index) => {
|
||||
if (element.frameId === frame.id) {
|
||||
acc.currTargetFrameChildrenMap.set(element.id, true);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
||||
},
|
||||
);
|
||||
): T => {
|
||||
const elementsMap = arrayToMap(allElements);
|
||||
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
|
||||
for (const element of allElements.values()) {
|
||||
if (element.frameId === frame.id) {
|
||||
currTargetFrameChildrenMap.set(element.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||
|
||||
const finalElementsToAdd: ExcalidrawElement[] = [];
|
||||
|
||||
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
|
||||
|
||||
for (const element of elementsToAdd) {
|
||||
if (isFrameLikeElement(element) && element.id !== frame.id) {
|
||||
otherFrames.add(element.id);
|
||||
}
|
||||
}
|
||||
|
||||
// - add bound text elements if not already in the array
|
||||
// - filter out elements that are already in the frame
|
||||
for (const element of omitGroupsContainingFrameLikes(
|
||||
allElements,
|
||||
elementsToAdd,
|
||||
)) {
|
||||
// don't add frames or their children
|
||||
if (
|
||||
isFrameLikeElement(element) ||
|
||||
(element.frameId && otherFrames.has(element.frameId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||
finalElementsToAdd.push(element);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (
|
||||
boundTextElement &&
|
||||
!suppliedElementsToAddSet.has(boundTextElement.id) &&
|
||||
@@ -424,13 +501,13 @@ export const addElementsToFrame = (
|
||||
false,
|
||||
);
|
||||
}
|
||||
return allElements.slice();
|
||||
|
||||
return allElements;
|
||||
};
|
||||
|
||||
export const removeElementsFromFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
elementsToRemove: NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
elementsToRemove: ReadonlySetLike<NonDeletedExcalidrawElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const _elementsToRemove = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
@@ -449,7 +526,7 @@ export const removeElementsFromFrame = (
|
||||
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
|
||||
arr.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
_elementsToRemove.set(boundTextElement.id, boundTextElement);
|
||||
arr.push(boundTextElement);
|
||||
@@ -468,35 +545,35 @@ export const removeElementsFromFrame = (
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return allElements.slice();
|
||||
};
|
||||
|
||||
export const removeAllElementsFromFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const removeAllElementsFromFrame = <T extends ExcalidrawElement>(
|
||||
allElements: readonly T[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const elementsInFrame = getFrameChildren(allElements, frame.id);
|
||||
return removeElementsFromFrame(allElements, elementsInFrame, appState);
|
||||
removeElementsFromFrame(elementsInFrame, arrayToMap(allElements));
|
||||
return allElements;
|
||||
};
|
||||
|
||||
export const replaceAllElementsInFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
|
||||
allElements: readonly T[],
|
||||
nextElementsInFrame: ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
app: AppClassProperties,
|
||||
): T[] => {
|
||||
return addElementsToFrame(
|
||||
removeAllElementsFromFrame(allElements, frame, appState),
|
||||
removeAllElementsFromFrame(allElements, frame),
|
||||
nextElementsInFrame,
|
||||
frame,
|
||||
);
|
||||
).slice();
|
||||
};
|
||||
|
||||
/** does not mutate elements, but returns new ones */
|
||||
export const updateFrameMembershipOfSelectedElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const updateFrameMembershipOfSelectedElements = <
|
||||
T extends ElementsMapOrArray,
|
||||
>(
|
||||
allElements: T,
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
@@ -521,19 +598,22 @@ export const updateFrameMembershipOfSelectedElements = (
|
||||
|
||||
const elementsToRemove = new Set<ExcalidrawElement>();
|
||||
|
||||
const elementsMap = arrayToMap(allElements);
|
||||
|
||||
elementsToFilter.forEach((element) => {
|
||||
if (
|
||||
element.frameId &&
|
||||
!isFrameLikeElement(element) &&
|
||||
!isElementInFrame(element, allElements, appState)
|
||||
!isElementInFrame(element, elementsMap, appState)
|
||||
) {
|
||||
elementsToRemove.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
return elementsToRemove.size > 0
|
||||
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
|
||||
: allElements;
|
||||
if (elementsToRemove.size > 0) {
|
||||
removeElementsFromFrame(elementsToRemove, elementsMap);
|
||||
}
|
||||
return allElements;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -541,14 +621,16 @@ export const updateFrameMembershipOfSelectedElements = (
|
||||
* anywhere in the group tree
|
||||
*/
|
||||
export const omitGroupsContainingFrameLikes = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
allElements: ElementsMapOrArray,
|
||||
/** subset of elements you want to filter. Optional perf optimization so we
|
||||
* don't have to filter all elements unnecessarily
|
||||
*/
|
||||
selectedElements?: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
const uniqueGroupIds = new Set<string>();
|
||||
for (const el of selectedElements || allElements) {
|
||||
const elements = selectedElements || allElements;
|
||||
|
||||
for (const el of elements.values()) {
|
||||
const topMostGroupId = el.groupIds[el.groupIds.length - 1];
|
||||
if (topMostGroupId) {
|
||||
uniqueGroupIds.add(topMostGroupId);
|
||||
@@ -566,9 +648,15 @@ export const omitGroupsContainingFrameLikes = (
|
||||
}
|
||||
}
|
||||
|
||||
return (selectedElements || allElements).filter(
|
||||
(el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]),
|
||||
);
|
||||
const ret: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elements.values()) {
|
||||
if (!rejectedGroupIds.has(element.groupIds[element.groupIds.length - 1])) {
|
||||
ret.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -577,10 +665,11 @@ export const omitGroupsContainingFrameLikes = (
|
||||
*/
|
||||
export const getTargetFrame = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const _element = isTextElement(element)
|
||||
? getContainerElement(element) || element
|
||||
? getContainerElement(element, elementsMap) || element
|
||||
: element;
|
||||
|
||||
return appState.selectedElementIds[_element.id] &&
|
||||
@@ -593,12 +682,12 @@ export const getTargetFrame = (
|
||||
// given an element, return if the element is in some frame
|
||||
export const isElementInFrame = (
|
||||
element: ExcalidrawElement,
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
allElements: ElementsMap,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const frame = getTargetFrame(element, appState);
|
||||
const frame = getTargetFrame(element, allElements, appState);
|
||||
const _element = isTextElement(element)
|
||||
? getContainerElement(element) || element
|
||||
? getContainerElement(element, allElements) || element
|
||||
: element;
|
||||
|
||||
if (frame) {
|
||||
@@ -657,10 +746,26 @@ export const getFrameLikeTitle = (
|
||||
element: ExcalidrawFrameLikeElement,
|
||||
frameIdx: number,
|
||||
) => {
|
||||
const existingName = element.name?.trim();
|
||||
if (existingName) {
|
||||
return existingName;
|
||||
}
|
||||
// TODO name frames AI only is specific to AI frames
|
||||
return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`;
|
||||
// TODO name frames "AI" only if specific to AI frames
|
||||
return element.name === null
|
||||
? isFrameElement(element)
|
||||
? `Frame ${frameIdx}`
|
||||
: `AI Frame $${frameIdx}`
|
||||
: element.name;
|
||||
};
|
||||
|
||||
export const getElementsOverlappingFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
return (
|
||||
elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: frame,
|
||||
type: "overlap",
|
||||
})
|
||||
// removes elements who are overlapping, but are in a different frame,
|
||||
// and thus invisible in target frame
|
||||
.filter((el) => !el.frameId || el.frameId === frame.id)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
ElementsMapOrArray,
|
||||
ElementsMap,
|
||||
} from "./element/types";
|
||||
import {
|
||||
AppClassProperties,
|
||||
@@ -270,9 +272,17 @@ export const isElementInGroup = (element: ExcalidrawElement, groupId: string) =>
|
||||
element.groupIds.includes(groupId);
|
||||
|
||||
export const getElementsInGroup = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
groupId: string,
|
||||
) => elements.filter((element) => isElementInGroup(element, groupId));
|
||||
) => {
|
||||
const elementsInGroup: ExcalidrawElement[] = [];
|
||||
for (const element of elements.values()) {
|
||||
if (isElementInGroup(element, groupId)) {
|
||||
elementsInGroup.push(element);
|
||||
}
|
||||
}
|
||||
return elementsInGroup;
|
||||
};
|
||||
|
||||
export const getSelectedGroupIdForElement = (
|
||||
element: ExcalidrawElement,
|
||||
@@ -320,12 +330,12 @@ export const removeFromSelectedGroups = (
|
||||
|
||||
export const getMaximumGroups = (
|
||||
elements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawElement[][] => {
|
||||
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
||||
String,
|
||||
ExcalidrawElement[]
|
||||
>();
|
||||
|
||||
elements.forEach((element: ExcalidrawElement) => {
|
||||
const groupId =
|
||||
element.groupIds.length === 0
|
||||
@@ -335,7 +345,7 @@ export const getMaximumGroups = (
|
||||
const currentGroupMembers = groups.get(groupId) || [];
|
||||
|
||||
// Include bound text if present when grouping
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
currentGroupMembers.push(boundTextElement);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
onScrollChange,
|
||||
children,
|
||||
validateEmbeddable,
|
||||
@@ -80,6 +81,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const importPolyfill = async () => {
|
||||
//@ts-ignore
|
||||
await import("canvas-roundrect-polyfill");
|
||||
};
|
||||
|
||||
importPolyfill();
|
||||
|
||||
// Block pinch-zooming on iOS outside of the content area
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
// @ts-ignore
|
||||
@@ -124,6 +132,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
@@ -223,7 +232,7 @@ export {
|
||||
} from "../utils/export";
|
||||
export { isLinearElement } from "./element/typeChecks";
|
||||
|
||||
export { FONT_FAMILY, THEME, MIME_TYPES } from "./constants";
|
||||
export { FONT_FAMILY, THEME, MIME_TYPES, ROUNDNESS } from "./constants";
|
||||
|
||||
export {
|
||||
mutateElement,
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./dist/dev/index.js",
|
||||
"default": "./dist/prod/index.js",
|
||||
"types": "./dist/excalidraw/index.d.ts"
|
||||
"types": "./dist/excalidraw/index.d.ts",
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./index.css": {
|
||||
"development": "./dist/dev/index.css",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import {
|
||||
isTextElement,
|
||||
@@ -21,7 +22,11 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import type { Drawable } from "roughjs/bin/core";
|
||||
import type { RoughSVG } from "roughjs/bin/svg";
|
||||
|
||||
import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types";
|
||||
import {
|
||||
SVGRenderConfig,
|
||||
StaticCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
} from "../scene/types";
|
||||
import {
|
||||
distance,
|
||||
getFontString,
|
||||
@@ -186,6 +191,7 @@ const cappedElementCanvasSize = (
|
||||
|
||||
const generateElementCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
zoom: Zoom,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
@@ -243,7 +249,8 @@ const generateElementCanvas = (
|
||||
zoomValue: zoom.value,
|
||||
canvasOffsetX,
|
||||
canvasOffsetY,
|
||||
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
||||
boundTextElementVersion:
|
||||
getBoundTextElement(element, elementsMap)?.version || null,
|
||||
containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
|
||||
};
|
||||
};
|
||||
@@ -337,6 +344,17 @@ const drawElementOnCanvas = (
|
||||
? renderConfig.imageCache.get(element.fileId)?.image
|
||||
: undefined;
|
||||
if (img != null && !(img instanceof Promise)) {
|
||||
if (element.roundness && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
getCornerRadius(Math.min(element.width, element.height), element),
|
||||
);
|
||||
context.clip();
|
||||
}
|
||||
context.drawImage(
|
||||
img,
|
||||
0 /* hardcoded for the selection box*/,
|
||||
@@ -403,6 +421,7 @@ export const elementWithCanvasCache = new WeakMap<
|
||||
|
||||
const generateElementWithCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
@@ -412,7 +431,9 @@ const generateElementWithCanvas = (
|
||||
prevElementWithCanvas &&
|
||||
prevElementWithCanvas.zoomValue !== zoom.value &&
|
||||
!appState?.shouldCacheIgnoreZoom;
|
||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||
const boundTextElementVersion =
|
||||
getBoundTextElement(element, elementsMap)?.version || null;
|
||||
|
||||
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
||||
|
||||
if (
|
||||
@@ -424,6 +445,7 @@ const generateElementWithCanvas = (
|
||||
) {
|
||||
const elementWithCanvas = generateElementCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
zoom,
|
||||
renderConfig,
|
||||
appState,
|
||||
@@ -441,6 +463,7 @@ const drawElementFromCanvas = (
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
) => {
|
||||
const element = elementWithCanvas.element;
|
||||
const padding = getCanvasPadding(element);
|
||||
@@ -460,7 +483,8 @@ const drawElementFromCanvas = (
|
||||
|
||||
context.save();
|
||||
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, allElementsMap);
|
||||
|
||||
if (isArrowElement(element) && boundTextElement) {
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
@@ -507,7 +531,6 @@ const drawElementFromCanvas = (
|
||||
offsetY -
|
||||
padding * zoom;
|
||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||
|
||||
// Clear the bound text area
|
||||
tempCanvasContext.clearRect(
|
||||
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
|
||||
@@ -569,6 +592,7 @@ const drawElementFromCanvas = (
|
||||
) {
|
||||
const textElement = getBoundTextElement(
|
||||
element,
|
||||
allElementsMap,
|
||||
) as ExcalidrawTextElementWithContainer;
|
||||
const coords = getContainerCoords(element);
|
||||
context.strokeStyle = "#c92a2a";
|
||||
@@ -576,7 +600,7 @@ const drawElementFromCanvas = (
|
||||
context.strokeRect(
|
||||
(coords.x + appState.scrollX) * window.devicePixelRatio,
|
||||
(coords.y + appState.scrollY) * window.devicePixelRatio,
|
||||
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||
getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio,
|
||||
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||
);
|
||||
}
|
||||
@@ -611,6 +635,8 @@ export const renderSelectionElement = (
|
||||
|
||||
export const renderElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
@@ -682,6 +708,7 @@ export const renderElement = (
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
@@ -690,6 +717,7 @@ export const renderElement = (
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -715,7 +743,7 @@ export const renderElement = (
|
||||
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
const boundTextCoords =
|
||||
LinearElementEditor.getBoundTextElementPosition(
|
||||
@@ -732,7 +760,7 @@ export const renderElement = (
|
||||
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||
context.filter = "none";
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (isArrowElement(element) && boundTextElement) {
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
@@ -815,6 +843,7 @@ export const renderElement = (
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
@@ -846,6 +875,7 @@ export const renderElement = (
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
|
||||
// reset
|
||||
@@ -900,6 +930,7 @@ const maybeWrapNodesInFrameClipPath = (
|
||||
|
||||
export const renderElementToSvg = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
files: BinaryFiles,
|
||||
@@ -912,7 +943,7 @@ export const renderElementToSvg = (
|
||||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||
let cy = (y2 - y1) / 2 - (element.y - y1);
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
|
||||
|
||||
@@ -1013,6 +1044,7 @@ export const renderElementToSvg = (
|
||||
createPlaceholderEmbeddableLabel(element);
|
||||
renderElementToSvg(
|
||||
label,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
root,
|
||||
files,
|
||||
@@ -1089,7 +1121,7 @@ export const renderElementToSvg = (
|
||||
}
|
||||
case "line":
|
||||
case "arrow": {
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
|
||||
if (boundText) {
|
||||
maskPath.setAttribute("id", `mask-${element.id}`);
|
||||
@@ -1280,6 +1312,31 @@ export const renderElementToSvg = (
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
|
||||
if (element.roundness) {
|
||||
const clipPath = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"clipPath",
|
||||
);
|
||||
clipPath.id = `image-clipPath-${element.id}`;
|
||||
|
||||
const clipRect = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"rect",
|
||||
);
|
||||
const radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
clipRect.setAttribute("width", `${element.width}`);
|
||||
clipRect.setAttribute("height", `${element.height}`);
|
||||
clipRect.setAttribute("rx", `${radius}`);
|
||||
clipRect.setAttribute("ry", `${radius}`);
|
||||
clipPath.appendChild(clipRect);
|
||||
addToRoot(clipPath, element);
|
||||
|
||||
g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`);
|
||||
}
|
||||
|
||||
const clipG = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
|
||||