mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
		
							
								
								
									
										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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Daniel J. Geiger
					Daniel J. Geiger