mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 02:44:50 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			dwelle/bum
			...
			feat-actio
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 60bab6a428 | 
| @@ -6,6 +6,6 @@ | |||||||
| !.prettierrc | !.prettierrc | ||||||
| !package.json | !package.json | ||||||
| !public/ | !public/ | ||||||
| !packages/ | !src/ | ||||||
| !tsconfig.json | !tsconfig.json | ||||||
| !yarn.lock | !yarn.lock | ||||||
|   | |||||||
| @@ -7,11 +7,12 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu | |||||||
| # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room) | # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room) | ||||||
| VITE_APP_WS_SERVER_URL=http://localhost:3002 | 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_LP=https://plus.excalidraw.com | ||||||
| VITE_APP_PLUS_APP=https://app.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"}' | 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! | # put these in your .env.local, or make sure you don't commit! | ||||||
|   | |||||||
| @@ -4,13 +4,14 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/ | |||||||
| VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com | VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com | ||||||
| VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries | 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_LP=https://plus.excalidraw.com | ||||||
| VITE_APP_PLUS_APP=https://app.excalidraw.com | VITE_APP_PLUS_APP=https://app.excalidraw.com | ||||||
|  |  | ||||||
| VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com | # Fill to set socket server URL used for collaboration. | ||||||
|  | # Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow | ||||||
| # socket server URL used for collaboration | VITE_APP_WS_SERVER_URL= | ||||||
| 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"}' | 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/ | firebase/ | ||||||
| dist/ | dist/ | ||||||
| public/workbox | public/workbox | ||||||
| packages/excalidraw/types | src/packages/excalidraw/types | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/autorelease-excalidraw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/autorelease-excalidraw.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,5 +23,5 @@ jobs: | |||||||
|           NPM_TOKEN: ${{ secrets.NPM_TOKEN }} |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | ||||||
|       - name: Auto release |       - name: Auto release | ||||||
|         run: | |         run: | | ||||||
|           yarn add @actions/core -W |           yarn add @actions/core | ||||||
|           yarn autorelease |           yarn autorelease | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/autorelease-preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/autorelease-preview.yml
									
									
									
									
										vendored
									
									
								
							| @@ -44,7 +44,7 @@ jobs: | |||||||
|       - name: Auto release preview |       - name: Auto release preview | ||||||
|         id: "autorelease" |         id: "autorelease" | ||||||
|         run: | |         run: | | ||||||
|           yarn add @actions/core -W |           yarn add @actions/core | ||||||
|           yarn autorelease preview ${{ github.event.issue.number }} |           yarn autorelease preview ${{ github.event.issue.number }} | ||||||
|       - name: Post comment post release |       - name: Post comment post release | ||||||
|         if: always() |         if: always() | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Install and lint |       - name: Install and lint | ||||||
|         run: | |         run: | | ||||||
|           yarn install |           yarn --frozen-lockfile | ||||||
|           yarn test:other |           yarn test:other | ||||||
|           yarn test:code |           yarn test:code | ||||||
|           yarn test:typecheck |           yarn test:typecheck | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/locales-coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/locales-coverage.yml
									
									
									
									
										vendored
									
									
								
							| @@ -22,11 +22,11 @@ jobs: | |||||||
