Compare commits
	
		
			91 Commits
		
	
	
		
			v0.17.5
			...
			feature/do
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f16967fc52 | ||
|   | 8a06b70588 | ||
|   | c31cbe646d | ||
|   | 0513b647ec | ||
|   | a289c42830 | ||
|   | d67eaa8710 | ||
|   | 0c3dffb082 | ||
|   | 0e0f34edd8 | ||
|   | 4888d9d355 | ||
|   | 1c39bd5781 | ||
|   | 90ad885446 | ||
|   | 1741c234a6 | ||
|   | 63b50b3586 | ||
|   | e0fefa8025 | ||
|   | d426cc968d | ||
|   | 2409c091ff | ||
|   | 626fe252ab | ||
|   | 10bd08ef19 | ||
|   | 2789d08154 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 678bb2b819 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 966f9aead9 | ||
|   | 4f0a2a9593 | ||
|   | f3f8217125 | ||
|   | 89bd6181f2 | ||
|   | c6fdac131b | ||
|   | 0415c616b1 | ||
|   | 740a165452 | ||
|   | 4997624a3a | ||
|   | b66daae1f3 | ||
|   | 1e7df58b5b | ||
|   | 46da032626 | ||
|   | 3b0593baa7 | ||
|   | dd530737a2 | ||
|   | a4e5e46dd1 | ||
|   | 0fa5f5de4c | ||
|   | 41cc746885 | ||
|   | 8ead8559e0 | ||
|   | 5245276409 | ||
|   | 0c24a7042f | ||
|   | 86cfeb714c | ||
|   | 872973f145 | ||
|   | 3ecf72a507 | ||
|   | 1aaa400876 | ||
|   | 65047cc2cb | ||
|   | 8b993d409e | ||
|   | 1cb350b2aa | ||
|   | 43ccc875fb | ||
|   | 4249b7dec8 | ||
|   | 49f15c736b | ||
|   | a8064ba3ee | ||
|   | e6c3c06c2e | ||
|   | d19b51d4f8 | ||
|   | c72e853c85 | ||
|   | 5f40a4cad4 | ||
|   | d91c98b82e | ||
|   | 57ea4e61d1 | ||
|   | 0808532b49 | ||
|   | 2a0fe2584e | ||
|   | 7bd6496854 | ||
|   | 537f6e7f68 | ||
|   | 6dfa89e846 | ||
|   | 561e919a2e | ||
|   | 20e3acf7a6 | ||
|   | 2c0929e537 | ||
|   | aad8ab0123 | ||
|   | 88a2b286c7 | ||
|   | b635b10b59 | ||
|   | 7ebda02b81 | ||
|   | d6cd8b78f1 | ||
|   | b7d7ccc929 | ||
|   | f14ad61bd0 | ||
|   | 8963baf5ad | ||
|   | 557add5bf7 | ||
|   | b9cfbc2077 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a04cc707c3 | ||
|   | 72ea8022bf | ||
|   | 4bdeaf999b | ||
|   | 42d8c5a040 | ||
|   | f299514e44 | ||
|   | dd220bcaea | ||
|   | fe75f29c15 | ||
|   | 14845a343b | ||
|   | dd8a7d41e2 | ||
|   | fda5c6fdf7 | ||
|   | 3d1631f375 | ||
|   | c7ee46e7f8 | ||
|   | d1e4421823 | ||
|   | 7c9cf30909 | ||
|   | 1e37dbd60e | ||
|   | f8d5c2a1b6 | ||
|   | 23b24ea5c3 | 
| @@ -6,6 +6,6 @@ | ||||
| !.prettierrc | ||||
| !package.json | ||||
| !public/ | ||||
| !src/ | ||||
| !packages/ | ||||
| !tsconfig.json | ||||
| !yarn.lock | ||||
|   | ||||
| @@ -7,12 +7,11 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu | ||||
| # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room) | ||||
| VITE_APP_WS_SERVER_URL=http://localhost:3002 | ||||
|  | ||||
| # set this only if using the collaboration workflow we use on excalidraw.com | ||||
| VITE_APP_PORTAL_URL= | ||||
|  | ||||
| VITE_APP_PLUS_LP=https://plus.excalidraw.com | ||||
| VITE_APP_PLUS_APP=https://app.excalidraw.com | ||||
|  | ||||
| VITE_APP_AI_BACKEND=http://localhost:3015 | ||||
|  | ||||
| VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}' | ||||
|  | ||||
| # put these in your .env.local, or make sure you don't commit! | ||||
|   | ||||
| @@ -4,14 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/ | ||||
| VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com | ||||
| VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries | ||||
|  | ||||
| VITE_APP_PORTAL_URL=https://portal.excalidraw.com | ||||
|  | ||||
| VITE_APP_PLUS_LP=https://plus.excalidraw.com | ||||
| VITE_APP_PLUS_APP=https://app.excalidraw.com | ||||
|  | ||||
| # Fill to set socket server URL used for collaboration. | ||||
| # Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow | ||||
| VITE_APP_WS_SERVER_URL= | ||||
| VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com | ||||
|  | ||||
| # socket server URL used for collaboration | ||||
| VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com | ||||
|  | ||||
| VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' | ||||
|  | ||||
|   | ||||
| @@ -5,4 +5,4 @@ package-lock.json | ||||
| firebase/ | ||||
| dist/ | ||||
| public/workbox | ||||
| src/packages/excalidraw/types | ||||
| packages/excalidraw/types | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/autorelease-excalidraw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -23,5 +23,5 @@ jobs: | ||||
|           NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | ||||
|       - name: Auto release | ||||
|         run: | | ||||
|           yarn add @actions/core | ||||
|           yarn add @actions/core -W | ||||
|           yarn autorelease | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/autorelease-preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -44,7 +44,7 @@ jobs: | ||||
|       - name: Auto release preview | ||||
|         id: "autorelease" | ||||
|         run: | | ||||
|           yarn add @actions/core | ||||
|           yarn add @actions/core -W | ||||
|           yarn autorelease preview ${{ github.event.issue.number }} | ||||
|       - name: Post comment post release | ||||
|         if: always() | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -16,7 +16,7 @@ jobs: | ||||
|  | ||||
|       - name: Install and lint | ||||
|         run: | | ||||
|           yarn --frozen-lockfile | ||||
|           yarn install | ||||
|           yarn test:other | ||||
|           yarn test:code | ||||
|           yarn test:typecheck | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/locales-coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -22,11 +22,11 @@ jobs: | ||||
|       - name: Create report file | ||||
|         run: | | ||||
|           yarn locales-coverage | ||||
|           FILE_CHANGED=$(git diff src/locales/percentages.json) | ||||
|           FILE_CHANGED=$(git diff packages/excalidraw/locales/percentages.json) | ||||
|           if [ ! -z "${FILE_CHANGED}" ]; then | ||||
|             git config --global user.name 'Excalidraw Bot' | ||||
|             git config --global user.email 'bot@excalidraw.com' | ||||
|             git add src/locales/percentages.json | ||||
|             git add packages/excalidraw/locales/percentages.json | ||||
|             git commit -am "Auto commit: Calculate translation coverage" | ||||
|             git push | ||||
|           fi | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/workflows/size-limit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -15,16 +15,14 @@ jobs: | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 18.x | ||||
|       - name: Install | ||||
|         run: yarn --frozen-lockfile | ||||
|       - name: Install in src/packages/excalidraw | ||||
|         run: yarn --frozen-lockfile | ||||
|         working-directory: src/packages/excalidraw | ||||
|       - name: Install in packages/excalidraw | ||||
|         run: yarn | ||||
|         working-directory: packages/excalidraw | ||||
|         env: | ||||
|           CI: true | ||||
|       - uses: andresz1/size-limit-action@v1 | ||||
|         with: | ||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           build_script: build:umd | ||||
|           build_script: build:esm | ||||
|           skip_step: install | ||||
|           directory: src/packages/excalidraw | ||||
|           directory: packages/excalidraw | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/test-coverage-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -16,7 +16,7 @@ jobs: | ||||
|         with: | ||||
|           node-version: "18.x" | ||||
|       - name: "Install Deps" | ||||
|         run: yarn --frozen-lockfile | ||||
|         run: yarn install | ||||
|       - name: "Test Coverage" | ||||
|         run: yarn test:coverage | ||||
|       - name: "Report Coverage" | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -13,5 +13,5 @@ jobs: | ||||
|           node-version: 18.x | ||||
|       - name: Install and test | ||||
|         run: | | ||||
|           yarn --frozen-lockfile | ||||
|           yarn install | ||||
|           yarn test:app | ||||
|   | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -21,10 +21,8 @@ npm-debug.log* | ||||
| package-lock.json | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| src/packages/excalidraw/types | ||||
| src/packages/excalidraw/example/public/bundle.js | ||||
| src/packages/excalidraw/example/public/excalidraw-assets-dev | ||||
| src/packages/excalidraw/example/public/excalidraw.development.js | ||||
| packages/excalidraw/types | ||||
| coverage | ||||
| dev-dist | ||||
| html | ||||
| examples/**/bundle.* | ||||
| @@ -85,7 +85,7 @@ We'll be adding these features as drop-in plugins for the npm package in the fut | ||||
|  | ||||
| ## Quick start | ||||
|  | ||||
| Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw): | ||||
| **Note:** following instructions are for installing the Excalidraw [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw) when integrating Excalidraw into your own app. To run the repository locally for development, please refer to our [Development Guide](https://docs.excalidraw.com/docs/introduction/development). | ||||
|  | ||||
| ``` | ||||
| npm install react react-dom @excalidraw/excalidraw | ||||
| @@ -97,7 +97,7 @@ or via yarn | ||||
| yarn add react react-dom @excalidraw/excalidraw | ||||
| ``` | ||||
|  | ||||
| Don't forget to check out our [Documentation](https://docs.excalidraw.com)! | ||||
| Check out our [documentation](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/installation) for more details! | ||||
|  | ||||
| ## Contributing | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| files: | ||||
|   - source: /src/locales/en.json | ||||
|     translation: /src/locales/%locale%.json | ||||
|   - source: /packages/excalidraw/locales/en.json | ||||
|     translation: /packages/excalidraw/locales/%locale%.json | ||||
|   | ||||
| @@ -133,7 +133,7 @@ function App() { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items. | ||||
| Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items. | ||||
|  | ||||
| ### MainMenu.Group | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,7 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme` | ||||
|  | ||||
| ### MIME_TYPES | ||||
|  | ||||
| [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L101) contains all the mime types supported by `Excalidraw`. | ||||
| [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L101) contains all the mime types supported by `Excalidraw`. | ||||
|  | ||||
| **How to use ** | ||||
|  | ||||
|   | ||||
| @@ -2,9 +2,9 @@ | ||||
|  | ||||
| We support a simplified API to make it easier to generate Excalidraw elements programmatically. This API is in beta and subject to change before stable. You can check the [PR](https://github.com/excalidraw/excalidraw/pull/6546) for more details. | ||||
|  | ||||
| For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers). | ||||
| For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers). | ||||
|  | ||||
| The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements). | ||||
| The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements). | ||||
|  | ||||
| ## convertToExcalidrawElements | ||||
|  | ||||
| @@ -19,7 +19,7 @@ convertToExcalidrawElements( | ||||
|  | ||||
| | Name | Type | Default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L137) |  | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. | | ||||
| | `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L137) |  | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. | | ||||
| | `opts` | `{ regenerateIds: boolean }` | ` {regenerateIds: true}` | By default `id` will be regenerated for all the elements irrespective of whether you pass the `id` so if you don't want the ids to regenerated, you can set this attribute to `false`. | | ||||
|  | ||||
| **_How to use_** | ||||
| @@ -71,7 +71,7 @@ function App() { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes. | ||||
| You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L27) as well to decorate the shapes. | ||||
|  | ||||
| :::info | ||||
|  | ||||
| @@ -192,7 +192,7 @@ convertToExcalidrawElements([ | ||||
|  | ||||
| ### Text Containers | ||||
|  | ||||
| In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional. | ||||
| In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional. | ||||
|  | ||||
| If you don't provide the dimensions of container, we calculate it based of the label dimensions. | ||||
|  | ||||
| @@ -326,7 +326,7 @@ convertToExcalidrawElements([ | ||||
|  | ||||
| ### Arrow bindings | ||||
|  | ||||
| To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional | ||||
| To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional | ||||
|  | ||||
| ```js | ||||
| convertToExcalidrawElements([ | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| <pre> | ||||
|   (api:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616"> | ||||
|     ExcalidrawAPI | ||||
|   </a> | ||||
|   ) => void; | ||||
| @@ -17,7 +17,7 @@ export default function App() { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616). We expose the below APIs :point_down: | ||||
| You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616). We expose the below APIs :point_down: | ||||
|  | ||||
| | API | Signature | Usage | | ||||
| | --- | --- | --- | | ||||
| @@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git | ||||
| | [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool | | ||||
| | [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas | | ||||
| | [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas | | ||||
| | [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off | | ||||
| | [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off | | ||||
| | [onChange](#onChange) | `function` | Subscribes to change events | | ||||
| | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events | | ||||
| | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events | | ||||
| @@ -52,7 +52,7 @@ Additionally `ready` and `readyPromise` from the API have been discontinued. The | ||||
|  | ||||
| <pre> | ||||
|   (scene:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L339"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L339"> | ||||
|     sceneData | ||||
|   </a> | ||||
|   ) => void | ||||
| @@ -62,9 +62,9 @@ You can use this function to update the scene with the sceneData. It accepts the | ||||
|  | ||||
| | Name | Type | Description | | ||||
| | --- | --- | --- | | ||||
| | `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L38) | The `elements` to be updated in the scene | | ||||
| | `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L39) | The `appState` to be updated in the scene. | | ||||
| | `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. | | ||||
| | `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene | | ||||
| | `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. | | ||||
| | `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. | | ||||
| | `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. | | ||||
|  | ||||
| ```jsx live | ||||
| @@ -125,13 +125,13 @@ function App() { | ||||
|  | ||||
| <pre> | ||||
|   (opts: { <br /> libraryItems:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249"> | ||||
|     LibraryItemsSource | ||||
|   </a> | ||||
|   ;<br /> merge?: boolean; <br /> prompt?: boolean; | ||||
|   <br /> openLibraryMenu?: boolean; | ||||
|   <br /> defaultStatus?: "unpublished" | "published"; <br /> }) => Promise< | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L246"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L246"> | ||||
|     LibraryItems | ||||
|   </a> | ||||
|   > | ||||
| @@ -141,7 +141,7 @@ You can use this function to update the library. It accepts the below attributes | ||||
|  | ||||
| | Name | Type | Default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library | | ||||
| | `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library | | ||||
| | `merge` | boolean | `false` | Whether to merge with existing library items. | | ||||
| | `prompt` | boolean | `false` | Whether to prompt user for confirmation. | | ||||
| | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. | | ||||
| @@ -189,7 +189,7 @@ function App() { | ||||
|       </button> | ||||
|       <Excalidraw | ||||
|         ref={(api) => setExcalidrawAPI(api)} | ||||
|         // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js | ||||
|         // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js | ||||
|         initialData={{ | ||||
|           libraryItems: initialData.libraryItems, | ||||
|           appState: { openSidebar: "library" }, | ||||
| @@ -204,7 +204,7 @@ function App() { | ||||
|  | ||||
| <pre> | ||||
|   (files:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59"> | ||||
|     BinaryFileData | ||||
|   </a> | ||||
|   ) => void | ||||
| @@ -224,7 +224,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for | ||||
|  | ||||
| <pre> | ||||
|   () =>{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115"> | ||||
|     ExcalidrawElement[] | ||||
|   </a> | ||||
| </pre> | ||||
| @@ -235,7 +235,7 @@ Returns all the elements including the deleted in the scene. | ||||
|  | ||||
| <pre> | ||||
|   () => NonDeleted< | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115"> | ||||
|     ExcalidrawElement | ||||
|   </a> | ||||
|   []> | ||||
| @@ -247,7 +247,7 @@ Returns all the elements excluding the deleted in the scene | ||||
|  | ||||
| <pre> | ||||
|   () =>{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> | ||||
|     AppState | ||||
|   </a> | ||||
| </pre> | ||||
| @@ -288,7 +288,7 @@ Scroll the nearest element out of the elements supplied to the center of the vie | ||||
|  | ||||
| | Attribute | type | default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | All scene elements | The element(s) to scroll to. | | ||||
| | target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | All scene elements | The element(s) to scroll to. | | ||||
| | opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. | | ||||
| | opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. | | ||||
| | opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) | | ||||
| @@ -336,7 +336,7 @@ The unique id of the excalidraw component. This can be used to identify the exca | ||||
|  | ||||
| <pre> | ||||
|   () =>{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82"> | ||||
|     files | ||||
|   </a> | ||||
| </pre> | ||||
| @@ -364,7 +364,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti | ||||
|  | ||||
| | Name | Type | Default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` | | ||||
| | `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` | | ||||
| | `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface | | ||||
|  | ||||
| ## setCursor | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| # initialData | ||||
|  | ||||
| <pre> | ||||
| { elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> } | ||||
| { elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> } | ||||
| </pre> | ||||
|  | ||||
| This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields. | ||||
|  | ||||
| | Name | Type | Description | | ||||
| | --- | --- | --- | | ||||
| | `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. | | ||||
| | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. | | ||||
| | `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. | | ||||
| | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. | | ||||
| | `scrollToContent` | `boolean` | This attribute indicates whether to `scroll` to the nearest element to center once `Excalidraw` is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained | | ||||
| | `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L247) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which `Excalidraw` should be mounted. | | ||||
| | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82) | The `files` added to the scene. | | ||||
| | `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L247) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200)> | This library items with which `Excalidraw` should be mounted. | | ||||
| | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82) | The `files` added to the scene. | | ||||
|  | ||||
| You might want to use this when you want to load excalidraw with some initial elements and app state. | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,7 @@ All `props` are _optional_. | ||||
| | [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | | ||||
| | [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component | | ||||
| | [`name`](#name) | `string` |  | Name of the drawing | | ||||
| | [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) | | ||||
| | [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) | | ||||
| | [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. | | ||||
| | [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. | | ||||
| | [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load | | ||||
| @@ -33,7 +33,7 @@ All `props` are _optional_. | ||||
|  | ||||
| ### Storing custom data on Excalidraw elements | ||||
|  | ||||
| Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L66) and is optional. | ||||
| Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L66) and is optional. | ||||
|  | ||||
| You can use this to add any extra information you need to keep track of. | ||||
|  | ||||
| @@ -59,11 +59,11 @@ Every time component updates, this callback if passed will get triggered and has | ||||
| (excalidrawElements, appState, files) => void; | ||||
| ``` | ||||
|  | ||||
| 1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene. | ||||
| 1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) in the scene. | ||||
|  | ||||
| 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene. | ||||
| 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene. | ||||
|  | ||||
| 3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene. | ||||
| 3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) which are added to the scene. | ||||
|  | ||||
| Here you can try saving the data to your backend or local storage for example. | ||||
|  | ||||
| @@ -79,14 +79,14 @@ This callback is triggered when mouse pointer is updated. | ||||
|  | ||||
| 2.`button`: The position of the button. This will be one of `["down", "up"]` | ||||
|  | ||||
| 3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) map of the scene | ||||
| 3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L131) map of the scene | ||||
|  | ||||
| ```js | ||||
| (exportedElements, appState, canvas) => void | ||||
| ``` | ||||
|  | ||||
| 1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported. | ||||
| 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene. | ||||
| 1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L87) which needs to be exported. | ||||
| 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene. | ||||
| 3. `canvas`: The `HTMLCanvasElement` of the scene. | ||||
|  | ||||
| ### onPointerDown | ||||
| @@ -96,11 +96,11 @@ This prop if passed will be triggered on pointer down events and has the below s | ||||
|  | ||||
| <pre> | ||||
| (activeTool:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115"> | ||||
|     {" "} | ||||
|     AppState["activeTool"] | ||||
|   </a> | ||||
|   , pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424"> | ||||
|   , pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L424"> | ||||
|     PointerDownState | ||||
|   </a>) => void | ||||
| </pre> | ||||
| @@ -119,7 +119,7 @@ This callback is triggered if passed when something is pasted into the scene. Yo | ||||
|  | ||||
| <pre> | ||||
|   (data:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/clipboard.ts#L18"> | ||||
|     ClipboardData | ||||
|   </a> | ||||
|   , event: ClipboardEvent | null) => boolean | ||||
| @@ -135,7 +135,7 @@ This callback if supplied will get triggered when the library is updated and has | ||||
|  | ||||
| <pre> | ||||
|   (items:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200"> | ||||
|     LibraryItems | ||||
|   </a> | ||||
|   ) => void | Promise<any> | ||||
| @@ -149,7 +149,7 @@ This prop if passed will be triggered when clicked on `link`. To handle the redi | ||||
|  | ||||
| <pre> | ||||
|   (element:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> | ||||
|     ExcalidrawElement | ||||
|   </a> | ||||
|   , event: CustomEvent<{ nativeEvent: MouseEvent }>) => void | ||||
| @@ -182,7 +182,7 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback( | ||||
|  | ||||
| ### langCode | ||||
|  | ||||
| Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below. | ||||
| Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below. | ||||
|  | ||||
| ```js | ||||
| import { defaultLang, languages } from "@excalidraw/excalidraw"; | ||||
| @@ -191,7 +191,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw"; | ||||
| | name | type | | ||||
| | --- | --- | | ||||
| | `defaultLang` | `string` | | ||||
| | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | | ||||
| | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | | ||||
|  | ||||
| ### viewModeEnabled | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|  | ||||
| <pre> | ||||
|   (isMobile: boolean, appState: | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> | ||||
|     AppState | ||||
|   </a>) => JSX | null | ||||
| </pre> | ||||
| @@ -66,7 +66,7 @@ function App() { | ||||
|  | ||||
| <pre> | ||||
|   (element: NonDeleted<ExcalidrawEmbeddableElement>, appState:{" "} | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> | ||||
|     AppState | ||||
|   </a> | ||||
|   ) => JSX.Element | null | ||||
|   | ||||
| @@ -4,7 +4,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom | ||||
|  | ||||
| <pre> | ||||
|   { | ||||
|   <br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L372"> | ||||
|   <br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L372"> | ||||
|     CanvasActions | ||||
|   </a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br /> | ||||
|  | ||||
| @@ -55,7 +55,7 @@ If `UIOptions.canvasActions.export` is `false` the export button will not be ren | ||||
|  | ||||
| ## dockedSidebarBreakpoint | ||||
|  | ||||
| This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L161).   | ||||
| This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L161).   | ||||
| If the _width_ of the _excalidraw_ container exceeds _dockedSidebarBreakpoint_, the sidebar will be `dockable` and the button to `dock` the sidebar will be shown   | ||||
| If user choses to `dock` the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below. | ||||
|  | ||||
| @@ -73,9 +73,9 @@ function App() { | ||||
|  | ||||
| ## tools | ||||
|  | ||||
| This `prop ` controls the visibility of the tools in the editor. | ||||
| This `prop` controls the visibility of the tools in the editor. | ||||
| Currently you can control the visibility of `image` tool via this prop. | ||||
|  | ||||
| | Prop | Type | Default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | image | boolean | true | Decides whether `image` tool should be visible. | ||||
| | image | boolean | true | Decides whether `image` tool should be visible. | ||||
|   | ||||
| @@ -20,16 +20,16 @@ exportToCanvas({<br/>  | ||||
|   getDimensions,<br/>  | ||||
|   files,<br/>  | ||||
|   exportPadding?: number;<br/> | ||||
| }: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> | ||||
| }: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a> | ||||
| </pre> | ||||
|  | ||||
| | Name | Type | Default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) |  | The elements to be exported to canvas. | | ||||
| | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. | | ||||
| | `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) |  | The elements to be exported to canvas. | | ||||
| | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L17) | The app state of the scene. | | ||||
| | [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to  `1`), with which canvas is to be exported. | | ||||
| | `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. | | ||||
| | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. | | ||||
| | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59) | _ | The files added to the scene. | | ||||
| | `exportPadding` | `number` | `10` | The `padding` to be added on canvas. | | ||||
|  | ||||
|  | ||||
| @@ -105,7 +105,7 @@ function App() { | ||||
|  | ||||
| <pre> | ||||
| exportToBlob(<br/>  | ||||
|   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {<br/>  | ||||
|   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L14">ExportOpts</a> & {<br/>  | ||||
|   mimeType?: string,<br/>  | ||||
|   quality?: number,<br/>  | ||||
|   exportPadding?: number;<br/> | ||||
| @@ -134,16 +134,16 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en- | ||||
| <pre> | ||||
| exportToSvg({<br/>  | ||||
|   elements:   | ||||
|     <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> | ||||
|     <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> | ||||
|       ExcalidrawElement[] | ||||
|     </a>,<br/>  | ||||
|   appState: | ||||
|     <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState | ||||
|     <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> AppState | ||||
|     </a>,<br/>  | ||||
|   exportPadding: number,<br/>  | ||||
|   metadata: string,<br/>  | ||||
|   files:  | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59"> | ||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59"> | ||||
|       BinaryFiles | ||||
|     </a>,<br/> | ||||
| }); | ||||
| @@ -151,10 +151,10 @@ exportToSvg({<br/>  | ||||
|  | ||||
| | Name | Type | Default | Description | | ||||
| | --- | --- | --- | --- | | ||||
| | elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) |  | The elements to exported as `svg `| | ||||
| | appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene | | ||||
| | elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) |  | The elements to exported as `svg `| | ||||
| | appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L11) | The `appState` of the scene | | ||||
| | exportPadding | number | 10 | The `padding` to be added on canvas | | ||||
| | files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. | | ||||
| | files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) | undefined | The `files` added to the scene. | | ||||
|  | ||||
| This function returns a promise which resolves to `svg` of the exported drawing. | ||||
|  | ||||
| @@ -164,7 +164,7 @@ This function returns a promise which resolves to `svg` of the exported drawing. | ||||
|  | ||||
| <pre> | ||||
| exportToClipboard(<br/>  | ||||
|   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & {<br/>  | ||||
|   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a> & {<br/>  | ||||
|   mimeType?: string,<br/>  | ||||
|   quality?: number;<br/>  | ||||
|   type: 'png' | 'svg' |'json'<br/> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ id: "restore" | ||||
| **_Signature_** | ||||
|  | ||||
| <pre> | ||||
| restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>  localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | ||||
| restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>  localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | ||||
| </pre> | ||||
|  | ||||
| **_How to use_** | ||||
| @@ -17,7 +17,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob | ||||
| import { restoreAppState } from "@excalidraw/excalidraw"; | ||||
| ``` | ||||
|  | ||||
| This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) and if any key is missing, it will be set to its `default` value. | ||||
| This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) and if any key is missing, it will be set to its `default` value. | ||||
|  | ||||
| When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.   | ||||
| Use this as a way to not override user's defaults if you persist them. | ||||
| @@ -29,16 +29,16 @@ You can pass `null` / `undefined` if not applicable. | ||||
|  | ||||
| <pre> | ||||
| restoreElements( | ||||
|   elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>  | ||||
|   elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>  | ||||
|   opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/> | ||||
| ) | ||||
| </pre> | ||||
|  | ||||
| | Prop | Type | Description | | ||||
| | ---- | ---- | ---- | | ||||
| | `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored | | ||||
| | [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined |  When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. | | ||||
| | `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored | | ||||
| | [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined |  When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. | | ||||
| | [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements | ||||
|  | ||||
| #### localElements | ||||
| @@ -70,15 +70,15 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex | ||||
|  | ||||
| <pre> | ||||
| restore( | ||||
|   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/>  | ||||
|   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><br/> | ||||
|   data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>  | ||||
|   localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L4">DataState</a><br/> | ||||
|   opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/> | ||||
|  | ||||
| ) | ||||
| </pre> | ||||
|  | ||||
| See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`. | ||||
| See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreElements) about `localElements`. | ||||
|  | ||||
| **_How to use_** | ||||
|  | ||||
| @@ -93,7 +93,7 @@ This function makes sure elements and state is set to appropriate values and set | ||||
| **_Signature_** | ||||
|  | ||||
| <pre> | ||||
| restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>  | ||||
| restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>  | ||||
| defaultStatus: "published" | "unpublished") | ||||
| </pre> | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ These are pure Javascript functions exported from the @excalidraw/excalidraw [`@ | ||||
|  | ||||
| ### serializeAsJSON | ||||
|  | ||||
| Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L42) source for details). | ||||
| Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/json.ts#L42) source for details). | ||||
|  | ||||
| If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value. | ||||
|  | ||||
| @@ -16,8 +16,8 @@ If you want to overwrite the `source` field in the `JSON` string, you can set `w | ||||
|  | ||||
| <pre> | ||||
| serializeAsJSON({<br/>  | ||||
|   elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>  | ||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>,<br/> | ||||
|   elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>  | ||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>,<br/> | ||||
| }): string | ||||
| </pre> | ||||
|  | ||||
| @@ -37,7 +37,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo | ||||
|  | ||||
| <pre> | ||||
| serializeLibraryAsJSON( | ||||
|   libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>) | ||||
|   libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems[]</a>) | ||||
| </pre> | ||||
|  | ||||
| **How to use** | ||||
| @@ -53,7 +53,7 @@ Returns `true` if element is invisibly small (e.g. width & height are zero). | ||||
| **_Signature_** | ||||
|  | ||||
| <pre> | ||||
| isInvisiblySmallElement(element:  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>): boolean | ||||
| isInvisiblySmallElement(element:  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement</a>): boolean | ||||
| </pre> | ||||
|  | ||||
| **How to use** | ||||
| @@ -80,10 +80,10 @@ excalidrawAPI.updateScene(scene); | ||||
| <pre> | ||||
| loadFromBlob(<br/>  | ||||
|   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>  | ||||
|   localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  | ||||
|   localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | null,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  | ||||
|   fileHandle?: FileSystemHandle | null <br/> | ||||
| ) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L61">RestoredDataState</a>> | ||||
| ) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L61">RestoredDataState</a>> | ||||
| </pre> | ||||
|  | ||||
| ### loadLibraryFromBlob | ||||
| @@ -130,10 +130,10 @@ if (contents.type === MIME_TYPES.excalidraw) { | ||||
| <pre> | ||||
| loadSceneOrLibraryFromBlob(<br/>  | ||||
|   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>  | ||||
|   localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  | ||||
|   localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | null,<br/>  | ||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  | ||||
|   fileHandle?: FileSystemHandle | null<br/> | ||||
| ) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}> | ||||
| ) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L33">ImportedLibraryState</a>}> | ||||
| </pre> | ||||
|  | ||||
| ### getFreeDrawSvgPath | ||||
| @@ -149,7 +149,7 @@ import { getFreeDrawSvgPath } from "@excalidraw/excalidraw"; | ||||
| **Signature** | ||||
|  | ||||
| <pre> | ||||
| getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L182">ExcalidrawFreeDrawElement</a>) | ||||
| getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L182">ExcalidrawFreeDrawElement</a>) | ||||
| </pre> | ||||
|  | ||||
| ### isLinearElement | ||||
| @@ -165,7 +165,7 @@ import { isLinearElement } from "@excalidraw/excalidraw"; | ||||
| **Signature** | ||||
|  | ||||
| <pre> | ||||
| isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean | ||||
| isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L80">ExcalidrawElement</a>): boolean | ||||
| </pre> | ||||
|  | ||||
| ### getNonDeletedElements | ||||
| @@ -181,7 +181,7 @@ import { getNonDeletedElements } from "@excalidraw/excalidraw"; | ||||
| **Signature** | ||||
|  | ||||
| <pre> | ||||
| getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L125">NonDeletedExcalidrawElement[]</a> | ||||
| getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L125">NonDeletedExcalidrawElement[]</a> | ||||
| </pre> | ||||
|  | ||||
| ### mergeLibraryItems | ||||
| @@ -196,9 +196,9 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw"; | ||||
|  | ||||
| <pre> | ||||
| mergeLibraryItems(<br/>  | ||||
|   localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>  | ||||
|   otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a><br/> | ||||
| ): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a> | ||||
|   localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a>,<br/>  | ||||
|   otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems</a><br/> | ||||
| ): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a> | ||||
| </pre> | ||||
|  | ||||
| ### parseLibraryTokensFromUrl | ||||
| @@ -239,8 +239,8 @@ export const App = () => { | ||||
|  | ||||
| <pre> | ||||
| useHandleLibrary(opts: {<br/>  | ||||
|   excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L494">ExcalidrawAPI</a>,<br/>  | ||||
|   getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L253">LibraryItemsSource</a><br/> | ||||
|   excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L494">ExcalidrawAPI</a>,<br/>  | ||||
|   getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L253">LibraryItemsSource</a><br/> | ||||
| }); | ||||
| </pre> | ||||
|  | ||||
| @@ -253,7 +253,7 @@ This function returns the current `scene` version. | ||||
| **_Signature_** | ||||
|  | ||||
| <pre> | ||||
| getSceneVersion(elements:  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>) | ||||
| getSceneVersion(elements:  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>) | ||||
| </pre> | ||||
|  | ||||
| **How to use** | ||||
| @@ -274,7 +274,7 @@ import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw"; | ||||
|  | ||||
| <pre> | ||||
| sceneCoordsToViewportCoords({ sceneX: number, sceneY: number },<br/>  | ||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): { x: number, y: number } | ||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): { x: number, y: number } | ||||
| </pre> | ||||
|  | ||||
| ### viewportCoordsToSceneCoords | ||||
| @@ -289,7 +289,7 @@ import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw"; | ||||
|  | ||||
| <pre> | ||||
| viewportCoordsToSceneCoords({ clientX: number, clientY: number },<br/>  | ||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): {x: number, y: number} | ||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): {x: number, y: number} | ||||
| </pre> | ||||
|  | ||||
| ### useDevice | ||||
| @@ -350,8 +350,8 @@ To help with localization, we export the following. | ||||
| | name | type | | ||||
| | --- | --- | | ||||
| | `defaultLang` | `string` | | ||||
| | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | | ||||
| | `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | | ||||
| | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | | ||||
| | `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | | ||||
|  | ||||
| ```js | ||||
| import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw"; | ||||
|   | ||||
| @@ -21,7 +21,7 @@ Most notably, you can customize the primary colors, by overriding these variable | ||||
| - `--color-primary-light` | ||||
| - `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present. | ||||
|  | ||||
| For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override. | ||||
| For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/css/theme.scss), though most of them will not make sense to override. | ||||
|  | ||||
| ```css showLineNumbers | ||||
| .custom-styles .excalidraw { | ||||
|   | ||||
| @@ -13,7 +13,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the | ||||
| 1. Install the dependencies | ||||
|  | ||||
|    ```bash | ||||
|    cd src/packages/excalidraw && yarn | ||||
|    cd packages/excalidraw && yarn | ||||
|    ``` | ||||
|  | ||||
| 2. Start the example app | ||||
|   | ||||
| @@ -39,7 +39,7 @@ Since Vite removes env variables by default, you can update the vite config to e | ||||
|  | ||||
| ``` | ||||
|  define: { | ||||
|     "process.env.IS_PREACT": process.env.IS_PREACT, | ||||
|     "process.env.IS_PREACT": JSON.stringify("true"), | ||||
|   }, | ||||
| ``` | ||||
|  | ||||
|   | ||||
| @@ -32,15 +32,9 @@ function App() { | ||||
|  | ||||
| ### Next.js | ||||
|  | ||||
| Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. | ||||
| Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`. | ||||
|  | ||||
| Here are two ways on how you can render **Excalidraw** on **Next.js**. | ||||
|  | ||||
|  | ||||
|  | ||||
| 1. Using **Next.js Dynamic** import [Recommended]. | ||||
|  | ||||
| Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`. | ||||
| If you want to only import `Excalidraw` component you can do :point_down: | ||||
|  | ||||
| ```jsx showLineNumbers | ||||
| import dynamic from "next/dynamic"; | ||||
| @@ -55,25 +49,88 @@ export default function App() { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2). | ||||
| However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically. | ||||
|  | ||||
| If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down: | ||||
|  | ||||
| 2. Importing Excalidraw once **client** is rendered. | ||||
| <Tabs> | ||||
|   <TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" > | ||||
|  | ||||
| ```jsx showLineNumbers | ||||
| import { useState, useEffect } from "react"; | ||||
| export default function App() { | ||||
|   const [Excalidraw, setExcalidraw] = useState(null); | ||||
|   useEffect(() => { | ||||
|     import("@excalidraw/excalidraw").then((comp) => | ||||
|       setExcalidraw(comp.Excalidraw), | ||||
|   ```jsx showLineNumbers | ||||
|   "use client"; | ||||
|   import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw"; | ||||
|  | ||||
|   import "@excalidraw/excalidraw/index.css"; | ||||
|  | ||||
|   const ExcalidrawWrapper: React.FC = () => { | ||||
|     console.info(convertToExcalidrawElements([{ | ||||
|       type: "rectangle", | ||||
|       id: "rect-1", | ||||
|       width: 186.47265625, | ||||
|       height: 141.9765625, | ||||
|     },])); | ||||
|     return ( | ||||
|       <div style={{height:"500px", width:"500px"}}   | ||||
|         <Excalidraw /> | ||||
|       </div>  | ||||
|     ); | ||||
|   }, []); | ||||
|   return <>{Excalidraw && <Excalidraw />}</>; | ||||
| } | ||||
| ``` | ||||
|   }; | ||||
|   export default ExcalidrawWrapper; | ||||
|   ``` | ||||
|  | ||||
|   </TabItem> | ||||
|  | ||||
|   <TabItem value="pages" label="Pages router"> | ||||
|  | ||||
|   ```jsx showLineNumbers | ||||
|   import dynamic from "next/dynamic"; | ||||
|    | ||||
|   // Since client components get prerenderd on server as well hence importing  | ||||
|   // the excalidraw stuff dynamically with ssr false | ||||
|  | ||||
|   const ExcalidrawWrapper = dynamic( | ||||
|     async () => (await import("../excalidrawWrapper")).default, | ||||
|     { | ||||
|       ssr: false, | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   export default function Page() { | ||||
|     return ( | ||||
|       <ExcalidrawWrapper />       | ||||
|     ); | ||||
|   } | ||||
|   ``` | ||||
|   </TabItem> | ||||
|  | ||||
|   <TabItem value="app" label="App router"> | ||||
|  | ||||
|   ```jsx showLineNumbers | ||||
|   import dynamic from "next/dynamic"; | ||||
|  | ||||
|   // Since client components get prerenderd on server as well hence importing  | ||||
|   // the excalidraw stuff dynamically with ssr false | ||||
|  | ||||
|   const ExcalidrawWrapper = dynamic( | ||||
|     async () => (await import("../excalidrawWrapper")).default, | ||||
|     { | ||||
|       ssr: false, | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   export default function Page() { | ||||
|     return ( | ||||
|       <ExcalidrawWrapper /> | ||||
|     ); | ||||
|   } | ||||
|   ``` | ||||
|  | ||||
|   </TabItem> | ||||
| </Tabs> | ||||
|  | ||||
|  | ||||
| Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/). | ||||
|  | ||||
| Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d) | ||||
|  | ||||
| The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm) | ||||
|  | ||||
| @@ -93,7 +150,7 @@ Since Vite removes env variables by default, you can update the vite config to e | ||||
|  | ||||
| ``` | ||||
|  define: { | ||||
|     "process.env.IS_PREACT": process.env.IS_PREACT, | ||||
|     "process.env.IS_PREACT": JSON.stringify("true"), | ||||
|   }, | ||||
| ``` | ||||
| :::  | ||||
| @@ -148,7 +205,7 @@ import TabItem from "@theme/TabItem"; | ||||
|       <h1>Excalidraw Embed Example</h1> | ||||
|       <div id="app"></div> | ||||
|     </div> | ||||
|     <script type="text/javascript" src="src/index.js"></script> | ||||
|     <script type="text/javascript" src="packages/excalidraw/index.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
| ``` | ||||
|   | ||||
| @@ -38,9 +38,9 @@ Add the diagram type in switch case in [`parseMermaid`](https://github.com/excal | ||||
|  | ||||
| ## Writing the Excalidraw Skeleton Convertor | ||||
|  | ||||
| With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) format. | ||||
| With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) format. | ||||
|  | ||||
| Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). | ||||
| Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133). | ||||
|  | ||||
| Thats it, you have added the new diagram type 🥳, now lets test it out! | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ In this section we will be diving into how the [flowchart parser](https://github | ||||
|  | ||||
|  | ||||
|  | ||||
| We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38). | ||||
| We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38). | ||||
|  | ||||
|  | ||||
| For computing `vertices` and `edge`s lets consider the below svg generated by mermaid | ||||
| @@ -42,7 +42,7 @@ Considering the same example this is the response from the API | ||||
| 	} | ||||
| } | ||||
| ``` | ||||
| The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response. | ||||
| The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response. | ||||
|  | ||||
|  The final output from `parseVertex` looks like :point_down: | ||||
|  | ||||
|   | ||||
| @@ -55,11 +55,11 @@ If you want to understand how flowchart parser works, you can navigate to [Flowc | ||||
|  | ||||
| ## Converting to ExcalidrawElementSkeleton | ||||
|  | ||||
| Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw. | ||||
| Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw. | ||||
|  | ||||
| For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton. | ||||
| For Unsupported types, we have already mentioned above that we convert it to `dataURL` and return the ExcalidrawImageSkeleton. | ||||
|  | ||||
| For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38). | ||||
| For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38). | ||||
|  | ||||
|  | ||||
| @@ -52,15 +52,6 @@ Make sure the title starts with a semantic prefix: | ||||
| - **chore**: Other changes that don't modify src or test files | ||||
| - **revert**: Reverts a previous commit | ||||
|  | ||||
| ### Changelog | ||||
|  | ||||
| Add a brief description of your pull request to the changelog located here: [changelog](https://github.com/excalidraw/excalidraw/blob/master/CHANGELOG.md) | ||||
|  | ||||
| Notes: | ||||
|  | ||||
| - Make sure to prepend to the section corresponding with the semantic prefix you selected in the title | ||||
| - Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request | ||||
|  | ||||
| ### Testing | ||||
|  | ||||
| Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise. | ||||
|   | ||||
| @@ -41,10 +41,7 @@ const config = { | ||||
|           showLastUpdateTime: true, | ||||
|         }, | ||||
|         theme: { | ||||
|           customCss: [ | ||||
|             require.resolve("./src/css/custom.scss"), | ||||
|             require.resolve("../src/packages/excalidraw/example/App.scss"), | ||||
|           ], | ||||
|           customCss: [require.resolve("./src/css/custom.scss")], | ||||
|         }, | ||||
|       }), | ||||
|     ], | ||||
|   | ||||
							
								
								
									
										4
									
								
								dev-docs/vercel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "outputDirectory": "build", | ||||
|   "installCommand": "yarn install" | ||||
| } | ||||
| @@ -15,14 +15,23 @@ | ||||
|       border-radius: 50%; | ||||
|     } | ||||
|   } | ||||
|   .app-title { | ||||
|     margin-block-start: 0.83em; | ||||
|     margin-block-end: 0.83em; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .button-wrapper button { | ||||
|   z-index: 1; | ||||
|   height: 40px; | ||||
|   max-width: 200px; | ||||
|   margin: 10px; | ||||
|   padding: 5px; | ||||
| .button-wrapper { | ||||
|   input[type="checkbox"] { | ||||
|     margin: 5px; | ||||
|   } | ||||
|   button { | ||||
|     z-index: 1; | ||||
|     height: 40px; | ||||
|     max-width: 200px; | ||||
|     margin: 10px; | ||||
|     padding: 5px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .excalidraw .App-menu_top .buttonList { | ||||
| @@ -1,23 +1,31 @@ | ||||
| import { useEffect, useState, useRef, useCallback } from "react"; | ||||
| 
 | ||||
| import React, { | ||||
|   useEffect, | ||||
|   useState, | ||||
|   useRef, | ||||
|   useCallback, | ||||
|   Children, | ||||
|   cloneElement, | ||||
| } from "react"; | ||||
| import ExampleSidebar from "./sidebar/ExampleSidebar"; | ||||
| 
 | ||||
| import type * as TExcalidraw from "../index"; | ||||
| import type * as TExcalidraw from "@excalidraw/excalidraw"; | ||||
| 
 | ||||
| import "./App.scss"; | ||||
| import initialData from "./initialData"; | ||||
| import { nanoid } from "nanoid"; | ||||
| 
 | ||||
| import { | ||||
|   resolvablePromise, | ||||
|   ResolvablePromise, | ||||
|   distance2d, | ||||
|   fileOpen, | ||||
|   withBatchedUpdates, | ||||
|   withBatchedUpdatesThrottled, | ||||
| } from "../../../utils"; | ||||
| import { EVENT, ROUNDNESS } from "../../../constants"; | ||||
| import { distance2d } from "../../../math"; | ||||
| import { fileOpen } from "../../../data/filesystem"; | ||||
| import { loadSceneOrLibraryFromBlob } from "../../utils"; | ||||
| import { | ||||
| } from "../utils"; | ||||
| 
 | ||||
| import CustomFooter from "./CustomFooter"; | ||||
| import MobileFooter from "./MobileFooter"; | ||||
| import initialData from "../initialData"; | ||||
| 
 | ||||
| import type { | ||||
|   AppState, | ||||
|   BinaryFileData, | ||||
|   ExcalidrawImperativeAPI, | ||||
| @@ -25,18 +33,14 @@ import { | ||||
|   Gesture, | ||||
|   LibraryItems, | ||||
|   PointerDownState as ExcalidrawPointerDownState, | ||||
| } from "../../../types"; | ||||
| import { NonDeletedExcalidrawElement, Theme } from "../../../element/types"; | ||||
| import { ImportedLibraryData } from "../../../data/types"; | ||||
| import CustomFooter from "./CustomFooter"; | ||||
| import MobileFooter from "./MobileFooter"; | ||||
| import { KEYS } from "../../../keys"; | ||||
| } from "@excalidraw/excalidraw/dist/excalidraw/types"; | ||||
| import type { | ||||
|   NonDeletedExcalidrawElement, | ||||
|   Theme, | ||||
| } from "@excalidraw/excalidraw/dist/excalidraw/element/types"; | ||||
| import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types"; | ||||
| 
 | ||||
| declare global { | ||||
|   interface Window { | ||||
|     ExcalidrawLib: typeof TExcalidraw; | ||||
|   } | ||||
| } | ||||
| import "./App.scss"; | ||||
| 
 | ||||
| type Comment = { | ||||
|   x: number; | ||||
| @@ -57,27 +61,6 @@ type PointerDownState = { | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| // This is so that we use the bundled excalidraw.development.js file instead
 | ||||
| // of the actual source code
 | ||||
| const { | ||||
|   exportToCanvas, | ||||
|   exportToSvg, | ||||
|   exportToBlob, | ||||
|   exportToClipboard, | ||||
|   Excalidraw, | ||||
|   useHandleLibrary, | ||||
|   MIME_TYPES, | ||||
|   sceneCoordsToViewportCoords, | ||||
|   viewportCoordsToSceneCoords, | ||||
|   restoreElements, | ||||
|   Sidebar, | ||||
|   Footer, | ||||
|   WelcomeScreen, | ||||
|   MainMenu, | ||||
|   LiveCollaborationTrigger, | ||||
|   convertToExcalidrawElements, | ||||
| } = window.ExcalidrawLib; | ||||
| 
 | ||||
| const COMMENT_ICON_DIMENSION = 32; | ||||
| const COMMENT_INPUT_HEIGHT = 50; | ||||
| const COMMENT_INPUT_WIDTH = 150; | ||||
| @@ -86,9 +69,38 @@ export interface AppProps { | ||||
|   appTitle: string; | ||||
|   useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void; | ||||
|   customArgs?: any[]; | ||||
|   children: React.ReactNode; | ||||
|   excalidrawLib: typeof TExcalidraw; | ||||
| } | ||||
| 
 | ||||
| export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
| export default function App({ | ||||
|   appTitle, | ||||
|   useCustom, | ||||
|   customArgs, | ||||
|   children, | ||||
|   excalidrawLib, | ||||
| }: AppProps) { | ||||
|   const { | ||||
|     exportToCanvas, | ||||
|     exportToSvg, | ||||
|     exportToBlob, | ||||
|     exportToClipboard, | ||||
|     useHandleLibrary, | ||||
|     MIME_TYPES, | ||||
|     sceneCoordsToViewportCoords, | ||||
|     viewportCoordsToSceneCoords, | ||||
|     restoreElements, | ||||
|     Sidebar, | ||||
|     Footer, | ||||
|     WelcomeScreen, | ||||
|     MainMenu, | ||||
|     LiveCollaborationTrigger, | ||||
|     convertToExcalidrawElements, | ||||
|     TTDDialog, | ||||
|     TTDDialogTrigger, | ||||
|     ROUNDNESS, | ||||
|     loadSceneOrLibraryFromBlob, | ||||
|   } = excalidrawLib; | ||||
|   const appRef = useRef<any>(null); | ||||
|   const [viewModeEnabled, setViewModeEnabled] = useState(false); | ||||
|   const [zenModeEnabled, setZenModeEnabled] = useState(false); | ||||
| @@ -150,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|       }; | ||||
|     }; | ||||
|     fetchData(); | ||||
|   }, [excalidrawAPI]); | ||||
|   }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]); | ||||
| 
 | ||||
|   const renderExcalidraw = (children: React.ReactNode) => { | ||||
|     const Excalidraw: any = Children.toArray(children).find( | ||||
|       (child) => | ||||
|         React.isValidElement(child) && | ||||
|         typeof child.type !== "string" && | ||||
|         //@ts-ignore
 | ||||
|         child.type.displayName === "Excalidraw", | ||||
|     ); | ||||
|     if (!Excalidraw) { | ||||
|       return; | ||||
|     } | ||||
|     const newElement = cloneElement( | ||||
|       Excalidraw, | ||||
|       { | ||||
|         excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api), | ||||
|         initialData: initialStatePromiseRef.current.promise, | ||||
|         onChange: ( | ||||
|           elements: NonDeletedExcalidrawElement[], | ||||
|           state: AppState, | ||||
|         ) => { | ||||
|           console.info("Elements :", elements, "State : ", state); | ||||
|         }, | ||||
|         onPointerUpdate: (payload: { | ||||
|           pointer: { x: number; y: number }; | ||||
|           button: "down" | "up"; | ||||
|           pointersMap: Gesture["pointers"]; | ||||
|         }) => setPointerData(payload), | ||||
|         viewModeEnabled, | ||||
|         zenModeEnabled, | ||||
|         gridModeEnabled, | ||||
|         theme, | ||||
|         name: "Custom name of drawing", | ||||
|         UIOptions: { | ||||
|           canvasActions: { | ||||
|             loadScene: false, | ||||
|           }, | ||||
|           tools: { image: !disableImageTool }, | ||||
|         }, | ||||
|         renderTopRightUI, | ||||
|         onLinkOpen, | ||||
|         onPointerDown, | ||||
|         onScrollChange: rerenderCommentIcons, | ||||
|         validateEmbeddable: true, | ||||
|       }, | ||||
|       <> | ||||
|         {excalidrawAPI && ( | ||||
|           <Footer> | ||||
|             <CustomFooter | ||||
|               excalidrawAPI={excalidrawAPI} | ||||
|               excalidrawLib={excalidrawLib} | ||||
|             /> | ||||
|           </Footer> | ||||
|         )} | ||||
|         <WelcomeScreen /> | ||||
|         <Sidebar name="custom"> | ||||
|           <Sidebar.Tabs> | ||||
|             <Sidebar.Header /> | ||||
|             <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab> | ||||
|             <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab> | ||||
|             <Sidebar.TabTriggers> | ||||
|               <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger> | ||||
|               <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger> | ||||
|             </Sidebar.TabTriggers> | ||||
|           </Sidebar.Tabs> | ||||
|         </Sidebar> | ||||
|         <Sidebar.Trigger | ||||
|           name="custom" | ||||
|           tab="one" | ||||
|           style={{ | ||||
|             position: "absolute", | ||||
|             left: "50%", | ||||
|             transform: "translateX(-50%)", | ||||
|             bottom: "20px", | ||||
|             zIndex: 9999999999999999, | ||||
|           }} | ||||
|         > | ||||
|           Toggle Custom Sidebar | ||||
|         </Sidebar.Trigger> | ||||
|         {renderMenu()} | ||||
|         {excalidrawAPI && ( | ||||
|           <TTDDialogTrigger icon={<span>😀</span>}> | ||||
|             Text to diagram | ||||
|           </TTDDialogTrigger> | ||||
|         )} | ||||
|         <TTDDialog | ||||
|           onTextSubmit={async (_) => { | ||||
|             console.info("submit"); | ||||
|             // sleep for 2s
 | ||||
|             await new Promise((resolve) => setTimeout(resolve, 2000)); | ||||
|             throw new Error("error, go away now"); | ||||
|             // return "dummy";
 | ||||
|           }} | ||||
|         /> | ||||
|       </>, | ||||
|     ); | ||||
|     return newElement; | ||||
|   }; | ||||
|   const renderTopRightUI = (isMobile: boolean) => { | ||||
|     return ( | ||||
|       <> | ||||
| @@ -335,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|     pointerDownState: PointerDownState, | ||||
|   ) => { | ||||
|     return withBatchedUpdates((event) => { | ||||
|       window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove); | ||||
|       window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp); | ||||
|       window.removeEventListener("pointermove", pointerDownState.onMove); | ||||
|       window.removeEventListener("pointerup", pointerDownState.onUp); | ||||
|       excalidrawAPI?.setActiveTool({ type: "selection" }); | ||||
|       const distance = distance2d( | ||||
|         pointerDownState.x, | ||||
| @@ -400,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|               onPointerMoveFromPointerDownHandler(pointerDownState); | ||||
|             const onPointerUp = | ||||
|               onPointerUpFromPointerDownHandler(pointerDownState); | ||||
|             window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); | ||||
|             window.addEventListener(EVENT.POINTER_UP, onPointerUp); | ||||
|             window.addEventListener("pointermove", onPointerMove); | ||||
|             window.addEventListener("pointerup", onPointerUp); | ||||
| 
 | ||||
|             pointerDownState.onMove = onPointerMove; | ||||
|             pointerDownState.onUp = onPointerUp; | ||||
| @@ -493,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|         }} | ||||
|         onBlur={saveComment} | ||||
|         onKeyDown={(event) => { | ||||
|           if (!event.shiftKey && event.key === KEYS.ENTER) { | ||||
|           if (!event.shiftKey && event.key === "Enter") { | ||||
|             event.preventDefault(); | ||||
|             saveComment(); | ||||
|           } | ||||
| @@ -526,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|         </MainMenu.ItemCustom> | ||||
|         <MainMenu.DefaultItems.Help /> | ||||
| 
 | ||||
|         {excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />} | ||||
|         {excalidrawAPI && ( | ||||
|           <MobileFooter | ||||
|             excalidrawLib={excalidrawLib} | ||||
|             excalidrawAPI={excalidrawAPI} | ||||
|           /> | ||||
|         )} | ||||
|       </MainMenu> | ||||
|     ); | ||||
|   }; | ||||
| @@ -675,69 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="excalidraw-wrapper"> | ||||
|           <Excalidraw | ||||
|             excalidrawAPI={(api: ExcalidrawImperativeAPI) => | ||||
|               setExcalidrawAPI(api) | ||||
|             } | ||||
|             initialData={initialStatePromiseRef.current.promise} | ||||
|             onChange={(elements, state) => { | ||||
|               console.info("Elements :", elements, "State : ", state); | ||||
|             }} | ||||
|             onPointerUpdate={(payload: { | ||||
|               pointer: { x: number; y: number }; | ||||
|               button: "down" | "up"; | ||||
|               pointersMap: Gesture["pointers"]; | ||||
|             }) => setPointerData(payload)} | ||||
|             viewModeEnabled={viewModeEnabled} | ||||
|             zenModeEnabled={zenModeEnabled} | ||||
|             gridModeEnabled={gridModeEnabled} | ||||
|             theme={theme} | ||||
|             name="Custom name of drawing" | ||||
|             UIOptions={{ | ||||
|               canvasActions: { | ||||
|                 loadScene: false, | ||||
|               }, | ||||
|               tools: { image: !disableImageTool }, | ||||
|             }} | ||||
|             renderTopRightUI={renderTopRightUI} | ||||
|             onLinkOpen={onLinkOpen} | ||||
|             onPointerDown={onPointerDown} | ||||
|             onScrollChange={rerenderCommentIcons} | ||||
|             // allow all urls
 | ||||
|             validateEmbeddable={true} | ||||
|           > | ||||
|             {excalidrawAPI && ( | ||||
|               <Footer> | ||||
|                 <CustomFooter excalidrawAPI={excalidrawAPI} /> | ||||
|               </Footer> | ||||
|             )} | ||||
|             <WelcomeScreen /> | ||||
|             <Sidebar name="custom"> | ||||
|               <Sidebar.Tabs> | ||||
|                 <Sidebar.Header /> | ||||
|                 <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab> | ||||
|                 <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab> | ||||
|                 <Sidebar.TabTriggers> | ||||
|                   <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger> | ||||
|                   <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger> | ||||
|                 </Sidebar.TabTriggers> | ||||
|               </Sidebar.Tabs> | ||||
|             </Sidebar> | ||||
|             <Sidebar.Trigger | ||||
|               name="custom" | ||||
|               tab="one" | ||||
|               style={{ | ||||
|                 position: "absolute", | ||||
|                 left: "50%", | ||||
|                 transform: "translateX(-50%)", | ||||
|                 bottom: "20px", | ||||
|                 zIndex: 9999999999999999, | ||||
|               }} | ||||
|             > | ||||
|               Toggle Custom Sidebar | ||||
|             </Sidebar.Trigger> | ||||
|             {renderMenu()} | ||||
|           </Excalidraw> | ||||
|           {renderExcalidraw(children)} | ||||
|           {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} | ||||
|           {comment && renderComment()} | ||||
|         </div> | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { ExcalidrawImperativeAPI } from "../../../types"; | ||||
| import { MIME_TYPES } from "../entry"; | ||||
| import { Button } from "../../../components/Button"; | ||||
| import type * as TExcalidraw from "@excalidraw/excalidraw"; | ||||
| import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; | ||||
| 
 | ||||
| const COMMENT_SVG = ( | ||||
|   <svg | ||||
| @@ -18,24 +17,28 @@ const COMMENT_SVG = ( | ||||
|     <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path> | ||||
|   </svg> | ||||
| ); | ||||
| 
 | ||||
| const CustomFooter = ({ | ||||
|   excalidrawAPI, | ||||
|   excalidrawLib, | ||||
| }: { | ||||
|   excalidrawAPI: ExcalidrawImperativeAPI; | ||||
|   excalidrawLib: typeof TExcalidraw; | ||||
| }) => { | ||||
|   const { Button, MIME_TYPES } = excalidrawLib; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Button | ||||
|         onSelect={() => alert("General Kenobi!")} | ||||
|         className="you are a bold one" | ||||
|         style={{ marginLeft: "1rem" }} | ||||
|         style={{ marginLeft: "1rem", width: "auto" }} | ||||
|         title="Hello there!" | ||||
|       > | ||||
|         {COMMENT_SVG} | ||||
|         Hit me | ||||
|       </Button> | ||||
|       <button | ||||
|       <Button | ||||
|         className="custom-element" | ||||
|         onClick={() => { | ||||
|         onSelect={() => { | ||||
|           excalidrawAPI?.setActiveTool({ | ||||
|             type: "custom", | ||||
|             customType: "comment", | ||||
| @@ -58,15 +61,10 @@ const CustomFooter = ({ | ||||
|           )}`;
 | ||||
|           excalidrawAPI?.setCursor(`url(${url}), auto`); | ||||
|         }} | ||||
|         title="Comments!" | ||||
|       > | ||||
|         {COMMENT_SVG} | ||||
|       </button> | ||||
|       <button | ||||
|         className="custom-footer" | ||||
|         onClick={() => alert("This is dummy footer")} | ||||
|       > | ||||
|         custom footer | ||||
|       </button> | ||||
|       </Button> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										27
									
								
								examples/excalidraw/components/MobileFooter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; | ||||
| import CustomFooter from "./CustomFooter"; | ||||
| import type * as TExcalidraw from "@excalidraw/excalidraw"; | ||||
|  | ||||
| const MobileFooter = ({ | ||||
|   excalidrawAPI, | ||||
|   excalidrawLib, | ||||
| }: { | ||||
|   excalidrawAPI: ExcalidrawImperativeAPI; | ||||
|   excalidrawLib: typeof TExcalidraw; | ||||
| }) => { | ||||
|   const { useDevice, Footer } = excalidrawLib; | ||||
|  | ||||
|   const device = useDevice(); | ||||
|   if (device.editor.isMobile) { | ||||
|     return ( | ||||
|       <Footer> | ||||
|         <CustomFooter | ||||
|           excalidrawAPI={excalidrawAPI} | ||||
|           excalidrawLib={excalidrawLib} | ||||
|         /> | ||||
|       </Footer> | ||||
|     ); | ||||
|   } | ||||
|   return null; | ||||
| }; | ||||
| export default MobileFooter; | ||||
| @@ -1,5 +1,6 @@ | ||||
| import React, { useState } from "react"; | ||||
| import { useState } from "react"; | ||||
| import "./ExampleSidebar.scss"; | ||||
| 
 | ||||
| export default function Sidebar({ children }: { children: React.ReactNode }) { | ||||
|   const [open, setOpen] = useState(false); | ||||
| 
 | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { ExcalidrawElementSkeleton } from "../../../data/transform"; | ||||
| import { FileId } from "../../../element/types"; | ||||
| import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform"; | ||||
| import type { FileId } from "@excalidraw/excalidraw/element/types"; | ||||
| 
 | ||||
| const elements: ExcalidrawElementSkeleton[] = [ | ||||
|   { | ||||
							
								
								
									
										13
									
								
								examples/excalidraw/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   "name": "examples", | ||||
|   "version": "1.0.0", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
|     "@excalidraw/excalidraw": "*" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "typescript": "^5" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										3
									
								
								examples/excalidraw/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "extends": "../../tsconfig" | ||||
| } | ||||
							
								
								
									
										146
									
								
								examples/excalidraw/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,146 @@ | ||||
| import { unstable_batchedUpdates } from "react-dom"; | ||||
| import { fileOpen as _fileOpen } from "browser-fs-access"; | ||||
| import type { MIME_TYPES } from "@excalidraw/excalidraw"; | ||||
| import { AbortError } from "../../packages/excalidraw/errors"; | ||||
|  | ||||
| type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; | ||||
|  | ||||
| const INPUT_CHANGE_INTERVAL_MS = 500; | ||||
|  | ||||
| export type ResolvablePromise<T> = Promise<T> & { | ||||
|   resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; | ||||
|   reject: (error: Error) => void; | ||||
| }; | ||||
| export const resolvablePromise = <T>() => { | ||||
|   let resolve!: any; | ||||
|   let reject!: any; | ||||
|   const promise = new Promise((_resolve, _reject) => { | ||||
|     resolve = _resolve; | ||||
|     reject = _reject; | ||||
|   }); | ||||
|   (promise as any).resolve = resolve; | ||||
|   (promise as any).reject = reject; | ||||
|   return promise as ResolvablePromise<T>; | ||||
| }; | ||||
|  | ||||
| export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { | ||||
|   const xd = x2 - x1; | ||||
|   const yd = y2 - y1; | ||||
|   return Math.hypot(xd, yd); | ||||
| }; | ||||
|  | ||||
| export const fileOpen = <M extends boolean | undefined = false>(opts: { | ||||
|   extensions?: FILE_EXTENSION[]; | ||||
|   description: string; | ||||
|   multiple?: M; | ||||
| }): Promise<M extends false | undefined ? File : File[]> => { | ||||
|   // an unsafe TS hack, alas not much we can do AFAIK | ||||
|   type RetType = M extends false | undefined ? File : File[]; | ||||
|  | ||||
|   const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { | ||||
|     mimeTypes.push(MIME_TYPES[type]); | ||||
|  | ||||
|     return mimeTypes; | ||||
|   }, [] as string[]); | ||||
|  | ||||
|   const extensions = opts.extensions?.reduce((acc, ext) => { | ||||
|     if (ext === "jpg") { | ||||
|       return acc.concat(".jpg", ".jpeg"); | ||||
|     } | ||||
|     return acc.concat(`.${ext}`); | ||||
|   }, [] as string[]); | ||||
|  | ||||
|   return _fileOpen({ | ||||
|     description: opts.description, | ||||
|     extensions, | ||||
|     mimeTypes, | ||||
|     multiple: opts.multiple ?? false, | ||||
|     legacySetup: (resolve, reject, input) => { | ||||
|       const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); | ||||
|       const focusHandler = () => { | ||||
|         checkForFile(); | ||||
|         document.addEventListener("keyup", scheduleRejection); | ||||
|         document.addEventListener("pointerup", scheduleRejection); | ||||
|         scheduleRejection(); | ||||
|       }; | ||||
|       const checkForFile = () => { | ||||
|         // this hack might not work when expecting multiple files | ||||
|         if (input.files?.length) { | ||||
|           const ret = opts.multiple ? [...input.files] : input.files[0]; | ||||
|           resolve(ret as RetType); | ||||
|         } | ||||
|       }; | ||||
|       requestAnimationFrame(() => { | ||||
|         window.addEventListener("focus", focusHandler); | ||||
|       }); | ||||
|       const interval = window.setInterval(() => { | ||||
|         checkForFile(); | ||||
|       }, INPUT_CHANGE_INTERVAL_MS); | ||||
|       return (rejectPromise) => { | ||||
|         clearInterval(interval); | ||||
|         scheduleRejection.cancel(); | ||||
|         window.removeEventListener("focus", focusHandler); | ||||
|         document.removeEventListener("keyup", scheduleRejection); | ||||
|         document.removeEventListener("pointerup", scheduleRejection); | ||||
|         if (rejectPromise) { | ||||
|           // so that something is shown in console if we need to debug this | ||||
|           console.warn("Opening the file was canceled (legacy-fs)."); | ||||
|           rejectPromise(new AbortError()); | ||||
|         } | ||||
|       }; | ||||
|     }, | ||||
|   }) as Promise<RetType>; | ||||
| }; | ||||
|  | ||||
| export const debounce = <T extends any[]>( | ||||
|   fn: (...args: T) => void, | ||||
|   timeout: number, | ||||
| ) => { | ||||
|   let handle = 0; | ||||
|   let lastArgs: T | null = null; | ||||
|   const ret = (...args: T) => { | ||||
|     lastArgs = args; | ||||
|     clearTimeout(handle); | ||||
|     handle = window.setTimeout(() => { | ||||
|       lastArgs = null; | ||||
|       fn(...args); | ||||
|     }, timeout); | ||||
|   }; | ||||
|   ret.flush = () => { | ||||
|     clearTimeout(handle); | ||||
|     if (lastArgs) { | ||||
|       const _lastArgs = lastArgs; | ||||
|       lastArgs = null; | ||||
|       fn(..._lastArgs); | ||||
|     } | ||||
|   }; | ||||
|   ret.cancel = () => { | ||||
|     lastArgs = null; | ||||
|     clearTimeout(handle); | ||||
|   }; | ||||
|   return ret; | ||||
| }; | ||||
|  | ||||
| export const withBatchedUpdates = < | ||||
|   TFunction extends ((event: any) => void) | (() => void), | ||||
| >( | ||||
|   func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never, | ||||
| ) => | ||||
|   ((event) => { | ||||
|     unstable_batchedUpdates(func as TFunction, event); | ||||
|   }) as TFunction; | ||||
|  | ||||
| /** | ||||
|  * barches React state updates and throttles the calls to a single call per | ||||
|  * animation frame | ||||
|  */ | ||||
| export const withBatchedUpdatesThrottled = < | ||||
|   TFunction extends ((event: any) => void) | (() => void), | ||||
| >( | ||||
|   func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never, | ||||
| ) => { | ||||
|   // @ts-ignore | ||||
|   return throttleRAF<Parameters<TFunction>>(((event) => { | ||||
|     unstable_batchedUpdates(func, event); | ||||
|   }) as TFunction); | ||||
| }; | ||||
							
								
								
									
										36
									
								
								examples/excalidraw/with-nextjs/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
|  | ||||
| # dependencies | ||||
| /node_modules | ||||
| /.pnp | ||||
| .pnp.js | ||||
| .yarn/install-state.gz | ||||
|  | ||||
| # testing | ||||
| /coverage | ||||
|  | ||||
| # next.js | ||||
| /.next/ | ||||
| /out/ | ||||
|  | ||||
| # production | ||||
| /build | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| *.pem | ||||
|  | ||||
| # debug | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
|  | ||||
| # local env files | ||||
| .env*.local | ||||
|  | ||||
| # vercel | ||||
| .vercel | ||||
|  | ||||
| # typescript | ||||
| *.tsbuildinfo | ||||
| next-env.d.ts | ||||
							
								
								
									
										36
									
								
								examples/excalidraw/with-nextjs/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | ||||
|  | ||||
| ## Getting Started | ||||
|  | ||||
| First, run the development server: | ||||
|  | ||||
| ```bash | ||||
| npm run dev | ||||
| # or | ||||
| yarn dev | ||||
| # or | ||||
| pnpm dev | ||||
| # or | ||||
| bun dev | ||||
| ``` | ||||
|  | ||||
| Open [http://localhost:3000](http://localhost:3005) with your browser to see the result. | ||||
|  | ||||
| You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | ||||
|  | ||||
| This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | ||||
|  | ||||
| ## Learn More | ||||
|  | ||||
| To learn more about Next.js, take a look at the following resources: | ||||
|  | ||||
| - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||||
| - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||||
|  | ||||
| You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | ||||
|  | ||||
| ## Deploy on Vercel | ||||
|  | ||||
| The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||||
|  | ||||
| Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | ||||
							
								
								
									
										12
									
								
								examples/excalidraw/with-nextjs/next.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| /** @type {import('next').NextConfig} */ | ||||
| const nextConfig = { | ||||
|   distDir: "build", | ||||
|   typescript: { | ||||
|     // The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed. | ||||
|     ignoreBuildErrors: true, | ||||
|   }, | ||||
|   // This is needed as in pages router the code for importing types throws error as its outside next js app | ||||
|   transpilePackages: ["../"], | ||||
| }; | ||||
|  | ||||
| module.exports = nextConfig; | ||||
							
								
								
									
										25
									
								
								examples/excalidraw/with-nextjs/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | ||||
| { | ||||
|   "name": "with-nextjs", | ||||
|   "version": "0.1.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm", | ||||
|     "dev": "yarn build:workspace && next dev -p 3005", | ||||
|     "build": "yarn build:workspace && next build", | ||||
|     "start": "next start -p 3006", | ||||
|     "lint": "next lint" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@excalidraw/excalidraw": "*", | ||||
|     "next": "14.1", | ||||
|     "react": "^18", | ||||
|     "react-dom": "^18" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/node": "^20", | ||||
|     "@types/react": "^18", | ||||
|     "@types/react-dom": "^18", | ||||
|     "path2d-polyfill": "2.0.1", | ||||
|     "typescript": "^5" | ||||
|   } | ||||
| } | ||||
| Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB | 
| Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB | 
| Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB | 
							
								
								
									
										
											BIN
										
									
								
								examples/excalidraw/with-nextjs/src/app/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										11
									
								
								examples/excalidraw/with-nextjs/src/app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| export default function RootLayout({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
| }) { | ||||
|   return ( | ||||
|     <html lang="en"> | ||||
|       <body>{children}</body> | ||||
|     </html> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										23
									
								
								examples/excalidraw/with-nextjs/src/app/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| import dynamic from "next/dynamic"; | ||||
| import "../common.scss"; | ||||
|  | ||||
| // Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically | ||||
| // with ssr false | ||||
| const ExcalidrawWithClientOnly = dynamic( | ||||
|   async () => (await import("../excalidrawWrapper")).default, | ||||
|   { | ||||
|     ssr: false, | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| export default function Page() { | ||||
|   return ( | ||||
|     <> | ||||
|       <a href="/excalidraw-in-pages">Switch to Pages router</a> | ||||
|       <h1 className="page-title">App Router</h1> | ||||
|  | ||||
|       {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} | ||||
|       <ExcalidrawWithClientOnly /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										15
									
								
								examples/excalidraw/with-nextjs/src/common.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| * { | ||||
|   box-sizing: border-box; | ||||
|   font-family: sans-serif; | ||||
| } | ||||
|  | ||||
| a { | ||||
|   color: #1c7ed6; | ||||
|   font-size: 20px; | ||||
|   text-decoration: none; | ||||
|   font-weight: 550; | ||||
| } | ||||
|  | ||||
| .page-title { | ||||
|   text-align: center; | ||||
| } | ||||
							
								
								
									
										22
									
								
								examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| "use client"; | ||||
| import * as excalidrawLib from "@excalidraw/excalidraw"; | ||||
| import { Excalidraw } from "@excalidraw/excalidraw"; | ||||
| import App from "../../components/App"; | ||||
|  | ||||
| import "@excalidraw/excalidraw/index.css"; | ||||
|  | ||||
| const ExcalidrawWrapper: React.FC = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <App | ||||
|         appTitle={"Excalidraw with Nextjs Example"} | ||||
|         useCustom={(api: any, args?: any[]) => {}} | ||||
|         excalidrawLib={excalidrawLib} | ||||
|       > | ||||
|         <Excalidraw /> | ||||
|       </App> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ExcalidrawWrapper; | ||||
| @@ -0,0 +1,22 @@ | ||||
| import dynamic from "next/dynamic"; | ||||
| import "../common.scss"; | ||||
|  | ||||
| // Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically | ||||
| // with ssr false | ||||
| const Excalidraw = dynamic( | ||||
|   async () => (await import("../excalidrawWrapper")).default, | ||||
|   { | ||||
|     ssr: false, | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| export default function Page() { | ||||
|   return ( | ||||
|     <> | ||||
|       <a href="/">Switch to App router</a> | ||||
|       <h1 className="page-title">Pages Router</h1> | ||||
|       {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} | ||||
|       <Excalidraw /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										28
									
								
								examples/excalidraw/with-nextjs/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es5", | ||||
|     "lib": ["dom", "dom.iterable", "esnext"], | ||||
|     "allowJs": true, | ||||
|     "skipLibCheck": true, | ||||
|     "strict": true, | ||||
|     "noEmit": true, | ||||
|     "esModuleInterop": true, | ||||
|     "module": "esnext", | ||||
|     "moduleResolution": "node", | ||||
|     "resolveJsonModule": true, | ||||
|     "isolatedModules": true, | ||||
|     "jsx": "preserve", | ||||
|     "incremental": true, | ||||
|     "plugins": [ | ||||
|       { | ||||
|         "name": "next" | ||||
|       } | ||||
|     ], | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     }, | ||||
|     "forceConsistentCasingInFileNames": true | ||||
|   }, | ||||
|   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"], | ||||
|   "exclude": ["node_modules"] | ||||
| } | ||||
							
								
								
									
										3
									
								
								examples/excalidraw/with-nextjs/vercel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "outputDirectory": "build" | ||||
| } | ||||
							
								
								
									
										252
									
								
								examples/excalidraw/with-nextjs/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,252 @@ | ||||
| # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | ||||
| # yarn lockfile v1 | ||||
|  | ||||
|  | ||||
| "@excalidraw/excalidraw@workspace:^": | ||||
|   version "0.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96" | ||||
|   integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ== | ||||
|  | ||||
| "@next/env@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" | ||||
|   integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== | ||||
|  | ||||
| "@next/swc-darwin-arm64@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" | ||||
|   integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== | ||||
|  | ||||
| "@next/swc-darwin-x64@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" | ||||
|   integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== | ||||
|  | ||||
| "@next/swc-linux-arm64-gnu@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" | ||||
|   integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== | ||||
|  | ||||
| "@next/swc-linux-arm64-musl@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" | ||||
|   integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== | ||||
|  | ||||
| "@next/swc-linux-x64-gnu@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" | ||||
|   integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== | ||||
|  | ||||
| "@next/swc-linux-x64-musl@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" | ||||
|   integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== | ||||
|  | ||||
| "@next/swc-win32-arm64-msvc@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" | ||||
|   integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== | ||||
|  | ||||
| "@next/swc-win32-ia32-msvc@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" | ||||
|   integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== | ||||
|  | ||||
| "@next/swc-win32-x64-msvc@14.0.4": | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" | ||||
|   integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== | ||||
|  | ||||
| "@swc/helpers@0.5.2": | ||||
|   version "0.5.2" | ||||
|   resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" | ||||
|   integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== | ||||
|   dependencies: | ||||
|     tslib "^2.4.0" | ||||
|  | ||||
| "@types/node@^20": | ||||
|   version "20.11.0" | ||||
|   resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f" | ||||
|   integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ== | ||||
|   dependencies: | ||||
|     undici-types "~5.26.4" | ||||
|  | ||||
| "@types/prop-types@*": | ||||
|   version "15.7.11" | ||||
|   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" | ||||
|   integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== | ||||
|  | ||||
| "@types/react-dom@^18": | ||||
|   version "18.2.18" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" | ||||
|   integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== | ||||
|   dependencies: | ||||
|     "@types/react" "*" | ||||
|  | ||||
| "@types/react@*", "@types/react@^18": | ||||
|   version "18.2.47" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40" | ||||
|   integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ== | ||||
|   dependencies: | ||||
|     "@types/prop-types" "*" | ||||
|     "@types/scheduler" "*" | ||||
|     csstype "^3.0.2" | ||||
|  | ||||
| "@types/scheduler@*": | ||||
|   version "0.16.8" | ||||
|   resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" | ||||
|   integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== | ||||
|  | ||||
| busboy@1.6.0: | ||||
|   version "1.6.0" | ||||
|   resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" | ||||
|   integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== | ||||
|   dependencies: | ||||
|     streamsearch "^1.1.0" | ||||
|  | ||||
| caniuse-lite@^1.0.30001406: | ||||
|   version "1.0.30001576" | ||||
|   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4" | ||||
|   integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== | ||||
|  | ||||
| client-only@0.0.1: | ||||
|   version "0.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" | ||||
|   integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== | ||||
|  | ||||
| csstype@^3.0.2: | ||||
|   version "3.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" | ||||
|   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== | ||||
|  | ||||
| glob-to-regexp@^0.4.1: | ||||
|   version "0.4.1" | ||||
|   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" | ||||
|   integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== | ||||
|  | ||||
| graceful-fs@^4.1.2, graceful-fs@^4.2.11: | ||||
|   version "4.2.11" | ||||
|   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" | ||||
|   integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== | ||||
|  | ||||
| "js-tokens@^3.0.0 || ^4.0.0": | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | ||||
|   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== | ||||
|  | ||||
| loose-envify@^1.1.0: | ||||
|   version "1.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" | ||||
|   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== | ||||
|   dependencies: | ||||
|     js-tokens "^3.0.0 || ^4.0.0" | ||||
|  | ||||
| nanoid@^3.3.6: | ||||
|   version "3.3.7" | ||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" | ||||
|   integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== | ||||
|  | ||||
| next@14.0.4: | ||||
|   version "14.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" | ||||
|   integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== | ||||
|   dependencies: | ||||
|     "@next/env" "14.0.4" | ||||
|     "@swc/helpers" "0.5.2" | ||||
|     busboy "1.6.0" | ||||
|     caniuse-lite "^1.0.30001406" | ||||
|     graceful-fs "^4.2.11" | ||||
|     postcss "8.4.31" | ||||
|     styled-jsx "5.1.1" | ||||
|     watchpack "2.4.0" | ||||
|   optionalDependencies: | ||||
|     "@next/swc-darwin-arm64" "14.0.4" | ||||
|     "@next/swc-darwin-x64" "14.0.4" | ||||
|     "@next/swc-linux-arm64-gnu" "14.0.4" | ||||
|     "@next/swc-linux-arm64-musl" "14.0.4" | ||||
|     "@next/swc-linux-x64-gnu" "14.0.4" | ||||
|     "@next/swc-linux-x64-musl" "14.0.4" | ||||
|     "@next/swc-win32-arm64-msvc" "14.0.4" | ||||
|     "@next/swc-win32-ia32-msvc" "14.0.4" | ||||
|     "@next/swc-win32-x64-msvc" "14.0.4" | ||||
|  | ||||
| path2d-polyfill@2.0.1: | ||||
|   version "2.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" | ||||
|   integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== | ||||
|  | ||||
| picocolors@^1.0.0: | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" | ||||
|   integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== | ||||
|  | ||||
| postcss@8.4.31: | ||||
|   version "8.4.31" | ||||
|   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" | ||||
|   integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== | ||||
|   dependencies: | ||||
|     nanoid "^3.3.6" | ||||
|     picocolors "^1.0.0" | ||||
|     source-map-js "^1.0.2" | ||||
|  | ||||
| react-dom@^18: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" | ||||
|   integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|     scheduler "^0.23.0" | ||||
|  | ||||
| react@^18: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" | ||||
|   integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|  | ||||
| scheduler@^0.23.0: | ||||
|   version "0.23.0" | ||||
|   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" | ||||
|   integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|  | ||||
| source-map-js@^1.0.2: | ||||
|   version "1.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" | ||||
|   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== | ||||
|  | ||||
| streamsearch@^1.1.0: | ||||
|   version "1.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" | ||||
|   integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== | ||||
|  | ||||
| styled-jsx@5.1.1: | ||||
|   version "5.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" | ||||
|   integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== | ||||
|   dependencies: | ||||
|     client-only "0.0.1" | ||||
|  | ||||
| tslib@^2.4.0: | ||||
|   version "2.6.2" | ||||
|   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" | ||||
|   integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== | ||||
|  | ||||
| typescript@^5: | ||||
|   version "5.3.3" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" | ||||
|   integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== | ||||
|  | ||||
| undici-types@~5.26.4: | ||||
|   version "5.26.5" | ||||
|   resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" | ||||
|   integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== | ||||
|  | ||||
| watchpack@2.4.0: | ||||
|   version "2.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" | ||||
|   integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== | ||||
|   dependencies: | ||||
|     glob-to-regexp "^0.4.1" | ||||
|     graceful-fs "^4.1.2" | ||||
| @@ -12,18 +12,21 @@ | ||||
|     <script> | ||||
|       window.name = "codesandbox"; | ||||
|     </script> | ||||
|     <link rel="stylesheet" href="/dist/browser/dev/index.css" /> | ||||
|   </head> | ||||
| 
 | ||||
|   <body> | ||||
|     <noscript> You need to enable JavaScript to run this app. </noscript> | ||||
|     <div id="root"></div> | ||||
|     <script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script> | ||||
|     <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script> | ||||
| 
 | ||||
|     <!-- This is so that we use the bundled excalidraw.development.js file instead | ||||
|     of the actual source code --> | ||||
|     <script src="./excalidraw.development.js"></script> | ||||
|     <script type="module"> | ||||
|       import * as ExcalidrawLib from "@excalidraw/excalidraw"; | ||||
| 
 | ||||
|     <script src="./bundle.js"></script> | ||||
|       console.log(ExcalidrawLib); | ||||
|       window.ExcalidrawLib = ExcalidrawLib; | ||||
|     </script> | ||||
|     <script type="module" src="index.tsx"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										28
									
								
								examples/excalidraw/with-script-in-browser/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| import App from "../components/App"; | ||||
| import React, { StrictMode } from "react"; | ||||
| import { createRoot } from "react-dom/client"; | ||||
|  | ||||
| import type * as TExcalidraw from "@excalidraw/excalidraw"; | ||||
|  | ||||
| import "@excalidraw/excalidraw/index.css"; | ||||
|  | ||||
| declare global { | ||||
|   interface Window { | ||||
|     ExcalidrawLib: typeof TExcalidraw; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const rootElement = document.getElementById("root")!; | ||||
| const root = createRoot(rootElement); | ||||
| const { Excalidraw } = window.ExcalidrawLib; | ||||
| root.render( | ||||
|   <StrictMode> | ||||
|     <App | ||||
|       appTitle={"Excalidraw Example"} | ||||
|       useCustom={(api: any, args?: any[]) => {}} | ||||
|       excalidrawLib={window.ExcalidrawLib} | ||||
|     > | ||||
|       <Excalidraw /> | ||||
|     </App> | ||||
|   </StrictMode>, | ||||
| ); | ||||
							
								
								
									
										19
									
								
								examples/excalidraw/with-script-in-browser/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| { | ||||
|   "name": "with-script-in-browser", | ||||
|   "version": "1.0.0", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
|     "@excalidraw/excalidraw": "*" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "vite": "5.0.12", | ||||
|     "typescript": "^5" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "yarn workspace @excalidraw/excalidraw run build:esm && vite", | ||||
|     "build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build", | ||||
|     "build:preview": "yarn build && vite preview --port 5002" | ||||
|   } | ||||
| } | ||||
| After Width: | Height: | Size: 197 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 39 KiB | 
							
								
								
									
										4
									
								
								examples/excalidraw/with-script-in-browser/vercel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "outputDirectory": "dist", | ||||
|   "installCommand": "yarn install" | ||||
| } | ||||
							
								
								
									
										11
									
								
								examples/excalidraw/with-script-in-browser/vite.config.mts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| import { defineConfig } from "vite"; | ||||
|  | ||||
| // https://vitejs.dev/config/ | ||||
| export default defineConfig({ | ||||
|   server: { | ||||
|     port: 3001, | ||||
|     // open the browser | ||||
|     open: true, | ||||
|   }, | ||||
|   publicDir: "public", | ||||
| }); | ||||
							
								
								
									
										313
									
								
								examples/excalidraw/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,313 @@ | ||||
| # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | ||||
| # yarn lockfile v1 | ||||
|  | ||||
|  | ||||
| "@esbuild/aix-ppc64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" | ||||
|   integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== | ||||
|  | ||||
| "@esbuild/android-arm64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220" | ||||
|   integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== | ||||
|  | ||||
| "@esbuild/android-arm@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c" | ||||
|   integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== | ||||
|  | ||||
| "@esbuild/android-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2" | ||||
|   integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== | ||||
|  | ||||
| "@esbuild/darwin-arm64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf" | ||||
|   integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== | ||||
|  | ||||
| "@esbuild/darwin-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e" | ||||
|   integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== | ||||
|  | ||||
| "@esbuild/freebsd-arm64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a" | ||||
|   integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== | ||||
|  | ||||
| "@esbuild/freebsd-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2" | ||||
|   integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== | ||||
|  | ||||
| "@esbuild/linux-arm64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545" | ||||
|   integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== | ||||
|  | ||||
| "@esbuild/linux-arm@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3" | ||||
|   integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== | ||||
|  | ||||
| "@esbuild/linux-ia32@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4" | ||||
|   integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== | ||||
|  | ||||
| "@esbuild/linux-loong64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121" | ||||
|   integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== | ||||
|  | ||||
| "@esbuild/linux-mips64el@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9" | ||||
|   integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== | ||||
|  | ||||
| "@esbuild/linux-ppc64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912" | ||||
|   integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== | ||||
|  | ||||
| "@esbuild/linux-riscv64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916" | ||||
|   integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== | ||||
|  | ||||
| "@esbuild/linux-s390x@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8" | ||||
|   integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== | ||||
|  | ||||
| "@esbuild/linux-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766" | ||||
|   integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== | ||||
|  | ||||
| "@esbuild/netbsd-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d" | ||||
|   integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== | ||||
|  | ||||
| "@esbuild/openbsd-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2" | ||||
|   integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== | ||||
|  | ||||
| "@esbuild/sunos-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767" | ||||
|   integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== | ||||
|  | ||||
| "@esbuild/win32-arm64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee" | ||||
|   integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== | ||||
|  | ||||
| "@esbuild/win32-ia32@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c" | ||||
|   integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== | ||||
|  | ||||
| "@esbuild/win32-x64@0.19.11": | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04" | ||||
|   integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== | ||||
|  | ||||
| "@rollup/rollup-android-arm-eabi@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9" | ||||
|   integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA== | ||||
|  | ||||
| "@rollup/rollup-android-arm64@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a" | ||||
|   integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg== | ||||
|  | ||||
| "@rollup/rollup-darwin-arm64@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb" | ||||
|   integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w== | ||||
|  | ||||
| "@rollup/rollup-darwin-x64@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1" | ||||
|   integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA== | ||||
|  | ||||
| "@rollup/rollup-linux-arm-gnueabihf@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560" | ||||
|   integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g== | ||||
|  | ||||
| "@rollup/rollup-linux-arm64-gnu@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114" | ||||
|   integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA== | ||||
|  | ||||
| "@rollup/rollup-linux-arm64-musl@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632" | ||||
|   integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ== | ||||
|  | ||||
| "@rollup/rollup-linux-riscv64-gnu@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad" | ||||
|   integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA== | ||||
|  | ||||
| "@rollup/rollup-linux-x64-gnu@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49" | ||||
|   integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA== | ||||
|  | ||||
| "@rollup/rollup-linux-x64-musl@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4" | ||||
|   integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg== | ||||
|  | ||||
| "@rollup/rollup-win32-arm64-msvc@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a" | ||||
|   integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ== | ||||
|  | ||||
| "@rollup/rollup-win32-ia32-msvc@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85" | ||||
|   integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA== | ||||
|  | ||||
| "@rollup/rollup-win32-x64-msvc@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602" | ||||
|   integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ== | ||||
|  | ||||
| "@types/estree@1.0.5": | ||||
|   version "1.0.5" | ||||
|   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" | ||||
|   integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== | ||||
|  | ||||
| esbuild@^0.19.3: | ||||
|   version "0.19.11" | ||||
|   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96" | ||||
|   integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA== | ||||
|   optionalDependencies: | ||||
|     "@esbuild/aix-ppc64" "0.19.11" | ||||
|     "@esbuild/android-arm" "0.19.11" | ||||
|     "@esbuild/android-arm64" "0.19.11" | ||||
|     "@esbuild/android-x64" "0.19.11" | ||||
|     "@esbuild/darwin-arm64" "0.19.11" | ||||
|     "@esbuild/darwin-x64" "0.19.11" | ||||
|     "@esbuild/freebsd-arm64" "0.19.11" | ||||
|     "@esbuild/freebsd-x64" "0.19.11" | ||||
|     "@esbuild/linux-arm" "0.19.11" | ||||
|     "@esbuild/linux-arm64" "0.19.11" | ||||
|     "@esbuild/linux-ia32" "0.19.11" | ||||
|     "@esbuild/linux-loong64" "0.19.11" | ||||
|     "@esbuild/linux-mips64el" "0.19.11" | ||||
|     "@esbuild/linux-ppc64" "0.19.11" | ||||
|     "@esbuild/linux-riscv64" "0.19.11" | ||||
|     "@esbuild/linux-s390x" "0.19.11" | ||||
|     "@esbuild/linux-x64" "0.19.11" | ||||
|     "@esbuild/netbsd-x64" "0.19.11" | ||||
|     "@esbuild/openbsd-x64" "0.19.11" | ||||
|     "@esbuild/sunos-x64" "0.19.11" | ||||
|     "@esbuild/win32-arm64" "0.19.11" | ||||
|     "@esbuild/win32-ia32" "0.19.11" | ||||
|     "@esbuild/win32-x64" "0.19.11" | ||||
|  | ||||
| fsevents@~2.3.2, fsevents@~2.3.3: | ||||
|   version "2.3.3" | ||||
|   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" | ||||
|   integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== | ||||
|  | ||||
| "js-tokens@^3.0.0 || ^4.0.0": | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | ||||
|   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== | ||||
|  | ||||
| loose-envify@^1.1.0: | ||||
|   version "1.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" | ||||
|   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== | ||||
|   dependencies: | ||||
|     js-tokens "^3.0.0 || ^4.0.0" | ||||
|  | ||||
| nanoid@^3.3.7: | ||||
|   version "3.3.7" | ||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" | ||||
|   integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== | ||||
|  | ||||
| picocolors@^1.0.0: | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" | ||||
|   integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== | ||||
|  | ||||
| postcss@^8.4.32: | ||||
|   version "8.4.33" | ||||
|   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" | ||||
|   integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== | ||||
|   dependencies: | ||||
|     nanoid "^3.3.7" | ||||
|     picocolors "^1.0.0" | ||||
|     source-map-js "^1.0.2" | ||||
|  | ||||
| react-dom@18.2.0: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" | ||||
|   integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|     scheduler "^0.23.0" | ||||
|  | ||||
| react@18.2.0: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" | ||||
|   integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|  | ||||
| rollup@^4.2.0: | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05" | ||||
|   integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ== | ||||
|   dependencies: | ||||
|     "@types/estree" "1.0.5" | ||||
|   optionalDependencies: | ||||
|     "@rollup/rollup-android-arm-eabi" "4.9.5" | ||||
|     "@rollup/rollup-android-arm64" "4.9.5" | ||||
|     "@rollup/rollup-darwin-arm64" "4.9.5" | ||||
|     "@rollup/rollup-darwin-x64" "4.9.5" | ||||
|     "@rollup/rollup-linux-arm-gnueabihf" "4.9.5" | ||||
|     "@rollup/rollup-linux-arm64-gnu" "4.9.5" | ||||
|     "@rollup/rollup-linux-arm64-musl" "4.9.5" | ||||
|     "@rollup/rollup-linux-riscv64-gnu" "4.9.5" | ||||
|     "@rollup/rollup-linux-x64-gnu" "4.9.5" | ||||
|     "@rollup/rollup-linux-x64-musl" "4.9.5" | ||||
|     "@rollup/rollup-win32-arm64-msvc" "4.9.5" | ||||
|     "@rollup/rollup-win32-ia32-msvc" "4.9.5" | ||||
|     "@rollup/rollup-win32-x64-msvc" "4.9.5" | ||||
|     fsevents "~2.3.2" | ||||
|  | ||||
| scheduler@^0.23.0: | ||||
|   version "0.23.0" | ||||
|   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" | ||||
|   integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|  | ||||
| source-map-js@^1.0.2: | ||||
|   version "1.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" | ||||
|   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== | ||||
|  | ||||
| vite@5.0.6: | ||||
|   version "5.0.6" | ||||
|   resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c" | ||||
|   integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ== | ||||
|   dependencies: | ||||
|     esbuild "^0.19.3" | ||||
|     postcss "^8.4.32" | ||||
|     rollup "^4.2.0" | ||||
|   optionalDependencies: | ||||
|     fsevents "~2.3.3" | ||||
							
								
								
									
										897
									
								
								excalidraw-app/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,897 @@ | ||||
| import polyfill from "../packages/excalidraw/polyfill"; | ||||
| import LanguageDetector from "i18next-browser-languagedetector"; | ||||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||||
| import { trackEvent } from "../packages/excalidraw/analytics"; | ||||
| import { getDefaultAppState } from "../packages/excalidraw/appState"; | ||||
| import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog"; | ||||
| import { TopErrorBoundary } from "./components/TopErrorBoundary"; | ||||
| import { | ||||
|   APP_NAME, | ||||
|   EVENT, | ||||
|   THEME, | ||||
|   TITLE_TIMEOUT, | ||||
|   VERSION_TIMEOUT, | ||||
| } from "../packages/excalidraw/constants"; | ||||
| import { loadFromBlob } from "../packages/excalidraw/data/blob"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   FileId, | ||||
|   NonDeletedExcalidrawElement, | ||||
|   Theme, | ||||
| } from "../packages/excalidraw/element/types"; | ||||
| import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState"; | ||||
| import { t } from "../packages/excalidraw/i18n"; | ||||
| import { | ||||
|   Excalidraw, | ||||
|   defaultLang, | ||||
|   LiveCollaborationTrigger, | ||||
|   TTDDialog, | ||||
|   TTDDialogTrigger, | ||||
| } from "../packages/excalidraw/index"; | ||||
| import { | ||||
|   AppState, | ||||
|   LibraryItems, | ||||
|   ExcalidrawImperativeAPI, | ||||
|   BinaryFiles, | ||||
|   ExcalidrawInitialDataState, | ||||
|   UIAppState, | ||||
| } from "../packages/excalidraw/types"; | ||||
| import { | ||||
|   debounce, | ||||
|   getVersion, | ||||
|   getFrame, | ||||
|   isTestEnv, | ||||
|   preventUnload, | ||||
|   ResolvablePromise, | ||||
|   resolvablePromise, | ||||
|   isRunningInIframe, | ||||
| } from "../packages/excalidraw/utils"; | ||||
| import { | ||||
|   FIREBASE_STORAGE_PREFIXES, | ||||
|   STORAGE_KEYS, | ||||
|   SYNC_BROWSER_TABS_TIMEOUT, | ||||
| } from "./app_constants"; | ||||
| import Collab, { | ||||
|   CollabAPI, | ||||
|   collabAPIAtom, | ||||
|   isCollaboratingAtom, | ||||
|   isOfflineAtom, | ||||
| } from "./collab/Collab"; | ||||
| import { | ||||
|   exportToBackend, | ||||
|   getCollaborationLinkData, | ||||
|   isCollaborationLink, | ||||
|   loadScene, | ||||
| } from "./data"; | ||||
| import { | ||||
|   getLibraryItemsFromStorage, | ||||
|   importFromLocalStorage, | ||||
|   importUsernameFromLocalStorage, | ||||
| } from "./data/localStorage"; | ||||
| import CustomStats from "./CustomStats"; | ||||
| import { | ||||
|   restore, | ||||
|   restoreAppState, | ||||
|   RestoredDataState, | ||||
| } from "../packages/excalidraw/data/restore"; | ||||
| import { | ||||
|   ExportToExcalidrawPlus, | ||||
|   exportToExcalidrawPlus, | ||||
| } from "./components/ExportToExcalidrawPlus"; | ||||
| import { updateStaleImageStatuses } from "./data/FileManager"; | ||||
| import { newElementWith } from "../packages/excalidraw/element/mutateElement"; | ||||
| import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks"; | ||||
| import { loadFilesFromFirebase } from "./data/firebase"; | ||||
| import { LocalData } from "./data/LocalData"; | ||||
| import { isBrowserStorageStateNewer } from "./data/tabSync"; | ||||
| import clsx from "clsx"; | ||||
| import { reconcileElements } from "./collab/reconciliation"; | ||||
| import { | ||||
|   parseLibraryTokensFromUrl, | ||||
|   useHandleLibrary, | ||||
| } from "../packages/excalidraw/data/library"; | ||||
| import { AppMainMenu } from "./components/AppMainMenu"; | ||||
| import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; | ||||
| import { AppFooter } from "./components/AppFooter"; | ||||
| import { atom, Provider, useAtom, useAtomValue } from "jotai"; | ||||
| import { useAtomWithInitialValue } from "../packages/excalidraw/jotai"; | ||||
| import { appJotaiStore } from "./app-jotai"; | ||||
|  | ||||
| import "./index.scss"; | ||||
| import { ResolutionType } from "../packages/excalidraw/utility-types"; | ||||
| import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog"; | ||||
| import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState"; | ||||
| import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm"; | ||||
| import Trans from "../packages/excalidraw/components/Trans"; | ||||
| import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; | ||||
|  | ||||
| polyfill(); | ||||
|  | ||||
| window.EXCALIDRAW_THROTTLE_RENDER = true; | ||||
|  | ||||
| let isSelfEmbedding = false; | ||||
|  | ||||
| if (window.self !== window.top) { | ||||
|   try { | ||||
|     const parentUrl = new URL(document.referrer); | ||||
|     const currentUrl = new URL(window.location.href); | ||||
|     if (parentUrl.origin === currentUrl.origin) { | ||||
|       isSelfEmbedding = true; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     // ignore | ||||
|   } | ||||
| } | ||||
|  | ||||
| const languageDetector = new LanguageDetector(); | ||||
| languageDetector.init({ | ||||
|   languageUtils: {}, | ||||
| }); | ||||
|  | ||||
| const shareableLinkConfirmDialog = { | ||||
|   title: t("overwriteConfirm.modal.shareableLink.title"), | ||||
|   description: ( | ||||
|     <Trans | ||||
|       i18nKey="overwriteConfirm.modal.shareableLink.description" | ||||
|       bold={(text) => <strong>{text}</strong>} | ||||
|       br={() => <br />} | ||||
|     /> | ||||
|   ), | ||||
|   actionLabel: t("overwriteConfirm.modal.shareableLink.button"), | ||||
|   color: "danger", | ||||
| } as const; | ||||
|  | ||||
| const initializeScene = async (opts: { | ||||
|   collabAPI: CollabAPI | null; | ||||
|   excalidrawAPI: ExcalidrawImperativeAPI; | ||||
| }): Promise< | ||||
|   { scene: ExcalidrawInitialDataState | null } & ( | ||||
|     | { isExternalScene: true; id: string; key: string } | ||||
|     | { isExternalScene: false; id?: null; key?: null } | ||||
|   ) | ||||
| > => { | ||||
|   const searchParams = new URLSearchParams(window.location.search); | ||||
|   const id = searchParams.get("id"); | ||||
|   const jsonBackendMatch = window.location.hash.match( | ||||
|     /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/, | ||||
|   ); | ||||
|   const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/); | ||||
|  | ||||
|   const localDataState = importFromLocalStorage(); | ||||
|  | ||||
|   let scene: RestoredDataState & { | ||||
|     scrollToContent?: boolean; | ||||
|   } = await loadScene(null, null, localDataState); | ||||
|  | ||||
|   let roomLinkData = getCollaborationLinkData(window.location.href); | ||||
|   const isExternalScene = !!(id || jsonBackendMatch || roomLinkData); | ||||
|   if (isExternalScene) { | ||||
|     if ( | ||||
|       // don't prompt if scene is empty | ||||
|       !scene.elements.length || | ||||
|       // don't prompt for collab scenes because we don't override local storage | ||||
|       roomLinkData || | ||||
|       // otherwise, prompt whether user wants to override current scene | ||||
|       (await openConfirmModal(shareableLinkConfirmDialog)) | ||||
|     ) { | ||||
|       if (jsonBackendMatch) { | ||||
|         scene = await loadScene( | ||||
|           jsonBackendMatch[1], | ||||
|           jsonBackendMatch[2], | ||||
|           localDataState, | ||||
|         ); | ||||
|       } | ||||
|       scene.scrollToContent = true; | ||||
|       if (!roomLinkData) { | ||||
|         window.history.replaceState({}, APP_NAME, window.location.origin); | ||||
|       } | ||||
|     } else { | ||||
|       // https://github.com/excalidraw/excalidraw/issues/1919 | ||||
|       if (document.hidden) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|           window.addEventListener( | ||||
|             "focus", | ||||
|             () => initializeScene(opts).then(resolve).catch(reject), | ||||
|             { | ||||
|               once: true, | ||||
|             }, | ||||
|           ); | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       roomLinkData = null; | ||||
|       window.history.replaceState({}, APP_NAME, window.location.origin); | ||||
|     } | ||||
|   } else if (externalUrlMatch) { | ||||
|     window.history.replaceState({}, APP_NAME, window.location.origin); | ||||
|  | ||||
|     const url = externalUrlMatch[1]; | ||||
|     try { | ||||
|       const request = await fetch(window.decodeURIComponent(url)); | ||||
|       const data = await loadFromBlob(await request.blob(), null, null); | ||||
|       if ( | ||||
|         !scene.elements.length || | ||||
|         (await openConfirmModal(shareableLinkConfirmDialog)) | ||||
|       ) { | ||||
|         return { scene: data, isExternalScene }; | ||||
|       } | ||||
|     } catch (error: any) { | ||||
|       return { | ||||
|         scene: { | ||||
|           appState: { | ||||
|             errorMessage: t("alerts.invalidSceneUrl"), | ||||
|           }, | ||||
|         }, | ||||
|         isExternalScene, | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (roomLinkData && opts.collabAPI) { | ||||
|     const { excalidrawAPI } = opts; | ||||
|  | ||||
|     const scene = await opts.collabAPI.startCollaboration(roomLinkData); | ||||
|  | ||||
|     return { | ||||
|       // when collaborating, the state may have already been updated at this | ||||
|       // point (we may have received updates from other clients), so reconcile | ||||
|       // elements and appState with existing state | ||||
|       scene: { | ||||
|         ...scene, | ||||
|         appState: { | ||||
|           ...restoreAppState( | ||||
|             { | ||||
|               ...scene?.appState, | ||||
|               theme: localDataState?.appState?.theme || scene?.appState?.theme, | ||||
|             }, | ||||
|             excalidrawAPI.getAppState(), | ||||
|           ), | ||||
|           // necessary if we're invoking from a hashchange handler which doesn't | ||||
|           // go through App.initializeScene() that resets this flag | ||||
|           isLoading: false, | ||||
|         }, | ||||
|         elements: reconcileElements( | ||||
|           scene?.elements || [], | ||||
|           excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|           excalidrawAPI.getAppState(), | ||||
|         ), | ||||
|       }, | ||||
|       isExternalScene: true, | ||||
|       id: roomLinkData.roomId, | ||||
|       key: roomLinkData.roomKey, | ||||
|     }; | ||||
|   } else if (scene) { | ||||
|     return isExternalScene && jsonBackendMatch | ||||
|       ? { | ||||
|           scene, | ||||
|           isExternalScene, | ||||
|           id: jsonBackendMatch[1], | ||||
|           key: jsonBackendMatch[2], | ||||
|         } | ||||
|       : { scene, isExternalScene: false }; | ||||
|   } | ||||
|   return { scene: null, isExternalScene: false }; | ||||
| }; | ||||
|  | ||||
| const detectedLangCode = languageDetector.detect() || defaultLang.code; | ||||
| export const appLangCodeAtom = atom( | ||||
|   Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode, | ||||
| ); | ||||
|  | ||||
| const ExcalidrawWrapper = () => { | ||||
|   const [errorMessage, setErrorMessage] = useState(""); | ||||
|   const [langCode, setLangCode] = useAtom(appLangCodeAtom); | ||||
|   const isCollabDisabled = isRunningInIframe(); | ||||
|  | ||||
|   // initial state | ||||
|   // --------------------------------------------------------------------------- | ||||
|  | ||||
|   const initialStatePromiseRef = useRef<{ | ||||
|     promise: ResolvablePromise<ExcalidrawInitialDataState | null>; | ||||
|   }>({ promise: null! }); | ||||
|   if (!initialStatePromiseRef.current.promise) { | ||||
|     initialStatePromiseRef.current.promise = | ||||
|       resolvablePromise<ExcalidrawInitialDataState | null>(); | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     trackEvent("load", "frame", getFrame()); | ||||
|     // Delayed so that the app has a time to load the latest SW | ||||
|     setTimeout(() => { | ||||
|       trackEvent("load", "version", getVersion()); | ||||
|     }, VERSION_TIMEOUT); | ||||
|   }, []); | ||||
|  | ||||
|   const [excalidrawAPI, excalidrawRefCallback] = | ||||
|     useCallbackRefState<ExcalidrawImperativeAPI>(); | ||||
|  | ||||
|   const [, setShareDialogState] = useAtom(shareDialogStateAtom); | ||||
|   const [collabAPI] = useAtom(collabAPIAtom); | ||||
|   const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => { | ||||
|     return isCollaborationLink(window.location.href); | ||||
|   }); | ||||
|  | ||||
|   useHandleLibrary({ | ||||
|     excalidrawAPI, | ||||
|     getInitialLibraryItems: getLibraryItemsFromStorage, | ||||
|   }); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const loadImages = ( | ||||
|       data: ResolutionType<typeof initializeScene>, | ||||
|       isInitialLoad = false, | ||||
|     ) => { | ||||
|       if (!data.scene) { | ||||
|         return; | ||||
|       } | ||||
|       if (collabAPI?.isCollaborating()) { | ||||
|         if (data.scene.elements) { | ||||
|           collabAPI | ||||
|             .fetchImageFilesFromFirebase({ | ||||
|               elements: data.scene.elements, | ||||
|               forceFetchFiles: true, | ||||
|             }) | ||||
|             .then(({ loadedFiles, erroredFiles }) => { | ||||
|               excalidrawAPI.addFiles(loadedFiles); | ||||
|               updateStaleImageStatuses({ | ||||
|                 excalidrawAPI, | ||||
|                 erroredFiles, | ||||
|                 elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|               }); | ||||
|             }); | ||||
|         } | ||||
|       } else { | ||||
|         const fileIds = | ||||
|           data.scene.elements?.reduce((acc, element) => { | ||||
|             if (isInitializedImageElement(element)) { | ||||
|               return acc.concat(element.fileId); | ||||
|             } | ||||
|             return acc; | ||||
|           }, [] as FileId[]) || []; | ||||
|  | ||||
|         if (data.isExternalScene) { | ||||
|           loadFilesFromFirebase( | ||||
|             `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`, | ||||
|             data.key, | ||||
|             fileIds, | ||||
|           ).then(({ loadedFiles, erroredFiles }) => { | ||||
|             excalidrawAPI.addFiles(loadedFiles); | ||||
|             updateStaleImageStatuses({ | ||||
|               excalidrawAPI, | ||||
|               erroredFiles, | ||||
|               elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|             }); | ||||
|           }); | ||||
|         } else if (isInitialLoad) { | ||||
|           if (fileIds.length) { | ||||
|             LocalData.fileStorage | ||||
|               .getFiles(fileIds) | ||||
|               .then(({ loadedFiles, erroredFiles }) => { | ||||
|                 if (loadedFiles.length) { | ||||
|                   excalidrawAPI.addFiles(loadedFiles); | ||||
|                 } | ||||
|                 updateStaleImageStatuses({ | ||||
|                   excalidrawAPI, | ||||
|                   erroredFiles, | ||||
|                   elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|                 }); | ||||
|               }); | ||||
|           } | ||||
|           // on fresh load, clear unused files from IDB (from previous | ||||
|           // session) | ||||
|           LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds }); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => { | ||||
|       loadImages(data, /* isInitialLoad */ true); | ||||
|       initialStatePromiseRef.current.promise.resolve(data.scene); | ||||
|     }); | ||||
|  | ||||
|     const onHashChange = async (event: HashChangeEvent) => { | ||||
|       event.preventDefault(); | ||||
|       const libraryUrlTokens = parseLibraryTokensFromUrl(); | ||||
|       if (!libraryUrlTokens) { | ||||
|         if ( | ||||
|           collabAPI?.isCollaborating() && | ||||
|           !isCollaborationLink(window.location.href) | ||||
|         ) { | ||||
|           collabAPI.stopCollaboration(false); | ||||
|         } | ||||
|         excalidrawAPI.updateScene({ appState: { isLoading: true } }); | ||||
|  | ||||
|         initializeScene({ collabAPI, excalidrawAPI }).then((data) => { | ||||
|           loadImages(data); | ||||
|           if (data.scene) { | ||||
|             excalidrawAPI.updateScene({ | ||||
|               ...data.scene, | ||||
|               ...restore(data.scene, null, null, { repairBindings: true }), | ||||
|               commitToHistory: true, | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const titleTimeout = setTimeout( | ||||
|       () => (document.title = APP_NAME), | ||||
|       TITLE_TIMEOUT, | ||||
|     ); | ||||
|  | ||||
|     const syncData = debounce(() => { | ||||
|       if (isTestEnv()) { | ||||
|         return; | ||||
|       } | ||||
|       if ( | ||||
|         !document.hidden && | ||||
|         ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled) | ||||
|       ) { | ||||
|         // don't sync if local state is newer or identical to browser state | ||||
|         if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { | ||||
|           const localDataState = importFromLocalStorage(); | ||||
|           const username = importUsernameFromLocalStorage(); | ||||
|           let langCode = languageDetector.detect() || defaultLang.code; | ||||
|           if (Array.isArray(langCode)) { | ||||
|             langCode = langCode[0]; | ||||
|           } | ||||
|           setLangCode(langCode); | ||||
|           excalidrawAPI.updateScene({ | ||||
|             ...localDataState, | ||||
|           }); | ||||
|           excalidrawAPI.updateLibrary({ | ||||
|             libraryItems: getLibraryItemsFromStorage(), | ||||
|           }); | ||||
|           collabAPI?.setUsername(username || ""); | ||||
|         } | ||||
|  | ||||
|         if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) { | ||||
|           const elements = excalidrawAPI.getSceneElementsIncludingDeleted(); | ||||
|           const currFiles = excalidrawAPI.getFiles(); | ||||
|           const fileIds = | ||||
|             elements?.reduce((acc, element) => { | ||||
|               if ( | ||||
|                 isInitializedImageElement(element) && | ||||
|                 // only load and update images that aren't already loaded | ||||
|                 !currFiles[element.fileId] | ||||
|               ) { | ||||
|                 return acc.concat(element.fileId); | ||||
|               } | ||||
|               return acc; | ||||
|             }, [] as FileId[]) || []; | ||||
|           if (fileIds.length) { | ||||
|             LocalData.fileStorage | ||||
|               .getFiles(fileIds) | ||||
|               .then(({ loadedFiles, erroredFiles }) => { | ||||
|                 if (loadedFiles.length) { | ||||
|                   excalidrawAPI.addFiles(loadedFiles); | ||||
|                 } | ||||
|                 updateStaleImageStatuses({ | ||||
|                   excalidrawAPI, | ||||
|                   erroredFiles, | ||||
|                   elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|                 }); | ||||
|               }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, SYNC_BROWSER_TABS_TIMEOUT); | ||||
|  | ||||
|     const onUnload = () => { | ||||
|       LocalData.flushSave(); | ||||
|     }; | ||||
|  | ||||
|     const visibilityChange = (event: FocusEvent | Event) => { | ||||
|       if (event.type === EVENT.BLUR || document.hidden) { | ||||
|         LocalData.flushSave(); | ||||
|       } | ||||
|       if ( | ||||
|         event.type === EVENT.VISIBILITY_CHANGE || | ||||
|         event.type === EVENT.FOCUS | ||||
|       ) { | ||||
|         syncData(); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     window.addEventListener(EVENT.HASHCHANGE, onHashChange, false); | ||||
|     window.addEventListener(EVENT.UNLOAD, onUnload, false); | ||||
|     window.addEventListener(EVENT.BLUR, visibilityChange, false); | ||||
|     document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false); | ||||
|     window.addEventListener(EVENT.FOCUS, visibilityChange, false); | ||||
|     return () => { | ||||
|       window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false); | ||||
|       window.removeEventListener(EVENT.UNLOAD, onUnload, false); | ||||
|       window.removeEventListener(EVENT.BLUR, visibilityChange, false); | ||||
|       window.removeEventListener(EVENT.FOCUS, visibilityChange, false); | ||||
|       document.removeEventListener( | ||||
|         EVENT.VISIBILITY_CHANGE, | ||||
|         visibilityChange, | ||||
|         false, | ||||
|       ); | ||||
|       clearTimeout(titleTimeout); | ||||
|     }; | ||||
|   }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const unloadHandler = (event: BeforeUnloadEvent) => { | ||||
|       LocalData.flushSave(); | ||||
|  | ||||
|       if ( | ||||
|         excalidrawAPI && | ||||
|         LocalData.fileStorage.shouldPreventUnload( | ||||
|           excalidrawAPI.getSceneElements(), | ||||
|         ) | ||||
|       ) { | ||||
|         preventUnload(event); | ||||
|       } | ||||
|     }; | ||||
|     window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); | ||||
|     return () => { | ||||
|       window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); | ||||
|     }; | ||||
|   }, [excalidrawAPI]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     languageDetector.cacheUserLanguage(langCode); | ||||
|   }, [langCode]); | ||||
|  | ||||
|   const [theme, setTheme] = useState<Theme>( | ||||
|     () => | ||||
|       (localStorage.getItem( | ||||
|         STORAGE_KEYS.LOCAL_STORAGE_THEME, | ||||
|       ) as Theme | null) || | ||||
|       // FIXME migration from old LS scheme. Can be removed later. #5660 | ||||
|       importFromLocalStorage().appState?.theme || | ||||
|       THEME.LIGHT, | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme); | ||||
|     // currently only used for body styling during init (see public/index.html), | ||||
|     // but may change in the future | ||||
|     document.documentElement.classList.toggle("dark", theme === THEME.DARK); | ||||
|   }, [theme]); | ||||
|  | ||||
|   const onChange = ( | ||||
|     elements: readonly ExcalidrawElement[], | ||||
|     appState: AppState, | ||||
|     files: BinaryFiles, | ||||
|   ) => { | ||||
|     if (collabAPI?.isCollaborating()) { | ||||
|       collabAPI.syncElements(elements); | ||||
|     } | ||||
|  | ||||
|     setTheme(appState.theme); | ||||
|  | ||||
|     // this check is redundant, but since this is a hot path, it's best | ||||
|     // not to evaludate the nested expression every time | ||||
|     if (!LocalData.isSavePaused()) { | ||||
|       LocalData.save(elements, appState, files, () => { | ||||
|         if (excalidrawAPI) { | ||||
|           let didChange = false; | ||||
|  | ||||
|           const elements = excalidrawAPI | ||||
|             .getSceneElementsIncludingDeleted() | ||||
|             .map((element) => { | ||||
|               if ( | ||||
|                 LocalData.fileStorage.shouldUpdateImageElementStatus(element) | ||||
|               ) { | ||||
|                 const newElement = newElementWith(element, { status: "saved" }); | ||||
|                 if (newElement !== element) { | ||||
|                   didChange = true; | ||||
|                 } | ||||
|                 return newElement; | ||||
|               } | ||||
|               return element; | ||||
|             }); | ||||
|  | ||||
|           if (didChange) { | ||||
|             excalidrawAPI.updateScene({ | ||||
|               elements, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const [latestShareableLink, setLatestShareableLink] = useState<string | null>( | ||||
|     null, | ||||
|   ); | ||||
|  | ||||
|   const onExportToBackend = async ( | ||||
|     exportedElements: readonly NonDeletedExcalidrawElement[], | ||||
|     appState: Partial<AppState>, | ||||
|     files: BinaryFiles, | ||||
|   ) => { | ||||
|     if (exportedElements.length === 0) { | ||||
|       throw new Error(t("alerts.cannotExportEmptyCanvas")); | ||||
|     } | ||||
|     try { | ||||
|       const { url, errorMessage } = await exportToBackend( | ||||
|         exportedElements, | ||||
|         { | ||||
|           ...appState, | ||||
|           viewBackgroundColor: appState.exportBackground | ||||
|             ? appState.viewBackgroundColor | ||||
|             : getDefaultAppState().viewBackgroundColor, | ||||
|         }, | ||||
|         files, | ||||
|       ); | ||||
|  | ||||
|       if (errorMessage) { | ||||
|         throw new Error(errorMessage); | ||||
|       } | ||||
|  | ||||
|       if (url) { | ||||
|         setLatestShareableLink(url); | ||||
|       } | ||||
|     } catch (error: any) { | ||||
|       if (error.name !== "AbortError") { | ||||
|         const { width, height } = appState; | ||||
|         console.error(error, { | ||||
|           width, | ||||
|           height, | ||||
|           devicePixelRatio: window.devicePixelRatio, | ||||
|         }); | ||||
|         throw new Error(error.message); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const renderCustomStats = ( | ||||
|     elements: readonly NonDeletedExcalidrawElement[], | ||||
|     appState: UIAppState, | ||||
|   ) => { | ||||
|     return ( | ||||
|       <CustomStats | ||||
|         setToast={(message) => excalidrawAPI!.setToast({ message })} | ||||
|         appState={appState} | ||||
|         elements={elements} | ||||
|       /> | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   const onLibraryChange = async (items: LibraryItems) => { | ||||
|     if (!items.length) { | ||||
|       localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); | ||||
|       return; | ||||
|     } | ||||
|     const serializedItems = JSON.stringify(items); | ||||
|     localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); | ||||
|   }; | ||||
|  | ||||
|   const isOffline = useAtomValue(isOfflineAtom); | ||||
|  | ||||
|   const onCollabDialogOpen = useCallback( | ||||
|     () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }), | ||||
|     [setShareDialogState], | ||||
|   ); | ||||
|  | ||||
|   // browsers generally prevent infinite self-embedding, there are | ||||
|   // cases where it still happens, and while we disallow self-embedding | ||||
|   // by not whitelisting our own origin, this serves as an additional guard | ||||
|   if (isSelfEmbedding) { | ||||
|     return ( | ||||
|       <div | ||||
|         style={{ | ||||
|           display: "flex", | ||||
|           alignItems: "center", | ||||
|           justifyContent: "center", | ||||
|           textAlign: "center", | ||||
|           height: "100%", | ||||
|         }} | ||||
|       > | ||||
|         <h1>I'm not a pretzel!</h1> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ height: "100%" }} | ||||
|       className={clsx("excalidraw-app", { | ||||
|         "is-collaborating": isCollaborating, | ||||
|       })} | ||||
|     > | ||||
|       <Excalidraw | ||||
|         excalidrawAPI={excalidrawRefCallback} | ||||
|         onChange={onChange} | ||||
|         initialData={initialStatePromiseRef.current.promise} | ||||
|         isCollaborating={isCollaborating} | ||||
|         onPointerUpdate={collabAPI?.onPointerUpdate} | ||||
|         UIOptions={{ | ||||
|           canvasActions: { | ||||
|             toggleTheme: true, | ||||
|             export: { | ||||
|               onExportToBackend, | ||||
|               renderCustomUI: (elements, appState, files) => { | ||||
|                 return ( | ||||
|                   <ExportToExcalidrawPlus | ||||
|                     elements={elements} | ||||
|                     appState={appState} | ||||
|                     files={files} | ||||
|                     onError={(error) => { | ||||
|                       excalidrawAPI?.updateScene({ | ||||
|                         appState: { | ||||
|                           errorMessage: error.message, | ||||
|                         }, | ||||
|                       }); | ||||
|                     }} | ||||
|                     onSuccess={() => { | ||||
|                       excalidrawAPI?.updateScene({ | ||||
|                         appState: { openDialog: null }, | ||||
|                       }); | ||||
|                     }} | ||||
|                   /> | ||||
|                 ); | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }} | ||||
|         langCode={langCode} | ||||
|         renderCustomStats={renderCustomStats} | ||||
|         detectScroll={false} | ||||
|         handleKeyboardGlobally={true} | ||||
|         onLibraryChange={onLibraryChange} | ||||
|         autoFocus={true} | ||||
|         theme={theme} | ||||
|         renderTopRightUI={(isMobile) => { | ||||
|           if (isMobile || !collabAPI || isCollabDisabled) { | ||||
|             return null; | ||||
|           } | ||||
|           return ( | ||||
|             <LiveCollaborationTrigger | ||||
|               isCollaborating={isCollaborating} | ||||
|               onSelect={() => | ||||
|                 setShareDialogState({ isOpen: true, type: "share" }) | ||||
|               } | ||||
|             /> | ||||
|           ); | ||||
|         }} | ||||
|       > | ||||
|         <AppMainMenu | ||||
|           onCollabDialogOpen={onCollabDialogOpen} | ||||
|           isCollaborating={isCollaborating} | ||||
|           isCollabEnabled={!isCollabDisabled} | ||||
|         /> | ||||
|         <AppWelcomeScreen | ||||
|           onCollabDialogOpen={onCollabDialogOpen} | ||||
|           isCollabEnabled={!isCollabDisabled} | ||||
|         /> | ||||
|         <OverwriteConfirmDialog> | ||||
|           <OverwriteConfirmDialog.Actions.ExportToImage /> | ||||
|           <OverwriteConfirmDialog.Actions.SaveToDisk /> | ||||
|           {excalidrawAPI && ( | ||||
|             <OverwriteConfirmDialog.Action | ||||
|               title={t("overwriteConfirm.action.excalidrawPlus.title")} | ||||
|               actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")} | ||||
|               onClick={() => { | ||||
|                 exportToExcalidrawPlus( | ||||
|                   excalidrawAPI.getSceneElements(), | ||||
|                   excalidrawAPI.getAppState(), | ||||
|                   excalidrawAPI.getFiles(), | ||||
|                 ); | ||||
|               }} | ||||
|             > | ||||
|               {t("overwriteConfirm.action.excalidrawPlus.description")} | ||||
|             </OverwriteConfirmDialog.Action> | ||||
|           )} | ||||
|         </OverwriteConfirmDialog> | ||||
|         <AppFooter /> | ||||
|         <TTDDialog | ||||
|           onTextSubmit={async (input) => { | ||||
|             try { | ||||
|               const response = await fetch( | ||||
|                 `${ | ||||
|                   import.meta.env.VITE_APP_AI_BACKEND | ||||
|                 }/v1/ai/text-to-diagram/generate`, | ||||
|                 { | ||||
|                   method: "POST", | ||||
|                   headers: { | ||||
|                     Accept: "application/json", | ||||
|                     "Content-Type": "application/json", | ||||
|                   }, | ||||
|                   body: JSON.stringify({ prompt: input }), | ||||
|                 }, | ||||
|               ); | ||||
|  | ||||
|               const rateLimit = response.headers.has("X-Ratelimit-Limit") | ||||
|                 ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10) | ||||
|                 : undefined; | ||||
|  | ||||
|               const rateLimitRemaining = response.headers.has( | ||||
|                 "X-Ratelimit-Remaining", | ||||
|               ) | ||||
|                 ? parseInt( | ||||
|                     response.headers.get("X-Ratelimit-Remaining") || "0", | ||||
|                     10, | ||||
|                   ) | ||||
|                 : undefined; | ||||
|  | ||||
|               const json = await response.json(); | ||||
|  | ||||
|               if (!response.ok) { | ||||
|                 if (response.status === 429) { | ||||
|                   return { | ||||
|                     rateLimit, | ||||
|                     rateLimitRemaining, | ||||
|                     error: new Error( | ||||
|                       "Too many requests today, please try again tomorrow!", | ||||
|                     ), | ||||
|                   }; | ||||
|                 } | ||||
|  | ||||
|                 throw new Error(json.message || "Generation failed..."); | ||||
|               } | ||||
|  | ||||
|               const generatedResponse = json.generatedResponse; | ||||
|               if (!generatedResponse) { | ||||
|                 throw new Error("Generation failed..."); | ||||
|               } | ||||
|  | ||||
|               return { generatedResponse, rateLimit, rateLimitRemaining }; | ||||
|             } catch (err: any) { | ||||
|               throw new Error("Request failed"); | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|         <TTDDialogTrigger /> | ||||
|         {isCollaborating && isOffline && ( | ||||
|           <div className="collab-offline-warning"> | ||||
|             {t("alerts.collabOfflineWarning")} | ||||
|           </div> | ||||
|         )} | ||||
|         {latestShareableLink && ( | ||||
|           <ShareableLinkDialog | ||||
|             link={latestShareableLink} | ||||
|             onCloseRequest={() => setLatestShareableLink(null)} | ||||
|             setErrorMessage={setErrorMessage} | ||||
|           /> | ||||
|         )} | ||||
|         {excalidrawAPI && !isCollabDisabled && ( | ||||
|           <Collab excalidrawAPI={excalidrawAPI} /> | ||||
|         )} | ||||
|  | ||||
|         <ShareDialog | ||||
|           collabAPI={collabAPI} | ||||
|           onExportToBackend={async () => { | ||||
|             if (excalidrawAPI) { | ||||
|               try { | ||||
|                 await onExportToBackend( | ||||
|                   excalidrawAPI.getSceneElements(), | ||||
|                   excalidrawAPI.getAppState(), | ||||
|                   excalidrawAPI.getFiles(), | ||||
|                 ); | ||||
|               } catch (error: any) { | ||||
|                 setErrorMessage(error.message); | ||||
|               } | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|  | ||||
|         {errorMessage && ( | ||||
|           <ErrorDialog onClose={() => setErrorMessage("")}> | ||||
|             {errorMessage} | ||||
|           </ErrorDialog> | ||||
|         )} | ||||
|       </Excalidraw> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const ExcalidrawApp = () => { | ||||
|   return ( | ||||
|     <TopErrorBoundary> | ||||
|       <Provider unstable_createStore={() => appJotaiStore}> | ||||
|         <ExcalidrawWrapper /> | ||||
|       </Provider> | ||||
|     </TopErrorBoundary> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ExcalidrawApp; | ||||
| @@ -1,14 +1,14 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| import { debounce, getVersion, nFormatter } from "../src/utils"; | ||||
| import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils"; | ||||
| import { | ||||
|   getElementsStorageSize, | ||||
|   getTotalStorageSize, | ||||
| } from "./data/localStorage"; | ||||
| import { DEFAULT_VERSION } from "../src/constants"; | ||||
| import { t } from "../src/i18n"; | ||||
| import { copyTextToSystemClipboard } from "../src/clipboard"; | ||||
| import { NonDeletedExcalidrawElement } from "../src/element/types"; | ||||
| import { UIAppState } from "../src/types"; | ||||
| import { DEFAULT_VERSION } from "../packages/excalidraw/constants"; | ||||
| import { t } from "../packages/excalidraw/i18n"; | ||||
| import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard"; | ||||
| import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types"; | ||||
| import { UIAppState } from "../packages/excalidraw/types"; | ||||
|  | ||||
| type StorageSizes = { scene: number; total: number }; | ||||
|  | ||||
|   | ||||
| @@ -15,11 +15,17 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000; | ||||
| export const WS_EVENTS = { | ||||
|   SERVER_VOLATILE: "server-volatile-broadcast", | ||||
|   SERVER: "server-broadcast", | ||||
| }; | ||||
|   USER_FOLLOW_CHANGE: "user-follow", | ||||
|   USER_FOLLOW_ROOM_CHANGE: "user-follow-room-change", | ||||
| } as const; | ||||
|  | ||||
| export enum WS_SCENE_EVENT_TYPES { | ||||
| export enum WS_SUBTYPES { | ||||
|   INVALID_RESPONSE = "INVALID_RESPONSE", | ||||
|   INIT = "SCENE_INIT", | ||||
|   UPDATE = "SCENE_UPDATE", | ||||
|   MOUSE_LOCATION = "MOUSE_LOCATION", | ||||
|   IDLE_STATUS = "IDLE_STATUS", | ||||
|   USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS", | ||||
| } | ||||
|  | ||||
| export const FIREBASE_STORAGE_PREFIXES = { | ||||
|   | ||||
| @@ -1,36 +1,41 @@ | ||||
| import throttle from "lodash.throttle"; | ||||
| import { PureComponent } from "react"; | ||||
| import { ExcalidrawImperativeAPI } from "../../src/types"; | ||||
| import { ErrorDialog } from "../../src/components/ErrorDialog"; | ||||
| import { APP_NAME, ENV, EVENT } from "../../src/constants"; | ||||
| import { ImportedDataState } from "../../src/data/types"; | ||||
| import { | ||||
|   ExcalidrawImperativeAPI, | ||||
|   SocketId, | ||||
| } from "../../packages/excalidraw/types"; | ||||
| import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog"; | ||||
| import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants"; | ||||
| import { ImportedDataState } from "../../packages/excalidraw/data/types"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   InitializedExcalidrawImageElement, | ||||
| } from "../../src/element/types"; | ||||
| } from "../../packages/excalidraw/element/types"; | ||||
| import { | ||||
|   getSceneVersion, | ||||
|   restoreElements, | ||||
| } from "../../src/packages/excalidraw/index"; | ||||
| import { Collaborator, Gesture } from "../../src/types"; | ||||
|   zoomToFitBounds, | ||||
| } from "../../packages/excalidraw/index"; | ||||
| import { Collaborator, Gesture } from "../../packages/excalidraw/types"; | ||||
| import { | ||||
|   assertNever, | ||||
|   preventUnload, | ||||
|   resolvablePromise, | ||||
|   withBatchedUpdates, | ||||
| } from "../../src/utils"; | ||||
|   throttleRAF, | ||||
| } from "../../packages/excalidraw/utils"; | ||||
| import { | ||||
|   CURSOR_SYNC_TIMEOUT, | ||||
|   FILE_UPLOAD_MAX_BYTES, | ||||
|   FIREBASE_STORAGE_PREFIXES, | ||||
|   INITIAL_SCENE_UPDATE_TIMEOUT, | ||||
|   LOAD_IMAGES_TIMEOUT, | ||||
|   WS_SCENE_EVENT_TYPES, | ||||
|   WS_SUBTYPES, | ||||
|   SYNC_FULL_SCENE_INTERVAL_MS, | ||||
|   WS_EVENTS, | ||||
| } from "../app_constants"; | ||||
| import { | ||||
|   generateCollaborationLinkData, | ||||
|   getCollaborationLink, | ||||
|   getCollabServer, | ||||
|   getSyncableElements, | ||||
|   SocketUpdateDataSource, | ||||
|   SyncableExcalidrawElement, | ||||
| @@ -47,42 +52,48 @@ import { | ||||
|   saveUsernameToLocalStorage, | ||||
| } from "../data/localStorage"; | ||||
| import Portal from "./Portal"; | ||||
| import RoomDialog from "./RoomDialog"; | ||||
| import { t } from "../../src/i18n"; | ||||
| import { UserIdleState } from "../../src/types"; | ||||
| import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants"; | ||||
| import { t } from "../../packages/excalidraw/i18n"; | ||||
| import { UserIdleState } from "../../packages/excalidraw/types"; | ||||
| import { | ||||
|   IDLE_THRESHOLD, | ||||
|   ACTIVE_THRESHOLD, | ||||
| } from "../../packages/excalidraw/constants"; | ||||
| import { | ||||
|   encodeFilesForUpload, | ||||
|   FileManager, | ||||
|   updateStaleImageStatuses, | ||||
| } from "../data/FileManager"; | ||||
| import { AbortError } from "../../src/errors"; | ||||
| import { AbortError } from "../../packages/excalidraw/errors"; | ||||
| import { | ||||
|   isImageElement, | ||||
|   isInitializedImageElement, | ||||
| } from "../../src/element/typeChecks"; | ||||
| import { newElementWith } from "../../src/element/mutateElement"; | ||||
| } from "../../packages/excalidraw/element/typeChecks"; | ||||
| import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; | ||||
| import { | ||||
|   ReconciledElements, | ||||
|   reconcileElements as _reconcileElements, | ||||
| } from "./reconciliation"; | ||||
| import { decryptData } from "../../src/data/encryption"; | ||||
| import { decryptData } from "../../packages/excalidraw/data/encryption"; | ||||
| import { resetBrowserStateVersions } from "../data/tabSync"; | ||||
| import { LocalData } from "../data/LocalData"; | ||||
| import { atom, useAtom } from "jotai"; | ||||
| import { atom } from "jotai"; | ||||
| import { appJotaiStore } from "../app-jotai"; | ||||
| import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types"; | ||||
| import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds"; | ||||
| import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils"; | ||||
|  | ||||
| export const collabAPIAtom = atom<CollabAPI | null>(null); | ||||
| export const collabDialogShownAtom = atom(false); | ||||
| export const isCollaboratingAtom = atom(false); | ||||
| export const isOfflineAtom = atom(false); | ||||
|  | ||||
| interface CollabState { | ||||
|   errorMessage: string; | ||||
|   errorMessage: string | null; | ||||
|   username: string; | ||||
|   activeRoomLink: string; | ||||
|   activeRoomLink: string | null; | ||||
| } | ||||
|  | ||||
| export const activeRoomLinkAtom = atom<string | null>(null); | ||||
|  | ||||
| type CollabInstance = InstanceType<typeof Collab>; | ||||
|  | ||||
| export interface CollabAPI { | ||||
| @@ -93,32 +104,33 @@ export interface CollabAPI { | ||||
|   stopCollaboration: CollabInstance["stopCollaboration"]; | ||||
|   syncElements: CollabInstance["syncElements"]; | ||||
|   fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; | ||||
|   setUsername: (username: string) => void; | ||||
|   setUsername: CollabInstance["setUsername"]; | ||||
|   getUsername: CollabInstance["getUsername"]; | ||||
|   getActiveRoomLink: CollabInstance["getActiveRoomLink"]; | ||||
|   setErrorMessage: CollabInstance["setErrorMessage"]; | ||||
| } | ||||
|  | ||||
| interface PublicProps { | ||||
| interface CollabProps { | ||||
|   excalidrawAPI: ExcalidrawImperativeAPI; | ||||
| } | ||||
|  | ||||
| type Props = PublicProps & { modalIsShown: boolean }; | ||||
|  | ||||
| class Collab extends PureComponent<Props, CollabState> { | ||||
| class Collab extends PureComponent<CollabProps, CollabState> { | ||||
|   portal: Portal; | ||||
|   fileManager: FileManager; | ||||
|   excalidrawAPI: Props["excalidrawAPI"]; | ||||
|   excalidrawAPI: CollabProps["excalidrawAPI"]; | ||||
|   activeIntervalId: number | null; | ||||
|   idleTimeoutId: number | null; | ||||
|  | ||||
|   private socketInitializationTimer?: number; | ||||
|   private lastBroadcastedOrReceivedSceneVersion: number = -1; | ||||
|   private collaborators = new Map<string, Collaborator>(); | ||||
|   private collaborators = new Map<SocketId, Collaborator>(); | ||||
|  | ||||
|   constructor(props: Props) { | ||||
|   constructor(props: CollabProps) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|       errorMessage: "", | ||||
|       errorMessage: null, | ||||
|       username: importUsernameFromLocalStorage() || "", | ||||
|       activeRoomLink: "", | ||||
|       activeRoomLink: null, | ||||
|     }; | ||||
|     this.portal = new Portal(this); | ||||
|     this.fileManager = new FileManager({ | ||||
| @@ -151,12 +163,28 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     this.idleTimeoutId = null; | ||||
|   } | ||||
|  | ||||
|   private onUmmount: (() => void) | null = null; | ||||
|  | ||||
|   componentDidMount() { | ||||
|     window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); | ||||
|     window.addEventListener("online", this.onOfflineStatusToggle); | ||||
|     window.addEventListener("offline", this.onOfflineStatusToggle); | ||||
|     window.addEventListener(EVENT.UNLOAD, this.onUnload); | ||||
|  | ||||
|     const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => { | ||||
|       this.portal.socket && this.portal.broadcastUserFollowed(payload); | ||||
|     }); | ||||
|     const throttledRelayUserViewportBounds = throttleRAF( | ||||
|       this.relayVisibleSceneBounds, | ||||
|     ); | ||||
|     const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() => | ||||
|       throttledRelayUserViewportBounds(), | ||||
|     ); | ||||
|     this.onUmmount = () => { | ||||
|       unsubOnUserFollow(); | ||||
|       unsubOnScrollChange(); | ||||
|     }; | ||||
|  | ||||
|     this.onOfflineStatusToggle(); | ||||
|  | ||||
|     const collabAPI: CollabAPI = { | ||||
| @@ -167,6 +195,9 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|       fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, | ||||
|       stopCollaboration: this.stopCollaboration, | ||||
|       setUsername: this.setUsername, | ||||
|       getUsername: this.getUsername, | ||||
|       getActiveRoomLink: this.getActiveRoomLink, | ||||
|       setErrorMessage: this.setErrorMessage, | ||||
|     }; | ||||
|  | ||||
|     appJotaiStore.set(collabAPIAtom, collabAPI); | ||||
| @@ -204,6 +235,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|       window.clearTimeout(this.idleTimeoutId); | ||||
|       this.idleTimeoutId = null; | ||||
|     } | ||||
|     this.onUmmount?.(); | ||||
|   } | ||||
|  | ||||
|   isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; | ||||
| @@ -313,9 +345,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     this.fileManager.reset(); | ||||
|     if (!opts?.isUnload) { | ||||
|       this.setIsCollaborating(false); | ||||
|       this.setState({ | ||||
|         activeRoomLink: "", | ||||
|       }); | ||||
|       this.setActiveRoomLink(null); | ||||
|       this.collaborators = new Map(); | ||||
|       this.excalidrawAPI.updateScene({ | ||||
|         collaborators: this.collaborators, | ||||
| @@ -356,7 +386,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     iv: Uint8Array, | ||||
|     encryptedData: ArrayBuffer, | ||||
|     decryptionKey: string, | ||||
|   ) => { | ||||
|   ): Promise<ValueOf<SocketUpdateDataSource>> => { | ||||
|     try { | ||||
|       const decrypted = await decryptData(iv, encryptedData, decryptionKey); | ||||
|  | ||||
| @@ -368,7 +398,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|       window.alert(t("alerts.decryptFailed")); | ||||
|       console.error(error); | ||||
|       return { | ||||
|         type: "INVALID_RESPONSE", | ||||
|         type: WS_SUBTYPES.INVALID_RESPONSE, | ||||
|       }; | ||||
|     } | ||||
|   }; | ||||
| @@ -381,7 +411,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     if (!this.state.username) { | ||||
|       import("@excalidraw/random-username").then(({ getRandomUsername }) => { | ||||
|         const username = getRandomUsername(); | ||||
|         this.onUsernameChange(username); | ||||
|         this.setUsername(username); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
| @@ -423,13 +453,9 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     this.fallbackInitializationHandler = fallbackInitializationHandler; | ||||
|  | ||||
|     try { | ||||
|       const socketServerData = await getCollabServer(); | ||||
|  | ||||
|       this.portal.socket = this.portal.open( | ||||
|         socketIOClient(socketServerData.url, { | ||||
|           transports: socketServerData.polling | ||||
|             ? ["websocket", "polling"] | ||||
|             : ["websocket"], | ||||
|         socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, { | ||||
|           transports: ["websocket", "polling"], | ||||
|         }), | ||||
|         roomId, | ||||
|         roomKey, | ||||
| @@ -484,9 +510,9 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|         ); | ||||
|  | ||||
|         switch (decryptedData.type) { | ||||
|           case "INVALID_RESPONSE": | ||||
|           case WS_SUBTYPES.INVALID_RESPONSE: | ||||
|             return; | ||||
|           case WS_SCENE_EVENT_TYPES.INIT: { | ||||
|           case WS_SUBTYPES.INIT: { | ||||
|             if (!this.portal.socketInitialized) { | ||||
|               this.initializeRoom({ fetchScene: false }); | ||||
|               const remoteElements = decryptedData.payload.elements; | ||||
| @@ -502,41 +528,75 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|             } | ||||
|             break; | ||||
|           } | ||||
|           case WS_SCENE_EVENT_TYPES.UPDATE: | ||||
|           case WS_SUBTYPES.UPDATE: | ||||
|             this.handleRemoteSceneUpdate( | ||||
|               this.reconcileElements(decryptedData.payload.elements), | ||||
|             ); | ||||
|             break; | ||||
|           case "MOUSE_LOCATION": { | ||||
|           case WS_SUBTYPES.MOUSE_LOCATION: { | ||||
|             const { pointer, button, username, selectedElementIds } = | ||||
|               decryptedData.payload; | ||||
|  | ||||
|             const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] = | ||||
|               decryptedData.payload.socketId || | ||||
|               // @ts-ignore legacy, see #2094 (#2097) | ||||
|               decryptedData.payload.socketID; | ||||
|  | ||||
|             const collaborators = new Map(this.collaborators); | ||||
|             const user = collaborators.get(socketId) || {}!; | ||||
|             user.pointer = pointer; | ||||
|             user.button = button; | ||||
|             user.selectedElementIds = selectedElementIds; | ||||
|             user.username = username; | ||||
|             collaborators.set(socketId, user); | ||||
|             this.updateCollaborator(socketId, { | ||||
|               pointer, | ||||
|               button, | ||||
|               selectedElementIds, | ||||
|               username, | ||||
|             }); | ||||
|  | ||||
|             break; | ||||
|           } | ||||
|  | ||||
|           case WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS: { | ||||
|             const { sceneBounds, socketId } = decryptedData.payload; | ||||
|  | ||||
|             const appState = this.excalidrawAPI.getAppState(); | ||||
|  | ||||
|             // we're not following the user | ||||
|             // (shouldn't happen, but could be late message or bug upstream) | ||||
|             if (appState.userToFollow?.socketId !== socketId) { | ||||
|               console.warn( | ||||
|                 `receiving remote client's (from ${socketId}) viewport bounds even though we're not subscribed to it!`, | ||||
|               ); | ||||
|               return; | ||||
|             } | ||||
|  | ||||
|             // cross-follow case, ignore updates in this case | ||||
|             if ( | ||||
|               appState.userToFollow && | ||||
|               appState.followedBy.has(appState.userToFollow.socketId) | ||||
|             ) { | ||||
|               return; | ||||
|             } | ||||
|  | ||||
|             this.excalidrawAPI.updateScene({ | ||||
|               collaborators, | ||||
|               appState: zoomToFitBounds({ | ||||
|                 appState, | ||||
|                 bounds: sceneBounds, | ||||
|                 fitToViewport: true, | ||||
|                 viewportZoomFactor: 1, | ||||
|               }).appState, | ||||
|             }); | ||||
|  | ||||
|             break; | ||||
|           } | ||||
|  | ||||
|           case WS_SUBTYPES.IDLE_STATUS: { | ||||
|             const { userState, socketId, username } = decryptedData.payload; | ||||
|             this.updateCollaborator(socketId, { | ||||
|               userState, | ||||
|               username, | ||||
|             }); | ||||
|             break; | ||||
|           } | ||||
|           case "IDLE_STATUS": { | ||||
|             const { userState, socketId, username } = decryptedData.payload; | ||||
|             const collaborators = new Map(this.collaborators); | ||||
|             const user = collaborators.get(socketId) || {}!; | ||||
|             user.userState = userState; | ||||
|             user.username = username; | ||||
|             this.excalidrawAPI.updateScene({ | ||||
|               collaborators, | ||||
|             }); | ||||
|             break; | ||||
|  | ||||
|           default: { | ||||
|             assertNever(decryptedData, null); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -553,11 +613,20 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|       scenePromise.resolve(sceneData); | ||||
|     }); | ||||
|  | ||||
|     this.portal.socket.on( | ||||
|       WS_EVENTS.USER_FOLLOW_ROOM_CHANGE, | ||||
|       (followedBy: SocketId[]) => { | ||||
|         this.excalidrawAPI.updateScene({ | ||||
|           appState: { followedBy: new Set(followedBy) }, | ||||
|         }); | ||||
|  | ||||
|         this.relayVisibleSceneBounds({ force: true }); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     this.initializeIdleDetector(); | ||||
|  | ||||
|     this.setState({ | ||||
|       activeRoomLink: window.location.href, | ||||
|     }); | ||||
|     this.setActiveRoomLink(window.location.href); | ||||
|  | ||||
|     return scenePromise; | ||||
|   }; | ||||
| @@ -721,20 +790,39 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange); | ||||
|   }; | ||||
|  | ||||
|   setCollaborators(sockets: string[]) { | ||||
|   setCollaborators(sockets: SocketId[]) { | ||||
|     const collaborators: InstanceType<typeof Collab>["collaborators"] = | ||||
|       new Map(); | ||||
|     for (const socketId of sockets) { | ||||
|       if (this.collaborators.has(socketId)) { | ||||
|         collaborators.set(socketId, this.collaborators.get(socketId)!); | ||||
|       } else { | ||||
|         collaborators.set(socketId, {}); | ||||
|       } | ||||
|       collaborators.set( | ||||
|         socketId, | ||||
|         Object.assign({}, this.collaborators.get(socketId), { | ||||
|           isCurrentUser: socketId === this.portal.socket?.id, | ||||
|         }), | ||||
|       ); | ||||
|     } | ||||
|     this.collaborators = collaborators; | ||||
|     this.excalidrawAPI.updateScene({ collaborators }); | ||||
|   } | ||||
|  | ||||
|   updateCollaborator = (socketId: SocketId, updates: Partial<Collaborator>) => { | ||||
|     const collaborators = new Map(this.collaborators); | ||||
|     const user: Mutable<Collaborator> = Object.assign( | ||||
|       {}, | ||||
|       collaborators.get(socketId), | ||||
|       updates, | ||||
|       { | ||||
|         isCurrentUser: socketId === this.portal.socket?.id, | ||||
|       }, | ||||
|     ); | ||||
|     collaborators.set(socketId, user); | ||||
|     this.collaborators = collaborators; | ||||
|  | ||||
|     this.excalidrawAPI.updateScene({ | ||||
|       collaborators, | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { | ||||
|     this.lastBroadcastedOrReceivedSceneVersion = version; | ||||
|   }; | ||||
| @@ -760,6 +848,19 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     CURSOR_SYNC_TIMEOUT, | ||||
|   ); | ||||
|  | ||||
|   relayVisibleSceneBounds = (props?: { force: boolean }) => { | ||||
|     const appState = this.excalidrawAPI.getAppState(); | ||||
|  | ||||
|     if (this.portal.socket && (appState.followedBy.size > 0 || props?.force)) { | ||||
|       this.portal.broadcastVisibleSceneBounds( | ||||
|         { | ||||
|           sceneBounds: getVisibleSceneBounds(appState), | ||||
|         }, | ||||
|         `follow@${this.portal.socket.id}`, | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   onIdleStateChange = (userState: UserIdleState) => { | ||||
|     this.portal.broadcastIdleChange(userState); | ||||
|   }; | ||||
| @@ -769,7 +870,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|       getSceneVersion(elements) > | ||||
|       this.getLastBroadcastedOrReceivedSceneVersion() | ||||
|     ) { | ||||
|       this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false); | ||||
|       this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false); | ||||
|       this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements); | ||||
|       this.queueBroadcastAllElements(); | ||||
|     } | ||||
| @@ -782,7 +883,7 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|  | ||||
|   queueBroadcastAllElements = throttle(() => { | ||||
|     this.portal.broadcastScene( | ||||
|       WS_SCENE_EVENT_TYPES.UPDATE, | ||||
|       WS_SUBTYPES.UPDATE, | ||||
|       this.excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|       true, | ||||
|     ); | ||||
| @@ -808,41 +909,31 @@ class Collab extends PureComponent<Props, CollabState> { | ||||
|     { leading: false }, | ||||
|   ); | ||||
|  | ||||
|   handleClose = () => { | ||||
|     appJotaiStore.set(collabDialogShownAtom, false); | ||||
|   }; | ||||
|  | ||||
|   setUsername = (username: string) => { | ||||
|     this.setState({ username }); | ||||
|   }; | ||||
|  | ||||
|   onUsernameChange = (username: string) => { | ||||
|     this.setUsername(username); | ||||
|     saveUsernameToLocalStorage(username); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { username, errorMessage, activeRoomLink } = this.state; | ||||
|   getUsername = () => this.state.username; | ||||
|  | ||||
|     const { modalIsShown } = this.props; | ||||
|   setActiveRoomLink = (activeRoomLink: string | null) => { | ||||
|     this.setState({ activeRoomLink }); | ||||
|     appJotaiStore.set(activeRoomLinkAtom, activeRoomLink); | ||||
|   }; | ||||
|  | ||||
|   getActiveRoomLink = () => this.state.activeRoomLink; | ||||
|  | ||||
|   setErrorMessage = (errorMessage: string | null) => { | ||||
|     this.setState({ errorMessage }); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { errorMessage } = this.state; | ||||
|  | ||||
|     return ( | ||||
|       <> | ||||
|         {modalIsShown && ( | ||||
|           <RoomDialog | ||||
|             handleClose={this.handleClose} | ||||
|             activeRoomLink={activeRoomLink} | ||||
|             username={username} | ||||
|             onUsernameChange={this.onUsernameChange} | ||||
|             onRoomCreate={() => this.startCollaboration(null)} | ||||
|             onRoomDestroy={this.stopCollaboration} | ||||
|             setErrorMessage={(errorMessage) => { | ||||
|               this.setState({ errorMessage }); | ||||
|             }} | ||||
|           /> | ||||
|         )} | ||||
|         {errorMessage && ( | ||||
|           <ErrorDialog onClose={() => this.setState({ errorMessage: "" })}> | ||||
|         {errorMessage != null && ( | ||||
|           <ErrorDialog onClose={() => this.setState({ errorMessage: null })}> | ||||
|             {errorMessage} | ||||
|           </ErrorDialog> | ||||
|         )} | ||||
| @@ -861,11 +952,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { | ||||
|   window.collab = window.collab || ({} as Window["collab"]); | ||||
| } | ||||
|  | ||||
| const _Collab: React.FC<PublicProps> = (props) => { | ||||
|   const [collabDialogShown] = useAtom(collabDialogShownAtom); | ||||
|   return <Collab {...props} modalIsShown={collabDialogShown} />; | ||||
| }; | ||||
|  | ||||
| export default _Collab; | ||||
| export default Collab; | ||||
|  | ||||
| export type TCollabClass = Collab; | ||||
|   | ||||
| @@ -6,23 +6,24 @@ import { | ||||
|  | ||||
| import { TCollabClass } from "./Collab"; | ||||
|  | ||||
| import { ExcalidrawElement } from "../../src/element/types"; | ||||
| import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; | ||||
| import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants"; | ||||
| import { | ||||
|   WS_EVENTS, | ||||
|   FILE_UPLOAD_TIMEOUT, | ||||
|   WS_SCENE_EVENT_TYPES, | ||||
| } from "../app_constants"; | ||||
| import { UserIdleState } from "../../src/types"; | ||||
| import { trackEvent } from "../../src/analytics"; | ||||
|   OnUserFollowedPayload, | ||||
|   SocketId, | ||||
|   UserIdleState, | ||||
| } from "../../packages/excalidraw/types"; | ||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | ||||
| import throttle from "lodash.throttle"; | ||||
| import { newElementWith } from "../../src/element/mutateElement"; | ||||
| import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; | ||||
| import { BroadcastedExcalidrawElement } from "./reconciliation"; | ||||
| import { encryptData } from "../../src/data/encryption"; | ||||
| import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; | ||||
| import { encryptData } from "../../packages/excalidraw/data/encryption"; | ||||
| import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; | ||||
| import type { Socket } from "socket.io-client"; | ||||
|  | ||||
| class Portal { | ||||
|   collab: TCollabClass; | ||||
|   socket: SocketIOClient.Socket | null = null; | ||||
|   socket: Socket | null = null; | ||||
|   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized | ||||
|   roomId: string | null = null; | ||||
|   roomKey: string | null = null; | ||||
| @@ -32,7 +33,7 @@ class Portal { | ||||
|     this.collab = collab; | ||||
|   } | ||||
|  | ||||
|   open(socket: SocketIOClient.Socket, id: string, key: string) { | ||||
|   open(socket: Socket, id: string, key: string) { | ||||
|     this.socket = socket; | ||||
|     this.roomId = id; | ||||
|     this.roomKey = key; | ||||
| @@ -46,12 +47,12 @@ class Portal { | ||||
|     }); | ||||
|     this.socket.on("new-user", async (_socketId: string) => { | ||||
|       this.broadcastScene( | ||||
|         WS_SCENE_EVENT_TYPES.INIT, | ||||
|         WS_SUBTYPES.INIT, | ||||
|         this.collab.getSceneElementsIncludingDeleted(), | ||||
|         /* syncAll */ true, | ||||
|       ); | ||||
|     }); | ||||
|     this.socket.on("room-user-change", (clients: string[]) => { | ||||
|     this.socket.on("room-user-change", (clients: SocketId[]) => { | ||||
|       this.collab.setCollaborators(clients); | ||||
|     }); | ||||
|  | ||||
| @@ -83,6 +84,7 @@ class Portal { | ||||
|   async _broadcastSocketData( | ||||
|     data: SocketUpdateData, | ||||
|     volatile: boolean = false, | ||||
|     roomId?: string, | ||||
|   ) { | ||||
|     if (this.isOpen()) { | ||||
|       const json = JSON.stringify(data); | ||||
| @@ -91,7 +93,7 @@ class Portal { | ||||
|  | ||||
|       this.socket?.emit( | ||||
|         volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER, | ||||
|         this.roomId, | ||||
|         roomId ?? this.roomId, | ||||
|         encryptedBuffer, | ||||
|         iv, | ||||
|       ); | ||||
| @@ -130,11 +132,11 @@ class Portal { | ||||
|   }, FILE_UPLOAD_TIMEOUT); | ||||
|  | ||||
|   broadcastScene = async ( | ||||
|     updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE, | ||||
|     updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE, | ||||
|     allElements: readonly ExcalidrawElement[], | ||||
|     syncAll: boolean, | ||||
|   ) => { | ||||
|     if (updateType === WS_SCENE_EVENT_TYPES.INIT && !syncAll) { | ||||
|     if (updateType === WS_SUBTYPES.INIT && !syncAll) { | ||||
|       throw new Error("syncAll must be true when sending SCENE.INIT"); | ||||
|     } | ||||
|  | ||||
| @@ -183,9 +185,9 @@ class Portal { | ||||
|   broadcastIdleChange = (userState: UserIdleState) => { | ||||
|     if (this.socket?.id) { | ||||
|       const data: SocketUpdateDataSource["IDLE_STATUS"] = { | ||||
|         type: "IDLE_STATUS", | ||||
|         type: WS_SUBTYPES.IDLE_STATUS, | ||||
|         payload: { | ||||
|           socketId: this.socket.id, | ||||
|           socketId: this.socket.id as SocketId, | ||||
|           userState, | ||||
|           username: this.collab.state.username, | ||||
|         }, | ||||
| @@ -203,9 +205,9 @@ class Portal { | ||||
|   }) => { | ||||
|     if (this.socket?.id) { | ||||
|       const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { | ||||
|         type: "MOUSE_LOCATION", | ||||
|         type: WS_SUBTYPES.MOUSE_LOCATION, | ||||
|         payload: { | ||||
|           socketId: this.socket.id, | ||||
|           socketId: this.socket.id as SocketId, | ||||
|           pointer: payload.pointer, | ||||
|           button: payload.button || "up", | ||||
|           selectedElementIds: | ||||
| @@ -213,12 +215,43 @@ class Portal { | ||||
|           username: this.collab.state.username, | ||||
|         }, | ||||
|       }; | ||||
|  | ||||
|       return this._broadcastSocketData( | ||||
|         data as SocketUpdateData, | ||||
|         true, // volatile | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   broadcastVisibleSceneBounds = ( | ||||
|     payload: { | ||||
|       sceneBounds: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"]["payload"]["sceneBounds"]; | ||||
|     }, | ||||
|     roomId: string, | ||||
|   ) => { | ||||
|     if (this.socket?.id) { | ||||
|       const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = { | ||||
|         type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS, | ||||
|         payload: { | ||||
|           socketId: this.socket.id as SocketId, | ||||
|           username: this.collab.state.username, | ||||
|           sceneBounds: payload.sceneBounds, | ||||
|         }, | ||||
|       }; | ||||
|  | ||||
|       return this._broadcastSocketData( | ||||
|         data as SocketUpdateData, | ||||
|         true, // volatile | ||||
|         roomId, | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   broadcastUserFollowed = (payload: OnUserFollowedPayload) => { | ||||
|     if (this.socket?.id) { | ||||
|       this.socket.emit(WS_EVENTS.USER_FOLLOW_CHANGE, payload); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default Portal; | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { useRef, useState } from "react"; | ||||
| import * as Popover from "@radix-ui/react-popover"; | ||||
|  | ||||
| import { copyTextToSystemClipboard } from "../../src/clipboard"; | ||||
| import { trackEvent } from "../../src/analytics"; | ||||
| import { getFrame } from "../../src/utils"; | ||||
| import { useI18n } from "../../src/i18n"; | ||||
| import { KEYS } from "../../src/keys"; | ||||
| import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; | ||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | ||||
| import { getFrame } from "../../packages/excalidraw/utils"; | ||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | ||||
| import { KEYS } from "../../packages/excalidraw/keys"; | ||||
|  | ||||
| import { Dialog } from "../../src/components/Dialog"; | ||||
| import { Dialog } from "../../packages/excalidraw/components/Dialog"; | ||||
| import { | ||||
|   copyIcon, | ||||
|   playerPlayIcon, | ||||
| @@ -16,11 +16,11 @@ import { | ||||
|   shareIOS, | ||||
|   shareWindows, | ||||
|   tablerCheckIcon, | ||||
| } from "../../src/components/icons"; | ||||
| import { TextField } from "../../src/components/TextField"; | ||||
| import { FilledButton } from "../../src/components/FilledButton"; | ||||
| } from "../../packages/excalidraw/components/icons"; | ||||
| import { TextField } from "../../packages/excalidraw/components/TextField"; | ||||
| import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; | ||||
|  | ||||
| import { ReactComponent as CollabImage } from "../../src/assets/lock.svg"; | ||||
| import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg"; | ||||
| import "./RoomDialog.scss"; | ||||
|  | ||||
| const getShareIcon = () => { | ||||
| @@ -120,7 +120,7 @@ export const RoomModal = ({ | ||||
|               size="large" | ||||
|               variant="icon" | ||||
|               label="Share" | ||||
|               startIcon={getShareIcon()} | ||||
|               icon={getShareIcon()} | ||||
|               className="RoomDialog__active__share" | ||||
|               onClick={shareRoomLink} | ||||
|             /> | ||||
| @@ -130,7 +130,7 @@ export const RoomModal = ({ | ||||
|               <FilledButton | ||||
|                 size="large" | ||||
|                 label="Copy link" | ||||
|                 startIcon={copyIcon} | ||||
|                 icon={copyIcon} | ||||
|                 onClick={copyRoomLink} | ||||
|               /> | ||||
|             </Popover.Trigger> | ||||
| @@ -166,7 +166,7 @@ export const RoomModal = ({ | ||||
|             variant="outlined" | ||||
|             color="danger" | ||||
|             label={t("roomDialog.button_stopSession")} | ||||
|             startIcon={playerStopFilledIcon} | ||||
|             icon={playerStopFilledIcon} | ||||
|             onClick={() => { | ||||
|               trackEvent("share", "room closed"); | ||||
|               onRoomDestroy(); | ||||
| @@ -195,7 +195,7 @@ export const RoomModal = ({ | ||||
|         <FilledButton | ||||
|           size="large" | ||||
|           label={t("roomDialog.button_startSession")} | ||||
|           startIcon={playerPlayIcon} | ||||
|           icon={playerPlayIcon} | ||||
|           onClick={() => { | ||||
|             trackEvent("share", "room creation", `ui (${getFrame()})`); | ||||
|             onRoomCreate(); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; | ||||
| import { ExcalidrawElement } from "../../src/element/types"; | ||||
| import { AppState } from "../../src/types"; | ||||
| import { arrayToMapWithIndex } from "../../src/utils"; | ||||
| import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; | ||||
| import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; | ||||
| import { AppState } from "../../packages/excalidraw/types"; | ||||
| import { arrayToMapWithIndex } from "../../packages/excalidraw/utils"; | ||||
|  | ||||
| export type ReconciledElements = readonly ExcalidrawElement[] & { | ||||
|   _brand: "reconciledElements"; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import React from "react"; | ||||
| import { Footer } from "../../src/packages/excalidraw/index"; | ||||
| import { Footer } from "../../packages/excalidraw/index"; | ||||
| import { EncryptedIcon } from "./EncryptedIcon"; | ||||
| import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; | ||||
| import { isExcalidrawPlusSignedUser } from "../app_constants"; | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import React from "react"; | ||||
| import { PlusPromoIcon } from "../../src/components/icons"; | ||||
| import { MainMenu } from "../../src/packages/excalidraw/index"; | ||||
| import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; | ||||
| import { MainMenu } from "../../packages/excalidraw/index"; | ||||
| import { LanguageList } from "./LanguageList"; | ||||
|  | ||||
| export const AppMainMenu: React.FC<{ | ||||
|   setCollabDialogShown: (toggle: boolean) => any; | ||||
|   onCollabDialogOpen: () => any; | ||||
|   isCollaborating: boolean; | ||||
|   isCollabEnabled: boolean; | ||||
| }> = React.memo((props) => { | ||||
| @@ -17,7 +17,7 @@ export const AppMainMenu: React.FC<{ | ||||
|       {props.isCollabEnabled && ( | ||||
|         <MainMenu.DefaultItems.LiveCollaborationTrigger | ||||
|           isCollaborating={props.isCollaborating} | ||||
|           onSelect={() => props.setCollabDialogShown(true)} | ||||
|           onSelect={() => props.onCollabDialogOpen()} | ||||
|         /> | ||||
|       )} | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import React from "react"; | ||||
| import { PlusPromoIcon } from "../../src/components/icons"; | ||||
| import { useI18n } from "../../src/i18n"; | ||||
| import { WelcomeScreen } from "../../src/packages/excalidraw/index"; | ||||
| import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; | ||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | ||||
| import { WelcomeScreen } from "../../packages/excalidraw/index"; | ||||
| import { isExcalidrawPlusSignedUser } from "../app_constants"; | ||||
| import { POINTER_EVENTS } from "../../src/constants"; | ||||
| import { POINTER_EVENTS } from "../../packages/excalidraw/constants"; | ||||
|  | ||||
| export const AppWelcomeScreen: React.FC<{ | ||||
|   setCollabDialogShown: (toggle: boolean) => any; | ||||
|   onCollabDialogOpen: () => any; | ||||
|   isCollabEnabled: boolean; | ||||
| }> = React.memo((props) => { | ||||
|   const { t } = useI18n(); | ||||
| @@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{ | ||||
|           <WelcomeScreen.Center.MenuItemHelp /> | ||||
|           {props.isCollabEnabled && ( | ||||
|             <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger | ||||
|               onSelect={() => props.setCollabDialogShown(true)} | ||||
|               onSelect={() => props.onCollabDialogOpen()} | ||||
|             /> | ||||
|           )} | ||||
|           {!isExcalidrawPlusSignedUser && ( | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { shield } from "../../src/components/icons"; | ||||
| import { Tooltip } from "../../src/components/Tooltip"; | ||||
| import { useI18n } from "../../src/i18n"; | ||||
| import { shield } from "../../packages/excalidraw/components/icons"; | ||||
| import { Tooltip } from "../../packages/excalidraw/components/Tooltip"; | ||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | ||||
|  | ||||
| export const EncryptedIcon = () => { | ||||
|   const { t } = useI18n(); | ||||
|   | ||||
| @@ -1,20 +1,30 @@ | ||||
| import React from "react"; | ||||
| import { Card } from "../../src/components/Card"; | ||||
| import { ToolButton } from "../../src/components/ToolButton"; | ||||
| import { serializeAsJSON } from "../../src/data/json"; | ||||
| import { Card } from "../../packages/excalidraw/components/Card"; | ||||
| import { ToolButton } from "../../packages/excalidraw/components/ToolButton"; | ||||
| import { serializeAsJSON } from "../../packages/excalidraw/data/json"; | ||||
| import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; | ||||
| import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types"; | ||||
| import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; | ||||
| import { | ||||
|   FileId, | ||||
|   NonDeletedExcalidrawElement, | ||||
| } from "../../packages/excalidraw/element/types"; | ||||
| import { | ||||
|   AppState, | ||||
|   BinaryFileData, | ||||
|   BinaryFiles, | ||||
| } from "../../packages/excalidraw/types"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import { useI18n } from "../../src/i18n"; | ||||
| import { encryptData, generateEncryptionKey } from "../../src/data/encryption"; | ||||
| import { isInitializedImageElement } from "../../src/element/typeChecks"; | ||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | ||||
| import { | ||||
|   encryptData, | ||||
|   generateEncryptionKey, | ||||
| } from "../../packages/excalidraw/data/encryption"; | ||||
| import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; | ||||
| import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; | ||||
| import { encodeFilesForUpload } from "../data/FileManager"; | ||||
| import { MIME_TYPES } from "../../src/constants"; | ||||
| import { trackEvent } from "../../src/analytics"; | ||||
| import { getFrame } from "../../src/utils"; | ||||
| import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo"; | ||||
| import { MIME_TYPES } from "../../packages/excalidraw/constants"; | ||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | ||||
| import { getFrame } from "../../packages/excalidraw/utils"; | ||||
| import { ExcalidrawLogo } from "../../packages/excalidraw/components/ExcalidrawLogo"; | ||||
|  | ||||
| export const exportToExcalidrawPlus = async ( | ||||
|   elements: readonly NonDeletedExcalidrawElement[], | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import oc from "open-color"; | ||||
| import React from "react"; | ||||
| import { THEME } from "../../src/constants"; | ||||
| import { Theme } from "../../src/element/types"; | ||||
| import { THEME } from "../../packages/excalidraw/constants"; | ||||
| import { Theme } from "../../packages/excalidraw/element/types"; | ||||
|  | ||||
| // https://github.com/tholman/github-corners | ||||
| export const GitHubCorner = React.memo( | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { useSetAtom } from "jotai"; | ||||
| import React from "react"; | ||||
| import { appLangCodeAtom } from ".."; | ||||
| import { useI18n } from "../../src/i18n"; | ||||
| import { languages } from "../../src/i18n"; | ||||
| import { appLangCodeAtom } from "../App"; | ||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | ||||
| import { languages } from "../../packages/excalidraw/i18n"; | ||||
|  | ||||
| export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { | ||||
|   const { t, langCode } = useI18n(); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import React from "react"; | ||||
| import * as Sentry from "@sentry/browser"; | ||||
| import { t } from "../i18n"; | ||||
| import Trans from "./Trans"; | ||||
| import { t } from "../../packages/excalidraw/i18n"; | ||||
| import Trans from "../../packages/excalidraw/components/Trans"; | ||||
| 
 | ||||
| interface TopErrorBoundaryState { | ||||
|   hasError: boolean; | ||||
| @@ -1,19 +1,19 @@ | ||||
| import { compressData } from "../../src/data/encode"; | ||||
| import { newElementWith } from "../../src/element/mutateElement"; | ||||
| import { isInitializedImageElement } from "../../src/element/typeChecks"; | ||||
| import { compressData } from "../../packages/excalidraw/data/encode"; | ||||
| import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; | ||||
| import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   ExcalidrawImageElement, | ||||
|   FileId, | ||||
|   InitializedExcalidrawImageElement, | ||||
| } from "../../src/element/types"; | ||||
| import { t } from "../../src/i18n"; | ||||
| } from "../../packages/excalidraw/element/types"; | ||||
| import { t } from "../../packages/excalidraw/i18n"; | ||||
| import { | ||||
|   BinaryFileData, | ||||
|   BinaryFileMetadata, | ||||
|   ExcalidrawImperativeAPI, | ||||
|   BinaryFiles, | ||||
| } from "../../src/types"; | ||||
| } from "../../packages/excalidraw/types"; | ||||
|  | ||||
| export class FileManager { | ||||
|   /** files being fetched */ | ||||
|   | ||||
| @@ -11,11 +11,18 @@ | ||||
|  */ | ||||
|  | ||||
| import { createStore, entries, del, getMany, set, setMany } from "idb-keyval"; | ||||
| import { clearAppStateForLocalStorage } from "../../src/appState"; | ||||
| import { clearElementsForLocalStorage } from "../../src/element"; | ||||
| import { ExcalidrawElement, FileId } from "../../src/element/types"; | ||||
| import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; | ||||
| import { debounce } from "../../src/utils"; | ||||
| import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState"; | ||||
| import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   FileId, | ||||
| } from "../../packages/excalidraw/element/types"; | ||||
| import { | ||||
|   AppState, | ||||
|   BinaryFileData, | ||||
|   BinaryFiles, | ||||
| } from "../../packages/excalidraw/types"; | ||||
| import { debounce } from "../../packages/excalidraw/utils"; | ||||
| import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; | ||||
| import { FileManager } from "./FileManager"; | ||||
| import { Locker } from "./Locker"; | ||||
|   | ||||
| @@ -1,20 +1,27 @@ | ||||
| import { ExcalidrawElement, FileId } from "../../src/element/types"; | ||||
| import { getSceneVersion } from "../../src/element"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   FileId, | ||||
| } from "../../packages/excalidraw/element/types"; | ||||
| import { getSceneVersion } from "../../packages/excalidraw/element"; | ||||
| import Portal from "../collab/Portal"; | ||||
| import { restoreElements } from "../../src/data/restore"; | ||||
| import { restoreElements } from "../../packages/excalidraw/data/restore"; | ||||
| import { | ||||
|   AppState, | ||||
|   BinaryFileData, | ||||
|   BinaryFileMetadata, | ||||
|   DataURL, | ||||
| } from "../../src/types"; | ||||
| } from "../../packages/excalidraw/types"; | ||||
| import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants"; | ||||
| import { decompressData } from "../../src/data/encode"; | ||||
| import { encryptData, decryptData } from "../../src/data/encryption"; | ||||
| import { MIME_TYPES } from "../../src/constants"; | ||||
| import { decompressData } from "../../packages/excalidraw/data/encode"; | ||||
| import { | ||||
|   encryptData, | ||||
|   decryptData, | ||||
| } from "../../packages/excalidraw/data/encryption"; | ||||
| import { MIME_TYPES } from "../../packages/excalidraw/constants"; | ||||
| import { reconcileElements } from "../collab/reconciliation"; | ||||
| import { getSyncableElements, SyncableExcalidrawElement } from "."; | ||||
| import { ResolutionType } from "../../src/utility-types"; | ||||
| import { ResolutionType } from "../../packages/excalidraw/utility-types"; | ||||
| import type { Socket } from "socket.io-client"; | ||||
|  | ||||
| // private | ||||
| // ----------------------------------------------------------------------------- | ||||
| @@ -132,12 +139,12 @@ const decryptElements = async ( | ||||
| }; | ||||
|  | ||||
| class FirebaseSceneVersionCache { | ||||
|   private static cache = new WeakMap<SocketIOClient.Socket, number>(); | ||||
|   static get = (socket: SocketIOClient.Socket) => { | ||||
|   private static cache = new WeakMap<Socket, number>(); | ||||
|   static get = (socket: Socket) => { | ||||
|     return FirebaseSceneVersionCache.cache.get(socket); | ||||
|   }; | ||||
|   static set = ( | ||||
|     socket: SocketIOClient.Socket, | ||||
|     socket: Socket, | ||||
|     elements: readonly SyncableExcalidrawElement[], | ||||
|   ) => { | ||||
|     FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements)); | ||||
| @@ -279,7 +286,7 @@ export const saveToFirebase = async ( | ||||
| export const loadFromFirebase = async ( | ||||
|   roomId: string, | ||||
|   roomKey: string, | ||||
|   socket: SocketIOClient.Socket | null, | ||||
|   socket: Socket | null, | ||||
| ): Promise<readonly ExcalidrawElement[] | null> => { | ||||
|   const firebase = await loadFirestore(); | ||||
|   const db = firebase.firestore(); | ||||
|   | ||||
| @@ -1,27 +1,36 @@ | ||||
| import { compressData, decompressData } from "../../src/data/encode"; | ||||
| import { | ||||
|   compressData, | ||||
|   decompressData, | ||||
| } from "../../packages/excalidraw/data/encode"; | ||||
| import { | ||||
|   decryptData, | ||||
|   generateEncryptionKey, | ||||
|   IV_LENGTH_BYTES, | ||||
| } from "../../src/data/encryption"; | ||||
| import { serializeAsJSON } from "../../src/data/json"; | ||||
| import { restore } from "../../src/data/restore"; | ||||
| import { ImportedDataState } from "../../src/data/types"; | ||||
| import { isInvisiblySmallElement } from "../../src/element/sizeHelpers"; | ||||
| import { isInitializedImageElement } from "../../src/element/typeChecks"; | ||||
| import { ExcalidrawElement, FileId } from "../../src/element/types"; | ||||
| import { t } from "../../src/i18n"; | ||||
| } from "../../packages/excalidraw/data/encryption"; | ||||
| import { serializeAsJSON } from "../../packages/excalidraw/data/json"; | ||||
| import { restore } from "../../packages/excalidraw/data/restore"; | ||||
| import { ImportedDataState } from "../../packages/excalidraw/data/types"; | ||||
| import { SceneBounds } from "../../packages/excalidraw/element/bounds"; | ||||
| import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers"; | ||||
| import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   FileId, | ||||
| } from "../../packages/excalidraw/element/types"; | ||||
| import { t } from "../../packages/excalidraw/i18n"; | ||||
| import { | ||||
|   AppState, | ||||
|   BinaryFileData, | ||||
|   BinaryFiles, | ||||
|   SocketId, | ||||
|   UserIdleState, | ||||
| } from "../../src/types"; | ||||
| import { bytesToHexString } from "../../src/utils"; | ||||
| } from "../../packages/excalidraw/types"; | ||||
| import { bytesToHexString } from "../../packages/excalidraw/utils"; | ||||
| import { | ||||
|   DELETED_ELEMENT_TIMEOUT, | ||||
|   FILE_UPLOAD_MAX_BYTES, | ||||
|   ROOM_ID_BYTES, | ||||
|   WS_SUBTYPES, | ||||
| } from "../app_constants"; | ||||
| import { encodeFilesForUpload } from "./FileManager"; | ||||
| import { saveFilesToFirebase } from "./firebase"; | ||||
| @@ -56,67 +65,49 @@ const generateRoomId = async () => { | ||||
|   return bytesToHexString(buffer); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Right now the reason why we resolve connection params (url, polling...) | ||||
|  * from upstream is to allow changing the params immediately when needed without | ||||
|  * having to wait for clients to update the SW. | ||||
|  * | ||||
|  * If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks) | ||||
|  */ | ||||
| export const getCollabServer = async (): Promise<{ | ||||
|   url: string; | ||||
|   polling: boolean; | ||||
| }> => { | ||||
|   if (import.meta.env.VITE_APP_WS_SERVER_URL) { | ||||
|     return { | ||||
|       url: import.meta.env.VITE_APP_WS_SERVER_URL, | ||||
|       polling: true, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const resp = await fetch( | ||||
|       `${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`, | ||||
|     ); | ||||
|     return await resp.json(); | ||||
|   } catch (error) { | ||||
|     console.error(error); | ||||
|     throw new Error(t("errors.cannotResolveCollabServer")); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export type EncryptedData = { | ||||
|   data: ArrayBuffer; | ||||
|   iv: Uint8Array; | ||||
| }; | ||||
|  | ||||
| export type SocketUpdateDataSource = { | ||||
|   INVALID_RESPONSE: { | ||||
|     type: WS_SUBTYPES.INVALID_RESPONSE; | ||||
|   }; | ||||
|   SCENE_INIT: { | ||||
|     type: "SCENE_INIT"; | ||||
|     type: WS_SUBTYPES.INIT; | ||||
|     payload: { | ||||
|       elements: readonly ExcalidrawElement[]; | ||||
|     }; | ||||
|   }; | ||||
|   SCENE_UPDATE: { | ||||
|     type: "SCENE_UPDATE"; | ||||
|     type: WS_SUBTYPES.UPDATE; | ||||
|     payload: { | ||||
|       elements: readonly ExcalidrawElement[]; | ||||
|     }; | ||||
|   }; | ||||
|   MOUSE_LOCATION: { | ||||
|     type: "MOUSE_LOCATION"; | ||||
|     type: WS_SUBTYPES.MOUSE_LOCATION; | ||||
|     payload: { | ||||
|       socketId: string; | ||||
|       socketId: SocketId; | ||||
|       pointer: { x: number; y: number; tool: "pointer" | "laser" }; | ||||
|       button: "down" | "up"; | ||||
|       selectedElementIds: AppState["selectedElementIds"]; | ||||
|       username: string; | ||||
|     }; | ||||
|   }; | ||||
|   IDLE_STATUS: { | ||||
|     type: "IDLE_STATUS"; | ||||
|   USER_VISIBLE_SCENE_BOUNDS: { | ||||
|     type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS; | ||||
|     payload: { | ||||
|       socketId: string; | ||||
|       socketId: SocketId; | ||||
|       username: string; | ||||
|       sceneBounds: SceneBounds; | ||||
|     }; | ||||
|   }; | ||||
|   IDLE_STATUS: { | ||||
|     type: WS_SUBTYPES.IDLE_STATUS; | ||||
|     payload: { | ||||
|       socketId: SocketId; | ||||
|       userState: UserIdleState; | ||||
|       username: string; | ||||
|     }; | ||||
| @@ -124,10 +115,7 @@ export type SocketUpdateDataSource = { | ||||
| }; | ||||
|  | ||||
| export type SocketUpdateDataIncoming = | ||||
|   | SocketUpdateDataSource[keyof SocketUpdateDataSource] | ||||
|   | { | ||||
|       type: "INVALID_RESPONSE"; | ||||
|     }; | ||||
|   SocketUpdateDataSource[keyof SocketUpdateDataSource]; | ||||
|  | ||||
| export type SocketUpdateData = | ||||
|   SocketUpdateDataSource[keyof SocketUpdateDataSource] & { | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { ExcalidrawElement } from "../../src/element/types"; | ||||
| import { AppState } from "../../src/types"; | ||||
| import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; | ||||
| import { AppState } from "../../packages/excalidraw/types"; | ||||
| import { | ||||
|   clearAppStateForLocalStorage, | ||||
|   getDefaultAppState, | ||||
| } from "../../src/appState"; | ||||
| import { clearElementsForLocalStorage } from "../../src/element"; | ||||
| } from "../../packages/excalidraw/appState"; | ||||
| import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; | ||||
| import { STORAGE_KEYS } from "../app_constants"; | ||||
| import { ImportedDataState } from "../../src/data/types"; | ||||
| import { ImportedDataState } from "../../packages/excalidraw/data/types"; | ||||
|  | ||||
| export const saveUsernameToLocalStorage = (username: string) => { | ||||
|   try { | ||||
|   | ||||
							
								
								
									
										3
									
								
								excalidraw-app/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| interface Window { | ||||
|   __EXCALIDRAW_SHA__: string | undefined; | ||||
| } | ||||
| @@ -121,7 +121,7 @@ | ||||
|       crossorigin="anonymous" | ||||
|     /> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="/fonts.css" type="text/css" /> | ||||
|     <link rel="stylesheet" href="/fonts/fonts.css" type="text/css" /> | ||||
|     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %> | ||||
|     <script> | ||||
|       { | ||||
| @@ -195,7 +195,7 @@ | ||||
|       <h1 class="visually-hidden">Excalidraw</h1> | ||||
|     </header> | ||||
|     <div id="root"></div> | ||||
|     <script type="module" src="/src/index.tsx"></script> | ||||
|     <script type="module" src="index.tsx"></script> | ||||
|     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %> | ||||
|     <!-- 100% privacy friendly analytics --> | ||||
|     <script> | ||||
| @@ -1,811 +1,15 @@ | ||||
| import polyfill from "../src/polyfill"; | ||||
| import LanguageDetector from "i18next-browser-languagedetector"; | ||||
| import { useEffect, useRef, useState } from "react"; | ||||
| import { trackEvent } from "../src/analytics"; | ||||
| import { getDefaultAppState } from "../src/appState"; | ||||
| import { ErrorDialog } from "../src/components/ErrorDialog"; | ||||
| import { TopErrorBoundary } from "../src/components/TopErrorBoundary"; | ||||
| import { | ||||
|   APP_NAME, | ||||
|   EVENT, | ||||
|   THEME, | ||||
|   TITLE_TIMEOUT, | ||||
|   VERSION_TIMEOUT, | ||||
| } from "../src/constants"; | ||||
| import { loadFromBlob } from "../src/data/blob"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   FileId, | ||||
|   NonDeletedExcalidrawElement, | ||||
|   Theme, | ||||
| } from "../src/element/types"; | ||||
| import { useCallbackRefState } from "../src/hooks/useCallbackRefState"; | ||||
| import { t } from "../src/i18n"; | ||||
| import { | ||||
|   Excalidraw, | ||||
|   defaultLang, | ||||
|   LiveCollaborationTrigger, | ||||
| } from "../src/packages/excalidraw/index"; | ||||
| import { | ||||
|   AppState, | ||||
|   LibraryItems, | ||||
|   ExcalidrawImperativeAPI, | ||||
|   BinaryFiles, | ||||
|   ExcalidrawInitialDataState, | ||||
|   UIAppState, | ||||
| } from "../src/types"; | ||||
| import { | ||||
|   debounce, | ||||
|   getVersion, | ||||
|   getFrame, | ||||
|   isTestEnv, | ||||
|   preventUnload, | ||||
|   ResolvablePromise, | ||||
|   resolvablePromise, | ||||
|   isRunningInIframe, | ||||
| } from "../src/utils"; | ||||
| import { | ||||
|   FIREBASE_STORAGE_PREFIXES, | ||||
|   STORAGE_KEYS, | ||||
|   SYNC_BROWSER_TABS_TIMEOUT, | ||||
| } from "./app_constants"; | ||||
| import Collab, { | ||||
|   CollabAPI, | ||||
|   collabAPIAtom, | ||||
|   collabDialogShownAtom, | ||||
|   isCollaboratingAtom, | ||||
|   isOfflineAtom, | ||||
| } from "./collab/Collab"; | ||||
| import { | ||||
|   exportToBackend, | ||||
|   getCollaborationLinkData, | ||||
|   isCollaborationLink, | ||||
|   loadScene, | ||||
| } from "./data"; | ||||
| import { | ||||
|   getLibraryItemsFromStorage, | ||||
|   importFromLocalStorage, | ||||
|   importUsernameFromLocalStorage, | ||||
| } from "./data/localStorage"; | ||||
| import CustomStats from "./CustomStats"; | ||||
| import { | ||||
|   restore, | ||||
|   restoreAppState, | ||||
|   RestoredDataState, | ||||
| } from "../src/data/restore"; | ||||
| import { | ||||
|   ExportToExcalidrawPlus, | ||||
|   exportToExcalidrawPlus, | ||||
| } from "./components/ExportToExcalidrawPlus"; | ||||
| import { updateStaleImageStatuses } from "./data/FileManager"; | ||||
| import { newElementWith } from "../src/element/mutateElement"; | ||||
| import { isInitializedImageElement } from "../src/element/typeChecks"; | ||||
| import { loadFilesFromFirebase } from "./data/firebase"; | ||||
| import { LocalData } from "./data/LocalData"; | ||||
| import { isBrowserStorageStateNewer } from "./data/tabSync"; | ||||
| import clsx from "clsx"; | ||||
| import { reconcileElements } from "./collab/reconciliation"; | ||||
| import { | ||||
|   parseLibraryTokensFromUrl, | ||||
|   useHandleLibrary, | ||||
| } from "../src/data/library"; | ||||
| import { AppMainMenu } from "./components/AppMainMenu"; | ||||
| import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; | ||||
| import { AppFooter } from "./components/AppFooter"; | ||||
| import { atom, Provider, useAtom, useAtomValue } from "jotai"; | ||||
| import { useAtomWithInitialValue } from "../src/jotai"; | ||||
| import { appJotaiStore } from "./app-jotai"; | ||||
| import { StrictMode } from "react"; | ||||
| import { createRoot } from "react-dom/client"; | ||||
| import ExcalidrawApp from "./App"; | ||||
| import { registerSW } from "virtual:pwa-register"; | ||||
|  | ||||
| import "./index.scss"; | ||||
| import { ResolutionType } from "../src/utility-types"; | ||||
| import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog"; | ||||
| import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState"; | ||||
| import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm"; | ||||
| import Trans from "../src/components/Trans"; | ||||
|  | ||||
| polyfill(); | ||||
|  | ||||
| window.EXCALIDRAW_THROTTLE_RENDER = true; | ||||
|  | ||||
| let isSelfEmbedding = false; | ||||
|  | ||||
| if (window.self !== window.top) { | ||||
|   try { | ||||
|     const parentUrl = new URL(document.referrer); | ||||
|     const currentUrl = new URL(window.location.href); | ||||
|     if (parentUrl.origin === currentUrl.origin) { | ||||
|       isSelfEmbedding = true; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     // ignore | ||||
|   } | ||||
| } | ||||
|  | ||||
| const languageDetector = new LanguageDetector(); | ||||
| languageDetector.init({ | ||||
|   languageUtils: {}, | ||||
| }); | ||||
|  | ||||
| const shareableLinkConfirmDialog = { | ||||
|   title: t("overwriteConfirm.modal.shareableLink.title"), | ||||
|   description: ( | ||||
|     <Trans | ||||
|       i18nKey="overwriteConfirm.modal.shareableLink.description" | ||||
|       bold={(text) => <strong>{text}</strong>} | ||||
|       br={() => <br />} | ||||
|     /> | ||||
|   ), | ||||
|   actionLabel: t("overwriteConfirm.modal.shareableLink.button"), | ||||
|   color: "danger", | ||||
| } as const; | ||||
|  | ||||
| const initializeScene = async (opts: { | ||||
|   collabAPI: CollabAPI | null; | ||||
|   excalidrawAPI: ExcalidrawImperativeAPI; | ||||
| }): Promise< | ||||
|   { scene: ExcalidrawInitialDataState | null } & ( | ||||
|     | { isExternalScene: true; id: string; key: string } | ||||
|     | { isExternalScene: false; id?: null; key?: null } | ||||
|   ) | ||||
| > => { | ||||
|   const searchParams = new URLSearchParams(window.location.search); | ||||
|   const id = searchParams.get("id"); | ||||
|   const jsonBackendMatch = window.location.hash.match( | ||||
|     /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/, | ||||
|   ); | ||||
|   const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/); | ||||
|  | ||||
|   const localDataState = importFromLocalStorage(); | ||||
|  | ||||
|   let scene: RestoredDataState & { | ||||
|     scrollToContent?: boolean; | ||||
|   } = await loadScene(null, null, localDataState); | ||||
|  | ||||
|   let roomLinkData = getCollaborationLinkData(window.location.href); | ||||
|   const isExternalScene = !!(id || jsonBackendMatch || roomLinkData); | ||||
|   if (isExternalScene) { | ||||
|     if ( | ||||
|       // don't prompt if scene is empty | ||||
|       !scene.elements.length || | ||||
|       // don't prompt for collab scenes because we don't override local storage | ||||
|       roomLinkData || | ||||
|       // otherwise, prompt whether user wants to override current scene | ||||
|       (await openConfirmModal(shareableLinkConfirmDialog)) | ||||
|     ) { | ||||
|       if (jsonBackendMatch) { | ||||
|         scene = await loadScene( | ||||
|           jsonBackendMatch[1], | ||||
|           jsonBackendMatch[2], | ||||
|           localDataState, | ||||
|         ); | ||||
|       } | ||||
|       scene.scrollToContent = true; | ||||
|       if (!roomLinkData) { | ||||
|         window.history.replaceState({}, APP_NAME, window.location.origin); | ||||
|       } | ||||
|     } else { | ||||
|       // https://github.com/excalidraw/excalidraw/issues/1919 | ||||
|       if (document.hidden) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|           window.addEventListener( | ||||
|             "focus", | ||||
|             () => initializeScene(opts).then(resolve).catch(reject), | ||||
|             { | ||||
|               once: true, | ||||
|             }, | ||||
|           ); | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       roomLinkData = null; | ||||
|       window.history.replaceState({}, APP_NAME, window.location.origin); | ||||
|     } | ||||
|   } else if (externalUrlMatch) { | ||||
|     window.history.replaceState({}, APP_NAME, window.location.origin); | ||||
|  | ||||
|     const url = externalUrlMatch[1]; | ||||
|     try { | ||||
|       const request = await fetch(window.decodeURIComponent(url)); | ||||
|       const data = await loadFromBlob(await request.blob(), null, null); | ||||
|       if ( | ||||
|         !scene.elements.length || | ||||
|         (await openConfirmModal(shareableLinkConfirmDialog)) | ||||
|       ) { | ||||
|         return { scene: data, isExternalScene }; | ||||
|       } | ||||
|     } catch (error: any) { | ||||
|       return { | ||||
|         scene: { | ||||
|           appState: { | ||||
|             errorMessage: t("alerts.invalidSceneUrl"), | ||||
|           }, | ||||
|         }, | ||||
|         isExternalScene, | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (roomLinkData && opts.collabAPI) { | ||||
|     const { excalidrawAPI } = opts; | ||||
|  | ||||
|     const scene = await opts.collabAPI.startCollaboration(roomLinkData); | ||||
|  | ||||
|     return { | ||||
|       // when collaborating, the state may have already been updated at this | ||||
|       // point (we may have received updates from other clients), so reconcile | ||||
|       // elements and appState with existing state | ||||
|       scene: { | ||||
|         ...scene, | ||||
|         appState: { | ||||
|           ...restoreAppState( | ||||
|             { | ||||
|               ...scene?.appState, | ||||
|               theme: localDataState?.appState?.theme || scene?.appState?.theme, | ||||
|             }, | ||||
|             excalidrawAPI.getAppState(), | ||||
|           ), | ||||
|           // necessary if we're invoking from a hashchange handler which doesn't | ||||
|           // go through App.initializeScene() that resets this flag | ||||
|           isLoading: false, | ||||
|         }, | ||||
|         elements: reconcileElements( | ||||
|           scene?.elements || [], | ||||
|           excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|           excalidrawAPI.getAppState(), | ||||
|         ), | ||||
|       }, | ||||
|       isExternalScene: true, | ||||
|       id: roomLinkData.roomId, | ||||
|       key: roomLinkData.roomKey, | ||||
|     }; | ||||
|   } else if (scene) { | ||||
|     return isExternalScene && jsonBackendMatch | ||||
|       ? { | ||||
|           scene, | ||||
|           isExternalScene, | ||||
|           id: jsonBackendMatch[1], | ||||
|           key: jsonBackendMatch[2], | ||||
|         } | ||||
|       : { scene, isExternalScene: false }; | ||||
|   } | ||||
|   return { scene: null, isExternalScene: false }; | ||||
| }; | ||||
|  | ||||
| const detectedLangCode = languageDetector.detect() || defaultLang.code; | ||||
| export const appLangCodeAtom = atom( | ||||
|   Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode, | ||||
| import "../excalidraw-app/sentry"; | ||||
| window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA; | ||||
| const rootElement = document.getElementById("root")!; | ||||
| const root = createRoot(rootElement); | ||||
| registerSW(); | ||||
| root.render( | ||||
|   <StrictMode> | ||||
|     <ExcalidrawApp /> | ||||
|   </StrictMode>, | ||||
| ); | ||||
|  | ||||
| const ExcalidrawWrapper = () => { | ||||
|   const [errorMessage, setErrorMessage] = useState(""); | ||||
|   const [langCode, setLangCode] = useAtom(appLangCodeAtom); | ||||
|   const isCollabDisabled = isRunningInIframe(); | ||||
|  | ||||
|   // initial state | ||||
|   // --------------------------------------------------------------------------- | ||||
|  | ||||
|   const initialStatePromiseRef = useRef<{ | ||||
|     promise: ResolvablePromise<ExcalidrawInitialDataState | null>; | ||||
|   }>({ promise: null! }); | ||||
|   if (!initialStatePromiseRef.current.promise) { | ||||
|     initialStatePromiseRef.current.promise = | ||||
|       resolvablePromise<ExcalidrawInitialDataState | null>(); | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     trackEvent("load", "frame", getFrame()); | ||||
|     // Delayed so that the app has a time to load the latest SW | ||||
|     setTimeout(() => { | ||||
|       trackEvent("load", "version", getVersion()); | ||||
|     }, VERSION_TIMEOUT); | ||||
|   }, []); | ||||
|  | ||||
|   const [excalidrawAPI, excalidrawRefCallback] = | ||||
|     useCallbackRefState<ExcalidrawImperativeAPI>(); | ||||
|  | ||||
|   const [collabAPI] = useAtom(collabAPIAtom); | ||||
|   const [, setCollabDialogShown] = useAtom(collabDialogShownAtom); | ||||
|   const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => { | ||||
|     return isCollaborationLink(window.location.href); | ||||
|   }); | ||||
|  | ||||
|   useHandleLibrary({ | ||||
|     excalidrawAPI, | ||||
|     getInitialLibraryItems: getLibraryItemsFromStorage, | ||||
|   }); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const loadImages = ( | ||||
|       data: ResolutionType<typeof initializeScene>, | ||||
|       isInitialLoad = false, | ||||
|     ) => { | ||||
|       if (!data.scene) { | ||||
|         return; | ||||
|       } | ||||
|       if (collabAPI?.isCollaborating()) { | ||||
|         if (data.scene.elements) { | ||||
|           collabAPI | ||||
|             .fetchImageFilesFromFirebase({ | ||||
|               elements: data.scene.elements, | ||||
|               forceFetchFiles: true, | ||||
|             }) | ||||
|             .then(({ loadedFiles, erroredFiles }) => { | ||||
|               excalidrawAPI.addFiles(loadedFiles); | ||||
|               updateStaleImageStatuses({ | ||||
|                 excalidrawAPI, | ||||
|                 erroredFiles, | ||||
|                 elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|               }); | ||||
|             }); | ||||
|         } | ||||
|       } else { | ||||
|         const fileIds = | ||||
|           data.scene.elements?.reduce((acc, element) => { | ||||
|             if (isInitializedImageElement(element)) { | ||||
|               return acc.concat(element.fileId); | ||||
|             } | ||||
|             return acc; | ||||
|           }, [] as FileId[]) || []; | ||||
|  | ||||
|         if (data.isExternalScene) { | ||||
|           loadFilesFromFirebase( | ||||
|             `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`, | ||||
|             data.key, | ||||
|             fileIds, | ||||
|           ).then(({ loadedFiles, erroredFiles }) => { | ||||
|             excalidrawAPI.addFiles(loadedFiles); | ||||
|             updateStaleImageStatuses({ | ||||
|               excalidrawAPI, | ||||
|               erroredFiles, | ||||
|               elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|             }); | ||||
|           }); | ||||
|         } else if (isInitialLoad) { | ||||
|           if (fileIds.length) { | ||||
|             LocalData.fileStorage | ||||
|               .getFiles(fileIds) | ||||
|               .then(({ loadedFiles, erroredFiles }) => { | ||||
|                 if (loadedFiles.length) { | ||||
|                   excalidrawAPI.addFiles(loadedFiles); | ||||
|                 } | ||||
|                 updateStaleImageStatuses({ | ||||
|                   excalidrawAPI, | ||||
|                   erroredFiles, | ||||
|                   elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|                 }); | ||||
|               }); | ||||
|           } | ||||
|           // on fresh load, clear unused files from IDB (from previous | ||||
|           // session) | ||||
|           LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds }); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => { | ||||
|       loadImages(data, /* isInitialLoad */ true); | ||||
|       initialStatePromiseRef.current.promise.resolve(data.scene); | ||||
|     }); | ||||
|  | ||||
|     const onHashChange = async (event: HashChangeEvent) => { | ||||
|       event.preventDefault(); | ||||
|       const libraryUrlTokens = parseLibraryTokensFromUrl(); | ||||
|       if (!libraryUrlTokens) { | ||||
|         if ( | ||||
|           collabAPI?.isCollaborating() && | ||||
|           !isCollaborationLink(window.location.href) | ||||
|         ) { | ||||
|           collabAPI.stopCollaboration(false); | ||||
|         } | ||||
|         excalidrawAPI.updateScene({ appState: { isLoading: true } }); | ||||
|  | ||||
|         initializeScene({ collabAPI, excalidrawAPI }).then((data) => { | ||||
|           loadImages(data); | ||||
|           if (data.scene) { | ||||
|             excalidrawAPI.updateScene({ | ||||
|               ...data.scene, | ||||
|               ...restore(data.scene, null, null, { repairBindings: true }), | ||||
|               commitToHistory: true, | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const titleTimeout = setTimeout( | ||||
|       () => (document.title = APP_NAME), | ||||
|       TITLE_TIMEOUT, | ||||
|     ); | ||||
|  | ||||
|     const syncData = debounce(() => { | ||||
|       if (isTestEnv()) { | ||||
|         return; | ||||
|       } | ||||
|       if ( | ||||
|         !document.hidden && | ||||
|         ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled) | ||||
|       ) { | ||||
|         // don't sync if local state is newer or identical to browser state | ||||
|         if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { | ||||
|           const localDataState = importFromLocalStorage(); | ||||
|           const username = importUsernameFromLocalStorage(); | ||||
|           let langCode = languageDetector.detect() || defaultLang.code; | ||||
|           if (Array.isArray(langCode)) { | ||||
|             langCode = langCode[0]; | ||||
|           } | ||||
|           setLangCode(langCode); | ||||
|           excalidrawAPI.updateScene({ | ||||
|             ...localDataState, | ||||
|           }); | ||||
|           excalidrawAPI.updateLibrary({ | ||||
|             libraryItems: getLibraryItemsFromStorage(), | ||||
|           }); | ||||
|           collabAPI?.setUsername(username || ""); | ||||
|         } | ||||
|  | ||||
|         if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) { | ||||
|           const elements = excalidrawAPI.getSceneElementsIncludingDeleted(); | ||||
|           const currFiles = excalidrawAPI.getFiles(); | ||||
|           const fileIds = | ||||
|             elements?.reduce((acc, element) => { | ||||
|               if ( | ||||
|                 isInitializedImageElement(element) && | ||||
|                 // only load and update images that aren't already loaded | ||||
|                 !currFiles[element.fileId] | ||||
|               ) { | ||||
|                 return acc.concat(element.fileId); | ||||
|               } | ||||
|               return acc; | ||||
|             }, [] as FileId[]) || []; | ||||
|           if (fileIds.length) { | ||||
|             LocalData.fileStorage | ||||
|               .getFiles(fileIds) | ||||
|               .then(({ loadedFiles, erroredFiles }) => { | ||||
|                 if (loadedFiles.length) { | ||||
|                   excalidrawAPI.addFiles(loadedFiles); | ||||
|                 } | ||||
|                 updateStaleImageStatuses({ | ||||
|                   excalidrawAPI, | ||||
|                   erroredFiles, | ||||
|                   elements: excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||
|                 }); | ||||
|               }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, SYNC_BROWSER_TABS_TIMEOUT); | ||||
|  | ||||
|     const onUnload = () => { | ||||
|       LocalData.flushSave(); | ||||
|     }; | ||||
|  | ||||
|     const visibilityChange = (event: FocusEvent | Event) => { | ||||
|       if (event.type === EVENT.BLUR || document.hidden) { | ||||
|         LocalData.flushSave(); | ||||
|       } | ||||
|       if ( | ||||
|         event.type === EVENT.VISIBILITY_CHANGE || | ||||
|         event.type === EVENT.FOCUS | ||||
|       ) { | ||||
|         syncData(); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     window.addEventListener(EVENT.HASHCHANGE, onHashChange, false); | ||||
|     window.addEventListener(EVENT.UNLOAD, onUnload, false); | ||||
|     window.addEventListener(EVENT.BLUR, visibilityChange, false); | ||||
|     document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false); | ||||
|     window.addEventListener(EVENT.FOCUS, visibilityChange, false); | ||||
|     return () => { | ||||
|       window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false); | ||||
|       window.removeEventListener(EVENT.UNLOAD, onUnload, false); | ||||
|       window.removeEventListener(EVENT.BLUR, visibilityChange, false); | ||||
|       window.removeEventListener(EVENT.FOCUS, visibilityChange, false); | ||||
|       document.removeEventListener( | ||||
|         EVENT.VISIBILITY_CHANGE, | ||||
|         visibilityChange, | ||||
|         false, | ||||
|       ); | ||||
|       clearTimeout(titleTimeout); | ||||
|     }; | ||||
|   }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const unloadHandler = (event: BeforeUnloadEvent) => { | ||||
|       LocalData.flushSave(); | ||||
|  | ||||
|       if ( | ||||
|         excalidrawAPI && | ||||
|         LocalData.fileStorage.shouldPreventUnload( | ||||
|           excalidrawAPI.getSceneElements(), | ||||
|         ) | ||||
|       ) { | ||||
|         preventUnload(event); | ||||
|       } | ||||
|     }; | ||||
|     window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); | ||||
|     return () => { | ||||
|       window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); | ||||
|     }; | ||||
|   }, [excalidrawAPI]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     languageDetector.cacheUserLanguage(langCode); | ||||
|   }, [langCode]); | ||||
|  | ||||
|   const [theme, setTheme] = useState<Theme>( | ||||
|     () => | ||||
|       (localStorage.getItem( | ||||
|         STORAGE_KEYS.LOCAL_STORAGE_THEME, | ||||
|       ) as Theme | null) || | ||||
|       // FIXME migration from old LS scheme. Can be removed later. #5660 | ||||
|       importFromLocalStorage().appState?.theme || | ||||
|       THEME.LIGHT, | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme); | ||||
|     // currently only used for body styling during init (see public/index.html), | ||||
|     // but may change in the future | ||||
|     document.documentElement.classList.toggle("dark", theme === THEME.DARK); | ||||
|   }, [theme]); | ||||
|  | ||||
|   const onChange = ( | ||||
|     elements: readonly ExcalidrawElement[], | ||||
|     appState: AppState, | ||||
|     files: BinaryFiles, | ||||
|   ) => { | ||||
|     if (collabAPI?.isCollaborating()) { | ||||
|       collabAPI.syncElements(elements); | ||||
|     } | ||||
|  | ||||
|     setTheme(appState.theme); | ||||
|  | ||||
|     // this check is redundant, but since this is a hot path, it's best | ||||
|     // not to evaludate the nested expression every time | ||||
|     if (!LocalData.isSavePaused()) { | ||||
|       LocalData.save(elements, appState, files, () => { | ||||
|         if (excalidrawAPI) { | ||||
|           let didChange = false; | ||||
|  | ||||
|           const elements = excalidrawAPI | ||||
|             .getSceneElementsIncludingDeleted() | ||||
|             .map((element) => { | ||||
|               if ( | ||||
|                 LocalData.fileStorage.shouldUpdateImageElementStatus(element) | ||||
|               ) { | ||||
|                 const newElement = newElementWith(element, { status: "saved" }); | ||||
|                 if (newElement !== element) { | ||||
|                   didChange = true; | ||||
|                 } | ||||
|                 return newElement; | ||||
|               } | ||||
|               return element; | ||||
|             }); | ||||
|  | ||||
|           if (didChange) { | ||||
|             excalidrawAPI.updateScene({ | ||||
|               elements, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const [latestShareableLink, setLatestShareableLink] = useState<string | null>( | ||||
|     null, | ||||
|   ); | ||||
|  | ||||
|   const onExportToBackend = async ( | ||||
|     exportedElements: readonly NonDeletedExcalidrawElement[], | ||||
|     appState: Partial<AppState>, | ||||
|     files: BinaryFiles, | ||||
|     canvas: HTMLCanvasElement, | ||||
|   ) => { | ||||
|     if (exportedElements.length === 0) { | ||||
|       throw new Error(t("alerts.cannotExportEmptyCanvas")); | ||||
|     } | ||||
|     if (canvas) { | ||||
|       try { | ||||
|         const { url, errorMessage } = await exportToBackend( | ||||
|           exportedElements, | ||||
|           { | ||||
|             ...appState, | ||||
|             viewBackgroundColor: appState.exportBackground | ||||
|               ? appState.viewBackgroundColor | ||||
|               : getDefaultAppState().viewBackgroundColor, | ||||
|           }, | ||||
|           files, | ||||
|         ); | ||||
|  | ||||
|         if (errorMessage) { | ||||
|           throw new Error(errorMessage); | ||||
|         } | ||||
|  | ||||
|         if (url) { | ||||
|           setLatestShareableLink(url); | ||||
|         } | ||||
|       } catch (error: any) { | ||||
|         if (error.name !== "AbortError") { | ||||
|           const { width, height } = canvas; | ||||
|           console.error(error, { width, height }); | ||||
|           throw new Error(error.message); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const renderCustomStats = ( | ||||
|     elements: readonly NonDeletedExcalidrawElement[], | ||||
|     appState: UIAppState, | ||||
|   ) => { | ||||
|     return ( | ||||
|       <CustomStats | ||||
|         setToast={(message) => excalidrawAPI!.setToast({ message })} | ||||
|         appState={appState} | ||||
|         elements={elements} | ||||
|       /> | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   const onLibraryChange = async (items: LibraryItems) => { | ||||
|     if (!items.length) { | ||||
|       localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); | ||||
|       return; | ||||
|     } | ||||
|     const serializedItems = JSON.stringify(items); | ||||
|     localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); | ||||
|   }; | ||||
|  | ||||
|   const isOffline = useAtomValue(isOfflineAtom); | ||||
|  | ||||
|   // browsers generally prevent infinite self-embedding, there are | ||||
|   // cases where it still happens, and while we disallow self-embedding | ||||
|   // by not whitelisting our own origin, this serves as an additional guard | ||||
|   if (isSelfEmbedding) { | ||||
|     return ( | ||||
|       <div | ||||
|         style={{ | ||||
|           display: "flex", | ||||
|           alignItems: "center", | ||||
|           justifyContent: "center", | ||||
|           textAlign: "center", | ||||
|           height: "100%", | ||||
|         }} | ||||
|       > | ||||
|         <h1>I'm not a pretzel!</h1> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ height: "100%" }} | ||||
|       className={clsx("excalidraw-app", { | ||||
|         "is-collaborating": isCollaborating, | ||||
|       })} | ||||
|     > | ||||
|       <Excalidraw | ||||
|         excalidrawAPI={excalidrawRefCallback} | ||||
|         onChange={onChange} | ||||
|         initialData={initialStatePromiseRef.current.promise} | ||||
|         isCollaborating={isCollaborating} | ||||
|         onPointerUpdate={collabAPI?.onPointerUpdate} | ||||
|         UIOptions={{ | ||||
|           canvasActions: { | ||||
|             toggleTheme: true, | ||||
|             export: { | ||||
|               onExportToBackend, | ||||
|               renderCustomUI: (elements, appState, files) => { | ||||
|                 return ( | ||||
|                   <ExportToExcalidrawPlus | ||||
|                     elements={elements} | ||||
|                     appState={appState} | ||||
|                     files={files} | ||||
|                     onError={(error) => { | ||||
|                       excalidrawAPI?.updateScene({ | ||||
|                         appState: { | ||||
|                           errorMessage: error.message, | ||||
|                         }, | ||||
|                       }); | ||||
|                     }} | ||||
|                     onSuccess={() => { | ||||
|                       excalidrawAPI?.updateScene({ | ||||
|                         appState: { openDialog: null }, | ||||
|                       }); | ||||
|                     }} | ||||
|                   /> | ||||
|                 ); | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }} | ||||
|         langCode={langCode} | ||||
|         renderCustomStats={renderCustomStats} | ||||
|         detectScroll={false} | ||||
|         handleKeyboardGlobally={true} | ||||
|         onLibraryChange={onLibraryChange} | ||||
|         autoFocus={true} | ||||
|         theme={theme} | ||||
|         renderTopRightUI={(isMobile) => { | ||||
|           if (isMobile || !collabAPI || isCollabDisabled) { | ||||
|             return null; | ||||
|           } | ||||
|           return ( | ||||
|             <LiveCollaborationTrigger | ||||
|               isCollaborating={isCollaborating} | ||||
|               onSelect={() => setCollabDialogShown(true)} | ||||
|             /> | ||||
|           ); | ||||
|         }} | ||||
|       > | ||||
|         <AppMainMenu | ||||
|           setCollabDialogShown={setCollabDialogShown} | ||||
|           isCollaborating={isCollaborating} | ||||
|           isCollabEnabled={!isCollabDisabled} | ||||
|         /> | ||||
|         <AppWelcomeScreen | ||||
|           setCollabDialogShown={setCollabDialogShown} | ||||
|           isCollabEnabled={!isCollabDisabled} | ||||
|         /> | ||||
|         <OverwriteConfirmDialog> | ||||
|           <OverwriteConfirmDialog.Actions.ExportToImage /> | ||||
|           <OverwriteConfirmDialog.Actions.SaveToDisk /> | ||||
|           {excalidrawAPI && ( | ||||
|             <OverwriteConfirmDialog.Action | ||||
|               title={t("overwriteConfirm.action.excalidrawPlus.title")} | ||||
|               actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")} | ||||
|               onClick={() => { | ||||
|                 exportToExcalidrawPlus( | ||||
|                   excalidrawAPI.getSceneElements(), | ||||
|                   excalidrawAPI.getAppState(), | ||||
|                   excalidrawAPI.getFiles(), | ||||
|                 ); | ||||
|               }} | ||||
|             > | ||||
|               {t("overwriteConfirm.action.excalidrawPlus.description")} | ||||
|             </OverwriteConfirmDialog.Action> | ||||
|           )} | ||||
|         </OverwriteConfirmDialog> | ||||
|         <AppFooter /> | ||||
|         {isCollaborating && isOffline && ( | ||||
|           <div className="collab-offline-warning"> | ||||
|             {t("alerts.collabOfflineWarning")} | ||||
|           </div> | ||||
|         )} | ||||
|         {latestShareableLink && ( | ||||
|           <ShareableLinkDialog | ||||
|             link={latestShareableLink} | ||||
|             onCloseRequest={() => setLatestShareableLink(null)} | ||||
|             setErrorMessage={setErrorMessage} | ||||
|           /> | ||||
|         )} | ||||
|         {excalidrawAPI && !isCollabDisabled && ( | ||||
|           <Collab excalidrawAPI={excalidrawAPI} /> | ||||
|         )} | ||||
|         {errorMessage && ( | ||||
|           <ErrorDialog onClose={() => setErrorMessage("")}> | ||||
|             {errorMessage} | ||||
|           </ErrorDialog> | ||||
|         )} | ||||
|       </Excalidraw> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const ExcalidrawApp = () => { | ||||
|   return ( | ||||
|     <TopErrorBoundary> | ||||
|       <Provider unstable_createStore={() => appJotaiStore}> | ||||
|         <ExcalidrawWrapper /> | ||||
|       </Provider> | ||||
|     </TopErrorBoundary> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ExcalidrawApp; | ||||
|   | ||||
							
								
								
									
										40
									
								
								excalidraw-app/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | ||||
| { | ||||
|   "name": "excalidraw-app", | ||||
|   "version": "1.0.0", | ||||
|   "private": true, | ||||
|   "homepage": ".", | ||||
|   "browserslist": { | ||||
|     "production": [ | ||||
|       ">0.2%", | ||||
|       "not dead", | ||||
|       "not ie <= 11", | ||||
|       "not op_mini all", | ||||
|       "not safari < 12", | ||||
|       "not kaios <= 2.5", | ||||
|       "not edge < 79", | ||||
|       "not chrome < 70", | ||||
|       "not and_uc < 13", | ||||
|       "not samsung < 10" | ||||
|     ], | ||||
|     "development": [ | ||||
|       "last 1 chrome version", | ||||
|       "last 1 firefox version", | ||||
|       "last 1 safari version" | ||||
|     ] | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=18.0.0" | ||||
|   }, | ||||
|   "dependencies": {}, | ||||
|   "prettier": "@excalidraw/prettier-config", | ||||
|   "scripts": { | ||||
|     "build-node": "node ./scripts/build-node.js", | ||||
|     "build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build", | ||||
|     "build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build", | ||||
|     "build:version": "node ../scripts/build-version.js", | ||||
|     "build": "yarn build:app && yarn build:version", | ||||
|     "start": "yarn && vite", | ||||
|     "start:production": "npm run build && npx http-server build -a localhost -p 5001 -o", | ||||
|     "build:preview": "yarn build && vite preview --port 5000" | ||||
|   } | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| @import "../../src/css/variables.module"; | ||||
| @import "../../packages/excalidraw/css/variables.module.scss"; | ||||
| 
 | ||||
| .excalidraw { | ||||
|   .RoomDialog { | ||||
|   .ShareDialog { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 1.5rem; | ||||
| @@ -10,8 +10,25 @@ | ||||
|       height: calc(100vh - 5rem); | ||||
|     } | ||||
| 
 | ||||
|     &__separator { | ||||
|       border-top: 1px solid var(--dialog-border-color); | ||||
|       text-align: center; | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
|       margin-top: 1em; | ||||
| 
 | ||||
|       span { | ||||
|         background: var(--island-bg-color); | ||||
|         padding: 0px 0.75rem; | ||||
|         transform: translateY(-1ch); | ||||
|         display: inline-flex; | ||||
|         line-height: 1; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &__popover { | ||||
|       @keyframes RoomDialog__popover__scaleIn { | ||||
|       @keyframes ShareDialog__popover__scaleIn { | ||||
|         from { | ||||
|           opacity: 0; | ||||
|         } | ||||
| @@ -50,10 +67,10 @@ | ||||
|       } | ||||
| 
 | ||||
|       transform-origin: var(--radix-popover-content-transform-origin); | ||||
|       animation: RoomDialog__popover__scaleIn 150ms ease-out; | ||||
|       animation: ShareDialog__popover__scaleIn 150ms ease-out; | ||||
|     } | ||||
| 
 | ||||
|     &__inactive { | ||||
|     &__picker { | ||||
|       font-family: "Assistant"; | ||||
| 
 | ||||
|       &__illustration { | ||||
| @@ -95,7 +112,7 @@ | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       &__start_session { | ||||
|       &__button { | ||||
|         display: flex; | ||||
| 
 | ||||
|         align-items: center; | ||||
							
								
								
									
										290
									
								
								excalidraw-app/share/ShareDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,290 @@ | ||||
| import { useRef, useState } from "react"; | ||||
| import * as Popover from "@radix-ui/react-popover"; | ||||
| import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; | ||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | ||||
| import { getFrame } from "../../packages/excalidraw/utils"; | ||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | ||||
| import { KEYS } from "../../packages/excalidraw/keys"; | ||||
| import { Dialog } from "../../packages/excalidraw/components/Dialog"; | ||||
| import { | ||||
|   copyIcon, | ||||
|   LinkIcon, | ||||
|   playerPlayIcon, | ||||
|   playerStopFilledIcon, | ||||
|   share, | ||||
|   shareIOS, | ||||
|   shareWindows, | ||||
|   tablerCheckIcon, | ||||
| } from "../../packages/excalidraw/components/icons"; | ||||
| import { TextField } from "../../packages/excalidraw/components/TextField"; | ||||
| import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; | ||||
| import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab"; | ||||
| import { atom, useAtom, useAtomValue } from "jotai"; | ||||
|  | ||||
| import "./ShareDialog.scss"; | ||||
|  | ||||
| type OnExportToBackend = () => void; | ||||
| type ShareDialogType = "share" | "collaborationOnly"; | ||||
|  | ||||
| export const shareDialogStateAtom = atom< | ||||
|   { isOpen: false } | { isOpen: true; type: ShareDialogType } | ||||
| >({ isOpen: false }); | ||||
|  | ||||
| const getShareIcon = () => { | ||||
|   const navigator = window.navigator as any; | ||||
|   const isAppleBrowser = /Apple/.test(navigator.vendor); | ||||
|   const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1; | ||||
|  | ||||
|   if (isAppleBrowser) { | ||||
|     return shareIOS; | ||||
|   } else if (isWindowsBrowser) { | ||||
|     return shareWindows; | ||||
|   } | ||||
|  | ||||
|   return share; | ||||
| }; | ||||
|  | ||||
| export type ShareDialogProps = { | ||||
|   collabAPI: CollabAPI | null; | ||||
|   handleClose: () => void; | ||||
|   onExportToBackend: OnExportToBackend; | ||||
|   type: ShareDialogType; | ||||
| }; | ||||
|  | ||||
| const ActiveRoomDialog = ({ | ||||
|   collabAPI, | ||||
|   activeRoomLink, | ||||
|   handleClose, | ||||
| }: { | ||||
|   collabAPI: CollabAPI; | ||||
|   activeRoomLink: string; | ||||
|   handleClose: () => void; | ||||
| }) => { | ||||
|   const { t } = useI18n(); | ||||
|   const [justCopied, setJustCopied] = useState(false); | ||||
|   const timerRef = useRef<number>(0); | ||||
|   const ref = useRef<HTMLInputElement>(null); | ||||
|   const isShareSupported = "share" in navigator; | ||||
|  | ||||
|   const copyRoomLink = async () => { | ||||
|     try { | ||||
|       await copyTextToSystemClipboard(activeRoomLink); | ||||
|  | ||||
|       setJustCopied(true); | ||||
|  | ||||
|       if (timerRef.current) { | ||||
|         window.clearTimeout(timerRef.current); | ||||
|       } | ||||
|  | ||||
|       timerRef.current = window.setTimeout(() => { | ||||
|         setJustCopied(false); | ||||
|       }, 3000); | ||||
|     } catch (error: any) { | ||||
|       collabAPI.setErrorMessage(error.message); | ||||
|     } | ||||
|  | ||||
|     ref.current?.select(); | ||||
|   }; | ||||
|  | ||||
|   const shareRoomLink = async () => { | ||||
|     try { | ||||
|       await navigator.share({ | ||||
|         title: t("roomDialog.shareTitle"), | ||||
|         text: t("roomDialog.shareTitle"), | ||||
|         url: activeRoomLink, | ||||
|       }); | ||||
|     } catch (error: any) { | ||||
|       // Just ignore. | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <h3 className="ShareDialog__active__header"> | ||||
|         {t("labels.liveCollaboration").replace(/\./g, "")} | ||||
|       </h3> | ||||
|       <TextField | ||||
|         defaultValue={collabAPI.getUsername()} | ||||
|         placeholder="Your name" | ||||
|         label="Your name" | ||||
|         onChange={collabAPI.setUsername} | ||||
|         onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()} | ||||
|       /> | ||||
|       <div className="ShareDialog__active__linkRow"> | ||||
|         <TextField | ||||
|           ref={ref} | ||||
|           label="Link" | ||||
|           readonly | ||||
|           fullWidth | ||||
|           value={activeRoomLink} | ||||
|         /> | ||||
|         {isShareSupported && ( | ||||
|           <FilledButton | ||||
|             size="large" | ||||
|             variant="icon" | ||||
|             label="Share" | ||||
|             icon={getShareIcon()} | ||||
|             className="ShareDialog__active__share" | ||||
|             onClick={shareRoomLink} | ||||
|           /> | ||||
|         )} | ||||
|         <Popover.Root open={justCopied}> | ||||
|           <Popover.Trigger asChild> | ||||
|             <FilledButton | ||||
|               size="large" | ||||
|               label="Copy link" | ||||
|               icon={copyIcon} | ||||
|               onClick={copyRoomLink} | ||||
|             /> | ||||
|           </Popover.Trigger> | ||||
|           <Popover.Content | ||||
|             onOpenAutoFocus={(event) => event.preventDefault()} | ||||
|             onCloseAutoFocus={(event) => event.preventDefault()} | ||||
|             className="ShareDialog__popover" | ||||
|             side="top" | ||||
|             align="end" | ||||
|             sideOffset={5.5} | ||||
|           > | ||||
|             {tablerCheckIcon} copied | ||||
|           </Popover.Content> | ||||
|         </Popover.Root> | ||||
|       </div> | ||||
|       <div className="ShareDialog__active__description"> | ||||
|         <p> | ||||
|           <span | ||||
|             role="img" | ||||
|             aria-hidden="true" | ||||
|             className="ShareDialog__active__description__emoji" | ||||
|           > | ||||
|             🔒{" "} | ||||
|           </span> | ||||
|           {t("roomDialog.desc_privacy")} | ||||
|         </p> | ||||
|         <p>{t("roomDialog.desc_exitSession")}</p> | ||||
|       </div> | ||||
|  | ||||
|       <div className="ShareDialog__active__actions"> | ||||
|         <FilledButton | ||||
|           size="large" | ||||
|           variant="outlined" | ||||
|           color="danger" | ||||
|           label={t("roomDialog.button_stopSession")} | ||||
|           icon={playerStopFilledIcon} | ||||
|           onClick={() => { | ||||
|             trackEvent("share", "room closed"); | ||||
|             collabAPI.stopCollaboration(); | ||||
|             if (!collabAPI.isCollaborating()) { | ||||
|               handleClose(); | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const ShareDialogPicker = (props: ShareDialogProps) => { | ||||
|   const { t } = useI18n(); | ||||
|  | ||||
|   const { collabAPI } = props; | ||||
|  | ||||
|   const startCollabJSX = collabAPI ? ( | ||||
|     <> | ||||
|       <div className="ShareDialog__picker__header"> | ||||
|         {t("labels.liveCollaboration").replace(/\./g, "")} | ||||
|       </div> | ||||
|  | ||||
|       <div className="ShareDialog__picker__description"> | ||||
|         <div style={{ marginBottom: "1em" }}>{t("roomDialog.desc_intro")}</div> | ||||
|         {t("roomDialog.desc_privacy")} | ||||
|       </div> | ||||
|  | ||||
|       <div className="ShareDialog__picker__button"> | ||||
|         <FilledButton | ||||
|           size="large" | ||||
|           label={t("roomDialog.button_startSession")} | ||||
|           icon={playerPlayIcon} | ||||
|           onClick={() => { | ||||
|             trackEvent("share", "room creation", `ui (${getFrame()})`); | ||||
|             collabAPI.startCollaboration(null); | ||||
|           }} | ||||
|         /> | ||||
|       </div> | ||||
|  | ||||
|       {props.type === "share" && ( | ||||
|         <div className="ShareDialog__separator"> | ||||
|           <span>{t("shareDialog.or")}</span> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ) : null; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {startCollabJSX} | ||||
|  | ||||
|       {props.type === "share" && ( | ||||
|         <> | ||||
|           <div className="ShareDialog__picker__header"> | ||||
|             {t("exportDialog.link_title")} | ||||
|           </div> | ||||
|           <div className="ShareDialog__picker__description"> | ||||
|             {t("exportDialog.link_details")} | ||||
|           </div> | ||||
|  | ||||
|           <div className="ShareDialog__picker__button"> | ||||
|             <FilledButton | ||||
|               size="large" | ||||
|               label={t("exportDialog.link_button")} | ||||
|               icon={LinkIcon} | ||||
|               onClick={async () => { | ||||
|                 await props.onExportToBackend(); | ||||
|                 props.handleClose(); | ||||
|               }} | ||||
|             /> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const ShareDialogInner = (props: ShareDialogProps) => { | ||||
|   const activeRoomLink = useAtomValue(activeRoomLinkAtom); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog size="small" onCloseRequest={props.handleClose} title={false}> | ||||
|       <div className="ShareDialog"> | ||||
|         {props.collabAPI && activeRoomLink ? ( | ||||
|           <ActiveRoomDialog | ||||
|             collabAPI={props.collabAPI} | ||||
|             activeRoomLink={activeRoomLink} | ||||
|             handleClose={props.handleClose} | ||||
|           /> | ||||
|         ) : ( | ||||
|           <ShareDialogPicker {...props} /> | ||||
|         )} | ||||
|       </div> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const ShareDialog = (props: { | ||||
|   collabAPI: CollabAPI | null; | ||||
|   onExportToBackend: OnExportToBackend; | ||||
| }) => { | ||||
|   const [shareDialogState, setShareDialogState] = useAtom(shareDialogStateAtom); | ||||
|  | ||||
|   if (!shareDialogState.isOpen) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <ShareDialogInner | ||||
|       handleClose={() => setShareDialogState({ isOpen: false })} | ||||
|       collabAPI={props.collabAPI} | ||||
|       onExportToBackend={props.onExportToBackend} | ||||
|       type={shareDialogState.type} | ||||
|     ></ShareDialogInner> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,8 +1,13 @@ | ||||
| import { defaultLang } from "../../src/i18n"; | ||||
| import { UI } from "../../src/tests/helpers/ui"; | ||||
| import { screen, fireEvent, waitFor, render } from "../../src/tests/test-utils"; | ||||
| import { defaultLang } from "../../packages/excalidraw/i18n"; | ||||
| import { UI } from "../../packages/excalidraw/tests/helpers/ui"; | ||||
| import { | ||||
|   screen, | ||||
|   fireEvent, | ||||
|   waitFor, | ||||
|   render, | ||||
| } from "../../packages/excalidraw/tests/test-utils"; | ||||
|  | ||||
| import ExcalidrawApp from "../../excalidraw-app"; | ||||
| import ExcalidrawApp from "../App"; | ||||
|  | ||||
| describe("Test LanguageList", () => { | ||||
|   it("rerenders UI on language change", async () => { | ||||
|   | ||||