mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-08-22 17:57:03 +02:00
Compare commits
8 Commits
dependabot
...
zsviczian-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
44390cb146 | ||
![]() |
9da3e47877 | ||
![]() |
9f6edd8eaa | ||
![]() |
0d60253de7 | ||
![]() |
4027a5b245 | ||
![]() |
49f2c88978 | ||
![]() |
156b8b422b | ||
![]() |
e2982a2968 |
@@ -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,7 +17,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/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 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -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,9 +62,9 @@ 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. |
|
||||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||||
|
|
||||||
```jsx live
|
```jsx live
|
||||||
@@ -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. |
|
||||||
@@ -189,7 +189,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={(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"
|
|
||||||
}
|
|
@@ -7614,9 +7614,9 @@ webpack-bundle-analyzer@^4.5.0:
|
|||||||
ws "^7.3.1"
|
ws "^7.3.1"
|
||||||
|
|
||||||
webpack-dev-middleware@^5.3.1:
|
webpack-dev-middleware@^5.3.1:
|
||||||
version "5.3.4"
|
version "5.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517"
|
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f"
|
||||||
integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==
|
integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==
|
||||||
dependencies:
|
dependencies:
|
||||||
colorette "^2.0.10"
|
colorette "^2.0.10"
|
||||||
memfs "^3.4.3"
|
memfs "^3.4.3"
|
||||||
|
@@ -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"
|
|
@@ -1,904 +0,0 @@
|
|||||||
import polyfill from "../packages/excalidraw/polyfill";
|
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { trackEvent } from "../packages/excalidraw/analytics";
|
|
||||||
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
|
||||||
import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
|
|
||||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
|
||||||
import {
|
|
||||||
APP_NAME,
|
|
||||||
EVENT,
|
|
||||||
THEME,
|
|
||||||
TITLE_TIMEOUT,
|
|
||||||
VERSION_TIMEOUT,
|
|
||||||
} from "../packages/excalidraw/constants";
|
|
||||||
import { loadFromBlob } from "../packages/excalidraw/data/blob";
|
|
||||||
import {
|
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
Theme,
|
|
||||||
} from "../packages/excalidraw/element/types";
|
|
||||||
import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
|
|
||||||
import { t } from "../packages/excalidraw/i18n";
|
|
||||||
import {
|
|
||||||
Excalidraw,
|
|
||||||
defaultLang,
|
|
||||||
LiveCollaborationTrigger,
|
|
||||||
TTDDialog,
|
|
||||||
TTDDialogTrigger,
|
|
||||||
} from "../packages/excalidraw/index";
|
|
||||||
import {
|
|
||||||
AppState,
|
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
BinaryFiles,
|
|
||||||
ExcalidrawInitialDataState,
|
|
||||||
UIAppState,
|
|
||||||
} from "../packages/excalidraw/types";
|
|
||||||
import {
|
|
||||||
debounce,
|
|
||||||
getVersion,
|
|
||||||
getFrame,
|
|
||||||
isTestEnv,
|
|
||||||
preventUnload,
|
|
||||||
ResolvablePromise,
|
|
||||||
resolvablePromise,
|
|
||||||
isRunningInIframe,
|
|
||||||
} from "../packages/excalidraw/utils";
|
|
||||||
import {
|
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
|
||||||
STORAGE_KEYS,
|
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
|
||||||
} from "./app_constants";
|
|
||||||
import Collab, {
|
|
||||||
CollabAPI,
|
|
||||||
collabAPIAtom,
|
|
||||||
isCollaboratingAtom,
|
|
||||||
isOfflineAtom,
|
|
||||||
} from "./collab/Collab";
|
|
||||||
import {
|
|
||||||
exportToBackend,
|
|
||||||
getCollaborationLinkData,
|
|
||||||
isCollaborationLink,
|
|
||||||
loadScene,
|
|
||||||
} from "./data";
|
|
||||||
import {
|
|
||||||
importFromLocalStorage,
|
|
||||||
importUsernameFromLocalStorage,
|
|
||||||
} from "./data/localStorage";
|
|
||||||
import CustomStats from "./CustomStats";
|
|
||||||
import {
|
|
||||||
restore,
|
|
||||||
restoreAppState,
|
|
||||||
RestoredDataState,
|
|
||||||
} from "../packages/excalidraw/data/restore";
|
|
||||||
import {
|
|
||||||
ExportToExcalidrawPlus,
|
|
||||||
exportToExcalidrawPlus,
|
|
||||||
} from "./components/ExportToExcalidrawPlus";
|
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
|
||||||
import { newElementWith } from "../packages/excalidraw/element/mutateElement";
|
|
||||||
import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks";
|
|
||||||
import { loadFilesFromFirebase } from "./data/firebase";
|
|
||||||
import {
|
|
||||||
LibraryIndexedDBAdapter,
|
|
||||||
LibraryLocalStorageMigrationAdapter,
|
|
||||||
LocalData,
|
|
||||||
} from "./data/LocalData";
|
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { reconcileElements } from "./collab/reconciliation";
|
|
||||||
import {
|
|
||||||
parseLibraryTokensFromUrl,
|
|
||||||
useHandleLibrary,
|
|
||||||
} from "../packages/excalidraw/data/library";
|
|
||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
|
||||||
import { AppFooter } from "./components/AppFooter";
|
|
||||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
|
||||||
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
|
|
||||||
import { appJotaiStore } from "./app-jotai";
|
|
||||||
|
|
||||||
import "./index.scss";
|
|
||||||
import { ResolutionType } from "../packages/excalidraw/utility-types";
|
|
||||||
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
|
|
||||||
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
|
||||||
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
|
||||||
import Trans from "../packages/excalidraw/components/Trans";
|
|
||||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
|
||||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
|
||||||
|
|
||||||
polyfill();
|
|
||||||
|
|
||||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
|
||||||
|
|
||||||
let isSelfEmbedding = false;
|
|
||||||
|
|
||||||
if (window.self !== window.top) {
|
|
||||||
try {
|
|
||||||
const parentUrl = new URL(document.referrer);
|
|
||||||
const currentUrl = new URL(window.location.href);
|
|
||||||
if (parentUrl.origin === currentUrl.origin) {
|
|
||||||
isSelfEmbedding = true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
|
||||||
languageDetector.init({
|
|
||||||
languageUtils: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const shareableLinkConfirmDialog = {
|
|
||||||
title: t("overwriteConfirm.modal.shareableLink.title"),
|
|
||||||
description: (
|
|
||||||
<Trans
|
|
||||||
i18nKey="overwriteConfirm.modal.shareableLink.description"
|
|
||||||
bold={(text) => <strong>{text}</strong>}
|
|
||||||
br={() => <br />}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
|
|
||||||
color: "danger",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
|
||||||
collabAPI: CollabAPI | null;
|
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
|
||||||
}): Promise<
|
|
||||||
{ scene: ExcalidrawInitialDataState | null } & (
|
|
||||||
| { isExternalScene: true; id: string; key: string }
|
|
||||||
| { isExternalScene: false; id?: null; key?: null }
|
|
||||||
)
|
|
||||||
> => {
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
|
||||||
const id = searchParams.get("id");
|
|
||||||
const jsonBackendMatch = window.location.hash.match(
|
|
||||||
/^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
|
|
||||||
);
|
|
||||||
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
|
||||||
|
|
||||||
const localDataState = importFromLocalStorage();
|
|
||||||
|
|
||||||
let scene: RestoredDataState & {
|
|
||||||
scrollToContent?: boolean;
|
|
||||||
} = await loadScene(null, null, localDataState);
|
|
||||||
|
|
||||||
let roomLinkData = getCollaborationLinkData(window.location.href);
|
|
||||||
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
|
||||||
if (isExternalScene) {
|
|
||||||
if (
|
|
||||||
// don't prompt if scene is empty
|
|
||||||
!scene.elements.length ||
|
|
||||||
// don't prompt for collab scenes because we don't override local storage
|
|
||||||
roomLinkData ||
|
|
||||||
// otherwise, prompt whether user wants to override current scene
|
|
||||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
|
||||||
) {
|
|
||||||
if (jsonBackendMatch) {
|
|
||||||
scene = await loadScene(
|
|
||||||
jsonBackendMatch[1],
|
|
||||||
jsonBackendMatch[2],
|
|
||||||
localDataState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
scene.scrollToContent = true;
|
|
||||||
if (!roomLinkData) {
|
|
||||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// https://github.com/excalidraw/excalidraw/issues/1919
|
|
||||||
if (document.hidden) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
window.addEventListener(
|
|
||||||
"focus",
|
|
||||||
() => initializeScene(opts).then(resolve).catch(reject),
|
|
||||||
{
|
|
||||||
once: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
roomLinkData = null;
|
|
||||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
|
||||||
}
|
|
||||||
} else if (externalUrlMatch) {
|
|
||||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
|
||||||
|
|
||||||
const url = externalUrlMatch[1];
|
|
||||||
try {
|
|
||||||
const request = await fetch(window.decodeURIComponent(url));
|
|
||||||
const data = await loadFromBlob(await request.blob(), null, null);
|
|
||||||
if (
|
|
||||||
!scene.elements.length ||
|
|
||||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
|
||||||
) {
|
|
||||||
return { scene: data, isExternalScene };
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
scene: {
|
|
||||||
appState: {
|
|
||||||
errorMessage: t("alerts.invalidSceneUrl"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isExternalScene,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (roomLinkData && opts.collabAPI) {
|
|
||||||
const { excalidrawAPI } = opts;
|
|
||||||
|
|
||||||
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// when collaborating, the state may have already been updated at this
|
|
||||||
// point (we may have received updates from other clients), so reconcile
|
|
||||||
// elements and appState with existing state
|
|
||||||
scene: {
|
|
||||||
...scene,
|
|
||||||
appState: {
|
|
||||||
...restoreAppState(
|
|
||||||
{
|
|
||||||
...scene?.appState,
|
|
||||||
theme: localDataState?.appState?.theme || scene?.appState?.theme,
|
|
||||||
},
|
|
||||||
excalidrawAPI.getAppState(),
|
|
||||||
),
|
|
||||||
// necessary if we're invoking from a hashchange handler which doesn't
|
|
||||||
// go through App.initializeScene() that resets this flag
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
elements: reconcileElements(
|
|
||||||
scene?.elements || [],
|
|
||||||
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
||||||
excalidrawAPI.getAppState(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
isExternalScene: true,
|
|
||||||
id: roomLinkData.roomId,
|
|
||||||
key: roomLinkData.roomKey,
|
|
||||||
};
|
|
||||||
} else if (scene) {
|
|
||||||
return isExternalScene && jsonBackendMatch
|
|
||||||
? {
|
|
||||||
scene,
|
|
||||||
isExternalScene,
|
|
||||||
id: jsonBackendMatch[1],
|
|
||||||
key: jsonBackendMatch[2],
|
|
||||||
}
|
|
||||||
: { scene, isExternalScene: false };
|
|
||||||
}
|
|
||||||
return { scene: null, isExternalScene: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
|
||||||
export const appLangCodeAtom = atom(
|
|
||||||
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
|
||||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
|
||||||
const isCollabDisabled = isRunningInIframe();
|
|
||||||
|
|
||||||
// initial state
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const initialStatePromiseRef = useRef<{
|
|
||||||
promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
|
|
||||||
}>({ promise: null! });
|
|
||||||
if (!initialStatePromiseRef.current.promise) {
|
|
||||||
initialStatePromiseRef.current.promise =
|
|
||||||
resolvablePromise<ExcalidrawInitialDataState | null>();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
trackEvent("load", "frame", getFrame());
|
|
||||||
// Delayed so that the app has a time to load the latest SW
|
|
||||||
setTimeout(() => {
|
|
||||||
trackEvent("load", "version", getVersion());
|
|
||||||
}, VERSION_TIMEOUT);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [excalidrawAPI, excalidrawRefCallback] =
|
|
||||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
|
||||||
|
|
||||||
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
|
||||||
const [collabAPI] = useAtom(collabAPIAtom);
|
|
||||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
|
||||||
return isCollaborationLink(window.location.href);
|
|
||||||
});
|
|
||||||
const collabError = useAtomValue(collabErrorIndicatorAtom);
|
|
||||||
|
|
||||||
useHandleLibrary({
|
|
||||||
excalidrawAPI,
|
|
||||||
adapter: LibraryIndexedDBAdapter,
|
|
||||||
// TODO maybe remove this in several months (shipped: 24-03-11)
|
|
||||||
migrationAdapter: LibraryLocalStorageMigrationAdapter,
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
LibraryIndexedDBAdapter.load().then((data) => {
|
|
||||||
if (data) {
|
|
||||||
excalidrawAPI.updateLibrary({
|
|
||||||
libraryItems: data.libraryItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
collabAPI?.setUsername(username || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
|
|
||||||
const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
|
|
||||||
const currFiles = excalidrawAPI.getFiles();
|
|
||||||
const fileIds =
|
|
||||||
elements?.reduce((acc, element) => {
|
|
||||||
if (
|
|
||||||
isInitializedImageElement(element) &&
|
|
||||||
// only load and update images that aren't already loaded
|
|
||||||
!currFiles[element.fileId]
|
|
||||||
) {
|
|
||||||
return acc.concat(element.fileId);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as FileId[]) || [];
|
|
||||||
if (fileIds.length) {
|
|
||||||
LocalData.fileStorage
|
|
||||||
.getFiles(fileIds)
|
|
||||||
.then(({ loadedFiles, erroredFiles }) => {
|
|
||||||
if (loadedFiles.length) {
|
|
||||||
excalidrawAPI.addFiles(loadedFiles);
|
|
||||||
}
|
|
||||||
updateStaleImageStatuses({
|
|
||||||
excalidrawAPI,
|
|
||||||
erroredFiles,
|
|
||||||
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, SYNC_BROWSER_TABS_TIMEOUT);
|
|
||||||
|
|
||||||
const onUnload = () => {
|
|
||||||
LocalData.flushSave();
|
|
||||||
};
|
|
||||||
|
|
||||||
const visibilityChange = (event: FocusEvent | Event) => {
|
|
||||||
if (event.type === EVENT.BLUR || document.hidden) {
|
|
||||||
LocalData.flushSave();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.type === EVENT.VISIBILITY_CHANGE ||
|
|
||||||
event.type === EVENT.FOCUS
|
|
||||||
) {
|
|
||||||
syncData();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
||||||
window.addEventListener(EVENT.UNLOAD, onUnload, false);
|
|
||||||
window.addEventListener(EVENT.BLUR, visibilityChange, false);
|
|
||||||
document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
|
|
||||||
window.addEventListener(EVENT.FOCUS, visibilityChange, false);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
||||||
window.removeEventListener(EVENT.UNLOAD, onUnload, false);
|
|
||||||
window.removeEventListener(EVENT.BLUR, visibilityChange, false);
|
|
||||||
window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
|
|
||||||
document.removeEventListener(
|
|
||||||
EVENT.VISIBILITY_CHANGE,
|
|
||||||
visibilityChange,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
clearTimeout(titleTimeout);
|
|
||||||
};
|
|
||||||
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unloadHandler = (event: BeforeUnloadEvent) => {
|
|
||||||
LocalData.flushSave();
|
|
||||||
|
|
||||||
if (
|
|
||||||
excalidrawAPI &&
|
|
||||||
LocalData.fileStorage.shouldPreventUnload(
|
|
||||||
excalidrawAPI.getSceneElements(),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preventUnload(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
|
||||||
};
|
|
||||||
}, [excalidrawAPI]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
languageDetector.cacheUserLanguage(langCode);
|
|
||||||
}, [langCode]);
|
|
||||||
|
|
||||||
const [theme, setTheme] = useState<Theme>(
|
|
||||||
() =>
|
|
||||||
(localStorage.getItem(
|
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_THEME,
|
|
||||||
) as Theme | null) ||
|
|
||||||
// FIXME migration from old LS scheme. Can be removed later. #5660
|
|
||||||
importFromLocalStorage().appState?.theme ||
|
|
||||||
THEME.LIGHT,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
|
|
||||||
// currently only used for body styling during init (see public/index.html),
|
|
||||||
// but may change in the future
|
|
||||||
document.documentElement.classList.toggle("dark", theme === THEME.DARK);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
const onChange = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles,
|
|
||||||
) => {
|
|
||||||
if (collabAPI?.isCollaborating()) {
|
|
||||||
collabAPI.syncElements(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTheme(appState.theme);
|
|
||||||
|
|
||||||
// this check is redundant, but since this is a hot path, it's best
|
|
||||||
// not to evaludate the nested expression every time
|
|
||||||
if (!LocalData.isSavePaused()) {
|
|
||||||
LocalData.save(elements, appState, files, () => {
|
|
||||||
if (excalidrawAPI) {
|
|
||||||
let didChange = false;
|
|
||||||
|
|
||||||
const elements = excalidrawAPI
|
|
||||||
.getSceneElementsIncludingDeleted()
|
|
||||||
.map((element) => {
|
|
||||||
if (
|
|
||||||
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
|
|
||||||
) {
|
|
||||||
const newElement = newElementWith(element, { status: "saved" });
|
|
||||||
if (newElement !== element) {
|
|
||||||
didChange = true;
|
|
||||||
}
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (didChange) {
|
|
||||||
excalidrawAPI.updateScene({
|
|
||||||
elements,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onExportToBackend = async (
|
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
appState: Partial<AppState>,
|
|
||||||
files: BinaryFiles,
|
|
||||||
) => {
|
|
||||||
if (exportedElements.length === 0) {
|
|
||||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { url, errorMessage } = await exportToBackend(
|
|
||||||
exportedElements,
|
|
||||||
{
|
|
||||||
...appState,
|
|
||||||
viewBackgroundColor: appState.exportBackground
|
|
||||||
? appState.viewBackgroundColor
|
|
||||||
: getDefaultAppState().viewBackgroundColor,
|
|
||||||
},
|
|
||||||
files,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (errorMessage) {
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
setLatestShareableLink(url);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name !== "AbortError") {
|
|
||||||
const { width, height } = appState;
|
|
||||||
console.error(error, {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
devicePixelRatio: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCustomStats = (
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
appState: UIAppState,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<CustomStats
|
|
||||||
setToast={(message) => excalidrawAPI!.setToast({ message })}
|
|
||||||
appState={appState}
|
|
||||||
elements={elements}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOffline = useAtomValue(isOfflineAtom);
|
|
||||||
|
|
||||||
const onCollabDialogOpen = useCallback(
|
|
||||||
() => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),
|
|
||||||
[setShareDialogState],
|
|
||||||
);
|
|
||||||
|
|
||||||
// browsers generally prevent infinite self-embedding, there are
|
|
||||||
// cases where it still happens, and while we disallow self-embedding
|
|
||||||
// by not whitelisting our own origin, this serves as an additional guard
|
|
||||||
if (isSelfEmbedding) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
textAlign: "center",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1>I'm not a pretzel!</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ height: "100%" }}
|
|
||||||
className={clsx("excalidraw-app", {
|
|
||||||
"is-collaborating": isCollaborating,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Excalidraw
|
|
||||||
excalidrawAPI={excalidrawRefCallback}
|
|
||||||
onChange={onChange}
|
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
|
||||||
UIOptions={{
|
|
||||||
canvasActions: {
|
|
||||||
toggleTheme: true,
|
|
||||||
export: {
|
|
||||||
onExportToBackend,
|
|
||||||
renderCustomUI: excalidrawAPI
|
|
||||||
? (elements, appState, files) => {
|
|
||||||
return (
|
|
||||||
<ExportToExcalidrawPlus
|
|
||||||
elements={elements}
|
|
||||||
appState={appState}
|
|
||||||
files={files}
|
|
||||||
name={excalidrawAPI.getName()}
|
|
||||||
onError={(error) => {
|
|
||||||
excalidrawAPI?.updateScene({
|
|
||||||
appState: {
|
|
||||||
errorMessage: error.message,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onSuccess={() => {
|
|
||||||
excalidrawAPI.updateScene({
|
|
||||||
appState: { openDialog: null },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
langCode={langCode}
|
|
||||||
renderCustomStats={renderCustomStats}
|
|
||||||
detectScroll={false}
|
|
||||||
handleKeyboardGlobally={true}
|
|
||||||
autoFocus={true}
|
|
||||||
theme={theme}
|
|
||||||
renderTopRightUI={(isMobile) => {
|
|
||||||
if (isMobile || !collabAPI || isCollabDisabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="top-right-ui">
|
|
||||||
{collabError.message && <CollabError collabError={collabError} />}
|
|
||||||
<LiveCollaborationTrigger
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
onSelect={() =>
|
|
||||||
setShareDialogState({ isOpen: true, type: "share" })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppMainMenu
|
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
isCollabEnabled={!isCollabDisabled}
|
|
||||||
/>
|
|
||||||
<AppWelcomeScreen
|
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
|
||||||
isCollabEnabled={!isCollabDisabled}
|
|
||||||
/>
|
|
||||||
<OverwriteConfirmDialog>
|
|
||||||
<OverwriteConfirmDialog.Actions.ExportToImage />
|
|
||||||
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
|
||||||
{excalidrawAPI && (
|
|
||||||
<OverwriteConfirmDialog.Action
|
|
||||||
title={t("overwriteConfirm.action.excalidrawPlus.title")}
|
|
||||||
actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
|
|
||||||
onClick={() => {
|
|
||||||
exportToExcalidrawPlus(
|
|
||||||
excalidrawAPI.getSceneElements(),
|
|
||||||
excalidrawAPI.getAppState(),
|
|
||||||
excalidrawAPI.getFiles(),
|
|
||||||
excalidrawAPI.getName(),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("overwriteConfirm.action.excalidrawPlus.description")}
|
|
||||||
</OverwriteConfirmDialog.Action>
|
|
||||||
)}
|
|
||||||
</OverwriteConfirmDialog>
|
|
||||||
<AppFooter />
|
|
||||||
<TTDDialog
|
|
||||||
onTextSubmit={async (input) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${
|
|
||||||
import.meta.env.VITE_APP_AI_BACKEND
|
|
||||||
}/v1/ai/text-to-diagram/generate`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ prompt: input }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const rateLimit = response.headers.has("X-Ratelimit-Limit")
|
|
||||||
? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const rateLimitRemaining = response.headers.has(
|
|
||||||
"X-Ratelimit-Remaining",
|
|
||||||
)
|
|
||||||
? parseInt(
|
|
||||||
response.headers.get("X-Ratelimit-Remaining") || "0",
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 429) {
|
|
||||||
return {
|
|
||||||
rateLimit,
|
|
||||||
rateLimitRemaining,
|
|
||||||
error: new Error(
|
|
||||||
"Too many requests today, please try again tomorrow!",
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(json.message || "Generation failed...");
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatedResponse = json.generatedResponse;
|
|
||||||
if (!generatedResponse) {
|
|
||||||
throw new Error("Generation failed...");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { generatedResponse, rateLimit, rateLimitRemaining };
|
|
||||||
} catch (err: any) {
|
|
||||||
throw new Error("Request failed");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TTDDialogTrigger />
|
|
||||||
{isCollaborating && isOffline && (
|
|
||||||
<div className="collab-offline-warning">
|
|
||||||
{t("alerts.collabOfflineWarning")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{latestShareableLink && (
|
|
||||||
<ShareableLinkDialog
|
|
||||||
link={latestShareableLink}
|
|
||||||
onCloseRequest={() => setLatestShareableLink(null)}
|
|
||||||
setErrorMessage={setErrorMessage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{excalidrawAPI && !isCollabDisabled && (
|
|
||||||
<Collab excalidrawAPI={excalidrawAPI} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ShareDialog
|
|
||||||
collabAPI={collabAPI}
|
|
||||||
onExportToBackend={async () => {
|
|
||||||
if (excalidrawAPI) {
|
|
||||||
try {
|
|
||||||
await onExportToBackend(
|
|
||||||
excalidrawAPI.getSceneElements(),
|
|
||||||
excalidrawAPI.getAppState(),
|
|
||||||
excalidrawAPI.getFiles(),
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
setErrorMessage(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{errorMessage && (
|
|
||||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
|
||||||
{errorMessage}
|
|
||||||
</ErrorDialog>
|
|
||||||
)}
|
|
||||||
</Excalidraw>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExcalidrawApp = () => {
|
|
||||||
return (
|
|
||||||
<TopErrorBoundary>
|
|
||||||
<Provider unstable_createStore={() => appJotaiStore}>
|
|
||||||
<ExcalidrawWrapper />
|
|
||||||
</Provider>
|
|
||||||
</TopErrorBoundary>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExcalidrawApp;
|
|
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { 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,41 +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,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../src/element/types";
|
||||||
import {
|
import {
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
zoomToFitBounds,
|
} from "../../src/packages/excalidraw/index";
|
||||||
} from "../../packages/excalidraw/index";
|
import { Collaborator, Gesture } from "../../src/types";
|
||||||
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,
|
||||||
@@ -52,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 {
|
import {
|
||||||
ReconciledElements,
|
ReconciledElements,
|
||||||
reconcileElements as _reconcileElements,
|
reconcileElements as _reconcileElements,
|
||||||
} from "./reconciliation";
|
} from "./reconciliation";
|
||||||
import { decryptData } from "../../packages/excalidraw/data/encryption";
|
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";
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -107,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({
|
||||||
@@ -167,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 = {
|
||||||
@@ -199,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);
|
||||||
@@ -239,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)!;
|
||||||
@@ -280,35 +244,18 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
this.excalidrawAPI.getAppState(),
|
this.excalidrawAPI.getAppState(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resetErrorIndicator();
|
|
||||||
|
|
||||||
if (this.isCollaborating() && savedData && savedData.reconciledElements) {
|
if (this.isCollaborating() && savedData && savedData.reconciledElements) {
|
||||||
this.handleRemoteSceneUpdate(
|
this.handleRemoteSceneUpdate(
|
||||||
this.reconcileElements(savedData.reconciledElements),
|
this.reconcileElements(savedData.reconciledElements),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -317,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(
|
||||||
@@ -367,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,
|
||||||
@@ -408,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);
|
||||||
|
|
||||||
@@ -420,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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -433,7 +381,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,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,
|
||||||
@@ -486,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,9 +484,9 @@ 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;
|
||||||
@@ -550,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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -635,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;
|
||||||
};
|
};
|
||||||
@@ -812,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;
|
||||||
};
|
};
|
||||||
@@ -870,19 +760,6 @@ 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);
|
||||||
};
|
};
|
||||||
@@ -892,7 +769,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
@@ -905,7 +782,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
|
|
||||||
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,
|
||||||
);
|
);
|
||||||
@@ -931,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>
|
||||||
)}
|
)}
|
||||||
@@ -992,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(0eg);
|
|
||||||
}
|
|
||||||
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;
|
|
@@ -6,24 +6,23 @@ import {
|
|||||||
|
|
||||||
import { TCollabClass } from "./Collab";
|
import { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import { ExcalidrawElement } 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 { BroadcastedExcalidrawElement } from "./reconciliation";
|
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||||
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
import { encryptData } from "../../src/data/encryption";
|
||||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||||
import type { Socket } from "socket.io-client";
|
|
||||||
|
|
||||||
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,
|
||||||
);
|
);
|
||||||
@@ -132,11 +130,11 @@ class Portal {
|
|||||||
}, 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,
|
||||||
allElements: readonly ExcalidrawElement[],
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,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,
|
||||||
},
|
},
|
||||||
@@ -205,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:
|
||||||
@@ -215,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();
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||||
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 { arrayToMapWithIndex } from "../../packages/excalidraw/utils";
|
import { arrayToMapWithIndex } from "../../src/utils";
|
||||||
|
|
||||||
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
||||||
_brand: "reconciledElements";
|
_brand: "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,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
|
import { PlusPromoIcon } from "../../src/components/icons";
|
||||||
import { MainMenu } from "../../packages/excalidraw/index";
|
import { MainMenu } from "../../src/packages/excalidraw/index";
|
||||||
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;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
@@ -17,7 +17,7 @@ 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)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } 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 && (
|
||||||
|
@@ -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,19 +1,19 @@
|
|||||||
import { compressData } from "../../packages/excalidraw/data/encode";
|
import { compressData } from "../../src/data/encode";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../src/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../src/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 */
|
||||||
|
@@ -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,27 +1,20 @@
|
|||||||
import {
|
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||||
ExcalidrawElement,
|
import { getSceneVersion } from "../../src/element";
|
||||||
FileId,
|
|
||||||
} 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,
|
|
||||||
} from "../../packages/excalidraw/data/encryption";
|
|
||||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
|
||||||
import { reconcileElements } from "../collab/reconciliation";
|
import { reconcileElements } from "../collab/reconciliation";
|
||||||
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";
|
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -139,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));
|
||||||
@@ -286,7 +279,7 @@ export const saveToFirebase = async (
|
|||||||
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 ExcalidrawElement[] | null> => {
|
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||||
const firebase = await loadFirestore();
|
const firebase = await loadFirestore();
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
@@ -1,36 +1,27 @@
|
|||||||
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,
|
|
||||||
} 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 { bytesToHexString } from "../../packages/excalidraw/utils";
|
import { bytesToHexString } from "../../src/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";
|
||||||
@@ -65,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;
|
||||||
};
|
};
|
||||||
@@ -115,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] & {
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,40 +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": {},
|
|
||||||
"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,290 +0,0 @@
|
|||||||
import { useRef, useState } from "react";
|
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard";
|
|
||||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
|
||||||
import { getFrame } from "../../packages/excalidraw/utils";
|
|
||||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
|
||||||
import { KEYS } from "../../packages/excalidraw/keys";
|
|
||||||
import { Dialog } from "../../packages/excalidraw/components/Dialog";
|
|
||||||
import {
|
|
||||||
copyIcon,
|
|
||||||
LinkIcon,
|
|
||||||
playerPlayIcon,
|
|
||||||
playerStopFilledIcon,
|
|
||||||
share,
|
|
||||||
shareIOS,
|
|
||||||
shareWindows,
|
|
||||||
tablerCheckIcon,
|
|
||||||
} from "../../packages/excalidraw/components/icons";
|
|
||||||
import { TextField } from "../../packages/excalidraw/components/TextField";
|
|
||||||
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
|
||||||
import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
|
|
||||||
import { atom, useAtom, useAtomValue } from "jotai";
|
|
||||||
|
|
||||||
import "./ShareDialog.scss";
|
|
||||||
|
|
||||||
type OnExportToBackend = () => void;
|
|
||||||
type ShareDialogType = "share" | "collaborationOnly";
|
|
||||||
|
|
||||||
export const shareDialogStateAtom = atom<
|
|
||||||
{ isOpen: false } | { isOpen: true; type: ShareDialogType }
|
|
||||||
>({ isOpen: false });
|
|
||||||
|
|
||||||
const getShareIcon = () => {
|
|
||||||
const navigator = window.navigator as any;
|
|
||||||
const isAppleBrowser = /Apple/.test(navigator.vendor);
|
|
||||||
const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
|
|
||||||
|
|
||||||
if (isAppleBrowser) {
|
|
||||||
return shareIOS;
|
|
||||||
} else if (isWindowsBrowser) {
|
|
||||||
return shareWindows;
|
|
||||||
}
|
|
||||||
|
|
||||||
return share;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShareDialogProps = {
|
|
||||||
collabAPI: CollabAPI | null;
|
|
||||||
handleClose: () => void;
|
|
||||||
onExportToBackend: OnExportToBackend;
|
|
||||||
type: ShareDialogType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ActiveRoomDialog = ({
|
|
||||||
collabAPI,
|
|
||||||
activeRoomLink,
|
|
||||||
handleClose,
|
|
||||||
}: {
|
|
||||||
collabAPI: CollabAPI;
|
|
||||||
activeRoomLink: string;
|
|
||||||
handleClose: () => void;
|
|
||||||
}) => {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const [justCopied, setJustCopied] = useState(false);
|
|
||||||
const timerRef = useRef<number>(0);
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
|
||||||
const isShareSupported = "share" in navigator;
|
|
||||||
|
|
||||||
const copyRoomLink = async () => {
|
|
||||||
try {
|
|
||||||
await copyTextToSystemClipboard(activeRoomLink);
|
|
||||||
} 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);
|
|
||||||
|
|
||||||
if (!shareDialogState.isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShareDialogInner
|
|
||||||
handleClose={() => setShareDialogState({ isOpen: false })}
|
|
||||||
collabAPI={props.collabAPI}
|
|
||||||
onExportToBackend={props.onExportToBackend}
|
|
||||||
type={shareDialogState.type}
|
|
||||||
></ShareDialogInner>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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;
|
||||||
|
@@ -1,12 +1,8 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import {
|
import { render, updateSceneData, waitFor } from "../../src/tests/test-utils";
|
||||||
render,
|
import ExcalidrawApp from "../../excalidraw-app";
|
||||||
updateSceneData,
|
import { API } from "../../src/tests/helpers/api";
|
||||||
waitFor,
|
import { createUndoAction } from "../../src/actions/actionHistory";
|
||||||
} from "../../packages/excalidraw/tests/test-utils";
|
|
||||||
import ExcalidrawApp from "../App";
|
|
||||||
import { API } from "../../packages/excalidraw/tests/helpers/api";
|
|
||||||
import { createUndoAction } from "../../packages/excalidraw/actions/actionHistory";
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
Object.defineProperty(window, "crypto", {
|
Object.defineProperty(window, "crypto", {
|
||||||
@@ -20,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 = () => {};
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
import { ExcalidrawElement } from "../../src/element/types";
|
||||||
import {
|
import {
|
||||||
BroadcastedExcalidrawElement,
|
BroadcastedExcalidrawElement,
|
||||||
ReconciledElements,
|
ReconciledElements,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
} from "../../excalidraw-app/collab/reconciliation";
|
} from "../../excalidraw-app/collab/reconciliation";
|
||||||
import { randomInteger } from "../../packages/excalidraw/random";
|
import { randomInteger } from "../../src/random";
|
||||||
import { AppState } from "../../packages/excalidraw/types";
|
import { AppState } from "../../src/types";
|
||||||
import { cloneJSON } from "../../packages/excalidraw/utils";
|
import { cloneJSON } from "../../src/utils";
|
||||||
|
|
||||||
type Id = string;
|
type Id = string;
|
||||||
type ElementLike = {
|
type ElementLike = {
|
||||||
|
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;
|
|
||||||
}
|
|
@@ -121,7 +121,7 @@
|
|||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" type="text/css" />
|
<link rel="stylesheet" href="/fonts.css" type="text/css" />
|
||||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
||||||
<script>
|
<script>
|
||||||
{
|
{
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<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') { %>
|
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
|
||||||
<!-- 100% privacy friendly analytics -->
|
<!-- 100% privacy friendly analytics -->
|
||||||
<script>
|
<script>
|
82
package.json
82
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/pako": "1.0.3",
|
||||||
|
"@types/pica": "5.1.3",
|
||||||
"@types/react": "18.0.15",
|
"@types/react": "18.0.15",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/socket.io-client": "3.0.0",
|
"@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.0.1",
|
"vitest": "0.34.1",
|
||||||
"vitest-canvas-mock": "0.3.2"
|
"vitest-canvas-mock": "0.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.0.0 - 20.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,54 +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 { getShortcutKey } from "../utils";
|
|
||||||
import { register } from "./register";
|
|
||||||
|
|
||||||
export const actionLink = register({
|
|
||||||
name: "hyperlink",
|
|
||||||
perform: (elements, appState) => {
|
|
||||||
if (appState.showHyperlinkPopup === "editor") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
showHyperlinkPopup: "editor",
|
|
||||||
openMenu: null,
|
|
||||||
},
|
|
||||||
commitToHistory: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
trackEvent: { category: "hyperlink", action: "click" },
|
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
|
||||||
contextItemLabel: (elements, appState) =>
|
|
||||||
getContextMenuLabel(elements, appState),
|
|
||||||
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,131 +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 { Collaborator } from "../types";
|
|
||||||
import { register } from "./register";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
export const actionGoToCollaborator = register({
|
|
||||||
name: "goToCollaborator",
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
userToFollow: {
|
|
||||||
socketId: collaborator.socketId,
|
|
||||||
username: collaborator.username || "",
|
|
||||||
},
|
|
||||||
// Close mobile menu
|
|
||||||
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
|
||||||
},
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,148 +0,0 @@
|
|||||||
import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
|
|
||||||
import { AnimationFrameHandler } from "./animation-frame-handler";
|
|
||||||
import { AppState } from "./types";
|
|
||||||
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
|
|
||||||
import type App from "./components/App";
|
|
||||||
import { SVG_NS } from "./constants";
|
|
||||||
|
|
||||||
export interface Trail {
|
|
||||||
start(container: SVGSVGElement): void;
|
|
||||||
stop(): void;
|
|
||||||
|
|
||||||
startPath(x: number, y: number): void;
|
|
||||||
addPointToPath(x: number, y: number): void;
|
|
||||||
endPath(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AnimatedTrailOptions {
|
|
||||||
fill: (trail: AnimatedTrail) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AnimatedTrail implements Trail {
|
|
||||||
private currentTrail?: LaserPointer;
|
|
||||||
private pastTrails: LaserPointer[] = [];
|
|
||||||
|
|
||||||
private container?: SVGSVGElement;
|
|
||||||
private trailElement: SVGPathElement;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private animationFrameHandler: AnimationFrameHandler,
|
|
||||||
private app: App,
|
|
||||||
private options: Partial<LaserPointerOptions> &
|
|
||||||
Partial<AnimatedTrailOptions>,
|
|
||||||
) {
|
|
||||||
this.animationFrameHandler.register(this, this.onFrame.bind(this));
|
|
||||||
|
|
||||||
this.trailElement = document.createElementNS(SVG_NS, "path");
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasCurrentTrail() {
|
|
||||||
return !!this.currentTrail;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasLastPoint(x: number, y: number) {
|
|
||||||
if (this.currentTrail) {
|
|
||||||
const len = this.currentTrail.originalPoints.length;
|
|
||||||
return (
|
|
||||||
this.currentTrail.originalPoints[len - 1][0] === x &&
|
|
||||||
this.currentTrail.originalPoints[len - 1][1] === y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
start(container?: SVGSVGElement) {
|
|
||||||
if (container) {
|
|
||||||
this.container = container;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.trailElement.parentNode !== this.container && this.container) {
|
|
||||||
this.container.appendChild(this.trailElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.animationFrameHandler.start(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.animationFrameHandler.stop(this);
|
|
||||||
|
|
||||||
if (this.trailElement.parentNode === this.container) {
|
|
||||||
this.container?.removeChild(this.trailElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startPath(x: number, y: number) {
|
|
||||||
this.currentTrail = new LaserPointer(this.options);
|
|
||||||
|
|
||||||
this.currentTrail.addPoint([x, y, performance.now()]);
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
addPointToPath(x: number, y: number) {
|
|
||||||
if (this.currentTrail) {
|
|
||||||
this.currentTrail.addPoint([x, y, performance.now()]);
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endPath() {
|
|
||||||
if (this.currentTrail) {
|
|
||||||
this.currentTrail.close();
|
|
||||||
this.currentTrail.options.keepHead = false;
|
|
||||||
this.pastTrails.push(this.currentTrail);
|
|
||||||
this.currentTrail = undefined;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onFrame() {
|
|
||||||
const paths: string[] = [];
|
|
||||||
|
|
||||||
for (const trail of this.pastTrails) {
|
|
||||||
paths.push(this.drawTrail(trail, this.app.state));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentTrail) {
|
|
||||||
const currentPath = this.drawTrail(this.currentTrail, this.app.state);
|
|
||||||
|
|
||||||
paths.push(currentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pastTrails = this.pastTrails.filter((trail) => {
|
|
||||||
return trail.getStrokeOutline().length !== 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (paths.length === 0) {
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgPaths = paths.join(" ").trim();
|
|
||||||
|
|
||||||
this.trailElement.setAttribute("d", svgPaths);
|
|
||||||
this.trailElement.setAttribute(
|
|
||||||
"fill",
|
|
||||||
(this.options.fill ?? (() => "black"))(this),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawTrail(trail: LaserPointer, state: AppState): string {
|
|
||||||
const stroke = trail
|
|
||||||
.getStrokeOutline(trail.options.size / state.zoom.value)
|
|
||||||
.map(([x, y]) => {
|
|
||||||
const result = sceneCoordsToViewportCoords(
|
|
||||||
{ sceneX: x, sceneY: y },
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [result.x, result.y];
|
|
||||||
});
|
|
||||||
|
|
||||||
return getSvgPathFromStroke(stroke, true);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,79 +0,0 @@
|
|||||||
export type AnimationCallback = (timestamp: number) => void | boolean;
|
|
||||||
|
|
||||||
export type AnimationTarget = {
|
|
||||||
callback: AnimationCallback;
|
|
||||||
stopped: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AnimationFrameHandler {
|
|
||||||
private targets = new WeakMap<object, AnimationTarget>();
|
|
||||||
private rafIds = new WeakMap<object, number>();
|
|
||||||
|
|
||||||
register(key: object, callback: AnimationCallback) {
|
|
||||||
this.targets.set(key, { callback, stopped: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
start(key: object) {
|
|
||||||
const target = this.targets.get(key);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rafIds.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.targets.set(key, { ...target, stopped: false });
|
|
||||||
this.scheduleFrame(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop(key: object) {
|
|
||||||
const target = this.targets.get(key);
|
|
||||||
if (target && !target.stopped) {
|
|
||||||
this.targets.set(key, { ...target, stopped: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cancelFrame(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructFrame(key: object): FrameRequestCallback {
|
|
||||||
return (timestamp: number) => {
|
|
||||||
const target = this.targets.get(key);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldAbort = this.onFrame(target, timestamp);
|
|
||||||
|
|
||||||
if (!target.stopped && !shouldAbort) {
|
|
||||||
this.scheduleFrame(key);
|
|
||||||
} else {
|
|
||||||
this.cancelFrame(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleFrame(key: object) {
|
|
||||||
const rafId = requestAnimationFrame(this.constructFrame(key));
|
|
||||||
|
|
||||||
this.rafIds.set(key, rafId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private cancelFrame(key: object) {
|
|
||||||
if (this.rafIds.has(key)) {
|
|
||||||
const rafId = this.rafIds.get(key)!;
|
|
||||||
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rafIds.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onFrame(target: AnimationTarget, timestamp: number): boolean {
|
|
||||||
const shouldAbort = target.callback(timestamp);
|
|
||||||
|
|
||||||
return shouldAbort ?? false;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user