Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax

This commit is contained in:
Daniel J. Geiger 2023-02-19 16:02:22 -06:00
commit 8f0d9f5230
18 changed files with 319 additions and 68 deletions

140
README.md
View File

@ -1,29 +1,121 @@
<div align="center" style="display:flex;flex-direction:column;"}> <a href="https://excalidraw.com/" target="_blank" rel="noopener">
<a href="https://excalidraw.com"> <picture>
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams."/> <source media="(prefers-color-scheme: dark)" alt="Excalidraw" srcset="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover_dark.png" />
</a> <img alt="Excalidraw" src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover.png" />
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br/>Collaborative and end-to-end encrypted.</h3> </picture>
<p> </a>
<a href="https://twitter.com/excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/> <h4 align="center">
</a> <a href="https://excalidraw.com">Excalidraw Editor</a> |
<a href="https://discord.gg/UexuTaE"> <a href="https://blog.excalidraw.com">Blog</a> |
<img alt="Chat with us on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/> <a href="https://docs.excalidraw.com">Documentation</a> |
</a> <a href="https://plus.excalidraw.com">Excalidraw+</a>
</p> </h4>
<div align="center">
<h2>
An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br>
<br />
</h3>
</div> </div>
## Try now <br />
<p align="center">
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
</a>
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
</a>
<a href="https://discord.gg/UexuTaE">
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
</a>
<a href="https://twitter.com/excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
</a>
</p>
Visit [excalidraw.com](https://excalidraw.com) to start sketching. <div align="center">
<figure>
<a href="https://excalidraw.com" target="_blank" rel="noopener">
<img src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2Fproduct_showcase.png" alt="Product showcase" />
</a>
<figcaption>
<p align="center">
Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
</p>
</figcaption>
</figure>
</div>
## Community ## Features
For latest updates, follow us on [twitter](https://twitter.com/excalidraw). If you need help or want to chat, join us on [Discord](https://discord.gg/UexuTaE). For releases and deep dives, check out our [blog](https://blog.excalidraw.com). Report bugs on [GitHub](https://github.com/excalidraw/excalidraw/issues). The Excalidraw editor (npm package) supports:
## Supporting Excalidraw - 💯&nbsp;Free & open-source.
- 🎨&nbsp;Infinite, canvas-based whiteboard.
- ✍️&nbsp;Hand-drawn like style.
- 🌓&nbsp;Dark mode.
- 🏗️&nbsp;Customizable.
- 📷&nbsp;Image support.
- 😀&nbsp;Shape libraries support.
- 👅&nbsp;Localization (i18n) support.
- 🖼️&nbsp;Export to PNG, SVG & clipboard.
- 💾&nbsp;Open format - export drawings as an `.excalidraw` json file.
- ⚒️&nbsp;Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
- ➡️&nbsp;Arrow-binding & labeled arrows.
- 🔙&nbsp;Undo / Redo.
- 🔍&nbsp;Zoom and panning support.
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw). ## Excalidraw.com
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/maielo/new-readme/src/excalidraw-app) is part of this repository as well, and the app features:
- 📡&nbsp;PWA support (works offline).
- 🤼&nbsp;Real-time collaboration.
- 🔒&nbsp;End-to-end encryption.
- 💾&nbsp;Local-first support (autosaves to the browser).
- 🔗&nbsp;Shareable links (export to a readonly link you can share with others).
We'll be adding these features as drop-in plugins for the npm package in the future.
## Quick start
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
```
npm install react react-dom @excalidraw/excalidraw
```
or via yarn
```
yarn add react react-dom @excalidraw/excalidraw
```
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
## Contributing
- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues).
- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE).
- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating).
## Integrations
- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others
## Sponsors & support
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/).
## Thank you for supporting Excalidraw
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
@ -32,13 +124,3 @@ If you like the project, you can become a sponsor at [Open Collective](https://o
Last but not least, we're thankful to these companies for offering their services for free: Last but not least, we're thankful to these companies for offering their services for free:
[![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com) [![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com)
## Developers
You can integrate Excalidraw into your app by installing our [npm component](https://npmjs.com/package/@excalidraw/excalidraw).
Visit our documentation on [https://docs.excalidraw.com](https://docs.excalidraw.com).
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/)

View File

@ -53,7 +53,7 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
**_Signature_** **_Signature_**
<pre> <pre>
restoreElements( restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp; data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a> localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>

View File

@ -34,14 +34,16 @@ function App() {
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, you should render the component once the host is `mounted`.
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
```jsx showLineNumbers ```jsx showLineNumbers
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
export default function App() { export default function App() {
const [Comp, setComp] = useState(null); const [Excalidraw, setExcalidraw] = useState(null);
useEffect(() => { useEffect(() => {
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default)); import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
}, []); }, []);
return <>{Comp && <Comp />}</>; return <>{Excalidraw && <Excalidraw />}</>;
} }
``` ```

View File

@ -20,7 +20,7 @@ Pull requests are welcome. For major changes, please [open an issue](https://git
### Option 2 - CodeSandbox ### Option 2 - CodeSandbox
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw 1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw
1. Connect your GitHub account 1. Connect your GitHub account
1. Go to Git tab on left side 1. Go to Git tab on left side
1. Tap on `Fork Sandbox` 1. Tap on `Fork Sandbox`

View File

@ -132,6 +132,11 @@ const config = {
tableOfContents: { tableOfContents: {
maxHeadingLevel: 4, maxHeadingLevel: 4,
}, },
algolia: {
appId: "8FEAOD28DI",
apiKey: "4b07cca33ff2d2919bc95ff98f148e9e",
indexName: "excalidraw",
},
}), }),
themes: ["@docusaurus/theme-live-codeblock"], themes: ["@docusaurus/theme-live-codeblock"],
plugins: ["docusaurus-plugin-sass"], plugins: ["docusaurus-plugin-sass"],

View File

@ -21,7 +21,7 @@ export const getClientColors = (clientId: string, appState: AppState) => {
}; };
export const getClientInitials = (userName?: string | null) => { export const getClientInitials = (userName?: string | null) => {
if (!userName) { if (!userName?.trim()) {
return "?"; return "?";
} }
return userName.trim()[0].toUpperCase(); return userName.trim()[0].toUpperCase();

View File

@ -227,6 +227,7 @@ import {
setEraserCursor, setEraserCursor,
updateActiveTool, updateActiveTool,
getShortcutKey, getShortcutKey,
isTransparent,
} from "../utils"; } from "../utils";
import { import {
ContextMenu, ContextMenu,
@ -884,7 +885,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
}; };
} }
const scene = restore(initialData, null, null); const scene = restore(initialData, null, null, { repairBindings: true });
scene.appState = { scene.appState = {
...scene.appState, ...scene.appState,
theme: this.props.theme || scene.appState.theme, theme: this.props.theme || scene.appState.theme,
@ -2827,7 +2828,15 @@ class App extends React.Component<AppProps, AppState> {
sceneY, sceneY,
); );
if (container) { if (container) {
if (isArrowElement(container) || hasBoundTextElement(container)) { if (
isArrowElement(container) ||
hasBoundTextElement(container) ||
!isTransparent(container.backgroundColor) ||
isHittingElementNotConsideringBoundingBox(container, this.state, [
sceneX,
sceneY,
])
) {
const midPoint = getContainerCenter(container, this.state); const midPoint = getContainerCenter(container, this.state);
sceneX = midPoint.x; sceneX = midPoint.x;

View File

@ -156,6 +156,7 @@ export const loadSceneOrLibraryFromBlob = async (
}, },
localAppState, localAppState,
localElements, localElements,
{ repairBindings: true },
), ),
}; };
} else if (isValidLibrary(data)) { } else if (isValidLibrary(data)) {

View File

@ -344,7 +344,7 @@ export const restoreElements = (
elements: ImportedDataState["elements"], elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */ /** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined,
refreshDimensions = false, opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const localElementsMap = localElements ? arrayToMap(localElements) : null; const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = (elements || []).reduce((elements, element) => { const restoredElements = (elements || []).reduce((elements, element) => {
@ -353,7 +353,7 @@ export const restoreElements = (
if (element.type !== "selection" && !isInvisiblySmallElement(element)) { if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement( let migratedElement: ExcalidrawElement | null = restoreElement(
element, element,
refreshDimensions, opts?.refreshDimensions,
); );
if (migratedElement) { if (migratedElement) {
const localElement = localElementsMap?.get(element.id); const localElement = localElementsMap?.get(element.id);
@ -366,6 +366,10 @@ export const restoreElements = (
return elements; return elements;
}, [] as ExcalidrawElement[]); }, [] as ExcalidrawElement[]);
if (!opts?.repairBindings) {
return restoredElements;
}
// repair binding. Mutates elements. // repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements); const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) { for (const element of restoredElements) {
@ -508,9 +512,10 @@ export const restore = (
*/ */
localAppState: Partial<AppState> | null | undefined, localAppState: Partial<AppState> | null | undefined,
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined,
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
): RestoredDataState => { ): RestoredDataState => {
return { return {
elements: restoreElements(data?.elements, localElements), elements: restoreElements(data?.elements, localElements, elementsConfig),
appState: restoreAppState(data?.appState, localAppState || null), appState: restoreAppState(data?.appState, localAppState || null),
files: data?.files || {}, files: data?.files || {},
}; };

View File

@ -465,14 +465,21 @@ describe("textWysiwyg", () => {
}); });
}); });
it("should bind text to container when double clicked on center of filled container", async () => { it("should bind text to container when double clicked inside filled container", async () => {
const rectangle = API.createElement({
type: "rectangle",
x: 10,
y: 20,
width: 90,
height: 75,
backgroundColor: "red",
});
h.elements = [rectangle];
expect(h.elements.length).toBe(1); expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id); expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt( mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
expect(h.elements.length).toBe(2); expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer; const text = h.elements[1] as ExcalidrawTextElementWithContainer;
@ -506,24 +513,37 @@ describe("textWysiwyg", () => {
}); });
h.elements = [rectangle]; h.elements = [rectangle];
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
expect(h.elements.length).toBe(2);
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
editor.blur();
mouse.doubleClickAt( mouse.doubleClickAt(
rectangle.x + rectangle.width / 2, rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
); );
expect(h.elements.length).toBe(2); expect(h.elements.length).toBe(3);
const text = h.elements[1] as ExcalidrawTextElementWithContainer; text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text"); expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id); expect(text.containerId).toBe(rectangle.id);
mouse.down(); mouse.down();
const editor = document.querySelector( editor = document.querySelector(
".excalidraw-textEditorContainer > textarea", ".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement; ) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } }); fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
editor.blur(); editor.blur();
expect(rectangle.boundElements).toStrictEqual([ expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" }, { id: text.id, type: "text" },
]); ]);
@ -553,6 +573,43 @@ describe("textWysiwyg", () => {
]); ]);
}); });
it("should bind text to container when double clicked on container stroke", async () => {
const rectangle = API.createElement({
type: "rectangle",
x: 10,
y: 20,
width: 90,
height: 75,
strokeWidth: 4,
});
h.elements = [rectangle];
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt(rectangle.x + 2, rectangle.y + 2);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("shouldn't bind to non-text-bindable containers", async () => { it("shouldn't bind to non-text-bindable containers", async () => {
const freedraw = API.createElement({ const freedraw = API.createElement({
type: "freedraw", type: "freedraw",

View File

@ -137,7 +137,7 @@ export const isExcalidrawElement = (element: any): boolean => {
export const hasBoundTextElement = ( export const hasBoundTextElement = (
element: ExcalidrawElement | null, element: ExcalidrawElement | null,
): element is ExcalidrawBindableElement => { ): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
return ( return (
isBindableElement(element) && isBindableElement(element) &&
!!element.boundElements?.some(({ type }) => type === "text") !!element.boundElements?.some(({ type }) => type === "text")

View File

@ -610,7 +610,7 @@ class Collab extends PureComponent<Props, CollabState> {
const localElements = this.getSceneElementsIncludingDeleted(); const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState(); const appState = this.excalidrawAPI.getAppState();
remoteElements = restoreElements(remoteElements, null, false); remoteElements = restoreElements(remoteElements, null);
const reconciledElements = _reconcileElements( const reconciledElements = _reconcileElements(
localElements, localElements,

View File

@ -144,7 +144,7 @@ const RoomDialog = ({
<input <input
type="text" type="text"
id="username" id="username"
value={username || ""} value={username.trim() || ""}
className="RoomDialog-username TextInput" className="RoomDialog-username TextInput"
onChange={(event) => onUsernameChange(event.target.value)} onChange={(event) => onUsernameChange(event.target.value)}
onKeyPress={(event) => event.key === "Enter" && handleClose()} onKeyPress={(event) => event.key === "Enter" && handleClose()}

View File

@ -263,9 +263,12 @@ export const loadScene = async (
await importFromBackend(id, privateKey), await importFromBackend(id, privateKey),
localDataState?.appState, localDataState?.appState,
localDataState?.elements, localDataState?.elements,
{ repairBindings: true },
); );
} else { } else {
data = restore(localDataState || null, null, null); data = restore(localDataState || null, null, null, {
repairBindings: true,
});
} }
return { return {

View File

@ -365,7 +365,7 @@ const ExcalidrawWrapper = () => {
if (data.scene) { if (data.scene) {
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
...data.scene, ...data.scene,
...restore(data.scene, null, null), ...restore(data.scene, null, null, { repairBindings: true }),
commitToHistory: true, commitToHistory: true,
}); });
} }

View File

@ -11,6 +11,24 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## Unreleased
### Features
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
```js
{ refreshDimensions?: boolean, repairBindings?: boolean }
```
The same `opts` param has been added to [`restore`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restore) API as well.
For more details refer to the [docs](https://docs.excalidraw.com)
#### BREAKING CHANGE
- The optional parameter `refreshDimensions` in [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) has been removed and can be enabled via `opts`
## 0.14.2 (2023-02-01) ## 0.14.2 (2023-02-01)
### Features ### Features

View File

@ -36,4 +36,9 @@ describe("getClientInitials", () => {
result = getClientInitials(null); result = getClientInitials(null);
expect(result).toBe("?"); expect(result).toBe("?");
}); });
it('returns "?" when value is blank', () => {
const result = getClientInitials(" ");
expect(result).toBe("?");
});
}); });

View File

@ -534,7 +534,7 @@ describe("restore", () => {
}); });
describe("repairing bindings", () => { describe("repairing bindings", () => {
it("should repair container boundElements", () => { it("should repair container boundElements when repair is true", () => {
const container = API.createElement({ const container = API.createElement({
type: "rectangle", type: "rectangle",
boundElements: [], boundElements: [],
@ -546,11 +546,28 @@ describe("repairing bindings", () => {
expect(container.boundElements).toEqual([]); expect(container.boundElements).toEqual([]);
const restoredElements = restore.restoreElements( let restoredElements = restore.restoreElements(
[container, boundElement], [container, boundElement],
null, null,
); );
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [],
}),
expect.objectContaining({
id: boundElement.id,
containerId: container.id,
}),
]);
restoredElements = restore.restoreElements(
[container, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: container.id, id: container.id,
@ -563,7 +580,7 @@ describe("repairing bindings", () => {
]); ]);
}); });
it("should repair containerId of boundElements", () => { it("should repair containerId of boundElements when repair is true", () => {
const boundElement = API.createElement({ const boundElement = API.createElement({
type: "text", type: "text",
containerId: null, containerId: null,
@ -573,11 +590,28 @@ describe("repairing bindings", () => {
boundElements: [{ type: boundElement.type, id: boundElement.id }], boundElements: [{ type: boundElement.type, id: boundElement.id }],
}); });
const restoredElements = restore.restoreElements( let restoredElements = restore.restoreElements(
[container, boundElement], [container, boundElement],
null, null,
); );
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [{ type: boundElement.type, id: boundElement.id }],
}),
expect.objectContaining({
id: boundElement.id,
containerId: null,
}),
]);
restoredElements = restore.restoreElements(
[container, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: container.id, id: container.id,
@ -620,7 +654,7 @@ describe("repairing bindings", () => {
]); ]);
}); });
it("should remove bindings of deleted elements from boundElements", () => { it("should remove bindings of deleted elements from boundElements when repair is true", () => {
const container = API.createElement({ const container = API.createElement({
type: "rectangle", type: "rectangle",
boundElements: [], boundElements: [],
@ -642,6 +676,8 @@ describe("repairing bindings", () => {
type: invisibleBoundElement.type, type: invisibleBoundElement.type,
id: invisibleBoundElement.id, id: invisibleBoundElement.id,
}; };
expect(container.boundElements).toEqual([]);
const nonExistentBinding = { type: "text", id: "non-existent" }; const nonExistentBinding = { type: "text", id: "non-existent" };
// @ts-ignore // @ts-ignore
container.boundElements = [ container.boundElements = [
@ -650,17 +686,28 @@ describe("repairing bindings", () => {
nonExistentBinding, nonExistentBinding,
]; ];
expect(container.boundElements).toEqual([ let restoredElements = restore.restoreElements(
obsoleteBinding,
invisibleBinding,
nonExistentBinding,
]);
const restoredElements = restore.restoreElements(
[container, invisibleBoundElement, boundElement], [container, invisibleBoundElement, boundElement],
null, null,
); );
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
}),
expect.objectContaining({
id: boundElement.id,
containerId: container.id,
}),
]);
restoredElements = restore.restoreElements(
[container, invisibleBoundElement, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: container.id, id: container.id,
@ -673,7 +720,7 @@ describe("repairing bindings", () => {
]); ]);
}); });
it("should remove containerId if container not exists", () => { it("should remove containerId if container not exists when repair is true", () => {
const boundElement = API.createElement({ const boundElement = API.createElement({
type: "text", type: "text",
containerId: "non-existent", containerId: "non-existent",
@ -684,11 +731,28 @@ describe("repairing bindings", () => {
isDeleted: true, isDeleted: true,
}); });
const restoredElements = restore.restoreElements( let restoredElements = restore.restoreElements(
[boundElement, boundElementDeleted], [boundElement, boundElementDeleted],
null, null,
); );
expect(restoredElements).toEqual([
expect.objectContaining({
id: boundElement.id,
containerId: "non-existent",
}),
expect.objectContaining({
id: boundElementDeleted.id,
containerId: "non-existent",
}),
]);
restoredElements = restore.restoreElements(
[boundElement, boundElementDeleted],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: boundElement.id, id: boundElement.id,