Compare commits
281 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 913921631b | |||
| ec39094933 | |||
| ef20c8b9fa | |||
| 1c55160e3f | |||
| e49db1dd3c | |||
| cd8bfb06b5 | |||
| 7762b925ae | |||
| 113dfc0023 | |||
| 96b5cfc35d | |||
| 9da26fb7e0 | |||
| 392b362118 | |||
| 0e02366dee | |||
| b2a6a87b10 | |||
| ab8b3537b3 | |||
| d21e0008dd | |||
| 840f1428c4 | |||
| 2db5bbcb29 | |||
| 0927431d0d | |||
| 98c0a67333 | |||
| 57cf577376 | |||
| 6e0ee89ee4 | |||
| 35f778a734 | |||
| ee091d0dbd | |||
| ef9ea14a75 | |||
| df168a6883 | |||
| 798f5f4dfb | |||
| d9ad7c039b | |||
| 7c0239e693 | |||
| da33481fa3 | |||
| 70e0e8dc29 | |||
| 2734e646ca | |||
| dfaaff4432 | |||
| 03028eaa8c | |||
| 79b181bcdc | |||
| f9815b8b4f | |||
| 96ed8a4331 | |||
| 33b01d4e80 | |||
| 7d52176fea | |||
| 958e03fcc6 | |||
| 4cedf3d966 | |||
| e957c8e9ee | |||
| eb09b48ae6 | |||
| 61623bbeba | |||
| 15ca182333 | |||
| b479f3bd65 | |||
| 21815fb930 | |||
| 47ee8a0094 | |||
| a977dd1bf5 | |||
| 3fe1883f3f | |||
| a80cb5896a | |||
| 6dfa18414a | |||
| 8ca4cf3260 | |||
| f3f0ab7c83 | |||
| 44a1c8d857 | |||
| e0a22edfbd | |||
| c07f5a0c80 | |||
| 508f16dc04 | |||
| c1b310c56b | |||
| d4900e8f19 | |||
| caf2db934c | |||
| 60e3801691 | |||
| 80f3b75d42 | |||
| dc812bee19 | |||
| 01e83cc9a5 | |||
| 813f9b702e | |||
| fd39712ba6 | |||
| b46ca0192b | |||
| 72b7c937b1 | |||
| d107215564 | |||
| 6959a363f0 | |||
| 5a11c70714 | |||
| 6ff56c36e3 | |||
| 51ea184938 | |||
| 21fff26d31 | |||
| f4dd23fc31 | |||
| e3d1dee9d0 | |||
| e3f31df747 | |||
| 60e75406e0 | |||
| b396e07b90 | |||
| 2d1d84a47b | |||
| ee30225062 | |||
| 16cae4fc07 | |||
| 576bc0dbe5 | |||
| 00af35c692 | |||
| ea7c702cfc | |||
| 26d2296578 | |||
| afb68a6467 | |||
| b459e5cfd2 | |||
| 5facc0d6da | |||
| 824ad603e1 | |||
| 5e1ff7cafe | |||
| b5d7f5b4ba | |||
| fb4bb29aa5 | |||
| 3cfcc7b489 | |||
| 4320a3cf41 | |||
| 8420e1aa13 | |||
| 5daf1a1b4e | |||
| 97981804d7 | |||
| f7b3befd0a | |||
| 7b2bee9746 | |||
| 88014ace4a | |||
| 87a9430809 | |||
| 99b91c46f7 | |||
| 1ea5b26f25 | |||
| d5f4ee7b3f | |||
| 261304c1a4 | |||
| 84398a7e5c | |||
| 54491d13d4 | |||
| dd1370381d | |||
| 72d6ee48fc | |||
| 232242d2e9 | |||
| f19ce30dfe | |||
| 3cf14c73a3 | |||
| 8d530cf102 | |||
| b87925d253 | |||
| a6684b09f2 | |||
| b427617f66 | |||
| 2907fbe34b | |||
| c67815f7b0 | |||
| c641860cb1 | |||
| 84d89b9a8a | |||
| e63dd025c9 | |||
| 15e019706d | |||
| a133a70e87 | |||
| 80ea7ca23f | |||
| e844580b14 | |||
| 5a0771ad9c | |||
| adcdbe2907 | |||
| 230d0edc44 | |||
| d0a380758e | |||
| 7b36de0476 | |||
| 2427e622b0 | |||
| 62228e0bbb | |||
| 4c5408263c | |||
| bd7b778f41 | |||
| 43b2476dfe | |||
| df8875a497 | |||
| 6fbc44fd1f | |||
| d25a7d365b | |||
| e52c2cd0b6 | |||
| 96eeec5119 | |||
| f5221d521b | |||
| db2c235cd4 | |||
| 148b895f46 | |||
| d9258a736b | |||
| 2e1f08c796 | |||
| 1d5b41dabb | |||
| 66a2f24296 | |||
| 04668d8263 | |||
| abbeed3d5f | |||
| ba8c09d529 | |||
| 744b3e5d09 | |||
| 6ba9bd60e8 | |||
| a1ffa064df | |||
| 4dc4590f24 | |||
| d2f67e619f | |||
| 22b39277f5 | |||
| 63dee03ef0 | |||
| 08b13f971d | |||
| 69f4cc70cb | |||
| 860308eb27 | |||
| 4eb9463f26 | |||
| 6ed6131169 | |||
| 1ed98f9c93 | |||
| a71bb63d1f | |||
| 661d6a4a75 | |||
| defd34923a | |||
| c540bd68aa | |||
| eddbe55f50 | |||
| 2f9526da24 | |||
| 1b6e3fe05b | |||
| afe52c89a7 | |||
| be4e127f6c | |||
| ff0b4394b1 | |||
| 7d8b7fc14d | |||
| 971b4d4ae6 | |||
| cc4c51996c | |||
| 79257a1923 | |||
| dc66261c19 | |||
| 273ba803d9 | |||
| 301e83805d | |||
| ed5ce8d3de | |||
| 1ed53b153c | |||
| c1926f33bb | |||
| 6539029d2a | |||
| d1f37cc64f | |||
| f0d25e34c3 | |||
| d9bbf1eda6 | |||
| f79fb9aae2 | |||
| 275f6fbe24 | |||
| 88812e0386 | |||
| 6e5aeb112d | |||
| 4d83d1c91e | |||
| a04676d423 | |||
| c851aaaf7b | |||
| 1bd2b1fe55 | |||
| 015b46ab23 | |||
| 530617be90 | |||
| 5211b003b8 | |||
| bbcca06b94 | |||
| f92f04c13c | |||
| 890ed9f31f | |||
| da2e507298 | |||
| f59b4f6af4 | |||
| afcde542f9 | |||
| 4689a6b300 | |||
| 0ae9b383d6 | |||
| f597bd3e01 | |||
| 4987cc53d0 | |||
| d917db438e | |||
| a33a400f01 | |||
| 8a162a4cb4 | |||
| c6a045d092 | |||
| cd50aa719f | |||
| 92bc08207c | |||
| 32df5502ae | |||
| bbdcd30a73 | |||
| 3e334a67ed | |||
| 1d71f84515 | |||
| 550a388b2b | |||
| 6b523563d8 | |||
| 65bc500598 | |||
| 7949aa1f1c | |||
| 15bfa626b4 | |||
| 068895db0e | |||
| b7babe554b | |||
| 6a385d6663 | |||
| 2382fad4f6 | |||
| 480572f893 | |||
| 68b1fdb20e | |||
| a38e82f999 | |||
| a07f6e9e3a | |||
| 7e471b55eb | |||
| 160440b860 | |||
| f207bd0a1c | |||
| 99601baffc | |||
| af1a3d5b76 | |||
| 36e56267c9 | |||
| b09b5cb5f4 | |||
| dd8529743a | |||
| f639d44a95 | |||
| f5ab3e4e12 | |||
| 361a9449bb | |||
| 2e719ff671 | |||
| 79d9dc2f8f | |||
| 9013c84524 | |||
| 47f87f4ecb | |||
| 73bf50e8a8 | |||
| 48c3465b19 | |||
| adc4c9f484 | |||
| def1df2c68 | |||
| 0513b647ec | |||
| a289c42830 | |||
| d67eaa8710 | |||
| 0c3dffb082 | |||
| 0e0f34edd8 | |||
| 4888d9d355 | |||
| 1c39bd5781 | |||
| 90ad885446 | |||
| 1741c234a6 | |||
| 63b50b3586 | |||
| e0fefa8025 | |||
| d426cc968d | |||
| 2409c091ff | |||
| 626fe252ab | |||
| 10bd08ef19 | |||
| 2789d08154 | |||
| 678bb2b819 | |||
| 966f9aead9 | |||
| 4f0a2a9593 | |||
| f3f8217125 | |||
| 89bd6181f2 | |||
| c6fdac131b | |||
| 0415c616b1 | |||
| 740a165452 | |||
| 4997624a3a | |||
| b66daae1f3 | |||
| 1e7df58b5b | |||
| 46da032626 | |||
| 3b0593baa7 | |||
| dd530737a2 |
@@ -4,8 +4,16 @@
|
|||||||
!.eslintrc.json
|
!.eslintrc.json
|
||||||
!.npmrc
|
!.npmrc
|
||||||
!.prettierrc
|
!.prettierrc
|
||||||
|
!excalidraw-app/
|
||||||
!package.json
|
!package.json
|
||||||
!public/
|
!public/
|
||||||
!packages/
|
!packages/
|
||||||
|
!scripts/
|
||||||
!tsconfig.json
|
!tsconfig.json
|
||||||
!yarn.lock
|
!yarn.lock
|
||||||
|
|
||||||
|
# keep (sub)sub directories at the end to exclude from explicit included
|
||||||
|
# e.g. ./packages/excalidraw/{dist,node_modules}
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
**/node_modules
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ 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=http://localhost:3000
|
||||||
|
|
||||||
VITE_APP_AI_BACKEND=http://localhost:3015
|
VITE_APP_AI_BACKEND=http://localhost:3015
|
||||||
|
|
||||||
@@ -20,12 +17,10 @@ VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","a
|
|||||||
# 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!
|
||||||
# must be lowercase `true` when turned on
|
# must be lowercase `true` when turned on
|
||||||
#
|
#
|
||||||
# whether to enable Service Workers in development
|
|
||||||
VITE_APP_DEV_ENABLE_SW=
|
|
||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
VITE_APP_DISABLE_TRACKING=true
|
VITE_APP_ENABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
@@ -42,3 +37,14 @@ VITE_APP_COLLAPSE_OVERLAY=true
|
|||||||
|
|
||||||
# Set this flag to false to disable eslint
|
# Set this flag to false to disable eslint
|
||||||
VITE_APP_ENABLE_ESLINT=true
|
VITE_APP_ENABLE_ESLINT=true
|
||||||
|
|
||||||
|
# Enable PWA in dev server
|
||||||
|
VITE_APP_ENABLE_PWA=false
|
||||||
|
|
||||||
|
VITE_APP_PLUS_EXPORT_PUBLIC_KEY='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm2g5T+Rub6Kbf1Mf57t0
|
||||||
|
7r2zeHuVg4dla3r5ryXMswtzz6x767octl6oLThn33mQsPSy3GKglFZoCTXJR4ij
|
||||||
|
ba8SxB04sL/N8eRrKja7TFWjCVtRwTTfyy771NYYNFVJclkxHyE5qw4m27crHF1y
|
||||||
|
UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
|
||||||
|
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
||||||
|
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
||||||
|
HQIDAQAB'
|
||||||
|
|||||||
@@ -4,17 +4,29 @@ 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
|
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
||||||
|
|
||||||
# Fill to set socket server URL used for collaboration.
|
# socket server URL used for collaboration
|
||||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
||||||
VITE_APP_WS_SERVER_URL=
|
|
||||||
|
|
||||||
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"}'
|
||||||
|
|
||||||
VITE_APP_DISABLE_TRACKING=
|
VITE_APP_ENABLE_TRACKING=false
|
||||||
|
|
||||||
|
VITE_APP_PLUS_EXPORT_PUBLIC_KEY='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApQ0jM9Qz8TdFLzcuAZZX
|
||||||
|
/WvuKSOJxiw6AR/ZcE3eFQWM/mbFdhQgyK8eHGkKQifKzH1xUZjCxyXcxW6ZO02t
|
||||||
|
kPOPxhz+nxUrIoWCD/V4NGmUA1lxwHuO21HN1gzKrN3xHg5EGjyouR9vibT9VDGF
|
||||||
|
gq6+4Ic/kJX+AD2MM7Yre2+FsOdysrmuW2Fu3ahuC1uQE7pOe1j0k7auNP0y1q53
|
||||||
|
PrB8Ts2LUpepWC1l7zIXFm4ViDULuyWXTEpUcHSsEH8vpd1tckjypxCwkipfZsXx
|
||||||
|
iPszy0o0Dx2iArPfWMXlFAI9mvyFCyFC3+nSvfyAUb2C4uZgCwAuyFh/ydPF4DEE
|
||||||
|
PQIDAQAB'
|
||||||
|
|
||||||
|
# Set the below flags explicitly to false in production mode since vite loads and merges .env.local vars when running the build command
|
||||||
|
VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=false
|
||||||
|
VITE_APP_COLLAPSE_OVERLAY=false
|
||||||
|
# Enable eslint in dev server
|
||||||
|
VITE_APP_ENABLE_ESLINT=false
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ firebase/
|
|||||||
dist/
|
dist/
|
||||||
public/workbox
|
public/workbox
|
||||||
packages/excalidraw/types
|
packages/excalidraw/types
|
||||||
|
examples/**/public
|
||||||
|
dev-dist
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/no-anonymous-default-export": "off",
|
"import/no-anonymous-default-export": "off",
|
||||||
"no-restricted-globals": "off"
|
"no-restricted-globals": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "disallowTypeAnnotations": false, "fixStyle": "separate-type-imports" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
yarn add @actions/core -W
|
||||||
yarn autorelease
|
yarn autorelease
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ jobs:
|
|||||||
semantic:
|
semantic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on: pull_request
|
on:
|
||||||
|
push:
|
||||||
|
branches: master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
|
|||||||
@@ -25,3 +25,5 @@ packages/excalidraw/types
|
|||||||
coverage
|
coverage
|
||||||
dev-dist
|
dev-dist
|
||||||
html
|
html
|
||||||
|
examples/**/bundle.*
|
||||||
|
meta*.json
|
||||||
@@ -2,16 +2,18 @@ FROM node:18 AS build
|
|||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY . .
|
||||||
RUN yarn --ignore-optional --network-timeout 600000
|
|
||||||
|
# do not ignore optional dependencies:
|
||||||
|
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
|
||||||
|
RUN yarn --network-timeout 600000
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN yarn build:app:docker
|
RUN yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.21-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
||||||
|
|
||||||
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
|
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
|
||||||
|
|||||||
@@ -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/packages/excalidraw/components/main-menu/DefaultItems.tsx) of the default items.
|
||||||
|
|
||||||
### MainMenu.Group
|
### MainMenu.Group
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ The welcome screen consists of two main groups of subcomponents:
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
src={require("@site/static/img/welcome-screen-overview.png").default}
|
src={require("@site/static/img/welcome-screen-overview.png").default}
|
||||||
alt="Excalidraw logo: Sketch handrawn like diagrams."
|
alt="Excalidraw logo: Sketch hand-drawn like diagrams."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
### Center
|
### Center
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { FONT_FAMILY } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
| Font Family | Description |
|
| Font Family | Description |
|
||||||
| ----------- | ---------------------- |
|
| ----------- | ---------------------- |
|
||||||
| `Virgil` | The `handwritten` font |
|
| `Virgil` | The `Hand-drawn` font |
|
||||||
| `Helvetica` | The `Normal` Font |
|
| `Helvetica` | The `Normal` Font |
|
||||||
| `Cascadia` | The `Code` Font |
|
| `Cascadia` | The `Code` Font |
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Once the callback is triggered, you will need to store the api in state to acces
|
|||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||||
return <Excalidraw excalidrawAPI={{(api)=> setExcalidrawAPI(api)}} />;
|
return <Excalidraw excalidrawAPI={(api)=> setExcalidrawAPI(api)} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
|
|||||||
| API | Signature | Usage |
|
| API | Signature | Usage |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
|
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
|
||||||
| [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
|
| [updateLibrary](#updatelibrary) | `function` | updates the library |
|
||||||
| [addFiles](#addfiles) | `function` | add files data to the appState |
|
| [addFiles](#addfiles) | `function` | add files data to the appState |
|
||||||
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
||||||
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
|
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
|
||||||
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
|
|||||||
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
|
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
|
||||||
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
|
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
|
||||||
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
|
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
|
||||||
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
|
| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar 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 |
|
||||||
@@ -65,7 +65,7 @@ You can use this function to update the scene with the sceneData. It accepts the
|
|||||||
| `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/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene |
|
||||||
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. |
|
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. |
|
||||||
| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
|
| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
|
||||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
| `commitToStore` | `boolean` | Implies if the change should be captured and commited to the `store`. Commited changes are emmitted and listened to by other components, such as `History` for undo / redo purposes. Defaults to `false`. |
|
||||||
|
|
||||||
```jsx live
|
```jsx live
|
||||||
function App() {
|
function App() {
|
||||||
@@ -115,7 +115,7 @@ function App() {
|
|||||||
<button className="custom-button" onClick={updateScene}>
|
<button className="custom-button" onClick={updateScene}>
|
||||||
Update Scene
|
Update Scene
|
||||||
</button>
|
</button>
|
||||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ function App() {
|
|||||||
Update Library
|
Update Library
|
||||||
</button>
|
</button>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={(api) => setExcalidrawAPI(api)}
|
excalidrawAPI={(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/packages/excalidraw/initialData.js
|
||||||
initialData={{
|
initialData={{
|
||||||
libraryItems: initialData.libraryItems,
|
libraryItems: initialData.libraryItems,
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ All `props` are _optional_.
|
|||||||
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
|
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
|
||||||
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
|
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
|
||||||
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
|
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
|
||||||
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
|
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down events |
|
||||||
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
|
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when something is pasted into the scene |
|
||||||
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||||
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
|
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
|
||||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||||
@@ -23,10 +23,10 @@ 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`](#canvasactions) |
|
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
|
||||||
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
| [`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 |
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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/packages/utils/export.ts#L24">ExportOpts</a>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
@@ -90,7 +90,7 @@ function App() {
|
|||||||
<img src={canvasUrl} alt="" />
|
<img src={canvasUrl} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "400px" }}>
|
<div style={{ height: "400px" }}>
|
||||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
|
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -32,15 +32,9 @@ function App() {
|
|||||||
|
|
||||||
### Next.js
|
### Next.js
|
||||||
|
|
||||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`.
|
||||||
|
|
||||||
Here are two ways on how you can render **Excalidraw** on **Next.js**.
|
If you want to only import `Excalidraw` component you can do :point_down:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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";
|
||||||
@@ -55,25 +49,88 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
|
However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically.
|
||||||
|
|
||||||
|
If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down:
|
||||||
|
|
||||||
2. Importing Excalidraw once **client** is rendered.
|
<Tabs>
|
||||||
|
<TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
export default function App() {
|
import { Excalidraw, convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||||
const [Excalidraw, setExcalidraw] = useState(null);
|
|
||||||
useEffect(() => {
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import("@excalidraw/excalidraw").then((comp) =>
|
|
||||||
setExcalidraw(comp.Excalidraw),
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
|
console.info(convertToExcalidrawElements([{
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rect-1",
|
||||||
|
width: 186.47265625,
|
||||||
|
height: 141.9765625,
|
||||||
|
},]));
|
||||||
|
return (
|
||||||
|
<div style={{height:"500px", width:"500px"}}>
|
||||||
|
<Excalidraw />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
return <>{Excalidraw && <Excalidraw />}</>;
|
export default ExcalidrawWrapper;
|
||||||
}
|
```
|
||||||
```
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="pages" label="Pages router">
|
||||||
|
|
||||||
|
```jsx showLineNumbers
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing
|
||||||
|
// the excalidraw stuff dynamically with ssr false
|
||||||
|
|
||||||
|
const ExcalidrawWrapper = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<ExcalidrawWrapper />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="app" label="App router">
|
||||||
|
|
||||||
|
```jsx showLineNumbers
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing
|
||||||
|
// the excalidraw stuff dynamically with ssr false
|
||||||
|
|
||||||
|
const ExcalidrawWrapper = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<ExcalidrawWrapper />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
|
||||||
|
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/).
|
||||||
|
|
||||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
|
|
||||||
|
|
||||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ This API receives the mermaid syntax as the input, and resolves to skeleton Exca
|
|||||||
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
||||||
import { convertToExcalidrawElements} from "@excalidraw/excalidraw"
|
import { convertToExcalidrawElements} from "@excalidraw/excalidraw"
|
||||||
try {
|
try {
|
||||||
const { elements, files } = await parseMermaid(mermaidSyntax: string, {
|
const { elements, files } = await parseMermaidToExcalidraw(mermaidSyntax: string, {
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
});
|
});
|
||||||
const excalidrawElements = convertToExcalidrawElements(elements);
|
const excalidrawElements = convertToExcalidrawElements(elements);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`)
|
|||||||
|
|
||||||
// editor state (canvas config, preferences, ...)
|
// editor state (canvas config, preferences, ...)
|
||||||
"appState": {
|
"appState": {
|
||||||
"gridSize": null,
|
"gridSize": 20,
|
||||||
"viewBackgroundColor": "#ffffff"
|
"viewBackgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,13 @@
|
|||||||
"@docusaurus/core": "2.2.0",
|
"@docusaurus/core": "2.2.0",
|
||||||
"@docusaurus/preset-classic": "2.2.0",
|
"@docusaurus/preset-classic": "2.2.0",
|
||||||
"@docusaurus/theme-live-codeblock": "2.2.0",
|
"@docusaurus/theme-live-codeblock": "2.2.0",
|
||||||
"@excalidraw/excalidraw": "0.17.0",
|
"@excalidraw/excalidraw": "0.17.6",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"docusaurus-plugin-sass": "0.2.3",
|
"docusaurus-plugin-sass": "0.2.3",
|
||||||
"prism-react-renderer": "^1.3.5",
|
"prism-react-renderer": "^1.3.5",
|
||||||
"react": "^17.0.2",
|
"react": "18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"sass": "1.57.1"
|
"sass": "1.57.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pre a {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
background: #70b1ec;
|
background: #70b1ec;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1547,7 +1547,7 @@
|
|||||||
"@docusaurus/theme-search-algolia" "2.2.0"
|
"@docusaurus/theme-search-algolia" "2.2.0"
|
||||||
"@docusaurus/types" "2.2.0"
|
"@docusaurus/types" "2.2.0"
|
||||||
|
|
||||||
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
"@docusaurus/react-loadable@5.5.2":
|
||||||
version "5.5.2"
|
version "5.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||||
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||||
@@ -1718,10 +1718,10 @@
|
|||||||
url-loader "^4.1.1"
|
url-loader "^4.1.1"
|
||||||
webpack "^5.73.0"
|
webpack "^5.73.0"
|
||||||
|
|
||||||
"@excalidraw/excalidraw@0.17.0":
|
"@excalidraw/excalidraw@0.17.6":
|
||||||
version "0.17.0"
|
version "0.17.6"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.0.tgz#3c64aa8e36406ac171b008cfecbdce5bb0755725"
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.6.tgz#5fd208ce69d33ca712d1804b50d7d06d5c46ac4d"
|
||||||
integrity sha512-NzP22v5xMqxYW27ZtTHhiGFe7kE8NeBk45aoeM/mDSkXiOXPDH+PcvwzHRN/Ei+Vj/0sTPHxejn8bZyRWKGjXg==
|
integrity sha512-fyCl+zG/Z5yhHDh5Fq2ZGmphcrALmuOdtITm8gN4d8w4ntnaopTXcTfnAAaU3VleDC6LhTkoLOTG6P5kgREiIg==
|
||||||
|
|
||||||
"@hapi/hoek@^9.0.0":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "9.3.0"
|
version "9.3.0"
|
||||||
@@ -2789,7 +2789,14 @@ brace-expansion@^1.1.7:
|
|||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
braces@^3.0.2, braces@~3.0.2:
|
braces@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||||
|
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||||
|
dependencies:
|
||||||
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
|
braces@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||||
@@ -4011,6 +4018,13 @@ fill-range@^7.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
|
fill-range@^7.1.1:
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
|
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||||
|
dependencies:
|
||||||
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
finalhandler@1.2.0:
|
finalhandler@1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
|
||||||
@@ -5207,11 +5221,11 @@ methods@~1.1.2:
|
|||||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||||
|
|
||||||
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
||||||
version "4.0.5"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||||
dependencies:
|
dependencies:
|
||||||
braces "^3.0.2"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
||||||
@@ -6190,14 +6204,13 @@ react-dev-utils@^12.0.1:
|
|||||||
strip-ansi "^6.0.1"
|
strip-ansi "^6.0.1"
|
||||||
text-table "^0.2.0"
|
text-table "^0.2.0"
|
||||||
|
|
||||||
react-dom@^17.0.2:
|
react-dom@18.2.0:
|
||||||
version "17.0.2"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
scheduler "^0.23.0"
|
||||||
scheduler "^0.20.2"
|
|
||||||
|
|
||||||
react-error-overlay@^6.0.11:
|
react-error-overlay@^6.0.11:
|
||||||
version "6.0.11"
|
version "6.0.11"
|
||||||
@@ -6260,6 +6273,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.3"
|
"@babel/runtime" "^7.10.3"
|
||||||
|
|
||||||
|
"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
||||||
|
version "5.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||||
|
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
react-router-config@^5.1.1:
|
react-router-config@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
||||||
@@ -6310,13 +6331,12 @@ react-textarea-autosize@^8.3.2:
|
|||||||
use-composed-ref "^1.3.0"
|
use-composed-ref "^1.3.0"
|
||||||
use-latest "^1.2.1"
|
use-latest "^1.2.1"
|
||||||
|
|
||||||
react@^17.0.2:
|
react@18.2.0:
|
||||||
version "17.0.2"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
|
||||||
|
|
||||||
readable-stream@^2.0.1:
|
readable-stream@^2.0.1:
|
||||||
version "2.3.7"
|
version "2.3.7"
|
||||||
@@ -6664,13 +6684,12 @@ sax@^1.2.4:
|
|||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
|
||||||
scheduler@^0.20.2:
|
scheduler@^0.23.0:
|
||||||
version "0.20.2"
|
version "0.23.2"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
|
||||||
|
|
||||||
schema-utils@2.7.0:
|
schema-utils@2.7.0:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ExcalidrawImperativeAPI } from "../types";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
|
|
||||||
const { Button, MIME_TYPES } = window.ExcalidrawLib;
|
|
||||||
const COMMENT_SVG = (
|
const COMMENT_SVG = (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -17,24 +17,28 @@ const COMMENT_SVG = (
|
|||||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const CustomFooter = ({
|
const CustomFooter = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
|
excalidrawLib,
|
||||||
}: {
|
}: {
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
|
excalidrawLib: typeof TExcalidraw;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { Button, MIME_TYPES } = excalidrawLib;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onSelect={() => alert("General Kenobi!")}
|
onSelect={() => alert("General Kenobi!")}
|
||||||
className="you are a bold one"
|
style={{ marginLeft: "1rem", width: "auto" }}
|
||||||
style={{ marginLeft: "1rem" }}
|
|
||||||
title="Hello there!"
|
title="Hello there!"
|
||||||
>
|
>
|
||||||
{COMMENT_SVG}
|
Hit me
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className="custom-element"
|
className="custom-element"
|
||||||
onClick={() => {
|
onSelect={() => {
|
||||||
excalidrawAPI?.setActiveTool({
|
excalidrawAPI?.setActiveTool({
|
||||||
type: "custom",
|
type: "custom",
|
||||||
customType: "comment",
|
customType: "comment",
|
||||||
@@ -57,15 +61,10 @@ const CustomFooter = ({
|
|||||||
)}`;
|
)}`;
|
||||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||||
}}
|
}}
|
||||||
|
title="Comments!"
|
||||||
>
|
>
|
||||||
{COMMENT_SVG}
|
{COMMENT_SVG}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
|
||||||
className="custom-footer"
|
|
||||||
onClick={() => alert("This is dummy footer")}
|
|
||||||
>
|
|
||||||
custom footer
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -15,14 +15,23 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.app-title {
|
||||||
|
margin-block-start: 0.83em;
|
||||||
|
margin-block-end: 0.83em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrapper button {
|
.button-wrapper {
|
||||||
z-index: 1;
|
input[type="checkbox"] {
|
||||||
height: 40px;
|
margin: 5px;
|
||||||
max-width: 200px;
|
}
|
||||||
margin: 10px;
|
button {
|
||||||
padding: 5px;
|
z-index: 1;
|
||||||
|
height: 40px;
|
||||||
|
max-width: 200px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw .App-menu_top .buttonList {
|
.excalidraw .App-menu_top .buttonList {
|
||||||
@@ -1,15 +1,30 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
Children,
|
||||||
|
cloneElement,
|
||||||
|
} from "react";
|
||||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||||
|
|
||||||
import type * as TExcalidraw from "../index";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import "./App.scss";
|
|
||||||
import initialData from "./initialData";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { resolvablePromise, ResolvablePromise } from "../utils";
|
|
||||||
import { EVENT, ROUNDNESS } from "../constants";
|
import type { ResolvablePromise } from "../utils";
|
||||||
import { distance2d } from "../math";
|
import {
|
||||||
import { fileOpen } from "../data/filesystem";
|
resolvablePromise,
|
||||||
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
distance2d,
|
||||||
|
fileOpen,
|
||||||
|
withBatchedUpdates,
|
||||||
|
withBatchedUpdatesThrottled,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
import CustomFooter from "./CustomFooter";
|
||||||
|
import MobileFooter from "./MobileFooter";
|
||||||
|
import initialData from "../initialData";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
@@ -18,19 +33,14 @@ import type {
|
|||||||
Gesture,
|
Gesture,
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
PointerDownState as ExcalidrawPointerDownState,
|
PointerDownState as ExcalidrawPointerDownState,
|
||||||
} from "../types";
|
} from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
import type { NonDeletedExcalidrawElement, Theme } from "../element/types";
|
import type {
|
||||||
import { ImportedLibraryData } from "../data/types";
|
NonDeletedExcalidrawElement,
|
||||||
import CustomFooter from "./CustomFooter";
|
Theme,
|
||||||
import MobileFooter from "./MobileFooter";
|
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
||||||
import { KEYS } from "../keys";
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
|
||||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
|
||||||
|
|
||||||
declare global {
|
import "./ExampleApp.scss";
|
||||||
interface Window {
|
|
||||||
ExcalidrawLib: typeof TExcalidraw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comment = {
|
type Comment = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -51,31 +61,6 @@ type PointerDownState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const { useEffect, useState, useRef, useCallback } = window.React;
|
|
||||||
|
|
||||||
// This is so that we use the bundled excalidraw.development.js file instead
|
|
||||||
// of the actual source code
|
|
||||||
const {
|
|
||||||
exportToCanvas,
|
|
||||||
exportToSvg,
|
|
||||||
exportToBlob,
|
|
||||||
exportToClipboard,
|
|
||||||
Excalidraw,
|
|
||||||
useHandleLibrary,
|
|
||||||
MIME_TYPES,
|
|
||||||
sceneCoordsToViewportCoords,
|
|
||||||
viewportCoordsToSceneCoords,
|
|
||||||
restoreElements,
|
|
||||||
Sidebar,
|
|
||||||
Footer,
|
|
||||||
WelcomeScreen,
|
|
||||||
MainMenu,
|
|
||||||
LiveCollaborationTrigger,
|
|
||||||
convertToExcalidrawElements,
|
|
||||||
TTDDialog,
|
|
||||||
TTDDialogTrigger,
|
|
||||||
} = window.ExcalidrawLib;
|
|
||||||
|
|
||||||
const COMMENT_ICON_DIMENSION = 32;
|
const COMMENT_ICON_DIMENSION = 32;
|
||||||
const COMMENT_INPUT_HEIGHT = 50;
|
const COMMENT_INPUT_HEIGHT = 50;
|
||||||
const COMMENT_INPUT_WIDTH = 150;
|
const COMMENT_INPUT_WIDTH = 150;
|
||||||
@@ -84,8 +69,38 @@ export interface AppProps {
|
|||||||
appTitle: string;
|
appTitle: string;
|
||||||
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
||||||
customArgs?: any[];
|
customArgs?: any[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
excalidrawLib: typeof TExcalidraw;
|
||||||
}
|
}
|
||||||
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|
||||||
|
export default function ExampleApp({
|
||||||
|
appTitle,
|
||||||
|
useCustom,
|
||||||
|
customArgs,
|
||||||
|
children,
|
||||||
|
excalidrawLib,
|
||||||
|
}: AppProps) {
|
||||||
|
const {
|
||||||
|
exportToCanvas,
|
||||||
|
exportToSvg,
|
||||||
|
exportToBlob,
|
||||||
|
exportToClipboard,
|
||||||
|
useHandleLibrary,
|
||||||
|
MIME_TYPES,
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
restoreElements,
|
||||||
|
Sidebar,
|
||||||
|
Footer,
|
||||||
|
WelcomeScreen,
|
||||||
|
MainMenu,
|
||||||
|
LiveCollaborationTrigger,
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
TTDDialog,
|
||||||
|
TTDDialogTrigger,
|
||||||
|
ROUNDNESS,
|
||||||
|
loadSceneOrLibraryFromBlob,
|
||||||
|
} = excalidrawLib;
|
||||||
const appRef = useRef<any>(null);
|
const appRef = useRef<any>(null);
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
@@ -147,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
|
||||||
|
|
||||||
|
const renderExcalidraw = (children: React.ReactNode) => {
|
||||||
|
const Excalidraw: any = Children.toArray(children).find(
|
||||||
|
(child) =>
|
||||||
|
React.isValidElement(child) &&
|
||||||
|
typeof child.type !== "string" &&
|
||||||
|
//@ts-ignore
|
||||||
|
child.type.displayName === "Excalidraw",
|
||||||
|
);
|
||||||
|
if (!Excalidraw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newElement = cloneElement(
|
||||||
|
Excalidraw,
|
||||||
|
{
|
||||||
|
excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
|
||||||
|
initialData: initialStatePromiseRef.current.promise,
|
||||||
|
onChange: (
|
||||||
|
elements: NonDeletedExcalidrawElement[],
|
||||||
|
state: AppState,
|
||||||
|
) => {
|
||||||
|
console.info("Elements :", elements, "State : ", state);
|
||||||
|
},
|
||||||
|
onPointerUpdate: (payload: {
|
||||||
|
pointer: { x: number; y: number };
|
||||||
|
button: "down" | "up";
|
||||||
|
pointersMap: Gesture["pointers"];
|
||||||
|
}) => setPointerData(payload),
|
||||||
|
viewModeEnabled,
|
||||||
|
zenModeEnabled,
|
||||||
|
gridModeEnabled,
|
||||||
|
theme,
|
||||||
|
name: "Custom name of drawing",
|
||||||
|
UIOptions: {
|
||||||
|
canvasActions: {
|
||||||
|
loadScene: false,
|
||||||
|
},
|
||||||
|
tools: { image: !disableImageTool },
|
||||||
|
},
|
||||||
|
renderTopRightUI,
|
||||||
|
onLinkOpen,
|
||||||
|
onPointerDown,
|
||||||
|
onScrollChange: rerenderCommentIcons,
|
||||||
|
validateEmbeddable: true,
|
||||||
|
},
|
||||||
|
<>
|
||||||
|
{excalidrawAPI && (
|
||||||
|
<Footer>
|
||||||
|
<CustomFooter
|
||||||
|
excalidrawAPI={excalidrawAPI}
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
/>
|
||||||
|
</Footer>
|
||||||
|
)}
|
||||||
|
<WelcomeScreen />
|
||||||
|
<Sidebar name="custom">
|
||||||
|
<Sidebar.Tabs>
|
||||||
|
<Sidebar.Header />
|
||||||
|
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
||||||
|
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
||||||
|
<Sidebar.TabTriggers>
|
||||||
|
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
||||||
|
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
||||||
|
</Sidebar.TabTriggers>
|
||||||
|
</Sidebar.Tabs>
|
||||||
|
</Sidebar>
|
||||||
|
<Sidebar.Trigger
|
||||||
|
name="custom"
|
||||||
|
tab="one"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
bottom: "20px",
|
||||||
|
zIndex: 9999999999999999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Toggle Custom Sidebar
|
||||||
|
</Sidebar.Trigger>
|
||||||
|
{renderMenu()}
|
||||||
|
{excalidrawAPI && (
|
||||||
|
<TTDDialogTrigger icon={<span>😀</span>}>
|
||||||
|
Text to diagram
|
||||||
|
</TTDDialogTrigger>
|
||||||
|
)}
|
||||||
|
<TTDDialog
|
||||||
|
onTextSubmit={async (_) => {
|
||||||
|
console.info("submit");
|
||||||
|
// sleep for 2s
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
throw new Error("error, go away now");
|
||||||
|
// return "dummy";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
return newElement;
|
||||||
|
};
|
||||||
const renderTopRightUI = (isMobile: boolean) => {
|
const renderTopRightUI = (isMobile: boolean) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -257,10 +369,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await exportToClipboard({
|
await exportToClipboard({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
data: {
|
||||||
appState: excalidrawAPI.getAppState(),
|
elements: excalidrawAPI.getSceneElements(),
|
||||||
files: excalidrawAPI.getFiles(),
|
appState: excalidrawAPI.getAppState(),
|
||||||
type,
|
files: excalidrawAPI.getFiles(),
|
||||||
|
},
|
||||||
|
type: "json",
|
||||||
});
|
});
|
||||||
window.alert(`Copied to clipboard as ${type} successfully`);
|
window.alert(`Copied to clipboard as ${type} successfully`);
|
||||||
};
|
};
|
||||||
@@ -332,8 +446,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
) => {
|
) => {
|
||||||
return withBatchedUpdates((event) => {
|
return withBatchedUpdates((event) => {
|
||||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
window.removeEventListener("pointermove", pointerDownState.onMove);
|
||||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
window.removeEventListener("pointerup", pointerDownState.onUp);
|
||||||
excalidrawAPI?.setActiveTool({ type: "selection" });
|
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||||
const distance = distance2d(
|
const distance = distance2d(
|
||||||
pointerDownState.x,
|
pointerDownState.x,
|
||||||
@@ -397,8 +511,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
onPointerMoveFromPointerDownHandler(pointerDownState);
|
onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||||
const onPointerUp =
|
const onPointerUp =
|
||||||
onPointerUpFromPointerDownHandler(pointerDownState);
|
onPointerUpFromPointerDownHandler(pointerDownState);
|
||||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.addEventListener("pointermove", onPointerMove);
|
||||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
|
||||||
pointerDownState.onMove = onPointerMove;
|
pointerDownState.onMove = onPointerMove;
|
||||||
pointerDownState.onUp = onPointerUp;
|
pointerDownState.onUp = onPointerUp;
|
||||||
@@ -490,7 +604,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
}}
|
}}
|
||||||
onBlur={saveComment}
|
onBlur={saveComment}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (!event.shiftKey && event.key === KEYS.ENTER) {
|
if (!event.shiftKey && event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
saveComment();
|
saveComment();
|
||||||
}
|
}
|
||||||
@@ -523,7 +637,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
|
|
||||||
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && (
|
||||||
|
<MobileFooter
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
excalidrawAPI={excalidrawAPI}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -672,83 +791,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="excalidraw-wrapper">
|
<div className="excalidraw-wrapper">
|
||||||
<Excalidraw
|
{renderExcalidraw(children)}
|
||||||
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
|
|
||||||
setExcalidrawAPI(api)
|
|
||||||
}
|
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
|
||||||
onChange={(elements, state) => {
|
|
||||||
// console.info("Elements :", elements, "State : ", state);
|
|
||||||
}}
|
|
||||||
onPointerUpdate={(payload: {
|
|
||||||
pointer: { x: number; y: number };
|
|
||||||
button: "down" | "up";
|
|
||||||
pointersMap: Gesture["pointers"];
|
|
||||||
}) => setPointerData(payload)}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
|
||||||
zenModeEnabled={zenModeEnabled}
|
|
||||||
gridModeEnabled={gridModeEnabled}
|
|
||||||
theme={theme}
|
|
||||||
name="Custom name of drawing"
|
|
||||||
UIOptions={{
|
|
||||||
canvasActions: {
|
|
||||||
loadScene: false,
|
|
||||||
},
|
|
||||||
tools: { image: !disableImageTool },
|
|
||||||
}}
|
|
||||||
renderTopRightUI={renderTopRightUI}
|
|
||||||
onLinkOpen={onLinkOpen}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
onScrollChange={rerenderCommentIcons}
|
|
||||||
// allow all urls
|
|
||||||
validateEmbeddable={true}
|
|
||||||
>
|
|
||||||
{excalidrawAPI && (
|
|
||||||
<Footer>
|
|
||||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
|
||||||
</Footer>
|
|
||||||
)}
|
|
||||||
<WelcomeScreen />
|
|
||||||
<Sidebar name="custom">
|
|
||||||
<Sidebar.Tabs>
|
|
||||||
<Sidebar.Header />
|
|
||||||
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
|
||||||
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
|
||||||
<Sidebar.TabTriggers>
|
|
||||||
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
|
||||||
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
|
||||||
</Sidebar.TabTriggers>
|
|
||||||
</Sidebar.Tabs>
|
|
||||||
</Sidebar>
|
|
||||||
<Sidebar.Trigger
|
|
||||||
name="custom"
|
|
||||||
tab="one"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
bottom: "20px",
|
|
||||||
zIndex: 9999999999999999,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Custom Sidebar
|
|
||||||
</Sidebar.Trigger>
|
|
||||||
{renderMenu()}
|
|
||||||
{excalidrawAPI && (
|
|
||||||
<TTDDialogTrigger icon={<span>😀</span>}>
|
|
||||||
Text to diagram
|
|
||||||
</TTDDialogTrigger>
|
|
||||||
)}
|
|
||||||
<TTDDialog
|
|
||||||
onTextSubmit={async (_) => {
|
|
||||||
console.info("submit");
|
|
||||||
// sleep for 2s
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
throw new Error("error, go away now");
|
|
||||||
// return "dummy";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Excalidraw>
|
|
||||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||||
{comment && renderComment()}
|
{comment && renderComment()}
|
||||||
</div>
|
</div>
|
||||||
@@ -776,15 +819,17 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const svg = await exportToSvg({
|
const svg = await exportToSvg({
|
||||||
elements: excalidrawAPI?.getSceneElements(),
|
data: {
|
||||||
appState: {
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
...initialData.appState,
|
appState: {
|
||||||
exportWithDarkMode,
|
...initialData.appState,
|
||||||
exportEmbedScene,
|
exportWithDarkMode,
|
||||||
width: 300,
|
exportEmbedScene,
|
||||||
height: 100,
|
width: 300,
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
files: excalidrawAPI?.getFiles(),
|
||||||
},
|
},
|
||||||
files: excalidrawAPI?.getFiles(),
|
|
||||||
});
|
});
|
||||||
appRef.current.querySelector(".export-svg").innerHTML =
|
appRef.current.querySelector(".export-svg").innerHTML =
|
||||||
svg.outerHTML;
|
svg.outerHTML;
|
||||||
@@ -800,14 +845,18 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = await exportToBlob({
|
const blob = await exportToBlob({
|
||||||
elements: excalidrawAPI?.getSceneElements(),
|
data: {
|
||||||
mimeType: "image/png",
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
appState: {
|
appState: {
|
||||||
...initialData.appState,
|
...initialData.appState,
|
||||||
exportEmbedScene,
|
exportEmbedScene,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
|
},
|
||||||
|
files: excalidrawAPI?.getFiles(),
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
mimeType: "image/png",
|
||||||
},
|
},
|
||||||
files: excalidrawAPI?.getFiles(),
|
|
||||||
});
|
});
|
||||||
setBlobUrl(window.URL.createObjectURL(blob));
|
setBlobUrl(window.URL.createObjectURL(blob));
|
||||||
}}
|
}}
|
||||||
@@ -823,15 +872,17 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canvas = await exportToCanvas({
|
const canvas = await exportToCanvas({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
data: {
|
||||||
appState: {
|
elements: excalidrawAPI.getSceneElements(),
|
||||||
...initialData.appState,
|
appState: {
|
||||||
exportWithDarkMode,
|
...initialData.appState,
|
||||||
|
exportWithDarkMode,
|
||||||
|
},
|
||||||
|
files: excalidrawAPI.getFiles(),
|
||||||
},
|
},
|
||||||
files: excalidrawAPI.getFiles(),
|
|
||||||
});
|
});
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
ctx.font = "30px Virgil";
|
ctx.font = "30px Excalifont";
|
||||||
ctx.strokeText("My custom text", 50, 60);
|
ctx.strokeText("My custom text", 50, 60);
|
||||||
setCanvasUrl(canvas.toDataURL());
|
setCanvasUrl(canvas.toDataURL());
|
||||||
}}
|
}}
|
||||||
@@ -844,15 +895,17 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canvas = await exportToCanvas({
|
const canvas = await exportToCanvas({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
data: {
|
||||||
appState: {
|
elements: excalidrawAPI.getSceneElements(),
|
||||||
...initialData.appState,
|
appState: {
|
||||||
exportWithDarkMode,
|
...initialData.appState,
|
||||||
|
exportWithDarkMode,
|
||||||
|
},
|
||||||
|
files: excalidrawAPI.getFiles(),
|
||||||
},
|
},
|
||||||
files: excalidrawAPI.getFiles(),
|
|
||||||
});
|
});
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
ctx.font = "30px Virgil";
|
ctx.font = "30px Excalifont";
|
||||||
ctx.strokeText("My custom text", 50, 60);
|
ctx.strokeText("My custom text", 50, 60);
|
||||||
setCanvasUrl(canvas.toDataURL());
|
setCanvasUrl(canvas.toDataURL());
|
||||||
}}
|
}}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { 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,9 +1,8 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import "./ExampleSidebar.scss";
|
import "./ExampleSidebar.scss";
|
||||||
|
|
||||||
const React = window.React;
|
|
||||||
|
|
||||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
|
||||||
import type { FileId } from "../element/types";
|
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
||||||
|
|
||||||
const elements: ExcalidrawElementSkeleton[] = [
|
const elements: ExcalidrawElementSkeleton[] = [
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
];
|
];
|
||||||
export default {
|
export default {
|
||||||
elements,
|
elements,
|
||||||
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
|
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 5 },
|
||||||
scrollToContent: true,
|
scrollToContent: true,
|
||||||
libraryItems: [
|
libraryItems: [
|
||||||
[
|
[
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "examples",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"@excalidraw/excalidraw": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig"
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||||
|
import { 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);
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# copied assets
|
||||||
|
public/**/*.woff2
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3005) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
distDir: "build",
|
||||||
|
typescript: {
|
||||||
|
// The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
// This is needed as in pages router the code for importing types throws error as its outside next js app
|
||||||
|
transpilePackages: ["../"],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "with-nextjs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
||||||
|
"copy:assets": "cp -r ../../../packages/excalidraw/dist/prod/fonts ./public",
|
||||||
|
"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.2.0",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "18.2.0",
|
||||||
|
"@types/react-dom": "18.2.0",
|
||||||
|
"path2d-polyfill": "2.0.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import Script from "next/script";
|
||||||
|
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>
|
||||||
|
<Script id="load-env-variables" strategy="beforeInteractive">
|
||||||
|
{`window["EXCALIDRAW_ASSET_PATH"] = window.origin;`}
|
||||||
|
</Script>
|
||||||
|
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||||
|
<ExcalidrawWithClientOnly />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1c7ed6;
|
||||||
|
font-size: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import App from "../../components/ExampleApp";
|
||||||
|
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<App
|
||||||
|
appTitle={"Excalidraw with Nextjs Example"}
|
||||||
|
useCustom={(api: any, args?: any[]) => {}}
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
>
|
||||||
|
<Excalidraw />
|
||||||
|
</App>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExcalidrawWrapper;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import "../common.scss";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
// with ssr false
|
||||||
|
const Excalidraw = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href="/">Switch to App router</a>
|
||||||
|
<h1 className="page-title">Pages Router</h1>
|
||||||
|
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||||
|
<Excalidraw />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"outputDirectory": "build"
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@excalidraw/excalidraw@workspace:^":
|
||||||
|
version "0.17.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96"
|
||||||
|
integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ==
|
||||||
|
|
||||||
|
"@next/env@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
|
||||||
|
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
|
||||||
|
|
||||||
|
"@next/swc-darwin-arm64@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
|
||||||
|
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
|
||||||
|
|
||||||
|
"@next/swc-darwin-x64@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
|
||||||
|
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-gnu@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
|
||||||
|
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-musl@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
|
||||||
|
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-gnu@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
|
||||||
|
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-musl@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
|
||||||
|
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
|
||||||
|
|
||||||
|
"@next/swc-win32-arm64-msvc@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
|
||||||
|
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
|
||||||
|
|
||||||
|
"@next/swc-win32-ia32-msvc@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
|
||||||
|
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
|
||||||
|
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
|
||||||
|
|
||||||
|
"@swc/helpers@0.5.2":
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||||
|
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@types/node@^20":
|
||||||
|
version "20.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
|
||||||
|
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
|
||||||
|
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||||
|
|
||||||
|
"@types/react-dom@^18":
|
||||||
|
version "18.2.18"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
|
||||||
|
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react@*", "@types/react@^18":
|
||||||
|
version "18.2.47"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40"
|
||||||
|
integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
|
||||||
|
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
|
||||||
|
|
||||||
|
busboy@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||||
|
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||||
|
dependencies:
|
||||||
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
|
caniuse-lite@^1.0.30001406:
|
||||||
|
version "1.0.30001576"
|
||||||
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4"
|
||||||
|
integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==
|
||||||
|
|
||||||
|
client-only@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
|
csstype@^3.0.2:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
glob-to-regexp@^0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||||
|
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||||
|
|
||||||
|
graceful-fs@^4.1.2, graceful-fs@^4.2.11:
|
||||||
|
version "4.2.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
|
|
||||||
|
"js-tokens@^3.0.0 || ^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
loose-envify@^1.1.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
dependencies:
|
||||||
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
nanoid@^3.3.6:
|
||||||
|
version "3.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
|
next@14.0.4:
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
|
||||||
|
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
|
||||||
|
dependencies:
|
||||||
|
"@next/env" "14.0.4"
|
||||||
|
"@swc/helpers" "0.5.2"
|
||||||
|
busboy "1.6.0"
|
||||||
|
caniuse-lite "^1.0.30001406"
|
||||||
|
graceful-fs "^4.2.11"
|
||||||
|
postcss "8.4.31"
|
||||||
|
styled-jsx "5.1.1"
|
||||||
|
watchpack "2.4.0"
|
||||||
|
optionalDependencies:
|
||||||
|
"@next/swc-darwin-arm64" "14.0.4"
|
||||||
|
"@next/swc-darwin-x64" "14.0.4"
|
||||||
|
"@next/swc-linux-arm64-gnu" "14.0.4"
|
||||||
|
"@next/swc-linux-arm64-musl" "14.0.4"
|
||||||
|
"@next/swc-linux-x64-gnu" "14.0.4"
|
||||||
|
"@next/swc-linux-x64-musl" "14.0.4"
|
||||||
|
"@next/swc-win32-arm64-msvc" "14.0.4"
|
||||||
|
"@next/swc-win32-ia32-msvc" "14.0.4"
|
||||||
|
"@next/swc-win32-x64-msvc" "14.0.4"
|
||||||
|
|
||||||
|
path2d-polyfill@2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391"
|
||||||
|
integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==
|
||||||
|
|
||||||
|
picocolors@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
|
postcss@8.4.31:
|
||||||
|
version "8.4.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||||
|
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.6"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
|
react-dom@^18:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react@^18:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
scheduler@^0.23.0:
|
||||||
|
version "0.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||||
|
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
source-map-js@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
|
streamsearch@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||||
|
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||||
|
|
||||||
|
styled-jsx@5.1.1:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
|
||||||
|
integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
|
||||||
|
dependencies:
|
||||||
|
client-only "0.0.1"
|
||||||
|
|
||||||
|
tslib@^2.4.0:
|
||||||
|
version "2.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||||
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|
||||||
|
typescript@^5:
|
||||||
|
version "5.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||||
|
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
|
watchpack@2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
|
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||||
|
dependencies:
|
||||||
|
glob-to-regexp "^0.4.1"
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# copied assets
|
||||||
|
public/**/*.woff2
|
||||||
@@ -11,22 +11,23 @@
|
|||||||
<title>React App</title>
|
<title>React App</title>
|
||||||
<script>
|
<script>
|
||||||
window.name = "codesandbox";
|
window.name = "codesandbox";
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||||
<link rel="stylesheet" href="bundle.css" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
|
|
||||||
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
|
|
||||||
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
||||||
of the actual source code -->
|
of the actual source code -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as ExcalidrawLib from "/dist/browser/dev/index.js";
|
import * as ExcalidrawLib from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
console.log(ExcalidrawLib);
|
||||||
window.ExcalidrawLib = ExcalidrawLib;
|
window.ExcalidrawLib = ExcalidrawLib;
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="bundle.js"></script>
|
<script type="module" src="index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import App from "../components/ExampleApp";
|
||||||
|
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>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
||||||
|
"copy:assets": "cp -r ../../../packages/excalidraw/dist/prod/fonts ./public",
|
||||||
|
"start": "yarn build:workspace && vite",
|
||||||
|
"build": "yarn build:workspace && vite build",
|
||||||
|
"build:preview": "yarn build && vite preview --port 5002"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"outputDirectory": "example/public",
|
"outputDirectory": "dist",
|
||||||
"installCommand": "yarn install"
|
"installCommand": "yarn install"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
// open the browser
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
publicDir: "public",
|
||||||
|
});
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3"
|
||||||
|
integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==
|
||||||
|
|
||||||
|
"@esbuild/android-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220"
|
||||||
|
integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==
|
||||||
|
|
||||||
|
"@esbuild/android-arm@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c"
|
||||||
|
integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==
|
||||||
|
|
||||||
|
"@esbuild/android-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2"
|
||||||
|
integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf"
|
||||||
|
integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e"
|
||||||
|
integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a"
|
||||||
|
integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2"
|
||||||
|
integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545"
|
||||||
|
integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3"
|
||||||
|
integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4"
|
||||||
|
integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121"
|
||||||
|
integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9"
|
||||||
|
integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912"
|
||||||
|
integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916"
|
||||||
|
integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8"
|
||||||
|
integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==
|
||||||
|
|
||||||
|
"@esbuild/linux-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766"
|
||||||
|
integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d"
|
||||||
|
integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2"
|
||||||
|
integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767"
|
||||||
|
integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee"
|
||||||
|
integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c"
|
||||||
|
integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04"
|
||||||
|
integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
|
||||||
|
integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a"
|
||||||
|
integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb"
|
||||||
|
integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1"
|
||||||
|
integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560"
|
||||||
|
integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114"
|
||||||
|
integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632"
|
||||||
|
integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad"
|
||||||
|
integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
|
||||||
|
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4"
|
||||||
|
integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a"
|
||||||
|
integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85"
|
||||||
|
integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
|
||||||
|
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
|
||||||
|
|
||||||
|
"@types/estree@1.0.5":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||||
|
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||||
|
|
||||||
|
esbuild@^0.19.3:
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96"
|
||||||
|
integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==
|
||||||
|
optionalDependencies:
|
||||||
|
"@esbuild/aix-ppc64" "0.19.11"
|
||||||
|
"@esbuild/android-arm" "0.19.11"
|
||||||
|
"@esbuild/android-arm64" "0.19.11"
|
||||||
|
"@esbuild/android-x64" "0.19.11"
|
||||||
|
"@esbuild/darwin-arm64" "0.19.11"
|
||||||
|
"@esbuild/darwin-x64" "0.19.11"
|
||||||
|
"@esbuild/freebsd-arm64" "0.19.11"
|
||||||
|
"@esbuild/freebsd-x64" "0.19.11"
|
||||||
|
"@esbuild/linux-arm" "0.19.11"
|
||||||
|
"@esbuild/linux-arm64" "0.19.11"
|
||||||
|
"@esbuild/linux-ia32" "0.19.11"
|
||||||
|
"@esbuild/linux-loong64" "0.19.11"
|
||||||
|
"@esbuild/linux-mips64el" "0.19.11"
|
||||||
|
"@esbuild/linux-ppc64" "0.19.11"
|
||||||
|
"@esbuild/linux-riscv64" "0.19.11"
|
||||||
|
"@esbuild/linux-s390x" "0.19.11"
|
||||||
|
"@esbuild/linux-x64" "0.19.11"
|
||||||
|
"@esbuild/netbsd-x64" "0.19.11"
|
||||||
|
"@esbuild/openbsd-x64" "0.19.11"
|
||||||
|
"@esbuild/sunos-x64" "0.19.11"
|
||||||
|
"@esbuild/win32-arm64" "0.19.11"
|
||||||
|
"@esbuild/win32-ia32" "0.19.11"
|
||||||
|
"@esbuild/win32-x64" "0.19.11"
|
||||||
|
|
||||||
|
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
|
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||||
|
|
||||||
|
"js-tokens@^3.0.0 || ^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
loose-envify@^1.1.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
dependencies:
|
||||||
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
nanoid@^3.3.7:
|
||||||
|
version "3.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
|
picocolors@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
|
postcss@^8.4.32:
|
||||||
|
version "8.4.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
|
||||||
|
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.7"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
|
react-dom@18.2.0:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react@18.2.0:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
rollup@^4.2.0:
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05"
|
||||||
|
integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "1.0.5"
|
||||||
|
optionalDependencies:
|
||||||
|
"@rollup/rollup-android-arm-eabi" "4.9.5"
|
||||||
|
"@rollup/rollup-android-arm64" "4.9.5"
|
||||||
|
"@rollup/rollup-darwin-arm64" "4.9.5"
|
||||||
|
"@rollup/rollup-darwin-x64" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-arm64-gnu" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-arm64-musl" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-x64-gnu" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-x64-musl" "4.9.5"
|
||||||
|
"@rollup/rollup-win32-arm64-msvc" "4.9.5"
|
||||||
|
"@rollup/rollup-win32-ia32-msvc" "4.9.5"
|
||||||
|
"@rollup/rollup-win32-x64-msvc" "4.9.5"
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
scheduler@^0.23.0:
|
||||||
|
version "0.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||||
|
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
source-map-js@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
|
vite@5.0.6:
|
||||||
|
version "5.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c"
|
||||||
|
integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==
|
||||||
|
dependencies:
|
||||||
|
esbuild "^0.19.3"
|
||||||
|
postcss "^8.4.32"
|
||||||
|
rollup "^4.2.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.3"
|
||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
|
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
|
||||||
import { t } from "../packages/excalidraw/i18n";
|
import { t } from "../packages/excalidraw/i18n";
|
||||||
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
|
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
|
||||||
import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
|
||||||
import { UIAppState } from "../packages/excalidraw/types";
|
import type { UIAppState } from "../packages/excalidraw/types";
|
||||||
|
import { Stats } from "../packages/excalidraw";
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
|
|
||||||
@@ -51,39 +52,33 @@ const CustomStats = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stats.StatsRows order={-1}>
|
||||||
<tr>
|
<Stats.StatsRow heading>{t("stats.version")}</Stats.StatsRow>
|
||||||
<th colSpan={2}>{t("stats.storage")}</th>
|
<Stats.StatsRow
|
||||||
</tr>
|
style={{ textAlign: "center", cursor: "pointer" }}
|
||||||
<tr>
|
onClick={async () => {
|
||||||
<td>{t("stats.scene")}</td>
|
try {
|
||||||
<td>{nFormatter(storageSizes.scene, 1)}</td>
|
await copyTextToSystemClipboard(getVersion());
|
||||||
</tr>
|
props.setToast(t("toast.copyToClipboard"));
|
||||||
<tr>
|
} catch {}
|
||||||
<td>{t("stats.total")}</td>
|
}}
|
||||||
<td>{nFormatter(storageSizes.total, 1)}</td>
|
title={t("stats.versionCopy")}
|
||||||
</tr>
|
>
|
||||||
<tr>
|
{timestamp}
|
||||||
<th colSpan={2}>{t("stats.version")}</th>
|
<br />
|
||||||
</tr>
|
{hash}
|
||||||
<tr>
|
</Stats.StatsRow>
|
||||||
<td
|
|
||||||
colSpan={2}
|
<Stats.StatsRow heading>{t("stats.storage")}</Stats.StatsRow>
|
||||||
style={{ textAlign: "center", cursor: "pointer" }}
|
<Stats.StatsRow columns={2}>
|
||||||
onClick={async () => {
|
<div>{t("stats.scene")}</div>
|
||||||
try {
|
<div>{nFormatter(storageSizes.scene, 1)}</div>
|
||||||
await copyTextToSystemClipboard(getVersion());
|
</Stats.StatsRow>
|
||||||
props.setToast(t("toast.copyToClipboard"));
|
<Stats.StatsRow columns={2}>
|
||||||
} catch {}
|
<div>{t("stats.total")}</div>
|
||||||
}}
|
<div>{nFormatter(storageSizes.total, 1)}</div>
|
||||||
title={t("stats.versionCopy")}
|
</Stats.StatsRow>
|
||||||
>
|
</Stats.StatsRows>
|
||||||
{timestamp}
|
|
||||||
<br />
|
|
||||||
{hash}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import { useLayoutEffect, useRef } from "react";
|
||||||
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
import { LocalData } from "./data/LocalData";
|
||||||
|
import type {
|
||||||
|
FileId,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "../packages/excalidraw/element/types";
|
||||||
|
import type { AppState, BinaryFileData } from "../packages/excalidraw/types";
|
||||||
|
import { ExcalidrawError } from "../packages/excalidraw/errors";
|
||||||
|
import { base64urlToString } from "../packages/excalidraw/data/encode";
|
||||||
|
|
||||||
|
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
||||||
|
|
||||||
|
const EXCALIDRAW_PLUS_ORIGIN = import.meta.env.VITE_APP_PLUS_APP;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// outgoing message
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
type MESSAGE_REQUEST_SCENE = {
|
||||||
|
type: "REQUEST_SCENE";
|
||||||
|
jwt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MESSAGE_FROM_PLUS = MESSAGE_REQUEST_SCENE;
|
||||||
|
|
||||||
|
// incoming messages
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
type MESSAGE_READY = { type: "READY" };
|
||||||
|
type MESSAGE_ERROR = { type: "ERROR"; message: string };
|
||||||
|
type MESSAGE_SCENE_DATA = {
|
||||||
|
type: "SCENE_DATA";
|
||||||
|
elements: OrderedExcalidrawElement[];
|
||||||
|
appState: Pick<AppState, "viewBackgroundColor">;
|
||||||
|
files: { loadedFiles: BinaryFileData[]; erroredFiles: Map<FileId, true> };
|
||||||
|
};
|
||||||
|
|
||||||
|
type MESSAGE_FROM_EDITOR = MESSAGE_ERROR | MESSAGE_SCENE_DATA | MESSAGE_READY;
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const parseSceneData = async ({
|
||||||
|
rawElementsString,
|
||||||
|
rawAppStateString,
|
||||||
|
}: {
|
||||||
|
rawElementsString: string | null;
|
||||||
|
rawAppStateString: string | null;
|
||||||
|
}): Promise<MESSAGE_SCENE_DATA> => {
|
||||||
|
if (!rawElementsString || !rawAppStateString) {
|
||||||
|
throw new ExcalidrawError("Elements or appstate is missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const elements = JSON.parse(
|
||||||
|
rawElementsString,
|
||||||
|
) as OrderedExcalidrawElement[];
|
||||||
|
|
||||||
|
if (!elements.length) {
|
||||||
|
throw new ExcalidrawError("Scene is empty, nothing to export.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const appState = JSON.parse(rawAppStateString) as Pick<
|
||||||
|
AppState,
|
||||||
|
"viewBackgroundColor"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const fileIds = elements.reduce((acc, el) => {
|
||||||
|
if ("fileId" in el && el.fileId) {
|
||||||
|
acc.push(el.fileId);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as FileId[]);
|
||||||
|
|
||||||
|
const files = await LocalData.fileStorage.getFiles(fileIds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "SCENE_DATA",
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw error instanceof ExcalidrawError
|
||||||
|
? error
|
||||||
|
: new ExcalidrawError("Failed to parse scene data.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyJWT = async ({
|
||||||
|
token,
|
||||||
|
publicKey,
|
||||||
|
}: {
|
||||||
|
token: string;
|
||||||
|
publicKey: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
if (!publicKey) {
|
||||||
|
throw new ExcalidrawError("Public key is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [header, payload, signature] = token.split(".");
|
||||||
|
|
||||||
|
if (!header || !payload || !signature) {
|
||||||
|
throw new ExcalidrawError("Invalid JWT format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT is using Base64URL encoding
|
||||||
|
const decodedPayload = base64urlToString(payload);
|
||||||
|
const decodedSignature = base64urlToString(signature);
|
||||||
|
|
||||||
|
const data = `${header}.${payload}`;
|
||||||
|
const signatureArrayBuffer = Uint8Array.from(decodedSignature, (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyData = publicKey.replace(/-----\w+ PUBLIC KEY-----/g, "");
|
||||||
|
const keyArrayBuffer = Uint8Array.from(atob(keyData), (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"spki",
|
||||||
|
keyArrayBuffer,
|
||||||
|
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||||
|
true,
|
||||||
|
["verify"],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValid = await crypto.subtle.verify(
|
||||||
|
"RSASSA-PKCS1-v1_5",
|
||||||
|
key,
|
||||||
|
signatureArrayBuffer,
|
||||||
|
new TextEncoder().encode(data),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error("Invalid JWT");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = JSON.parse(decodedPayload);
|
||||||
|
|
||||||
|
// Check for expiration
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
if (parsedPayload.exp && parsedPayload.exp < currentTime) {
|
||||||
|
throw new Error("JWT has expired");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to verify JWT:", error);
|
||||||
|
throw new Error(error instanceof Error ? error.message : "Invalid JWT");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExcalidrawPlusIframeExport = () => {
|
||||||
|
const readyRef = useRef(false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const handleMessage = async (event: MessageEvent<MESSAGE_FROM_PLUS>) => {
|
||||||
|
if (event.origin !== EXCALIDRAW_PLUS_ORIGIN) {
|
||||||
|
throw new ExcalidrawError("Invalid origin");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.type === EVENT_REQUEST_SCENE) {
|
||||||
|
if (!event.data.jwt) {
|
||||||
|
throw new ExcalidrawError("JWT is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await verifyJWT({
|
||||||
|
token: event.data.jwt,
|
||||||
|
publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Failed to verify JWT: ${error.message}`);
|
||||||
|
throw new ExcalidrawError("Failed to verify JWT");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedSceneData: MESSAGE_SCENE_DATA = await parseSceneData({
|
||||||
|
rawAppStateString: localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
|
),
|
||||||
|
rawElementsString: localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
event.source!.postMessage(parsedSceneData, {
|
||||||
|
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const responseData: MESSAGE_ERROR = {
|
||||||
|
type: "ERROR",
|
||||||
|
message:
|
||||||
|
error instanceof ExcalidrawError
|
||||||
|
? error.message
|
||||||
|
: "Failed to export scene data",
|
||||||
|
};
|
||||||
|
event.source!.postMessage(responseData, {
|
||||||
|
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handleMessage);
|
||||||
|
|
||||||
|
// so we don't send twice in StrictMode
|
||||||
|
if (!readyRef.current) {
|
||||||
|
readyRef.current = true;
|
||||||
|
const message: MESSAGE_FROM_EDITOR = { type: "READY" };
|
||||||
|
window.parent.postMessage(message, EXCALIDRAW_PLUS_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Since this component is expected to run in a hidden iframe on Excaildraw+,
|
||||||
|
// it doesn't need to render anything. All the data we need is available in
|
||||||
|
// LocalStorage and IndexedDB. It only needs to handle the messaging between
|
||||||
|
// the parent window and the iframe with the relevant data.
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { appLangCodeAtom } from "../App";
|
import { useI18n, languages } from "../../packages/excalidraw/i18n";
|
||||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
import { appLangCodeAtom } from "./language-state";
|
||||||
import { languages } from "../../packages/excalidraw/i18n";
|
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
const { t, langCode } = useI18n();
|
const { t, langCode } = useI18n();
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import { defaultLang, languages } from "../../packages/excalidraw";
|
||||||
|
|
||||||
|
export const languageDetector = new LanguageDetector();
|
||||||
|
|
||||||
|
languageDetector.init({
|
||||||
|
languageUtils: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getPreferredLanguage = () => {
|
||||||
|
const detectedLanguages = languageDetector.detect();
|
||||||
|
|
||||||
|
const detectedLanguage = Array.isArray(detectedLanguages)
|
||||||
|
? detectedLanguages[0]
|
||||||
|
: detectedLanguages;
|
||||||
|
|
||||||
|
const initialLanguage =
|
||||||
|
(detectedLanguage
|
||||||
|
? // region code may not be defined if user uses generic preferred language
|
||||||
|
// (e.g. chinese vs instead of chinese-simplified)
|
||||||
|
languages.find((lang) => lang.code.startsWith(detectedLanguage))?.code
|
||||||
|
: null) || defaultLang.code;
|
||||||
|
|
||||||
|
return initialLanguage;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
||||||
|
|
||||||
|
export const appLangCodeAtom = atom(getPreferredLanguage());
|
||||||
|
|
||||||
|
export const useAppLangCode = () => {
|
||||||
|
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
languageDetector.cacheUserLanguage(langCode);
|
||||||
|
}, [langCode]);
|
||||||
|
|
||||||
|
return [langCode, setLangCode] as const;
|
||||||
|
};
|
||||||
@@ -39,10 +39,15 @@ 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",
|
||||||
|
LOCAL_STORAGE_DEBUG: "excalidraw-debug",
|
||||||
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,22 +1,27 @@
|
|||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { PureComponent } from "react";
|
import { PureComponent } from "react";
|
||||||
import {
|
import type {
|
||||||
|
BinaryFileData,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
SocketId,
|
SocketId,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
||||||
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import {
|
import {
|
||||||
|
StoreAction,
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
zoomToFitBounds,
|
zoomToFitBounds,
|
||||||
} from "../../packages/excalidraw/index";
|
reconcileElements,
|
||||||
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw";
|
||||||
|
import type { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
assertNever,
|
assertNever,
|
||||||
preventUnload,
|
preventUnload,
|
||||||
@@ -33,13 +38,14 @@ import {
|
|||||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
WS_EVENTS,
|
WS_EVENTS,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
|
import type {
|
||||||
|
SocketUpdateDataSource,
|
||||||
|
SyncableExcalidrawElement,
|
||||||
|
} from "../data";
|
||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
getCollabServer,
|
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
SocketUpdateDataSource,
|
|
||||||
SyncableExcalidrawElement,
|
|
||||||
} from "../data";
|
} from "../data";
|
||||||
import {
|
import {
|
||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
@@ -53,7 +59,6 @@ import {
|
|||||||
saveUsernameToLocalStorage,
|
saveUsernameToLocalStorage,
|
||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
import Portal from "./Portal";
|
import Portal from "./Portal";
|
||||||
import RoomDialog from "./RoomDialog";
|
|
||||||
import { t } from "../../packages/excalidraw/i18n";
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
import { UserIdleState } from "../../packages/excalidraw/types";
|
import { UserIdleState } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
@@ -71,30 +76,34 @@ import {
|
|||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
} from "../../packages/excalidraw/element/typeChecks";
|
} from "../../packages/excalidraw/element/typeChecks";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
import {
|
|
||||||
ReconciledElements,
|
|
||||||
reconcileElements as _reconcileElements,
|
|
||||||
} from "./reconciliation";
|
|
||||||
import { decryptData } from "../../packages/excalidraw/data/encryption";
|
import { decryptData } from "../../packages/excalidraw/data/encryption";
|
||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { appJotaiStore } from "../app-jotai";
|
import { appJotaiStore } from "../app-jotai";
|
||||||
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
import type { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
||||||
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
||||||
|
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||||
|
import type {
|
||||||
|
ReconciledExcalidrawElement,
|
||||||
|
RemoteExcalidrawElement,
|
||||||
|
} from "../../packages/excalidraw/data/reconcile";
|
||||||
|
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const collabDialogShownAtom = atom(false);
|
|
||||||
export const isCollaboratingAtom = atom(false);
|
export const isCollaboratingAtom = atom(false);
|
||||||
export const isOfflineAtom = atom(false);
|
export const isOfflineAtom = atom(false);
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
errorMessage: string;
|
errorMessage: string | null;
|
||||||
|
/** errors related to saving */
|
||||||
|
dialogNotifiedErrors: Record<string, boolean>;
|
||||||
username: string;
|
username: string;
|
||||||
activeRoomLink: string;
|
activeRoomLink: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const activeRoomLinkAtom = atom<string | null>(null);
|
||||||
|
|
||||||
type CollabInstance = InstanceType<typeof Collab>;
|
type CollabInstance = InstanceType<typeof Collab>;
|
||||||
|
|
||||||
export interface CollabAPI {
|
export interface CollabAPI {
|
||||||
@@ -105,19 +114,20 @@ export interface CollabAPI {
|
|||||||
stopCollaboration: CollabInstance["stopCollaboration"];
|
stopCollaboration: CollabInstance["stopCollaboration"];
|
||||||
syncElements: CollabInstance["syncElements"];
|
syncElements: CollabInstance["syncElements"];
|
||||||
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
||||||
setUsername: (username: string) => void;
|
setUsername: CollabInstance["setUsername"];
|
||||||
|
getUsername: CollabInstance["getUsername"];
|
||||||
|
getActiveRoomLink: CollabInstance["getActiveRoomLink"];
|
||||||
|
setCollabError: CollabInstance["setErrorDialog"];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublicProps {
|
interface CollabProps {
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = PublicProps & { modalIsShown: boolean };
|
class Collab extends PureComponent<CollabProps, CollabState> {
|
||||||
|
|
||||||
class Collab extends PureComponent<Props, CollabState> {
|
|
||||||
portal: Portal;
|
portal: Portal;
|
||||||
fileManager: FileManager;
|
fileManager: FileManager;
|
||||||
excalidrawAPI: Props["excalidrawAPI"];
|
excalidrawAPI: CollabProps["excalidrawAPI"];
|
||||||
activeIntervalId: number | null;
|
activeIntervalId: number | null;
|
||||||
idleTimeoutId: number | null;
|
idleTimeoutId: number | null;
|
||||||
|
|
||||||
@@ -125,12 +135,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||||
private collaborators = new Map<SocketId, Collaborator>();
|
private collaborators = new Map<SocketId, Collaborator>();
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: CollabProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
errorMessage: "",
|
errorMessage: null,
|
||||||
|
dialogNotifiedErrors: {},
|
||||||
username: importUsernameFromLocalStorage() || "",
|
username: importUsernameFromLocalStorage() || "",
|
||||||
activeRoomLink: "",
|
activeRoomLink: null,
|
||||||
};
|
};
|
||||||
this.portal = new Portal(this);
|
this.portal = new Portal(this);
|
||||||
this.fileManager = new FileManager({
|
this.fileManager = new FileManager({
|
||||||
@@ -148,7 +159,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
throw new AbortError();
|
throw new AbortError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveFilesToFirebase({
|
const { savedFiles, erroredFiles } = await saveFilesToFirebase({
|
||||||
prefix: `${FIREBASE_STORAGE_PREFIXES.collabFiles}/${roomId}`,
|
prefix: `${FIREBASE_STORAGE_PREFIXES.collabFiles}/${roomId}`,
|
||||||
files: await encodeFilesForUpload({
|
files: await encodeFilesForUpload({
|
||||||
files: addedFiles,
|
files: addedFiles,
|
||||||
@@ -156,6 +167,29 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
maxBytes: FILE_UPLOAD_MAX_BYTES,
|
maxBytes: FILE_UPLOAD_MAX_BYTES,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
savedFiles: savedFiles.reduce(
|
||||||
|
(acc: Map<FileId, BinaryFileData>, id) => {
|
||||||
|
const fileData = addedFiles.get(id);
|
||||||
|
if (fileData) {
|
||||||
|
acc.set(id, fileData);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
new Map(),
|
||||||
|
),
|
||||||
|
erroredFiles: erroredFiles.reduce(
|
||||||
|
(acc: Map<FileId, BinaryFileData>, id) => {
|
||||||
|
const fileData = addedFiles.get(id);
|
||||||
|
if (fileData) {
|
||||||
|
acc.set(id, fileData);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
new Map(),
|
||||||
|
),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.excalidrawAPI = props.excalidrawAPI;
|
this.excalidrawAPI = props.excalidrawAPI;
|
||||||
@@ -195,6 +229,9 @@ class Collab extends PureComponent<Props, 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);
|
||||||
@@ -267,24 +304,39 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
syncableElements: readonly SyncableExcalidrawElement[],
|
syncableElements: readonly SyncableExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const savedData = await saveToFirebase(
|
const storedElements = await saveToFirebase(
|
||||||
this.portal,
|
this.portal,
|
||||||
syncableElements,
|
syncableElements,
|
||||||
this.excalidrawAPI.getAppState(),
|
this.excalidrawAPI.getAppState(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.isCollaborating() && savedData && savedData.reconciledElements) {
|
this.resetErrorIndicator();
|
||||||
this.handleRemoteSceneUpdate(
|
|
||||||
this.reconcileElements(savedData.reconciledElements),
|
if (this.isCollaborating() && storedElements) {
|
||||||
);
|
this.handleRemoteSceneUpdate(this._reconcileElements(storedElements));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.setState({
|
const errorMessage = /is longer than.*?bytes/.test(error.message)
|
||||||
// firestore doesn't return a specific error code when size exceeded
|
? t("errors.collabSaveFailed_sizeExceeded")
|
||||||
errorMessage: /is longer than.*?bytes/.test(error.message)
|
: t("errors.collabSaveFailed");
|
||||||
? t("errors.collabSaveFailed_sizeExceeded")
|
|
||||||
: t("errors.collabSaveFailed"),
|
if (
|
||||||
});
|
!this.state.dialogNotifiedErrors[errorMessage] ||
|
||||||
|
!this.isCollaborating()
|
||||||
|
) {
|
||||||
|
this.setErrorDialog(errorMessage);
|
||||||
|
this.setState({
|
||||||
|
dialogNotifiedErrors: {
|
||||||
|
...this.state.dialogNotifiedErrors,
|
||||||
|
[errorMessage]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isCollaborating()) {
|
||||||
|
this.setErrorIndicator(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -293,6 +345,7 @@ class Collab extends PureComponent<Props, 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(
|
||||||
@@ -331,7 +384,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
commitToHistory: false,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -342,9 +395,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
this.fileManager.reset();
|
this.fileManager.reset();
|
||||||
if (!opts?.isUnload) {
|
if (!opts?.isUnload) {
|
||||||
this.setIsCollaborating(false);
|
this.setIsCollaborating(false);
|
||||||
this.setState({
|
this.setActiveRoomLink(null);
|
||||||
activeRoomLink: "",
|
|
||||||
});
|
|
||||||
this.collaborators = new Map();
|
this.collaborators = new Map();
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
collaborators: this.collaborators,
|
collaborators: this.collaborators,
|
||||||
@@ -368,7 +419,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
.filter((element) => {
|
.filter((element) => {
|
||||||
return (
|
return (
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
!this.fileManager.isFileHandled(element.fileId) &&
|
!this.fileManager.isFileTracked(element.fileId) &&
|
||||||
!element.isDeleted &&
|
!element.isDeleted &&
|
||||||
(opts.forceFetchFiles
|
(opts.forceFetchFiles
|
||||||
? element.status !== "pending" ||
|
? element.status !== "pending" ||
|
||||||
@@ -406,11 +457,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
startCollaboration = async (
|
startCollaboration = async (
|
||||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||||
): Promise<ImportedDataState | null> => {
|
) => {
|
||||||
if (!this.state.username) {
|
if (!this.state.username) {
|
||||||
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
||||||
const username = getRandomUsername();
|
const username = getRandomUsername();
|
||||||
this.onUsernameChange(username);
|
this.setUsername(username);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +483,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
// TODO: `ImportedDataState` type here seems abused
|
||||||
|
const scenePromise = resolvablePromise<
|
||||||
|
| (ImportedDataState & { elements: readonly OrderedExcalidrawElement[] })
|
||||||
|
| null
|
||||||
|
>();
|
||||||
|
|
||||||
this.setIsCollaborating(true);
|
this.setIsCollaborating(true);
|
||||||
LocalData.pauseSave("collaboration");
|
LocalData.pauseSave("collaboration");
|
||||||
@@ -452,13 +507,9 @@ class Collab extends PureComponent<Props, 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(socketServerData.url, {
|
socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
||||||
transports: socketServerData.polling
|
transports: ["websocket", "polling"],
|
||||||
? ["websocket", "polling"]
|
|
||||||
: ["websocket"],
|
|
||||||
}),
|
}),
|
||||||
roomId,
|
roomId,
|
||||||
roomKey,
|
roomKey,
|
||||||
@@ -467,7 +518,7 @@ class Collab extends PureComponent<Props, 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.setState({ errorMessage: error.message });
|
this.setErrorDialog(error.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,14 +529,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
});
|
});
|
||||||
// remove deleted elements from elements array & history to ensure we don't
|
// remove deleted elements from elements array to ensure we don't
|
||||||
// expose potentially sensitive user data in case user manually deletes
|
// expose potentially sensitive user data in case user manually deletes
|
||||||
// existing elements (or clears scene), which would otherwise be persisted
|
// existing elements (or clears scene), which would otherwise be persisted
|
||||||
// to database even if deleted before creating the room.
|
// to database even if deleted before creating the room.
|
||||||
this.excalidrawAPI.history.clear();
|
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
commitToHistory: true,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
||||||
@@ -519,10 +569,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
if (!this.portal.socketInitialized) {
|
if (!this.portal.socketInitialized) {
|
||||||
this.initializeRoom({ fetchScene: false });
|
this.initializeRoom({ fetchScene: false });
|
||||||
const remoteElements = decryptedData.payload.elements;
|
const remoteElements = decryptedData.payload.elements;
|
||||||
const reconciledElements = this.reconcileElements(remoteElements);
|
const reconciledElements =
|
||||||
this.handleRemoteSceneUpdate(reconciledElements, {
|
this._reconcileElements(remoteElements);
|
||||||
init: true,
|
this.handleRemoteSceneUpdate(reconciledElements);
|
||||||
});
|
|
||||||
// noop if already resolved via init from firebase
|
// noop if already resolved via init from firebase
|
||||||
scenePromise.resolve({
|
scenePromise.resolve({
|
||||||
elements: reconciledElements,
|
elements: reconciledElements,
|
||||||
@@ -533,7 +582,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
case WS_SUBTYPES.UPDATE:
|
case WS_SUBTYPES.UPDATE:
|
||||||
this.handleRemoteSceneUpdate(
|
this.handleRemoteSceneUpdate(
|
||||||
this.reconcileElements(decryptedData.payload.elements),
|
this._reconcileElements(decryptedData.payload.elements),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case WS_SUBTYPES.MOUSE_LOCATION: {
|
case WS_SUBTYPES.MOUSE_LOCATION: {
|
||||||
@@ -629,9 +678,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
this.initializeIdleDetector();
|
this.initializeIdleDetector();
|
||||||
|
|
||||||
this.setState({
|
this.setActiveRoomLink(window.location.href);
|
||||||
activeRoomLink: window.location.href,
|
|
||||||
});
|
|
||||||
|
|
||||||
return scenePromise;
|
return scenePromise;
|
||||||
};
|
};
|
||||||
@@ -683,17 +730,15 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private reconcileElements = (
|
private _reconcileElements = (
|
||||||
remoteElements: readonly ExcalidrawElement[],
|
remoteElements: readonly ExcalidrawElement[],
|
||||||
): ReconciledElements => {
|
): ReconciledExcalidrawElement[] => {
|
||||||
const localElements = this.getSceneElementsIncludingDeleted();
|
const localElements = this.getSceneElementsIncludingDeleted();
|
||||||
const appState = this.excalidrawAPI.getAppState();
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
|
const restoredRemoteElements = restoreElements(remoteElements, null);
|
||||||
remoteElements = restoreElements(remoteElements, null);
|
const reconciledElements = reconcileElements(
|
||||||
|
|
||||||
const reconciledElements = _reconcileElements(
|
|
||||||
localElements,
|
localElements,
|
||||||
remoteElements,
|
restoredRemoteElements as RemoteExcalidrawElement[],
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -724,20 +769,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}, LOAD_IMAGES_TIMEOUT);
|
}, LOAD_IMAGES_TIMEOUT);
|
||||||
|
|
||||||
private handleRemoteSceneUpdate = (
|
private handleRemoteSceneUpdate = (
|
||||||
elements: ReconciledElements,
|
elements: ReconciledExcalidrawElement[],
|
||||||
{ init = false }: { init?: boolean } = {},
|
|
||||||
) => {
|
) => {
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
commitToHistory: !!init,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
// We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
|
|
||||||
// when we receive any messages from another peer. This UX can be pretty rough -- if you
|
|
||||||
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
|
|
||||||
// right now we think this is the right tradeoff.
|
|
||||||
this.excalidrawAPI.history.clear();
|
|
||||||
|
|
||||||
this.loadImageFiles();
|
this.loadImageFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -870,7 +908,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
this.portal.broadcastIdleChange(userState);
|
this.portal.broadcastIdleChange(userState);
|
||||||
};
|
};
|
||||||
|
|
||||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
broadcastElements = (elements: readonly OrderedExcalidrawElement[]) => {
|
||||||
if (
|
if (
|
||||||
getSceneVersion(elements) >
|
getSceneVersion(elements) >
|
||||||
this.getLastBroadcastedOrReceivedSceneVersion()
|
this.getLastBroadcastedOrReceivedSceneVersion()
|
||||||
@@ -881,7 +919,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
syncElements = (elements: readonly ExcalidrawElement[]) => {
|
syncElements = (elements: readonly OrderedExcalidrawElement[]) => {
|
||||||
this.broadcastElements(elements);
|
this.broadcastElements(elements);
|
||||||
this.queueSaveToFirebase();
|
this.queueSaveToFirebase();
|
||||||
};
|
};
|
||||||
@@ -914,41 +952,49 @@ class Collab extends PureComponent<Props, 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
getUsername = () => this.state.username;
|
||||||
const { username, errorMessage, activeRoomLink } = this.state;
|
|
||||||
|
|
||||||
const { modalIsShown } = this.props;
|
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() {
|
||||||
|
const { errorMessage } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{modalIsShown && (
|
{errorMessage != null && (
|
||||||
<RoomDialog
|
<ErrorDialog onClose={() => this.setErrorDialog(null)}>
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
@@ -967,11 +1013,6 @@ 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"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _Collab: React.FC<PublicProps> = (props) => {
|
export default Collab;
|
||||||
const [collabDialogShown] = useAtom(collabDialogShownAtom);
|
|
||||||
return <Collab {...props} modalIsShown={collabDialogShown} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default _Collab;
|
|
||||||
|
|
||||||
export type TCollabClass = Collab;
|
export type TCollabClass = Collab;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@import "../../packages/excalidraw/css/variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.collab-errors-button {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin-inline-end: 1rem;
|
||||||
|
|
||||||
|
color: var(--color-danger);
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collab-errors-button-shake {
|
||||||
|
animation: strong-shake 0.15s 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes strong-shake {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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;
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import {
|
import type {
|
||||||
isSyncableElement,
|
|
||||||
SocketUpdateData,
|
SocketUpdateData,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
|
SyncableExcalidrawElement,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
|
import { isSyncableElement } from "../data";
|
||||||
|
|
||||||
import { TCollabClass } from "./Collab";
|
import type { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
import type { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||||
import {
|
import type {
|
||||||
OnUserFollowedPayload,
|
OnUserFollowedPayload,
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
@@ -16,10 +17,9 @@ import {
|
|||||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
|
||||||
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
||||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
|
import { StoreAction } from "../../packages/excalidraw";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: TCollabClass;
|
collab: TCollabClass;
|
||||||
@@ -116,24 +116,31 @@ class Portal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.collab.excalidrawAPI.updateScene({
|
let isChanged = false;
|
||||||
elements: this.collab.excalidrawAPI
|
const newElements = this.collab.excalidrawAPI
|
||||||
.getSceneElementsIncludingDeleted()
|
.getSceneElementsIncludingDeleted()
|
||||||
.map((element) => {
|
.map((element) => {
|
||||||
if (this.collab.fileManager.shouldUpdateImageElementStatus(element)) {
|
if (this.collab.fileManager.shouldUpdateImageElementStatus(element)) {
|
||||||
// this will signal collaborators to pull image data from server
|
isChanged = true;
|
||||||
// (using mutation instead of newElementWith otherwise it'd break
|
// this will signal collaborators to pull image data from server
|
||||||
// in-progress dragging)
|
// (using mutation instead of newElementWith otherwise it'd break
|
||||||
return newElementWith(element, { status: "saved" });
|
// in-progress dragging)
|
||||||
}
|
return newElementWith(element, { status: "saved" });
|
||||||
return element;
|
}
|
||||||
}),
|
return element;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isChanged) {
|
||||||
|
this.collab.excalidrawAPI.updateScene({
|
||||||
|
elements: newElements,
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
|
});
|
||||||
|
}
|
||||||
}, FILE_UPLOAD_TIMEOUT);
|
}, FILE_UPLOAD_TIMEOUT);
|
||||||
|
|
||||||
broadcastScene = async (
|
broadcastScene = async (
|
||||||
updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
|
updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
|
||||||
allElements: readonly ExcalidrawElement[],
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
syncAll: boolean,
|
syncAll: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (updateType === WS_SUBTYPES.INIT && !syncAll) {
|
if (updateType === WS_SUBTYPES.INIT && !syncAll) {
|
||||||
@@ -143,25 +150,17 @@ class Portal {
|
|||||||
// sync out only the elements we think we need to to save bandwidth.
|
// sync out only the elements we think we need to to save bandwidth.
|
||||||
// periodically we'll resync the whole thing to make sure no one diverges
|
// periodically we'll resync the whole thing to make sure no one diverges
|
||||||
// due to a dropped message (server goes down etc).
|
// due to a dropped message (server goes down etc).
|
||||||
const syncableElements = allElements.reduce(
|
const syncableElements = elements.reduce((acc, element) => {
|
||||||
(acc, element: BroadcastedExcalidrawElement, idx, elements) => {
|
if (
|
||||||
if (
|
(syncAll ||
|
||||||
(syncAll ||
|
!this.broadcastedElementVersions.has(element.id) ||
|
||||||
!this.broadcastedElementVersions.has(element.id) ||
|
element.version > this.broadcastedElementVersions.get(element.id)!) &&
|
||||||
element.version >
|
isSyncableElement(element)
|
||||||
this.broadcastedElementVersions.get(element.id)!) &&
|
) {
|
||||||
isSyncableElement(element)
|
acc.push(element);
|
||||||
) {
|
}
|
||||||
acc.push({
|
return acc;
|
||||||
...element,
|
}, [] as SyncableExcalidrawElement[]);
|
||||||
// z-index info for the reconciler
|
|
||||||
[PRECEDING_ELEMENT_KEY]: idx === 0 ? "^" : elements[idx - 1]?.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[] as BroadcastedExcalidrawElement[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const data: SocketUpdateDataSource[typeof updateType] = {
|
const data: SocketUpdateDataSource[typeof updateType] = {
|
||||||
type: updateType,
|
type: updateType,
|
||||||
|
|||||||
@@ -1,219 +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,
|
|
||||||
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 { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg";
|
|
||||||
import "./RoomDialog.scss";
|
|
||||||
|
|
||||||
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 RoomModalProps = {
|
|
||||||
handleClose: () => void;
|
|
||||||
activeRoomLink: string;
|
|
||||||
username: string;
|
|
||||||
onUsernameChange: (username: string) => void;
|
|
||||||
onRoomCreate: () => void;
|
|
||||||
onRoomDestroy: () => void;
|
|
||||||
setErrorMessage: (message: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RoomModal = ({
|
|
||||||
activeRoomLink,
|
|
||||||
onRoomCreate,
|
|
||||||
onRoomDestroy,
|
|
||||||
setErrorMessage,
|
|
||||||
username,
|
|
||||||
onUsernameChange,
|
|
||||||
handleClose,
|
|
||||||
}: RoomModalProps) => {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const [justCopied, setJustCopied] = useState(false);
|
|
||||||
const timerRef = useRef<number>(0);
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
|
||||||
const isShareSupported = "share" in navigator;
|
|
||||||
|
|
||||||
const copyRoomLink = async () => {
|
|
||||||
try {
|
|
||||||
await copyTextToSystemClipboard(activeRoomLink);
|
|
||||||
|
|
||||||
setJustCopied(true);
|
|
||||||
|
|
||||||
if (timerRef.current) {
|
|
||||||
window.clearTimeout(timerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
timerRef.current = window.setTimeout(() => {
|
|
||||||
setJustCopied(false);
|
|
||||||
}, 3000);
|
|
||||||
} catch (error: any) {
|
|
||||||
setErrorMessage(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.current?.select();
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareRoomLink = async () => {
|
|
||||||
try {
|
|
||||||
await navigator.share({
|
|
||||||
title: t("roomDialog.shareTitle"),
|
|
||||||
text: t("roomDialog.shareTitle"),
|
|
||||||
url: activeRoomLink,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
// Just ignore.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (activeRoomLink) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3 className="RoomDialog__active__header">
|
|
||||||
{t("labels.liveCollaboration")}
|
|
||||||
</h3>
|
|
||||||
<TextField
|
|
||||||
value={username}
|
|
||||||
placeholder="Your name"
|
|
||||||
label="Your name"
|
|
||||||
onChange={onUsernameChange}
|
|
||||||
onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
|
|
||||||
/>
|
|
||||||
<div className="RoomDialog__active__linkRow">
|
|
||||||
<TextField
|
|
||||||
ref={ref}
|
|
||||||
label="Link"
|
|
||||||
readonly
|
|
||||||
fullWidth
|
|
||||||
value={activeRoomLink}
|
|
||||||
/>
|
|
||||||
{isShareSupported && (
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
variant="icon"
|
|
||||||
label="Share"
|
|
||||||
startIcon={getShareIcon()}
|
|
||||||
className="RoomDialog__active__share"
|
|
||||||
onClick={shareRoomLink}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Popover.Root open={justCopied}>
|
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
label="Copy link"
|
|
||||||
startIcon={copyIcon}
|
|
||||||
onClick={copyRoomLink}
|
|
||||||
/>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Content
|
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
||||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
||||||
className="RoomDialog__popover"
|
|
||||||
side="top"
|
|
||||||
align="end"
|
|
||||||
sideOffset={5.5}
|
|
||||||
>
|
|
||||||
{tablerCheckIcon} copied
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
</div>
|
|
||||||
<div className="RoomDialog__active__description">
|
|
||||||
<p>
|
|
||||||
<span
|
|
||||||
role="img"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="RoomDialog__active__description__emoji"
|
|
||||||
>
|
|
||||||
🔒{" "}
|
|
||||||
</span>
|
|
||||||
{t("roomDialog.desc_privacy")}
|
|
||||||
</p>
|
|
||||||
<p>{t("roomDialog.desc_exitSession")}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="RoomDialog__active__actions">
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
variant="outlined"
|
|
||||||
color="danger"
|
|
||||||
label={t("roomDialog.button_stopSession")}
|
|
||||||
startIcon={playerStopFilledIcon}
|
|
||||||
onClick={() => {
|
|
||||||
trackEvent("share", "room closed");
|
|
||||||
onRoomDestroy();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="RoomDialog__inactive__illustration">
|
|
||||||
<CollabImage />
|
|
||||||
</div>
|
|
||||||
<div className="RoomDialog__inactive__header">
|
|
||||||
{t("labels.liveCollaboration")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="RoomDialog__inactive__description">
|
|
||||||
<strong>{t("roomDialog.desc_intro")}</strong>
|
|
||||||
{t("roomDialog.desc_privacy")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="RoomDialog__inactive__start_session">
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
label={t("roomDialog.button_startSession")}
|
|
||||||
startIcon={playerPlayIcon}
|
|
||||||
onClick={() => {
|
|
||||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
|
||||||
onRoomCreate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RoomDialog = (props: RoomModalProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog size="small" onCloseRequest={props.handleClose} title={false}>
|
|
||||||
<div className="RoomDialog">
|
|
||||||
<RoomModal {...props} />
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomDialog;
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
|
||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
|
||||||
import { AppState } from "../../packages/excalidraw/types";
|
|
||||||
import { arrayToMapWithIndex } from "../../packages/excalidraw/utils";
|
|
||||||
|
|
||||||
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
|
||||||
_brand: "reconciledElements";
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BroadcastedExcalidrawElement = ExcalidrawElement & {
|
|
||||||
[PRECEDING_ELEMENT_KEY]?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldDiscardRemoteElement = (
|
|
||||||
localAppState: AppState,
|
|
||||||
local: ExcalidrawElement | undefined,
|
|
||||||
remote: BroadcastedExcalidrawElement,
|
|
||||||
): boolean => {
|
|
||||||
if (
|
|
||||||
local &&
|
|
||||||
// local element is being edited
|
|
||||||
(local.id === localAppState.editingElement?.id ||
|
|
||||||
local.id === localAppState.resizingElement?.id ||
|
|
||||||
local.id === localAppState.draggingElement?.id ||
|
|
||||||
// local element is newer
|
|
||||||
local.version > remote.version ||
|
|
||||||
// resolve conflicting edits deterministically by taking the one with
|
|
||||||
// the lowest versionNonce
|
|
||||||
(local.version === remote.version &&
|
|
||||||
local.versionNonce < remote.versionNonce))
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reconcileElements = (
|
|
||||||
localElements: readonly ExcalidrawElement[],
|
|
||||||
remoteElements: readonly BroadcastedExcalidrawElement[],
|
|
||||||
localAppState: AppState,
|
|
||||||
): ReconciledElements => {
|
|
||||||
const localElementsData =
|
|
||||||
arrayToMapWithIndex<ExcalidrawElement>(localElements);
|
|
||||||
|
|
||||||
const reconciledElements: ExcalidrawElement[] = localElements.slice();
|
|
||||||
|
|
||||||
const duplicates = new WeakMap<ExcalidrawElement, true>();
|
|
||||||
|
|
||||||
let cursor = 0;
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
let remoteElementIdx = -1;
|
|
||||||
for (const remoteElement of remoteElements) {
|
|
||||||
remoteElementIdx++;
|
|
||||||
|
|
||||||
const local = localElementsData.get(remoteElement.id);
|
|
||||||
|
|
||||||
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
|
|
||||||
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
|
|
||||||
delete remoteElement[PRECEDING_ELEMENT_KEY];
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark duplicate for removal as it'll be replaced with the remote element
|
|
||||||
if (local) {
|
|
||||||
// Unless the remote and local elements are the same element in which case
|
|
||||||
// we need to keep it as we'd otherwise discard it from the resulting
|
|
||||||
// array.
|
|
||||||
if (local[0] === remoteElement) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
duplicates.set(local[0], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// parent may not be defined in case the remote client is running an older
|
|
||||||
// excalidraw version
|
|
||||||
const parent =
|
|
||||||
remoteElement[PRECEDING_ELEMENT_KEY] ||
|
|
||||||
remoteElements[remoteElementIdx - 1]?.id ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (parent != null) {
|
|
||||||
delete remoteElement[PRECEDING_ELEMENT_KEY];
|
|
||||||
|
|
||||||
// ^ indicates the element is the first in elements array
|
|
||||||
if (parent === "^") {
|
|
||||||
offset++;
|
|
||||||
if (cursor === 0) {
|
|
||||||
reconciledElements.unshift(remoteElement);
|
|
||||||
localElementsData.set(remoteElement.id, [
|
|
||||||
remoteElement,
|
|
||||||
cursor - offset,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
|
||||||
localElementsData.set(remoteElement.id, [
|
|
||||||
remoteElement,
|
|
||||||
cursor + 1 - offset,
|
|
||||||
]);
|
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let idx = localElementsData.has(parent)
|
|
||||||
? localElementsData.get(parent)![1]
|
|
||||||
: null;
|
|
||||||
if (idx != null) {
|
|
||||||
idx += offset;
|
|
||||||
}
|
|
||||||
if (idx != null && idx >= cursor) {
|
|
||||||
reconciledElements.splice(idx + 1, 0, remoteElement);
|
|
||||||
offset++;
|
|
||||||
localElementsData.set(remoteElement.id, [
|
|
||||||
remoteElement,
|
|
||||||
idx + 1 - offset,
|
|
||||||
]);
|
|
||||||
cursor = idx + 1;
|
|
||||||
} else if (idx != null) {
|
|
||||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
|
||||||
offset++;
|
|
||||||
localElementsData.set(remoteElement.id, [
|
|
||||||
remoteElement,
|
|
||||||
cursor + 1 - offset,
|
|
||||||
]);
|
|
||||||
cursor++;
|
|
||||||
} else {
|
|
||||||
reconciledElements.push(remoteElement);
|
|
||||||
localElementsData.set(remoteElement.id, [
|
|
||||||
remoteElement,
|
|
||||||
reconciledElements.length - 1 - offset,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// no parent z-index information, local element exists → replace in place
|
|
||||||
} else if (local) {
|
|
||||||
reconciledElements[local[1]] = remoteElement;
|
|
||||||
localElementsData.set(remoteElement.id, [remoteElement, local[1]]);
|
|
||||||
// otherwise push to the end
|
|
||||||
} else {
|
|
||||||
reconciledElements.push(remoteElement);
|
|
||||||
localElementsData.set(remoteElement.id, [
|
|
||||||
remoteElement,
|
|
||||||
reconciledElements.length - 1 - offset,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret: readonly ExcalidrawElement[] = reconciledElements.filter(
|
|
||||||
(element) => !duplicates.has(element),
|
|
||||||
);
|
|
||||||
|
|
||||||
return ret as ReconciledElements;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import type { ExcalidrawImperativeAPI } from "../../packages/excalidraw/types";
|
||||||
|
import {
|
||||||
|
DiagramToCodePlugin,
|
||||||
|
exportToBlob,
|
||||||
|
getTextFromElements,
|
||||||
|
MIME_TYPES,
|
||||||
|
TTDDialog,
|
||||||
|
} from "../../packages/excalidraw";
|
||||||
|
import { getDataURL } from "../../packages/excalidraw/data/blob";
|
||||||
|
import { safelyParseJSON } from "../../packages/excalidraw/utils";
|
||||||
|
|
||||||
|
export const AIComponents = ({
|
||||||
|
excalidrawAPI,
|
||||||
|
}: {
|
||||||
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DiagramToCodePlugin
|
||||||
|
generate={async ({ frame, children }) => {
|
||||||
|
const appState = excalidrawAPI.getAppState();
|
||||||
|
|
||||||
|
const blob = await exportToBlob({
|
||||||
|
data: {
|
||||||
|
elements: children,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
exportBackground: true,
|
||||||
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
|
},
|
||||||
|
files: excalidrawAPI.getFiles(),
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
exportingFrame: frame,
|
||||||
|
mimeType: MIME_TYPES.jpg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataURL = await getDataURL(blob);
|
||||||
|
|
||||||
|
const textFromFrameChildren = getTextFromElements(children);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${
|
||||||
|
import.meta.env.VITE_APP_AI_BACKEND
|
||||||
|
}/v1/ai/diagram-to-code/generate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
texts: textFromFrameChildren,
|
||||||
|
image: dataURL,
|
||||||
|
theme: appState.theme,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
const errorJSON = safelyParseJSON(text);
|
||||||
|
|
||||||
|
if (!errorJSON) {
|
||||||
|
throw new Error(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorJSON.statusCode === 429) {
|
||||||
|
return {
|
||||||
|
html: `<html>
|
||||||
|
<body style="margin: 0; text-align: center">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100vh; padding: 0 60px">
|
||||||
|
<div style="color:red">Too many requests today,</br>please try again tomorrow!</div>
|
||||||
|
</br>
|
||||||
|
</br>
|
||||||
|
<div>You can also try <a href="${
|
||||||
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorJSON.message || text);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { html } = await response.json();
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
throw new Error("Generation failed (invalid response)");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error("Generation failed (invalid response)");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,23 +3,27 @@ import { Footer } from "../../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";
|
||||||
|
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||||
|
|
||||||
export const AppFooter = React.memo(() => {
|
export const AppFooter = React.memo(
|
||||||
return (
|
({ onChange }: { onChange: () => void }) => {
|
||||||
<Footer>
|
return (
|
||||||
<div
|
<Footer>
|
||||||
style={{
|
<div
|
||||||
display: "flex",
|
style={{
|
||||||
gap: ".5rem",
|
display: "flex",
|
||||||
alignItems: "center",
|
gap: ".5rem",
|
||||||
}}
|
alignItems: "center",
|
||||||
>
|
}}
|
||||||
{isExcalidrawPlusSignedUser ? (
|
>
|
||||||
<ExcalidrawPlusAppLink />
|
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
||||||
) : (
|
{isExcalidrawPlusSignedUser ? (
|
||||||
<EncryptedIcon />
|
<ExcalidrawPlusAppLink />
|
||||||
)}
|
) : (
|
||||||
</div>
|
<EncryptedIcon />
|
||||||
</Footer>
|
)}
|
||||||
);
|
</div>
|
||||||
});
|
</Footer>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
|
import {
|
||||||
|
loginIcon,
|
||||||
|
ExcalLogo,
|
||||||
|
eyeIcon,
|
||||||
|
} from "../../packages/excalidraw/components/icons";
|
||||||
|
import type { Theme } from "../../packages/excalidraw/element/types";
|
||||||
import { MainMenu } from "../../packages/excalidraw/index";
|
import { MainMenu } from "../../packages/excalidraw/index";
|
||||||
import { LanguageList } from "./LanguageList";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
|
import { saveDebugState } from "./DebugCanvas";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
onCollabDialogOpen: () => any;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
isCollabEnabled: boolean;
|
isCollabEnabled: boolean;
|
||||||
|
theme: Theme | "system";
|
||||||
|
setTheme: (theme: Theme | "system") => void;
|
||||||
|
refresh: () => void;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
@@ -17,25 +27,56 @@ export const AppMainMenu: React.FC<{
|
|||||||
{props.isCollabEnabled && (
|
{props.isCollabEnabled && (
|
||||||
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||||
isCollaborating={props.isCollaborating}
|
isCollaborating={props.isCollaborating}
|
||||||
onSelect={() => props.setCollabDialogShown(true)}
|
onSelect={() => props.onCollabDialogOpen()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
||||||
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.ItemLink
|
<MainMenu.ItemLink
|
||||||
icon={PlusPromoIcon}
|
icon={ExcalLogo}
|
||||||
href={`${
|
href={`${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
||||||
className="ExcalidrawPlus"
|
className=""
|
||||||
>
|
>
|
||||||
Excalidraw+
|
Excalidraw+
|
||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
<MainMenu.DefaultItems.Socials />
|
<MainMenu.DefaultItems.Socials />
|
||||||
|
<MainMenu.ItemLink
|
||||||
|
icon={loginIcon}
|
||||||
|
href={`${import.meta.env.VITE_APP_PLUS_APP}${
|
||||||
|
isExcalidrawPlusSignedUser ? "" : "/sign-up"
|
||||||
|
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
|
||||||
|
className="highlighted"
|
||||||
|
>
|
||||||
|
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||||
|
</MainMenu.ItemLink>
|
||||||
|
{import.meta.env.DEV && (
|
||||||
|
<MainMenu.Item
|
||||||
|
icon={eyeIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.visualDebug) {
|
||||||
|
delete window.visualDebug;
|
||||||
|
saveDebugState({ enabled: false });
|
||||||
|
} else {
|
||||||
|
window.visualDebug = { data: [] };
|
||||||
|
saveDebugState({ enabled: true });
|
||||||
|
}
|
||||||
|
props?.refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Visual Debug
|
||||||
|
</MainMenu.Item>
|
||||||
|
)}
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.DefaultItems.ToggleTheme />
|
<MainMenu.DefaultItems.ToggleTheme
|
||||||
|
allowSystemTheme
|
||||||
|
theme={props.theme}
|
||||||
|
onSelect={props.setTheme}
|
||||||
|
/>
|
||||||
<MainMenu.ItemCustom>
|
<MainMenu.ItemCustom>
|
||||||
<LanguageList style={{ width: "100%" }} />
|
<LanguageList style={{ width: "100%" }} />
|
||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
|
import { loginIcon } from "../../packages/excalidraw/components/icons";
|
||||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { POINTER_EVENTS } from "../../packages/excalidraw/constants";
|
import { POINTER_EVENTS } from "../../packages/excalidraw/constants";
|
||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
onCollabDialogOpen: () => 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.setCollabDialogShown(true)}
|
onSelect={() => props.onCollabDialogOpen()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isExcalidrawPlusSignedUser && (
|
{!isExcalidrawPlusSignedUser && (
|
||||||
@@ -61,9 +61,9 @@ export const AppWelcomeScreen: React.FC<{
|
|||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
|
||||||
shortcut={null}
|
shortcut={null}
|
||||||
icon={PlusPromoIcon}
|
icon={loginIcon}
|
||||||
>
|
>
|
||||||
Try Excalidraw Plus!
|
Sign up
|
||||||
</WelcomeScreen.Center.MenuItemLink>
|
</WelcomeScreen.Center.MenuItemLink>
|
||||||
)}
|
)}
|
||||||
</WelcomeScreen.Center.Menu>
|
</WelcomeScreen.Center.Menu>
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||||
|
import { type AppState } from "../../packages/excalidraw/types";
|
||||||
|
import { throttleRAF } from "../../packages/excalidraw/utils";
|
||||||
|
import {
|
||||||
|
bootstrapCanvas,
|
||||||
|
getNormalizedCanvasDimensions,
|
||||||
|
} from "../../packages/excalidraw/renderer/helpers";
|
||||||
|
import type { DebugElement } from "../../packages/excalidraw/visualdebug";
|
||||||
|
import {
|
||||||
|
ArrowheadArrowIcon,
|
||||||
|
CloseIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "../../packages/excalidraw/components/icons";
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
import {
|
||||||
|
isLineSegment,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LineSegment,
|
||||||
|
} from "../../packages/math";
|
||||||
|
|
||||||
|
const renderLine = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
zoom: number,
|
||||||
|
segment: LineSegment<GlobalPoint>,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(segment[0][0] * zoom, segment[0][1] * zoom);
|
||||||
|
context.lineTo(segment[1][0] * zoom, segment[1][1] * zoom);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||||
|
context.strokeStyle = "#888";
|
||||||
|
context.save();
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(-10 * zoom, -10 * zoom);
|
||||||
|
context.lineTo(10 * zoom, 10 * zoom);
|
||||||
|
context.moveTo(10 * zoom, -10 * zoom);
|
||||||
|
context.lineTo(-10 * zoom, 10 * zoom);
|
||||||
|
context.stroke();
|
||||||
|
context.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = (
|
||||||
|
frame: DebugElement[],
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
frame.forEach((el: DebugElement) => {
|
||||||
|
switch (true) {
|
||||||
|
case isLineSegment(el.data):
|
||||||
|
renderLine(
|
||||||
|
context,
|
||||||
|
appState.zoom.value,
|
||||||
|
el.data as LineSegment<GlobalPoint>,
|
||||||
|
el.color,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _debugRenderer = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
appState: AppState,
|
||||||
|
scale: number,
|
||||||
|
refresh: () => void,
|
||||||
|
) => {
|
||||||
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
|
canvas,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (appState.height !== canvas.height || appState.width !== canvas.width) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = bootstrapCanvas({
|
||||||
|
canvas,
|
||||||
|
scale,
|
||||||
|
normalizedWidth,
|
||||||
|
normalizedHeight,
|
||||||
|
canvasBackgroundColor: "transparent",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply zoom
|
||||||
|
context.save();
|
||||||
|
context.translate(
|
||||||
|
appState.scrollX * appState.zoom.value,
|
||||||
|
appState.scrollY * appState.zoom.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderOrigin(context, appState.zoom.value);
|
||||||
|
|
||||||
|
if (
|
||||||
|
window.visualDebug?.currentFrame &&
|
||||||
|
window.visualDebug?.data &&
|
||||||
|
window.visualDebug.data.length > 0
|
||||||
|
) {
|
||||||
|
// Render only one frame
|
||||||
|
const [idx] = debugFrameData();
|
||||||
|
|
||||||
|
render(window.visualDebug.data[idx], context, appState);
|
||||||
|
} else {
|
||||||
|
// Render all debug frames
|
||||||
|
window.visualDebug?.data.forEach((frame) => {
|
||||||
|
render(frame, context, appState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.visualDebug) {
|
||||||
|
window.visualDebug!.data =
|
||||||
|
window.visualDebug?.data.map((frame) =>
|
||||||
|
frame.filter((el) => el.permanent),
|
||||||
|
) ?? [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debugFrameData = (): [number, number] => {
|
||||||
|
const currentFrame = window.visualDebug?.currentFrame ?? 0;
|
||||||
|
const frameCount = window.visualDebug?.data.length ?? 0;
|
||||||
|
|
||||||
|
if (frameCount > 0) {
|
||||||
|
return [currentFrame % frameCount, window.visualDebug?.currentFrame ?? 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0, 0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveDebugState = (debug: { enabled: boolean }) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
|
||||||
|
JSON.stringify(debug),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugRenderer = throttleRAF(
|
||||||
|
(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
appState: AppState,
|
||||||
|
scale: number,
|
||||||
|
refresh: () => void,
|
||||||
|
) => {
|
||||||
|
_debugRenderer(canvas, appState, scale, refresh);
|
||||||
|
},
|
||||||
|
{ trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadSavedDebugState = () => {
|
||||||
|
let debug;
|
||||||
|
try {
|
||||||
|
const savedDebugState = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
|
||||||
|
);
|
||||||
|
if (savedDebugState) {
|
||||||
|
debug = JSON.parse(savedDebugState) as { enabled: boolean };
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return debug ?? { enabled: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isVisualDebuggerEnabled = () =>
|
||||||
|
Array.isArray(window.visualDebug?.data);
|
||||||
|
|
||||||
|
export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
||||||
|
const moveForward = useCallback(() => {
|
||||||
|
if (
|
||||||
|
!window.visualDebug?.currentFrame ||
|
||||||
|
isNaN(window.visualDebug?.currentFrame ?? -1)
|
||||||
|
) {
|
||||||
|
window.visualDebug!.currentFrame = 0;
|
||||||
|
}
|
||||||
|
window.visualDebug!.currentFrame += 1;
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
const moveBackward = useCallback(() => {
|
||||||
|
if (
|
||||||
|
!window.visualDebug?.currentFrame ||
|
||||||
|
isNaN(window.visualDebug?.currentFrame ?? -1) ||
|
||||||
|
window.visualDebug?.currentFrame < 1
|
||||||
|
) {
|
||||||
|
window.visualDebug!.currentFrame = 1;
|
||||||
|
}
|
||||||
|
window.visualDebug!.currentFrame -= 1;
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
window.visualDebug!.currentFrame = undefined;
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
const trashFrames = useCallback(() => {
|
||||||
|
if (window.visualDebug) {
|
||||||
|
window.visualDebug.currentFrame = undefined;
|
||||||
|
window.visualDebug.data = [];
|
||||||
|
}
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-forward"
|
||||||
|
aria-label="Move forward"
|
||||||
|
type="button"
|
||||||
|
onClick={trashFrames}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
{TrashIcon}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-forward"
|
||||||
|
aria-label="Move forward"
|
||||||
|
type="button"
|
||||||
|
onClick={moveBackward}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
<ArrowheadArrowIcon flip />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-forward"
|
||||||
|
aria-label="Move forward"
|
||||||
|
type="button"
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
{CloseIcon}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-backward"
|
||||||
|
aria-label="Move backward"
|
||||||
|
type="button"
|
||||||
|
onClick={moveForward}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
<ArrowheadArrowIcon />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DebugCanvasProps {
|
||||||
|
appState: AppState;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DebugCanvas = forwardRef<HTMLCanvasElement, DebugCanvasProps>(
|
||||||
|
({ appState, scale }, ref) => {
|
||||||
|
const { width, height } = appState;
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
|
||||||
|
ref,
|
||||||
|
() => canvasRef.current,
|
||||||
|
[canvasRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 2,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
width={width * scale}
|
||||||
|
height={height * scale}
|
||||||
|
ref={canvasRef}
|
||||||
|
>
|
||||||
|
Debug Canvas
|
||||||
|
</canvas>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DebugCanvas;
|
||||||
@@ -3,11 +3,11 @@ import { Card } from "../../packages/excalidraw/components/Card";
|
|||||||
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
|
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
|
||||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||||
import {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
@@ -30,6 +30,7 @@ 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();
|
||||||
|
|
||||||
@@ -53,7 +54,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: appState.name }),
|
data: JSON.stringify({ version: 2, name }),
|
||||||
created: Date.now().toString(),
|
created: Date.now().toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -89,9 +90,10 @@ 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, onError, onSuccess }) => {
|
}> = ({ elements, appState, files, name, onError, onSuccess }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Card color="primary">
|
<Card color="primary">
|
||||||
@@ -117,7 +119,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);
|
await exportToExcalidrawPlus(elements, appState, files, name);
|
||||||
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 "../../packages/excalidraw/constants";
|
||||||
import { Theme } from "../../packages/excalidraw/element/types";
|
import type { Theme } from "../../packages/excalidraw/element/types";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export class TopErrorBoundary extends React.Component<
|
|||||||
|
|
||||||
window.open(
|
window.open(
|
||||||
`https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
|
`https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener noreferrer",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
|
import { StoreAction } from "../../packages/excalidraw";
|
||||||
import { compressData } from "../../packages/excalidraw/data/encode";
|
import { compressData } from "../../packages/excalidraw/data/encode";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { t } from "../../packages/excalidraw/i18n";
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
import {
|
import type {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
|
|
||||||
|
type FileVersion = Required<BinaryFileData>["version"];
|
||||||
|
|
||||||
export class FileManager {
|
export class FileManager {
|
||||||
/** files being fetched */
|
/** files being fetched */
|
||||||
private fetchingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
private fetchingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
||||||
|
private erroredFiles_fetch = new Map<
|
||||||
|
ExcalidrawImageElement["fileId"],
|
||||||
|
true
|
||||||
|
>();
|
||||||
/** files being saved */
|
/** files being saved */
|
||||||
private savingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
private savingFiles = new Map<
|
||||||
|
ExcalidrawImageElement["fileId"],
|
||||||
|
FileVersion
|
||||||
|
>();
|
||||||
/* files already saved to persistent storage */
|
/* files already saved to persistent storage */
|
||||||
private savedFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
private savedFiles = new Map<ExcalidrawImageElement["fileId"], FileVersion>();
|
||||||
private erroredFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
private erroredFiles_save = new Map<
|
||||||
|
ExcalidrawImageElement["fileId"],
|
||||||
|
FileVersion
|
||||||
|
>();
|
||||||
|
|
||||||
private _getFiles;
|
private _getFiles;
|
||||||
private _saveFiles;
|
private _saveFiles;
|
||||||
@@ -36,8 +49,8 @@ export class FileManager {
|
|||||||
erroredFiles: Map<FileId, true>;
|
erroredFiles: Map<FileId, true>;
|
||||||
}>;
|
}>;
|
||||||
saveFiles: (data: { addedFiles: Map<FileId, BinaryFileData> }) => Promise<{
|
saveFiles: (data: { addedFiles: Map<FileId, BinaryFileData> }) => Promise<{
|
||||||
savedFiles: Map<FileId, true>;
|
savedFiles: Map<FileId, BinaryFileData>;
|
||||||
erroredFiles: Map<FileId, true>;
|
erroredFiles: Map<FileId, BinaryFileData>;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
this._getFiles = getFiles;
|
this._getFiles = getFiles;
|
||||||
@@ -45,19 +58,28 @@ export class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns whether file is already saved or being processed
|
* returns whether file is saved/errored, or being processed
|
||||||
*/
|
*/
|
||||||
isFileHandled = (id: FileId) => {
|
isFileTracked = (id: FileId) => {
|
||||||
return (
|
return (
|
||||||
this.savedFiles.has(id) ||
|
this.savedFiles.has(id) ||
|
||||||
this.fetchingFiles.has(id) ||
|
|
||||||
this.savingFiles.has(id) ||
|
this.savingFiles.has(id) ||
|
||||||
this.erroredFiles.has(id)
|
this.fetchingFiles.has(id) ||
|
||||||
|
this.erroredFiles_fetch.has(id) ||
|
||||||
|
this.erroredFiles_save.has(id)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
isFileSaved = (id: FileId) => {
|
isFileSavedOrBeingSaved = (file: BinaryFileData) => {
|
||||||
return this.savedFiles.has(id);
|
const fileVersion = this.getFileVersion(file);
|
||||||
|
return (
|
||||||
|
this.savedFiles.get(file.id) === fileVersion ||
|
||||||
|
this.savingFiles.get(file.id) === fileVersion
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getFileVersion = (file: BinaryFileData) => {
|
||||||
|
return file.version ?? 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
saveFiles = async ({
|
saveFiles = async ({
|
||||||
@@ -70,13 +92,16 @@ export class FileManager {
|
|||||||
const addedFiles: Map<FileId, BinaryFileData> = new Map();
|
const addedFiles: Map<FileId, BinaryFileData> = new Map();
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
|
const fileData =
|
||||||
|
isInitializedImageElement(element) && files[element.fileId];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isInitializedImageElement(element) &&
|
fileData &&
|
||||||
files[element.fileId] &&
|
// NOTE if errored during save, won't retry due to this check
|
||||||
!this.isFileHandled(element.fileId)
|
!this.isFileSavedOrBeingSaved(fileData)
|
||||||
) {
|
) {
|
||||||
addedFiles.set(element.fileId, files[element.fileId]);
|
addedFiles.set(element.fileId, files[element.fileId]);
|
||||||
this.savingFiles.set(element.fileId, true);
|
this.savingFiles.set(element.fileId, this.getFileVersion(fileData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +110,12 @@ export class FileManager {
|
|||||||
addedFiles,
|
addedFiles,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [fileId] of savedFiles) {
|
for (const [fileId, fileData] of savedFiles) {
|
||||||
this.savedFiles.set(fileId, true);
|
this.savedFiles.set(fileId, this.getFileVersion(fileData));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [fileId, fileData] of erroredFiles) {
|
||||||
|
this.erroredFiles_save.set(fileId, this.getFileVersion(fileData));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -120,10 +149,10 @@ export class FileManager {
|
|||||||
const { loadedFiles, erroredFiles } = await this._getFiles(ids);
|
const { loadedFiles, erroredFiles } = await this._getFiles(ids);
|
||||||
|
|
||||||
for (const file of loadedFiles) {
|
for (const file of loadedFiles) {
|
||||||
this.savedFiles.set(file.id, true);
|
this.savedFiles.set(file.id, this.getFileVersion(file));
|
||||||
}
|
}
|
||||||
for (const [fileId] of erroredFiles) {
|
for (const [fileId] of erroredFiles) {
|
||||||
this.erroredFiles.set(fileId, true);
|
this.erroredFiles_fetch.set(fileId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { loadedFiles, erroredFiles };
|
return { loadedFiles, erroredFiles };
|
||||||
@@ -159,7 +188,7 @@ export class FileManager {
|
|||||||
): element is InitializedExcalidrawImageElement => {
|
): element is InitializedExcalidrawImageElement => {
|
||||||
return (
|
return (
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
this.isFileSaved(element.fileId) &&
|
this.savedFiles.has(element.fileId) &&
|
||||||
element.status === "pending"
|
element.status === "pending"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -168,7 +197,8 @@ export class FileManager {
|
|||||||
this.fetchingFiles.clear();
|
this.fetchingFiles.clear();
|
||||||
this.savingFiles.clear();
|
this.savingFiles.clear();
|
||||||
this.savedFiles.clear();
|
this.savedFiles.clear();
|
||||||
this.erroredFiles.clear();
|
this.erroredFiles_fetch.clear();
|
||||||
|
this.erroredFiles_save.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,5 +268,6 @@ export const updateStaleImageStatuses = (params: {
|
|||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}),
|
}),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,18 +10,33 @@
|
|||||||
* (localStorage, indexedDB).
|
* (localStorage, indexedDB).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
|
||||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
|
||||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
|
||||||
import {
|
import {
|
||||||
|
createStore,
|
||||||
|
entries,
|
||||||
|
del,
|
||||||
|
getMany,
|
||||||
|
set,
|
||||||
|
setMany,
|
||||||
|
get,
|
||||||
|
} from "idb-keyval";
|
||||||
|
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||||
|
import {
|
||||||
|
CANVAS_SEARCH_TAB,
|
||||||
|
DEFAULT_SIDEBAR,
|
||||||
|
} from "../../packages/excalidraw/constants";
|
||||||
|
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
||||||
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
|
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||||
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
|
import type { MaybePromise } from "../../packages/excalidraw/utility-types";
|
||||||
import { debounce } from "../../packages/excalidraw/utils";
|
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";
|
||||||
@@ -55,13 +70,22 @@ const saveDataStateToLocalStorage = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
const _appState = clearAppStateForLocalStorage(appState);
|
||||||
|
|
||||||
|
if (
|
||||||
|
_appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
|
||||||
|
_appState.openSidebar.tab === CANVAS_SEARCH_TAB
|
||||||
|
) {
|
||||||
|
_appState.openSidebar = null;
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
JSON.stringify(clearElementsForLocalStorage(elements)),
|
||||||
);
|
);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
JSON.stringify(_appState),
|
||||||
);
|
);
|
||||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -159,8 +183,8 @@ export class LocalData {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
async saveFiles({ addedFiles }) {
|
async saveFiles({ addedFiles }) {
|
||||||
const savedFiles = new Map<FileId, true>();
|
const savedFiles = new Map<FileId, BinaryFileData>();
|
||||||
const erroredFiles = new Map<FileId, true>();
|
const erroredFiles = new Map<FileId, BinaryFileData>();
|
||||||
|
|
||||||
// before we use `storage` event synchronization, let's update the flag
|
// before we use `storage` event synchronization, let's update the flag
|
||||||
// optimistically. Hopefully nothing fails, and an IDB read executed
|
// optimistically. Hopefully nothing fails, and an IDB read executed
|
||||||
@@ -171,10 +195,10 @@ export class LocalData {
|
|||||||
[...addedFiles].map(async ([id, fileData]) => {
|
[...addedFiles].map(async ([id, fileData]) => {
|
||||||
try {
|
try {
|
||||||
await set(id, fileData, filesStore);
|
await set(id, fileData, filesStore);
|
||||||
savedFiles.set(id, true);
|
savedFiles.set(id, fileData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
erroredFiles.set(id, true);
|
erroredFiles.set(id, fileData);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -183,3 +207,52 @@ 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,11 +1,13 @@
|
|||||||
import {
|
import { reconcileElements } from "../../packages/excalidraw";
|
||||||
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { getSceneVersion } from "../../packages/excalidraw/element";
|
import { getSceneVersion } from "../../packages/excalidraw/element";
|
||||||
import Portal from "../collab/Portal";
|
import type Portal from "../collab/Portal";
|
||||||
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
@@ -18,10 +20,11 @@ import {
|
|||||||
decryptData,
|
decryptData,
|
||||||
} from "../../packages/excalidraw/data/encryption";
|
} from "../../packages/excalidraw/data/encryption";
|
||||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
||||||
import { reconcileElements } from "../collab/reconciliation";
|
import type { SyncableExcalidrawElement } from ".";
|
||||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
import { getSyncableElements } from ".";
|
||||||
import { ResolutionType } from "../../packages/excalidraw/utility-types";
|
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
|
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -174,8 +177,8 @@ export const saveFilesToFirebase = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const firebase = await loadFirebaseStorage();
|
const firebase = await loadFirebaseStorage();
|
||||||
|
|
||||||
const erroredFiles = new Map<FileId, true>();
|
const erroredFiles: FileId[] = [];
|
||||||
const savedFiles = new Map<FileId, true>();
|
const savedFiles: FileId[] = [];
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async ({ id, buffer }) => {
|
files.map(async ({ id, buffer }) => {
|
||||||
@@ -191,9 +194,9 @@ export const saveFilesToFirebase = async ({
|
|||||||
cacheControl: `public, max-age=${FILE_CACHE_MAX_AGE_SEC}`,
|
cacheControl: `public, max-age=${FILE_CACHE_MAX_AGE_SEC}`,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
savedFiles.set(id, true);
|
savedFiles.push(id);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
erroredFiles.set(id, true);
|
erroredFiles.push(id);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -230,7 +233,7 @@ export const saveToFirebase = async (
|
|||||||
!socket ||
|
!socket ||
|
||||||
isSavedToFirebase(portal, elements)
|
isSavedToFirebase(portal, elements)
|
||||||
) {
|
) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firebase = await loadFirestore();
|
const firebase = await loadFirestore();
|
||||||
@@ -238,56 +241,59 @@ export const saveToFirebase = async (
|
|||||||
|
|
||||||
const docRef = firestore.collection("scenes").doc(roomId);
|
const docRef = firestore.collection("scenes").doc(roomId);
|
||||||
|
|
||||||
const savedData = await firestore.runTransaction(async (transaction) => {
|
const storedScene = await firestore.runTransaction(async (transaction) => {
|
||||||
const snapshot = await transaction.get(docRef);
|
const snapshot = await transaction.get(docRef);
|
||||||
|
|
||||||
if (!snapshot.exists) {
|
if (!snapshot.exists) {
|
||||||
const sceneDocument = await createFirebaseSceneDocument(
|
const storedScene = await createFirebaseSceneDocument(
|
||||||
firebase,
|
firebase,
|
||||||
elements,
|
elements,
|
||||||
roomKey,
|
roomKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
transaction.set(docRef, sceneDocument);
|
transaction.set(docRef, storedScene);
|
||||||
|
|
||||||
return {
|
return storedScene;
|
||||||
elements,
|
|
||||||
reconciledElements: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevDocData = snapshot.data() as FirebaseStoredScene;
|
const prevStoredScene = snapshot.data() as FirebaseStoredScene;
|
||||||
const prevElements = getSyncableElements(
|
const prevStoredElements = getSyncableElements(
|
||||||
await decryptElements(prevDocData, roomKey),
|
restoreElements(await decryptElements(prevStoredScene, roomKey), null),
|
||||||
);
|
);
|
||||||
|
|
||||||
const reconciledElements = getSyncableElements(
|
const reconciledElements = getSyncableElements(
|
||||||
reconcileElements(elements, prevElements, appState),
|
reconcileElements(
|
||||||
|
elements,
|
||||||
|
prevStoredElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[],
|
||||||
|
appState,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sceneDocument = await createFirebaseSceneDocument(
|
const storedScene = await createFirebaseSceneDocument(
|
||||||
firebase,
|
firebase,
|
||||||
reconciledElements,
|
reconciledElements,
|
||||||
roomKey,
|
roomKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
transaction.update(docRef, sceneDocument);
|
transaction.update(docRef, storedScene);
|
||||||
return {
|
|
||||||
elements,
|
// Return the stored elements as the in memory `reconciledElements` could have mutated in the meantime
|
||||||
reconciledElements,
|
return storedScene;
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
FirebaseSceneVersionCache.set(socket, savedData.elements);
|
const storedElements = getSyncableElements(
|
||||||
|
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||||
|
);
|
||||||
|
|
||||||
return { reconciledElements: savedData.reconciledElements };
|
FirebaseSceneVersionCache.set(socket, storedElements);
|
||||||
|
|
||||||
|
return storedElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadFromFirebase = async (
|
export const loadFromFirebase = async (
|
||||||
roomId: string,
|
roomId: string,
|
||||||
roomKey: string,
|
roomKey: string,
|
||||||
socket: Socket | null,
|
socket: Socket | null,
|
||||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
): Promise<readonly SyncableExcalidrawElement[] | null> => {
|
||||||
const firebase = await loadFirestore();
|
const firebase = await loadFirestore();
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
|
||||||
@@ -298,14 +304,14 @@ export const loadFromFirebase = async (
|
|||||||
}
|
}
|
||||||
const storedScene = doc.data() as FirebaseStoredScene;
|
const storedScene = doc.data() as FirebaseStoredScene;
|
||||||
const elements = getSyncableElements(
|
const elements = getSyncableElements(
|
||||||
await decryptElements(storedScene, roomKey),
|
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
FirebaseSceneVersionCache.set(socket, elements);
|
FirebaseSceneVersionCache.set(socket, elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoreElements(elements, null);
|
return elements;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadFilesFromFirebase = async (
|
export const loadFilesFromFirebase = async (
|
||||||
|
|||||||
@@ -9,38 +9,39 @@ import {
|
|||||||
} from "../../packages/excalidraw/data/encryption";
|
} from "../../packages/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||||
import { restore } from "../../packages/excalidraw/data/restore";
|
import { restore } from "../../packages/excalidraw/data/restore";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { t } from "../../packages/excalidraw/i18n";
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
|
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
|
||||||
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
||||||
|
import type { WS_SUBTYPES } from "../app_constants";
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
ROOM_ID_BYTES,
|
ROOM_ID_BYTES,
|
||||||
WS_SUBTYPES,
|
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "./FileManager";
|
import { encodeFilesForUpload } from "./FileManager";
|
||||||
import { saveFilesToFirebase } from "./firebase";
|
import { saveFilesToFirebase } from "./firebase";
|
||||||
|
|
||||||
export type SyncableExcalidrawElement = ExcalidrawElement & {
|
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||||
_brand: "SyncableExcalidrawElement";
|
MakeBrand<"SyncableExcalidrawElement">;
|
||||||
};
|
|
||||||
|
|
||||||
export const isSyncableElement = (
|
export const isSyncableElement = (
|
||||||
element: ExcalidrawElement,
|
element: OrderedExcalidrawElement,
|
||||||
): element is SyncableExcalidrawElement => {
|
): element is SyncableExcalidrawElement => {
|
||||||
if (element.isDeleted) {
|
if (element.isDeleted) {
|
||||||
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
|
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
|
||||||
@@ -51,7 +52,9 @@ export const isSyncableElement = (
|
|||||||
return !isInvisiblySmallElement(element);
|
return !isInvisiblySmallElement(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
export const getSyncableElements = (
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
|
) =>
|
||||||
elements.filter((element) =>
|
elements.filter((element) =>
|
||||||
isSyncableElement(element),
|
isSyncableElement(element),
|
||||||
) as SyncableExcalidrawElement[];
|
) as SyncableExcalidrawElement[];
|
||||||
@@ -65,35 +68,6 @@ 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;
|
||||||
@@ -295,7 +269,6 @@ export const loadScene = async (
|
|||||||
// in the scene database/localStorage, and instead fetch them async
|
// in the scene database/localStorage, and instead fetch them async
|
||||||
// from a different database
|
// from a different database
|
||||||
files: data.files,
|
files: data.files,
|
||||||
commitToHistory: false,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||||
import { AppState } from "../../packages/excalidraw/types";
|
import type { AppState } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "../../packages/excalidraw/appState";
|
} from "../../packages/excalidraw/appState";
|
||||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -88,28 +87,13 @@ 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 + librarySize + getElementsStorageSize();
|
return appStateSize + collabSize + 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 [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<meta name="image" content="https://excalidraw.com/og-image-2.png" />
|
<meta name="image" content="https://excalidraw.com/og-image-3.png" />
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:site_name" content="Excalidraw" />
|
<meta property="og:site_name" content="Excalidraw" />
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
property="og:description"
|
property="og:description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content="https://excalidraw.com/og-image-2.png" />
|
<meta property="og:image" content="https://excalidraw.com/og-image-3.png" />
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
@@ -51,25 +51,37 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="twitter:image"
|
property="twitter:image"
|
||||||
content="https://excalidraw.com/og-twitter-v2.png"
|
content="https://excalidraw.com/og-image-3.png"
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- General tags -->
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<!-- to minimize white flash on load when user has dark mode enabled -->
|
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||||
<script>
|
<script>
|
||||||
try {
|
try {
|
||||||
//
|
function setTheme(theme) {
|
||||||
const theme = window.localStorage.getItem("excalidraw-theme");
|
if (theme === "dark") {
|
||||||
if (theme === "dark") {
|
document.documentElement.classList.add("dark");
|
||||||
document.documentElement.classList.add("dark");
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
|
function getTheme() {
|
||||||
|
const theme = window.localStorage.getItem("excalidraw-theme");
|
||||||
|
|
||||||
|
if (theme && theme === "system") {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
} else {
|
||||||
|
return theme || "light";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(getTheme());
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error setting dark mode", e);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
html.dark {
|
html.dark {
|
||||||
@@ -77,8 +89,13 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Warmup the connection for Google fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<% if ("%PROD%" === "true") { %>
|
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||||
<script>
|
<script>
|
||||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||||
//
|
//
|
||||||
@@ -97,6 +114,21 @@
|
|||||||
window.location.href = "https://app.excalidraw.com";
|
window.location.href = "https://app.excalidraw.com";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Following placeholder is replaced during the build step -->
|
||||||
|
<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->
|
||||||
|
|
||||||
|
<!-- Register Assistant as the UI font, before the scene inits -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="../packages/excalidraw/fonts/fonts.css"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
<script>
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
@@ -106,23 +138,8 @@
|
|||||||
<!-- Excalidraw version -->
|
<!-- Excalidraw version -->
|
||||||
<meta name="version" content="{version}" />
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
<link
|
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
||||||
rel="preload"
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
||||||
href="/Virgil.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/Cascadia.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" type="text/css" />
|
|
||||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
|
||||||
<script>
|
<script>
|
||||||
{
|
{
|
||||||
const _WebSocket = window.WebSocket;
|
const _WebSocket = window.WebSocket;
|
||||||
@@ -139,7 +156,6 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
|
||||||
// setting this so that libraries installation reuses this window tab.
|
// setting this so that libraries installation reuses this window tab.
|
||||||
window.name = "_excalidraw";
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
@@ -196,7 +212,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="index.tsx"></script>
|
<script type="module" src="index.tsx"></script>
|
||||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
|
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||||
<!-- 100% privacy friendly analytics -->
|
<!-- 100% privacy friendly analytics -->
|
||||||
<script>
|
<script>
|
||||||
// need to load this script dynamically bcs. of iframe embed tracking
|
// need to load this script dynamically bcs. of iframe embed tracking
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
&.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;
|
||||||
@@ -18,6 +25,7 @@
|
|||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
margin-inline-end: 0.6em;
|
margin-inline-end: 0.6em;
|
||||||
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
@@ -31,8 +39,12 @@
|
|||||||
background-color: #ecfdf5;
|
background-color: #ecfdf5;
|
||||||
color: #064e3c;
|
color: #064e3c;
|
||||||
}
|
}
|
||||||
&.ExcalidrawPlus {
|
&.highlighted {
|
||||||
color: var(--color-promo);
|
color: var(--color-promo);
|
||||||
|
font-weight: 700;
|
||||||
|
.dropdown-menu-item__icon g {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,18 +23,34 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": "18.0.0 - 22.x.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@excalidraw/random-username": "1.0.0",
|
||||||
|
"@sentry/browser": "6.2.5",
|
||||||
|
"@sentry/integrations": "6.2.5",
|
||||||
|
"firebase": "8.3.3",
|
||||||
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
|
"idb-keyval": "6.0.3",
|
||||||
|
"jotai": "1.13.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"socket.io-client": "4.7.2",
|
||||||
|
"vite-plugin-html": "3.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"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 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 cross-env VITE_APP_ENABLE_TRACKING=true vite build",
|
||||||
"build:version": "node ../scripts/build-version.js",
|
"build:version": "node ../scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build": "yarn build:app && yarn build:version",
|
||||||
"start": "yarn && vite",
|
"start": "yarn && vite",
|
||||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"start:production": "yarn build && yarn serve",
|
||||||
|
"serve": "npx http-server build -a localhost -p 5001 -o",
|
||||||
"build:preview": "yarn build && vite preview --port 5000"
|
"build:preview": "yarn build && vite preview --port 5000"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite-plugin-sitemap": "0.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import "../../packages/excalidraw/css/variables.module.scss";
|
@import "../../packages/excalidraw/css/variables.module.scss";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.RoomDialog {
|
.ShareDialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
@@ -10,8 +10,25 @@
|
|||||||
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 RoomDialog__popover__scaleIn {
|
@keyframes ShareDialog__popover__scaleIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -41,8 +58,8 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 110%;
|
line-height: 110%;
|
||||||
|
|
||||||
background: var(--color-success-lighter);
|
background: var(--color-success);
|
||||||
color: var(--color-success);
|
color: var(--color-success-text);
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
width: 0.875rem;
|
width: 0.875rem;
|
||||||
@@ -50,10 +67,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
transform-origin: var(--radix-popover-content-transform-origin);
|
transform-origin: var(--radix-popover-content-transform-origin);
|
||||||
animation: RoomDialog__popover__scaleIn 150ms ease-out;
|
animation: ShareDialog__popover__scaleIn 150ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__inactive {
|
&__picker {
|
||||||
font-family: "Assistant";
|
font-family: "Assistant";
|
||||||
|
|
||||||
&__illustration {
|
&__illustration {
|
||||||
@@ -95,7 +112,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__start_session {
|
&__button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
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,
|
||||||
|
} from "../../packages/excalidraw/components/icons";
|
||||||
|
import { TextField } from "../../packages/excalidraw/components/TextField";
|
||||||
|
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
||||||
|
import type { CollabAPI } from "../collab/Collab";
|
||||||
|
import { activeRoomLinkAtom } from "../collab/Collab";
|
||||||
|
import { atom, useAtom, useAtomValue } from "jotai";
|
||||||
|
|
||||||
|
import "./ShareDialog.scss";
|
||||||
|
import { useUIAppState } from "../../packages/excalidraw/context/ui-appState";
|
||||||
|
import { useCopyStatus } from "../../packages/excalidraw/hooks/useCopiedIndicator";
|
||||||
|
|
||||||
|
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 [, setJustCopied] = useState(false);
|
||||||
|
const timerRef = useRef<number>(0);
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
const isShareSupported = "share" in navigator;
|
||||||
|
const { onCopy, copyStatus } = useCopyStatus();
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FilledButton
|
||||||
|
size="large"
|
||||||
|
label={t("buttons.copyLink")}
|
||||||
|
icon={copyIcon}
|
||||||
|
status={copyStatus}
|
||||||
|
onClick={() => {
|
||||||
|
copyRoomLink();
|
||||||
|
onCopy();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ShareDialog__active__description">
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
role="img"
|
||||||
|
aria-hidden="true"
|
||||||
|
className="ShareDialog__active__description__emoji"
|
||||||
|
>
|
||||||
|
🔒{" "}
|
||||||
|
</span>
|
||||||
|
{t("roomDialog.desc_privacy")}
|
||||||
|
</p>
|
||||||
|
<p>{t("roomDialog.desc_exitSession")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ShareDialog__active__actions">
|
||||||
|
<FilledButton
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
color="danger"
|
||||||
|
label={t("roomDialog.button_stopSession")}
|
||||||
|
icon={playerStopFilledIcon}
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent("share", "room closed");
|
||||||
|
collabAPI.stopCollaboration();
|
||||||
|
if (!collabAPI.isCollaborating()) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareDialogPicker = (props: ShareDialogProps) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const { collabAPI } = props;
|
||||||
|
|
||||||
|
const startCollabJSX = collabAPI ? (
|
||||||
|
<>
|
||||||
|
<div className="ShareDialog__picker__header">
|
||||||
|
{t("labels.liveCollaboration").replace(/\./g, "")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ShareDialog__picker__description">
|
||||||
|
<div style={{ marginBottom: "1em" }}>{t("roomDialog.desc_intro")}</div>
|
||||||
|
{t("roomDialog.desc_privacy")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ShareDialog__picker__button">
|
||||||
|
<FilledButton
|
||||||
|
size="large"
|
||||||
|
label={t("roomDialog.button_startSession")}
|
||||||
|
icon={playerPlayIcon}
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||||
|
collabAPI.startCollaboration(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{props.type === "share" && (
|
||||||
|
<div className="ShareDialog__separator">
|
||||||
|
<span>{t("shareDialog.or")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{startCollabJSX}
|
||||||
|
|
||||||
|
{props.type === "share" && (
|
||||||
|
<>
|
||||||
|
<div className="ShareDialog__picker__header">
|
||||||
|
{t("exportDialog.link_title")}
|
||||||
|
</div>
|
||||||
|
<div className="ShareDialog__picker__description">
|
||||||
|
{t("exportDialog.link_details")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ShareDialog__picker__button">
|
||||||
|
<FilledButton
|
||||||
|
size="large"
|
||||||
|
label={t("exportDialog.link_button")}
|
||||||
|
icon={LinkIcon}
|
||||||
|
onClick={async () => {
|
||||||
|
await props.onExportToBackend();
|
||||||
|
props.handleClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareDialogInner = (props: ShareDialogProps) => {
|
||||||
|
const activeRoomLink = useAtomValue(activeRoomLinkAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog size="small" onCloseRequest={props.handleClose} title={false}>
|
||||||
|
<div className="ShareDialog">
|
||||||
|
{props.collabAPI && activeRoomLink ? (
|
||||||
|
<ActiveRoomDialog
|
||||||
|
collabAPI={props.collabAPI}
|
||||||
|
activeRoomLink={activeRoomLink}
|
||||||
|
handleClose={props.handleClose}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ShareDialogPicker {...props} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareDialog = (props: {
|
||||||
|
collabAPI: CollabAPI | null;
|
||||||
|
onExportToBackend: OnExportToBackend;
|
||||||
|
}) => {
|
||||||
|
const [shareDialogState, setShareDialogState] = useAtom(shareDialogStateAtom);
|
||||||
|
|
||||||
|
const { openDialog } = useUIAppState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openDialog) {
|
||||||
|
setShareDialogState({ isOpen: false });
|
||||||
|
}
|
||||||
|
}, [openDialog, setShareDialogState]);
|
||||||
|
|
||||||
|
if (!shareDialogState.isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShareDialogInner
|
||||||
|
handleClose={() => setShareDialogState({ isOpen: false })}
|
||||||
|
collabAPI={props.collabAPI}
|
||||||
|
onExportToBackend={props.onExportToBackend}
|
||||||
|
type={shareDialogState.type}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,7 +5,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
class="welcome-screen-center"
|
class="welcome-screen-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="welcome-screen-center__logo virgil welcome-screen-decor"
|
class="welcome-screen-center__logo excalifont welcome-screen-decor"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ExcalidrawLogo is-small"
|
class="ExcalidrawLogo is-small"
|
||||||
@@ -48,7 +48,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="welcome-screen-center__heading welcome-screen-decor virgil"
|
class="welcome-screen-center__heading welcome-screen-decor excalifont"
|
||||||
>
|
>
|
||||||
All your data is saved locally in your browser.
|
All your data is saved locally in your browser.
|
||||||
</div>
|
</div>
|
||||||
@@ -224,24 +224,14 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
fill="none"
|
fill="none"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
/>
|
/>
|
||||||
<rect
|
<path
|
||||||
height="4"
|
d="M15 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
|
||||||
rx="1"
|
|
||||||
width="18"
|
|
||||||
x="3"
|
|
||||||
y="8"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
x2="12"
|
|
||||||
y1="8"
|
|
||||||
y2="21"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M19 12v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-7"
|
d="M21 12h-13l3 -3"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M7.5 8a2.5 2.5 0 0 1 0 -5a4.8 8 0 0 1 4.5 5a4.8 8 0 0 1 4.5 -5a2.5 2.5 0 0 1 0 5"
|
d="M11 15l-3 -3"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -249,7 +239,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
<div
|
<div
|
||||||
class="welcome-screen-menu-item__text"
|
class="welcome-screen-menu-item__text"
|
||||||
>
|
>
|
||||||
Try Excalidraw Plus!
|
Sign up
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
act,
|
||||||
render,
|
render,
|
||||||
updateSceneData,
|
|
||||||
waitFor,
|
waitFor,
|
||||||
} from "../../packages/excalidraw/tests/test-utils";
|
} from "../../packages/excalidraw/tests/test-utils";
|
||||||
import ExcalidrawApp from "../App";
|
import ExcalidrawApp from "../App";
|
||||||
import { API } from "../../packages/excalidraw/tests/helpers/api";
|
import { API } from "../../packages/excalidraw/tests/helpers/api";
|
||||||
import { createUndoAction } from "../../packages/excalidraw/actions/actionHistory";
|
import { syncInvalidIndices } from "../../packages/excalidraw/fractionalIndex";
|
||||||
|
import {
|
||||||
|
createRedoAction,
|
||||||
|
createUndoAction,
|
||||||
|
} from "../../packages/excalidraw/actions/actionHistory";
|
||||||
|
import { StoreAction, newElementWith } from "../../packages/excalidraw";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
Object.defineProperty(window, "crypto", {
|
Object.defineProperty(window, "crypto", {
|
||||||
@@ -20,17 +26,6 @@ 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 = () => {};
|
||||||
@@ -67,39 +62,190 @@ vi.mock("socket.io-client", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These test would deserve to be extended by testing collab with (at least) two clients simultanouesly,
|
||||||
|
* while having access to both scenes, appstates stores, histories and etc.
|
||||||
|
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
||||||
|
*/
|
||||||
describe("collaboration", () => {
|
describe("collaboration", () => {
|
||||||
it("creating room should reset deleted elements", async () => {
|
it("should allow to undo / redo even on force-deleted elements", async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
// To update the scene with deleted elements before starting collab
|
const rect1Props = {
|
||||||
updateSceneData({
|
type: "rectangle",
|
||||||
elements: [
|
id: "A",
|
||||||
API.createElement({ type: "rectangle", id: "A" }),
|
height: 200,
|
||||||
API.createElement({
|
width: 100,
|
||||||
type: "rectangle",
|
} as const;
|
||||||
id: "B",
|
|
||||||
isDeleted: true,
|
const rect2Props = {
|
||||||
}),
|
type: "rectangle",
|
||||||
],
|
id: "B",
|
||||||
});
|
width: 100,
|
||||||
await waitFor(() => {
|
height: 200,
|
||||||
expect(h.elements).toEqual([
|
} as const;
|
||||||
expect.objectContaining({ id: "A" }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true }),
|
const rect1 = API.createElement({ ...rect1Props });
|
||||||
]);
|
const rect2 = API.createElement({ ...rect2Props });
|
||||||
expect(API.getStateHistory().length).toBe(1);
|
|
||||||
});
|
API.updateScene({
|
||||||
window.collab.startCollaboration(null);
|
elements: syncInvalidIndices([rect1, rect2]),
|
||||||
await waitFor(() => {
|
storeAction: StoreAction.CAPTURE,
|
||||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
});
|
||||||
expect(API.getStateHistory().length).toBe(1);
|
|
||||||
|
API.updateScene({
|
||||||
|
elements: syncInvalidIndices([
|
||||||
|
rect1,
|
||||||
|
newElementWith(h.elements[1], { isDeleted: true }),
|
||||||
|
]),
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const undoAction = createUndoAction(h.history);
|
|
||||||
// noop
|
|
||||||
h.app.actionManager.executeAction(undoAction);
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getStateHistory().length).toBe(1);
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// one form of force deletion happens when starting the collab, not to sync potentially sensitive data into the server
|
||||||
|
window.collab.startCollaboration(null);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
|
// we never delete from the local snapshot as it is used for correct diff calculation
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const undoAction = createUndoAction(h.history, h.store);
|
||||||
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
|
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// simulate force deleting the element remotely
|
||||||
|
API.updateScene({
|
||||||
|
elements: syncInvalidIndices([rect1]),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const redoAction = createRedoAction(h.history, h.store);
|
||||||
|
act(() => h.app.actionManager.executeAction(redoAction));
|
||||||
|
|
||||||
|
// with explicit redo (as removal) we again restore the element from the snapshot!
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
|
// simulate local update
|
||||||
|
API.updateScene({
|
||||||
|
elements: syncInvalidIndices([
|
||||||
|
h.elements[0],
|
||||||
|
newElementWith(h.elements[1], { x: 100 }),
|
||||||
|
]),
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
|
// we expect to iterate the stack to the first visible change
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// simulate force deleting the element remotely
|
||||||
|
API.updateScene({
|
||||||
|
elements: syncInvalidIndices([rect1]),
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// snapshot was correctly updated and marked the element as deleted
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining(rect1Props),
|
||||||
|
expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }),
|
||||||
|
]);
|
||||||
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(redoAction));
|
||||||
|
|
||||||
|
// with explicit redo (as update) we again restored the element from the snapshot!
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
|
expect(API.getSnapshot()).toEqual([
|
||||||
|
expect.objectContaining({ id: "A", isDeleted: false }),
|
||||||
|
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
||||||
|
]);
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining({ id: "A", isDeleted: false }),
|
||||||
|
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,421 +0,0 @@
|
|||||||
import { expect } from "chai";
|
|
||||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
|
||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
|
||||||
import {
|
|
||||||
BroadcastedExcalidrawElement,
|
|
||||||
ReconciledElements,
|
|
||||||
reconcileElements,
|
|
||||||
} from "../../excalidraw-app/collab/reconciliation";
|
|
||||||
import { randomInteger } from "../../packages/excalidraw/random";
|
|
||||||
import { AppState } from "../../packages/excalidraw/types";
|
|
||||||
import { cloneJSON } from "../../packages/excalidraw/utils";
|
|
||||||
|
|
||||||
type Id = string;
|
|
||||||
type ElementLike = {
|
|
||||||
id: string;
|
|
||||||
version: number;
|
|
||||||
versionNonce: number;
|
|
||||||
[PRECEDING_ELEMENT_KEY]?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Cache = Record<string, ExcalidrawElement | undefined>;
|
|
||||||
|
|
||||||
const createElement = (opts: { uid: string } | ElementLike) => {
|
|
||||||
let uid: string;
|
|
||||||
let id: string;
|
|
||||||
let version: number | null;
|
|
||||||
let parent: string | null = null;
|
|
||||||
let versionNonce: number | null = null;
|
|
||||||
if ("uid" in opts) {
|
|
||||||
const match = opts.uid.match(
|
|
||||||
/^(?:\((\^|\w+)\))?(\w+)(?::(\d+))?(?:\((\w+)\))?$/,
|
|
||||||
)!;
|
|
||||||
parent = match[1];
|
|
||||||
id = match[2];
|
|
||||||
version = match[3] ? parseInt(match[3]) : null;
|
|
||||||
uid = version ? `${id}:${version}` : id;
|
|
||||||
} else {
|
|
||||||
({ id, version, versionNonce } = opts);
|
|
||||||
parent = parent || null;
|
|
||||||
uid = id;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
uid,
|
|
||||||
id,
|
|
||||||
version,
|
|
||||||
versionNonce: versionNonce || randomInteger(),
|
|
||||||
[PRECEDING_ELEMENT_KEY]: parent || null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const idsToElements = (
|
|
||||||
ids: (Id | ElementLike)[],
|
|
||||||
cache: Cache = {},
|
|
||||||
): readonly ExcalidrawElement[] => {
|
|
||||||
return ids.reduce((acc, _uid, idx) => {
|
|
||||||
const {
|
|
||||||
uid,
|
|
||||||
id,
|
|
||||||
version,
|
|
||||||
[PRECEDING_ELEMENT_KEY]: parent,
|
|
||||||
versionNonce,
|
|
||||||
} = createElement(typeof _uid === "string" ? { uid: _uid } : _uid);
|
|
||||||
const cached = cache[uid];
|
|
||||||
const elem = {
|
|
||||||
id,
|
|
||||||
version: version ?? 0,
|
|
||||||
versionNonce,
|
|
||||||
...cached,
|
|
||||||
[PRECEDING_ELEMENT_KEY]: parent,
|
|
||||||
} as BroadcastedExcalidrawElement;
|
|
||||||
// @ts-ignore
|
|
||||||
cache[uid] = elem;
|
|
||||||
acc.push(elem);
|
|
||||||
return acc;
|
|
||||||
}, [] as ExcalidrawElement[]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addParents = (elements: BroadcastedExcalidrawElement[]) => {
|
|
||||||
return elements.map((el, idx, els) => {
|
|
||||||
el[PRECEDING_ELEMENT_KEY] = els[idx - 1]?.id || "^";
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanElements = (elements: ReconciledElements) => {
|
|
||||||
return elements.map((el) => {
|
|
||||||
// @ts-ignore
|
|
||||||
delete el[PRECEDING_ELEMENT_KEY];
|
|
||||||
// @ts-ignore
|
|
||||||
delete el.next;
|
|
||||||
// @ts-ignore
|
|
||||||
delete el.prev;
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const test = <U extends `${string}:${"L" | "R"}`>(
|
|
||||||
local: (Id | ElementLike)[],
|
|
||||||
remote: (Id | ElementLike)[],
|
|
||||||
target: U[],
|
|
||||||
bidirectional = true,
|
|
||||||
) => {
|
|
||||||
const cache: Cache = {};
|
|
||||||
const _local = idsToElements(local, cache);
|
|
||||||
const _remote = idsToElements(remote, cache);
|
|
||||||
const _target = target.map((uid) => {
|
|
||||||
const [, id, source] = uid.match(/^(\w+):([LR])$/)!;
|
|
||||||
return (source === "L" ? _local : _remote).find((e) => e.id === id)!;
|
|
||||||
}) as any as ReconciledElements;
|
|
||||||
const remoteReconciled = reconcileElements(_local, _remote, {} as AppState);
|
|
||||||
expect(target.length).equal(remoteReconciled.length);
|
|
||||||
expect(cleanElements(remoteReconciled)).deep.equal(
|
|
||||||
cleanElements(_target),
|
|
||||||
"remote reconciliation",
|
|
||||||
);
|
|
||||||
|
|
||||||
const __local = cleanElements(cloneJSON(_remote) as ReconciledElements);
|
|
||||||
const __remote = addParents(cleanElements(cloneJSON(remoteReconciled)));
|
|
||||||
if (bidirectional) {
|
|
||||||
try {
|
|
||||||
expect(
|
|
||||||
cleanElements(
|
|
||||||
reconcileElements(
|
|
||||||
cloneJSON(__local),
|
|
||||||
cloneJSON(__remote),
|
|
||||||
{} as AppState,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).deep.equal(cleanElements(remoteReconciled), "local re-reconciliation");
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("local original", __local);
|
|
||||||
console.error("remote reconciled", __remote);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findIndex = <T>(
|
|
||||||
array: readonly T[],
|
|
||||||
cb: (element: T, index: number, array: readonly T[]) => boolean,
|
|
||||||
fromIndex: number = 0,
|
|
||||||
) => {
|
|
||||||
if (fromIndex < 0) {
|
|
||||||
fromIndex = array.length + fromIndex;
|
|
||||||
}
|
|
||||||
fromIndex = Math.min(array.length, Math.max(fromIndex, 0));
|
|
||||||
let index = fromIndex - 1;
|
|
||||||
while (++index < array.length) {
|
|
||||||
if (cb(array[index], index, array)) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("elements reconciliation", () => {
|
|
||||||
it("reconcileElements()", () => {
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// in following tests, we pass:
|
|
||||||
// (1) an array of local elements and their version (:1, :2...)
|
|
||||||
// (2) an array of remote elements and their version (:1, :2...)
|
|
||||||
// (3) expected reconciled elements
|
|
||||||
//
|
|
||||||
// in the reconciled array:
|
|
||||||
// :L means local element was resolved
|
|
||||||
// :R means remote element was resolved
|
|
||||||
//
|
|
||||||
// if a remote element is prefixed with parentheses, the enclosed string:
|
|
||||||
// (^) means the element is the first element in the array
|
|
||||||
// (<id>) means the element is preceded by <id> element
|
|
||||||
//
|
|
||||||
// if versions are missing, it defaults to version 0
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// non-annotated elements
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// usually when we sync elements they should always be annotated with
|
|
||||||
// their (preceding elements) parents, but let's test a couple of cases when
|
|
||||||
// they're not for whatever reason (remote clients are on older version...),
|
|
||||||
// in which case the first synced element either replaces existing element
|
|
||||||
// or is pushed at the end of the array
|
|
||||||
|
|
||||||
test(["A:1", "B:1", "C:1"], ["B:2"], ["A:L", "B:R", "C:L"]);
|
|
||||||
test(["A:1", "B:1", "C"], ["B:2", "A:2"], ["B:R", "A:R", "C:L"]);
|
|
||||||
test(["A:2", "B:1", "C"], ["B:2", "A:1"], ["A:L", "B:R", "C:L"]);
|
|
||||||
test(["A:1", "B:1"], ["C:1"], ["A:L", "B:L", "C:R"]);
|
|
||||||
test(["A", "B"], ["A:1"], ["A:R", "B:L"]);
|
|
||||||
test(["A"], ["A", "B"], ["A:L", "B:R"]);
|
|
||||||
test(["A"], ["A:1", "B"], ["A:R", "B:R"]);
|
|
||||||
test(["A:2"], ["A:1", "B"], ["A:L", "B:R"]);
|
|
||||||
test(["A:2"], ["B", "A:1"], ["A:L", "B:R"]);
|
|
||||||
test(["A:1"], ["B", "A:2"], ["B:R", "A:R"]);
|
|
||||||
test(["A"], ["A:1"], ["A:R"]);
|
|
||||||
|
|
||||||
// C isn't added to the end because it follows B (even if B was resolved
|
|
||||||
// to local version)
|
|
||||||
test(["A", "B:1", "D"], ["B", "C:2", "A"], ["B:L", "C:R", "A:R", "D:L"]);
|
|
||||||
|
|
||||||
// some of the following tests are kinda arbitrary and they're less
|
|
||||||
// likely to happen in real-world cases
|
|
||||||
|
|
||||||
test(["A", "B"], ["B:1", "A:1"], ["B:R", "A:R"]);
|
|
||||||
test(["A:2", "B:2"], ["B:1", "A:1"], ["A:L", "B:L"]);
|
|
||||||
test(["A", "B", "C"], ["A", "B:2", "G", "C"], ["A:L", "B:R", "G:R", "C:L"]);
|
|
||||||
test(["A", "B", "C"], ["A", "B:2", "G"], ["A:L", "B:R", "G:R", "C:L"]);
|
|
||||||
test(["A", "B", "C"], ["A", "B:2", "G"], ["A:L", "B:R", "G:R", "C:L"]);
|
|
||||||
test(
|
|
||||||
["A:2", "B:2", "C"],
|
|
||||||
["D", "B:1", "A:3"],
|
|
||||||
["B:L", "A:R", "C:L", "D:R"],
|
|
||||||
);
|
|
||||||
test(
|
|
||||||
["A:2", "B:2", "C"],
|
|
||||||
["D", "B:2", "A:3", "C"],
|
|
||||||
["D:R", "B:L", "A:R", "C:L"],
|
|
||||||
);
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D", "E", "F"],
|
|
||||||
["A", "B:2", "X", "E:2", "F", "Y"],
|
|
||||||
["A:L", "B:R", "X:R", "E:R", "F:L", "Y:R", "C:L", "D:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
// annotated elements
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C"],
|
|
||||||
["(B)X", "(A)Y", "(Y)Z"],
|
|
||||||
["A:L", "B:L", "X:R", "Y:R", "Z:R", "C:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(["A"], ["(^)X", "Y"], ["X:R", "Y:R", "A:L"]);
|
|
||||||
test(["A"], ["(^)X", "Y", "Z"], ["X:R", "Y:R", "Z:R", "A:L"]);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B"],
|
|
||||||
["(A)C", "(^)D", "F"],
|
|
||||||
["A:L", "C:R", "D:R", "F:R", "B:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D"],
|
|
||||||
["(B)C:1", "B", "D:1"],
|
|
||||||
["A:L", "C:R", "B:L", "D:R"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C"],
|
|
||||||
["(^)X", "(A)Y", "(B)Z"],
|
|
||||||
["X:R", "A:L", "Y:R", "B:L", "Z:R", "C:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["B", "A", "C"],
|
|
||||||
["(^)X", "(A)Y", "(B)Z"],
|
|
||||||
["X:R", "B:L", "A:L", "Y:R", "Z:R", "C:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(["A", "B"], ["(A)X", "(A)Y"], ["A:L", "X:R", "Y:R", "B:L"]);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D", "E"],
|
|
||||||
["(A)X", "(C)Y", "(D)Z"],
|
|
||||||
["A:L", "X:R", "B:L", "C:L", "Y:R", "D:L", "Z:R", "E:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["X", "Y", "Z"],
|
|
||||||
["(^)A", "(A)B", "(B)C", "(C)X", "(X)D", "(D)Y", "(Y)Z"],
|
|
||||||
["A:R", "B:R", "C:R", "X:L", "D:R", "Y:L", "Z:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D", "E"],
|
|
||||||
["(C)X", "(A)Y", "(D)E:1"],
|
|
||||||
["A:L", "B:L", "C:L", "X:R", "Y:R", "D:L", "E:R"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["C:1", "B", "D:1"],
|
|
||||||
["A", "B", "C:1", "D:1"],
|
|
||||||
["A:R", "B:L", "C:L", "D:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D"],
|
|
||||||
["(A)C:1", "(C)B", "(B)D:1"],
|
|
||||||
["A:L", "C:R", "B:L", "D:R"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D"],
|
|
||||||
["(A)C:1", "(C)B", "(B)D:1"],
|
|
||||||
["A:L", "C:R", "B:L", "D:R"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["C:1", "B", "D:1"],
|
|
||||||
["(^)A", "(A)B", "(B)C:2", "(C)D:1"],
|
|
||||||
["A:R", "B:L", "C:R", "D:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D"],
|
|
||||||
["(C)X", "(B)Y", "(A)Z"],
|
|
||||||
["A:L", "B:L", "C:L", "X:R", "Y:R", "Z:R", "D:L"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(["A", "B", "C", "D"], ["(A)B:1", "C:1"], ["A:L", "B:R", "C:R", "D:L"]);
|
|
||||||
test(["A", "B", "C", "D"], ["(A)C:1", "B:1"], ["A:L", "C:R", "B:R", "D:L"]);
|
|
||||||
test(
|
|
||||||
["A", "B", "C", "D"],
|
|
||||||
["(A)C:1", "B", "D:1"],
|
|
||||||
["A:L", "C:R", "B:L", "D:R"],
|
|
||||||
);
|
|
||||||
|
|
||||||
test(["A:1", "B:1", "C"], ["B:2"], ["A:L", "B:R", "C:L"]);
|
|
||||||
test(["A:1", "B:1", "C"], ["B:2", "C:2"], ["A:L", "B:R", "C:R"]);
|
|
||||||
|
|
||||||
test(["A", "B"], ["(A)C", "(B)D"], ["A:L", "C:R", "B:L", "D:R"]);
|
|
||||||
test(["A", "B"], ["(X)C", "(X)D"], ["A:L", "B:L", "C:R", "D:R"]);
|
|
||||||
test(["A", "B"], ["(X)C", "(A)D"], ["A:L", "D:R", "B:L", "C:R"]);
|
|
||||||
test(["A", "B"], ["(A)B:1"], ["A:L", "B:R"]);
|
|
||||||
test(["A:2", "B"], ["(A)B:1"], ["A:L", "B:R"]);
|
|
||||||
test(["A:2", "B:2"], ["B:1"], ["A:L", "B:L"]);
|
|
||||||
test(["A:2", "B:2"], ["B:1", "C"], ["A:L", "B:L", "C:R"]);
|
|
||||||
test(["A:2", "B:2"], ["(A)C", "B:1"], ["A:L", "C:R", "B:L"]);
|
|
||||||
test(["A:2", "B:2"], ["(A)C", "B:1"], ["A:L", "C:R", "B:L"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("test identical elements reconciliation", () => {
|
|
||||||
const testIdentical = (
|
|
||||||
local: ElementLike[],
|
|
||||||
remote: ElementLike[],
|
|
||||||
expected: Id[],
|
|
||||||
) => {
|
|
||||||
const ret = reconcileElements(
|
|
||||||
local as any as ExcalidrawElement[],
|
|
||||||
remote as any as ExcalidrawElement[],
|
|
||||||
{} as AppState,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (new Set(ret.map((x) => x.id)).size !== ret.length) {
|
|
||||||
throw new Error("reconcileElements: duplicate elements found");
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(ret.map((x) => x.id)).to.deep.equal(expected);
|
|
||||||
};
|
|
||||||
|
|
||||||
// identical id/version/versionNonce
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
testIdentical(
|
|
||||||
[{ id: "A", version: 1, versionNonce: 1 }],
|
|
||||||
[{ id: "A", version: 1, versionNonce: 1 }],
|
|
||||||
["A"],
|
|
||||||
);
|
|
||||||
testIdentical(
|
|
||||||
[
|
|
||||||
{ id: "A", version: 1, versionNonce: 1 },
|
|
||||||
{ id: "B", version: 1, versionNonce: 1 },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ id: "B", version: 1, versionNonce: 1 },
|
|
||||||
{ id: "A", version: 1, versionNonce: 1 },
|
|
||||||
],
|
|
||||||
["B", "A"],
|
|
||||||
);
|
|
||||||
testIdentical(
|
|
||||||
[
|
|
||||||
{ id: "A", version: 1, versionNonce: 1 },
|
|
||||||
{ id: "B", version: 1, versionNonce: 1 },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ id: "B", version: 1, versionNonce: 1 },
|
|
||||||
{ id: "A", version: 1, versionNonce: 1 },
|
|
||||||
],
|
|
||||||
["B", "A"],
|
|
||||||
);
|
|
||||||
|
|
||||||
// actually identical (arrays and element objects)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const elements1 = [
|
|
||||||
{
|
|
||||||
id: "A",
|
|
||||||
version: 1,
|
|
||||||
versionNonce: 1,
|
|
||||||
[PRECEDING_ELEMENT_KEY]: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "B",
|
|
||||||
version: 1,
|
|
||||||
versionNonce: 1,
|
|
||||||
[PRECEDING_ELEMENT_KEY]: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
testIdentical(elements1, elements1, ["A", "B"]);
|
|
||||||
testIdentical(elements1, elements1.slice(), ["A", "B"]);
|
|
||||||
testIdentical(elements1.slice(), elements1, ["A", "B"]);
|
|
||||||
testIdentical(elements1.slice(), elements1.slice(), ["A", "B"]);
|
|
||||||
|
|
||||||
const el1 = {
|
|
||||||
id: "A",
|
|
||||||
version: 1,
|
|
||||||
versionNonce: 1,
|
|
||||||
[PRECEDING_ELEMENT_KEY]: null,
|
|
||||||
};
|
|
||||||
const el2 = {
|
|
||||||
id: "B",
|
|
||||||
version: 1,
|
|
||||||
versionNonce: 1,
|
|
||||||
[PRECEDING_ELEMENT_KEY]: null,
|
|
||||||
};
|
|
||||||
testIdentical([el1, el2], [el2, el1], ["A", "B"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
|
import { THEME } from "../packages/excalidraw";
|
||||||
|
import { EVENT } from "../packages/excalidraw/constants";
|
||||||
|
import type { Theme } from "../packages/excalidraw/element/types";
|
||||||
|
import { CODES, KEYS } from "../packages/excalidraw/keys";
|
||||||
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
|
||||||
|
export const appThemeAtom = atom<Theme | "system">(
|
||||||
|
(localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) as
|
||||||
|
| Theme
|
||||||
|
| "system"
|
||||||
|
| null) || THEME.LIGHT,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
||||||
|
window.matchMedia?.("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
export const useHandleAppTheme = () => {
|
||||||
|
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
||||||
|
const [editorTheme, setEditorTheme] = useState<Theme>(THEME.LIGHT);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = getDarkThemeMediaQuery();
|
||||||
|
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
setEditorTheme(e.matches ? THEME.DARK : THEME.LIGHT);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (appTheme === "system") {
|
||||||
|
mediaQuery?.addEventListener("change", handleChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
!event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.altKey &&
|
||||||
|
event.shiftKey &&
|
||||||
|
event.code === CODES.D
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
setAppTheme(editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener(EVENT.KEYDOWN, handleKeydown, { capture: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery?.removeEventListener("change", handleChange);
|
||||||
|
document.removeEventListener(EVENT.KEYDOWN, handleKeydown, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [appTheme, editorTheme, setAppTheme]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, appTheme);
|
||||||
|
|
||||||
|
if (appTheme === "system") {
|
||||||
|
setEditorTheme(
|
||||||
|
getDarkThemeMediaQuery()?.matches ? THEME.DARK : THEME.LIGHT,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setEditorTheme(appTheme);
|
||||||
|
}
|
||||||
|
}, [appTheme]);
|
||||||
|
|
||||||
|
return { editorTheme };
|
||||||
|
};
|
||||||
@@ -29,6 +29,9 @@ interface ImportMetaEnv {
|
|||||||
// Enable eslint in dev server
|
// Enable eslint in dev server
|
||||||
VITE_APP_ENABLE_ESLINT: string;
|
VITE_APP_ENABLE_ESLINT: string;
|
||||||
|
|
||||||
|
// Enable PWA in dev server
|
||||||
|
VITE_APP_ENABLE_PWA: string;
|
||||||
|
|
||||||
VITE_APP_PLUS_LP: string;
|
VITE_APP_PLUS_LP: string;
|
||||||
|
|
||||||
VITE_APP_PLUS_APP: string;
|
VITE_APP_PLUS_APP: string;
|
||||||
|
|||||||
@@ -4,191 +4,238 @@ import svgrPlugin from "vite-plugin-svgr";
|
|||||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import checker from "vite-plugin-checker";
|
import checker from "vite-plugin-checker";
|
||||||
|
import { createHtmlPlugin } from "vite-plugin-html";
|
||||||
|
import Sitemap from "vite-plugin-sitemap";
|
||||||
|
import { woff2BrowserPlugin } from "../scripts/woff2/woff2-vite-plugins";
|
||||||
|
|
||||||
// To load .env.local variables
|
export default defineConfig(({ mode }) => {
|
||||||
const envVars = loadEnv("", `../`);
|
// To load .env variables
|
||||||
// https://vitejs.dev/config/
|
const envVars = loadEnv(mode, `../`);
|
||||||
export default defineConfig({
|
// https://vitejs.dev/config/
|
||||||
server: {
|
return {
|
||||||
port: Number(envVars.VITE_APP_PORT || 3000),
|
server: {
|
||||||
// open the browser
|
port: Number(envVars.VITE_APP_PORT || 3000),
|
||||||
open: true,
|
// open the browser
|
||||||
},
|
open: true,
|
||||||
// We need to specify the envDir since now there are no
|
|
||||||
//more located in parallel with the vite.config.ts file but in parent dir
|
|
||||||
envDir: "../",
|
|
||||||
build: {
|
|
||||||
outDir: "build",
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
// Creating separate chunk for locales except for en and percentages.json so they
|
|
||||||
// can be cached at runtime and not merged with
|
|
||||||
// app precache. en.json and percentages.json are needed for first load
|
|
||||||
// or fallback hence not clubbing with locales so first load followed by offline mode works fine. This is how CRA used to work too.
|
|
||||||
manualChunks(id) {
|
|
||||||
if (
|
|
||||||
id.includes("packages/excalidraw/locales") &&
|
|
||||||
id.match(/en.json|percentages.json/) === null
|
|
||||||
) {
|
|
||||||
const index = id.indexOf("locales/");
|
|
||||||
// Taking the substring after "locales/"
|
|
||||||
return `locales/${id.substring(index + 8)}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
// We need to specify the envDir since now there are no
|
||||||
},
|
//more located in parallel with the vite.config.ts file but in parent dir
|
||||||
plugins: [
|
envDir: "../",
|
||||||
react(),
|
build: {
|
||||||
checker({
|
outDir: "build",
|
||||||
typescript: true,
|
rollupOptions: {
|
||||||
eslint:
|
output: {
|
||||||
envVars.VITE_APP_ENABLE_ESLINT === "false"
|
assetFileNames(chunkInfo) {
|
||||||
? undefined
|
if (chunkInfo?.name?.endsWith(".woff2")) {
|
||||||
: { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' },
|
const family = chunkInfo.name.split("-")[0];
|
||||||
overlay: {
|
return `fonts/${family}/[name][extname]`;
|
||||||
initialIsOpen: envVars.VITE_APP_COLLAPSE_OVERLAY === "false",
|
}
|
||||||
badgeStyle: "margin-bottom: 4rem; margin-left: 1rem",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
svgrPlugin(),
|
|
||||||
ViteEjsPlugin(),
|
|
||||||
VitePWA({
|
|
||||||
registerType: "autoUpdate",
|
|
||||||
devOptions: {
|
|
||||||
/* set this flag to true to enable in Development mode */
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
workbox: {
|
return "assets/[name]-[hash][extname]";
|
||||||
// Don't push fonts and locales to app precache
|
|
||||||
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"],
|
|
||||||
runtimeCaching: [
|
|
||||||
{
|
|
||||||
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
|
||||||
handler: "CacheFirst",
|
|
||||||
options: {
|
|
||||||
cacheName: "fonts",
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 50,
|
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
// Creating separate chunk for locales except for en and percentages.json so they
|
||||||
urlPattern: new RegExp("fonts.css"),
|
// can be cached at runtime and not merged with
|
||||||
handler: "StaleWhileRevalidate",
|
// app precache. en.json and percentages.json are needed for first load
|
||||||
options: {
|
// or fallback hence not clubbing with locales so first load followed by offline mode works fine. This is how CRA used to work too.
|
||||||
cacheName: "fonts",
|
manualChunks(id) {
|
||||||
expiration: {
|
if (
|
||||||
maxEntries: 50,
|
id.includes("packages/excalidraw/locales") &&
|
||||||
},
|
id.match(/en.json|percentages.json/) === null
|
||||||
},
|
) {
|
||||||
},
|
const index = id.indexOf("locales/");
|
||||||
{
|
// Taking the substring after "locales/"
|
||||||
urlPattern: new RegExp("locales/[^/]+.js"),
|
return `locales/${id.substring(index + 8)}`;
|
||||||
handler: "CacheFirst",
|
}
|
||||||
options: {
|
|
||||||
cacheName: "locales",
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 50,
|
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
manifest: {
|
|
||||||
short_name: "Excalidraw",
|
|
||||||
name: "Excalidraw",
|
|
||||||
description:
|
|
||||||
"Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.",
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: "android-chrome-192x192.png",
|
|
||||||
sizes: "192x192",
|
|
||||||
type: "image/png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "apple-touch-icon.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "180x180",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "favicon-32x32.png",
|
|
||||||
sizes: "32x32",
|
|
||||||
type: "image/png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "favicon-16x16.png",
|
|
||||||
sizes: "16x16",
|
|
||||||
type: "image/png",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
start_url: "/",
|
|
||||||
display: "standalone",
|
|
||||||
theme_color: "#121212",
|
|
||||||
background_color: "#ffffff",
|
|
||||||
file_handlers: [
|
|
||||||
{
|
|
||||||
action: "/",
|
|
||||||
accept: {
|
|
||||||
"application/vnd.excalidraw+json": [".excalidraw"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
share_target: {
|
|
||||||
action: "/web-share-target",
|
|
||||||
method: "POST",
|
|
||||||
enctype: "multipart/form-data",
|
|
||||||
params: {
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
name: "file",
|
|
||||||
accept: [
|
|
||||||
"application/vnd.excalidraw+json",
|
|
||||||
"application/json",
|
|
||||||
".excalidraw",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
screenshots: [
|
|
||||||
{
|
|
||||||
src: "/screenshots/virtual-whiteboard.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "462x945",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "/screenshots/wireframe.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "462x945",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "/screenshots/illustration.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "462x945",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "/screenshots/shapes.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "462x945",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "/screenshots/collaboration.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "462x945",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "/screenshots/export.png",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "462x945",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}),
|
sourcemap: true,
|
||||||
],
|
// don't auto-inline small assets (i.e. fonts hosted on CDN)
|
||||||
publicDir: "../public",
|
assetsInlineLimit: 0,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
Sitemap({
|
||||||
|
hostname: "https://excalidraw.com",
|
||||||
|
outDir: "build",
|
||||||
|
changefreq: "monthly",
|
||||||
|
// its static in public folder
|
||||||
|
generateRobotsTxt: false,
|
||||||
|
}),
|
||||||
|
woff2BrowserPlugin(),
|
||||||
|
react(),
|
||||||
|
checker({
|
||||||
|
typescript: true,
|
||||||
|
eslint:
|
||||||
|
envVars.VITE_APP_ENABLE_ESLINT === "false"
|
||||||
|
? undefined
|
||||||
|
: { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' },
|
||||||
|
overlay: {
|
||||||
|
initialIsOpen: envVars.VITE_APP_COLLAPSE_OVERLAY === "false",
|
||||||
|
badgeStyle: "margin-bottom: 4rem; margin-left: 1rem",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
svgrPlugin(),
|
||||||
|
ViteEjsPlugin(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
devOptions: {
|
||||||
|
/* set this flag to true to enable in Development mode */
|
||||||
|
enabled: envVars.VITE_APP_ENABLE_PWA === "true",
|
||||||
|
},
|
||||||
|
|
||||||
|
workbox: {
|
||||||
|
// don't precache fonts, locales and separate chunks
|
||||||
|
globIgnores: [
|
||||||
|
"fonts.css",
|
||||||
|
"**/locales/**",
|
||||||
|
"service-worker.js",
|
||||||
|
"**/*.chunk-*.js",
|
||||||
|
],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp(".+.woff2"),
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "fonts",
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 1000,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 90, // 90 days
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
// 0 to cache "opaque" responses from cross-origin requests (i.e. CDN)
|
||||||
|
statuses: [0, 200],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp("fonts.css"),
|
||||||
|
handler: "StaleWhileRevalidate",
|
||||||
|
options: {
|
||||||
|
cacheName: "fonts",
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp("locales/[^/]+.js"),
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "locales",
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 50,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp(".chunk-.+.js"),
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "chunk",
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 50,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
short_name: "Excalidraw",
|
||||||
|
name: "Excalidraw",
|
||||||
|
description:
|
||||||
|
"Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "apple-touch-icon.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "180x180",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "favicon-32x32.png",
|
||||||
|
sizes: "32x32",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "favicon-16x16.png",
|
||||||
|
sizes: "16x16",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
start_url: "/",
|
||||||
|
id:"excalidraw",
|
||||||
|
display: "standalone",
|
||||||
|
theme_color: "#121212",
|
||||||
|
background_color: "#ffffff",
|
||||||
|
file_handlers: [
|
||||||
|
{
|
||||||
|
action: "/",
|
||||||
|
accept: {
|
||||||
|
"application/vnd.excalidraw+json": [".excalidraw"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
share_target: {
|
||||||
|
action: "/web-share-target",
|
||||||
|
method: "POST",
|
||||||
|
enctype: "multipart/form-data",
|
||||||
|
params: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
name: "file",
|
||||||
|
accept: [
|
||||||
|
"application/vnd.excalidraw+json",
|
||||||
|
"application/json",
|
||||||
|
".excalidraw",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
screenshots: [
|
||||||
|
{
|
||||||
|
src: "/screenshots/virtual-whiteboard.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "462x945",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/wireframe.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "462x945",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/illustration.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "462x945",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/shapes.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "462x945",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/collaboration.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "462x945",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/export.png",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "462x945",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createHtmlPlugin({
|
||||||
|
minify: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
publicDir: "../public",
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +1,28 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "excalidraw-monorepo",
|
"name": "excalidraw-monorepo",
|
||||||
|
"packageManager": "yarn@1.22.22",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/excalidraw",
|
||||||
"packages/utils"
|
"packages/utils",
|
||||||
|
"packages/math",
|
||||||
|
"examples/excalidraw",
|
||||||
|
"examples/excalidraw/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
|
||||||
"@excalidraw/random-username": "1.0.0",
|
|
||||||
"@sentry/browser": "6.2.5",
|
|
||||||
"@sentry/integrations": "6.2.5",
|
|
||||||
"firebase": "8.3.3",
|
|
||||||
"i18next-browser-languagedetector": "6.1.4",
|
|
||||||
"idb-keyval": "6.0.3",
|
|
||||||
"jotai": "1.13.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"socket.io-client": "4.7.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
|
||||||
"@excalidraw/eslint-config": "1.0.3",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/react": "18.0.15",
|
"@types/react": "18.2.0",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.2.0",
|
||||||
"@types/socket.io-client": "3.0.0",
|
"@types/socket.io-client": "3.0.0",
|
||||||
"@vitejs/plugin-react": "3.1.0",
|
"@vitejs/plugin-react": "3.1.0",
|
||||||
"@vitest/coverage-v8": "0.33.0",
|
"@vitest/coverage-v8": "2.0.5",
|
||||||
"@vitest/ui": "0.32.2",
|
"@vitest/ui": "2.0.5",
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
@@ -43,34 +36,28 @@
|
|||||||
"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.6",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-checker": "0.6.1",
|
"vite-plugin-checker": "0.7.2",
|
||||||
"vite-plugin-ejs": "1.7.0",
|
"vite-plugin-ejs": "1.7.0",
|
||||||
"vite-plugin-pwa": "0.17.4",
|
"vite-plugin-pwa": "0.17.4",
|
||||||
"vite-plugin-svgr": "2.4.0",
|
"vite-plugin-svgr": "4.2.0",
|
||||||
"vitest": "1.0.1",
|
"vitest": "2.0.5",
|
||||||
"vitest-canvas-mock": "0.3.2"
|
"vitest-canvas-mock": "0.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.0.0 - 20.x.x"
|
"node": "18.0.0 - 22.x.x"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"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": "yarn --cwd ./excalidraw-app build:app:docker",
|
||||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
"build:app": "yarn --cwd ./excalidraw-app build:app",
|
||||||
"build:version": "node ./scripts/build-version.js",
|
"build:version": "yarn --cwd ./excalidraw-app build:version",
|
||||||
"build": "yarn --cwd ./excalidraw-app build",
|
"build": "yarn --cwd ./excalidraw-app build",
|
||||||
"fix:code": "yarn test:code --fix",
|
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
|
||||||
"fix:other": "yarn prettier --write",
|
|
||||||
"fix": "yarn fix:other && yarn fix:code",
|
|
||||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
|
||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
|
||||||
"prepare": "husky install",
|
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
|
||||||
"start": "yarn --cwd ./excalidraw-app start",
|
"start": "yarn --cwd ./excalidraw-app start",
|
||||||
"start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"start:production": "yarn --cwd ./excalidraw-app start:production",
|
||||||
"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",
|
||||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||||
@@ -81,9 +68,22 @@
|
|||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:coverage:watch": "vitest --coverage --watch",
|
"test:coverage:watch": "vitest --coverage --watch",
|
||||||
"test:ui": "yarn test --ui --coverage.enabled=true",
|
"test:ui": "yarn test --ui --coverage.enabled=true",
|
||||||
|
"fix:code": "yarn test:code --fix",
|
||||||
|
"fix:other": "yarn prettier --write",
|
||||||
|
"fix": "yarn fix:other && yarn fix:code",
|
||||||
|
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||||
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
"prerelease:excalidraw": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
"release:excalidraw": "node scripts/release.js",
|
||||||
"release:excalidraw": "node scripts/release.js"
|
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/*/{build,dist}",
|
||||||
|
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
|
||||||
|
"clean-install": "yarn rm:node_modules && yarn install"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "18.2.0",
|
||||||
|
"strip-ansi": "6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
types
|
types
|
||||||
bundle.js
|
|
||||||
bundle.css
|
|
||||||
|
|||||||