|       - name: Create report file |       - name: Create report file | ||||||
|         run: | |         run: | | ||||||
|           yarn locales-coverage |           yarn locales-coverage | ||||||
|           FILE_CHANGED=$(git diff packages/excalidraw/locales/percentages.json) |           FILE_CHANGED=$(git diff src/locales/percentages.json) | ||||||
|           if [ ! -z "${FILE_CHANGED}" ]; then |           if [ ! -z "${FILE_CHANGED}" ]; then | ||||||
|             git config --global user.name 'Excalidraw Bot' |             git config --global user.name 'Excalidraw Bot' | ||||||
|             git config --global user.email 'bot@excalidraw.com' |             git config --global user.email 'bot@excalidraw.com' | ||||||
|             git add packages/excalidraw/locales/percentages.json |             git add src/locales/percentages.json | ||||||
|             git commit -am "Auto commit: Calculate translation coverage" |             git commit -am "Auto commit: Calculate translation coverage" | ||||||
|             git push |             git push | ||||||
|           fi |           fi | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								.github/workflows/size-limit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/size-limit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,14 +15,16 @@ jobs: | |||||||
|         uses: actions/setup-node@v3 |         uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: 18.x |           node-version: 18.x | ||||||
|       - name: Install in packages/excalidraw |       - name: Install | ||||||
|         run: yarn |         run: yarn --frozen-lockfile | ||||||
|         working-directory: packages/excalidraw |       - name: Install in src/packages/excalidraw | ||||||
|  |         run: yarn --frozen-lockfile | ||||||
|  |         working-directory: src/packages/excalidraw | ||||||
|         env: |         env: | ||||||
|           CI: true |           CI: true | ||||||
|       - uses: andresz1/size-limit-action@v1 |       - uses: andresz1/size-limit-action@v1 | ||||||
|         with: |         with: | ||||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} |           github_token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           build_script: build:esm |           build_script: build:umd | ||||||
|           skip_step: install |           skip_step: install | ||||||
|           directory: packages/excalidraw |           directory: src/packages/excalidraw | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/test-coverage-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-coverage-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           node-version: "18.x" |           node-version: "18.x" | ||||||
|       - name: "Install Deps" |       - name: "Install Deps" | ||||||
|         run: yarn install |         run: yarn --frozen-lockfile | ||||||
|       - name: "Test Coverage" |       - name: "Test Coverage" | ||||||
|         run: yarn test:coverage |         run: yarn test:coverage | ||||||
|       - name: "Report Coverage" |       - name: "Report Coverage" | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,5 +13,5 @@ jobs: | |||||||
|           node-version: 18.x |           node-version: 18.x | ||||||
|       - name: Install and test |       - name: Install and test | ||||||
|         run: | |         run: | | ||||||
|           yarn install |           yarn --frozen-lockfile | ||||||
|           yarn test:app |           yarn test:app | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -21,9 +21,10 @@ npm-debug.log* | |||||||
| package-lock.json | package-lock.json | ||||||
| yarn-debug.log* | yarn-debug.log* | ||||||
| yarn-error.log* | yarn-error.log* | ||||||
| packages/excalidraw/types | 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 | ||||||
| coverage | coverage | ||||||
| dev-dist | dev-dist | ||||||
| html | html | ||||||
| examples/**/bundle.* |  | ||||||
| meta*.json |  | ||||||
| @@ -85,7 +85,7 @@ We'll be adding these features as drop-in plugins for the npm package in the fut | |||||||
|  |  | ||||||
| ## Quick start | ## Quick start | ||||||
|  |  | ||||||
| **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). | Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw): | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| npm install react react-dom @excalidraw/excalidraw | npm install react react-dom @excalidraw/excalidraw | ||||||
| @@ -97,7 +97,7 @@ or via yarn | |||||||
| yarn add react react-dom @excalidraw/excalidraw | yarn add react react-dom @excalidraw/excalidraw | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Check out our [documentation](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/installation) for more details! | Don't forget to check out our [Documentation](https://docs.excalidraw.com)! | ||||||
|  |  | ||||||
| ## Contributing | ## Contributing | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,3 @@ | |||||||
| files: | files: | ||||||
|   - source: /packages/excalidraw/locales/en.json |   - source: /src/locales/en.json | ||||||
|     translation: /packages/excalidraw/locales/%locale%.json |     translation: /src/locales/%locale%.json | ||||||
|   | |||||||
| @@ -133,7 +133,7 @@ function App() { | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items. | Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items. | ||||||
|  |  | ||||||
| ### MainMenu.Group | ### MainMenu.Group | ||||||
|  |  | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme` | |||||||
|  |  | ||||||
| ### MIME_TYPES | ### MIME_TYPES | ||||||
|  |  | ||||||
| [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L101) contains all the mime types supported by `Excalidraw`. | [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L101) contains all the mime types supported by `Excalidraw`. | ||||||
|  |  | ||||||
| **How to use ** | **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. | 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/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). | 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). | ||||||
|  |  | ||||||
| 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). | 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). | ||||||
|  |  | ||||||
| ## convertToExcalidrawElements | ## convertToExcalidrawElements | ||||||
|  |  | ||||||
| @@ -19,7 +19,7 @@ convertToExcalidrawElements( | |||||||
|  |  | ||||||
| | Name | Type | Default | Description | | | Name | Type | Default | Description | | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | | ||||||
| | `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. | | | `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. | | ||||||
| | `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`. | | | `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_** | **_How to use_** | ||||||
| @@ -71,7 +71,7 @@ function App() { | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L27) as well to decorate the shapes. | You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes. | ||||||
|  |  | ||||||
| :::info | :::info | ||||||
|  |  | ||||||
| @@ -192,7 +192,7 @@ convertToExcalidrawElements([ | |||||||
|  |  | ||||||
| ### Text Containers | ### Text Containers | ||||||
|  |  | ||||||
| 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. | 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. | ||||||
|  |  | ||||||
| If you don't provide the dimensions of container, we calculate it based of the label dimensions. | If you don't provide the dimensions of container, we calculate it based of the label dimensions. | ||||||
|  |  | ||||||
| @@ -326,7 +326,7 @@ convertToExcalidrawElements([ | |||||||
|  |  | ||||||
| ### Arrow bindings | ### Arrow bindings | ||||||
|  |  | ||||||
| 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 | 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 | ||||||
|  |  | ||||||
| ```js | ```js | ||||||
| convertToExcalidrawElements([ | convertToExcalidrawElements([ | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (api:{" "} |   (api:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616"> | ||||||
|     ExcalidrawAPI |     ExcalidrawAPI | ||||||
|   </a> |   </a> | ||||||
|   ) => void; |   ) => void; | ||||||
| @@ -17,12 +17,12 @@ export default function App() { | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| 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: | 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: | ||||||
|  |  | ||||||
| | API | Signature | Usage | | | API | Signature | Usage | | ||||||
| | --- | --- | --- | | | --- | --- | --- | | ||||||
| | [updateScene](#updatescene) | `function` | updates the scene with the sceneData | | | [updateScene](#updatescene) | `function` | updates the scene with the sceneData | | ||||||
| | [updateLibrary](#updatelibrary) | `function` | updates the library | | | [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData | | ||||||
| | [addFiles](#addfiles) | `function` | add files data to the appState | | | [addFiles](#addfiles) | `function` | add files data to the appState | | ||||||
| | [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. | | | [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. | | ||||||
| | [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene | | | [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene | | ||||||
| @@ -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 | | | [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 | | | [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 | | | [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas | | ||||||
| | [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off | | | [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off | | ||||||
| | [onChange](#onChange) | `function` | Subscribes to change events | | | [onChange](#onChange) | `function` | Subscribes to change events | | ||||||
| | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events | | | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events | | ||||||
| | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events | | | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events | | ||||||
| @@ -52,7 +52,7 @@ Additionally `ready` and `readyPromise` from the API have been discontinued. The | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (scene:{" "} |   (scene:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L339"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L339"> | ||||||
|     sceneData |     sceneData | ||||||
|   </a> |   </a> | ||||||
|   ) => void |   ) => void | ||||||
| @@ -62,10 +62,10 @@ You can use this function to update the scene with the sceneData. It accepts the | |||||||
|  |  | ||||||
| | Name | Type | Description | | | Name | Type | Description | | ||||||
| | --- | --- | --- | | | --- | --- | --- | | ||||||
| | `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene | | | `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/packages/excalidraw/data/types.ts#L39) | The `appState` 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/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators 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. | | ||||||
| | `commitToStore` | `boolean` | Implies if the change should be captured and commited to the `store`. Commited changes are emmitted and listened to by other components, such as `History` for undo / redo purposes. Defaults to `false`. | | | `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. | | ||||||
|  |  | ||||||
| ```jsx live | ```jsx live | ||||||
| function App() { | function App() { | ||||||
| @@ -115,7 +115,7 @@ function App() { | |||||||
|       <button className="custom-button" onClick={updateScene}> |       <button className="custom-button" onClick={updateScene}> | ||||||
|         Update Scene |         Update Scene | ||||||
|       </button> |       </button> | ||||||
|       <Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} /> |       <Excalidraw ref={(api) => setExcalidrawAPI(api)} /> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| @@ -125,13 +125,13 @@ function App() { | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (opts: { <br /> libraryItems:{" "} |   (opts: { <br /> libraryItems:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249"> | ||||||
|     LibraryItemsSource |     LibraryItemsSource | ||||||
|   </a> |   </a> | ||||||
|   ;<br /> merge?: boolean; <br /> prompt?: boolean; |   ;<br /> merge?: boolean; <br /> prompt?: boolean; | ||||||
|   <br /> openLibraryMenu?: boolean; |   <br /> openLibraryMenu?: boolean; | ||||||
|   <br /> defaultStatus?: "unpublished" | "published"; <br /> }) => Promise< |   <br /> defaultStatus?: "unpublished" | "published"; <br /> }) => Promise< | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L246"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L246"> | ||||||
|     LibraryItems |     LibraryItems | ||||||
|   </a> |   </a> | ||||||
|   > |   > | ||||||
| @@ -141,7 +141,7 @@ You can use this function to update the library. It accepts the below attributes | |||||||
|  |  | ||||||
| | Name | Type | Default | Description | | | Name | Type | Default | Description | | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | | ||||||
| | `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library | | | `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library | | ||||||
| | `merge` | boolean | `false` | Whether to merge with existing library items. | | | `merge` | boolean | `false` | Whether to merge with existing library items. | | ||||||
| | `prompt` | boolean | `false` | Whether to prompt user for confirmation. | | | `prompt` | boolean | `false` | Whether to prompt user for confirmation. | | ||||||
| | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. | | | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. | | ||||||
| @@ -188,8 +188,8 @@ function App() { | |||||||
|         Update Library |         Update Library | ||||||
|       </button> |       </button> | ||||||
|       <Excalidraw |       <Excalidraw | ||||||
|         excalidrawAPI={(api) => setExcalidrawAPI(api)} |         ref={(api) => setExcalidrawAPI(api)} | ||||||
|         // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js |         // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js | ||||||
|         initialData={{ |         initialData={{ | ||||||
|           libraryItems: initialData.libraryItems, |           libraryItems: initialData.libraryItems, | ||||||
|           appState: { openSidebar: "library" }, |           appState: { openSidebar: "library" }, | ||||||
| @@ -204,7 +204,7 @@ function App() { | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (files:{" "} |   (files:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59"> | ||||||
|     BinaryFileData |     BinaryFileData | ||||||
|   </a> |   </a> | ||||||
|   ) => void |   ) => void | ||||||
| @@ -224,7 +224,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   () =>{" "} |   () =>{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115"> | ||||||
|     ExcalidrawElement[] |     ExcalidrawElement[] | ||||||
|   </a> |   </a> | ||||||
| </pre> | </pre> | ||||||
| @@ -235,7 +235,7 @@ Returns all the elements including the deleted in the scene. | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   () => NonDeleted< |   () => NonDeleted< | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115"> | ||||||
|     ExcalidrawElement |     ExcalidrawElement | ||||||
|   </a> |   </a> | ||||||
|   []> |   []> | ||||||
| @@ -247,7 +247,7 @@ Returns all the elements excluding the deleted in the scene | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   () =>{" "} |   () =>{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> | ||||||
|     AppState |     AppState | ||||||
|   </a> |   </a> | ||||||
| </pre> | </pre> | ||||||
| @@ -288,7 +288,7 @@ Scroll the nearest element out of the elements supplied to the center of the vie | |||||||
|  |  | ||||||
| | Attribute | type | default | Description | | | Attribute | type | default | Description | | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | | ||||||
| | 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. | | | 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. | | ||||||
| | 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.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.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%) | | | 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> | <pre> | ||||||
|   () =>{" "} |   () =>{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82"> | ||||||
|     files |     files | ||||||
|   </a> |   </a> | ||||||
| </pre> | </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 | | | Name | Type | Default | Description | | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | | ||||||
| | `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` | | | `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` | | ||||||
| | `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 | | | `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 | ## setCursor | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| # initialData | # initialData | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| { 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> } | { 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> } | ||||||
| </pre> | </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. | 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 | | | Name | Type | Description | | ||||||
| | --- | --- | --- | | | --- | --- | --- | | ||||||
| | `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. | | | `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/packages/excalidraw/types.ts#L95) | The `AppState` 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. | | ||||||
| | `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 | | | `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/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. | | | `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/packages/excalidraw/types.ts#L82) | The `files` added to the scene. | | | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/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. | 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 | | | [`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 | | | [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component | | ||||||
| | [`name`](#name) | `string` |  | Name of the drawing | | | [`name`](#name) | `string` |  | Name of the drawing | | ||||||
| | [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) | | | [`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) | | ||||||
| | [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. | | | [`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. | | | [`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 | | | [`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 | ### 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/packages/excalidraw/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/src/element/types.ts#L66) and is optional. | ||||||
|  |  | ||||||
| You can use this to add any extra information you need to keep track of. | 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; | (excalidrawElements, appState, files) => void; | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| 1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) in the scene. | 1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene. | ||||||
|  |  | ||||||
| 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene. | 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene. | ||||||
|  |  | ||||||
| 3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) which are added to the scene. | 3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene. | ||||||
|  |  | ||||||
| Here you can try saving the data to your backend or local storage for example. | 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"]` | 2.`button`: The position of the button. This will be one of `["down", "up"]` | ||||||
|  |  | ||||||
| 3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L131) map of the scene | 3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) map of the scene | ||||||
|  |  | ||||||
| ```js | ```js | ||||||
| (exportedElements, appState, canvas) => void | (exportedElements, appState, canvas) => void | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| 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. | 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/packages/excalidraw/types.ts#L95) of the scene. | 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene. | ||||||
| 3. `canvas`: The `HTMLCanvasElement` of the scene. | 3. `canvas`: The `HTMLCanvasElement` of the scene. | ||||||
|  |  | ||||||
| ### onPointerDown | ### onPointerDown | ||||||
| @@ -96,11 +96,11 @@ This prop if passed will be triggered on pointer down events and has the below s | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| (activeTool:{" "} | (activeTool:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115"> | ||||||
|     {" "} |     {" "} | ||||||
|     AppState["activeTool"] |     AppState["activeTool"] | ||||||
|   </a> |   </a> | ||||||
|   , pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L424"> |   , pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424"> | ||||||
|     PointerDownState |     PointerDownState | ||||||
|   </a>) => void |   </a>) => void | ||||||
| </pre> | </pre> | ||||||
| @@ -119,7 +119,7 @@ This callback is triggered if passed when something is pasted into the scene. Yo | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (data:{" "} |   (data:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/clipboard.ts#L18"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18"> | ||||||
|     ClipboardData |     ClipboardData | ||||||
|   </a> |   </a> | ||||||
|   , event: ClipboardEvent | null) => boolean |   , event: ClipboardEvent | null) => boolean | ||||||
| @@ -135,7 +135,7 @@ This callback if supplied will get triggered when the library is updated and has | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (items:{" "} |   (items:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200"> | ||||||
|     LibraryItems |     LibraryItems | ||||||
|   </a> |   </a> | ||||||
|   ) => void | Promise<any> |   ) => void | Promise<any> | ||||||
| @@ -149,7 +149,7 @@ This prop if passed will be triggered when clicked on `link`. To handle the redi | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (element:{" "} |   (element:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> | ||||||
|     ExcalidrawElement |     ExcalidrawElement | ||||||
|   </a> |   </a> | ||||||
|   , event: CustomEvent<{ nativeEvent: MouseEvent }>) => void |   , event: CustomEvent<{ nativeEvent: MouseEvent }>) => void | ||||||
| @@ -182,7 +182,7 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback( | |||||||
|  |  | ||||||
| ### langCode | ### langCode | ||||||
|  |  | ||||||
| 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. | 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. | ||||||
|  |  | ||||||
| ```js | ```js | ||||||
| import { defaultLang, languages } from "@excalidraw/excalidraw"; | import { defaultLang, languages } from "@excalidraw/excalidraw"; | ||||||
| @@ -191,7 +191,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw"; | |||||||
| | name | type | | | name | type | | ||||||
| | --- | --- | | | --- | --- | | ||||||
| | `defaultLang` | `string` | | | `defaultLang` | `string` | | ||||||
| | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | | | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | | ||||||
|  |  | ||||||
| ### viewModeEnabled | ### viewModeEnabled | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (isMobile: boolean, appState: |   (isMobile: boolean, appState: | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> | ||||||
|     AppState |     AppState | ||||||
|   </a>) => JSX | null |   </a>) => JSX | null | ||||||
| </pre> | </pre> | ||||||
| @@ -66,7 +66,7 @@ function App() { | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   (element: NonDeleted<ExcalidrawEmbeddableElement>, appState:{" "} |   (element: NonDeleted<ExcalidrawEmbeddableElement>, appState:{" "} | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> | ||||||
|     AppState |     AppState | ||||||
|   </a> |   </a> | ||||||
|   ) => JSX.Element | null |   ) => JSX.Element | null | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
|   { |   { | ||||||
|   <br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L372"> |   <br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L372"> | ||||||
|     CanvasActions |     CanvasActions | ||||||
|   </a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br /> |   </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 | ## 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/packages/excalidraw/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/src/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 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. | 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,7 +73,7 @@ function App() { | |||||||
|  |  | ||||||
| ## tools | ## 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. | Currently you can control the visibility of `image` tool via this prop. | ||||||
|  |  | ||||||
| | Prop | Type | Default | Description | | | Prop | Type | Default | Description | | ||||||
|   | |||||||
| @@ -20,16 +20,16 @@ exportToCanvas({<br/>  | |||||||
|   getDimensions,<br/>  |   getDimensions,<br/>  | ||||||
|   files,<br/>  |   files,<br/>  | ||||||
|   exportPadding?: number;<br/> |   exportPadding?: number;<br/> | ||||||
| }: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a> | }: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| | Name | Type | Default | Description | | | Name | Type | Default | Description | | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | | ||||||
| | `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) |  | The elements to be exported to canvas. | | | `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/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. | | | `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. | | ||||||
| | [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to  `1`), with which canvas is to be exported. | | | [`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. | | | `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/packages/excalidraw/types.ts#L59) | _ | The files added to the scene. | | | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. | | ||||||
| | `exportPadding` | `number` | `10` | The `padding` to be added on canvas. | | | `exportPadding` | `number` | `10` | The `padding` to be added on canvas. | | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -105,7 +105,7 @@ function App() { | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| exportToBlob(<br/>  | exportToBlob(<br/>  | ||||||
|   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L14">ExportOpts</a> & {<br/>  |   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {<br/>  | ||||||
|   mimeType?: string,<br/>  |   mimeType?: string,<br/>  | ||||||
|   quality?: number,<br/>  |   quality?: number,<br/>  | ||||||
|   exportPadding?: number;<br/> |   exportPadding?: number;<br/> | ||||||
| @@ -134,16 +134,16 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en- | |||||||
| <pre> | <pre> | ||||||
| exportToSvg({<br/>  | exportToSvg({<br/>  | ||||||
|   elements:   |   elements:   | ||||||
|     <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> |     <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> | ||||||
|       ExcalidrawElement[] |       ExcalidrawElement[] | ||||||
|     </a>,<br/>  |     </a>,<br/>  | ||||||
|   appState: |   appState: | ||||||
|     <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> AppState |     <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState | ||||||
|     </a>,<br/>  |     </a>,<br/>  | ||||||
|   exportPadding: number,<br/>  |   exportPadding: number,<br/>  | ||||||
|   metadata: string,<br/>  |   metadata: string,<br/>  | ||||||
|   files:  |   files:  | ||||||
|   <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59"> |   <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59"> | ||||||
|       BinaryFiles |       BinaryFiles | ||||||
|     </a>,<br/> |     </a>,<br/> | ||||||
| }); | }); | ||||||
| @@ -151,10 +151,10 @@ exportToSvg({<br/>  | |||||||
|  |  | ||||||
| | Name | Type | Default | Description | | | Name | Type | Default | Description | | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | | ||||||
| | elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) |  | The elements to exported as `svg `| | | 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/packages/excalidraw/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L11) | The `appState` of the scene | | | 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 | | ||||||
| | exportPadding | number | 10 | The `padding` to be added on canvas | | | exportPadding | number | 10 | The `padding` to be added on canvas | | ||||||
| | files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) | undefined | The `files` added to the scene. | | | files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. | | ||||||
|  |  | ||||||
| This function returns a promise which resolves to `svg` of the exported drawing. | 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> | <pre> | ||||||
| exportToClipboard(<br/>  | exportToClipboard(<br/>  | ||||||
|   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a> & {<br/>  |   opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & {<br/>  | ||||||
|   mimeType?: string,<br/>  |   mimeType?: string,<br/>  | ||||||
|   quality?: number;<br/>  |   quality?: number;<br/>  | ||||||
|   type: 'png' | 'svg' |'json'<br/> |   type: 'png' | 'svg' |'json'<br/> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ id: "restore" | |||||||
| **_Signature_** | **_Signature_** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| 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> | 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> | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| **_How to use_** | **_How to use_** | ||||||
| @@ -17,7 +17,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob | |||||||
| import { restoreAppState } from "@excalidraw/excalidraw"; | 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/packages/excalidraw/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/src/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.   | 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. | 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> | <pre> | ||||||
| restoreElements( | restoreElements( | ||||||
|   elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>  |   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/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/>  |   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/>  | ||||||
|   opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/> |   opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/> | ||||||
| ) | ) | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| | Prop | Type | Description | | | Prop | Type | Description | | ||||||
| | ---- | ---- | ---- | | | ---- | ---- | ---- | | ||||||
| | `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored | | | `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/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`. | | | [`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`. | | ||||||
| | [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements | | [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements | ||||||
|  |  | ||||||
| #### localElements | #### localElements | ||||||
| @@ -70,15 +70,15 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| restore( | restore( | ||||||
|   data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>  |   data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>  | ||||||
|   localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>  |   localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>  | ||||||
|   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/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/> |   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/> | ||||||
|   opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/> |   opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/> | ||||||
|  |  | ||||||
| ) | ) | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| 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`. | 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`. | ||||||
|  |  | ||||||
| **_How to use_** | **_How to use_** | ||||||
|  |  | ||||||
| @@ -93,7 +93,7 @@ This function makes sure elements and state is set to appropriate values and set | |||||||
| **_Signature_** | **_Signature_** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>  | restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>  | ||||||
| defaultStatus: "published" | "unpublished") | defaultStatus: "published" | "unpublished") | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ These are pure Javascript functions exported from the @excalidraw/excalidraw [`@ | |||||||
|  |  | ||||||
| ### serializeAsJSON | ### 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/packages/excalidraw/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/src/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. | 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> | <pre> | ||||||
| serializeAsJSON({<br/>  | serializeAsJSON({<br/>  | ||||||
|   elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<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/packages/excalidraw/types.ts#L95">AppState</a>,<br/> |   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>,<br/> | ||||||
| }): string | }): string | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| @@ -37,7 +37,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| serializeLibraryAsJSON( | serializeLibraryAsJSON( | ||||||
|   libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems[]</a>) |   libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>) | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| **How to use** | **How to use** | ||||||
| @@ -53,7 +53,7 @@ Returns `true` if element is invisibly small (e.g. width & height are zero). | |||||||
| **_Signature_** | **_Signature_** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| isInvisiblySmallElement(element:  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement</a>): boolean | isInvisiblySmallElement(element:  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>): boolean | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| **How to use** | **How to use** | ||||||
| @@ -80,10 +80,10 @@ excalidrawAPI.updateScene(scene); | |||||||
| <pre> | <pre> | ||||||
| loadFromBlob(<br/>  | loadFromBlob(<br/>  | ||||||
|   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<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/packages/excalidraw/types.ts#L95">AppState</a> | null,<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/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  |   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  | ||||||
|   fileHandle?: FileSystemHandle | null <br/> |   fileHandle?: FileSystemHandle | null <br/> | ||||||
| ) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L61">RestoredDataState</a>> | ) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L61">RestoredDataState</a>> | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### loadLibraryFromBlob | ### loadLibraryFromBlob | ||||||
| @@ -130,10 +130,10 @@ if (contents.type === MIME_TYPES.excalidraw) { | |||||||
| <pre> | <pre> | ||||||
| loadSceneOrLibraryFromBlob(<br/>  | loadSceneOrLibraryFromBlob(<br/>  | ||||||
|   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<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/packages/excalidraw/types.ts#L95">AppState</a> | null,<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/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  |   localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>  | ||||||
|   fileHandle?: FileSystemHandle | null<br/> |   fileHandle?: FileSystemHandle | null<br/> | ||||||
| ) => 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>}> | ) => 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>}> | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### getFreeDrawSvgPath | ### getFreeDrawSvgPath | ||||||
| @@ -149,7 +149,7 @@ import { getFreeDrawSvgPath } from "@excalidraw/excalidraw"; | |||||||
| **Signature** | **Signature** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L182">ExcalidrawFreeDrawElement</a>) | getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L182">ExcalidrawFreeDrawElement</a>) | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### isLinearElement | ### isLinearElement | ||||||
| @@ -165,7 +165,7 @@ import { isLinearElement } from "@excalidraw/excalidraw"; | |||||||
| **Signature** | **Signature** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L80">ExcalidrawElement</a>): boolean | isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### getNonDeletedElements | ### getNonDeletedElements | ||||||
| @@ -181,7 +181,7 @@ import { getNonDeletedElements } from "@excalidraw/excalidraw"; | |||||||
| **Signature** | **Signature** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| 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> | 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> | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### mergeLibraryItems | ### mergeLibraryItems | ||||||
| @@ -196,9 +196,9 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw"; | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| mergeLibraryItems(<br/>  | mergeLibraryItems(<br/>  | ||||||
|   localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a>,<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/packages/excalidraw/types.ts#L200">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/packages/excalidraw/types.ts#L250">LibraryItems</a> | ): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a> | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### parseLibraryTokensFromUrl | ### parseLibraryTokensFromUrl | ||||||
| @@ -239,8 +239,8 @@ export const App = () => { | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| useHandleLibrary(opts: {<br/>  | useHandleLibrary(opts: {<br/>  | ||||||
|   excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L494">ExcalidrawAPI</a>,<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/packages/excalidraw/types.ts#L253">LibraryItemsSource</a><br/> |   getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L253">LibraryItemsSource</a><br/> | ||||||
| }); | }); | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| @@ -253,7 +253,7 @@ This function returns the current `scene` version. | |||||||
| **_Signature_** | **_Signature_** | ||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| getSceneVersion(elements:  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>) | getSceneVersion(elements:  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>) | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| **How to use** | **How to use** | ||||||
| @@ -274,7 +274,7 @@ import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw"; | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| sceneCoordsToViewportCoords({ sceneX: number, sceneY: number },<br/>  | sceneCoordsToViewportCoords({ sceneX: number, sceneY: number },<br/>  | ||||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): { x: number, y: number } |   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): { x: number, y: number } | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### viewportCoordsToSceneCoords | ### viewportCoordsToSceneCoords | ||||||
| @@ -289,7 +289,7 @@ import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw"; | |||||||
|  |  | ||||||
| <pre> | <pre> | ||||||
| viewportCoordsToSceneCoords({ clientX: number, clientY: number },<br/>  | viewportCoordsToSceneCoords({ clientX: number, clientY: number },<br/>  | ||||||
|   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): {x: number, y: number} |   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): {x: number, y: number} | ||||||
| </pre> | </pre> | ||||||
|  |  | ||||||
| ### useDevice | ### useDevice | ||||||
| @@ -350,8 +350,8 @@ To help with localization, we export the following. | |||||||
| | name | type | | | name | type | | ||||||
| | --- | --- | | | --- | --- | | ||||||
| | `defaultLang` | `string` | | | `defaultLang` | `string` | | ||||||
| | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | | | `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | | ||||||
| | `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | | | `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | | ||||||
|  |  | ||||||
| ```js | ```js | ||||||
| import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw"; | 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-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. | - `--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/packages/excalidraw/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/src/css/theme.scss), though most of them will not make sense to override. | ||||||
|  |  | ||||||
| ```css showLineNumbers | ```css showLineNumbers | ||||||
| .custom-styles .excalidraw { | .custom-styles .excalidraw { | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the | |||||||
| 1. Install the dependencies | 1. Install the dependencies | ||||||
|  |  | ||||||
|    ```bash |    ```bash | ||||||
|    cd packages/excalidraw && yarn |    cd src/packages/excalidraw && yarn | ||||||
|    ``` |    ``` | ||||||
|  |  | ||||||
| 2. Start the example app | 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: { |  define: { | ||||||
|     "process.env.IS_PREACT": JSON.stringify("true"), |     "process.env.IS_PREACT": process.env.IS_PREACT, | ||||||
|   }, |   }, | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,9 +32,15 @@ function App() { | |||||||
|  |  | ||||||
| ### Next.js | ### Next.js | ||||||
|  |  | ||||||
| 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`. | Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. | ||||||
|  |  | ||||||
| If you want to only import `Excalidraw` component you can do :point_down: | 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`. | ||||||
|  |  | ||||||
| ```jsx showLineNumbers | ```jsx showLineNumbers | ||||||
| import dynamic from "next/dynamic"; | import dynamic from "next/dynamic"; | ||||||
| @@ -49,88 +55,25 @@ export default function App() { | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| 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. | Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2). | ||||||
|  |  | ||||||
| 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: |  | ||||||
|  |  | ||||||
| <Tabs> | 2. Importing Excalidraw once **client** is rendered. | ||||||
|   <TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" > |  | ||||||
|  |  | ||||||
|   ```jsx showLineNumbers | ```jsx showLineNumbers | ||||||
|   "use client"; | import { useState, useEffect } from "react"; | ||||||
|   import { Excalidraw, convertToExcalidrawElements } from "@excalidraw/excalidraw"; | export default function App() { | ||||||
|  |   const [Excalidraw, setExcalidraw] = useState(null); | ||||||
|   import "@excalidraw/excalidraw/index.css"; |   useEffect(() => { | ||||||
|  |     import("@excalidraw/excalidraw").then((comp) => | ||||||
|   const ExcalidrawWrapper: React.FC = () => { |       setExcalidraw(comp.Excalidraw), | ||||||
|     console.info(convertToExcalidrawElements([{ |  | ||||||
|       type: "rectangle", |  | ||||||
|       id: "rect-1", |  | ||||||
|       width: 186.47265625, |  | ||||||
|       height: 141.9765625, |  | ||||||
|     },])); |  | ||||||
|     return ( |  | ||||||
|       <div style={{height:"500px", width:"500px"}}>   |  | ||||||
|         <Excalidraw /> |  | ||||||
|       </div>  |  | ||||||
|     ); |     ); | ||||||
|   }; |   }, []); | ||||||
|   export default ExcalidrawWrapper; |   return <>{Excalidraw && <Excalidraw />}</>; | ||||||
|   ``` | } | ||||||
|  | ``` | ||||||
|   </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) | The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm) | ||||||
|  |  | ||||||
| @@ -150,7 +93,7 @@ Since Vite removes env variables by default, you can update the vite config to e | |||||||
|  |  | ||||||
| ``` | ``` | ||||||
|  define: { |  define: { | ||||||
|     "process.env.IS_PREACT": JSON.stringify("true"), |     "process.env.IS_PREACT": process.env.IS_PREACT, | ||||||
|   }, |   }, | ||||||
| ``` | ``` | ||||||
| :::  | :::  | ||||||
| @@ -205,7 +148,7 @@ import TabItem from "@theme/TabItem"; | |||||||
|       <h1>Excalidraw Embed Example</h1> |       <h1>Excalidraw Embed Example</h1> | ||||||
|       <div id="app"></div> |       <div id="app"></div> | ||||||
|     </div> |     </div> | ||||||
|     <script type="text/javascript" src="packages/excalidraw/index.js"></script> |     <script type="text/javascript" src="src/index.js"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -38,9 +38,9 @@ Add the diagram type in switch case in [`parseMermaid`](https://github.com/excal | |||||||
|  |  | ||||||
| ## Writing the Excalidraw Skeleton Convertor | ## 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/packages/excalidraw/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/src/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/packages/excalidraw/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/src/data/transform.ts#L133). | ||||||
|  |  | ||||||
| Thats it, you have added the new diagram type 🥳, now lets test it out! | 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/packages/excalidraw/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/src/data/transform.ts#L133C13-L133C38). | ||||||
|  |  | ||||||
|  |  | ||||||
| For computing `vertices` and `edge`s lets consider the below svg generated by mermaid | 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/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 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 final output from `parseVertex` looks like :point_down: |  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 | ## Converting to ExcalidrawElementSkeleton | ||||||
|  |  | ||||||
| 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. | 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. | ||||||
|  |  | ||||||
| For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton. | 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 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/packages/excalidraw/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/src/data/transform.ts#L133C13-L133C38). | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -52,6 +52,15 @@ Make sure the title starts with a semantic prefix: | |||||||
| - **chore**: Other changes that don't modify src or test files | - **chore**: Other changes that don't modify src or test files | ||||||
| - **revert**: Reverts a previous commit | - **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 | ### 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. | 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,7 +41,10 @@ const config = { | |||||||
|           showLastUpdateTime: true, |           showLastUpdateTime: true, | ||||||
|         }, |         }, | ||||||
|         theme: { |         theme: { | ||||||
|           customCss: [require.resolve("./src/css/custom.scss")], |           customCss: [ | ||||||
|  |             require.resolve("./src/css/custom.scss"), | ||||||
|  |             require.resolve("../src/packages/excalidraw/example/App.scss"), | ||||||
|  |           ], | ||||||
|         }, |         }, | ||||||
|       }), |       }), | ||||||
|     ], |     ], | ||||||
|   | |||||||
| @@ -1,4 +0,0 @@ | |||||||
| { |  | ||||||
|   "outputDirectory": "build", |  | ||||||
|   "installCommand": "yarn install" |  | ||||||
| } |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| 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,13 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "examples", |  | ||||||
|   "version": "1.0.0", |  | ||||||
|   "private": true, |  | ||||||
|   "dependencies": { |  | ||||||
|     "react": "18.2.0", |  | ||||||
|     "react-dom": "18.2.0", |  | ||||||
|     "@excalidraw/excalidraw": "*" |  | ||||||
|   }, |  | ||||||
|   "devDependencies": { |  | ||||||
|     "typescript": "^5" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| { |  | ||||||
|   "extends": "../../tsconfig" |  | ||||||
| } |  | ||||||
| @@ -1,146 +0,0 @@ | |||||||
| 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
									
									
								
							
							
						
						
									
										36
									
								
								examples/excalidraw/with-nextjs/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,36 +0,0 @@ | |||||||
| # 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 |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| 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. |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| /** @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; |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| { |  | ||||||
|   "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" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 25 KiB | 
| @@ -1,11 +0,0 @@ | |||||||
| export default function RootLayout({ |  | ||||||
|   children, |  | ||||||
| }: { |  | ||||||
|   children: React.ReactNode; |  | ||||||
| }) { |  | ||||||
|   return ( |  | ||||||
|     <html lang="en"> |  | ||||||
|       <body>{children}</body> |  | ||||||
|     </html> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| 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 /> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| * { |  | ||||||
|   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; |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| "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; |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| 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 /> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| { |  | ||||||
|   "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"] |  | ||||||
| } |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| { |  | ||||||
|   "outputDirectory": "build" |  | ||||||
| } |  | ||||||
| @@ -1,252 +0,0 @@ | |||||||
| # 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" |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| 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>, |  | ||||||
| ); |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| { |  | ||||||
|   "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" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 197 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 30 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 6.1 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 39 KiB | 
| @@ -1,4 +0,0 @@ | |||||||
| { |  | ||||||
|   "outputDirectory": "dist", |  | ||||||
|   "installCommand": "yarn install" |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| import { defineConfig } from "vite"; |  | ||||||
|  |  | ||||||
| // https://vitejs.dev/config/ |  | ||||||
| export default defineConfig({ |  | ||||||
|   server: { |  | ||||||
|     port: 3001, |  | ||||||
|     // open the browser |  | ||||||
|     open: true, |  | ||||||
|   }, |  | ||||||
|   publicDir: "public", |  | ||||||
| }); |  | ||||||
| @@ -1,313 +0,0 @@ | |||||||
| # 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" |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,14 +1,14 @@ | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils"; | import { debounce, getVersion, nFormatter } from "../src/utils"; | ||||||
| import { | import { | ||||||
|   getElementsStorageSize, |   getElementsStorageSize, | ||||||
|   getTotalStorageSize, |   getTotalStorageSize, | ||||||
| } from "./data/localStorage"; | } from "./data/localStorage"; | ||||||
| import { DEFAULT_VERSION } from "../packages/excalidraw/constants"; | import { DEFAULT_VERSION } from "../src/constants"; | ||||||
| import { t } from "../packages/excalidraw/i18n"; | import { t } from "../src/i18n"; | ||||||
| import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard"; | import { copyTextToSystemClipboard } from "../src/clipboard"; | ||||||
| import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types"; | import { NonDeletedExcalidrawElement } from "../src/element/types"; | ||||||
| import { UIAppState } from "../packages/excalidraw/types"; | import { UIAppState } from "../src/types"; | ||||||
|  |  | ||||||
| type StorageSizes = { scene: number; total: number }; | type StorageSizes = { scene: number; total: number }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,17 +15,11 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000; | |||||||
| export const WS_EVENTS = { | export const WS_EVENTS = { | ||||||
|   SERVER_VOLATILE: "server-volatile-broadcast", |   SERVER_VOLATILE: "server-volatile-broadcast", | ||||||
|   SERVER: "server-broadcast", |   SERVER: "server-broadcast", | ||||||
|   USER_FOLLOW_CHANGE: "user-follow", | }; | ||||||
|   USER_FOLLOW_ROOM_CHANGE: "user-follow-room-change", |  | ||||||
| } as const; |  | ||||||
|  |  | ||||||
| export enum WS_SUBTYPES { | export enum WS_SCENE_EVENT_TYPES { | ||||||
|   INVALID_RESPONSE = "INVALID_RESPONSE", |  | ||||||
|   INIT = "SCENE_INIT", |   INIT = "SCENE_INIT", | ||||||
|   UPDATE = "SCENE_UPDATE", |   UPDATE = "SCENE_UPDATE", | ||||||
|   MOUSE_LOCATION = "MOUSE_LOCATION", |  | ||||||
|   IDLE_STATUS = "IDLE_STATUS", |  | ||||||
|   USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS", |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export const FIREBASE_STORAGE_PREFIXES = { | export const FIREBASE_STORAGE_PREFIXES = { | ||||||
| @@ -39,14 +33,10 @@ export const STORAGE_KEYS = { | |||||||
|   LOCAL_STORAGE_ELEMENTS: "excalidraw", |   LOCAL_STORAGE_ELEMENTS: "excalidraw", | ||||||
|   LOCAL_STORAGE_APP_STATE: "excalidraw-state", |   LOCAL_STORAGE_APP_STATE: "excalidraw-state", | ||||||
|   LOCAL_STORAGE_COLLAB: "excalidraw-collab", |   LOCAL_STORAGE_COLLAB: "excalidraw-collab", | ||||||
|  |   LOCAL_STORAGE_LIBRARY: "excalidraw-library", | ||||||
|   LOCAL_STORAGE_THEME: "excalidraw-theme", |   LOCAL_STORAGE_THEME: "excalidraw-theme", | ||||||
|   VERSION_DATA_STATE: "version-dataState", |   VERSION_DATA_STATE: "version-dataState", | ||||||
|   VERSION_FILES: "version-files", |   VERSION_FILES: "version-files", | ||||||
|  |  | ||||||
|   IDB_LIBRARY: "excalidraw-library", |  | ||||||
|  |  | ||||||
|   // do not use apart from migrations |  | ||||||
|   __LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library", |  | ||||||
| } as const; | } as const; | ||||||
|  |  | ||||||
| export const COOKIES = { | export const COOKIES = { | ||||||
|   | |||||||
| @@ -1,44 +1,36 @@ | |||||||
| import throttle from "lodash.throttle"; | import throttle from "lodash.throttle"; | ||||||
| import { PureComponent } from "react"; | import { PureComponent } from "react"; | ||||||
| import { | import { ExcalidrawImperativeAPI } from "../../src/types"; | ||||||
|   ExcalidrawImperativeAPI, | import { ErrorDialog } from "../../src/components/ErrorDialog"; | ||||||
|   SocketId, | import { APP_NAME, ENV, EVENT } from "../../src/constants"; | ||||||
| } from "../../packages/excalidraw/types"; | import { ImportedDataState } from "../../src/data/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 { | import { | ||||||
|   ExcalidrawElement, |   ExcalidrawElement, | ||||||
|   InitializedExcalidrawImageElement, |   InitializedExcalidrawImageElement, | ||||||
|   OrderedExcalidrawElement, | } from "../../src/element/types"; | ||||||
| } from "../../packages/excalidraw/element/types"; |  | ||||||
| import { | import { | ||||||
|   StoreAction, |  | ||||||
|   getSceneVersion, |   getSceneVersion, | ||||||
|   restoreElements, |   restoreElements, | ||||||
|   zoomToFitBounds, | } from "../../src/packages/excalidraw/index"; | ||||||
|   reconcileElements, | import { Collaborator, Gesture } from "../../src/types"; | ||||||
| } from "../../packages/excalidraw"; |  | ||||||
| import { Collaborator, Gesture } from "../../packages/excalidraw/types"; |  | ||||||
| import { | import { | ||||||
|   assertNever, |  | ||||||
|   preventUnload, |   preventUnload, | ||||||
|   resolvablePromise, |   resolvablePromise, | ||||||
|   throttleRAF, |   withBatchedUpdates, | ||||||
| } from "../../packages/excalidraw/utils"; | } from "../../src/utils"; | ||||||
| import { | import { | ||||||
|   CURSOR_SYNC_TIMEOUT, |   CURSOR_SYNC_TIMEOUT, | ||||||
|   FILE_UPLOAD_MAX_BYTES, |   FILE_UPLOAD_MAX_BYTES, | ||||||
|   FIREBASE_STORAGE_PREFIXES, |   FIREBASE_STORAGE_PREFIXES, | ||||||
|   INITIAL_SCENE_UPDATE_TIMEOUT, |   INITIAL_SCENE_UPDATE_TIMEOUT, | ||||||
|   LOAD_IMAGES_TIMEOUT, |   LOAD_IMAGES_TIMEOUT, | ||||||
|   WS_SUBTYPES, |   WS_SCENE_EVENT_TYPES, | ||||||
|   SYNC_FULL_SCENE_INTERVAL_MS, |   SYNC_FULL_SCENE_INTERVAL_MS, | ||||||
|   WS_EVENTS, |  | ||||||
| } from "../app_constants"; | } from "../app_constants"; | ||||||
| import { | import { | ||||||
|   generateCollaborationLinkData, |   generateCollaborationLinkData, | ||||||
|   getCollaborationLink, |   getCollaborationLink, | ||||||
|  |   getCollabServer, | ||||||
|   getSyncableElements, |   getSyncableElements, | ||||||
|   SocketUpdateDataSource, |   SocketUpdateDataSource, | ||||||
|   SyncableExcalidrawElement, |   SyncableExcalidrawElement, | ||||||
| @@ -55,51 +47,42 @@ import { | |||||||
|   saveUsernameToLocalStorage, |   saveUsernameToLocalStorage, | ||||||
| } from "../data/localStorage"; | } from "../data/localStorage"; | ||||||
| import Portal from "./Portal"; | import Portal from "./Portal"; | ||||||
| import { t } from "../../packages/excalidraw/i18n"; | import RoomDialog from "./RoomDialog"; | ||||||
| import { UserIdleState } from "../../packages/excalidraw/types"; | import { t } from "../../src/i18n"; | ||||||
| import { | import { UserIdleState } from "../../src/types"; | ||||||
|   IDLE_THRESHOLD, | import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants"; | ||||||
|   ACTIVE_THRESHOLD, |  | ||||||
| } from "../../packages/excalidraw/constants"; |  | ||||||
| import { | import { | ||||||
|   encodeFilesForUpload, |   encodeFilesForUpload, | ||||||
|   FileManager, |   FileManager, | ||||||
|   updateStaleImageStatuses, |   updateStaleImageStatuses, | ||||||
| } from "../data/FileManager"; | } from "../data/FileManager"; | ||||||
| import { AbortError } from "../../packages/excalidraw/errors"; | import { AbortError } from "../../src/errors"; | ||||||
| import { | import { | ||||||
|   isImageElement, |   isImageElement, | ||||||
|   isInitializedImageElement, |   isInitializedImageElement, | ||||||
| } from "../../packages/excalidraw/element/typeChecks"; | } from "../../src/element/typeChecks"; | ||||||
| import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; | import { newElementWith } from "../../src/element/mutateElement"; | ||||||
| import { decryptData } from "../../packages/excalidraw/data/encryption"; | import { | ||||||
|  |   ReconciledElements, | ||||||
|  |   reconcileElements as _reconcileElements, | ||||||
|  | } from "./reconciliation"; | ||||||
|  | import { decryptData } from "../../src/data/encryption"; | ||||||
| import { resetBrowserStateVersions } from "../data/tabSync"; | import { resetBrowserStateVersions } from "../data/tabSync"; | ||||||
| import { LocalData } from "../data/LocalData"; | import { LocalData } from "../data/LocalData"; | ||||||
| import { atom } from "jotai"; | import { atom, useAtom } from "jotai"; | ||||||
| import { appJotaiStore } from "../app-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"; |  | ||||||
| import { collabErrorIndicatorAtom } from "./CollabError"; |  | ||||||
| import type { |  | ||||||
|   ReconciledExcalidrawElement, |  | ||||||
|   RemoteExcalidrawElement, |  | ||||||
| } from "../../packages/excalidraw/data/reconcile"; |  | ||||||
|  |  | ||||||
| export const collabAPIAtom = atom<CollabAPI | null>(null); | export const collabAPIAtom = atom<CollabAPI | null>(null); | ||||||
|  | export const collabDialogShownAtom = atom(false); | ||||||
| export const isCollaboratingAtom = atom(false); | export const isCollaboratingAtom = atom(false); | ||||||
| export const isOfflineAtom = atom(false); | export const isOfflineAtom = atom(false); | ||||||
|  |  | ||||||
| interface CollabState { | interface CollabState { | ||||||
|   errorMessage: string | null; |   errorMessage: string; | ||||||
|   /** errors related to saving */ |  | ||||||
|   dialogNotifiedErrors: Record<string, boolean>; |  | ||||||
|   username: string; |   username: string; | ||||||
|   activeRoomLink: string | null; |   activeRoomLink: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const activeRoomLinkAtom = atom<string | null>(null); |  | ||||||
|  |  | ||||||
| type CollabInstance = InstanceType<typeof Collab>; | type CollabInstance = InstanceType<typeof Collab>; | ||||||
|  |  | ||||||
| export interface CollabAPI { | export interface CollabAPI { | ||||||
| @@ -110,34 +93,32 @@ export interface CollabAPI { | |||||||
|   stopCollaboration: CollabInstance["stopCollaboration"]; |   stopCollaboration: CollabInstance["stopCollaboration"]; | ||||||
|   syncElements: CollabInstance["syncElements"]; |   syncElements: CollabInstance["syncElements"]; | ||||||
|   fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; |   fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; | ||||||
|   setUsername: CollabInstance["setUsername"]; |   setUsername: (username: string) => void; | ||||||
|   getUsername: CollabInstance["getUsername"]; |  | ||||||
|   getActiveRoomLink: CollabInstance["getActiveRoomLink"]; |  | ||||||
|   setCollabError: CollabInstance["setErrorDialog"]; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| interface CollabProps { | interface PublicProps { | ||||||
|   excalidrawAPI: ExcalidrawImperativeAPI; |   excalidrawAPI: ExcalidrawImperativeAPI; | ||||||
| } | } | ||||||
|  |  | ||||||
| class Collab extends PureComponent<CollabProps, CollabState> { | type Props = PublicProps & { modalIsShown: boolean }; | ||||||
|  |  | ||||||
|  | class Collab extends PureComponent<Props, CollabState> { | ||||||
|   portal: Portal; |   portal: Portal; | ||||||
|   fileManager: FileManager; |   fileManager: FileManager; | ||||||
|   excalidrawAPI: CollabProps["excalidrawAPI"]; |   excalidrawAPI: Props["excalidrawAPI"]; | ||||||
|   activeIntervalId: number | null; |   activeIntervalId: number | null; | ||||||
|   idleTimeoutId: number | null; |   idleTimeoutId: number | null; | ||||||
|  |  | ||||||
|   private socketInitializationTimer?: number; |   private socketInitializationTimer?: number; | ||||||
|   private lastBroadcastedOrReceivedSceneVersion: number = -1; |   private lastBroadcastedOrReceivedSceneVersion: number = -1; | ||||||
|   private collaborators = new Map<SocketId, Collaborator>(); |   private collaborators = new Map<string, Collaborator>(); | ||||||
|  |  | ||||||
|   constructor(props: CollabProps) { |   constructor(props: Props) { | ||||||
|     super(props); |     super(props); | ||||||
|     this.state = { |     this.state = { | ||||||
|       errorMessage: null, |       errorMessage: "", | ||||||
|       dialogNotifiedErrors: {}, |  | ||||||
|       username: importUsernameFromLocalStorage() || "", |       username: importUsernameFromLocalStorage() || "", | ||||||
|       activeRoomLink: null, |       activeRoomLink: "", | ||||||
|     }; |     }; | ||||||
|     this.portal = new Portal(this); |     this.portal = new Portal(this); | ||||||
|     this.fileManager = new FileManager({ |     this.fileManager = new FileManager({ | ||||||
| @@ -170,28 +151,12 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     this.idleTimeoutId = null; |     this.idleTimeoutId = null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private onUmmount: (() => void) | null = null; |  | ||||||
|  |  | ||||||
|   componentDidMount() { |   componentDidMount() { | ||||||
|     window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); |     window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); | ||||||
|     window.addEventListener("online", this.onOfflineStatusToggle); |     window.addEventListener("online", this.onOfflineStatusToggle); | ||||||
|     window.addEventListener("offline", this.onOfflineStatusToggle); |     window.addEventListener("offline", this.onOfflineStatusToggle); | ||||||
|     window.addEventListener(EVENT.UNLOAD, this.onUnload); |     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(); |     this.onOfflineStatusToggle(); | ||||||
|  |  | ||||||
|     const collabAPI: CollabAPI = { |     const collabAPI: CollabAPI = { | ||||||
| @@ -202,9 +167,6 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|       fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, |       fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, | ||||||
|       stopCollaboration: this.stopCollaboration, |       stopCollaboration: this.stopCollaboration, | ||||||
|       setUsername: this.setUsername, |       setUsername: this.setUsername, | ||||||
|       getUsername: this.getUsername, |  | ||||||
|       getActiveRoomLink: this.getActiveRoomLink, |  | ||||||
|       setCollabError: this.setErrorDialog, |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     appJotaiStore.set(collabAPIAtom, collabAPI); |     appJotaiStore.set(collabAPIAtom, collabAPI); | ||||||
| @@ -242,7 +204,6 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|       window.clearTimeout(this.idleTimeoutId); |       window.clearTimeout(this.idleTimeoutId); | ||||||
|       this.idleTimeoutId = null; |       this.idleTimeoutId = null; | ||||||
|     } |     } | ||||||
|     this.onUmmount?.(); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; |   isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; | ||||||
| @@ -277,39 +238,24 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     syncableElements: readonly SyncableExcalidrawElement[], |     syncableElements: readonly SyncableExcalidrawElement[], | ||||||
|   ) => { |   ) => { | ||||||
|     try { |     try { | ||||||
|       const storedElements = await saveToFirebase( |       const savedData = await saveToFirebase( | ||||||
|         this.portal, |         this.portal, | ||||||
|         syncableElements, |         syncableElements, | ||||||
|         this.excalidrawAPI.getAppState(), |         this.excalidrawAPI.getAppState(), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       this.resetErrorIndicator(); |       if (this.isCollaborating() && savedData && savedData.reconciledElements) { | ||||||
|  |         this.handleRemoteSceneUpdate( | ||||||
|       if (this.isCollaborating() && storedElements) { |           this.reconcileElements(savedData.reconciledElements), | ||||||
|         this.handleRemoteSceneUpdate(this._reconcileElements(storedElements)); |         ); | ||||||
|       } |       } | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
|       const errorMessage = /is longer than.*?bytes/.test(error.message) |  | ||||||
|         ? t("errors.collabSaveFailed_sizeExceeded") |  | ||||||
|         : t("errors.collabSaveFailed"); |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         !this.state.dialogNotifiedErrors[errorMessage] || |  | ||||||
|         !this.isCollaborating() |  | ||||||
|       ) { |  | ||||||
|         this.setErrorDialog(errorMessage); |  | ||||||
|       this.setState({ |       this.setState({ | ||||||
|           dialogNotifiedErrors: { |         // firestore doesn't return a specific error code when size exceeded | ||||||
|             ...this.state.dialogNotifiedErrors, |         errorMessage: /is longer than.*?bytes/.test(error.message) | ||||||
|             [errorMessage]: true, |           ? t("errors.collabSaveFailed_sizeExceeded") | ||||||
|           }, |           : t("errors.collabSaveFailed"), | ||||||
|       }); |       }); | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (this.isCollaborating()) { |  | ||||||
|         this.setErrorIndicator(errorMessage); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       console.error(error); |       console.error(error); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @@ -318,7 +264,6 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     this.queueBroadcastAllElements.cancel(); |     this.queueBroadcastAllElements.cancel(); | ||||||
|     this.queueSaveToFirebase.cancel(); |     this.queueSaveToFirebase.cancel(); | ||||||
|     this.loadImageFiles.cancel(); |     this.loadImageFiles.cancel(); | ||||||
|     this.resetErrorIndicator(true); |  | ||||||
|  |  | ||||||
|     this.saveCollabRoomToFirebase( |     this.saveCollabRoomToFirebase( | ||||||
|       getSyncableElements( |       getSyncableElements( | ||||||
| @@ -357,7 +302,7 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|  |  | ||||||
|       this.excalidrawAPI.updateScene({ |       this.excalidrawAPI.updateScene({ | ||||||
|         elements, |         elements, | ||||||
|         storeAction: StoreAction.UPDATE, |         commitToHistory: false, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @@ -368,7 +313,9 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     this.fileManager.reset(); |     this.fileManager.reset(); | ||||||
|     if (!opts?.isUnload) { |     if (!opts?.isUnload) { | ||||||
|       this.setIsCollaborating(false); |       this.setIsCollaborating(false); | ||||||
|       this.setActiveRoomLink(null); |       this.setState({ | ||||||
|  |         activeRoomLink: "", | ||||||
|  |       }); | ||||||
|       this.collaborators = new Map(); |       this.collaborators = new Map(); | ||||||
|       this.excalidrawAPI.updateScene({ |       this.excalidrawAPI.updateScene({ | ||||||
|         collaborators: this.collaborators, |         collaborators: this.collaborators, | ||||||
| @@ -409,7 +356,7 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     iv: Uint8Array, |     iv: Uint8Array, | ||||||
|     encryptedData: ArrayBuffer, |     encryptedData: ArrayBuffer, | ||||||
|     decryptionKey: string, |     decryptionKey: string, | ||||||
|   ): Promise<ValueOf<SocketUpdateDataSource>> => { |   ) => { | ||||||
|     try { |     try { | ||||||
|       const decrypted = await decryptData(iv, encryptedData, decryptionKey); |       const decrypted = await decryptData(iv, encryptedData, decryptionKey); | ||||||
|  |  | ||||||
| @@ -421,7 +368,7 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|       window.alert(t("alerts.decryptFailed")); |       window.alert(t("alerts.decryptFailed")); | ||||||
|       console.error(error); |       console.error(error); | ||||||
|       return { |       return { | ||||||
|         type: WS_SUBTYPES.INVALID_RESPONSE, |         type: "INVALID_RESPONSE", | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @@ -430,11 +377,11 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|  |  | ||||||
|   startCollaboration = async ( |   startCollaboration = async ( | ||||||
|     existingRoomLinkData: null | { roomId: string; roomKey: string }, |     existingRoomLinkData: null | { roomId: string; roomKey: string }, | ||||||
|   ) => { |   ): Promise<ImportedDataState | null> => { | ||||||
|     if (!this.state.username) { |     if (!this.state.username) { | ||||||
|       import("@excalidraw/random-username").then(({ getRandomUsername }) => { |       import("@excalidraw/random-username").then(({ getRandomUsername }) => { | ||||||
|         const username = getRandomUsername(); |         const username = getRandomUsername(); | ||||||
|         this.setUsername(username); |         this.onUsernameChange(username); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -456,11 +403,7 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO: `ImportedDataState` type here seems abused |     const scenePromise = resolvablePromise<ImportedDataState | null>(); | ||||||
|     const scenePromise = resolvablePromise< |  | ||||||
|       | (ImportedDataState & { elements: readonly OrderedExcalidrawElement[] }) |  | ||||||
|       | null |  | ||||||
|     >(); |  | ||||||
|  |  | ||||||
|     this.setIsCollaborating(true); |     this.setIsCollaborating(true); | ||||||
|     LocalData.pauseSave("collaboration"); |     LocalData.pauseSave("collaboration"); | ||||||
| @@ -480,9 +423,13 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     this.fallbackInitializationHandler = fallbackInitializationHandler; |     this.fallbackInitializationHandler = fallbackInitializationHandler; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|  |       const socketServerData = await getCollabServer(); | ||||||
|  |  | ||||||
|       this.portal.socket = this.portal.open( |       this.portal.socket = this.portal.open( | ||||||
|         socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, { |         socketIOClient(socketServerData.url, { | ||||||
|           transports: ["websocket", "polling"], |           transports: socketServerData.polling | ||||||
|  |             ? ["websocket", "polling"] | ||||||
|  |             : ["websocket"], | ||||||
|         }), |         }), | ||||||
|         roomId, |         roomId, | ||||||
|         roomKey, |         roomKey, | ||||||
| @@ -491,7 +438,7 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|       this.portal.socket.once("connect_error", fallbackInitializationHandler); |       this.portal.socket.once("connect_error", fallbackInitializationHandler); | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
|       console.error(error); |       console.error(error); | ||||||
|       this.setErrorDialog(error.message); |       this.setState({ errorMessage: error.message }); | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -502,13 +449,14 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|         } |         } | ||||||
|         return element; |         return element; | ||||||
|       }); |       }); | ||||||
|       // remove deleted elements from elements array to ensure we don't |       // remove deleted elements from elements array & history to ensure we don't | ||||||
|       // expose potentially sensitive user data in case user manually deletes |       // expose potentially sensitive user data in case user manually deletes | ||||||
|       // existing elements (or clears scene), which would otherwise be persisted |       // existing elements (or clears scene), which would otherwise be persisted | ||||||
|       // to database even if deleted before creating the room. |       // to database even if deleted before creating the room. | ||||||
|  |       this.excalidrawAPI.history.clear(); | ||||||
|       this.excalidrawAPI.updateScene({ |       this.excalidrawAPI.updateScene({ | ||||||
|         elements, |         elements, | ||||||
|         storeAction: StoreAction.UPDATE, |         commitToHistory: true, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       this.saveCollabRoomToFirebase(getSyncableElements(elements)); |       this.saveCollabRoomToFirebase(getSyncableElements(elements)); | ||||||
| @@ -536,15 +484,16 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         switch (decryptedData.type) { |         switch (decryptedData.type) { | ||||||
|           case WS_SUBTYPES.INVALID_RESPONSE: |           case "INVALID_RESPONSE": | ||||||
|             return; |             return; | ||||||
|           case WS_SUBTYPES.INIT: { |           case WS_SCENE_EVENT_TYPES.INIT: { | ||||||
|             if (!this.portal.socketInitialized) { |             if (!this.portal.socketInitialized) { | ||||||
|               this.initializeRoom({ fetchScene: false }); |               this.initializeRoom({ fetchScene: false }); | ||||||
|               const remoteElements = decryptedData.payload.elements; |               const remoteElements = decryptedData.payload.elements; | ||||||
|               const reconciledElements = |               const reconciledElements = this.reconcileElements(remoteElements); | ||||||
|                 this._reconcileElements(remoteElements); |               this.handleRemoteSceneUpdate(reconciledElements, { | ||||||
|               this.handleRemoteSceneUpdate(reconciledElements); |                 init: true, | ||||||
|  |               }); | ||||||
|               // noop if already resolved via init from firebase |               // noop if already resolved via init from firebase | ||||||
|               scenePromise.resolve({ |               scenePromise.resolve({ | ||||||
|                 elements: reconciledElements, |                 elements: reconciledElements, | ||||||
| @@ -553,76 +502,42 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|             } |             } | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|           case WS_SUBTYPES.UPDATE: |           case WS_SCENE_EVENT_TYPES.UPDATE: | ||||||
|             this.handleRemoteSceneUpdate( |             this.handleRemoteSceneUpdate( | ||||||
|               this._reconcileElements(decryptedData.payload.elements), |               this.reconcileElements(decryptedData.payload.elements), | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           case WS_SUBTYPES.MOUSE_LOCATION: { |           case "MOUSE_LOCATION": { | ||||||
|             const { pointer, button, username, selectedElementIds } = |             const { pointer, button, username, selectedElementIds } = | ||||||
|               decryptedData.payload; |               decryptedData.payload; | ||||||
|  |  | ||||||
|             const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] = |             const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] = | ||||||
|               decryptedData.payload.socketId || |               decryptedData.payload.socketId || | ||||||
|               // @ts-ignore legacy, see #2094 (#2097) |               // @ts-ignore legacy, see #2094 (#2097) | ||||||
|               decryptedData.payload.socketID; |               decryptedData.payload.socketID; | ||||||
|  |  | ||||||
|             this.updateCollaborator(socketId, { |             const collaborators = new Map(this.collaborators); | ||||||
|               pointer, |             const user = collaborators.get(socketId) || {}!; | ||||||
|               button, |             user.pointer = pointer; | ||||||
|               selectedElementIds, |             user.button = button; | ||||||
|               username, |             user.selectedElementIds = selectedElementIds; | ||||||
|             }); |             user.username = username; | ||||||
|  |             collaborators.set(socketId, user); | ||||||
|             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({ |             this.excalidrawAPI.updateScene({ | ||||||
|               appState: zoomToFitBounds({ |               collaborators, | ||||||
|                 appState, |  | ||||||
|                 bounds: sceneBounds, |  | ||||||
|                 fitToViewport: true, |  | ||||||
|                 viewportZoomFactor: 1, |  | ||||||
|               }).appState, |  | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|  |           case "IDLE_STATUS": { | ||||||
|           case WS_SUBTYPES.IDLE_STATUS: { |  | ||||||
|             const { userState, socketId, username } = decryptedData.payload; |             const { userState, socketId, username } = decryptedData.payload; | ||||||
|             this.updateCollaborator(socketId, { |             const collaborators = new Map(this.collaborators); | ||||||
|               userState, |             const user = collaborators.get(socketId) || {}!; | ||||||
|               username, |             user.userState = userState; | ||||||
|  |             user.username = username; | ||||||
|  |             this.excalidrawAPI.updateScene({ | ||||||
|  |               collaborators, | ||||||
|             }); |             }); | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           default: { |  | ||||||
|             assertNever(decryptedData, null); |  | ||||||
|           } |  | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
| @@ -638,20 +553,11 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|       scenePromise.resolve(sceneData); |       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.initializeIdleDetector(); | ||||||
|  |  | ||||||
|     this.setActiveRoomLink(window.location.href); |     this.setState({ | ||||||
|  |       activeRoomLink: window.location.href, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     return scenePromise; |     return scenePromise; | ||||||
|   }; |   }; | ||||||
| @@ -703,15 +609,17 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     return null; |     return null; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private _reconcileElements = ( |   private reconcileElements = ( | ||||||
|     remoteElements: readonly ExcalidrawElement[], |     remoteElements: readonly ExcalidrawElement[], | ||||||
|   ): ReconciledExcalidrawElement[] => { |   ): ReconciledElements => { | ||||||
|     const localElements = this.getSceneElementsIncludingDeleted(); |     const localElements = this.getSceneElementsIncludingDeleted(); | ||||||
|     const appState = this.excalidrawAPI.getAppState(); |     const appState = this.excalidrawAPI.getAppState(); | ||||||
|     const restoredRemoteElements = restoreElements(remoteElements, null); |  | ||||||
|     const reconciledElements = reconcileElements( |     remoteElements = restoreElements(remoteElements, null); | ||||||
|  |  | ||||||
|  |     const reconciledElements = _reconcileElements( | ||||||
|       localElements, |       localElements, | ||||||
|       restoredRemoteElements as RemoteExcalidrawElement[], |       remoteElements, | ||||||
|       appState, |       appState, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -742,13 +650,20 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|   }, LOAD_IMAGES_TIMEOUT); |   }, LOAD_IMAGES_TIMEOUT); | ||||||
|  |  | ||||||
|   private handleRemoteSceneUpdate = ( |   private handleRemoteSceneUpdate = ( | ||||||
|     elements: ReconciledExcalidrawElement[], |     elements: ReconciledElements, | ||||||
|  |     { init = false }: { init?: boolean } = {}, | ||||||
|   ) => { |   ) => { | ||||||
|     this.excalidrawAPI.updateScene({ |     this.excalidrawAPI.updateScene({ | ||||||
|       elements, |       elements, | ||||||
|       storeAction: StoreAction.UPDATE, |       commitToHistory: !!init, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     // We haven't yet implemented multiplayer undo functionality, so we clear the undo stack | ||||||
|  |     // when we receive any messages from another peer. This UX can be pretty rough -- if you | ||||||
|  |     // undo, a user makes a change, and then try to redo, your element(s) will be lost. However, | ||||||
|  |     // right now we think this is the right tradeoff. | ||||||
|  |     this.excalidrawAPI.history.clear(); | ||||||
|  |  | ||||||
|     this.loadImageFiles(); |     this.loadImageFiles(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -806,39 +721,20 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange); |     document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   setCollaborators(sockets: SocketId[]) { |   setCollaborators(sockets: string[]) { | ||||||
|     const collaborators: InstanceType<typeof Collab>["collaborators"] = |     const collaborators: InstanceType<typeof Collab>["collaborators"] = | ||||||
|       new Map(); |       new Map(); | ||||||
|     for (const socketId of sockets) { |     for (const socketId of sockets) { | ||||||
|       collaborators.set( |       if (this.collaborators.has(socketId)) { | ||||||
|         socketId, |         collaborators.set(socketId, this.collaborators.get(socketId)!); | ||||||
|         Object.assign({}, this.collaborators.get(socketId), { |       } else { | ||||||
|           isCurrentUser: socketId === this.portal.socket?.id, |         collaborators.set(socketId, {}); | ||||||
|         }), |       } | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|     this.collaborators = collaborators; |     this.collaborators = collaborators; | ||||||
|     this.excalidrawAPI.updateScene({ 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) => { |   public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { | ||||||
|     this.lastBroadcastedOrReceivedSceneVersion = version; |     this.lastBroadcastedOrReceivedSceneVersion = version; | ||||||
|   }; |   }; | ||||||
| @@ -864,42 +760,29 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     CURSOR_SYNC_TIMEOUT, |     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) => { |   onIdleStateChange = (userState: UserIdleState) => { | ||||||
|     this.portal.broadcastIdleChange(userState); |     this.portal.broadcastIdleChange(userState); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   broadcastElements = (elements: readonly OrderedExcalidrawElement[]) => { |   broadcastElements = (elements: readonly ExcalidrawElement[]) => { | ||||||
|     if ( |     if ( | ||||||
|       getSceneVersion(elements) > |       getSceneVersion(elements) > | ||||||
|       this.getLastBroadcastedOrReceivedSceneVersion() |       this.getLastBroadcastedOrReceivedSceneVersion() | ||||||
|     ) { |     ) { | ||||||
|       this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false); |       this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false); | ||||||
|       this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements); |       this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements); | ||||||
|       this.queueBroadcastAllElements(); |       this.queueBroadcastAllElements(); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   syncElements = (elements: readonly OrderedExcalidrawElement[]) => { |   syncElements = (elements: readonly ExcalidrawElement[]) => { | ||||||
|     this.broadcastElements(elements); |     this.broadcastElements(elements); | ||||||
|     this.queueSaveToFirebase(); |     this.queueSaveToFirebase(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   queueBroadcastAllElements = throttle(() => { |   queueBroadcastAllElements = throttle(() => { | ||||||
|     this.portal.broadcastScene( |     this.portal.broadcastScene( | ||||||
|       WS_SUBTYPES.UPDATE, |       WS_SCENE_EVENT_TYPES.UPDATE, | ||||||
|       this.excalidrawAPI.getSceneElementsIncludingDeleted(), |       this.excalidrawAPI.getSceneElementsIncludingDeleted(), | ||||||
|       true, |       true, | ||||||
|     ); |     ); | ||||||
| @@ -925,49 +808,41 @@ class Collab extends PureComponent<CollabProps, CollabState> { | |||||||
|     { leading: false }, |     { leading: false }, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   handleClose = () => { | ||||||
|  |     appJotaiStore.set(collabDialogShownAtom, false); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   setUsername = (username: string) => { |   setUsername = (username: string) => { | ||||||
|     this.setState({ username }); |     this.setState({ username }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   onUsernameChange = (username: string) => { | ||||||
|  |     this.setUsername(username); | ||||||
|     saveUsernameToLocalStorage(username); |     saveUsernameToLocalStorage(username); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   getUsername = () => this.state.username; |  | ||||||
|  |  | ||||||
|   setActiveRoomLink = (activeRoomLink: string | null) => { |  | ||||||
|     this.setState({ activeRoomLink }); |  | ||||||
|     appJotaiStore.set(activeRoomLinkAtom, activeRoomLink); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   getActiveRoomLink = () => this.state.activeRoomLink; |  | ||||||
|  |  | ||||||
|   setErrorIndicator = (errorMessage: string | null) => { |  | ||||||
|     appJotaiStore.set(collabErrorIndicatorAtom, { |  | ||||||
|       message: errorMessage, |  | ||||||
|       nonce: Date.now(), |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   resetErrorIndicator = (resetDialogNotifiedErrors = false) => { |  | ||||||
|     appJotaiStore.set(collabErrorIndicatorAtom, { message: null, nonce: 0 }); |  | ||||||
|     if (resetDialogNotifiedErrors) { |  | ||||||
|       this.setState({ |  | ||||||
|         dialogNotifiedErrors: {}, |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   setErrorDialog = (errorMessage: string | null) => { |  | ||||||
|     this.setState({ |  | ||||||
|       errorMessage, |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   render() { |   render() { | ||||||
|     const { errorMessage } = this.state; |     const { username, errorMessage, activeRoomLink } = this.state; | ||||||
|  |  | ||||||
|  |     const { modalIsShown } = this.props; | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <> |       <> | ||||||
|         {errorMessage != null && ( |         {modalIsShown && ( | ||||||
|           <ErrorDialog onClose={() => this.setErrorDialog(null)}> |           <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} |             {errorMessage} | ||||||
|           </ErrorDialog> |           </ErrorDialog> | ||||||
|         )} |         )} | ||||||
| @@ -986,6 +861,11 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { | |||||||
|   window.collab = window.collab || ({} as Window["collab"]); |   window.collab = window.collab || ({} as Window["collab"]); | ||||||
| } | } | ||||||
|  |  | ||||||
| export default Collab; | const _Collab: React.FC<PublicProps> = (props) => { | ||||||
|  |   const [collabDialogShown] = useAtom(collabDialogShownAtom); | ||||||
|  |   return <Collab {...props} modalIsShown={collabDialogShown} />; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default _Collab; | ||||||
|  |  | ||||||
| export type TCollabClass = Collab; | export type TCollabClass = Collab; | ||||||
|   | |||||||
| @@ -1,35 +0,0 @@ | |||||||
| @import "../../packages/excalidraw/css/variables.module.scss"; |  | ||||||
|  |  | ||||||
| .excalidraw { |  | ||||||
|   .collab-errors-button { |  | ||||||
|     width: 26px; |  | ||||||
|     height: 26px; |  | ||||||
|     margin-inline-end: 1rem; |  | ||||||
|  |  | ||||||
|     color: var(--color-danger); |  | ||||||
|  |  | ||||||
|     flex-shrink: 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .collab-errors-button-shake { |  | ||||||
|     animation: strong-shake 0.15s 6; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @keyframes strong-shake { |  | ||||||
|     0% { |  | ||||||
|       transform: rotate(0deg); |  | ||||||
|     } |  | ||||||
|     25% { |  | ||||||
|       transform: rotate(10deg); |  | ||||||
|     } |  | ||||||
|     50% { |  | ||||||
|       transform: rotate(0deg); |  | ||||||
|     } |  | ||||||
|     75% { |  | ||||||
|       transform: rotate(-10deg); |  | ||||||
|     } |  | ||||||
|     100% { |  | ||||||
|       transform: rotate(0deg); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,54 +0,0 @@ | |||||||
| import { Tooltip } from "../../packages/excalidraw/components/Tooltip"; |  | ||||||
| import { warning } from "../../packages/excalidraw/components/icons"; |  | ||||||
| import clsx from "clsx"; |  | ||||||
| import { useEffect, useRef, useState } from "react"; |  | ||||||
|  |  | ||||||
| import "./CollabError.scss"; |  | ||||||
| import { atom } from "jotai"; |  | ||||||
|  |  | ||||||
| type ErrorIndicator = { |  | ||||||
|   message: string | null; |  | ||||||
|   /** used to rerun the useEffect responsible for animation */ |  | ||||||
|   nonce: number; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const collabErrorIndicatorAtom = atom<ErrorIndicator>({ |  | ||||||
|   message: null, |  | ||||||
|   nonce: 0, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| const CollabError = ({ collabError }: { collabError: ErrorIndicator }) => { |  | ||||||
|   const [isAnimating, setIsAnimating] = useState(false); |  | ||||||
|   const clearAnimationRef = useRef<string | number | NodeJS.Timeout>(); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     setIsAnimating(true); |  | ||||||
|     clearAnimationRef.current = setTimeout(() => { |  | ||||||
|       setIsAnimating(false); |  | ||||||
|     }, 1000); |  | ||||||
|  |  | ||||||
|     return () => { |  | ||||||
|       clearTimeout(clearAnimationRef.current); |  | ||||||
|     }; |  | ||||||
|   }, [collabError.message, collabError.nonce]); |  | ||||||
|  |  | ||||||
|   if (!collabError.message) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Tooltip label={collabError.message} long={true}> |  | ||||||
|       <div |  | ||||||
|         className={clsx("collab-errors-button", { |  | ||||||
|           "collab-errors-button-shake": isAnimating, |  | ||||||
|         })} |  | ||||||
|       > |  | ||||||
|         {warning} |  | ||||||
|       </div> |  | ||||||
|     </Tooltip> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| CollabError.displayName = "CollabError"; |  | ||||||
|  |  | ||||||
| export default CollabError; |  | ||||||
| @@ -2,28 +2,27 @@ import { | |||||||
|   isSyncableElement, |   isSyncableElement, | ||||||
|   SocketUpdateData, |   SocketUpdateData, | ||||||
|   SocketUpdateDataSource, |   SocketUpdateDataSource, | ||||||
|   SyncableExcalidrawElement, |  | ||||||
| } from "../data"; | } from "../data"; | ||||||
|  |  | ||||||
| import { TCollabClass } from "./Collab"; | import { TCollabClass } from "./Collab"; | ||||||
|  |  | ||||||
| import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types"; | import { ExcalidrawElement } from "../../src/element/types"; | ||||||
| import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants"; |  | ||||||
| import { | import { | ||||||
|   OnUserFollowedPayload, |   WS_EVENTS, | ||||||
|   SocketId, |   FILE_UPLOAD_TIMEOUT, | ||||||
|   UserIdleState, |   WS_SCENE_EVENT_TYPES, | ||||||
| } from "../../packages/excalidraw/types"; | } from "../app_constants"; | ||||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | import { UserIdleState } from "../../src/types"; | ||||||
|  | import { trackEvent } from "../../src/analytics"; | ||||||
| import throttle from "lodash.throttle"; | import throttle from "lodash.throttle"; | ||||||
| import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; | import { newElementWith } from "../../src/element/mutateElement"; | ||||||
| import { encryptData } from "../../packages/excalidraw/data/encryption"; | import { BroadcastedExcalidrawElement } from "./reconciliation"; | ||||||
| import type { Socket } from "socket.io-client"; | import { encryptData } from "../../src/data/encryption"; | ||||||
| import { StoreAction } from "../../packages/excalidraw"; | import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; | ||||||
|  |  | ||||||
| class Portal { | class Portal { | ||||||
|   collab: TCollabClass; |   collab: TCollabClass; | ||||||
|   socket: Socket | null = null; |   socket: SocketIOClient.Socket | null = null; | ||||||
|   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized |   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized | ||||||
|   roomId: string | null = null; |   roomId: string | null = null; | ||||||
|   roomKey: string | null = null; |   roomKey: string | null = null; | ||||||
| @@ -33,7 +32,7 @@ class Portal { | |||||||
|     this.collab = collab; |     this.collab = collab; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   open(socket: Socket, id: string, key: string) { |   open(socket: SocketIOClient.Socket, id: string, key: string) { | ||||||
|     this.socket = socket; |     this.socket = socket; | ||||||
|     this.roomId = id; |     this.roomId = id; | ||||||
|     this.roomKey = key; |     this.roomKey = key; | ||||||
| @@ -47,12 +46,12 @@ class Portal { | |||||||
|     }); |     }); | ||||||
|     this.socket.on("new-user", async (_socketId: string) => { |     this.socket.on("new-user", async (_socketId: string) => { | ||||||
|       this.broadcastScene( |       this.broadcastScene( | ||||||
|         WS_SUBTYPES.INIT, |         WS_SCENE_EVENT_TYPES.INIT, | ||||||
|         this.collab.getSceneElementsIncludingDeleted(), |         this.collab.getSceneElementsIncludingDeleted(), | ||||||
|         /* syncAll */ true, |         /* syncAll */ true, | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|     this.socket.on("room-user-change", (clients: SocketId[]) => { |     this.socket.on("room-user-change", (clients: string[]) => { | ||||||
|       this.collab.setCollaborators(clients); |       this.collab.setCollaborators(clients); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -84,7 +83,6 @@ class Portal { | |||||||
|   async _broadcastSocketData( |   async _broadcastSocketData( | ||||||
|     data: SocketUpdateData, |     data: SocketUpdateData, | ||||||
|     volatile: boolean = false, |     volatile: boolean = false, | ||||||
|     roomId?: string, |  | ||||||
|   ) { |   ) { | ||||||
|     if (this.isOpen()) { |     if (this.isOpen()) { | ||||||
|       const json = JSON.stringify(data); |       const json = JSON.stringify(data); | ||||||
| @@ -93,7 +91,7 @@ class Portal { | |||||||
|  |  | ||||||
|       this.socket?.emit( |       this.socket?.emit( | ||||||
|         volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER, |         volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER, | ||||||
|         roomId ?? this.roomId, |         this.roomId, | ||||||
|         encryptedBuffer, |         encryptedBuffer, | ||||||
|         iv, |         iv, | ||||||
|       ); |       ); | ||||||
| @@ -128,33 +126,40 @@ class Portal { | |||||||
|           } |           } | ||||||
|           return element; |           return element; | ||||||
|         }), |         }), | ||||||
|       storeAction: StoreAction.UPDATE, |  | ||||||
|     }); |     }); | ||||||
|   }, FILE_UPLOAD_TIMEOUT); |   }, FILE_UPLOAD_TIMEOUT); | ||||||
|  |  | ||||||
|   broadcastScene = async ( |   broadcastScene = async ( | ||||||
|     updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE, |     updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE, | ||||||
|     elements: readonly OrderedExcalidrawElement[], |     allElements: readonly ExcalidrawElement[], | ||||||
|     syncAll: boolean, |     syncAll: boolean, | ||||||
|   ) => { |   ) => { | ||||||
|     if (updateType === WS_SUBTYPES.INIT && !syncAll) { |     if (updateType === WS_SCENE_EVENT_TYPES.INIT && !syncAll) { | ||||||
|       throw new Error("syncAll must be true when sending SCENE.INIT"); |       throw new Error("syncAll must be true when sending SCENE.INIT"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // sync out only the elements we think we need to to save bandwidth. |     // sync out only the elements we think we need to to save bandwidth. | ||||||
|     // periodically we'll resync the whole thing to make sure no one diverges |     // periodically we'll resync the whole thing to make sure no one diverges | ||||||
|     // due to a dropped message (server goes down etc). |     // due to a dropped message (server goes down etc). | ||||||
|     const syncableElements = elements.reduce((acc, element) => { |     const syncableElements = allElements.reduce( | ||||||
|  |       (acc, element: BroadcastedExcalidrawElement, idx, elements) => { | ||||||
|         if ( |         if ( | ||||||
|           (syncAll || |           (syncAll || | ||||||
|             !this.broadcastedElementVersions.has(element.id) || |             !this.broadcastedElementVersions.has(element.id) || | ||||||
|           element.version > this.broadcastedElementVersions.get(element.id)!) && |             element.version > | ||||||
|  |               this.broadcastedElementVersions.get(element.id)!) && | ||||||
|           isSyncableElement(element) |           isSyncableElement(element) | ||||||
|         ) { |         ) { | ||||||
|         acc.push(element); |           acc.push({ | ||||||
|  |             ...element, | ||||||
|  |             // z-index info for the reconciler | ||||||
|  |             [PRECEDING_ELEMENT_KEY]: idx === 0 ? "^" : elements[idx - 1]?.id, | ||||||
|  |           }); | ||||||
|         } |         } | ||||||
|         return acc; |         return acc; | ||||||
|     }, [] as SyncableExcalidrawElement[]); |       }, | ||||||
|  |       [] as BroadcastedExcalidrawElement[], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const data: SocketUpdateDataSource[typeof updateType] = { |     const data: SocketUpdateDataSource[typeof updateType] = { | ||||||
|       type: updateType, |       type: updateType, | ||||||
| @@ -178,9 +183,9 @@ class Portal { | |||||||
|   broadcastIdleChange = (userState: UserIdleState) => { |   broadcastIdleChange = (userState: UserIdleState) => { | ||||||
|     if (this.socket?.id) { |     if (this.socket?.id) { | ||||||
|       const data: SocketUpdateDataSource["IDLE_STATUS"] = { |       const data: SocketUpdateDataSource["IDLE_STATUS"] = { | ||||||
|         type: WS_SUBTYPES.IDLE_STATUS, |         type: "IDLE_STATUS", | ||||||
|         payload: { |         payload: { | ||||||
|           socketId: this.socket.id as SocketId, |           socketId: this.socket.id, | ||||||
|           userState, |           userState, | ||||||
|           username: this.collab.state.username, |           username: this.collab.state.username, | ||||||
|         }, |         }, | ||||||
| @@ -198,9 +203,9 @@ class Portal { | |||||||
|   }) => { |   }) => { | ||||||
|     if (this.socket?.id) { |     if (this.socket?.id) { | ||||||
|       const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { |       const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { | ||||||
|         type: WS_SUBTYPES.MOUSE_LOCATION, |         type: "MOUSE_LOCATION", | ||||||
|         payload: { |         payload: { | ||||||
|           socketId: this.socket.id as SocketId, |           socketId: this.socket.id, | ||||||
|           pointer: payload.pointer, |           pointer: payload.pointer, | ||||||
|           button: payload.button || "up", |           button: payload.button || "up", | ||||||
|           selectedElementIds: |           selectedElementIds: | ||||||
| @@ -208,43 +213,12 @@ class Portal { | |||||||
|           username: this.collab.state.username, |           username: this.collab.state.username, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       return this._broadcastSocketData( |       return this._broadcastSocketData( | ||||||
|         data as SocketUpdateData, |         data as SocketUpdateData, | ||||||
|         true, // volatile |         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; | export default Portal; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| @import "../../packages/excalidraw/css/variables.module.scss"; | @import "../../src/css/variables.module"; | ||||||
| 
 | 
 | ||||||
| .excalidraw { | .excalidraw { | ||||||
|   .ShareDialog { |   .RoomDialog { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     gap: 1.5rem; |     gap: 1.5rem; | ||||||
| @@ -10,25 +10,8 @@ | |||||||
|       height: calc(100vh - 5rem); |       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 { |     &__popover { | ||||||
|       @keyframes ShareDialog__popover__scaleIn { |       @keyframes RoomDialog__popover__scaleIn { | ||||||
|         from { |         from { | ||||||
|           opacity: 0; |           opacity: 0; | ||||||
|         } |         } | ||||||
| @@ -67,10 +50,10 @@ | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       transform-origin: var(--radix-popover-content-transform-origin); |       transform-origin: var(--radix-popover-content-transform-origin); | ||||||
|       animation: ShareDialog__popover__scaleIn 150ms ease-out; |       animation: RoomDialog__popover__scaleIn 150ms ease-out; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     &__picker { |     &__inactive { | ||||||
|       font-family: "Assistant"; |       font-family: "Assistant"; | ||||||
| 
 | 
 | ||||||
|       &__illustration { |       &__illustration { | ||||||
| @@ -112,7 +95,7 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       &__button { |       &__start_session { | ||||||
|         display: flex; |         display: flex; | ||||||
| 
 | 
 | ||||||
|         align-items: center; |         align-items: center; | ||||||
| @@ -1,13 +1,13 @@ | |||||||
| import { useRef, useState } from "react"; | import { useRef, useState } from "react"; | ||||||
| import * as Popover from "@radix-ui/react-popover"; | import * as Popover from "@radix-ui/react-popover"; | ||||||
|  |  | ||||||
| import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; | import { copyTextToSystemClipboard } from "../../src/clipboard"; | ||||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | import { trackEvent } from "../../src/analytics"; | ||||||
| import { getFrame } from "../../packages/excalidraw/utils"; | import { getFrame } from "../../src/utils"; | ||||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | import { useI18n } from "../../src/i18n"; | ||||||
| import { KEYS } from "../../packages/excalidraw/keys"; | import { KEYS } from "../../src/keys"; | ||||||
|  |  | ||||||
| import { Dialog } from "../../packages/excalidraw/components/Dialog"; | import { Dialog } from "../../src/components/Dialog"; | ||||||
| import { | import { | ||||||
|   copyIcon, |   copyIcon, | ||||||
|   playerPlayIcon, |   playerPlayIcon, | ||||||
| @@ -16,11 +16,11 @@ import { | |||||||
|   shareIOS, |   shareIOS, | ||||||
|   shareWindows, |   shareWindows, | ||||||
|   tablerCheckIcon, |   tablerCheckIcon, | ||||||
| } from "../../packages/excalidraw/components/icons"; | } from "../../src/components/icons"; | ||||||
| import { TextField } from "../../packages/excalidraw/components/TextField"; | import { TextField } from "../../src/components/TextField"; | ||||||
| import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; | import { FilledButton } from "../../src/components/FilledButton"; | ||||||
|  |  | ||||||
| import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg"; | import { ReactComponent as CollabImage } from "../../src/assets/lock.svg"; | ||||||
| import "./RoomDialog.scss"; | import "./RoomDialog.scss"; | ||||||
|  |  | ||||||
| const getShareIcon = () => { | const getShareIcon = () => { | ||||||
| @@ -65,9 +65,7 @@ export const RoomModal = ({ | |||||||
|   const copyRoomLink = async () => { |   const copyRoomLink = async () => { | ||||||
|     try { |     try { | ||||||
|       await copyTextToSystemClipboard(activeRoomLink); |       await copyTextToSystemClipboard(activeRoomLink); | ||||||
|     } catch (e) { |  | ||||||
|       setErrorMessage(t("errors.copyToSystemClipboardFailed")); |  | ||||||
|     } |  | ||||||
|       setJustCopied(true); |       setJustCopied(true); | ||||||
|  |  | ||||||
|       if (timerRef.current) { |       if (timerRef.current) { | ||||||
| @@ -77,6 +75,9 @@ export const RoomModal = ({ | |||||||
|       timerRef.current = window.setTimeout(() => { |       timerRef.current = window.setTimeout(() => { | ||||||
|         setJustCopied(false); |         setJustCopied(false); | ||||||
|       }, 3000); |       }, 3000); | ||||||
|  |     } catch (error: any) { | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     ref.current?.select(); |     ref.current?.select(); | ||||||
|   }; |   }; | ||||||
| @@ -119,7 +120,7 @@ export const RoomModal = ({ | |||||||
|               size="large" |               size="large" | ||||||
|               variant="icon" |               variant="icon" | ||||||
|               label="Share" |               label="Share" | ||||||
|               icon={getShareIcon()} |               startIcon={getShareIcon()} | ||||||
|               className="RoomDialog__active__share" |               className="RoomDialog__active__share" | ||||||
|               onClick={shareRoomLink} |               onClick={shareRoomLink} | ||||||
|             /> |             /> | ||||||
| @@ -129,7 +130,7 @@ export const RoomModal = ({ | |||||||
|               <FilledButton |               <FilledButton | ||||||
|                 size="large" |                 size="large" | ||||||
|                 label="Copy link" |                 label="Copy link" | ||||||
|                 icon={copyIcon} |                 startIcon={copyIcon} | ||||||
|                 onClick={copyRoomLink} |                 onClick={copyRoomLink} | ||||||
|               /> |               /> | ||||||
|             </Popover.Trigger> |             </Popover.Trigger> | ||||||
| @@ -165,7 +166,7 @@ export const RoomModal = ({ | |||||||
|             variant="outlined" |             variant="outlined" | ||||||
|             color="danger" |             color="danger" | ||||||
|             label={t("roomDialog.button_stopSession")} |             label={t("roomDialog.button_stopSession")} | ||||||
|             icon={playerStopFilledIcon} |             startIcon={playerStopFilledIcon} | ||||||
|             onClick={() => { |             onClick={() => { | ||||||
|               trackEvent("share", "room closed"); |               trackEvent("share", "room closed"); | ||||||
|               onRoomDestroy(); |               onRoomDestroy(); | ||||||
| @@ -194,7 +195,7 @@ export const RoomModal = ({ | |||||||
|         <FilledButton |         <FilledButton | ||||||
|           size="large" |           size="large" | ||||||
|           label={t("roomDialog.button_startSession")} |           label={t("roomDialog.button_startSession")} | ||||||
|           icon={playerPlayIcon} |           startIcon={playerPlayIcon} | ||||||
|           onClick={() => { |           onClick={() => { | ||||||
|             trackEvent("share", "room creation", `ui (${getFrame()})`); |             trackEvent("share", "room creation", `ui (${getFrame()})`); | ||||||
|             onRoomCreate(); |             onRoomCreate(); | ||||||
|   | |||||||
							
								
								
									
										154
									
								
								excalidraw-app/collab/reconciliation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								excalidraw-app/collab/reconciliation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | |||||||
|  | import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; | ||||||
|  | import { ExcalidrawElement } from "../../src/element/types"; | ||||||
|  | import { AppState } from "../../src/types"; | ||||||
|  | import { arrayToMapWithIndex } from "../../src/utils"; | ||||||
|  |  | ||||||
|  | export type ReconciledElements = readonly ExcalidrawElement[] & { | ||||||
|  |   _brand: "reconciledElements"; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type BroadcastedExcalidrawElement = ExcalidrawElement & { | ||||||
|  |   [PRECEDING_ELEMENT_KEY]?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const shouldDiscardRemoteElement = ( | ||||||
|  |   localAppState: AppState, | ||||||
|  |   local: ExcalidrawElement | undefined, | ||||||
|  |   remote: BroadcastedExcalidrawElement, | ||||||
|  | ): boolean => { | ||||||
|  |   if ( | ||||||
|  |     local && | ||||||
|  |     // local element is being edited | ||||||
|  |     (local.id === localAppState.editingElement?.id || | ||||||
|  |       local.id === localAppState.resizingElement?.id || | ||||||
|  |       local.id === localAppState.draggingElement?.id || | ||||||
|  |       // local element is newer | ||||||
|  |       local.version > remote.version || | ||||||
|  |       // resolve conflicting edits deterministically by taking the one with | ||||||
|  |       // the lowest versionNonce | ||||||
|  |       (local.version === remote.version && | ||||||
|  |         local.versionNonce < remote.versionNonce)) | ||||||
|  |   ) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const reconcileElements = ( | ||||||
|  |   localElements: readonly ExcalidrawElement[], | ||||||
|  |   remoteElements: readonly BroadcastedExcalidrawElement[], | ||||||
|  |   localAppState: AppState, | ||||||
|  | ): ReconciledElements => { | ||||||
|  |   const localElementsData = | ||||||
|  |     arrayToMapWithIndex<ExcalidrawElement>(localElements); | ||||||
|  |  | ||||||
|  |   const reconciledElements: ExcalidrawElement[] = localElements.slice(); | ||||||
|  |  | ||||||
|  |   const duplicates = new WeakMap<ExcalidrawElement, true>(); | ||||||
|  |  | ||||||
|  |   let cursor = 0; | ||||||
|  |   let offset = 0; | ||||||
|  |  | ||||||
|  |   let remoteElementIdx = -1; | ||||||
|  |   for (const remoteElement of remoteElements) { | ||||||
|  |     remoteElementIdx++; | ||||||
|  |  | ||||||
|  |     const local = localElementsData.get(remoteElement.id); | ||||||
|  |  | ||||||
|  |     if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) { | ||||||
|  |       if (remoteElement[PRECEDING_ELEMENT_KEY]) { | ||||||
|  |         delete remoteElement[PRECEDING_ELEMENT_KEY]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Mark duplicate for removal as it'll be replaced with the remote element | ||||||
|  |     if (local) { | ||||||
|  |       // Unless the remote and local elements are the same element in which case | ||||||
|  |       // we need to keep it as we'd otherwise discard it from the resulting | ||||||
|  |       // array. | ||||||
|  |       if (local[0] === remoteElement) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       duplicates.set(local[0], true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // parent may not be defined in case the remote client is running an older | ||||||
|  |     // excalidraw version | ||||||
|  |     const parent = | ||||||
|  |       remoteElement[PRECEDING_ELEMENT_KEY] || | ||||||
|  |       remoteElements[remoteElementIdx - 1]?.id || | ||||||
|  |       null; | ||||||
|  |  | ||||||
|  |     if (parent != null) { | ||||||
|  |       delete remoteElement[PRECEDING_ELEMENT_KEY]; | ||||||
|  |  | ||||||
|  |       // ^ indicates the element is the first in elements array | ||||||
|  |       if (parent === "^") { | ||||||
|  |         offset++; | ||||||
|  |         if (cursor === 0) { | ||||||
|  |           reconciledElements.unshift(remoteElement); | ||||||
|  |           localElementsData.set(remoteElement.id, [ | ||||||
|  |             remoteElement, | ||||||
|  |             cursor - offset, | ||||||
|  |           ]); | ||||||
|  |         } else { | ||||||
|  |           reconciledElements.splice(cursor + 1, 0, remoteElement); | ||||||
|  |           localElementsData.set(remoteElement.id, [ | ||||||
|  |             remoteElement, | ||||||
|  |             cursor + 1 - offset, | ||||||
|  |           ]); | ||||||
|  |           cursor++; | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         let idx = localElementsData.has(parent) | ||||||
|  |           ? localElementsData.get(parent)![1] | ||||||
|  |           : null; | ||||||
|  |         if (idx != null) { | ||||||
|  |           idx += offset; | ||||||
|  |         } | ||||||
|  |         if (idx != null && idx >= cursor) { | ||||||
|  |           reconciledElements.splice(idx + 1, 0, remoteElement); | ||||||
|  |           offset++; | ||||||
|  |           localElementsData.set(remoteElement.id, [ | ||||||
|  |             remoteElement, | ||||||
|  |             idx + 1 - offset, | ||||||
|  |           ]); | ||||||
|  |           cursor = idx + 1; | ||||||
|  |         } else if (idx != null) { | ||||||
|  |           reconciledElements.splice(cursor + 1, 0, remoteElement); | ||||||
|  |           offset++; | ||||||
|  |           localElementsData.set(remoteElement.id, [ | ||||||
|  |             remoteElement, | ||||||
|  |             cursor + 1 - offset, | ||||||
|  |           ]); | ||||||
|  |           cursor++; | ||||||
|  |         } else { | ||||||
|  |           reconciledElements.push(remoteElement); | ||||||
|  |           localElementsData.set(remoteElement.id, [ | ||||||
|  |             remoteElement, | ||||||
|  |             reconciledElements.length - 1 - offset, | ||||||
|  |           ]); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // no parent z-index information, local element exists → replace in place | ||||||
|  |     } else if (local) { | ||||||
|  |       reconciledElements[local[1]] = remoteElement; | ||||||
|  |       localElementsData.set(remoteElement.id, [remoteElement, local[1]]); | ||||||
|  |       // otherwise push to the end | ||||||
|  |     } else { | ||||||
|  |       reconciledElements.push(remoteElement); | ||||||
|  |       localElementsData.set(remoteElement.id, [ | ||||||
|  |         remoteElement, | ||||||
|  |         reconciledElements.length - 1 - offset, | ||||||
|  |       ]); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const ret: readonly ExcalidrawElement[] = reconciledElements.filter( | ||||||
|  |     (element) => !duplicates.has(element), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return ret as ReconciledElements; | ||||||
|  | }; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import React from "react"; | import React from "react"; | ||||||
| import { Footer } from "../../packages/excalidraw/index"; | import { Footer } from "../../src/packages/excalidraw/index"; | ||||||
| import { EncryptedIcon } from "./EncryptedIcon"; | import { EncryptedIcon } from "./EncryptedIcon"; | ||||||
| import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; | import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; | ||||||
| import { isExcalidrawPlusSignedUser } from "../app_constants"; | import { isExcalidrawPlusSignedUser } from "../app_constants"; | ||||||
|   | |||||||
| @@ -1,19 +1,12 @@ | |||||||
| import React from "react"; | import React from "react"; | ||||||
| import { | import { PlusPromoIcon } from "../../src/components/icons"; | ||||||
|   arrowBarToLeftIcon, | import { MainMenu } from "../../src/packages/excalidraw/index"; | ||||||
|   ExcalLogo, |  | ||||||
| } from "../../packages/excalidraw/components/icons"; |  | ||||||
| import { Theme } from "../../packages/excalidraw/element/types"; |  | ||||||
| import { MainMenu } from "../../packages/excalidraw/index"; |  | ||||||
| import { isExcalidrawPlusSignedUser } from "../app_constants"; |  | ||||||
| import { LanguageList } from "./LanguageList"; | import { LanguageList } from "./LanguageList"; | ||||||
|  |  | ||||||
| export const AppMainMenu: React.FC<{ | export const AppMainMenu: React.FC<{ | ||||||
|   onCollabDialogOpen: () => any; |   setCollabDialogShown: (toggle: boolean) => any; | ||||||
|   isCollaborating: boolean; |   isCollaborating: boolean; | ||||||
|   isCollabEnabled: boolean; |   isCollabEnabled: boolean; | ||||||
|   theme: Theme | "system"; |  | ||||||
|   setTheme: (theme: Theme | "system") => void; |  | ||||||
| }> = React.memo((props) => { | }> = React.memo((props) => { | ||||||
|   return ( |   return ( | ||||||
|     <MainMenu> |     <MainMenu> | ||||||
| @@ -24,38 +17,25 @@ export const AppMainMenu: React.FC<{ | |||||||
|       {props.isCollabEnabled && ( |       {props.isCollabEnabled && ( | ||||||
|         <MainMenu.DefaultItems.LiveCollaborationTrigger |         <MainMenu.DefaultItems.LiveCollaborationTrigger | ||||||
|           isCollaborating={props.isCollaborating} |           isCollaborating={props.isCollaborating} | ||||||
|           onSelect={() => props.onCollabDialogOpen()} |           onSelect={() => props.setCollabDialogShown(true)} | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
|       <MainMenu.DefaultItems.CommandPalette className="highlighted" /> |  | ||||||
|       <MainMenu.DefaultItems.Help /> |       <MainMenu.DefaultItems.Help /> | ||||||
|       <MainMenu.DefaultItems.ClearCanvas /> |       <MainMenu.DefaultItems.ClearCanvas /> | ||||||
|       <MainMenu.Separator /> |       <MainMenu.Separator /> | ||||||
|       <MainMenu.ItemLink |       <MainMenu.ItemLink | ||||||
|         icon={ExcalLogo} |         icon={PlusPromoIcon} | ||||||
|         href={`${ |         href={`${ | ||||||
|           import.meta.env.VITE_APP_PLUS_APP |           import.meta.env.VITE_APP_PLUS_LP | ||||||
|         }/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`} |         }/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`} | ||||||
|         className="" |         className="ExcalidrawPlus" | ||||||
|       > |       > | ||||||
|         Excalidraw+ |         Excalidraw+ | ||||||
|       </MainMenu.ItemLink> |       </MainMenu.ItemLink> | ||||||
|       <MainMenu.DefaultItems.Socials /> |       <MainMenu.DefaultItems.Socials /> | ||||||
|       <MainMenu.ItemLink |  | ||||||
|         icon={arrowBarToLeftIcon} |  | ||||||
|         href={`${import.meta.env.VITE_APP_PLUS_APP}${ |  | ||||||
|           isExcalidrawPlusSignedUser ? "" : "/sign-up" |  | ||||||
|         }?utm_source=signin&utm_medium=app&utm_content=hamburger`} |  | ||||||
|         className="highlighted" |  | ||||||
|       > |  | ||||||
|         {isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"} |  | ||||||
|       </MainMenu.ItemLink> |  | ||||||
|       <MainMenu.Separator /> |       <MainMenu.Separator /> | ||||||
|       <MainMenu.DefaultItems.ToggleTheme |       <MainMenu.DefaultItems.ToggleTheme /> | ||||||
|         allowSystemTheme |  | ||||||
|         theme={props.theme} |  | ||||||
|         onSelect={props.setTheme} |  | ||||||
|       /> |  | ||||||
|       <MainMenu.ItemCustom> |       <MainMenu.ItemCustom> | ||||||
|         <LanguageList style={{ width: "100%" }} /> |         <LanguageList style={{ width: "100%" }} /> | ||||||
|       </MainMenu.ItemCustom> |       </MainMenu.ItemCustom> | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import React from "react"; | import React from "react"; | ||||||
| import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons"; | import { PlusPromoIcon } from "../../src/components/icons"; | ||||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | import { useI18n } from "../../src/i18n"; | ||||||
| import { WelcomeScreen } from "../../packages/excalidraw/index"; | import { WelcomeScreen } from "../../src/packages/excalidraw/index"; | ||||||
| import { isExcalidrawPlusSignedUser } from "../app_constants"; | import { isExcalidrawPlusSignedUser } from "../app_constants"; | ||||||
| import { POINTER_EVENTS } from "../../packages/excalidraw/constants"; | import { POINTER_EVENTS } from "../../src/constants"; | ||||||
|  |  | ||||||
| export const AppWelcomeScreen: React.FC<{ | export const AppWelcomeScreen: React.FC<{ | ||||||
|   onCollabDialogOpen: () => any; |   setCollabDialogShown: (toggle: boolean) => any; | ||||||
|   isCollabEnabled: boolean; |   isCollabEnabled: boolean; | ||||||
| }> = React.memo((props) => { | }> = React.memo((props) => { | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
| @@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{ | |||||||
|           <WelcomeScreen.Center.MenuItemHelp /> |           <WelcomeScreen.Center.MenuItemHelp /> | ||||||
|           {props.isCollabEnabled && ( |           {props.isCollabEnabled && ( | ||||||
|             <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger |             <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger | ||||||
|               onSelect={() => props.onCollabDialogOpen()} |               onSelect={() => props.setCollabDialogShown(true)} | ||||||
|             /> |             /> | ||||||
|           )} |           )} | ||||||
|           {!isExcalidrawPlusSignedUser && ( |           {!isExcalidrawPlusSignedUser && ( | ||||||
| @@ -61,9 +61,9 @@ export const AppWelcomeScreen: React.FC<{ | |||||||
|                 import.meta.env.VITE_APP_PLUS_LP |                 import.meta.env.VITE_APP_PLUS_LP | ||||||
|               }/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`} |               }/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`} | ||||||
|               shortcut={null} |               shortcut={null} | ||||||
|               icon={arrowBarToLeftIcon} |               icon={PlusPromoIcon} | ||||||
|             > |             > | ||||||
|               Sign up |               Try Excalidraw Plus! | ||||||
|             </WelcomeScreen.Center.MenuItemLink> |             </WelcomeScreen.Center.MenuItemLink> | ||||||
|           )} |           )} | ||||||
|         </WelcomeScreen.Center.Menu> |         </WelcomeScreen.Center.Menu> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { shield } from "../../packages/excalidraw/components/icons"; | import { shield } from "../../src/components/icons"; | ||||||
| import { Tooltip } from "../../packages/excalidraw/components/Tooltip"; | import { Tooltip } from "../../src/components/Tooltip"; | ||||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | import { useI18n } from "../../src/i18n"; | ||||||
|  |  | ||||||
| export const EncryptedIcon = () => { | export const EncryptedIcon = () => { | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|   | |||||||
| @@ -1,36 +1,25 @@ | |||||||
| import React from "react"; | import React from "react"; | ||||||
| import { Card } from "../../packages/excalidraw/components/Card"; | import { Card } from "../../src/components/Card"; | ||||||
| import { ToolButton } from "../../packages/excalidraw/components/ToolButton"; | import { ToolButton } from "../../src/components/ToolButton"; | ||||||
| import { serializeAsJSON } from "../../packages/excalidraw/data/json"; | import { serializeAsJSON } from "../../src/data/json"; | ||||||
| import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; | import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; | ||||||
| import { | import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types"; | ||||||
|   FileId, | import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; | ||||||
|   NonDeletedExcalidrawElement, |  | ||||||
| } from "../../packages/excalidraw/element/types"; |  | ||||||
| import { |  | ||||||
|   AppState, |  | ||||||
|   BinaryFileData, |  | ||||||
|   BinaryFiles, |  | ||||||
| } from "../../packages/excalidraw/types"; |  | ||||||
| import { nanoid } from "nanoid"; | import { nanoid } from "nanoid"; | ||||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | import { useI18n } from "../../src/i18n"; | ||||||
| import { | import { encryptData, generateEncryptionKey } from "../../src/data/encryption"; | ||||||
|   encryptData, | import { isInitializedImageElement } from "../../src/element/typeChecks"; | ||||||
|   generateEncryptionKey, |  | ||||||
| } from "../../packages/excalidraw/data/encryption"; |  | ||||||
| import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; |  | ||||||
| import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; | import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; | ||||||
| import { encodeFilesForUpload } from "../data/FileManager"; | import { encodeFilesForUpload } from "../data/FileManager"; | ||||||
| import { MIME_TYPES } from "../../packages/excalidraw/constants"; | import { MIME_TYPES } from "../../src/constants"; | ||||||
| import { trackEvent } from "../../packages/excalidraw/analytics"; | import { trackEvent } from "../../src/analytics"; | ||||||
| import { getFrame } from "../../packages/excalidraw/utils"; | import { getFrame } from "../../src/utils"; | ||||||
| import { ExcalidrawLogo } from "../../packages/excalidraw/components/ExcalidrawLogo"; | import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo"; | ||||||
|  |  | ||||||
| export const exportToExcalidrawPlus = async ( | export const exportToExcalidrawPlus = async ( | ||||||
|   elements: readonly NonDeletedExcalidrawElement[], |   elements: readonly NonDeletedExcalidrawElement[], | ||||||
|   appState: Partial<AppState>, |   appState: Partial<AppState>, | ||||||
|   files: BinaryFiles, |   files: BinaryFiles, | ||||||
|   name: string, |  | ||||||
| ) => { | ) => { | ||||||
|   const firebase = await loadFirebaseStorage(); |   const firebase = await loadFirebaseStorage(); | ||||||
|  |  | ||||||
| @@ -54,7 +43,7 @@ export const exportToExcalidrawPlus = async ( | |||||||
|     .ref(`/migrations/scenes/${id}`) |     .ref(`/migrations/scenes/${id}`) | ||||||
|     .put(blob, { |     .put(blob, { | ||||||
|       customMetadata: { |       customMetadata: { | ||||||
|         data: JSON.stringify({ version: 2, name }), |         data: JSON.stringify({ version: 2, name: appState.name }), | ||||||
|         created: Date.now().toString(), |         created: Date.now().toString(), | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
| @@ -90,10 +79,9 @@ export const ExportToExcalidrawPlus: React.FC<{ | |||||||
|   elements: readonly NonDeletedExcalidrawElement[]; |   elements: readonly NonDeletedExcalidrawElement[]; | ||||||
|   appState: Partial<AppState>; |   appState: Partial<AppState>; | ||||||
|   files: BinaryFiles; |   files: BinaryFiles; | ||||||
|   name: string; |  | ||||||
|   onError: (error: Error) => void; |   onError: (error: Error) => void; | ||||||
|   onSuccess: () => void; |   onSuccess: () => void; | ||||||
| }> = ({ elements, appState, files, name, onError, onSuccess }) => { | }> = ({ elements, appState, files, onError, onSuccess }) => { | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|   return ( |   return ( | ||||||
|     <Card color="primary"> |     <Card color="primary"> | ||||||
| @@ -119,7 +107,7 @@ export const ExportToExcalidrawPlus: React.FC<{ | |||||||
|         onClick={async () => { |         onClick={async () => { | ||||||
|           try { |           try { | ||||||
|             trackEvent("export", "eplus", `ui (${getFrame()})`); |             trackEvent("export", "eplus", `ui (${getFrame()})`); | ||||||
|             await exportToExcalidrawPlus(elements, appState, files, name); |             await exportToExcalidrawPlus(elements, appState, files); | ||||||
|             onSuccess(); |             onSuccess(); | ||||||
|           } catch (error: any) { |           } catch (error: any) { | ||||||
|             console.error(error); |             console.error(error); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import oc from "open-color"; | import oc from "open-color"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { THEME } from "../../packages/excalidraw/constants"; | import { THEME } from "../../src/constants"; | ||||||
| import { Theme } from "../../packages/excalidraw/element/types"; | import { Theme } from "../../src/element/types"; | ||||||
|  |  | ||||||
| // https://github.com/tholman/github-corners | // https://github.com/tholman/github-corners | ||||||
| export const GitHubCorner = React.memo( | export const GitHubCorner = React.memo( | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { useSetAtom } from "jotai"; | import { useSetAtom } from "jotai"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { appLangCodeAtom } from "../App"; | import { appLangCodeAtom } from ".."; | ||||||
| import { useI18n } from "../../packages/excalidraw/i18n"; | import { useI18n } from "../../src/i18n"; | ||||||
| import { languages } from "../../packages/excalidraw/i18n"; | import { languages } from "../../src/i18n"; | ||||||
|  |  | ||||||
| export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { | export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { | ||||||
|   const { t, langCode } = useI18n(); |   const { t, langCode } = useI18n(); | ||||||
|   | |||||||
| @@ -1,20 +1,19 @@ | |||||||
| import { StoreAction } from "../../packages/excalidraw"; | import { compressData } from "../../src/data/encode"; | ||||||
| import { compressData } from "../../packages/excalidraw/data/encode"; | import { newElementWith } from "../../src/element/mutateElement"; | ||||||
| import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; | import { isInitializedImageElement } from "../../src/element/typeChecks"; | ||||||
| import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; |  | ||||||
| import { | import { | ||||||
|   ExcalidrawElement, |   ExcalidrawElement, | ||||||
|   ExcalidrawImageElement, |   ExcalidrawImageElement, | ||||||
|   FileId, |   FileId, | ||||||
|   InitializedExcalidrawImageElement, |   InitializedExcalidrawImageElement, | ||||||
| } from "../../packages/excalidraw/element/types"; | } from "../../src/element/types"; | ||||||
| import { t } from "../../packages/excalidraw/i18n"; | import { t } from "../../src/i18n"; | ||||||
| import { | import { | ||||||
|   BinaryFileData, |   BinaryFileData, | ||||||
|   BinaryFileMetadata, |   BinaryFileMetadata, | ||||||
|   ExcalidrawImperativeAPI, |   ExcalidrawImperativeAPI, | ||||||
|   BinaryFiles, |   BinaryFiles, | ||||||
| } from "../../packages/excalidraw/types"; | } from "../../src/types"; | ||||||
|  |  | ||||||
| export class FileManager { | export class FileManager { | ||||||
|   /** files being fetched */ |   /** files being fetched */ | ||||||
| @@ -239,6 +238,5 @@ export const updateStaleImageStatuses = (params: { | |||||||
|         } |         } | ||||||
|         return element; |         return element; | ||||||
|       }), |       }), | ||||||
|     storeAction: StoreAction.UPDATE, |  | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -10,30 +10,12 @@ | |||||||
|  *   (localStorage, indexedDB). |  *   (localStorage, indexedDB). | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import { | import { createStore, entries, del, getMany, set, setMany } from "idb-keyval"; | ||||||
|   createStore, | import { clearAppStateForLocalStorage } from "../../src/appState"; | ||||||
|   entries, | import { clearElementsForLocalStorage } from "../../src/element"; | ||||||
|   del, | import { ExcalidrawElement, FileId } from "../../src/element/types"; | ||||||
|   getMany, | import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; | ||||||
|   set, | import { debounce } from "../../src/utils"; | ||||||
|   setMany, |  | ||||||
|   get, |  | ||||||
| } from "idb-keyval"; |  | ||||||
| import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState"; |  | ||||||
| import { LibraryPersistedData } from "../../packages/excalidraw/data/library"; |  | ||||||
| import { ImportedDataState } from "../../packages/excalidraw/data/types"; |  | ||||||
| import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; |  | ||||||
| import { |  | ||||||
|   ExcalidrawElement, |  | ||||||
|   FileId, |  | ||||||
| } from "../../packages/excalidraw/element/types"; |  | ||||||
| import { |  | ||||||
|   AppState, |  | ||||||
|   BinaryFileData, |  | ||||||
|   BinaryFiles, |  | ||||||
| } from "../../packages/excalidraw/types"; |  | ||||||
| import { MaybePromise } from "../../packages/excalidraw/utility-types"; |  | ||||||
| import { debounce } from "../../packages/excalidraw/utils"; |  | ||||||
| import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; | import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; | ||||||
| import { FileManager } from "./FileManager"; | import { FileManager } from "./FileManager"; | ||||||
| import { Locker } from "./Locker"; | import { Locker } from "./Locker"; | ||||||
| @@ -194,52 +176,3 @@ export class LocalData { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| export class LibraryIndexedDBAdapter { |  | ||||||
|   /** IndexedDB database and store name */ |  | ||||||
|   private static idb_name = STORAGE_KEYS.IDB_LIBRARY; |  | ||||||
|   /** library data store key */ |  | ||||||
|   private static key = "libraryData"; |  | ||||||
|  |  | ||||||
|   private static store = createStore( |  | ||||||
|     `${LibraryIndexedDBAdapter.idb_name}-db`, |  | ||||||
|     `${LibraryIndexedDBAdapter.idb_name}-store`, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   static async load() { |  | ||||||
|     const IDBData = await get<LibraryPersistedData>( |  | ||||||
|       LibraryIndexedDBAdapter.key, |  | ||||||
|       LibraryIndexedDBAdapter.store, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return IDBData || null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static save(data: LibraryPersistedData): MaybePromise<void> { |  | ||||||
|     return set( |  | ||||||
|       LibraryIndexedDBAdapter.key, |  | ||||||
|       data, |  | ||||||
|       LibraryIndexedDBAdapter.store, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** LS Adapter used only for migrating LS library data |  | ||||||
|  * to indexedDB */ |  | ||||||
| export class LibraryLocalStorageMigrationAdapter { |  | ||||||
|   static load() { |  | ||||||
|     const LSData = localStorage.getItem( |  | ||||||
|       STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY, |  | ||||||
|     ); |  | ||||||
|     if (LSData != null) { |  | ||||||
|       const libraryItems: ImportedDataState["libraryItems"] = |  | ||||||
|         JSON.parse(LSData); |  | ||||||
|       if (libraryItems) { |  | ||||||
|         return { libraryItems }; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|   static clear() { |  | ||||||
|     localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,29 +1,20 @@ | |||||||
| import { reconcileElements } from "../../packages/excalidraw"; | import { ExcalidrawElement, FileId } from "../../src/element/types"; | ||||||
| import { | import { getSceneVersion } from "../../src/element"; | ||||||
|   ExcalidrawElement, |  | ||||||
|   FileId, |  | ||||||
|   OrderedExcalidrawElement, |  | ||||||
| } from "../../packages/excalidraw/element/types"; |  | ||||||
| import { getSceneVersion } from "../../packages/excalidraw/element"; |  | ||||||
| import Portal from "../collab/Portal"; | import Portal from "../collab/Portal"; | ||||||
| import { restoreElements } from "../../packages/excalidraw/data/restore"; | import { restoreElements } from "../../src/data/restore"; | ||||||
| import { | import { | ||||||
|   AppState, |   AppState, | ||||||
|   BinaryFileData, |   BinaryFileData, | ||||||
|   BinaryFileMetadata, |   BinaryFileMetadata, | ||||||
|   DataURL, |   DataURL, | ||||||
| } from "../../packages/excalidraw/types"; | } from "../../src/types"; | ||||||
| import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants"; | import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants"; | ||||||
| import { decompressData } from "../../packages/excalidraw/data/encode"; | import { decompressData } from "../../src/data/encode"; | ||||||
| import { | import { encryptData, decryptData } from "../../src/data/encryption"; | ||||||
|   encryptData, | import { MIME_TYPES } from "../../src/constants"; | ||||||
|   decryptData, | import { reconcileElements } from "../collab/reconciliation"; | ||||||
| } from "../../packages/excalidraw/data/encryption"; |  | ||||||
| import { MIME_TYPES } from "../../packages/excalidraw/constants"; |  | ||||||
| import { getSyncableElements, SyncableExcalidrawElement } from "."; | import { getSyncableElements, SyncableExcalidrawElement } from "."; | ||||||
| import { ResolutionType } from "../../packages/excalidraw/utility-types"; | import { ResolutionType } from "../../src/utility-types"; | ||||||
| import type { Socket } from "socket.io-client"; |  | ||||||
| import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile"; |  | ||||||
|  |  | ||||||
| // private | // private | ||||||
| // ----------------------------------------------------------------------------- | // ----------------------------------------------------------------------------- | ||||||
| @@ -141,12 +132,12 @@ const decryptElements = async ( | |||||||
| }; | }; | ||||||
|  |  | ||||||
| class FirebaseSceneVersionCache { | class FirebaseSceneVersionCache { | ||||||
|   private static cache = new WeakMap<Socket, number>(); |   private static cache = new WeakMap<SocketIOClient.Socket, number>(); | ||||||
|   static get = (socket: Socket) => { |   static get = (socket: SocketIOClient.Socket) => { | ||||||
|     return FirebaseSceneVersionCache.cache.get(socket); |     return FirebaseSceneVersionCache.cache.get(socket); | ||||||
|   }; |   }; | ||||||
|   static set = ( |   static set = ( | ||||||
|     socket: Socket, |     socket: SocketIOClient.Socket, | ||||||
|     elements: readonly SyncableExcalidrawElement[], |     elements: readonly SyncableExcalidrawElement[], | ||||||
|   ) => { |   ) => { | ||||||
|     FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements)); |     FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements)); | ||||||
| @@ -232,7 +223,7 @@ export const saveToFirebase = async ( | |||||||
|     !socket || |     !socket || | ||||||
|     isSavedToFirebase(portal, elements) |     isSavedToFirebase(portal, elements) | ||||||
|   ) { |   ) { | ||||||
|     return null; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const firebase = await loadFirestore(); |   const firebase = await loadFirestore(); | ||||||
| @@ -240,59 +231,56 @@ export const saveToFirebase = async ( | |||||||
|  |  | ||||||
|   const docRef = firestore.collection("scenes").doc(roomId); |   const docRef = firestore.collection("scenes").doc(roomId); | ||||||
|  |  | ||||||
|   const storedScene = await firestore.runTransaction(async (transaction) => { |   const savedData = await firestore.runTransaction(async (transaction) => { | ||||||
|     const snapshot = await transaction.get(docRef); |     const snapshot = await transaction.get(docRef); | ||||||
|  |  | ||||||
|     if (!snapshot.exists) { |     if (!snapshot.exists) { | ||||||
|       const storedScene = await createFirebaseSceneDocument( |       const sceneDocument = await createFirebaseSceneDocument( | ||||||
|         firebase, |         firebase, | ||||||
|         elements, |         elements, | ||||||
|         roomKey, |         roomKey, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       transaction.set(docRef, storedScene); |       transaction.set(docRef, sceneDocument); | ||||||
|  |  | ||||||
|       return storedScene; |       return { | ||||||
|  |         elements, | ||||||
|  |         reconciledElements: null, | ||||||
|  |       }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const prevStoredScene = snapshot.data() as FirebaseStoredScene; |     const prevDocData = snapshot.data() as FirebaseStoredScene; | ||||||
|     const prevStoredElements = getSyncableElements( |     const prevElements = getSyncableElements( | ||||||
|       restoreElements(await decryptElements(prevStoredScene, roomKey), null), |       await decryptElements(prevDocData, roomKey), | ||||||
|     ); |  | ||||||
|     const reconciledElements = getSyncableElements( |  | ||||||
|       reconcileElements( |  | ||||||
|         elements, |  | ||||||
|         prevStoredElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[], |  | ||||||
|         appState, |  | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const storedScene = await createFirebaseSceneDocument( |     const reconciledElements = getSyncableElements( | ||||||
|  |       reconcileElements(elements, prevElements, appState), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const sceneDocument = await createFirebaseSceneDocument( | ||||||
|       firebase, |       firebase, | ||||||
|       reconciledElements, |       reconciledElements, | ||||||
|       roomKey, |       roomKey, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     transaction.update(docRef, storedScene); |     transaction.update(docRef, sceneDocument); | ||||||
|  |     return { | ||||||
|     // Return the stored elements as the in memory `reconciledElements` could have mutated in the meantime |       elements, | ||||||
|     return storedScene; |       reconciledElements, | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const storedElements = getSyncableElements( |   FirebaseSceneVersionCache.set(socket, savedData.elements); | ||||||
|     restoreElements(await decryptElements(storedScene, roomKey), null), |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   FirebaseSceneVersionCache.set(socket, storedElements); |   return { reconciledElements: savedData.reconciledElements }; | ||||||
|  |  | ||||||
|   return storedElements; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const loadFromFirebase = async ( | export const loadFromFirebase = async ( | ||||||
|   roomId: string, |   roomId: string, | ||||||
|   roomKey: string, |   roomKey: string, | ||||||
|   socket: Socket | null, |   socket: SocketIOClient.Socket | null, | ||||||
| ): Promise<readonly SyncableExcalidrawElement[] | null> => { | ): Promise<readonly ExcalidrawElement[] | null> => { | ||||||
|   const firebase = await loadFirestore(); |   const firebase = await loadFirestore(); | ||||||
|   const db = firebase.firestore(); |   const db = firebase.firestore(); | ||||||
|  |  | ||||||
| @@ -303,14 +291,14 @@ export const loadFromFirebase = async ( | |||||||
|   } |   } | ||||||
|   const storedScene = doc.data() as FirebaseStoredScene; |   const storedScene = doc.data() as FirebaseStoredScene; | ||||||
|   const elements = getSyncableElements( |   const elements = getSyncableElements( | ||||||
|     restoreElements(await decryptElements(storedScene, roomKey), null), |     await decryptElements(storedScene, roomKey), | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   if (socket) { |   if (socket) { | ||||||
|     FirebaseSceneVersionCache.set(socket, elements); |     FirebaseSceneVersionCache.set(socket, elements); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return elements; |   return restoreElements(elements, null); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const loadFilesFromFirebase = async ( | export const loadFilesFromFirebase = async ( | ||||||
|   | |||||||
| @@ -1,47 +1,37 @@ | |||||||
| import { | import { compressData, decompressData } from "../../src/data/encode"; | ||||||
|   compressData, |  | ||||||
|   decompressData, |  | ||||||
| } from "../../packages/excalidraw/data/encode"; |  | ||||||
| import { | import { | ||||||
|   decryptData, |   decryptData, | ||||||
|   generateEncryptionKey, |   generateEncryptionKey, | ||||||
|   IV_LENGTH_BYTES, |   IV_LENGTH_BYTES, | ||||||
| } from "../../packages/excalidraw/data/encryption"; | } from "../../src/data/encryption"; | ||||||
| import { serializeAsJSON } from "../../packages/excalidraw/data/json"; | import { serializeAsJSON } from "../../src/data/json"; | ||||||
| import { restore } from "../../packages/excalidraw/data/restore"; | import { restore } from "../../src/data/restore"; | ||||||
| import { ImportedDataState } from "../../packages/excalidraw/data/types"; | import { ImportedDataState } from "../../src/data/types"; | ||||||
| import { SceneBounds } from "../../packages/excalidraw/element/bounds"; | import { isInvisiblySmallElement } from "../../src/element/sizeHelpers"; | ||||||
| import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers"; | import { isInitializedImageElement } from "../../src/element/typeChecks"; | ||||||
| import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; | import { ExcalidrawElement, FileId } from "../../src/element/types"; | ||||||
| import { | import { t } from "../../src/i18n"; | ||||||
|   ExcalidrawElement, |  | ||||||
|   FileId, |  | ||||||
|   OrderedExcalidrawElement, |  | ||||||
| } from "../../packages/excalidraw/element/types"; |  | ||||||
| import { t } from "../../packages/excalidraw/i18n"; |  | ||||||
| import { | import { | ||||||
|   AppState, |   AppState, | ||||||
|   BinaryFileData, |   BinaryFileData, | ||||||
|   BinaryFiles, |   BinaryFiles, | ||||||
|   SocketId, |  | ||||||
|   UserIdleState, |   UserIdleState, | ||||||
| } from "../../packages/excalidraw/types"; | } from "../../src/types"; | ||||||
| import { MakeBrand } from "../../packages/excalidraw/utility-types"; | import { bytesToHexString } from "../../src/utils"; | ||||||
| import { bytesToHexString } from "../../packages/excalidraw/utils"; |  | ||||||
| import { | import { | ||||||
|   DELETED_ELEMENT_TIMEOUT, |   DELETED_ELEMENT_TIMEOUT, | ||||||
|   FILE_UPLOAD_MAX_BYTES, |   FILE_UPLOAD_MAX_BYTES, | ||||||
|   ROOM_ID_BYTES, |   ROOM_ID_BYTES, | ||||||
|   WS_SUBTYPES, |  | ||||||
| } from "../app_constants"; | } from "../app_constants"; | ||||||
| import { encodeFilesForUpload } from "./FileManager"; | import { encodeFilesForUpload } from "./FileManager"; | ||||||
| import { saveFilesToFirebase } from "./firebase"; | import { saveFilesToFirebase } from "./firebase"; | ||||||
|  |  | ||||||
| export type SyncableExcalidrawElement = OrderedExcalidrawElement & | export type SyncableExcalidrawElement = ExcalidrawElement & { | ||||||
|   MakeBrand<"SyncableExcalidrawElement">; |   _brand: "SyncableExcalidrawElement"; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const isSyncableElement = ( | export const isSyncableElement = ( | ||||||
|   element: OrderedExcalidrawElement, |   element: ExcalidrawElement, | ||||||
| ): element is SyncableExcalidrawElement => { | ): element is SyncableExcalidrawElement => { | ||||||
|   if (element.isDeleted) { |   if (element.isDeleted) { | ||||||
|     if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) { |     if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) { | ||||||
| @@ -52,9 +42,7 @@ export const isSyncableElement = ( | |||||||
|   return !isInvisiblySmallElement(element); |   return !isInvisiblySmallElement(element); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const getSyncableElements = ( | export const getSyncableElements = (elements: readonly ExcalidrawElement[]) => | ||||||
|   elements: readonly OrderedExcalidrawElement[], |  | ||||||
| ) => |  | ||||||
|   elements.filter((element) => |   elements.filter((element) => | ||||||
|     isSyncableElement(element), |     isSyncableElement(element), | ||||||
|   ) as SyncableExcalidrawElement[]; |   ) as SyncableExcalidrawElement[]; | ||||||
| @@ -68,49 +56,67 @@ const generateRoomId = async () => { | |||||||
|   return bytesToHexString(buffer); |   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 = { | export type EncryptedData = { | ||||||
|   data: ArrayBuffer; |   data: ArrayBuffer; | ||||||
|   iv: Uint8Array; |   iv: Uint8Array; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type SocketUpdateDataSource = { | export type SocketUpdateDataSource = { | ||||||
|   INVALID_RESPONSE: { |  | ||||||
|     type: WS_SUBTYPES.INVALID_RESPONSE; |  | ||||||
|   }; |  | ||||||
|   SCENE_INIT: { |   SCENE_INIT: { | ||||||
|     type: WS_SUBTYPES.INIT; |     type: "SCENE_INIT"; | ||||||
|     payload: { |     payload: { | ||||||
|       elements: readonly ExcalidrawElement[]; |       elements: readonly ExcalidrawElement[]; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   SCENE_UPDATE: { |   SCENE_UPDATE: { | ||||||
|     type: WS_SUBTYPES.UPDATE; |     type: "SCENE_UPDATE"; | ||||||
|     payload: { |     payload: { | ||||||
|       elements: readonly ExcalidrawElement[]; |       elements: readonly ExcalidrawElement[]; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   MOUSE_LOCATION: { |   MOUSE_LOCATION: { | ||||||
|     type: WS_SUBTYPES.MOUSE_LOCATION; |     type: "MOUSE_LOCATION"; | ||||||
|     payload: { |     payload: { | ||||||
|       socketId: SocketId; |       socketId: string; | ||||||
|       pointer: { x: number; y: number; tool: "pointer" | "laser" }; |       pointer: { x: number; y: number; tool: "pointer" | "laser" }; | ||||||
|       button: "down" | "up"; |       button: "down" | "up"; | ||||||
|       selectedElementIds: AppState["selectedElementIds"]; |       selectedElementIds: AppState["selectedElementIds"]; | ||||||
|       username: string; |       username: string; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   USER_VISIBLE_SCENE_BOUNDS: { |  | ||||||
|     type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS; |  | ||||||
|     payload: { |  | ||||||
|       socketId: SocketId; |  | ||||||
|       username: string; |  | ||||||
|       sceneBounds: SceneBounds; |  | ||||||
|     }; |  | ||||||
|   }; |  | ||||||
|   IDLE_STATUS: { |   IDLE_STATUS: { | ||||||
|     type: WS_SUBTYPES.IDLE_STATUS; |     type: "IDLE_STATUS"; | ||||||
|     payload: { |     payload: { | ||||||
|       socketId: SocketId; |       socketId: string; | ||||||
|       userState: UserIdleState; |       userState: UserIdleState; | ||||||
|       username: string; |       username: string; | ||||||
|     }; |     }; | ||||||
| @@ -118,7 +124,10 @@ export type SocketUpdateDataSource = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export type SocketUpdateDataIncoming = | export type SocketUpdateDataIncoming = | ||||||
|   SocketUpdateDataSource[keyof SocketUpdateDataSource]; |   | SocketUpdateDataSource[keyof SocketUpdateDataSource] | ||||||
|  |   | { | ||||||
|  |       type: "INVALID_RESPONSE"; | ||||||
|  |     }; | ||||||
|  |  | ||||||
| export type SocketUpdateData = | export type SocketUpdateData = | ||||||
|   SocketUpdateDataSource[keyof SocketUpdateDataSource] & { |   SocketUpdateDataSource[keyof SocketUpdateDataSource] & { | ||||||
| @@ -269,6 +278,7 @@ export const loadScene = async ( | |||||||
|     // in the scene database/localStorage, and instead fetch them async |     // in the scene database/localStorage, and instead fetch them async | ||||||
|     // from a different database |     // from a different database | ||||||
|     files: data.files, |     files: data.files, | ||||||
|  |     commitToHistory: false, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; | import { ExcalidrawElement } from "../../src/element/types"; | ||||||
| import { AppState } from "../../packages/excalidraw/types"; | import { AppState } from "../../src/types"; | ||||||
| import { | import { | ||||||
|   clearAppStateForLocalStorage, |   clearAppStateForLocalStorage, | ||||||
|   getDefaultAppState, |   getDefaultAppState, | ||||||
| } from "../../packages/excalidraw/appState"; | } from "../../src/appState"; | ||||||
| import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; | import { clearElementsForLocalStorage } from "../../src/element"; | ||||||
| import { STORAGE_KEYS } from "../app_constants"; | import { STORAGE_KEYS } from "../app_constants"; | ||||||
|  | import { ImportedDataState } from "../../src/data/types"; | ||||||
|  |  | ||||||
| export const saveUsernameToLocalStorage = (username: string) => { | export const saveUsernameToLocalStorage = (username: string) => { | ||||||
|   try { |   try { | ||||||
| @@ -87,13 +88,28 @@ export const getTotalStorageSize = () => { | |||||||
|   try { |   try { | ||||||
|     const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE); |     const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE); | ||||||
|     const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB); |     const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB); | ||||||
|  |     const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); | ||||||
|  |  | ||||||
|     const appStateSize = appState?.length || 0; |     const appStateSize = appState?.length || 0; | ||||||
|     const collabSize = collab?.length || 0; |     const collabSize = collab?.length || 0; | ||||||
|  |     const librarySize = library?.length || 0; | ||||||
|  |  | ||||||
|     return appStateSize + collabSize + getElementsStorageSize(); |     return appStateSize + collabSize + librarySize + getElementsStorageSize(); | ||||||
|   } catch (error: any) { |   } catch (error: any) { | ||||||
|     console.error(error); |     console.error(error); | ||||||
|     return 0; |     return 0; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const getLibraryItemsFromStorage = () => { | ||||||
|  |   try { | ||||||
|  |     const libraryItems: ImportedDataState["libraryItems"] = JSON.parse( | ||||||
|  |       localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return libraryItems || []; | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error(error); | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								excalidraw-app/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								excalidraw-app/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | |||||||
| interface Window { |  | ||||||
|   __EXCALIDRAW_SHA__: string | undefined; |  | ||||||
| } |  | ||||||
| @@ -4,13 +4,6 @@ | |||||||
|   &.theme--dark { |   &.theme--dark { | ||||||
|     --color-primary-contrast-offset: #726dff; // to offset Chubb illusion |     --color-primary-contrast-offset: #726dff; // to offset Chubb illusion | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .top-right-ui { |  | ||||||
|     display: flex; |  | ||||||
|     justify-content: center; |  | ||||||
|     align-items: flex-start; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .footer-center { |   .footer-center { | ||||||
|     justify-content: flex-end; |     justify-content: flex-end; | ||||||
|     margin-top: auto; |     margin-top: auto; | ||||||
| @@ -38,7 +31,7 @@ | |||||||
|         background-color: #ecfdf5; |         background-color: #ecfdf5; | ||||||
|         color: #064e3c; |         color: #064e3c; | ||||||
|       } |       } | ||||||
|       &.highlighted { |       &.ExcalidrawPlus { | ||||||
|         color: var(--color-promo); |         color: var(--color-promo); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,15 +1,811 @@ | |||||||
| import { StrictMode } from "react"; | import polyfill from "../src/polyfill"; | ||||||
| import { createRoot } from "react-dom/client"; | import LanguageDetector from "i18next-browser-languagedetector"; | ||||||
| import ExcalidrawApp from "./App"; | import { useEffect, useRef, useState } from "react"; | ||||||
| import { registerSW } from "virtual:pwa-register"; | 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 "../excalidraw-app/sentry"; | import "./index.scss"; | ||||||
| window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA; | import { ResolutionType } from "../src/utility-types"; | ||||||
| const rootElement = document.getElementById("root")!; | import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog"; | ||||||
| const root = createRoot(rootElement); | import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState"; | ||||||
| registerSW(); | import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm"; | ||||||
| root.render( | import Trans from "../src/components/Trans"; | ||||||
|   <StrictMode> |  | ||||||
|     <ExcalidrawApp /> | polyfill(); | ||||||
|   </StrictMode>, |  | ||||||
|  | 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 [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; | ||||||
|   | |||||||
| @@ -1,42 +0,0 @@ | |||||||
| { |  | ||||||
|   "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": { |  | ||||||
|     "vite-plugin-html": "3.2.2" |  | ||||||
|   }, |  | ||||||
|   "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,299 +0,0 @@ | |||||||
| import { useEffect, 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"; |  | ||||||
| import { useUIAppState } from "../../packages/excalidraw/context/ui-appState"; |  | ||||||
|  |  | ||||||
| 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); |  | ||||||
|     } catch (e) { |  | ||||||
|       collabAPI.setCollabError(t("errors.copyToSystemClipboardFailed")); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setJustCopied(true); |  | ||||||
|  |  | ||||||
|     if (timerRef.current) { |  | ||||||
|       window.clearTimeout(timerRef.current); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     timerRef.current = window.setTimeout(() => { |  | ||||||
|       setJustCopied(false); |  | ||||||
|     }, 3000); |  | ||||||
|  |  | ||||||
|     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); |  | ||||||
|  |  | ||||||
|   const { openDialog } = useUIAppState(); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (openDialog) { |  | ||||||
|       setShareDialogState({ isOpen: false }); |  | ||||||
|     } |  | ||||||
|   }, [openDialog, setShareDialogState]); |  | ||||||
|  |  | ||||||
|   if (!shareDialogState.isOpen) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <ShareDialogInner |  | ||||||
|       handleClose={() => setShareDialogState({ isOpen: false })} |  | ||||||
|       collabAPI={props.collabAPI} |  | ||||||
|       onExportToBackend={props.onExportToBackend} |  | ||||||
|       type={shareDialogState.type} |  | ||||||
|     /> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,13 +1,8 @@ | |||||||
| import { defaultLang } from "../../packages/excalidraw/i18n"; | import { defaultLang } from "../../src/i18n"; | ||||||
| import { UI } from "../../packages/excalidraw/tests/helpers/ui"; | import { UI } from "../../src/tests/helpers/ui"; | ||||||
| import { | import { screen, fireEvent, waitFor, render } from "../../src/tests/test-utils"; | ||||||
|   screen, |  | ||||||
|   fireEvent, |  | ||||||
|   waitFor, |  | ||||||
|   render, |  | ||||||
| } from "../../packages/excalidraw/tests/test-utils"; |  | ||||||
|  |  | ||||||
| import ExcalidrawApp from "../App"; | import ExcalidrawApp from "../../excalidraw-app"; | ||||||
|  |  | ||||||
| describe("Test LanguageList", () => { | describe("Test LanguageList", () => { | ||||||
|   it("rerenders UI on language change", async () => { |   it("rerenders UI on language change", async () => { | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import ExcalidrawApp from "../App"; | import ExcalidrawApp from "../../excalidraw-app"; | ||||||
| import { | import { | ||||||
|   mockBoundingClientRect, |   mockBoundingClientRect, | ||||||
|   render, |   render, | ||||||
|   restoreOriginalGetBoundingClientRect, |   restoreOriginalGetBoundingClientRect, | ||||||
| } from "../../packages/excalidraw/tests/test-utils"; | } from "../../src/tests/test-utils"; | ||||||
|  |  | ||||||
| import { UI } from "../../packages/excalidraw/tests/helpers/ui"; | import { UI } from "../../src/tests/helpers/ui"; | ||||||
|  |  | ||||||
| describe("Test MobileMenu", () => { | describe("Test MobileMenu", () => { | ||||||
|   const { h } = window; |   const { h } = window; | ||||||
|   | |||||||
| @@ -216,23 +216,32 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u | |||||||
|           stroke-width="2" |           stroke-width="2" | ||||||
|           viewBox="0 0 24 24" |           viewBox="0 0 24 24" | ||||||
|         > |         > | ||||||
|           <g> |           <g | ||||||
|  |             stroke-width="1.5" | ||||||
|  |           > | ||||||
|             <path |             <path | ||||||
|               d="M0 0h24v24H0z" |               d="M0 0h24v24H0z" | ||||||
|               fill="none" |               fill="none" | ||||||
|               stroke="none" |               stroke="none" | ||||||
|             /> |             /> | ||||||
|             <path |             <rect | ||||||
|               d="M10 12l10 0" |               height="4" | ||||||
|  |               rx="1" | ||||||
|  |               width="18" | ||||||
|  |               x="3" | ||||||
|  |               y="8" | ||||||
|  |             /> | ||||||
|  |             <line | ||||||
|  |               x1="12" | ||||||
|  |               x2="12" | ||||||
|  |               y1="8" | ||||||
|  |               y2="21" | ||||||
|             /> |             /> | ||||||
|             <path |             <path | ||||||
|               d="M10 12l4 4" |               d="M19 12v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-7" | ||||||
|             /> |             /> | ||||||
|             <path |             <path | ||||||
|               d="M10 12l4 -4" |               d="M7.5 8a2.5 2.5 0 0 1 0 -5a4.8 8 0 0 1 4.5 5a4.8 8 0 0 1 4.5 -5a2.5 2.5 0 0 1 0 5" | ||||||
|             /> |  | ||||||
|             <path |  | ||||||
|               d="M4 4l0 16" |  | ||||||
|             /> |             /> | ||||||
|           </g> |           </g> | ||||||
|         </svg> |         </svg> | ||||||
| @@ -240,7 +249,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u | |||||||
|       <div |       <div | ||||||
|         class="welcome-screen-menu-item__text" |         class="welcome-screen-menu-item__text" | ||||||
|       > |       > | ||||||
|         Sign up |         Try Excalidraw Plus! | ||||||
|       </div> |       </div> | ||||||
|     </a> |     </a> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -1,19 +1,8 @@ | |||||||
| import { vi } from "vitest"; | import { vi } from "vitest"; | ||||||
| import { | import { render, updateSceneData, waitFor } from "../../src/tests/test-utils"; | ||||||
|   act, | import ExcalidrawApp from "../../excalidraw-app"; | ||||||
|   render, | import { API } from "../../src/tests/helpers/api"; | ||||||
|   updateSceneData, | import { createUndoAction } from "../../src/actions/actionHistory"; | ||||||
|   waitFor, |  | ||||||
| } from "../../packages/excalidraw/tests/test-utils"; |  | ||||||
| import ExcalidrawApp from "../App"; |  | ||||||
| import { API } from "../../packages/excalidraw/tests/helpers/api"; |  | ||||||
| import { syncInvalidIndices } from "../../packages/excalidraw/fractionalIndex"; |  | ||||||
| import { |  | ||||||
|   createRedoAction, |  | ||||||
|   createUndoAction, |  | ||||||
| } from "../../packages/excalidraw/actions/actionHistory"; |  | ||||||
| import { StoreAction, newElementWith } from "../../packages/excalidraw"; |  | ||||||
|  |  | ||||||
| const { h } = window; | const { h } = window; | ||||||
|  |  | ||||||
| Object.defineProperty(window, "crypto", { | Object.defineProperty(window, "crypto", { | ||||||
| @@ -27,6 +16,17 @@ Object.defineProperty(window, "crypto", { | |||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => { | ||||||
|  |   const module = (await importActual()) as any; | ||||||
|  |   return { | ||||||
|  |     __esmodule: true, | ||||||
|  |     ...module, | ||||||
|  |     getCollabServer: vi.fn(() => ({ | ||||||
|  |       url: /* doesn't really matter */ "http://localhost:3002", | ||||||
|  |     })), | ||||||
|  |   }; | ||||||
|  | }); | ||||||
|  |  | ||||||
| vi.mock("../../excalidraw-app/data/firebase.ts", () => { | vi.mock("../../excalidraw-app/data/firebase.ts", () => { | ||||||
|   const loadFromFirebase = async () => null; |   const loadFromFirebase = async () => null; | ||||||
|   const saveToFirebase = () => {}; |   const saveToFirebase = () => {}; | ||||||
| @@ -63,190 +63,39 @@ vi.mock("socket.io-client", () => { | |||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * These test would deserve to be extended by testing collab with (at least) two clients simultanouesly, |  | ||||||
|  * while having access to both scenes, appstates stores, histories and etc. |  | ||||||
|  * i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously. |  | ||||||
|  */ |  | ||||||
| describe("collaboration", () => { | describe("collaboration", () => { | ||||||
|   it("should allow to undo / redo even on force-deleted elements", async () => { |   it("creating room should reset deleted elements", async () => { | ||||||
|     await render(<ExcalidrawApp />); |     await render(<ExcalidrawApp />); | ||||||
|     const rect1Props = { |     // To update the scene with deleted elements before starting collab | ||||||
|       type: "rectangle", |     updateSceneData({ | ||||||
|       id: "A", |       elements: [ | ||||||
|       height: 200, |         API.createElement({ type: "rectangle", id: "A" }), | ||||||
|       width: 100, |         API.createElement({ | ||||||
|     } as const; |  | ||||||
|  |  | ||||||
|     const rect2Props = { |  | ||||||
|           type: "rectangle", |           type: "rectangle", | ||||||
|           id: "B", |           id: "B", | ||||||
|       width: 100, |           isDeleted: true, | ||||||
|       height: 200, |         }), | ||||||
|     } as const; |       ], | ||||||
|  |  | ||||||
|     const rect1 = API.createElement({ ...rect1Props }); |  | ||||||
|     const rect2 = API.createElement({ ...rect2Props }); |  | ||||||
|  |  | ||||||
|     updateSceneData({ |  | ||||||
|       elements: syncInvalidIndices([rect1, rect2]), |  | ||||||
|       storeAction: StoreAction.CAPTURE, |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     updateSceneData({ |  | ||||||
|       elements: syncInvalidIndices([ |  | ||||||
|         rect1, |  | ||||||
|         newElementWith(h.elements[1], { isDeleted: true }), |  | ||||||
|       ]), |  | ||||||
|       storeAction: StoreAction.CAPTURE, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await waitFor(() => { |     await waitFor(() => { | ||||||
|       expect(API.getUndoStack().length).toBe(2); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([ |       expect(h.elements).toEqual([ | ||||||
|         expect.objectContaining(rect1Props), |         expect.objectContaining({ id: "A" }), | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true }), |         expect.objectContaining({ id: "B", isDeleted: true }), | ||||||
|       ]); |       ]); | ||||||
|  |       expect(API.getStateHistory().length).toBe(1); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // one form of force deletion happens when starting the collab, not to sync potentially sensitive data into the server |  | ||||||
|     window.collab.startCollaboration(null); |     window.collab.startCollaboration(null); | ||||||
|  |  | ||||||
|     await waitFor(() => { |     await waitFor(() => { | ||||||
|       expect(API.getUndoStack().length).toBe(2); |       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); | ||||||
|       // we never delete from the local snapshot as it is used for correct diff calculation |       expect(API.getStateHistory().length).toBe(1); | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([expect.objectContaining(rect1Props)]); |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const undoAction = createUndoAction(h.history, h.store); |     const undoAction = createUndoAction(h.history); | ||||||
|     act(() => h.app.actionManager.executeAction(undoAction)); |     // noop | ||||||
|  |     h.app.actionManager.executeAction(undoAction); | ||||||
|     // with explicit undo (as addition) we expect our item to be restored from the snapshot! |  | ||||||
|     await waitFor(() => { |     await waitFor(() => { | ||||||
|       expect(API.getUndoStack().length).toBe(1); |       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); | ||||||
|       expect(API.getSnapshot()).toEqual([ |       expect(API.getStateHistory().length).toBe(1); | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: false }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: false }), |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // simulate force deleting the element remotely |  | ||||||
|     updateSceneData({ |  | ||||||
|       elements: syncInvalidIndices([rect1]), |  | ||||||
|       storeAction: StoreAction.UPDATE, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(API.getUndoStack().length).toBe(1); |  | ||||||
|       expect(API.getRedoStack().length).toBe(1); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([expect.objectContaining(rect1Props)]); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const redoAction = createRedoAction(h.history, h.store); |  | ||||||
|     act(() => h.app.actionManager.executeAction(redoAction)); |  | ||||||
|  |  | ||||||
|     // with explicit redo (as removal) we again restore the element from the snapshot! |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(API.getUndoStack().length).toBe(2); |  | ||||||
|       expect(API.getRedoStack().length).toBe(0); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true }), |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     act(() => h.app.actionManager.executeAction(undoAction)); |  | ||||||
|  |  | ||||||
|     // simulate local update |  | ||||||
|     updateSceneData({ |  | ||||||
|       elements: syncInvalidIndices([ |  | ||||||
|         h.elements[0], |  | ||||||
|         newElementWith(h.elements[1], { x: 100 }), |  | ||||||
|       ]), |  | ||||||
|       storeAction: StoreAction.CAPTURE, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(API.getUndoStack().length).toBe(2); |  | ||||||
|       expect(API.getRedoStack().length).toBe(0); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }), |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     act(() => h.app.actionManager.executeAction(undoAction)); |  | ||||||
|  |  | ||||||
|     // we expect to iterate the stack to the first visible change |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(API.getUndoStack().length).toBe(1); |  | ||||||
|       expect(API.getRedoStack().length).toBe(1); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }), |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // simulate force deleting the element remotely |  | ||||||
|     updateSceneData({ |  | ||||||
|       elements: syncInvalidIndices([rect1]), |  | ||||||
|       storeAction: StoreAction.UPDATE, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // snapshot was correctly updated and marked the element as deleted |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(API.getUndoStack().length).toBe(1); |  | ||||||
|       expect(API.getRedoStack().length).toBe(1); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining(rect1Props), |  | ||||||
|         expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.elements).toEqual([expect.objectContaining(rect1Props)]); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     act(() => h.app.actionManager.executeAction(redoAction)); |  | ||||||
|  |  | ||||||
|     // with explicit redo (as update) we again restored the element from the snapshot! |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(API.getUndoStack().length).toBe(2); |  | ||||||
|       expect(API.getRedoStack().length).toBe(0); |  | ||||||
|       expect(API.getSnapshot()).toEqual([ |  | ||||||
|         expect.objectContaining({ id: "A", isDeleted: false }), |  | ||||||
|         expect.objectContaining({ id: "B", isDeleted: true, x: 100 }), |  | ||||||
|       ]); |  | ||||||
|       expect(h.history.isRedoStackEmpty).toBeTruthy(); |  | ||||||
|       expect(h.elements).toEqual([ |  | ||||||
|         expect.objectContaining({ id: "A", isDeleted: false }), |  | ||||||
|         expect.objectContaining({ id: "B", isDeleted: true, x: 100 }), |  | ||||||
|       ]); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										421
									
								
								excalidraw-app/tests/reconciliation.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								excalidraw-app/tests/reconciliation.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | |||||||
|  | import { expect } from "chai"; | ||||||
|  | import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; | ||||||
|  | import { ExcalidrawElement } from "../../src/element/types"; | ||||||
|  | import { | ||||||
|  |   BroadcastedExcalidrawElement, | ||||||
|  |   ReconciledElements, | ||||||
|  |   reconcileElements, | ||||||
|  | } from "../../excalidraw-app/collab/reconciliation"; | ||||||
|  | import { randomInteger } from "../../src/random"; | ||||||
|  | import { AppState } from "../../src/types"; | ||||||
|  | import { cloneJSON } from "../../src/utils"; | ||||||
|  |  | ||||||
|  | type Id = string; | ||||||
|  | type ElementLike = { | ||||||
|  |   id: string; | ||||||
|  |   version: number; | ||||||
|  |   versionNonce: number; | ||||||
|  |   [PRECEDING_ELEMENT_KEY]?: string | null; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type Cache = Record<string, ExcalidrawElement | undefined>; | ||||||
|  |  | ||||||
|  | const createElement = (opts: { uid: string } | ElementLike) => { | ||||||
|  |   let uid: string; | ||||||
|  |   let id: string; | ||||||
|  |   let version: number | null; | ||||||
|  |   let parent: string | null = null; | ||||||
|  |   let versionNonce: number | null = null; | ||||||
|  |   if ("uid" in opts) { | ||||||
|  |     const match = opts.uid.match( | ||||||
|  |       /^(?:\((\^|\w+)\))?(\w+)(?::(\d+))?(?:\((\w+)\))?$/, | ||||||
|  |     )!; | ||||||
|  |     parent = match[1]; | ||||||
|  |     id = match[2]; | ||||||
|  |     version = match[3] ? parseInt(match[3]) : null; | ||||||
|  |     uid = version ? `${id}:${version}` : id; | ||||||
|  |   } else { | ||||||
|  |     ({ id, version, versionNonce } = opts); | ||||||
|  |     parent = parent || null; | ||||||
|  |     uid = id; | ||||||
|  |   } | ||||||
|  |   return { | ||||||
|  |     uid, | ||||||
|  |     id, | ||||||
|  |     version, | ||||||
|  |     versionNonce: versionNonce || randomInteger(), | ||||||
|  |     [PRECEDING_ELEMENT_KEY]: parent || null, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const idsToElements = ( | ||||||
|  |   ids: (Id | ElementLike)[], | ||||||
|  |   cache: Cache = {}, | ||||||
|  | ): readonly ExcalidrawElement[] => { | ||||||
|  |   return ids.reduce((acc, _uid, idx) => { | ||||||
|  |     const { | ||||||
|  |       uid, | ||||||
|  |       id, | ||||||
|  |       version, | ||||||
|  |       [PRECEDING_ELEMENT_KEY]: parent, | ||||||
|  |       versionNonce, | ||||||
|  |     } = createElement(typeof _uid === "string" ? { uid: _uid } : _uid); | ||||||
|  |     const cached = cache[uid]; | ||||||
|  |     const elem = { | ||||||
|  |       id, | ||||||
|  |       version: version ?? 0, | ||||||
|  |       versionNonce, | ||||||
|  |       ...cached, | ||||||
|  |       [PRECEDING_ELEMENT_KEY]: parent, | ||||||
|  |     } as BroadcastedExcalidrawElement; | ||||||
|  |     // @ts-ignore | ||||||
|  |     cache[uid] = elem; | ||||||
|  |     acc.push(elem); | ||||||
|  |     return acc; | ||||||
|  |   }, [] as ExcalidrawElement[]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addParents = (elements: BroadcastedExcalidrawElement[]) => { | ||||||
|  |   return elements.map((el, idx, els) => { | ||||||
|  |     el[PRECEDING_ELEMENT_KEY] = els[idx - 1]?.id || "^"; | ||||||
|  |     return el; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const cleanElements = (elements: ReconciledElements) => { | ||||||
|  |   return elements.map((el) => { | ||||||
|  |     // @ts-ignore | ||||||
|  |     delete el[PRECEDING_ELEMENT_KEY]; | ||||||
|  |     // @ts-ignore | ||||||
|  |     delete el.next; | ||||||
|  |     // @ts-ignore | ||||||
|  |     delete el.prev; | ||||||
|  |     return el; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const test = <U extends `${string}:${"L" | "R"}`>( | ||||||
|  |   local: (Id | ElementLike)[], | ||||||
|  |   remote: (Id | ElementLike)[], | ||||||
|  |   target: U[], | ||||||
|  |   bidirectional = true, | ||||||
|  | ) => { | ||||||
|  |   const cache: Cache = {}; | ||||||
|  |   const _local = idsToElements(local, cache); | ||||||
|  |   const _remote = idsToElements(remote, cache); | ||||||
|  |   const _target = target.map((uid) => { | ||||||
|  |     const [, id, source] = uid.match(/^(\w+):([LR])$/)!; | ||||||
|  |     return (source === "L" ? _local : _remote).find((e) => e.id === id)!; | ||||||
|  |   }) as any as ReconciledElements; | ||||||
|  |   const remoteReconciled = reconcileElements(_local, _remote, {} as AppState); | ||||||
|  |   expect(target.length).equal(remoteReconciled.length); | ||||||
|  |   expect(cleanElements(remoteReconciled)).deep.equal( | ||||||
|  |     cleanElements(_target), | ||||||
|  |     "remote reconciliation", | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const __local = cleanElements(cloneJSON(_remote) as ReconciledElements); | ||||||
|  |   const __remote = addParents(cleanElements(cloneJSON(remoteReconciled))); | ||||||
|  |   if (bidirectional) { | ||||||
|  |     try { | ||||||
|  |       expect( | ||||||
|  |         cleanElements( | ||||||
|  |           reconcileElements( | ||||||
|  |             cloneJSON(__local), | ||||||
|  |             cloneJSON(__remote), | ||||||
|  |             {} as AppState, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ).deep.equal(cleanElements(remoteReconciled), "local re-reconciliation"); | ||||||
|  |     } catch (error: any) { | ||||||
|  |       console.error("local original", __local); | ||||||
|  |       console.error("remote reconciled", __remote); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const findIndex = <T>( | ||||||
|  |   array: readonly T[], | ||||||
|  |   cb: (element: T, index: number, array: readonly T[]) => boolean, | ||||||
|  |   fromIndex: number = 0, | ||||||
|  | ) => { | ||||||
|  |   if (fromIndex < 0) { | ||||||
|  |     fromIndex = array.length + fromIndex; | ||||||
|  |   } | ||||||
|  |   fromIndex = Math.min(array.length, Math.max(fromIndex, 0)); | ||||||
|  |   let index = fromIndex - 1; | ||||||
|  |   while (++index < array.length) { | ||||||
|  |     if (cb(array[index], index, array)) { | ||||||
|  |       return index; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return -1; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // ----------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | describe("elements reconciliation", () => { | ||||||
|  |   it("reconcileElements()", () => { | ||||||
|  |     // ------------------------------------------------------------------------- | ||||||
|  |     // | ||||||
|  |     // in following tests, we pass: | ||||||
|  |     //  (1) an array of local elements and their version (:1, :2...) | ||||||
|  |     //  (2) an array of remote elements and their version (:1, :2...) | ||||||
|  |     //  (3) expected reconciled elements | ||||||
|  |     // | ||||||
|  |     // in the reconciled array: | ||||||
|  |     //  :L means local element was resolved | ||||||
|  |     //  :R means remote element was resolved | ||||||
|  |     // | ||||||
|  |     // if a remote element is prefixed with parentheses, the enclosed string: | ||||||
|  |     //  (^) means the element is the first element in the array | ||||||
|  |     //  (<id>) means the element is preceded by <id> element | ||||||
|  |     // | ||||||
|  |     // if versions are missing, it defaults to version 0 | ||||||
|  |     // ------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  |     // non-annotated elements | ||||||
|  |     // ------------------------------------------------------------------------- | ||||||
|  |     // usually when we sync elements they should always be annotated with | ||||||
|  |     // their (preceding elements) parents, but let's test a couple of cases when | ||||||
|  |     // they're not for whatever reason (remote clients are on older version...), | ||||||
|  |     // in which case the first synced element either replaces existing element | ||||||
|  |     // or is pushed at the end of the array | ||||||
|  |  | ||||||
|  |     test(["A:1", "B:1", "C:1"], ["B:2"], ["A:L", "B:R", "C:L"]); | ||||||
|  |     test(["A:1", "B:1", "C"], ["B:2", "A:2"], ["B:R", "A:R", "C:L"]); | ||||||
|  |     test(["A:2", "B:1", "C"], ["B:2", "A:1"], ["A:L", "B:R", "C:L"]); | ||||||
|  |     test(["A:1", "B:1"], ["C:1"], ["A:L", "B:L", "C:R"]); | ||||||
|  |     test(["A", "B"], ["A:1"], ["A:R", "B:L"]); | ||||||
|  |     test(["A"], ["A", "B"], ["A:L", "B:R"]); | ||||||
|  |     test(["A"], ["A:1", "B"], ["A:R", "B:R"]); | ||||||
|  |     test(["A:2"], ["A:1", "B"], ["A:L", "B:R"]); | ||||||
|  |     test(["A:2"], ["B", "A:1"], ["A:L", "B:R"]); | ||||||
|  |     test(["A:1"], ["B", "A:2"], ["B:R", "A:R"]); | ||||||
|  |     test(["A"], ["A:1"], ["A:R"]); | ||||||
|  |  | ||||||
|  |     // C isn't added to the end because it follows B (even if B was resolved | ||||||
|  |     // to local version) | ||||||
|  |     test(["A", "B:1", "D"], ["B", "C:2", "A"], ["B:L", "C:R", "A:R", "D:L"]); | ||||||
|  |  | ||||||
|  |     // some of the following tests are kinda arbitrary and they're less | ||||||
|  |     // likely to happen in real-world cases | ||||||
|  |  | ||||||
|  |     test(["A", "B"], ["B:1", "A:1"], ["B:R", "A:R"]); | ||||||
|  |     test(["A:2", "B:2"], ["B:1", "A:1"], ["A:L", "B:L"]); | ||||||
|  |     test(["A", "B", "C"], ["A", "B:2", "G", "C"], ["A:L", "B:R", "G:R", "C:L"]); | ||||||
|  |     test(["A", "B", "C"], ["A", "B:2", "G"], ["A:L", "B:R", "G:R", "C:L"]); | ||||||
|  |     test(["A", "B", "C"], ["A", "B:2", "G"], ["A:L", "B:R", "G:R", "C:L"]); | ||||||
|  |     test( | ||||||
|  |       ["A:2", "B:2", "C"], | ||||||
|  |       ["D", "B:1", "A:3"], | ||||||
|  |       ["B:L", "A:R", "C:L", "D:R"], | ||||||
|  |     ); | ||||||
|  |     test( | ||||||
|  |       ["A:2", "B:2", "C"], | ||||||
|  |       ["D", "B:2", "A:3", "C"], | ||||||
|  |       ["D:R", "B:L", "A:R", "C:L"], | ||||||
|  |     ); | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D", "E", "F"], | ||||||
|  |       ["A", "B:2", "X", "E:2", "F", "Y"], | ||||||
|  |       ["A:L", "B:R", "X:R", "E:R", "F:L", "Y:R", "C:L", "D:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // annotated elements | ||||||
|  |     // ------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C"], | ||||||
|  |       ["(B)X", "(A)Y", "(Y)Z"], | ||||||
|  |       ["A:L", "B:L", "X:R", "Y:R", "Z:R", "C:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test(["A"], ["(^)X", "Y"], ["X:R", "Y:R", "A:L"]); | ||||||
|  |     test(["A"], ["(^)X", "Y", "Z"], ["X:R", "Y:R", "Z:R", "A:L"]); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B"], | ||||||
|  |       ["(A)C", "(^)D", "F"], | ||||||
|  |       ["A:L", "C:R", "D:R", "F:R", "B:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D"], | ||||||
|  |       ["(B)C:1", "B", "D:1"], | ||||||
|  |       ["A:L", "C:R", "B:L", "D:R"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C"], | ||||||
|  |       ["(^)X", "(A)Y", "(B)Z"], | ||||||
|  |       ["X:R", "A:L", "Y:R", "B:L", "Z:R", "C:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["B", "A", "C"], | ||||||
|  |       ["(^)X", "(A)Y", "(B)Z"], | ||||||
|  |       ["X:R", "B:L", "A:L", "Y:R", "Z:R", "C:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test(["A", "B"], ["(A)X", "(A)Y"], ["A:L", "X:R", "Y:R", "B:L"]); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D", "E"], | ||||||
|  |       ["(A)X", "(C)Y", "(D)Z"], | ||||||
|  |       ["A:L", "X:R", "B:L", "C:L", "Y:R", "D:L", "Z:R", "E:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["X", "Y", "Z"], | ||||||
|  |       ["(^)A", "(A)B", "(B)C", "(C)X", "(X)D", "(D)Y", "(Y)Z"], | ||||||
|  |       ["A:R", "B:R", "C:R", "X:L", "D:R", "Y:L", "Z:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D", "E"], | ||||||
|  |       ["(C)X", "(A)Y", "(D)E:1"], | ||||||
|  |       ["A:L", "B:L", "C:L", "X:R", "Y:R", "D:L", "E:R"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["C:1", "B", "D:1"], | ||||||
|  |       ["A", "B", "C:1", "D:1"], | ||||||
|  |       ["A:R", "B:L", "C:L", "D:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D"], | ||||||
|  |       ["(A)C:1", "(C)B", "(B)D:1"], | ||||||
|  |       ["A:L", "C:R", "B:L", "D:R"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D"], | ||||||
|  |       ["(A)C:1", "(C)B", "(B)D:1"], | ||||||
|  |       ["A:L", "C:R", "B:L", "D:R"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["C:1", "B", "D:1"], | ||||||
|  |       ["(^)A", "(A)B", "(B)C:2", "(C)D:1"], | ||||||
|  |       ["A:R", "B:L", "C:R", "D:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D"], | ||||||
|  |       ["(C)X", "(B)Y", "(A)Z"], | ||||||
|  |       ["A:L", "B:L", "C:L", "X:R", "Y:R", "Z:R", "D:L"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test(["A", "B", "C", "D"], ["(A)B:1", "C:1"], ["A:L", "B:R", "C:R", "D:L"]); | ||||||
|  |     test(["A", "B", "C", "D"], ["(A)C:1", "B:1"], ["A:L", "C:R", "B:R", "D:L"]); | ||||||
|  |     test( | ||||||
|  |       ["A", "B", "C", "D"], | ||||||
|  |       ["(A)C:1", "B", "D:1"], | ||||||
|  |       ["A:L", "C:R", "B:L", "D:R"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     test(["A:1", "B:1", "C"], ["B:2"], ["A:L", "B:R", "C:L"]); | ||||||
|  |     test(["A:1", "B:1", "C"], ["B:2", "C:2"], ["A:L", "B:R", "C:R"]); | ||||||
|  |  | ||||||
|  |     test(["A", "B"], ["(A)C", "(B)D"], ["A:L", "C:R", "B:L", "D:R"]); | ||||||
|  |     test(["A", "B"], ["(X)C", "(X)D"], ["A:L", "B:L", "C:R", "D:R"]); | ||||||
|  |     test(["A", "B"], ["(X)C", "(A)D"], ["A:L", "D:R", "B:L", "C:R"]); | ||||||
|  |     test(["A", "B"], ["(A)B:1"], ["A:L", "B:R"]); | ||||||
|  |     test(["A:2", "B"], ["(A)B:1"], ["A:L", "B:R"]); | ||||||
|  |     test(["A:2", "B:2"], ["B:1"], ["A:L", "B:L"]); | ||||||
|  |     test(["A:2", "B:2"], ["B:1", "C"], ["A:L", "B:L", "C:R"]); | ||||||
|  |     test(["A:2", "B:2"], ["(A)C", "B:1"], ["A:L", "C:R", "B:L"]); | ||||||
|  |     test(["A:2", "B:2"], ["(A)C", "B:1"], ["A:L", "C:R", "B:L"]); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it("test identical elements reconciliation", () => { | ||||||
|  |     const testIdentical = ( | ||||||
|  |       local: ElementLike[], | ||||||
|  |       remote: ElementLike[], | ||||||
|  |       expected: Id[], | ||||||
|  |     ) => { | ||||||
|  |       const ret = reconcileElements( | ||||||
|  |         local as any as ExcalidrawElement[], | ||||||
|  |         remote as any as ExcalidrawElement[], | ||||||
|  |         {} as AppState, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (new Set(ret.map((x) => x.id)).size !== ret.length) { | ||||||
|  |         throw new Error("reconcileElements: duplicate elements found"); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       expect(ret.map((x) => x.id)).to.deep.equal(expected); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // identical id/version/versionNonce | ||||||
|  |     // ------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  |     testIdentical( | ||||||
|  |       [{ id: "A", version: 1, versionNonce: 1 }], | ||||||
|  |       [{ id: "A", version: 1, versionNonce: 1 }], | ||||||
|  |       ["A"], | ||||||
|  |     ); | ||||||
|  |     testIdentical( | ||||||
|  |       [ | ||||||
|  |         { id: "A", version: 1, versionNonce: 1 }, | ||||||
|  |         { id: "B", version: 1, versionNonce: 1 }, | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         { id: "B", version: 1, versionNonce: 1 }, | ||||||
|  |         { id: "A", version: 1, versionNonce: 1 }, | ||||||
|  |       ], | ||||||
|  |       ["B", "A"], | ||||||
|  |     ); | ||||||
|  |     testIdentical( | ||||||
|  |       [ | ||||||
|  |         { id: "A", version: 1, versionNonce: 1 }, | ||||||
|  |         { id: "B", version: 1, versionNonce: 1 }, | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         { id: "B", version: 1, versionNonce: 1 }, | ||||||
|  |         { id: "A", version: 1, versionNonce: 1 }, | ||||||
|  |       ], | ||||||
|  |       ["B", "A"], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // actually identical (arrays and element objects) | ||||||
|  |     // ------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  |     const elements1 = [ | ||||||
|  |       { | ||||||
|  |         id: "A", | ||||||
|  |         version: 1, | ||||||
|  |         versionNonce: 1, | ||||||
|  |         [PRECEDING_ELEMENT_KEY]: null, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: "B", | ||||||
|  |         version: 1, | ||||||
|  |         versionNonce: 1, | ||||||
|  |         [PRECEDING_ELEMENT_KEY]: null, | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     testIdentical(elements1, elements1, ["A", "B"]); | ||||||
|  |     testIdentical(elements1, elements1.slice(), ["A", "B"]); | ||||||
|  |     testIdentical(elements1.slice(), elements1, ["A", "B"]); | ||||||
|  |     testIdentical(elements1.slice(), elements1.slice(), ["A", "B"]); | ||||||
|  |  | ||||||
|  |     const el1 = { | ||||||
|  |       id: "A", | ||||||
|  |       version: 1, | ||||||
|  |       versionNonce: 1, | ||||||
|  |       [PRECEDING_ELEMENT_KEY]: null, | ||||||
|  |     }; | ||||||
|  |     const el2 = { | ||||||
|  |       id: "B", | ||||||
|  |       version: 1, | ||||||
|  |       versionNonce: 1, | ||||||
|  |       [PRECEDING_ELEMENT_KEY]: null, | ||||||
|  |     }; | ||||||
|  |     testIdentical([el1, el2], [el2, el1], ["A", "B"]); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -1,70 +0,0 @@ | |||||||
| import { atom, useAtom } from "jotai"; |  | ||||||
| import { useEffect, useLayoutEffect, useState } from "react"; |  | ||||||
| import { THEME } from "../packages/excalidraw"; |  | ||||||
| import { EVENT } from "../packages/excalidraw/constants"; |  | ||||||
| import { Theme } from "../packages/excalidraw/element/types"; |  | ||||||
| import { CODES, KEYS } from "../packages/excalidraw/keys"; |  | ||||||
| import { STORAGE_KEYS } from "./app_constants"; |  | ||||||
|  |  | ||||||
| export const appThemeAtom = atom<Theme | "system">( |  | ||||||
|   (localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) as |  | ||||||
|     | Theme |  | ||||||
|     | "system" |  | ||||||
|     | null) || THEME.LIGHT, |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| const getDarkThemeMediaQuery = (): MediaQueryList | undefined => |  | ||||||
|   window.matchMedia?.("(prefers-color-scheme: dark)"); |  | ||||||
|  |  | ||||||
| export const useHandleAppTheme = () => { |  | ||||||
|   const [appTheme, setAppTheme] = useAtom(appThemeAtom); |  | ||||||
|   const [editorTheme, setEditorTheme] = useState<Theme>(THEME.LIGHT); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     const mediaQuery = getDarkThemeMediaQuery(); |  | ||||||
|  |  | ||||||
|     const handleChange = (e: MediaQueryListEvent) => { |  | ||||||
|       setEditorTheme(e.matches ? THEME.DARK : THEME.LIGHT); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     if (appTheme === "system") { |  | ||||||
|       mediaQuery?.addEventListener("change", handleChange); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const handleKeydown = (event: KeyboardEvent) => { |  | ||||||
|       if ( |  | ||||||
|         !event[KEYS.CTRL_OR_CMD] && |  | ||||||
|         event.altKey && |  | ||||||
|         event.shiftKey && |  | ||||||
|         event.code === CODES.D |  | ||||||
|       ) { |  | ||||||
|         event.preventDefault(); |  | ||||||
|         event.stopImmediatePropagation(); |  | ||||||
|         setAppTheme(editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     document.addEventListener(EVENT.KEYDOWN, handleKeydown, { capture: true }); |  | ||||||
|  |  | ||||||
|     return () => { |  | ||||||
|       mediaQuery?.removeEventListener("change", handleChange); |  | ||||||
|       document.removeEventListener(EVENT.KEYDOWN, handleKeydown, { |  | ||||||
|         capture: true, |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
|   }, [appTheme, editorTheme, setAppTheme]); |  | ||||||
|  |  | ||||||
|   useLayoutEffect(() => { |  | ||||||
|     localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, appTheme); |  | ||||||
|  |  | ||||||
|     if (appTheme === "system") { |  | ||||||
|       setEditorTheme( |  | ||||||
|         getDarkThemeMediaQuery()?.matches ? THEME.DARK : THEME.LIGHT, |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       setEditorTheme(appTheme); |  | ||||||
|     } |  | ||||||
|   }, [appTheme]); |  | ||||||
|  |  | ||||||
|   return { editorTheme }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										46
									
								
								excalidraw-app/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								excalidraw-app/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,46 +0,0 @@ | |||||||
| /// <reference types="vite-plugin-pwa/vanillajs" /> |  | ||||||
| /// <reference types="vite-plugin-pwa/info" /> |  | ||||||
| /// <reference types="vite-plugin-svgr/client" /> |  | ||||||
| interface ImportMetaEnv { |  | ||||||
|   // The port to run the dev server |  | ||||||
|   VITE_APP_PORT: string; |  | ||||||
|  |  | ||||||
|   VITE_APP_BACKEND_V2_GET_URL: string; |  | ||||||
|   VITE_APP_BACKEND_V2_POST_URL: string; |  | ||||||
|  |  | ||||||
|   // collaboration WebSocket server (https: string |  | ||||||
|   VITE_APP_WS_SERVER_URL: string; |  | ||||||
|  |  | ||||||
|   // set this only if using the collaboration workflow we use on excalidraw.com |  | ||||||
|   VITE_APP_PORTAL_URL: string; |  | ||||||
|   VITE_APP_AI_BACKEND: string; |  | ||||||
|  |  | ||||||
|   VITE_APP_FIREBASE_CONFIG: string; |  | ||||||
|  |  | ||||||
|   // whether to disable live reload / HMR. Usuaully what you want to do when |  | ||||||
|   // debugging Service Workers. |  | ||||||
|   VITE_APP_DEV_DISABLE_LIVE_RELOAD: string; |  | ||||||
|  |  | ||||||
|   VITE_APP_DISABLE_SENTRY: string; |  | ||||||
|  |  | ||||||
|   // Set this flag to false if you want to open the overlay by default |  | ||||||
|   VITE_APP_COLLAPSE_OVERLAY: string; |  | ||||||
|  |  | ||||||
|   // Enable eslint in dev server |  | ||||||
|   VITE_APP_ENABLE_ESLINT: string; |  | ||||||
|  |  | ||||||
|   VITE_APP_PLUS_LP: string; |  | ||||||
|  |  | ||||||
|   VITE_APP_PLUS_APP: string; |  | ||||||
|  |  | ||||||
|   VITE_APP_GIT_SHA: string; |  | ||||||
|  |  | ||||||
|   MODE: string; |  | ||||||
|  |  | ||||||
|   DEV: string; |  | ||||||
|   PROD: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface ImportMeta { |  | ||||||
|   readonly env: ImportMetaEnv; |  | ||||||
| } |  | ||||||
| @@ -64,30 +64,12 @@ | |||||||
|     <!--   to minimize white flash on load when user has dark mode enabled   --> |     <!--   to minimize white flash on load when user has dark mode enabled   --> | ||||||
|     <script> |     <script> | ||||||
|       try { |       try { | ||||||
|         function setTheme(theme) { |         // | ||||||
|  |         const theme = window.localStorage.getItem("excalidraw-theme"); | ||||||
|         if (theme === "dark") { |         if (theme === "dark") { | ||||||
|           document.documentElement.classList.add("dark"); |           document.documentElement.classList.add("dark"); | ||||||
|           } else { |  | ||||||
|             document.documentElement.classList.remove("dark"); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function getTheme() { |  | ||||||
|           const theme = window.localStorage.getItem("excalidraw-theme"); |  | ||||||
| 
 |  | ||||||
|           if (theme && theme === "system") { |  | ||||||
|             return window.matchMedia("(prefers-color-scheme: dark)").matches |  | ||||||
|               ? "dark" |  | ||||||
|               : "light"; |  | ||||||
|           } else { |  | ||||||
|             return theme || "light"; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         setTheme(getTheme()); |  | ||||||
|       } catch (e) { |  | ||||||
|         console.error("Error setting dark mode", e); |  | ||||||
|         } |         } | ||||||
|  |       } catch {} | ||||||
|     </script> |     </script> | ||||||
|     <style> |     <style> | ||||||
|       html.dark { |       html.dark { | ||||||
| @@ -96,7 +78,7 @@ | |||||||
|       } |       } | ||||||
|     </style> |     </style> | ||||||
|     <!-------------------------------------------------------------------------> |     <!-------------------------------------------------------------------------> | ||||||
|     <% if (typeof PROD != 'undefined' && PROD == true) { %> |     <% if ("%PROD%" === "true") { %> | ||||||
|     <script> |     <script> | ||||||
|       // Redirect Excalidraw+ users which have auto-redirect enabled. |       // Redirect Excalidraw+ users which have auto-redirect enabled. | ||||||
|       // |       // | ||||||
| @@ -139,9 +121,8 @@ | |||||||
|       crossorigin="anonymous" |       crossorigin="anonymous" | ||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     <link rel="stylesheet" href="/fonts/fonts.css" type="text/css" /> |     <link rel="stylesheet" href="/fonts.css" type="text/css" /> | ||||||
|     <% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' && |     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %> | ||||||
|     VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %> |  | ||||||
|     <script> |     <script> | ||||||
|       { |       { | ||||||
|         const _WebSocket = window.WebSocket; |         const _WebSocket = window.WebSocket; | ||||||
| @@ -214,7 +195,8 @@ | |||||||
|       <h1 class="visually-hidden">Excalidraw</h1> |       <h1 class="visually-hidden">Excalidraw</h1> | ||||||
|     </header> |     </header> | ||||||
|     <div id="root"></div> |     <div id="root"></div> | ||||||
|     <script type="module" src="index.tsx"></script> |     <script type="module" src="/src/index.tsx"></script> | ||||||
|  |     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %> | ||||||
|     <!-- 100% privacy friendly analytics --> |     <!-- 100% privacy friendly analytics --> | ||||||
|     <script> |     <script> | ||||||
|       // need to load this script dynamically bcs. of iframe embed tracking |       // need to load this script dynamically bcs. of iframe embed tracking | ||||||
| @@ -247,5 +229,6 @@ | |||||||
|       } |       } | ||||||
|     </script> |     </script> | ||||||
|     <!-- end LEGACY GOOGLE ANALYTICS --> |     <!-- end LEGACY GOOGLE ANALYTICS --> | ||||||
|  |     <% } %> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
							
								
								
									
										88
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,24 +1,63 @@ | |||||||
| { | { | ||||||
|   "private": true, |   "browserslist": { | ||||||
|   "name": "excalidraw-monorepo", |     "production": [ | ||||||
|   "workspaces": [ |       ">0.2%", | ||||||
|     "excalidraw-app", |       "not dead", | ||||||
|     "packages/excalidraw", |       "not ie <= 11", | ||||||
|     "packages/utils", |       "not op_mini all", | ||||||
|     "examples/excalidraw", |       "not safari < 12", | ||||||
|     "examples/excalidraw/*" |       "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" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@excalidraw/random-username": "1.0.0", |     "@braintree/sanitize-url": "6.0.2", | ||||||
|  |     "@excalidraw/laser-pointer": "1.2.0", | ||||||
|  |     "@excalidraw/mermaid-to-excalidraw": "0.1.2", | ||||||
|  |     "@excalidraw/random-username": "1.1.0", | ||||||
|  |     "@radix-ui/react-popover": "1.0.3", | ||||||
|  |     "@radix-ui/react-tabs": "1.0.2", | ||||||
|     "@sentry/browser": "6.2.5", |     "@sentry/browser": "6.2.5", | ||||||
|     "@sentry/integrations": "6.2.5", |     "@sentry/integrations": "6.2.5", | ||||||
|  |     "@testing-library/jest-dom": "5.16.2", | ||||||
|  |     "@testing-library/react": "12.1.5", | ||||||
|  |     "@tldraw/vec": "1.7.1", | ||||||
|  |     "browser-fs-access": "0.29.1", | ||||||
|  |     "canvas-roundrect-polyfill": "0.0.1", | ||||||
|  |     "clsx": "1.1.1", | ||||||
|  |     "cross-env": "7.0.3", | ||||||
|  |     "eslint-plugin-react": "7.32.2", | ||||||
|  |     "fake-indexeddb": "3.1.7", | ||||||
|     "firebase": "8.3.3", |     "firebase": "8.3.3", | ||||||
|     "i18next-browser-languagedetector": "6.1.4", |     "i18next-browser-languagedetector": "6.1.4", | ||||||
|     "idb-keyval": "6.0.3", |     "idb-keyval": "6.0.3", | ||||||
|  |     "image-blob-reduce": "3.0.1", | ||||||
|     "jotai": "1.13.1", |     "jotai": "1.13.1", | ||||||
|  |     "lodash.throttle": "4.1.1", | ||||||
|  |     "nanoid": "3.3.3", | ||||||
|  |     "open-color": "1.9.1", | ||||||
|  |     "pako": "1.0.11", | ||||||
|  |     "perfect-freehand": "1.2.0", | ||||||
|  |     "pica": "7.1.1", | ||||||
|  |     "png-chunk-text": "1.0.0", | ||||||
|  |     "png-chunks-encode": "1.0.0", | ||||||
|  |     "png-chunks-extract": "1.0.0", | ||||||
|  |     "points-on-curve": "1.0.1", | ||||||
|  |     "pwacompat": "2.0.17", | ||||||
|     "react": "18.2.0", |     "react": "18.2.0", | ||||||
|     "react-dom": "18.2.0", |     "react-dom": "18.2.0", | ||||||
|     "socket.io-client": "4.7.2" |     "roughjs": "4.6.4", | ||||||
|  |     "sass": "1.51.0", | ||||||
|  |     "socket.io-client": "2.3.1", | ||||||
|  |     "tunnel-rat": "0.1.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@excalidraw/eslint-config": "1.0.3", |     "@excalidraw/eslint-config": "1.0.3", | ||||||
| @@ -26,9 +65,12 @@ | |||||||
|     "@types/chai": "4.3.0", |     "@types/chai": "4.3.0", | ||||||
|     "@types/jest": "27.4.0", |     "@types/jest": "27.4.0", | ||||||
|     "@types/lodash.throttle": "4.1.7", |     "@types/lodash.throttle": "4.1.7", | ||||||
|     "@types/react": "18.2.0", |     "@types/pako": "1.0.3", | ||||||
|     "@types/react-dom": "18.2.0", |     "@types/pica": "5.1.3", | ||||||
|     "@types/socket.io-client": "3.0.0", |     "@types/react": "18.0.15", | ||||||
|  |     "@types/react-dom": "18.0.6", | ||||||
|  |     "@types/resize-observer-browser": "0.1.7", | ||||||
|  |     "@types/socket.io-client": "1.4.36", | ||||||
|     "@vitejs/plugin-react": "3.1.0", |     "@vitejs/plugin-react": "3.1.0", | ||||||
|     "@vitest/coverage-v8": "0.33.0", |     "@vitest/coverage-v8": "0.33.0", | ||||||
|     "@vitest/ui": "0.32.2", |     "@vitest/ui": "0.32.2", | ||||||
| @@ -45,25 +87,27 @@ | |||||||
|     "prettier": "2.6.2", |     "prettier": "2.6.2", | ||||||
|     "rewire": "6.0.0", |     "rewire": "6.0.0", | ||||||
|     "typescript": "4.9.4", |     "typescript": "4.9.4", | ||||||
|     "vite": "5.0.12", |     "vite": "4.4.2", | ||||||
|     "vite-plugin-checker": "0.6.1", |     "vite-plugin-checker": "0.6.1", | ||||||
|     "vite-plugin-ejs": "1.7.0", |     "vite-plugin-ejs": "1.6.4", | ||||||
|     "vite-plugin-pwa": "0.17.4", |     "vite-plugin-pwa": "0.16.4", | ||||||
|     "vite-plugin-svgr": "2.4.0", |     "vite-plugin-svgr": "2.4.0", | ||||||
|     "vitest": "1.5.3", |     "vitest": "0.34.1", | ||||||
|     "vitest-canvas-mock": "0.3.2" |     "vitest-canvas-mock": "0.3.2" | ||||||
|   }, |   }, | ||||||
|   "engines": { |   "engines": { | ||||||
|     "node": "18.0.0 - 22.x.x" |     "node": "18.0.0 - 20.x.x" | ||||||
|   }, |   }, | ||||||
|   "homepage": ".", |   "homepage": ".", | ||||||
|  |   "name": "excalidraw", | ||||||
|   "prettier": "@excalidraw/prettier-config", |   "prettier": "@excalidraw/prettier-config", | ||||||
|  |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "build-node": "node ./scripts/build-node.js", |     "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: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:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build", | ||||||
|     "build:version": "node ./scripts/build-version.js", |     "build:version": "node ./scripts/build-version.js", | ||||||
|     "build": "yarn --cwd ./excalidraw-app build", |     "build": "yarn build:app && yarn build:version", | ||||||
|     "fix:code": "yarn test:code --fix", |     "fix:code": "yarn test:code --fix", | ||||||
|     "fix:other": "yarn prettier --write", |     "fix:other": "yarn prettier --write", | ||||||
|     "fix": "yarn fix:other && yarn fix:code", |     "fix": "yarn fix:other && yarn fix:code", | ||||||
| @@ -71,10 +115,10 @@ | |||||||
|     "locales-coverage:description": "node scripts/locales-coverage-description.js", |     "locales-coverage:description": "node scripts/locales-coverage-description.js", | ||||||
|     "prepare": "husky install", |     "prepare": "husky install", | ||||||
|     "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore", |     "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore", | ||||||
|     "start": "yarn --cwd ./excalidraw-app start", |     "start": "vite", | ||||||
|     "start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o", |     "start:production": "npm run build && npx http-server build -a localhost -p 5001 -o", | ||||||
|     "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false", |     "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false", | ||||||
|     "test:app": "vitest", |     "test:app": "vitest --config vitest.config.ts", | ||||||
|     "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", |     "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", | ||||||
|     "test:other": "yarn prettier --list-different", |     "test:other": "yarn prettier --list-different", | ||||||
|     "test:typecheck": "tsc", |     "test:typecheck": "tsc", | ||||||
|   | |||||||
| @@ -1,118 +0,0 @@ | |||||||
| import { Action, ActionResult } from "./types"; |  | ||||||
| import { UndoIcon, RedoIcon } from "../components/icons"; |  | ||||||
| import { ToolButton } from "../components/ToolButton"; |  | ||||||
| import { t } from "../i18n"; |  | ||||||
| import { History, HistoryChangedEvent } from "../history"; |  | ||||||
| import { AppState } from "../types"; |  | ||||||
| import { KEYS } from "../keys"; |  | ||||||
| import { arrayToMap } from "../utils"; |  | ||||||
| import { isWindows } from "../constants"; |  | ||||||
| import { SceneElementsMap } from "../element/types"; |  | ||||||
| import { Store, StoreAction } from "../store"; |  | ||||||
| import { useEmitter } from "../hooks/useEmitter"; |  | ||||||
|  |  | ||||||
| const writeData = ( |  | ||||||
|   appState: Readonly<AppState>, |  | ||||||
|   updater: () => [SceneElementsMap, AppState] | void, |  | ||||||
| ): ActionResult => { |  | ||||||
|   if ( |  | ||||||
|     !appState.multiElement && |  | ||||||
|     !appState.resizingElement && |  | ||||||
|     !appState.editingElement && |  | ||||||
|     !appState.draggingElement |  | ||||||
|   ) { |  | ||||||
|     const result = updater(); |  | ||||||
|  |  | ||||||
|     if (!result) { |  | ||||||
|       return { storeAction: StoreAction.NONE }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const [nextElementsMap, nextAppState] = result; |  | ||||||
|     const nextElements = Array.from(nextElementsMap.values()); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       appState: nextAppState, |  | ||||||
|       elements: nextElements, |  | ||||||
|       storeAction: StoreAction.UPDATE, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return { storeAction: StoreAction.NONE }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type ActionCreator = (history: History, store: Store) => Action; |  | ||||||
|  |  | ||||||
| export const createUndoAction: ActionCreator = (history, store) => ({ |  | ||||||
|   name: "undo", |  | ||||||
|   label: "buttons.undo", |  | ||||||
|   icon: UndoIcon, |  | ||||||
|   trackEvent: { category: "history" }, |  | ||||||
|   viewMode: false, |  | ||||||
|   perform: (elements, appState) => |  | ||||||
|     writeData(appState, () => |  | ||||||
|       history.undo( |  | ||||||
|         arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap` |  | ||||||
|         appState, |  | ||||||
|         store.snapshot, |  | ||||||
|       ), |  | ||||||
|     ), |  | ||||||
|   keyTest: (event) => |  | ||||||
|     event[KEYS.CTRL_OR_CMD] && |  | ||||||
|     event.key.toLowerCase() === KEYS.Z && |  | ||||||
|     !event.shiftKey, |  | ||||||
|   PanelComponent: ({ updateData, data }) => { |  | ||||||
|     const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>( |  | ||||||
|       history.onHistoryChangedEmitter, |  | ||||||
|       new HistoryChangedEvent(), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|       <ToolButton |  | ||||||
|         type="button" |  | ||||||
|         icon={UndoIcon} |  | ||||||
|         aria-label={t("buttons.undo")} |  | ||||||
|         onClick={updateData} |  | ||||||
|         size={data?.size || "medium"} |  | ||||||
|         disabled={isUndoStackEmpty} |  | ||||||
|       /> |  | ||||||
|     ); |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const createRedoAction: ActionCreator = (history, store) => ({ |  | ||||||
|   name: "redo", |  | ||||||
|   label: "buttons.redo", |  | ||||||
|   icon: RedoIcon, |  | ||||||
|   trackEvent: { category: "history" }, |  | ||||||
|   viewMode: false, |  | ||||||
|   perform: (elements, appState) => |  | ||||||
|     writeData(appState, () => |  | ||||||
|       history.redo( |  | ||||||
|         arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap` |  | ||||||
|         appState, |  | ||||||
|         store.snapshot, |  | ||||||
|       ), |  | ||||||
|     ), |  | ||||||
|   keyTest: (event) => |  | ||||||
|     (event[KEYS.CTRL_OR_CMD] && |  | ||||||
|       event.shiftKey && |  | ||||||
|       event.key.toLowerCase() === KEYS.Z) || |  | ||||||
|     (isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y), |  | ||||||
|   PanelComponent: ({ updateData, data }) => { |  | ||||||
|     const { isRedoStackEmpty } = useEmitter( |  | ||||||
|       history.onHistoryChangedEmitter, |  | ||||||
|       new HistoryChangedEvent(), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|       <ToolButton |  | ||||||
|         type="button" |  | ||||||
|         icon={RedoIcon} |  | ||||||
|         aria-label={t("buttons.redo")} |  | ||||||
|         onClick={updateData} |  | ||||||
|         size={data?.size || "medium"} |  | ||||||
|         disabled={isRedoStackEmpty} |  | ||||||
|       /> |  | ||||||
|     ); |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| @@ -1,55 +0,0 @@ | |||||||
| import { getContextMenuLabel } from "../components/hyperlink/Hyperlink"; |  | ||||||
| import { LinkIcon } from "../components/icons"; |  | ||||||
| import { ToolButton } from "../components/ToolButton"; |  | ||||||
| import { isEmbeddableElement } from "../element/typeChecks"; |  | ||||||
| import { t } from "../i18n"; |  | ||||||
| import { KEYS } from "../keys"; |  | ||||||
| import { getSelectedElements } from "../scene"; |  | ||||||
| import { StoreAction } from "../store"; |  | ||||||
| import { getShortcutKey } from "../utils"; |  | ||||||
| import { register } from "./register"; |  | ||||||
|  |  | ||||||
| export const actionLink = register({ |  | ||||||
|   name: "hyperlink", |  | ||||||
|   label: (elements, appState) => getContextMenuLabel(elements, appState), |  | ||||||
|   icon: LinkIcon, |  | ||||||
|   perform: (elements, appState) => { |  | ||||||
|     if (appState.showHyperlinkPopup === "editor") { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       elements, |  | ||||||
|       appState: { |  | ||||||
|         ...appState, |  | ||||||
|         showHyperlinkPopup: "editor", |  | ||||||
|         openMenu: null, |  | ||||||
|       }, |  | ||||||
|       storeAction: StoreAction.CAPTURE, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   trackEvent: { category: "hyperlink", action: "click" }, |  | ||||||
|   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K, |  | ||||||
|   predicate: (elements, appState) => { |  | ||||||
|     const selectedElements = getSelectedElements(elements, appState); |  | ||||||
|     return selectedElements.length === 1; |  | ||||||
|   }, |  | ||||||
|   PanelComponent: ({ elements, appState, updateData }) => { |  | ||||||
|     const selectedElements = getSelectedElements(elements, appState); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|       <ToolButton |  | ||||||
|         type="button" |  | ||||||
|         icon={LinkIcon} |  | ||||||
|         aria-label={t(getContextMenuLabel(elements, appState))} |  | ||||||
|         title={`${ |  | ||||||
|           isEmbeddableElement(elements[0]) |  | ||||||
|             ? t("labels.link.labelEmbed") |  | ||||||
|             : t("labels.link.label") |  | ||||||
|         } - ${getShortcutKey("CtrlOrCmd+K")}`} |  | ||||||
|         onClick={() => updateData(null)} |  | ||||||
|         selected={selectedElements.length === 1 && !!selectedElements[0].link} |  | ||||||
|       /> |  | ||||||
|     ); |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| @@ -1,133 +0,0 @@ | |||||||
| import { getClientColor } from "../clients"; |  | ||||||
| import { Avatar } from "../components/Avatar"; |  | ||||||
| import { GoToCollaboratorComponentProps } from "../components/UserList"; |  | ||||||
| import { |  | ||||||
|   eyeIcon, |  | ||||||
|   microphoneIcon, |  | ||||||
|   microphoneMutedIcon, |  | ||||||
| } from "../components/icons"; |  | ||||||
| import { t } from "../i18n"; |  | ||||||
| import { StoreAction } from "../store"; |  | ||||||
| import { Collaborator } from "../types"; |  | ||||||
| import { register } from "./register"; |  | ||||||
| import clsx from "clsx"; |  | ||||||
|  |  | ||||||
| export const actionGoToCollaborator = register({ |  | ||||||
|   name: "goToCollaborator", |  | ||||||
|   label: "Go to a collaborator", |  | ||||||
|   viewMode: true, |  | ||||||
|   trackEvent: { category: "collab" }, |  | ||||||
|   perform: (_elements, appState, collaborator: Collaborator) => { |  | ||||||
|     if ( |  | ||||||
|       !collaborator.socketId || |  | ||||||
|       appState.userToFollow?.socketId === collaborator.socketId || |  | ||||||
|       collaborator.isCurrentUser |  | ||||||
|     ) { |  | ||||||
|       return { |  | ||||||
|         appState: { |  | ||||||
|           ...appState, |  | ||||||
|           userToFollow: null, |  | ||||||
|         }, |  | ||||||
|         storeAction: StoreAction.NONE, |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       appState: { |  | ||||||
|         ...appState, |  | ||||||
|         userToFollow: { |  | ||||||
|           socketId: collaborator.socketId, |  | ||||||
|           username: collaborator.username || "", |  | ||||||
|         }, |  | ||||||
|         // Close mobile menu |  | ||||||
|         openMenu: appState.openMenu === "canvas" ? null : appState.openMenu, |  | ||||||
|       }, |  | ||||||
|       storeAction: StoreAction.NONE, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   PanelComponent: ({ updateData, data, appState }) => { |  | ||||||
|     const { socketId, collaborator, withName, isBeingFollowed } = |  | ||||||
|       data as GoToCollaboratorComponentProps; |  | ||||||
|  |  | ||||||
|     const background = getClientColor(socketId, collaborator); |  | ||||||
|  |  | ||||||
|     const statusClassNames = clsx({ |  | ||||||
|       "is-followed": isBeingFollowed, |  | ||||||
|       "is-current-user": collaborator.isCurrentUser === true, |  | ||||||
|       "is-speaking": collaborator.isSpeaking, |  | ||||||
|       "is-in-call": collaborator.isInCall, |  | ||||||
|       "is-muted": collaborator.isMuted, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const statusIconJSX = collaborator.isInCall ? ( |  | ||||||
|       collaborator.isSpeaking ? ( |  | ||||||
|         <div |  | ||||||
|           className="UserList__collaborator-status-icon-speaking-indicator" |  | ||||||
|           title={t("userList.hint.isSpeaking")} |  | ||||||
|         > |  | ||||||
|           <div /> |  | ||||||
|           <div /> |  | ||||||
|           <div /> |  | ||||||
|         </div> |  | ||||||
|       ) : collaborator.isMuted ? ( |  | ||||||
|         <div |  | ||||||
|           className="UserList__collaborator-status-icon-microphone-muted" |  | ||||||
|           title={t("userList.hint.micMuted")} |  | ||||||
|         > |  | ||||||
|           {microphoneMutedIcon} |  | ||||||
|         </div> |  | ||||||
|       ) : ( |  | ||||||
|         <div title={t("userList.hint.inCall")}>{microphoneIcon}</div> |  | ||||||
|       ) |  | ||||||
|     ) : null; |  | ||||||
|  |  | ||||||
|     return withName ? ( |  | ||||||
|       <div |  | ||||||
|         className={`dropdown-menu-item dropdown-menu-item-base UserList__collaborator ${statusClassNames}`} |  | ||||||
|         style={{ [`--avatar-size` as any]: "1.5rem" }} |  | ||||||
|         onClick={() => updateData<Collaborator>(collaborator)} |  | ||||||
|       > |  | ||||||
|         <Avatar |  | ||||||
|           color={background} |  | ||||||
|           onClick={() => {}} |  | ||||||
|           name={collaborator.username || ""} |  | ||||||
|           src={collaborator.avatarUrl} |  | ||||||
|           className={statusClassNames} |  | ||||||
|         /> |  | ||||||
|         <div className="UserList__collaborator-name"> |  | ||||||
|           {collaborator.username} |  | ||||||
|         </div> |  | ||||||
|         <div className="UserList__collaborator-status-icons" aria-hidden> |  | ||||||
|           {isBeingFollowed && ( |  | ||||||
|             <div |  | ||||||
|               className="UserList__collaborator-status-icon-is-followed" |  | ||||||
|               title={t("userList.hint.followStatus")} |  | ||||||
|             > |  | ||||||
|               {eyeIcon} |  | ||||||
|             </div> |  | ||||||
|           )} |  | ||||||
|           {statusIconJSX} |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     ) : ( |  | ||||||
|       <div |  | ||||||
|         className={`UserList__collaborator UserList__collaborator--avatar-only ${statusClassNames}`} |  | ||||||
|       > |  | ||||||
|         <Avatar |  | ||||||
|           color={background} |  | ||||||
|           onClick={() => { |  | ||||||
|             updateData(collaborator); |  | ||||||
|           }} |  | ||||||
|           name={collaborator.username || ""} |  | ||||||
|           src={collaborator.avatarUrl} |  | ||||||
|           className={statusClassNames} |  | ||||||
|         /> |  | ||||||
|         {statusIconJSX && ( |  | ||||||
|           <div className="UserList__collaborator-status-icon"> |  | ||||||
|             {statusIconJSX} |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|       </div> |  | ||||||
|     ); |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user