Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
commit
8f0d9f5230
140
README.md
140
README.md
@ -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
|
- 💯 Free & open-source.
|
||||||
|
- 🎨 Infinite, canvas-based whiteboard.
|
||||||
|
- ✍️ Hand-drawn like style.
|
||||||
|
- 🌓 Dark mode.
|
||||||
|
- 🏗️ Customizable.
|
||||||
|
- 📷 Image support.
|
||||||
|
- 😀 Shape libraries support.
|
||||||
|
- 👅 Localization (i18n) support.
|
||||||
|
- 🖼️ Export to PNG, SVG & clipboard.
|
||||||
|
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||||
|
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||||
|
- ➡️ Arrow-binding & labeled arrows.
|
||||||
|
- 🔙 Undo / Redo.
|
||||||
|
- 🔍 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:
|
||||||
|
|
||||||
|
- 📡 PWA support (works offline).
|
||||||
|
- 🤼 Real-time collaboration.
|
||||||
|
- 🔒 End-to-end encryption.
|
||||||
|
- 💾 Local-first support (autosaves to the browser).
|
||||||
|
- 🔗 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:
|
||||||
|
|
||||||
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
[](https://vercel.com) [](https://sentry.io) [](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/)
|
|
||||||
|
@ -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/>
|
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||||
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>
|
||||||
|
@ -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 />}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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`
|
||||||
|
@ -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"],
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -156,6 +156,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
|
{ repairBindings: true },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
@ -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 || {},
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
|
@ -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()}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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("?");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user