Compare commits
30 Commits
master
...
barnabasmo
Author | SHA1 | Date | |
---|---|---|---|
|
5082142b36 | ||
|
74cb027fd7 | ||
|
bc09ac757f | ||
|
66e347f7d2 | ||
|
d5974e66b2 | ||
|
2a1b22a504 | ||
|
b3d241ba7f | ||
|
8ff1ac8097 | ||
|
d967123383 | ||
|
05cd1a79cc | ||
|
bd08bdf4c7 | ||
|
011b268dde | ||
|
b6a7f05761 | ||
|
8787c7d8cf | ||
|
6d21d7cab1 | ||
|
c9df3e143b | ||
|
5b11660cc0 | ||
|
bf0b2965e6 | ||
|
8f8b6e7144 | ||
|
b63d17045e | ||
|
70d48d5472 | ||
|
097000a2b7 | ||
|
461661afc6 | ||
|
c88f3c84eb | ||
|
7d791b86f8 | ||
|
e615056302 | ||
|
14ad745d00 | ||
|
9c3ff73a73 | ||
|
79cf71cccb | ||
|
e094b8b539 |
@ -1,5 +0,0 @@
|
|||||||
FROM node:18-bullseye
|
|
||||||
|
|
||||||
# Vite wants to open the browser using `open`, so we
|
|
||||||
# need to install those utils.
|
|
||||||
RUN apt update -y && apt install -y xdg-utils
|
|
@ -27,10 +27,7 @@
|
|||||||
"start": {
|
"start": {
|
||||||
"name": "Start Excalidraw",
|
"name": "Start Excalidraw",
|
||||||
"command": "yarn start",
|
"command": "yarn start",
|
||||||
"runAtStart": true,
|
"runAtStart": true
|
||||||
"preview": {
|
|
||||||
"port": 3000
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"name": "Run Tests",
|
"name": "Run Tests",
|
||||||
@ -40,11 +37,7 @@
|
|||||||
"install-deps": {
|
"install-deps": {
|
||||||
"name": "Install Dependencies",
|
"name": "Install Dependencies",
|
||||||
"command": "yarn install",
|
"command": "yarn install",
|
||||||
"restartOn": {
|
"restartOn": { "files": ["yarn.lock"] }
|
||||||
"files": ["yarn.lock"],
|
|
||||||
"branch": false,
|
|
||||||
"resume": false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,30 @@
|
|||||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
# 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
|
REACT_APP_WS_SERVER_URL=http://localhost:3002
|
||||||
|
|
||||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
# set this only if using the collaboration workflow we use on excalidraw.com
|
||||||
VITE_APP_PORTAL_URL=
|
REACT_APP_PORTAL_URL=
|
||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||||
|
|
||||||
# put these in your .env.local, or make sure you don't commit!
|
# put these in your .env.local, or make sure you don't commit!
|
||||||
# must be lowercase `true` when turned on
|
# must be lowercase `true` when turned on
|
||||||
#
|
#
|
||||||
# whether to enable Service Workers in development
|
# whether to enable Service Workers in development
|
||||||
VITE_APP_DEV_ENABLE_SW=
|
REACT_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=
|
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
VITE_APP_DISABLE_TRACKING=true
|
REACT_APP_DISABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
# The port the run the dev server
|
|
||||||
VITE_APP_PORT=3000
|
|
||||||
|
|
||||||
#Debug flags
|
#Debug flags
|
||||||
|
|
||||||
# To enable bounding box for text containers
|
# To enable bounding box for text containers
|
||||||
VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
||||||
|
|
||||||
# Set this flag to false if you want to open the overlay by default
|
|
||||||
VITE_APP_COLLAPSE_OVERLAY=true
|
|
||||||
|
|
||||||
# Set this flag to false to disable eslint
|
|
||||||
VITE_APP_ENABLE_ESLINT=true
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
|
REACT_APP_PORTAL_URL=https://portal.excalidraw.com
|
||||||
# Fill to set socket server URL used for collaboration.
|
# Fill to set socket server URL used for collaboration.
|
||||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
|
||||||
VITE_APP_WS_SERVER_URL=
|
REACT_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"}'
|
REACT_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_PLUS_APP=https://app.excalidraw.com
|
REACT_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
VITE_APP_DISABLE_TRACKING=
|
REACT_APP_DISABLE_TRACKING=
|
||||||
|
4
.github/workflows/autorelease-excalidraw.yml
vendored
4
.github/workflows/autorelease-excalidraw.yml
vendored
@ -12,10 +12,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- name: Set up publish access
|
- name: Set up publish access
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
4
.github/workflows/autorelease-preview.yml
vendored
4
.github/workflows/autorelease-preview.yml
vendored
@ -32,10 +32,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ steps.sha.outputs.result }}
|
ref: ${{ steps.sha.outputs.result }}
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- name: Set up publish access
|
- name: Set up publish access
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@ -9,10 +9,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and lint
|
- name: Install and lint
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/locales-coverage.yml
vendored
4
.github/workflows/locales-coverage.yml
vendored
@ -14,10 +14,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/sentry-production.yml
vendored
4
.github/workflows/sentry-production.yml
vendored
@ -10,10 +10,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
26
.github/workflows/test-coverage-pr.yml
vendored
26
.github/workflows/test-coverage-pr.yml
vendored
@ -1,26 +0,0 @@
|
|||||||
name: Test Coverage PR
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: "Install Node"
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "18.x"
|
|
||||||
- name: "Install Deps"
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
- name: "Test Coverage"
|
|
||||||
run: yarn test:coverage
|
|
||||||
- name: "Report Coverage"
|
|
||||||
if: always() # Also generate the report if tests are failing
|
|
||||||
uses: davelosert/vitest-coverage-report-action@v2
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -7,10 +7,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -26,5 +26,3 @@ src/packages/excalidraw/example/public/bundle.js
|
|||||||
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
||||||
src/packages/excalidraw/example/public/excalidraw.development.js
|
src/packages/excalidraw/example/public/excalidraw.development.js
|
||||||
coverage
|
coverage
|
||||||
dev-dist
|
|
||||||
html
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:18 AS build
|
FROM node:14-alpine AS build
|
||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
|
@ -69,10 +69,6 @@ It's also a good idea to consider if your change should include additional tests
|
|||||||
|
|
||||||
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
||||||
|
|
||||||
:::note
|
|
||||||
Some checks, such as the `lint` and `test`, require approval from the maintainers to run.
|
|
||||||
They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Translating
|
## Translating
|
||||||
|
|
||||||
|
@ -6611,19 +6611,19 @@ semver@7.0.0:
|
|||||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||||
|
|
||||||
semver@^5.4.1:
|
semver@^5.4.1:
|
||||||
version "5.7.2"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
|
||||||
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
|
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
|
||||||
version "6.3.1"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
|
semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
|
||||||
version "7.5.4"
|
version "7.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
|
||||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
|
71
package.json
71
package.json
@ -23,6 +23,8 @@
|
|||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "2.0.4",
|
||||||
|
"@radix-ui/react-portal": "1.0.2",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
@ -32,7 +34,6 @@
|
|||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-react": "7.32.2",
|
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.4",
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
@ -52,13 +53,26 @@
|
|||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2",
|
||||||
|
"workbox-background-sync": "^6.5.4",
|
||||||
|
"workbox-broadcast-update": "^6.5.4",
|
||||||
|
"workbox-cacheable-response": "^6.5.4",
|
||||||
|
"workbox-core": "^6.5.4",
|
||||||
|
"workbox-expiration": "^6.5.4",
|
||||||
|
"workbox-google-analytics": "^6.5.4",
|
||||||
|
"workbox-navigation-preload": "^6.5.4",
|
||||||
|
"workbox-precaching": "^6.5.4",
|
||||||
|
"workbox-range-requests": "^6.5.4",
|
||||||
|
"workbox-routing": "^6.5.4",
|
||||||
|
"workbox-strategies": "^6.5.4",
|
||||||
|
"workbox-streams": "^6.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@excalidraw/eslint-config": "1.0.3",
|
"@excalidraw/eslint-config": "1.0.0",
|
||||||
"@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",
|
||||||
@ -69,43 +83,48 @@
|
|||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/resize-observer-browser": "0.1.7",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"@vitejs/plugin-react": "3.1.0",
|
|
||||||
"@vitest/coverage-v8": "0.33.0",
|
|
||||||
"@vitest/ui": "0.32.2",
|
|
||||||
"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",
|
||||||
"eslint-config-react-app": "7.0.1",
|
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jsdom": "22.1.0",
|
"jest-canvas-mock": "2.4.0",
|
||||||
"lint-staged": "12.3.7",
|
"lint-staged": "12.3.7",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"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": "4.4.2",
|
|
||||||
"vite-plugin-checker": "0.6.1",
|
|
||||||
"vite-plugin-ejs": "1.6.4",
|
|
||||||
"vite-plugin-pwa": "0.16.4",
|
|
||||||
"vite-plugin-svgr": "2.4.0",
|
|
||||||
"vitest": "0.32.2",
|
|
||||||
"vitest-canvas-mock": "0.3.2"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
|
"jest": {
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx,ts,tsx}"
|
||||||
|
],
|
||||||
|
"coveragePathIgnorePatterns": [
|
||||||
|
"<rootDir>/locales",
|
||||||
|
"<rootDir>/src/packages/excalidraw/dist/",
|
||||||
|
"<rootDir>/src/packages/excalidraw/types",
|
||||||
|
"<rootDir>/src/packages/excalidraw/example"
|
||||||
|
],
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
|
||||||
|
],
|
||||||
|
"resetMocks": false
|
||||||
|
},
|
||||||
"name": "excalidraw",
|
"name": "excalidraw",
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
|
||||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts 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",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
"fix:code": "yarn test:code --fix",
|
"fix:code": "yarn test:code --fix",
|
||||||
"fix:other": "yarn prettier --write",
|
"fix:other": "yarn prettier --write",
|
||||||
"fix": "yarn fix:other && yarn fix:code",
|
"fix": "yarn fix:other && yarn fix:code",
|
||||||
@ -113,21 +132,19 @@
|
|||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "vite",
|
"start": "react-scripts start",
|
||||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
||||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
|
||||||
"test:app": "vitest --config vitest.config.ts",
|
"test:app": "react-scripts test --passWithNoTests",
|
||||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||||
|
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
||||||
"test:other": "yarn prettier --list-different",
|
"test:other": "yarn prettier --list-different",
|
||||||
"test:typecheck": "tsc",
|
"test:typecheck": "tsc",
|
||||||
"test:update": "yarn test:app --update --watch=false",
|
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "react-scripts test --passWithNoTests --coverage --watchAll",
|
||||||
"test:coverage:watch": "vitest --coverage --watch",
|
|
||||||
"test:ui": "yarn test --ui",
|
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease": "node scripts/prerelease.js",
|
"prerelease": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
|
||||||
"release": "node scripts/release.js"
|
"release": "node scripts/release.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,8 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<% if ("%PROD%" === "true") { %>
|
|
||||||
|
<% if (process.env.NODE_ENV === "production") { %>
|
||||||
<script>
|
<script>
|
||||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||||
//
|
//
|
||||||
@ -99,35 +100,41 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
||||||
|
|
||||||
<!-- Excalidraw version -->
|
<!-- Excalidraw version -->
|
||||||
<meta name="version" content="{version}" />
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="/Virgil.woff2"
|
href="Virgil.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="/Cascadia.woff2"
|
href="Cascadia.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/fonts.css" type="text/css" />
|
<link
|
||||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
rel="manifest"
|
||||||
|
href="manifest.json"
|
||||||
|
style="--pwacompat-splash-font: 24px Virgil"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||||
|
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %>
|
||||||
<script>
|
<script>
|
||||||
{
|
{
|
||||||
const _WebSocket = window.WebSocket;
|
const _WebSocket = window.WebSocket;
|
||||||
window.WebSocket = function (url) {
|
window.WebSocket = function (url) {
|
||||||
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
||||||
console.info(
|
console.info(
|
||||||
"[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return new _WebSocket(url);
|
return new _WebSocket(url);
|
||||||
@ -193,8 +200,7 @@
|
|||||||
<h1 class="visually-hidden">Excalidraw</h1>
|
<h1 class="visually-hidden">Excalidraw</h1>
|
||||||
</header>
|
</header>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
|
||||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
|
|
||||||
<!-- 100% privacy friendly analytics -->
|
<!-- 100% privacy friendly analytics -->
|
||||||
<script>
|
<script>
|
||||||
// need to load this script dynamically bcs. of iframe embed tracking
|
// need to load this script dynamically bcs. of iframe embed tracking
|
@ -1,20 +0,0 @@
|
|||||||
// Since we migrated to Vite, the service worker strategy changed, in CRA it was a custom service worker named service-worker.js and in Vite its sw.js handled by vite-plugin-pwa
|
|
||||||
// Due to this the existing CRA users were not able to migrate to Vite or any new changes post Vite unless browser is hard refreshed
|
|
||||||
// Hence adding a self destroying worker so all CRA service workers are destroyed and migrated to Vite
|
|
||||||
// We should remove this code after sometime when we are confident that
|
|
||||||
// all users have migrated to Vite
|
|
||||||
|
|
||||||
self.addEventListener("install", () => {
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("activate", () => {
|
|
||||||
self.registration
|
|
||||||
.unregister()
|
|
||||||
.then(() => {
|
|
||||||
return self.clients.matchAll();
|
|
||||||
})
|
|
||||||
.then((clients) => {
|
|
||||||
clients.forEach((client) => client.navigate(client.url));
|
|
||||||
});
|
|
||||||
});
|
|
2
public/workbox/workbox-background-sync.prod.js
Normal file
2
public/workbox/workbox-background-sync.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s){"use strict";try{self["workbox:background-sync:4.3.1"]&&_()}catch(t){}const i=3,n="workbox-background-sync",a="requests",r="queueName";class c{constructor(t){this.t=t,this.s=new s.DBWrapper(n,i,{onupgradeneeded:this.i})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add(a,t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching(a,{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add(a,t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async getAll(){return await this.s.getAllMatching(a,{index:r,query:IDBKeyRange.only(this.t)})}async deleteEntry(t){await this.s.delete(a,t)}async h({direction:t}){const[e]=await this.s.getAllMatching(a,{direction:t,index:r,query:IDBKeyRange.only(this.t),count:1});if(e)return await this.deleteEntry(e.id),e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersion<i&&e.objectStoreNames.contains(a)&&e.deleteObjectStore(a),e.createObjectStore(a,{autoIncrement:!0,keyPath:"id"}).createIndex(r,r,{unique:!1})}}const h=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];class o{static async fromRequest(t){const e={url:t.url,headers:{}};"GET"!==t.method&&(e.body=await t.clone().arrayBuffer());for(const[s,i]of t.headers.entries())e.headers[s]=i;for(const s of h)void 0!==t[s]&&(e[s]=t[s]);return new o(e)}constructor(t){"navigate"===t.mode&&(t.mode="same-origin"),this.o=t}toObject(){const t=Object.assign({},this.o);return t.headers=Object.assign({},this.o.headers),t.body&&(t.body=t.body.slice(0)),t}toRequest(){return new Request(this.o.url,this.o)}clone(){return new o(this.toObject())}}const u="workbox-background-sync",y=10080,w=new Set;class d{constructor(t,{onSync:s,maxRetentionTime:i}={}){if(w.has(t))throw new e.WorkboxError("duplicate-queue-name",{name:t});w.add(t),this.u=t,this.l=s||this.replayRequests,this.q=i||y,this.m=new c(this.u),this.p()}get name(){return this.u}async pushRequest(t){await this.g(t,"push")}async unshiftRequest(t){await this.g(t,"unshift")}async popRequest(){return this.R("pop")}async shiftRequest(){return this.R("shift")}async getAll(){const t=await this.m.getAll(),e=Date.now(),s=[];for(const i of t){const t=60*this.q*1e3;e-i.timestamp>t?await this.m.deleteEntry(i.id):s.push(f(i))}return s}async g({request:t,metadata:e,timestamp:s=Date.now()},i){const n={requestData:(await o.fromRequest(t.clone())).toObject(),timestamp:s};e&&(n.metadata=e),await this.m[`${i}Entry`](n),this.k?this.D=!0:await this.registerSync()}async R(t){const e=Date.now(),s=await this.m[`${t}Entry`]();if(s){const i=60*this.q*1e3;return e-s.timestamp>i?this.R(t):f(s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request.clone())}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.u})}}async registerSync(){if("sync"in registration)try{await registration.sync.register(`${u}:${this.u}`)}catch(t){}}p(){"sync"in registration?self.addEventListener("sync",t=>{if(t.tag===`${u}:${this.u}`){const e=async()=>{let e;this.k=!0;try{await this.l({queue:this})}catch(t){throw e=t}finally{!this.D||e&&!t.lastChance||await this.registerSync(),this.k=!1,this.D=!1}};t.waitUntil(e())}}):this.l({queue:this})}static get _(){return w}}const f=t=>{const e={request:new o(t.requestData).toRequest(),timestamp:t.timestamp};return t.metadata&&(e.metadata=t.metadata),e};return t.Queue=d,t.Plugin=class{constructor(...t){this.v=new d(...t),this.fetchDidFail=this.fetchDidFail.bind(this)}async fetchDidFail({request:t}){await this.v.pushRequest({request:t})}},t}({},workbox.core._private,workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-background-sync.prod.js.map
|
2
public/workbox/workbox-broadcast-update.prod.js
Normal file
2
public/workbox/workbox-broadcast-update.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(e,t){"use strict";try{self["workbox:broadcast-update:4.3.1"]&&_()}catch(e){}const s=(e,t,s)=>{return!s.some(s=>e.headers.has(s)&&t.headers.has(s))||s.every(s=>{const n=e.headers.has(s)===t.headers.has(s),a=e.headers.get(s)===t.headers.get(s);return n&&a})},n="workbox",a=1e4,i=["content-length","etag","last-modified"],o=async({channel:e,cacheName:t,url:s})=>{const n={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:{cacheName:t,updatedURL:s}};if(e)e.postMessage(n);else{const e=await clients.matchAll({type:"window"});for(const t of e)t.postMessage(n)}};class c{constructor({headersToCheck:e,channelName:t,deferNoticationTimeout:s}={}){this.t=e||i,this.s=t||n,this.i=s||a,this.o()}notifyIfUpdated({oldResponse:e,newResponse:t,url:n,cacheName:a,event:i}){if(!s(e,t,this.t)){const e=(async()=>{i&&i.request&&"navigate"===i.request.mode&&await this.h(i),await this.l({channel:this.u(),cacheName:a,url:n})})();if(i)try{i.waitUntil(e)}catch(e){}return e}}async l(e){await o(e)}u(){return"BroadcastChannel"in self&&!this.p&&(this.p=new BroadcastChannel(this.s)),this.p}h(e){if(!this.m.has(e)){const s=new t.Deferred;this.m.set(e,s);const n=setTimeout(()=>{s.resolve()},this.i);s.promise.then(()=>clearTimeout(n))}return this.m.get(e).promise}o(){this.m=new Map,self.addEventListener("message",e=>{if("WINDOW_READY"===e.data.type&&"workbox-window"===e.data.meta&&this.m.size>0){for(const e of this.m.values())e.resolve();this.m.clear()}})}}return e.BroadcastCacheUpdate=c,e.Plugin=class{constructor(e){this.l=new c(e)}cacheDidUpdate({cacheName:e,oldResponse:t,newResponse:s,request:n,event:a}){t&&this.l.notifyIfUpdated({cacheName:e,oldResponse:t,newResponse:s,event:a,url:n.url})}},e.broadcastUpdate=o,e.responsesAreSame=s,e}({},workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-broadcast-update.prod.js.map
|
2
public/workbox/workbox-cacheable-response.prod.js
Normal file
2
public/workbox/workbox-cacheable-response.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.3.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({});
|
||||||
|
//# sourceMappingURL=workbox-cacheable-response.prod.js.map
|
2
public/workbox/workbox-core.prod.js
Normal file
2
public/workbox/workbox-core.prod.js
Normal file
File diff suppressed because one or more lines are too long
2
public/workbox/workbox-expiration.prod.js
Normal file
2
public/workbox/workbox-expiration.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,n){"use strict";try{self["workbox:expiration:4.3.1"]&&_()}catch(t){}const h="workbox-expiration",c="cache-entries",r=t=>{const e=new URL(t,location);return e.hash="",e.href};class o{constructor(t){this.t=t,this.s=new e.DBWrapper(h,1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore(c,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),s.deleteDatabase(this.t)}async setTimestamp(t,e){t=r(t),await this.s.put(c,{url:t,timestamp:e,cacheName:this.t,id:this.h(t)})}async getTimestamp(t){return(await this.s.get(c,this.h(t))).timestamp}async expireEntries(t,e){const s=await this.s.transaction(c,"readwrite",(s,i)=>{const a=s.objectStore(c),n=[];let h=0;a.index("timestamp").openCursor(null,"prev").onsuccess=(({target:s})=>{const a=s.result;if(a){const s=a.value;s.cacheName===this.t&&(t&&s.timestamp<t||e&&h>=e?n.push(a.value):h++),a.continue()}else i(n)})}),i=[];for(const t of s)await this.s.delete(c,t.id),i.push(t.url);return i}h(t){return this.t+"|"+r(t)}}class u{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.p=e.maxAgeSeconds,this.t=t,this.m=new o(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.p?Date.now()-1e3*this.p:void 0,e=await this.m.expireEntries(t,this.l),s=await caches.open(this.t);for(const t of e)await s.delete(t);this.o=!1,this.u&&(this.u=!1,this.expireEntries())}async updateTimestamp(t){await this.m.setTimestamp(t,Date.now())}async isURLExpired(t){return await this.m.getTimestamp(t)<Date.now()-1e3*this.p}async delete(){this.u=!1,await this.m.expireEntries(1/0)}}return t.CacheExpiration=u,t.Plugin=class{constructor(t={}){this.D=t,this.p=t.maxAgeSeconds,this.g=new Map,t.purgeOnQuotaError&&n.registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata())}k(t){if(t===a.cacheNames.getRuntimeName())throw new i.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new u(t,this.D),this.g.set(t,e)),e}cachedResponseWillBeUsed({event:t,request:e,cacheName:s,cachedResponse:i}){if(!i)return null;let a=this.N(i);const n=this.k(s);n.expireEntries();const h=n.updateTimestamp(e.url);if(t)try{t.waitUntil(h)}catch(t){}return a?i:null}N(t){if(!this.p)return!0;const e=this._(t);return null===e||e>=Date.now()-1e3*this.p}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async cacheDidUpdate({cacheName:t,request:e}){const s=this.k(t);await s.updateTimestamp(e.url),await s.expireEntries()}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core);
|
||||||
|
//# sourceMappingURL=workbox-expiration.prod.js.map
|
2
public/workbox/workbox-navigation-preload.prod.js
Normal file
2
public/workbox/workbox-navigation-preload.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:4.3.1"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({});
|
||||||
|
//# sourceMappingURL=workbox-navigation-preload.prod.js.map
|
2
public/workbox/workbox-offline-ga.prod.js
Normal file
2
public/workbox/workbox-offline-ga.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(e,t,o,n,a,c,w){"use strict";try{self["workbox:google-analytics:4.3.1"]&&_()}catch(e){}const r=/^\/(\w+\/)?collect/,s=e=>async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:n,timestamp:a}=o,c=new URL(n.url);try{const w="POST"===n.method?new URLSearchParams(await n.clone().text()):c.searchParams,r=a-(Number(w.get("qt"))||0),s=Date.now()-r;if(w.set("qt",s),e.parameterOverrides)for(const t of Object.keys(e.parameterOverrides)){const o=e.parameterOverrides[t];w.set(t,o)}"function"==typeof e.hitFilter&&e.hitFilter.call(null,w),await fetch(new Request(c.origin+c.pathname,{body:w.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}},i=e=>{const t=({url:e})=>"www.google-analytics.com"===e.hostname&&r.test(e.pathname),o=new w.NetworkOnly({plugins:[e]});return[new n.Route(t,o,"GET"),new n.Route(t,o,"POST")]},l=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,t,"GET")},m=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,t,"GET")},u=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,t,"GET")};return e.initialize=((e={})=>{const n=o.cacheNames.getGoogleAnalyticsName(e.cacheName),c=new t.Plugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:s(e)}),w=[u(n),l(n),m(n),...i(c)],r=new a.Router;for(const e of w)r.registerRoute(e);r.addFetchListener()}),e}({},workbox.backgroundSync,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies);
|
||||||
|
//# sourceMappingURL=workbox-offline-ga.prod.js.map
|
2
public/workbox/workbox-precaching.prod.js
Normal file
2
public/workbox/workbox-precaching.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.precaching=function(t,e,n,s,c){"use strict";try{self["workbox:precaching:4.3.1"]&&_()}catch(t){}const o=[],i={get:()=>o,add(t){o.push(...t)}};const a="__WB_REVISION__";function r(t){if(!t)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location);return{cacheKey:t.href,url:t.href}}const s=new URL(n,location),o=new URL(n,location);return o.searchParams.set(a,e),{cacheKey:o.href,url:s.href}}class l{constructor(t){this.t=e.cacheNames.getPrecacheName(t),this.s=new Map}addToCacheList(t){for(const e of t){const{cacheKey:t,url:n}=r(e);if(this.s.has(n)&&this.s.get(n)!==t)throw new c.WorkboxError("add-to-cache-list-conflicting-entries",{firstEntry:this.s.get(n),secondEntry:t});this.s.set(n,t)}}async install({event:t,plugins:e}={}){const n=[],s=[],c=await caches.open(this.t),o=await c.keys(),i=new Set(o.map(t=>t.url));for(const t of this.s.values())i.has(t)?s.push(t):n.push(t);const a=n.map(n=>this.o({event:t,plugins:e,url:n}));return await Promise.all(a),{updatedURLs:n,notUpdatedURLs:s}}async activate(){const t=await caches.open(this.t),e=await t.keys(),n=new Set(this.s.values()),s=[];for(const c of e)n.has(c.url)||(await t.delete(c),s.push(c.url));return{deletedURLs:s}}async o({url:t,event:e,plugins:o}){const i=new Request(t,{credentials:"same-origin"});let a,r=await s.fetchWrapper.fetch({event:e,plugins:o,request:i});for(const t of o||[])"cacheWillUpdate"in t&&(a=t.cacheWillUpdate.bind(t));if(!(a?a({event:e,request:i,response:r}):r.status<400))throw new c.WorkboxError("bad-precaching-response",{url:t,status:r.status});r.redirected&&(r=await async function(t){const e=t.clone(),n="body"in e?Promise.resolve(e.body):e.blob(),s=await n;return new Response(s,{headers:e.headers,status:e.status,statusText:e.statusText})}(r)),await n.cacheWrapper.put({event:e,plugins:o,request:i,response:r,cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.s}getCachedURLs(){return[...this.s.keys()]}getCacheKeyForURL(t){const e=new URL(t,location);return this.s.get(e.href)}}let u;const h=()=>(u||(u=new l),u);const d=(t,e)=>{const n=h().getURLsToCacheKeys();for(const s of function*(t,{ignoreURLParametersMatching:e,directoryIndex:n,cleanURLs:s,urlManipulation:c}={}){const o=new URL(t,location);o.hash="",yield o.href;const i=function(t,e){for(const n of[...t.searchParams.keys()])e.some(t=>t.test(n))&&t.searchParams.delete(n);return t}(o,e);if(yield i.href,n&&i.pathname.endsWith("/")){const t=new URL(i);t.pathname+=n,yield t.href}if(s){const t=new URL(i);t.pathname+=".html",yield t.href}if(c){const t=c({url:o});for(const e of t)yield e.href}}(t,e)){const t=n.get(s);if(t)return t}};let w=!1;const f=t=>{w||((({ignoreURLParametersMatching:t=[/^utm_/],directoryIndex:n="index.html",cleanURLs:s=!0,urlManipulation:c=null}={})=>{const o=e.cacheNames.getPrecacheName();addEventListener("fetch",e=>{const i=d(e.request.url,{cleanURLs:s,directoryIndex:n,ignoreURLParametersMatching:t,urlManipulation:c});if(!i)return;let a=caches.open(o).then(t=>t.match(i)).then(t=>t||fetch(i));e.respondWith(a)})})(t),w=!0)},y=t=>{const e=h(),n=i.get();t.waitUntil(e.install({event:t,plugins:n}).catch(t=>{throw t}))},p=t=>{const e=h(),n=i.get();t.waitUntil(e.activate({event:t,plugins:n}))},L=t=>{h().addToCacheList(t),t.length>0&&(addEventListener("install",y),addEventListener("activate",p))};return t.addPlugins=(t=>{i.add(t)}),t.addRoute=f,t.cleanupOutdatedCaches=(()=>{addEventListener("activate",t=>{const n=e.cacheNames.getPrecacheName();t.waitUntil((async(t,e="-precache-")=>{const n=(await caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==t);return await Promise.all(n.map(t=>caches.delete(t))),n})(n).then(t=>{}))})}),t.getCacheKeyForURL=(t=>{return h().getCacheKeyForURL(t)}),t.precache=L,t.precacheAndRoute=((t,e)=>{L(t),f(e)}),t.PrecacheController=l,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-precaching.prod.js.map
|
2
public/workbox/workbox-range-requests.prod.js
Normal file
2
public/workbox/workbox-range-requests.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.rangeRequests=function(e,n){"use strict";try{self["workbox:range-requests:4.3.1"]&&_()}catch(e){}async function t(e,t){try{if(206===t.status)return t;const s=e.headers.get("range");if(!s)throw new n.WorkboxError("no-range-header");const a=function(e){const t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new n.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new n.WorkboxError("single-range-only",{normalizedRangeHeader:t});const s=/(\d*)-(\d*)/.exec(t);if(null===s||!s[1]&&!s[2])throw new n.WorkboxError("invalid-range-values",{normalizedRangeHeader:t});return{start:""===s[1]?null:Number(s[1]),end:""===s[2]?null:Number(s[2])}}(s),r=await t.blob(),i=function(e,t,s){const a=e.size;if(s>a||t<0)throw new n.WorkboxError("range-not-satisfiable",{size:a,end:s,start:t});let r,i;return null===t?(r=a-s,i=a):null===s?(r=t,i=a):(r=t,i=s+1),{start:r,end:i}}(r,a.start,a.end),o=r.slice(i.start,i.end),u=o.size,l=new Response(o,{status:206,statusText:"Partial Content",headers:t.headers});return l.headers.set("Content-Length",u),l.headers.set("Content-Range",`bytes ${i.start}-${i.end-1}/`+r.size),l}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return e.createPartialResponse=t,e.Plugin=class{async cachedResponseWillBeUsed({request:e,cachedResponse:n}){return n&&e.headers.has("range")?await t(e,n):n}},e}({},workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-range-requests.prod.js.map
|
2
public/workbox/workbox-routing.prod.js
Normal file
2
public/workbox/workbox-routing.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.routing=function(t,e,r){"use strict";try{self["workbox:routing:4.3.1"]&&_()}catch(t){}const s="GET",n=t=>t&&"object"==typeof t?t:{handle:t};class o{constructor(t,e,r){this.handler=n(e),this.match=t,this.method=r||s}}class i extends o{constructor(t,{whitelist:e=[/./],blacklist:r=[]}={}){super(t=>this.t(t),t),this.s=e,this.o=r}t({url:t,request:e}){if("navigate"!==e.mode)return!1;const r=t.pathname+t.search;for(const t of this.o)if(t.test(r))return!1;return!!this.s.some(t=>t.test(r))}}class u extends o{constructor(t,e,r){super(({url:e})=>{const r=t.exec(e.href);return r?e.origin!==location.origin&&0!==r.index?null:r.slice(1):null},e,r)}}class c{constructor(){this.i=new Map}get routes(){return this.i}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,r=this.handleRequest({request:e,event:t});r&&t.respondWith(r)})}addCacheListener(){self.addEventListener("message",async t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,r=Promise.all(e.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);const e=new Request(...t);return this.handleRequest({request:e})}));t.waitUntil(r),t.ports&&t.ports[0]&&(await r,t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const r=new URL(t.url,location);if(!r.protocol.startsWith("http"))return;let s,{params:n,route:o}=this.findMatchingRoute({url:r,request:t,event:e}),i=o&&o.handler;if(!i&&this.u&&(i=this.u),i){try{s=i.handle({url:r,request:t,event:e,params:n})}catch(t){s=Promise.reject(t)}return s&&this.h&&(s=s.catch(t=>this.h.handle({url:r,event:e,err:t}))),s}}findMatchingRoute({url:t,request:e,event:r}){const s=this.i.get(e.method)||[];for(const n of s){let s,o=n.match({url:t,request:e,event:r});if(o)return Array.isArray(o)&&o.length>0?s=o:o.constructor===Object&&Object.keys(o).length>0&&(s=o),{route:n,params:s}}return{}}setDefaultHandler(t){this.u=n(t)}setCatchHandler(t){this.h=n(t)}registerRoute(t){this.i.has(t.method)||this.i.set(t.method,[]),this.i.get(t.method).push(t)}unregisterRoute(t){if(!this.i.has(t.method))throw new r.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const e=this.i.get(t.method).indexOf(t);if(!(e>-1))throw new r.WorkboxError("unregister-route-route-not-registered");this.i.get(t.method).splice(e,1)}}let a;const h=()=>(a||((a=new c).addFetchListener(),a.addCacheListener()),a);return t.NavigationRoute=i,t.RegExpRoute=u,t.registerNavigationRoute=((t,r={})=>{const s=e.cacheNames.getPrecacheName(r.cacheName),n=new i(async()=>{try{const e=await caches.match(t,{cacheName:s});if(e)return e;throw new Error(`The cache ${s} did not have an entry for `+`${t}.`)}catch(e){return fetch(t)}},{whitelist:r.whitelist,blacklist:r.blacklist});return h().registerRoute(n),n}),t.registerRoute=((t,e,s="GET")=>{let n;if("string"==typeof t){const r=new URL(t,location);n=new o(({url:t})=>t.href===r.href,e,s)}else if(t instanceof RegExp)n=new u(t,e,s);else if("function"==typeof t)n=new o(t,e,s);else{if(!(t instanceof o))throw new r.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});n=t}return h().registerRoute(n),n}),t.Route=o,t.Router=c,t.setCatchHandler=(t=>{h().setCatchHandler(t)}),t.setDefaultHandler=(t=>{h().setDefaultHandler(t)}),t}({},workbox.core._private,workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-routing.prod.js.map
|
2
public/workbox/workbox-strategies.prod.js
Normal file
2
public/workbox/workbox-strategies.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.strategies=function(e,t,s,n,r){"use strict";try{self["workbox:strategies:4.3.1"]&&_()}catch(e){}class i{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));let n,i=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!i)try{i=await this.u(t,e)}catch(e){n=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:n});return i}async u(e,t){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=r.clone(),h=s.cacheWrapper.put({cacheName:this.t,request:e,response:i,event:t,plugins:this.s});if(t)try{t.waitUntil(h)}catch(e){}return r}}class h{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!n)throw new r.WorkboxError("no-response",{url:t.url});return n}}const u={cacheWillUpdate:({response:e})=>200===e.status||0===e.status?e:null};class a{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.o=e.networkTimeoutSeconds,this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){const s=[];"string"==typeof t&&(t=new Request(t));const n=[];let i;if(this.o){const{id:r,promise:h}=this.l({request:t,event:e,logs:s});i=r,n.push(h)}const h=this.q({timeoutId:i,request:t,event:e,logs:s});n.push(h);let u=await Promise.race(n);if(u||(u=await h),!u)throw new r.WorkboxError("no-response",{url:t.url});return u}l({request:e,logs:t,event:s}){let n;return{promise:new Promise(t=>{n=setTimeout(async()=>{t(await this.p({request:e,event:s}))},1e3*this.o)}),id:n}}async q({timeoutId:e,request:t,logs:r,event:i}){let h,u;try{u=await n.fetchWrapper.fetch({request:t,event:i,fetchOptions:this.i,plugins:this.s})}catch(e){h=e}if(e&&clearTimeout(e),h||!u)u=await this.p({request:t,event:i});else{const e=u.clone(),n=s.cacheWrapper.put({cacheName:this.t,request:t,response:e,event:i,plugins:this.s});if(i)try{i.waitUntil(n)}catch(e){}}return u}p({event:e,request:t}){return s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s})}}class c{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){let s,i;"string"==typeof t&&(t=new Request(t));try{i=await n.fetchWrapper.fetch({request:t,event:e,fetchOptions:this.i,plugins:this.s})}catch(e){s=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:s});return i}}class o{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=this.u({request:t,event:e});let i,h=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(h){if(e)try{e.waitUntil(n)}catch(i){}}else try{h=await n}catch(e){i=e}if(!h)throw new r.WorkboxError("no-response",{url:t.url,error:i});return h}async u({request:e,event:t}){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=s.cacheWrapper.put({cacheName:this.t,request:e,response:r.clone(),event:t,plugins:this.s});if(t)try{t.waitUntil(i)}catch(e){}return r}}const l={cacheFirst:i,cacheOnly:h,networkFirst:a,networkOnly:c,staleWhileRevalidate:o},q=e=>{const t=l[e];return e=>new t(e)},w=q("cacheFirst"),p=q("cacheOnly"),v=q("networkFirst"),y=q("networkOnly"),m=q("staleWhileRevalidate");return e.CacheFirst=i,e.CacheOnly=h,e.NetworkFirst=a,e.NetworkOnly=c,e.StaleWhileRevalidate=o,e.cacheFirst=w,e.cacheOnly=p,e.networkFirst=v,e.networkOnly=y,e.staleWhileRevalidate=m,e}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-strategies.prod.js.map
|
2
public/workbox/workbox-streams.prod.js
Normal file
2
public/workbox/workbox-streams.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.streams=function(e){"use strict";try{self["workbox:streams:4.3.1"]&&_()}catch(e){}function n(e){const n=e.map(e=>Promise.resolve(e).then(e=>(function(e){return e.body&&e.body.getReader?e.body.getReader():e.getReader?e.getReader():new Response(e).body.getReader()})(e)));let t,r;const s=new Promise((e,n)=>{t=e,r=n});let o=0;return{done:s,stream:new ReadableStream({pull(e){return n[o].then(e=>e.read()).then(r=>{if(r.done)return++o>=n.length?(e.close(),void t()):this.pull(e);e.enqueue(r.value)}).catch(e=>{throw r(e),e})},cancel(){t()}})}}function t(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function r(e,r){const{done:s,stream:o}=n(e),a=t(r);return{done:s,response:new Response(o,{headers:a})}}let s=void 0;function o(){if(void 0===s)try{new ReadableStream({start(){}}),s=!0}catch(e){s=!1}return s}return e.concatenate=n,e.concatenateToResponse=r,e.isSupported=o,e.strategy=function(e,n){return async({event:s,url:a,params:c})=>{if(o()){const{done:t,response:o}=r(e.map(e=>e({event:s,url:a,params:c})),n);return s.waitUntil(t),o}const i=await Promise.all(e.map(e=>e({event:s,url:a,params:c})).map(async e=>{const n=await e;return n instanceof Response?n.blob():n})),u=t(n);return new Response(new Blob(i),{headers:u})}},e}({});
|
||||||
|
//# sourceMappingURL=workbox-streams.prod.js.map
|
2
public/workbox/workbox-sw.js
Normal file
2
public/workbox/workbox-sw.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}();
|
||||||
|
//# sourceMappingURL=workbox-sw.js.map
|
2
public/workbox/workbox-window.prod.es5.mjs
Normal file
2
public/workbox/workbox-window.prod.es5.mjs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
try{self["workbox:window:4.3.1"]&&_()}catch(n){}var n=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function t(n,t){for(var i=0;i<t.length;i++){var e=t[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(n,e.key,e)}}function i(n){if(void 0===n)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n}try{self["workbox:core:4.3.1"]&&_()}catch(n){}var e=function(){var n=this;this.promise=new Promise(function(t,i){n.resolve=t,n.reject=i})},r=function(n,t){return new URL(n,location).href===new URL(t,location).href},o=function(n,t){Object.assign(this,t,{type:n})};function u(n){return function(){for(var t=[],i=0;i<arguments.length;i++)t[i]=arguments[i];try{return Promise.resolve(n.apply(this,t))}catch(n){return Promise.reject(n)}}}function a(n,t,i){return i?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function s(){}var c=function(c){var f,h;function v(n,t){var r;return void 0===t&&(t={}),(r=c.call(this)||this).t=n,r.i=t,r.o=0,r.u=new e,r.s=new e,r.h=new e,r.v=r.v.bind(i(i(r))),r.l=r.l.bind(i(i(r))),r.g=r.g.bind(i(i(r))),r.m=r.m.bind(i(i(r))),r}h=c,(f=v).prototype=Object.create(h.prototype),f.prototype.constructor=f,f.__proto__=h;var l,w,g,d=v.prototype;return d.register=u(function(n){var t,i,e=this,u=(void 0===n?{}:n).immediate,c=void 0!==u&&u;return t=function(){return e.p=Boolean(navigator.serviceWorker.controller),e.P=e.R(),a(e.k(),function(n){e.B=n,e.P&&(e.O=e.P,e.s.resolve(e.P),e.h.resolve(e.P),e.j(e.P),e.P.addEventListener("statechange",e.l,{once:!0}));var t=e.B.waiting;return t&&r(t.scriptURL,e.t)&&(e.O=t,Promise.resolve().then(function(){e.dispatchEvent(new o("waiting",{sw:t,wasWaitingBeforeRegister:!0}))})),e.O&&e.u.resolve(e.O),e.B.addEventListener("updatefound",e.g),navigator.serviceWorker.addEventListener("controllerchange",e.m,{once:!0}),"BroadcastChannel"in self&&(e.C=new BroadcastChannel("workbox"),e.C.addEventListener("message",e.v)),navigator.serviceWorker.addEventListener("message",e.v),e.B})},(i=function(){if(!c&&"complete"!==document.readyState)return function(n,t){if(!t)return n&&n.then?n.then(s):Promise.resolve()}(new Promise(function(n){return addEventListener("load",n)}))}())&&i.then?i.then(t):t(i)}),d.getSW=u(function(){return this.O||this.u.promise}),d.messageSW=u(function(t){return a(this.getSW(),function(i){return n(i,t)})}),d.R=function(){var n=navigator.serviceWorker.controller;if(n&&r(n.scriptURL,this.t))return n},d.k=u(function(){var n=this;return function(n,t){try{var i=n()}catch(n){return t(n)}return i&&i.then?i.then(void 0,t):i}(function(){return a(navigator.serviceWorker.register(n.t,n.i),function(t){return n.L=performance.now(),t})},function(n){throw n})}),d.j=function(t){n(t,{type:"WINDOW_READY",meta:"workbox-window"})},d.g=function(){var n=this.B.installing;this.o>0||!r(n.scriptURL,this.t)||performance.now()>this.L+6e4?(this.W=n,this.B.removeEventListener("updatefound",this.g)):(this.O=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},d.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.W,u=r?"external":"",a={sw:i,originalEvent:n};!r&&this.p&&(a.isUpdate=!0),this.dispatchEvent(new o(u+e,a)),"installed"===e?this._=setTimeout(function(){"installed"===e&&t.B.waiting===i&&t.dispatchEvent(new o(u+"waiting",a))},200):"activating"===e&&(clearTimeout(this._),r||this.s.resolve(i))},d.m=function(n){var t=this.O;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new o("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},d.v=function(n){var t=n.data;this.dispatchEvent(new o("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&t(l.prototype,w),g&&t(l,g),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.T(n).add(t)},t.removeEventListener=function(n,t){this.T(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.T(n.type).forEach(function(t){return t(n)})},t.T=function(n){return this.D[n]=this.D[n]||new Set},n}());export{c as Workbox,n as messageSW};
|
||||||
|
//# sourceMappingURL=workbox-window.prod.es5.mjs.map
|
2
public/workbox/workbox-window.prod.mjs
Normal file
2
public/workbox/workbox-window.prod.mjs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
try{self["workbox:window:4.3.1"]&&_()}catch(t){}const t=(t,s)=>new Promise(i=>{let e=new MessageChannel;e.port1.onmessage=(t=>i(t.data)),t.postMessage(s,[e.port2])});try{self["workbox:core:4.3.1"]&&_()}catch(t){}class s{constructor(){this.promise=new Promise((t,s)=>{this.resolve=t,this.reject=s})}}class i{constructor(){this.t={}}addEventListener(t,s){this.s(t).add(s)}removeEventListener(t,s){this.s(t).delete(s)}dispatchEvent(t){t.target=this,this.s(t.type).forEach(s=>s(t))}s(t){return this.t[t]=this.t[t]||new Set}}const e=(t,s)=>new URL(t,location).href===new URL(s,location).href;class n{constructor(t,s){Object.assign(this,s,{type:t})}}const h=200,a=6e4;class o extends i{constructor(t,i={}){super(),this.i=t,this.h=i,this.o=0,this.l=new s,this.g=new s,this.u=new s,this.m=this.m.bind(this),this.v=this.v.bind(this),this.p=this.p.bind(this),this._=this._.bind(this)}async register({immediate:t=!1}={}){t||"complete"===document.readyState||await new Promise(t=>addEventListener("load",t)),this.C=Boolean(navigator.serviceWorker.controller),this.W=this.L(),this.S=await this.B(),this.W&&(this.R=this.W,this.g.resolve(this.W),this.u.resolve(this.W),this.P(this.W),this.W.addEventListener("statechange",this.v,{once:!0}));const s=this.S.waiting;return s&&e(s.scriptURL,this.i)&&(this.R=s,Promise.resolve().then(()=>{this.dispatchEvent(new n("waiting",{sw:s,wasWaitingBeforeRegister:!0}))})),this.R&&this.l.resolve(this.R),this.S.addEventListener("updatefound",this.p),navigator.serviceWorker.addEventListener("controllerchange",this._,{once:!0}),"BroadcastChannel"in self&&(this.T=new BroadcastChannel("workbox"),this.T.addEventListener("message",this.m)),navigator.serviceWorker.addEventListener("message",this.m),this.S}get active(){return this.g.promise}get controlling(){return this.u.promise}async getSW(){return this.R||this.l.promise}async messageSW(s){const i=await this.getSW();return t(i,s)}L(){const t=navigator.serviceWorker.controller;if(t&&e(t.scriptURL,this.i))return t}async B(){try{const t=await navigator.serviceWorker.register(this.i,this.h);return this.U=performance.now(),t}catch(t){throw t}}P(s){t(s,{type:"WINDOW_READY",meta:"workbox-window"})}p(){const t=this.S.installing;this.o>0||!e(t.scriptURL,this.i)||performance.now()>this.U+a?(this.k=t,this.S.removeEventListener("updatefound",this.p)):(this.R=t,this.l.resolve(t)),++this.o,t.addEventListener("statechange",this.v)}v(t){const s=t.target,{state:i}=s,e=s===this.k,a=e?"external":"",o={sw:s,originalEvent:t};!e&&this.C&&(o.isUpdate=!0),this.dispatchEvent(new n(a+i,o)),"installed"===i?this.D=setTimeout(()=>{"installed"===i&&this.S.waiting===s&&this.dispatchEvent(new n(a+"waiting",o))},h):"activating"===i&&(clearTimeout(this.D),e||this.g.resolve(s))}_(t){const s=this.R;s===navigator.serviceWorker.controller&&(this.dispatchEvent(new n("controlling",{sw:s,originalEvent:t})),this.u.resolve(s))}m(t){const{data:s}=t;this.dispatchEvent(new n("message",{data:s,originalEvent:t}))}}export{o as Workbox,t as messageSW};
|
||||||
|
//# sourceMappingURL=workbox-window.prod.mjs.map
|
2
public/workbox/workbox-window.prod.umd.js
Normal file
2
public/workbox/workbox-window.prod.umd.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).workbox={})}(this,function(n){"use strict";try{self["workbox:window:4.3.1"]&&_()}catch(n){}var t=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function i(n,t){for(var i=0;i<t.length;i++){var e=t[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(n,e.key,e)}}function e(n){if(void 0===n)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n}try{self["workbox:core:4.3.1"]&&_()}catch(n){}var r=function(){var n=this;this.promise=new Promise(function(t,i){n.resolve=t,n.reject=i})},o=function(n,t){return new URL(n,location).href===new URL(t,location).href},u=function(n,t){Object.assign(this,t,{type:n})};function s(n){return function(){for(var t=[],i=0;i<arguments.length;i++)t[i]=arguments[i];try{return Promise.resolve(n.apply(this,t))}catch(n){return Promise.reject(n)}}}function a(n,t,i){return i?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function c(){}var f=function(n){var f,h;function v(t,i){var o;return void 0===i&&(i={}),(o=n.call(this)||this).t=t,o.i=i,o.o=0,o.u=new r,o.s=new r,o.h=new r,o.v=o.v.bind(e(e(o))),o.l=o.l.bind(e(e(o))),o.g=o.g.bind(e(e(o))),o.m=o.m.bind(e(e(o))),o}h=n,(f=v).prototype=Object.create(h.prototype),f.prototype.constructor=f,f.__proto__=h;var l,w,d,g=v.prototype;return g.register=s(function(n){var t,i,e=this,r=(void 0===n?{}:n).immediate,s=void 0!==r&&r;return t=function(){return e.p=Boolean(navigator.serviceWorker.controller),e.P=e.j(),a(e.O(),function(n){e.R=n,e.P&&(e._=e.P,e.s.resolve(e.P),e.h.resolve(e.P),e.k(e.P),e.P.addEventListener("statechange",e.l,{once:!0}));var t=e.R.waiting;return t&&o(t.scriptURL,e.t)&&(e._=t,Promise.resolve().then(function(){e.dispatchEvent(new u("waiting",{sw:t,wasWaitingBeforeRegister:!0}))})),e._&&e.u.resolve(e._),e.R.addEventListener("updatefound",e.g),navigator.serviceWorker.addEventListener("controllerchange",e.m,{once:!0}),"BroadcastChannel"in self&&(e.B=new BroadcastChannel("workbox"),e.B.addEventListener("message",e.v)),navigator.serviceWorker.addEventListener("message",e.v),e.R})},(i=function(){if(!s&&"complete"!==document.readyState)return function(n,t){if(!t)return n&&n.then?n.then(c):Promise.resolve()}(new Promise(function(n){return addEventListener("load",n)}))}())&&i.then?i.then(t):t(i)}),g.getSW=s(function(){return this._||this.u.promise}),g.messageSW=s(function(n){return a(this.getSW(),function(i){return t(i,n)})}),g.j=function(){var n=navigator.serviceWorker.controller;if(n&&o(n.scriptURL,this.t))return n},g.O=s(function(){var n=this;return function(n,t){try{var i=n()}catch(n){return t(n)}return i&&i.then?i.then(void 0,t):i}(function(){return a(navigator.serviceWorker.register(n.t,n.i),function(t){return n.C=performance.now(),t})},function(n){throw n})}),g.k=function(n){t(n,{type:"WINDOW_READY",meta:"workbox-window"})},g.g=function(){var n=this.R.installing;this.o>0||!o(n.scriptURL,this.t)||performance.now()>this.C+6e4?(this.L=n,this.R.removeEventListener("updatefound",this.g)):(this._=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},g.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.L,o=r?"external":"",s={sw:i,originalEvent:n};!r&&this.p&&(s.isUpdate=!0),this.dispatchEvent(new u(o+e,s)),"installed"===e?this.W=setTimeout(function(){"installed"===e&&t.R.waiting===i&&t.dispatchEvent(new u(o+"waiting",s))},200):"activating"===e&&(clearTimeout(this.W),r||this.s.resolve(i))},g.m=function(n){var t=this._;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new u("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},g.v=function(n){var t=n.data;this.dispatchEvent(new u("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&i(l.prototype,w),d&&i(l,d),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.M(n).add(t)},t.removeEventListener=function(n,t){this.M(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.M(n.type).forEach(function(t){return t(n)})},t.M=function(n){return this.D[n]=this.D[n]||new Set},n}());n.Workbox=f,n.messageSW=t,Object.defineProperty(n,"__esModule",{value:!0})});
|
||||||
|
//# sourceMappingURL=workbox-window.prod.umd.js.map
|
@ -11,7 +11,7 @@ export const trackEvent = (
|
|||||||
// Uncomment the next line to track locally
|
// Uncomment the next line to track locally
|
||||||
// console.log("Track Event", { category, action, label, value });
|
// console.log("Track Event", { category, action, label, value });
|
||||||
|
|
||||||
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
|
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
|
ENV,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
@ -383,7 +384,7 @@ const chartTypeBar = (
|
|||||||
y,
|
y,
|
||||||
groupId,
|
groupId,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
import.meta.env.DEV,
|
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@ -472,7 +473,7 @@ const chartTypeLine = (
|
|||||||
y,
|
y,
|
||||||
groupId,
|
groupId,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
import.meta.env.DEV,
|
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||||
),
|
),
|
||||||
line,
|
line,
|
||||||
...lines,
|
...lines,
|
||||||
|
@ -24,7 +24,6 @@ export interface ClipboardData {
|
|||||||
files?: BinaryFiles;
|
files?: BinaryFiles;
|
||||||
text?: string;
|
text?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
programmaticAPI?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
let CLIPBOARD = "";
|
||||||
@ -49,7 +48,6 @@ const clipboardContainsElements = (
|
|||||||
[
|
[
|
||||||
EXPORT_DATA_TYPES.excalidraw,
|
EXPORT_DATA_TYPES.excalidraw,
|
||||||
EXPORT_DATA_TYPES.excalidrawClipboard,
|
EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
EXPORT_DATA_TYPES.excalidrawClipboardWithAPI,
|
|
||||||
].includes(contents?.type) &&
|
].includes(contents?.type) &&
|
||||||
Array.isArray(contents.elements)
|
Array.isArray(contents.elements)
|
||||||
) {
|
) {
|
||||||
@ -193,8 +191,6 @@ export const parseClipboard = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
const programmaticAPI =
|
|
||||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
return {
|
return {
|
||||||
elements: systemClipboardData.elements,
|
elements: systemClipboardData.elements,
|
||||||
@ -202,7 +198,6 @@ export const parseClipboard = async (
|
|||||||
text: isPlainPaste
|
text: isPlainPaste
|
||||||
? JSON.stringify(systemClipboardData.elements, null, 2)
|
? JSON.stringify(systemClipboardData.elements, null, 2)
|
||||||
: undefined,
|
: undefined,
|
||||||
programmaticAPI,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
@ -363,6 +363,7 @@ export const ShapesSwitcher = ({
|
|||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||||
className="App-toolbar__extra-tools-dropdown"
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
|
align="end"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
@ -4,9 +4,8 @@ import { reseed } from "../random";
|
|||||||
import { render, queryByTestId } from "../tests/test-utils";
|
import { render, queryByTestId } from "../tests/test-utils";
|
||||||
|
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||||
|
|
||||||
describe("Test <App/>", () => {
|
describe("Test <App/>", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -255,7 +255,6 @@ import {
|
|||||||
isTransparent,
|
isTransparent,
|
||||||
easeToValuesRAF,
|
easeToValuesRAF,
|
||||||
muteFSAbortError,
|
muteFSAbortError,
|
||||||
isTestEnv,
|
|
||||||
easeOut,
|
easeOut,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
@ -298,6 +297,7 @@ import {
|
|||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
@ -346,10 +346,6 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||||
import {
|
|
||||||
ExcalidrawElementSkeleton,
|
|
||||||
convertToExcalidrawElements,
|
|
||||||
} from "../data/transform";
|
|
||||||
import { ValueOf } from "../utility-types";
|
import { ValueOf } from "../utility-types";
|
||||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||||
|
|
||||||
@ -944,7 +940,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
title="Excalidraw Embedded Content"
|
title="Excalidraw Embedded Content"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads"
|
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1599,7 +1595,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.excalidrawContainerValue.container =
|
this.excalidrawContainerValue.container =
|
||||||
this.excalidrawContainerRef.current;
|
this.excalidrawContainerRef.current;
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (
|
||||||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||||
|
) {
|
||||||
const setState = this.setState.bind(this);
|
const setState = this.setState.bind(this);
|
||||||
Object.defineProperties(window.h, {
|
Object.defineProperties(window.h, {
|
||||||
state: {
|
state: {
|
||||||
@ -1637,7 +1636,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// bounding rects don't work in tests so updating
|
// bounding rects don't work in tests so updating
|
||||||
// the state on init would result in making the test enviro run
|
// the state on init would result in making the test enviro run
|
||||||
// in mobile breakpoint (0 width/height), making everything fail
|
// in mobile breakpoint (0 width/height), making everything fail
|
||||||
!isTestEnv()
|
process.env.NODE_ENV !== "test"
|
||||||
) {
|
) {
|
||||||
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
||||||
}
|
}
|
||||||
@ -2235,6 +2234,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let file = event?.clipboardData?.files[0];
|
let file = event?.clipboardData?.files[0];
|
||||||
|
|
||||||
const data = await parseClipboard(event, isPlainPaste);
|
const data = await parseClipboard(event, isPlainPaste);
|
||||||
|
|
||||||
if (!file && data.text && !isPlainPaste) {
|
if (!file && data.text && !isPlainPaste) {
|
||||||
const string = data.text.trim();
|
const string = data.text.trim();
|
||||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||||
@ -2289,16 +2289,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (data.elements) {
|
} else if (data.elements) {
|
||||||
const elements = (
|
|
||||||
data.programmaticAPI
|
|
||||||
? convertToExcalidrawElements(
|
|
||||||
data.elements as ExcalidrawElementSkeleton[],
|
|
||||||
)
|
|
||||||
: data.elements
|
|
||||||
) as readonly ExcalidrawElement[];
|
|
||||||
// TODO remove formatting from elements if isPlainPaste
|
// TODO remove formatting from elements if isPlainPaste
|
||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements: data.elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
position: "cursor",
|
position: "cursor",
|
||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
@ -3557,8 +3550,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lineHeight,
|
lineHeight,
|
||||||
);
|
);
|
||||||
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||||
const newHeight = Math.max(container.height, minHeight);
|
const containerDims = getContainerDims(container);
|
||||||
const newWidth = Math.max(container.width, minWidth);
|
const newHeight = Math.max(containerDims.height, minHeight);
|
||||||
|
const newWidth = Math.max(containerDims.width, minWidth);
|
||||||
mutateElement(container, { height: newHeight, width: newWidth });
|
mutateElement(container, { height: newHeight, width: newWidth });
|
||||||
sceneX = container.x + newWidth / 2;
|
sceneX = container.x + newWidth / 2;
|
||||||
sceneY = container.y + newHeight / 2;
|
sceneY = container.y + newHeight / 2;
|
||||||
@ -5405,7 +5399,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
width: embedLink.aspectRatio.w,
|
width: embedLink.aspectRatio.w,
|
||||||
height: embedLink.aspectRatio.h,
|
height: embedLink.aspectRatio.h,
|
||||||
link,
|
link,
|
||||||
validated: null,
|
validated: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
@ -5593,7 +5587,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createGenericElementOnPointerDown = (
|
private createGenericElementOnPointerDown = (
|
||||||
elementType: ExcalidrawGenericElement["type"] | "embeddable",
|
elementType: ExcalidrawGenericElement["type"],
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): void => {
|
): void => {
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
@ -5607,7 +5601,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
y: gridY,
|
y: gridY,
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseElementAttributes = {
|
const element = newElement({
|
||||||
|
type: elementType,
|
||||||
x: gridX,
|
x: gridX,
|
||||||
y: gridY,
|
y: gridY,
|
||||||
strokeColor: this.state.currentItemStrokeColor,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
@ -5620,21 +5615,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roundness: this.getCurrentItemRoundness(elementType),
|
roundness: this.getCurrentItemRoundness(elementType),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
} as const;
|
...(elementType === "embeddable" ? { validated: false } : {}),
|
||||||
|
|
||||||
let element;
|
|
||||||
if (elementType === "embeddable") {
|
|
||||||
element = newEmbeddableElement({
|
|
||||||
type: "embeddable",
|
|
||||||
validated: null,
|
|
||||||
...baseElementAttributes,
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
element = newElement({
|
|
||||||
type: elementType,
|
|
||||||
...baseElementAttributes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.type === "selection") {
|
if (element.type === "selection") {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -6086,23 +6068,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.boxSelection.hasOccurred = true;
|
pointerDownState.boxSelection.hasOccurred = true;
|
||||||
|
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
|
|
||||||
// box-select line editor points
|
|
||||||
if (this.state.editingLinearElement) {
|
|
||||||
LinearElementEditor.handleBoxSelection(
|
|
||||||
event,
|
|
||||||
this.state,
|
|
||||||
this.setState.bind(this),
|
|
||||||
);
|
|
||||||
// regular box-select
|
|
||||||
} else {
|
|
||||||
let shouldReuseSelection = true;
|
|
||||||
|
|
||||||
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
|
||||||
if (
|
if (
|
||||||
pointerDownState.withCmdOrCtrl &&
|
!event.shiftKey &&
|
||||||
pointerDownState.hit.element
|
// allows for box-selecting points (without shift)
|
||||||
|
!this.state.editingLinearElement &&
|
||||||
|
isSomeElementSelected(elements, this.state)
|
||||||
) {
|
) {
|
||||||
|
if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
|
||||||
this.setState((prevState) =>
|
this.setState((prevState) =>
|
||||||
selectGroupsForSelectedElements(
|
selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
@ -6116,26 +6088,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this,
|
this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// box-select line editor points
|
||||||
|
if (this.state.editingLinearElement) {
|
||||||
|
LinearElementEditor.handleBoxSelection(
|
||||||
|
event,
|
||||||
|
this.state,
|
||||||
|
this.setState.bind(this),
|
||||||
|
);
|
||||||
|
// regular box-select
|
||||||
} else {
|
} else {
|
||||||
shouldReuseSelection = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const elementsWithinSelection = getElementsWithinSelection(
|
const elementsWithinSelection = getElementsWithinSelection(
|
||||||
elements,
|
elements,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
const nextSelectedElementIds = {
|
const nextSelectedElementIds = elementsWithinSelection.reduce(
|
||||||
...(shouldReuseSelection && prevState.selectedElementIds),
|
|
||||||
...elementsWithinSelection.reduce(
|
|
||||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
acc[element.id] = true;
|
acc[element.id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
),
|
);
|
||||||
};
|
|
||||||
|
|
||||||
if (pointerDownState.hit.element) {
|
if (pointerDownState.hit.element) {
|
||||||
// if using ctrl/cmd, select the hitElement only if we
|
// if using ctrl/cmd, select the hitElement only if we
|
||||||
@ -6150,10 +6125,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return selectGroupsForSelectedElements(
|
return selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
...prevState,
|
...prevState,
|
||||||
...(!shouldReuseSelection && {
|
|
||||||
selectedGroupIds: {},
|
|
||||||
editingGroupId: null,
|
|
||||||
}),
|
|
||||||
selectedElementIds: nextSelectedElementIds,
|
selectedElementIds: nextSelectedElementIds,
|
||||||
showHyperlinkPopup:
|
showHyperlinkPopup:
|
||||||
elementsWithinSelection.length === 1 &&
|
elementsWithinSelection.length === 1 &&
|
||||||
@ -8191,7 +8162,10 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (
|
||||||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||||
|
) {
|
||||||
window.h = window.h || ({} as Window["h"]);
|
window.h = window.h || ({} as Window["h"]);
|
||||||
|
|
||||||
Object.defineProperties(window.h, {
|
Object.defineProperties(window.h, {
|
||||||
|
@ -29,6 +29,18 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#canvas-bg-color-picker-container {
|
||||||
|
.color-picker__top-picks {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-container {
|
||||||
|
@include isMobile {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.color-picker__button {
|
.color-picker__button {
|
||||||
--radius: 0.25rem;
|
--radius: 0.25rem;
|
||||||
|
|
||||||
|
@ -77,8 +77,8 @@ export const EyeDropper: React.FC<{
|
|||||||
colorPreviewDiv.style.left = `${clientX + 20}px`;
|
colorPreviewDiv.style.left = `${clientX + 20}px`;
|
||||||
|
|
||||||
const pixel = ctx.getImageData(
|
const pixel = ctx.getImageData(
|
||||||
(clientX - appState.offsetLeft) * window.devicePixelRatio,
|
clientX * window.devicePixelRatio - appState.offsetLeft,
|
||||||
(clientY - appState.offsetTop) * window.devicePixelRatio,
|
clientY * window.devicePixelRatio - appState.offsetTop,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
).data;
|
).data;
|
||||||
|
@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="library-menu-browse-button"
|
className="library-menu-browse-button"
|
||||||
href={`${import.meta.env.VITE_APP_LIBRARY_URL}?target=${
|
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||||
window.name || "_blank"
|
window.name || "_blank"
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||||
VERSIONS.excalidrawLibrary
|
VERSIONS.excalidrawLibrary
|
||||||
|
@ -191,6 +191,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||||||
<DropdownMenu open={isLibraryMenuOpen}>
|
<DropdownMenu open={isLibraryMenuOpen}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
|
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
|
||||||
|
aria-label="Library menu"
|
||||||
>
|
>
|
||||||
{DotsIcon}
|
{DotsIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
@ -198,6 +199,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||||||
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
||||||
onSelect={() => setIsLibraryMenuOpen(false)}
|
onSelect={() => setIsLibraryMenuOpen(false)}
|
||||||
className="library-menu"
|
className="library-menu"
|
||||||
|
align="end"
|
||||||
>
|
>
|
||||||
{!itemsSelected && (
|
{!itemsSelected && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
@ -319,7 +319,7 @@ const PublishLibrary = ({
|
|||||||
formData.append("twitterHandle", libraryData.twitterHandle);
|
formData.append("twitterHandle", libraryData.twitterHandle);
|
||||||
formData.append("website", libraryData.website);
|
formData.append("website", libraryData.website);
|
||||||
|
|
||||||
fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
|
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
|
||||||
method: "post",
|
method: "post",
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
withExcalidrawDimensions,
|
withExcalidrawDimensions,
|
||||||
} from "../../tests/test-utils";
|
} from "../../tests/test-utils";
|
||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
export const assertSidebarDockButton = async <T extends boolean>(
|
export const assertSidebarDockButton = async <T extends boolean>(
|
||||||
hasDockButton: T,
|
hasDockButton: T,
|
||||||
@ -206,7 +205,7 @@ describe("Sidebar", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("<Sidebar.Header> should render close button", async () => {
|
it("<Sidebar.Header> should render close button", async () => {
|
||||||
const onStateChange = vi.fn();
|
const onStateChange = jest.fn();
|
||||||
const CustomExcalidraw = () => {
|
const CustomExcalidraw = () => {
|
||||||
return (
|
return (
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
|
@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
|
|||||||
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
|
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
|
||||||
ref: React.ForwardedRef<HTMLDivElement>,
|
ref: React.ForwardedRef<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
if (import.meta.env.DEV && onDock && docked == null) {
|
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
|
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
|
exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
|
||||||
<div
|
<div
|
||||||
data-testid="brave-measure-text-error"
|
data-testid="brave-measure-text-error"
|
||||||
>
|
>
|
||||||
|
@ -1,19 +1,35 @@
|
|||||||
@import "../../css/variables.module";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
[data-dropdown-menu-trigger] + [data-radix-popper-content-wrapper] {
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
max-width: 16rem;
|
||||||
top: 100%;
|
|
||||||
margin-top: 0.25rem;
|
&__submenu-trigger {
|
||||||
|
&[aria-expanded="true"] {
|
||||||
|
.dropdown-menu-item {
|
||||||
|
background-color: var(--button-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submenu-trigger-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radix-menu-item {
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
bottom: 55px;
|
|
||||||
top: auto;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
row-gap: 0.75rem;
|
|
||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
@ -30,21 +46,22 @@
|
|||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
max-height: calc(100vh - 150px);
|
max-height: var(--radix-popper-available-height);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
--gap: 2;
|
--gap: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-item-base {
|
.dropdown-menu-item-base {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 0.625rem;
|
padding: 0 0.75rem;
|
||||||
column-gap: 0.625rem;
|
column-gap: 0.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-gray-100);
|
color: var(--color-gray-100);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-item {
|
.dropdown-menu-item {
|
||||||
@ -53,7 +70,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-sm);
|
||||||
|
|
||||||
@media screen and (min-width: 1921px) {
|
@media screen and (min-width: 1921px) {
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
|
@ -13,6 +13,8 @@ import {
|
|||||||
|
|
||||||
import "./DropdownMenu.scss";
|
import "./DropdownMenu.scss";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
const DropdownMenu = ({
|
const DropdownMenu = ({
|
||||||
children,
|
children,
|
||||||
open,
|
open,
|
||||||
@ -22,11 +24,12 @@ const DropdownMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const MenuTriggerComp = getMenuTriggerComponent(children);
|
const MenuTriggerComp = getMenuTriggerComponent(children);
|
||||||
const MenuContentComp = getMenuContentComponent(children);
|
const MenuContentComp = getMenuContentComponent(children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DropdownMenuPrimitive.Root open={open} modal={false}>
|
||||||
{MenuTriggerComp}
|
{MenuTriggerComp}
|
||||||
{open && MenuContentComp}
|
{MenuContentComp}
|
||||||
</>
|
</DropdownMenuPrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,12 +6,17 @@ import React, { useRef } from "react";
|
|||||||
import { DropdownMenuContentPropsContext } from "./common";
|
import { DropdownMenuContentPropsContext } from "./common";
|
||||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
const MenuContent = ({
|
const MenuContent = ({
|
||||||
children,
|
children,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
className = "",
|
className = "",
|
||||||
onSelect,
|
onSelect,
|
||||||
style,
|
style,
|
||||||
|
sideOffset = 4,
|
||||||
|
align = "start",
|
||||||
|
collisionPadding,
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
@ -21,6 +26,11 @@ const MenuContent = ({
|
|||||||
*/
|
*/
|
||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
sideOffset?: number;
|
||||||
|
align?: "start" | "center" | "end";
|
||||||
|
collisionPadding?:
|
||||||
|
| number
|
||||||
|
| Partial<Record<"top" | "right" | "bottom" | "left", number>>;
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -35,11 +45,15 @@ const MenuContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||||
<div
|
<DropdownMenuPrimitive.Content
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={classNames}
|
className={classNames}
|
||||||
style={style}
|
style={style}
|
||||||
data-testid="dropdown-menu"
|
data-testid="dropdown-menu"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
collisionPadding={collisionPadding}
|
||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
@ -48,13 +62,13 @@ const MenuContent = ({
|
|||||||
) : (
|
) : (
|
||||||
<Island
|
<Island
|
||||||
className="dropdown-menu-container"
|
className="dropdown-menu-container"
|
||||||
padding={2}
|
padding={1}
|
||||||
style={{ zIndex: 2 }}
|
style={{ zIndex: 2 }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Island>
|
</Island>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DropdownMenuPrimitive.Content>
|
||||||
</DropdownMenuContentPropsContext.Provider>
|
</DropdownMenuContentPropsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import { Button } from "../Button";
|
||||||
import {
|
import {
|
||||||
getDropdownMenuItemClassName,
|
getDropdownMenuItemClassName,
|
||||||
useHandleDropdownMenuItemClick,
|
useHandleDropdownMenuItemClick,
|
||||||
} from "./common";
|
} from "./common";
|
||||||
import MenuItemContent from "./DropdownMenuItemContent";
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
const DropdownMenuItem = ({
|
const DropdownMenuItem = ({
|
||||||
icon,
|
icon,
|
||||||
onSelect,
|
onSelect,
|
||||||
@ -22,17 +24,19 @@ const DropdownMenuItem = ({
|
|||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<DropdownMenuPrimitive.Item className="radix-menu-item">
|
||||||
|
<Button
|
||||||
{...rest}
|
{...rest}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type="button"
|
onSelect={() => {}}
|
||||||
className={getDropdownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
</MenuItemContent>
|
</MenuItemContent>
|
||||||
</button>
|
</Button>
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
26
src/components/dropdownMenu/DropdownMenuSub.tsx
Normal file
26
src/components/dropdownMenu/DropdownMenuSub.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import {
|
||||||
|
getSubMenuContentComponent,
|
||||||
|
getSubMenuTriggerComponent,
|
||||||
|
} from "./dropdownMenuUtils";
|
||||||
|
import DropdownMenuSubTrigger from "./DropdownMenuSubTrigger";
|
||||||
|
import DropdownMenuSubContent from "./DropdownMenuSubContent";
|
||||||
|
import DropdownMenuSubItem from "./DropdownMenuSubItem";
|
||||||
|
|
||||||
|
const DropdownMenuSub = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const MenuTriggerComp = getSubMenuTriggerComponent(children);
|
||||||
|
const MenuContentComp = getSubMenuContentComponent(children);
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Sub>
|
||||||
|
{MenuTriggerComp}
|
||||||
|
{MenuContentComp}
|
||||||
|
</DropdownMenuPrimitive.Sub>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DropdownMenuSub.Trigger = DropdownMenuSubTrigger;
|
||||||
|
DropdownMenuSub.Content = DropdownMenuSubContent;
|
||||||
|
DropdownMenuSub.Item = DropdownMenuSubItem;
|
||||||
|
|
||||||
|
export default DropdownMenuSub;
|
||||||
|
DropdownMenuSub.displayName = "DropdownMenuSub";
|
42
src/components/dropdownMenu/DropdownMenuSubContent.tsx
Normal file
42
src/components/dropdownMenu/DropdownMenuSubContent.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { useDevice } from "../App";
|
||||||
|
import Stack from "../Stack";
|
||||||
|
import { Island } from "../Island";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
|
||||||
|
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||||
|
"dropdown-menu--mobile": device.isMobile,
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
className={classNames}
|
||||||
|
sideOffset={8}
|
||||||
|
alignOffset={-4}
|
||||||
|
>
|
||||||
|
{device.isMobile ? (
|
||||||
|
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||||
|
) : (
|
||||||
|
<Island
|
||||||
|
className="dropdown-menu-container"
|
||||||
|
padding={1}
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Island>
|
||||||
|
)}
|
||||||
|
</DropdownMenuPrimitive.SubContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuSubContent;
|
||||||
|
DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
|
45
src/components/dropdownMenu/DropdownMenuSubItem.tsx
Normal file
45
src/components/dropdownMenu/DropdownMenuSubItem.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Button } from "../Button";
|
||||||
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
import {
|
||||||
|
getDropdownMenuItemClassName,
|
||||||
|
useHandleDropdownMenuItemClick,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
const DropdownMenuSubItem = ({
|
||||||
|
icon,
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
shortcut,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
onSelect: (event: Event) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
shortcut?: string;
|
||||||
|
className?: string;
|
||||||
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item className="radix-menu-item">
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
onClick={handleClick}
|
||||||
|
onSelect={() => {}}
|
||||||
|
type="button"
|
||||||
|
className={getDropdownMenuItemClassName(className)}
|
||||||
|
title={rest.title ?? rest["aria-label"]}
|
||||||
|
>
|
||||||
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</MenuItemContent>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuSubItem;
|
||||||
|
DropdownMenuSubItem.displayName = "DropdownMenuSubItem";
|
34
src/components/dropdownMenu/DropdownMenuSubTrigger.tsx
Normal file
34
src/components/dropdownMenu/DropdownMenuSubTrigger.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import React from "react";
|
||||||
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
import { getDropdownMenuItemClassName } from "./common";
|
||||||
|
import { ChevronRight } from "../icons";
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
className?: string;
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger className="radix-menu-item dropdown-menu__submenu-trigger">
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
className={getDropdownMenuItemClassName(className)}
|
||||||
|
title={rest.title ?? rest["aria-label"]}
|
||||||
|
>
|
||||||
|
<MenuItemContent icon={icon}>{children}</MenuItemContent>
|
||||||
|
<div className="dropdown-menu__submenu-trigger-icon">
|
||||||
|
{ChevronRight}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuSubTrigger;
|
||||||
|
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger";
|
@ -1,5 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
const MenuTrigger = ({
|
const MenuTrigger = ({
|
||||||
className = "",
|
className = "",
|
||||||
@ -22,7 +23,8 @@ const MenuTrigger = ({
|
|||||||
},
|
},
|
||||||
).trim();
|
).trim();
|
||||||
return (
|
return (
|
||||||
<button
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-dropdown-menu-trigger
|
||||||
data-prevent-outside-click
|
data-prevent-outside-click
|
||||||
className={classNames}
|
className={classNames}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
@ -32,7 +34,7 @@ const MenuTrigger = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</DropdownMenuPrimitive.Trigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
const getMenuComponent = (component: string) => (children: React.ReactNode) => {
|
||||||
const comp = React.Children.toArray(children).find(
|
const comp = React.Children.toArray(children).find(
|
||||||
(child) =>
|
(child) =>
|
||||||
React.isValidElement(child) &&
|
React.isValidElement(child) &&
|
||||||
@ -8,7 +8,7 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
child?.type.displayName &&
|
child?.type.displayName &&
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
child.type.displayName === "DropdownMenuTrigger",
|
child.type.displayName === component,
|
||||||
);
|
);
|
||||||
if (!comp) {
|
if (!comp) {
|
||||||
return null;
|
return null;
|
||||||
@ -17,19 +17,11 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
|||||||
return comp;
|
return comp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMenuContentComponent = (children: React.ReactNode) => {
|
export const getMenuTriggerComponent = getMenuComponent("DropdownMenuTrigger");
|
||||||
const comp = React.Children.toArray(children).find(
|
export const getMenuContentComponent = getMenuComponent("DropdownMenuContent");
|
||||||
(child) =>
|
export const getSubMenuTriggerComponent = getMenuComponent(
|
||||||
React.isValidElement(child) &&
|
"DropdownMenuSubTrigger",
|
||||||
typeof child.type !== "string" &&
|
);
|
||||||
//@ts-ignore
|
export const getSubMenuContentComponent = getMenuComponent(
|
||||||
child?.type.displayName &&
|
"DropdownMenuSubContent",
|
||||||
//@ts-ignore
|
|
||||||
child.type.displayName === "DropdownMenuContent",
|
|
||||||
);
|
);
|
||||||
if (!comp) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//@ts-ignore
|
|
||||||
return comp;
|
|
||||||
};
|
|
||||||
|
@ -71,6 +71,15 @@ const modifiedTablerIconProps: Opts = {
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//tabler-icons: chevron-right
|
||||||
|
export const ChevronRight = createIcon(
|
||||||
|
<g strokeWidth="1.5">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<polyline points="9 6 15 12 9 18" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
// tabler-icons: present
|
// tabler-icons: present
|
||||||
export const PlusPromoIcon = createIcon(
|
export const PlusPromoIcon = createIcon(
|
||||||
<g strokeWidth="1.5">
|
<g strokeWidth="1.5">
|
||||||
|
@ -215,7 +215,10 @@ export const ChangeCanvasBackground = () => {
|
|||||||
>
|
>
|
||||||
{t("labels.canvasBackground")}
|
{t("labels.canvasBackground")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: "0 0.625rem" }}>
|
<div
|
||||||
|
style={{ padding: "0 0.625rem" }}
|
||||||
|
id="canvas-bg-color-picker-container"
|
||||||
|
>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,9 @@ import { withInternalFallback } from "../hoc/withInternalFallback";
|
|||||||
import { composeEventHandlers } from "../../utils";
|
import { composeEventHandlers } from "../../utils";
|
||||||
import { useTunnels } from "../../context/tunnels";
|
import { useTunnels } from "../../context/tunnels";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
import DropdownMenuSub from "../dropdownMenu/DropdownMenuSub";
|
||||||
|
|
||||||
|
import * as Portal from "@radix-ui/react-portal";
|
||||||
|
|
||||||
const MainMenu = Object.assign(
|
const MainMenu = Object.assign(
|
||||||
withInternalFallback(
|
withInternalFallback(
|
||||||
@ -35,6 +38,17 @@ const MainMenu = Object.assign(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainMenuTunnel.In>
|
<MainMenuTunnel.In>
|
||||||
|
{appState.openMenu === "canvas" && device.isMobile && (
|
||||||
|
<Portal.Root
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgba(18, 18, 18, 0.2)",
|
||||||
|
position: "fixed",
|
||||||
|
inset: "0px",
|
||||||
|
zIndex: "var(--zIndex-layerUI)",
|
||||||
|
}}
|
||||||
|
onClick={() => setAppState({ openMenu: null })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DropdownMenu open={appState.openMenu === "canvas"}>
|
<DropdownMenu open={appState.openMenu === "canvas"}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
@ -43,14 +57,26 @@ const MainMenu = Object.assign(
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
data-testid="main-menu-trigger"
|
data-testid="main-menu-trigger"
|
||||||
|
aria-label="Main menu"
|
||||||
>
|
>
|
||||||
{HamburgerMenuIcon}
|
{HamburgerMenuIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
|
sideOffset={device.isMobile ? 20 : undefined}
|
||||||
|
className="main-menu-content"
|
||||||
onClickOutside={onClickOutside}
|
onClickOutside={onClickOutside}
|
||||||
onSelect={composeEventHandlers(onSelect, () => {
|
onSelect={composeEventHandlers(onSelect, () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
})}
|
})}
|
||||||
|
collisionPadding={
|
||||||
|
// accounting for
|
||||||
|
// - editor footer on desktop
|
||||||
|
// - toolbar on mobile
|
||||||
|
// we probably don't want the menu to overlay these elements
|
||||||
|
!device.isMobile
|
||||||
|
? { bottom: 90, top: 10 }
|
||||||
|
: { top: 90, bottom: 10 }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{device.isMobile && appState.collaborators.size > 0 && (
|
{device.isMobile && appState.collaborators.size > 0 && (
|
||||||
@ -75,6 +101,7 @@ const MainMenu = Object.assign(
|
|||||||
ItemCustom: DropdownMenu.ItemCustom,
|
ItemCustom: DropdownMenu.ItemCustom,
|
||||||
Group: DropdownMenu.Group,
|
Group: DropdownMenu.Group,
|
||||||
Separator: DropdownMenu.Separator,
|
Separator: DropdownMenu.Separator,
|
||||||
|
Sub: DropdownMenuSub,
|
||||||
DefaultItems,
|
DefaultItems,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -117,7 +117,6 @@ export const FRAME_STYLE = {
|
|||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const MIN_FONT_SIZE = 1;
|
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
@ -164,7 +163,6 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
excalidraw: "excalidraw",
|
excalidraw: "excalidraw",
|
||||||
excalidrawClipboard: "excalidraw/clipboard",
|
excalidrawClipboard: "excalidraw/clipboard",
|
||||||
excalidrawLibrary: "excalidrawlib",
|
excalidrawLibrary: "excalidrawlib",
|
||||||
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_SOURCE =
|
export const EXPORT_SOURCE =
|
||||||
@ -241,8 +239,6 @@ export const VERSIONS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BOUND_TEXT_PADDING = 5;
|
export const BOUND_TEXT_PADDING = 5;
|
||||||
export const ARROW_LABEL_WIDTH_FRACTION = 0.7;
|
|
||||||
export const ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO = 11;
|
|
||||||
|
|
||||||
export const VERTICAL_ALIGN = {
|
export const VERTICAL_ALIGN = {
|
||||||
TOP: "top",
|
TOP: "top",
|
||||||
|
@ -126,6 +126,7 @@
|
|||||||
--color-success: #268029;
|
--color-success: #268029;
|
||||||
--color-success-lighter: #cafccc;
|
--color-success-lighter: #cafccc;
|
||||||
|
|
||||||
|
--border-radius-sm: 0.25rem;
|
||||||
--border-radius-md: 0.375rem;
|
--border-radius-md: 0.375rem;
|
||||||
--border-radius-lg: 0.5rem;
|
--border-radius-lg: 0.5rem;
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -29,7 +29,6 @@ import {
|
|||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
DEFAULT_ELEMENT_PROPS,
|
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
@ -42,6 +41,7 @@ import {
|
|||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
import { COLOR_PALETTE } from "../colors";
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
@ -122,18 +122,16 @@ const restoreElementWithProperties = <
|
|||||||
versionNonce: element.versionNonce ?? 0,
|
versionNonce: element.versionNonce ?? 0,
|
||||||
isDeleted: element.isDeleted ?? false,
|
isDeleted: element.isDeleted ?? false,
|
||||||
id: element.id || randomId(),
|
id: element.id || randomId(),
|
||||||
fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle,
|
fillStyle: element.fillStyle || "hachure",
|
||||||
strokeWidth: element.strokeWidth || DEFAULT_ELEMENT_PROPS.strokeWidth,
|
strokeWidth: element.strokeWidth || 1,
|
||||||
strokeStyle: element.strokeStyle ?? DEFAULT_ELEMENT_PROPS.strokeStyle,
|
strokeStyle: element.strokeStyle ?? "solid",
|
||||||
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
roughness: element.roughness ?? 1,
|
||||||
opacity:
|
opacity: element.opacity == null ? 100 : element.opacity,
|
||||||
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
|
|
||||||
angle: element.angle || 0,
|
angle: element.angle || 0,
|
||||||
x: extra.x ?? element.x ?? 0,
|
x: extra.x ?? element.x ?? 0,
|
||||||
y: extra.y ?? element.y ?? 0,
|
y: extra.y ?? element.y ?? 0,
|
||||||
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
strokeColor: element.strokeColor || COLOR_PALETTE.black,
|
||||||
backgroundColor:
|
backgroundColor: element.backgroundColor || COLOR_PALETTE.transparent,
|
||||||
element.backgroundColor || DEFAULT_ELEMENT_PROPS.backgroundColor,
|
|
||||||
width: element.width || 0,
|
width: element.width || 0,
|
||||||
height: element.height || 0,
|
height: element.height || 0,
|
||||||
seed: element.seed ?? 1,
|
seed: element.seed ?? 1,
|
||||||
@ -248,6 +246,7 @@ const restoreElement = (
|
|||||||
startArrowhead = null,
|
startArrowhead = null,
|
||||||
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
||||||
} = element;
|
} = element;
|
||||||
|
|
||||||
let x = element.x;
|
let x = element.x;
|
||||||
let y = element.y;
|
let y = element.y;
|
||||||
let points = // migrate old arrow model to new one
|
let points = // migrate old arrow model to new one
|
||||||
@ -287,7 +286,7 @@ const restoreElement = (
|
|||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
validated: null,
|
validated: undefined,
|
||||||
});
|
});
|
||||||
case "frame":
|
case "frame":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
@ -411,6 +410,7 @@ export const restoreElements = (
|
|||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
// used to detect duplicate top-level element ids
|
// used to detect duplicate top-level element ids
|
||||||
const existingIds = new Set<string>();
|
const existingIds = new Set<string>();
|
||||||
|
|
||||||
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
||||||
const restoredElements = (elements || []).reduce((elements, element) => {
|
const restoredElements = (elements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
@ -429,7 +429,6 @@ export const restoreElements = (
|
|||||||
migratedElement = { ...migratedElement, id: randomId() };
|
migratedElement = { ...migratedElement, id: randomId() };
|
||||||
}
|
}
|
||||||
existingIds.add(migratedElement.id);
|
existingIds.add(migratedElement.id);
|
||||||
|
|
||||||
elements.push(migratedElement);
|
elements.push(migratedElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,706 +0,0 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
import {
|
|
||||||
ExcalidrawElementSkeleton,
|
|
||||||
convertToExcalidrawElements,
|
|
||||||
} from "./transform";
|
|
||||||
import { ExcalidrawArrowElement } from "../element/types";
|
|
||||||
|
|
||||||
describe("Test Transform", () => {
|
|
||||||
it("should transform regular shapes", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "ellipse",
|
|
||||||
x: 100,
|
|
||||||
y: 250,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 100,
|
|
||||||
y: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 300,
|
|
||||||
y: 100,
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
backgroundColor: "#c0eb75",
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "ellipse",
|
|
||||||
x: 300,
|
|
||||||
y: 250,
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
backgroundColor: "#ffc9c9",
|
|
||||||
strokeStyle: "dotted",
|
|
||||||
fillStyle: "solid",
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 300,
|
|
||||||
y: 400,
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
backgroundColor: "#a5d8ff",
|
|
||||||
strokeColor: "#1971c2",
|
|
||||||
strokeStyle: "dashed",
|
|
||||||
fillStyle: "cross-hatch",
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
).forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should transform text element", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
text: "HELLO WORLD!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
x: 100,
|
|
||||||
y: 150,
|
|
||||||
text: "STYLED HELLO WORLD!",
|
|
||||||
fontSize: 20,
|
|
||||||
strokeColor: "#5f3dc4",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
).forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should transform linear elements", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 100,
|
|
||||||
y: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 450,
|
|
||||||
y: 20,
|
|
||||||
startArrowhead: "dot",
|
|
||||||
endArrowhead: "triangle",
|
|
||||||
strokeColor: "#1971c2",
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "line",
|
|
||||||
x: 100,
|
|
||||||
y: 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "line",
|
|
||||||
x: 450,
|
|
||||||
y: 60,
|
|
||||||
strokeColor: "#2f9e44",
|
|
||||||
strokeWidth: 2,
|
|
||||||
strokeStyle: "dotted",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should transform to text containers when label provided", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
label: {
|
|
||||||
text: "RECTANGLE TEXT CONTAINER",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "ellipse",
|
|
||||||
x: 500,
|
|
||||||
y: 100,
|
|
||||||
width: 200,
|
|
||||||
label: {
|
|
||||||
text: "ELLIPSE TEXT CONTAINER",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 100,
|
|
||||||
y: 150,
|
|
||||||
width: 280,
|
|
||||||
label: {
|
|
||||||
text: "DIAMOND\nTEXT CONTAINER",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 100,
|
|
||||||
y: 400,
|
|
||||||
width: 300,
|
|
||||||
backgroundColor: "#fff3bf",
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
text: "STYLED DIAMOND TEXT CONTAINER",
|
|
||||||
strokeColor: "#099268",
|
|
||||||
fontSize: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 500,
|
|
||||||
y: 300,
|
|
||||||
width: 200,
|
|
||||||
strokeColor: "#c2255c",
|
|
||||||
label: {
|
|
||||||
text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
|
|
||||||
textAlign: "left",
|
|
||||||
verticalAlign: "top",
|
|
||||||
fontSize: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "ellipse",
|
|
||||||
x: 500,
|
|
||||||
y: 500,
|
|
||||||
strokeColor: "#f08c00",
|
|
||||||
backgroundColor: "#ffec99",
|
|
||||||
width: 200,
|
|
||||||
label: {
|
|
||||||
text: "STYLED ELLIPSE TEXT CONTAINER",
|
|
||||||
strokeColor: "#c2255c",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(12);
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should transform to labelled arrows when label provided for arrows", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
label: {
|
|
||||||
text: "LABELED ARROW",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 100,
|
|
||||||
y: 200,
|
|
||||||
label: {
|
|
||||||
text: "STYLED LABELED ARROW",
|
|
||||||
strokeColor: "#099268",
|
|
||||||
fontSize: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 100,
|
|
||||||
y: 300,
|
|
||||||
strokeColor: "#1098ad",
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
text: "ANOTHER STYLED LABELLED ARROW",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 100,
|
|
||||||
y: 400,
|
|
||||||
strokeColor: "#1098ad",
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
text: "ANOTHER STYLED LABELLED ARROW",
|
|
||||||
strokeColor: "#099268",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(8);
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test arrow bindings", () => {
|
|
||||||
it("should bind arrows to shapes when start / end provided without ids", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
label: {
|
|
||||||
text: "HELLO WORLD!!",
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
type: "rectangle",
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: "ellipse",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
|
||||||
const [arrow, text, rectangle, ellipse] = excaldrawElements;
|
|
||||||
expect(arrow).toMatchObject({
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
boundElements: [{ id: text.id, type: "text" }],
|
|
||||||
startBinding: {
|
|
||||||
elementId: rectangle.id,
|
|
||||||
focus: 0,
|
|
||||||
gap: 1,
|
|
||||||
},
|
|
||||||
endBinding: {
|
|
||||||
elementId: ellipse.id,
|
|
||||||
focus: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(text).toMatchObject({
|
|
||||||
x: 340,
|
|
||||||
y: 226.5,
|
|
||||||
type: "text",
|
|
||||||
text: "HELLO WORLD!!",
|
|
||||||
containerId: arrow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(rectangle).toMatchObject({
|
|
||||||
x: 155,
|
|
||||||
y: 189,
|
|
||||||
type: "rectangle",
|
|
||||||
boundElements: [
|
|
||||||
{
|
|
||||||
id: arrow.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ellipse).toMatchObject({
|
|
||||||
x: 555,
|
|
||||||
y: 189,
|
|
||||||
type: "ellipse",
|
|
||||||
boundElements: [
|
|
||||||
{
|
|
||||||
id: arrow.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bind arrows to text when start / end provided without ids", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
label: {
|
|
||||||
text: "HELLO WORLD!!",
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
type: "text",
|
|
||||||
text: "HEYYYYY",
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: "text",
|
|
||||||
text: "WHATS UP ?",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
|
||||||
|
|
||||||
const [arrow, text1, text2, text3] = excaldrawElements;
|
|
||||||
|
|
||||||
expect(arrow).toMatchObject({
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
boundElements: [{ id: text1.id, type: "text" }],
|
|
||||||
startBinding: {
|
|
||||||
elementId: text2.id,
|
|
||||||
focus: 0,
|
|
||||||
gap: 1,
|
|
||||||
},
|
|
||||||
endBinding: {
|
|
||||||
elementId: text3.id,
|
|
||||||
focus: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(text1).toMatchObject({
|
|
||||||
x: 340,
|
|
||||||
y: 226.5,
|
|
||||||
type: "text",
|
|
||||||
text: "HELLO WORLD!!",
|
|
||||||
containerId: arrow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(text2).toMatchObject({
|
|
||||||
x: 185,
|
|
||||||
y: 226.5,
|
|
||||||
type: "text",
|
|
||||||
boundElements: [
|
|
||||||
{
|
|
||||||
id: arrow.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(text3).toMatchObject({
|
|
||||||
x: 555,
|
|
||||||
y: 226.5,
|
|
||||||
type: "text",
|
|
||||||
boundElements: [
|
|
||||||
{
|
|
||||||
id: arrow.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bind arrows to existing shapes when start / end provided with ids", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "ellipse",
|
|
||||||
id: "ellipse-1",
|
|
||||||
strokeColor: "#66a80f",
|
|
||||||
x: 630,
|
|
||||||
y: 316,
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
backgroundColor: "#d8f5a2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
id: "diamond-1",
|
|
||||||
strokeColor: "#9c36b5",
|
|
||||||
width: 140,
|
|
||||||
x: 96,
|
|
||||||
y: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 247,
|
|
||||||
y: 420,
|
|
||||||
width: 395,
|
|
||||||
height: 35,
|
|
||||||
strokeColor: "#1864ab",
|
|
||||||
start: {
|
|
||||||
type: "rectangle",
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
id: "ellipse-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 227,
|
|
||||||
y: 450,
|
|
||||||
width: 400,
|
|
||||||
strokeColor: "#e67700",
|
|
||||||
start: {
|
|
||||||
id: "diamond-1",
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
id: "ellipse-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(5);
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bind arrows to existing text elements when start / end provided with ids", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
x: 100,
|
|
||||||
y: 239,
|
|
||||||
type: "text",
|
|
||||||
text: "HEYYYYY",
|
|
||||||
id: "text-1",
|
|
||||||
strokeColor: "#c2255c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
id: "text-2",
|
|
||||||
x: 560,
|
|
||||||
y: 239,
|
|
||||||
text: "Whats up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
label: {
|
|
||||||
text: "HELLO WORLD!!",
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
id: "text-1",
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
id: "text-2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
|
||||||
|
|
||||||
excaldrawElements.forEach((ele) => {
|
|
||||||
expect(ele).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
id: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bind arrows to existing elements if ids are correct", () => {
|
|
||||||
const consoleErrorSpy = vi
|
|
||||||
.spyOn(console, "error")
|
|
||||||
.mockImplementationOnce(() => void 0);
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
x: 100,
|
|
||||||
y: 239,
|
|
||||||
type: "text",
|
|
||||||
text: "HEYYYYY",
|
|
||||||
id: "text-1",
|
|
||||||
strokeColor: "#c2255c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 560,
|
|
||||||
y: 139,
|
|
||||||
id: "rect-1",
|
|
||||||
width: 100,
|
|
||||||
height: 200,
|
|
||||||
backgroundColor: "#bac8ff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
label: {
|
|
||||||
text: "HELLO WORLD!!",
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
id: "text-13",
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
id: "rect-11",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
|
||||||
const [, , arrow] = excaldrawElements;
|
|
||||||
expect(arrow).toMatchObject({
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
boundElements: [
|
|
||||||
{
|
|
||||||
id: "id46",
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
startBinding: null,
|
|
||||||
endBinding: null,
|
|
||||||
});
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenNthCalledWith(
|
|
||||||
1,
|
|
||||||
"No element for start binding with id text-13 found",
|
|
||||||
);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"No element for end binding with id rect-11 found",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bind when ids referenced before the element data", () => {
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
x: 255,
|
|
||||||
y: 239,
|
|
||||||
end: {
|
|
||||||
id: "rect-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 560,
|
|
||||||
y: 139,
|
|
||||||
id: "rect-1",
|
|
||||||
width: 100,
|
|
||||||
height: 200,
|
|
||||||
backgroundColor: "#bac8ff",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
expect(excaldrawElements.length).toBe(2);
|
|
||||||
const [arrow, rect] = excaldrawElements;
|
|
||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
|
||||||
elementId: "rect-1",
|
|
||||||
focus: 0,
|
|
||||||
gap: 5,
|
|
||||||
});
|
|
||||||
expect(rect.boundElements).toStrictEqual([
|
|
||||||
{
|
|
||||||
id: "id47",
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not allow duplicate ids", () => {
|
|
||||||
const consoleErrorSpy = vi
|
|
||||||
.spyOn(console, "error")
|
|
||||||
.mockImplementationOnce(() => void 0);
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 300,
|
|
||||||
y: 100,
|
|
||||||
id: "rect-1",
|
|
||||||
width: 100,
|
|
||||||
height: 200,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 100,
|
|
||||||
y: 200,
|
|
||||||
id: "rect-1",
|
|
||||||
width: 100,
|
|
||||||
height: 200,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
|
||||||
elements as ExcalidrawElementSkeleton[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(1);
|
|
||||||
expect(excaldrawElements[0]).toMatchSnapshot({
|
|
||||||
seed: expect.any(Number),
|
|
||||||
versionNonce: expect.any(Number),
|
|
||||||
});
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
||||||
"Duplicate id found for rect-1",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,561 +0,0 @@
|
|||||||
import {
|
|
||||||
DEFAULT_FONT_FAMILY,
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
TEXT_ALIGN,
|
|
||||||
VERTICAL_ALIGN,
|
|
||||||
} from "../constants";
|
|
||||||
import {
|
|
||||||
newElement,
|
|
||||||
newLinearElement,
|
|
||||||
redrawTextBoundingBox,
|
|
||||||
} from "../element";
|
|
||||||
import { bindLinearElement } from "../element/binding";
|
|
||||||
import {
|
|
||||||
ElementConstructorOpts,
|
|
||||||
newImageElement,
|
|
||||||
newTextElement,
|
|
||||||
} from "../element/newElement";
|
|
||||||
import {
|
|
||||||
getDefaultLineHeight,
|
|
||||||
measureText,
|
|
||||||
normalizeText,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import {
|
|
||||||
ExcalidrawArrowElement,
|
|
||||||
ExcalidrawBindableElement,
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawEmbeddableElement,
|
|
||||||
ExcalidrawFrameElement,
|
|
||||||
ExcalidrawFreeDrawElement,
|
|
||||||
ExcalidrawGenericElement,
|
|
||||||
ExcalidrawImageElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawSelectionElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
FileId,
|
|
||||||
FontFamilyValues,
|
|
||||||
TextAlign,
|
|
||||||
VerticalAlign,
|
|
||||||
} from "../element/types";
|
|
||||||
import { MarkOptional } from "../utility-types";
|
|
||||||
import { assertNever, getFontString } from "../utils";
|
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
|
||||||
type: "arrow" | "line";
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
label?: {
|
|
||||||
text: string;
|
|
||||||
fontSize?: number;
|
|
||||||
fontFamily?: FontFamilyValues;
|
|
||||||
textAlign?: TextAlign;
|
|
||||||
verticalAlign?: VerticalAlign;
|
|
||||||
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
|
||||||
end?:
|
|
||||||
| (
|
|
||||||
| (
|
|
||||||
| {
|
|
||||||
type: Exclude<
|
|
||||||
ExcalidrawBindableElement["type"],
|
|
||||||
"image" | "text" | "frame" | "embeddable"
|
|
||||||
>;
|
|
||||||
id?: ExcalidrawGenericElement["id"];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: ExcalidrawGenericElement["id"];
|
|
||||||
type?: Exclude<
|
|
||||||
ExcalidrawBindableElement["type"],
|
|
||||||
"image" | "text" | "frame" | "embeddable"
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
| ((
|
|
||||||
| {
|
|
||||||
type: "text";
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type?: "text";
|
|
||||||
id: ExcalidrawTextElement["id"];
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
) &
|
|
||||||
Partial<ExcalidrawTextElement>)
|
|
||||||
) &
|
|
||||||
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
|
||||||
start?:
|
|
||||||
| (
|
|
||||||
| (
|
|
||||||
| {
|
|
||||||
type: Exclude<
|
|
||||||
ExcalidrawBindableElement["type"],
|
|
||||||
"image" | "text" | "frame" | "embeddable"
|
|
||||||
>;
|
|
||||||
id?: ExcalidrawGenericElement["id"];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: ExcalidrawGenericElement["id"];
|
|
||||||
type?: Exclude<
|
|
||||||
ExcalidrawBindableElement["type"],
|
|
||||||
"image" | "text" | "frame" | "embeddable"
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
| ((
|
|
||||||
| {
|
|
||||||
type: "text";
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type?: "text";
|
|
||||||
id: ExcalidrawTextElement["id"];
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
) &
|
|
||||||
Partial<ExcalidrawTextElement>)
|
|
||||||
) &
|
|
||||||
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
|
||||||
} & Partial<ExcalidrawLinearElement>;
|
|
||||||
|
|
||||||
export type ValidContainer =
|
|
||||||
| {
|
|
||||||
type: Exclude<ExcalidrawGenericElement["type"], "selection">;
|
|
||||||
id?: ExcalidrawGenericElement["id"];
|
|
||||||
label?: {
|
|
||||||
text: string;
|
|
||||||
fontSize?: number;
|
|
||||||
fontFamily?: FontFamilyValues;
|
|
||||||
textAlign?: TextAlign;
|
|
||||||
verticalAlign?: VerticalAlign;
|
|
||||||
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
|
||||||
} & ElementConstructorOpts;
|
|
||||||
|
|
||||||
export type ExcalidrawElementSkeleton =
|
|
||||||
| Extract<
|
|
||||||
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
|
||||||
| ExcalidrawEmbeddableElement
|
|
||||||
| ExcalidrawFreeDrawElement
|
|
||||||
| ExcalidrawFrameElement
|
|
||||||
>
|
|
||||||
| ({
|
|
||||||
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
} & Partial<ExcalidrawLinearElement>)
|
|
||||||
| ValidContainer
|
|
||||||
| ValidLinearElement
|
|
||||||
| ({
|
|
||||||
type: "text";
|
|
||||||
text: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
id?: ExcalidrawTextElement["id"];
|
|
||||||
} & Partial<ExcalidrawTextElement>)
|
|
||||||
| ({
|
|
||||||
type: Extract<ExcalidrawImageElement["type"], "image">;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
fileId: FileId;
|
|
||||||
} & Partial<ExcalidrawImageElement>);
|
|
||||||
|
|
||||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
|
||||||
width: 300,
|
|
||||||
height: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_DIMENSION = 100;
|
|
||||||
|
|
||||||
const bindTextToContainer = (
|
|
||||||
container: ExcalidrawElement,
|
|
||||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
|
||||||
) => {
|
|
||||||
const textElement: ExcalidrawTextElement = newTextElement({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
|
||||||
...textProps,
|
|
||||||
containerId: container.id,
|
|
||||||
strokeColor: textProps.strokeColor || container.strokeColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(container, {
|
|
||||||
boundElements: (container.boundElements || []).concat({
|
|
||||||
type: "text",
|
|
||||||
id: textElement.id,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
redrawTextBoundingBox(textElement, container);
|
|
||||||
return [container, textElement] as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindLinearElementToElement = (
|
|
||||||
linearElement: ExcalidrawArrowElement,
|
|
||||||
start: ValidLinearElement["start"],
|
|
||||||
end: ValidLinearElement["end"],
|
|
||||||
elementStore: ElementStore,
|
|
||||||
): {
|
|
||||||
linearElement: ExcalidrawLinearElement;
|
|
||||||
startBoundElement?: ExcalidrawElement;
|
|
||||||
endBoundElement?: ExcalidrawElement;
|
|
||||||
} => {
|
|
||||||
let startBoundElement;
|
|
||||||
let endBoundElement;
|
|
||||||
|
|
||||||
Object.assign(linearElement, {
|
|
||||||
startBinding: linearElement?.startBinding || null,
|
|
||||||
endBinding: linearElement.endBinding || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (start) {
|
|
||||||
const width = start?.width ?? DEFAULT_DIMENSION;
|
|
||||||
const height = start?.height ?? DEFAULT_DIMENSION;
|
|
||||||
|
|
||||||
let existingElement;
|
|
||||||
if (start.id) {
|
|
||||||
existingElement = elementStore.getElement(start.id);
|
|
||||||
if (!existingElement) {
|
|
||||||
console.error(`No element for start binding with id ${start.id} found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startX = start.x || linearElement.x - width;
|
|
||||||
const startY = start.y || linearElement.y - height / 2;
|
|
||||||
const startType = existingElement ? existingElement.type : start.type;
|
|
||||||
|
|
||||||
if (startType) {
|
|
||||||
if (startType === "text") {
|
|
||||||
let text = "";
|
|
||||||
if (existingElement && existingElement.type === "text") {
|
|
||||||
text = existingElement.text;
|
|
||||||
} else if (start.type === "text") {
|
|
||||||
text = start.text;
|
|
||||||
}
|
|
||||||
if (!text) {
|
|
||||||
console.error(
|
|
||||||
`No text found for start binding text element for ${linearElement.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
startBoundElement = newTextElement({
|
|
||||||
x: startX,
|
|
||||||
y: startY,
|
|
||||||
type: "text",
|
|
||||||
...existingElement,
|
|
||||||
...start,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
// to position the text correctly when coordinates not provided
|
|
||||||
Object.assign(startBoundElement, {
|
|
||||||
x: start.x || linearElement.x - startBoundElement.width,
|
|
||||||
y: start.y || linearElement.y - startBoundElement.height / 2,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
switch (startType) {
|
|
||||||
case "rectangle":
|
|
||||||
case "ellipse":
|
|
||||||
case "diamond": {
|
|
||||||
startBoundElement = newElement({
|
|
||||||
x: startX,
|
|
||||||
y: startY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...existingElement,
|
|
||||||
...start,
|
|
||||||
type: startType,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertNever(
|
|
||||||
linearElement as never,
|
|
||||||
`Unhandled element start type "${start.type}"`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindLinearElement(
|
|
||||||
linearElement,
|
|
||||||
startBoundElement as ExcalidrawBindableElement,
|
|
||||||
"start",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (end) {
|
|
||||||
const height = end?.height ?? DEFAULT_DIMENSION;
|
|
||||||
const width = end?.width ?? DEFAULT_DIMENSION;
|
|
||||||
|
|
||||||
let existingElement;
|
|
||||||
if (end.id) {
|
|
||||||
existingElement = elementStore.getElement(end.id);
|
|
||||||
if (!existingElement) {
|
|
||||||
console.error(`No element for end binding with id ${end.id} found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const endX = end.x || linearElement.x + linearElement.width;
|
|
||||||
const endY = end.y || linearElement.y - height / 2;
|
|
||||||
const endType = existingElement ? existingElement.type : end.type;
|
|
||||||
|
|
||||||
if (endType) {
|
|
||||||
if (endType === "text") {
|
|
||||||
let text = "";
|
|
||||||
if (existingElement && existingElement.type === "text") {
|
|
||||||
text = existingElement.text;
|
|
||||||
} else if (end.type === "text") {
|
|
||||||
text = end.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
console.error(
|
|
||||||
`No text found for end binding text element for ${linearElement.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
endBoundElement = newTextElement({
|
|
||||||
x: endX,
|
|
||||||
y: endY,
|
|
||||||
type: "text",
|
|
||||||
...existingElement,
|
|
||||||
...end,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
// to position the text correctly when coordinates not provided
|
|
||||||
Object.assign(endBoundElement, {
|
|
||||||
y: end.y || linearElement.y - endBoundElement.height / 2,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
switch (endType) {
|
|
||||||
case "rectangle":
|
|
||||||
case "ellipse":
|
|
||||||
case "diamond": {
|
|
||||||
endBoundElement = newElement({
|
|
||||||
x: endX,
|
|
||||||
y: endY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...existingElement,
|
|
||||||
...end,
|
|
||||||
type: endType,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertNever(
|
|
||||||
linearElement as never,
|
|
||||||
`Unhandled element end type "${endType}"`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindLinearElement(
|
|
||||||
linearElement,
|
|
||||||
endBoundElement as ExcalidrawBindableElement,
|
|
||||||
"end",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
linearElement,
|
|
||||||
startBoundElement,
|
|
||||||
endBoundElement,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class ElementStore {
|
|
||||||
excalidrawElements = new Map<string, ExcalidrawElement>();
|
|
||||||
|
|
||||||
add = (ele?: ExcalidrawElement) => {
|
|
||||||
if (!ele) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.excalidrawElements.set(ele.id, ele);
|
|
||||||
};
|
|
||||||
getElements = () => {
|
|
||||||
return Array.from(this.excalidrawElements.values());
|
|
||||||
};
|
|
||||||
|
|
||||||
getElement = (id: string) => {
|
|
||||||
return this.excalidrawElements.get(id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const convertToExcalidrawElements = (
|
|
||||||
elements: ExcalidrawElementSkeleton[] | null,
|
|
||||||
) => {
|
|
||||||
if (!elements) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementStore = new ElementStore();
|
|
||||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
|
||||||
|
|
||||||
// Create individual elements
|
|
||||||
for (const element of elements) {
|
|
||||||
let excalidrawElement: ExcalidrawElement;
|
|
||||||
switch (element.type) {
|
|
||||||
case "rectangle":
|
|
||||||
case "ellipse":
|
|
||||||
case "diamond": {
|
|
||||||
const width =
|
|
||||||
element?.label?.text && element.width === undefined
|
|
||||||
? 0
|
|
||||||
: element?.width || DEFAULT_DIMENSION;
|
|
||||||
const height =
|
|
||||||
element?.label?.text && element.height === undefined
|
|
||||||
? 0
|
|
||||||
: element?.height || DEFAULT_DIMENSION;
|
|
||||||
excalidrawElement = newElement({
|
|
||||||
...element,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "line": {
|
|
||||||
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
|
||||||
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
|
||||||
excalidrawElement = newLinearElement({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[width, height],
|
|
||||||
],
|
|
||||||
...element,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "arrow": {
|
|
||||||
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
|
||||||
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
|
||||||
excalidrawElement = newLinearElement({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
endArrowhead: "arrow",
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[width, height],
|
|
||||||
],
|
|
||||||
...element,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "text": {
|
|
||||||
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
|
|
||||||
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
|
|
||||||
const lineHeight =
|
|
||||||
element?.lineHeight || getDefaultLineHeight(fontFamily);
|
|
||||||
const text = element.text ?? "";
|
|
||||||
const normalizedText = normalizeText(text);
|
|
||||||
const metrics = measureText(
|
|
||||||
normalizedText,
|
|
||||||
getFontString({ fontFamily, fontSize }),
|
|
||||||
lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
excalidrawElement = newTextElement({
|
|
||||||
width: metrics.width,
|
|
||||||
height: metrics.height,
|
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
...element,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "image": {
|
|
||||||
excalidrawElement = newImageElement({
|
|
||||||
width: element?.width || DEFAULT_DIMENSION,
|
|
||||||
height: element?.height || DEFAULT_DIMENSION,
|
|
||||||
...element,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "freedraw":
|
|
||||||
case "frame":
|
|
||||||
case "embeddable": {
|
|
||||||
excalidrawElement = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
excalidrawElement = element;
|
|
||||||
assertNever(
|
|
||||||
element,
|
|
||||||
`Unhandled element type "${(element as any).type}"`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const existingElement = elementStore.getElement(excalidrawElement.id);
|
|
||||||
if (existingElement) {
|
|
||||||
console.error(`Duplicate id found for ${excalidrawElement.id}`);
|
|
||||||
} else {
|
|
||||||
elementStore.add(excalidrawElement);
|
|
||||||
elementsWithIds.set(excalidrawElement.id, element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add labels and arrow bindings
|
|
||||||
for (const [id, element] of elementsWithIds) {
|
|
||||||
const excalidrawElement = elementStore.getElement(id)!;
|
|
||||||
|
|
||||||
switch (element.type) {
|
|
||||||
case "rectangle":
|
|
||||||
case "ellipse":
|
|
||||||
case "diamond":
|
|
||||||
case "arrow": {
|
|
||||||
if (element.label?.text) {
|
|
||||||
let [container, text] = bindTextToContainer(
|
|
||||||
excalidrawElement,
|
|
||||||
element?.label,
|
|
||||||
);
|
|
||||||
elementStore.add(container);
|
|
||||||
elementStore.add(text);
|
|
||||||
|
|
||||||
if (container.type === "arrow") {
|
|
||||||
const originalStart =
|
|
||||||
element.type === "arrow" ? element?.start : undefined;
|
|
||||||
const originalEnd =
|
|
||||||
element.type === "arrow" ? element?.end : undefined;
|
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
|
||||||
bindLinearElementToElement(
|
|
||||||
container as ExcalidrawArrowElement,
|
|
||||||
originalStart,
|
|
||||||
originalEnd,
|
|
||||||
elementStore,
|
|
||||||
);
|
|
||||||
container = linearElement;
|
|
||||||
elementStore.add(linearElement);
|
|
||||||
elementStore.add(startBoundElement);
|
|
||||||
elementStore.add(endBoundElement);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (element.type) {
|
|
||||||
case "arrow": {
|
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
|
||||||
bindLinearElementToElement(
|
|
||||||
excalidrawElement as ExcalidrawArrowElement,
|
|
||||||
element.start,
|
|
||||||
element.end,
|
|
||||||
elementStore,
|
|
||||||
);
|
|
||||||
elementStore.add(linearElement);
|
|
||||||
elementStore.add(startBoundElement);
|
|
||||||
elementStore.add(endBoundElement);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return elementStore.getElements();
|
|
||||||
};
|
|
@ -190,7 +190,7 @@ export const maybeBindLinearElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindLinearElement = (
|
const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
@ -40,9 +40,6 @@ const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
|||||||
const RE_TWITTER_EMBED =
|
const RE_TWITTER_EMBED =
|
||||||
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
|
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
|
||||||
|
|
||||||
const RE_VALTOWN =
|
|
||||||
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
|
||||||
|
|
||||||
const RE_GENERIC_EMBED =
|
const RE_GENERIC_EMBED =
|
||||||
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||||
|
|
||||||
@ -55,9 +52,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"link.excalidraw.com",
|
"link.excalidraw.com",
|
||||||
"gist.github.com",
|
"gist.github.com",
|
||||||
"twitter.com",
|
"twitter.com",
|
||||||
"*.simplepdf.eu",
|
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const createSrcDoc = (body: string) => {
|
const createSrcDoc = (body: string) => {
|
||||||
@ -127,14 +122,6 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||||||
return { link, aspectRatio, type };
|
return { link, aspectRatio, type };
|
||||||
}
|
}
|
||||||
|
|
||||||
const valLink = link.match(RE_VALTOWN);
|
|
||||||
if (valLink) {
|
|
||||||
link =
|
|
||||||
valLink[1] === "embed" ? valLink[0] : valLink[0].replace("/v", "/embed");
|
|
||||||
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
|
||||||
return { link, aspectRatio, type };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RE_TWITTER.test(link)) {
|
if (RE_TWITTER.test(link)) {
|
||||||
let ret: EmbeddedLink;
|
let ret: EmbeddedLink;
|
||||||
// assume embed code
|
// assume embed code
|
||||||
@ -275,16 +262,9 @@ const validateHostname = (
|
|||||||
const { hostname } = new URL(url);
|
const { hostname } = new URL(url);
|
||||||
|
|
||||||
const bareDomain = hostname.replace(/^www\./, "");
|
const bareDomain = hostname.replace(/^www\./, "");
|
||||||
const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace(
|
|
||||||
/^([^.]+)/,
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allowedHostnames instanceof Set) {
|
if (allowedHostnames instanceof Set) {
|
||||||
return (
|
return ALLOWED_DOMAINS.has(bareDomain);
|
||||||
ALLOWED_DOMAINS.has(bareDomain) ||
|
|
||||||
ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bareDomain === allowedHostnames.replace(/^www\./, "")) {
|
if (bareDomain === allowedHostnames.replace(/^www\./, "")) {
|
||||||
|
@ -264,12 +264,12 @@ export class LinearElementEditor {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
handleBindTextResize(element, false);
|
handleBindTextResize(element, false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// suggest bindings for first and last point if selected
|
// suggest bindings for first and last point if selected
|
||||||
if (isBindingElement(element, false)) {
|
if (isBindingElement(element, false)) {
|
||||||
|
@ -46,7 +46,7 @@ import {
|
|||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
| "width"
|
| "width"
|
||||||
| "height"
|
| "height"
|
||||||
@ -134,7 +134,7 @@ export const newElement = (
|
|||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: "embeddable";
|
type: "embeddable";
|
||||||
validated: ExcalidrawEmbeddableElement["validated"];
|
validated: boolean | undefined;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
||||||
return {
|
return {
|
||||||
@ -187,7 +187,7 @@ export const newTextElement = (
|
|||||||
fontFamily?: FontFamilyValues;
|
fontFamily?: FontFamilyValues;
|
||||||
textAlign?: TextAlign;
|
textAlign?: TextAlign;
|
||||||
verticalAlign?: VerticalAlign;
|
verticalAlign?: VerticalAlign;
|
||||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
containerId?: ExcalidrawTextContainer["id"];
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
@ -361,8 +361,8 @@ export const newFreeDrawElement = (
|
|||||||
export const newLinearElement = (
|
export const newLinearElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
startArrowhead?: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
endArrowhead?: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
points?: ExcalidrawLinearElement["points"];
|
points?: ExcalidrawLinearElement["points"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawLinearElement> => {
|
): NonDeleted<ExcalidrawLinearElement> => {
|
||||||
@ -372,8 +372,8 @@ export const newLinearElement = (
|
|||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead || null,
|
startArrowhead: opts.startArrowhead,
|
||||||
endArrowhead: opts.endArrowhead || null,
|
endArrowhead: opts.endArrowhead,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -443,7 +443,7 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
|
|||||||
// we're not cloning non-array & non-plain-object objects because we
|
// we're not cloning non-array & non-plain-object objects because we
|
||||||
// don't support them on excalidraw elements yet. If we do, we need to make
|
// don't support them on excalidraw elements yet. If we do, we need to make
|
||||||
// sure we start cloning them, so let's warn about it.
|
// sure we start cloning them, so let's warn about it.
|
||||||
if (import.meta.env.DEV) {
|
if (process.env.NODE_ENV === "development") {
|
||||||
if (
|
if (
|
||||||
objectType !== "[object Object]" &&
|
objectType !== "[object Object]" &&
|
||||||
objectType !== "[object Array]" &&
|
objectType !== "[object Array]" &&
|
||||||
@ -477,7 +477,7 @@ export const deepCopyElement = <T extends ExcalidrawElement>(
|
|||||||
* utility wrapper to generate new id. In test env it reuses the old + postfix
|
* utility wrapper to generate new id. In test env it reuses the old + postfix
|
||||||
* for test assertions.
|
* for test assertions.
|
||||||
*/
|
*/
|
||||||
export const regenerateId = (
|
const regenerateId = (
|
||||||
/** supply null if no previous id exists */
|
/** supply null if no previous id exists */
|
||||||
previousId: string | null,
|
previousId: string | null,
|
||||||
) => {
|
) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -204,6 +204,8 @@ const rescalePointsInElement = (
|
|||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
const MIN_FONT_SIZE = 1;
|
||||||
|
|
||||||
const measureFontSizeFromWidth = (
|
const measureFontSizeFromWidth = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
@ -587,42 +589,24 @@ export const resizeSingleElement = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isArrowElement(element) &&
|
|
||||||
boundTextElement &&
|
|
||||||
shouldMaintainAspectRatio
|
|
||||||
) {
|
|
||||||
const fontSize =
|
|
||||||
(resizedElement.width / element.width) * boundTextElement.fontSize;
|
|
||||||
if (fontSize < MIN_FONT_SIZE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boundTextFont.fontSize = fontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
resizedElement.width !== 0 &&
|
resizedElement.width !== 0 &&
|
||||||
resizedElement.height !== 0 &&
|
resizedElement.height !== 0 &&
|
||||||
Number.isFinite(resizedElement.x) &&
|
Number.isFinite(resizedElement.x) &&
|
||||||
Number.isFinite(resizedElement.y)
|
Number.isFinite(resizedElement.y)
|
||||||
) {
|
) {
|
||||||
mutateElement(element, resizedElement);
|
|
||||||
|
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
newSize: { width: resizedElement.width, height: resizedElement.height },
|
newSize: { width: resizedElement.width, height: resizedElement.height },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mutateElement(element, resizedElement);
|
||||||
if (boundTextElement && boundTextFont != null) {
|
if (boundTextElement && boundTextFont != null) {
|
||||||
mutateElement(boundTextElement, {
|
mutateElement(boundTextElement, {
|
||||||
fontSize: boundTextFont.fontSize,
|
fontSize: boundTextFont.fontSize,
|
||||||
baseline: boundTextFont.baseline,
|
baseline: boundTextFont.baseline,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleBindTextResize(
|
handleBindTextResize(element, transformHandleDirection);
|
||||||
element,
|
|
||||||
transformHandleDirection,
|
|
||||||
shouldMaintainAspectRatio,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -738,8 +722,12 @@ export const resizeMultipleElements = (
|
|||||||
fontSize?: ExcalidrawTextElement["fontSize"];
|
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
baseline?: ExcalidrawTextElement["baseline"];
|
baseline?: ExcalidrawTextElement["baseline"];
|
||||||
scale?: ExcalidrawImageElement["scale"];
|
scale?: ExcalidrawImageElement["scale"];
|
||||||
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
|
||||||
};
|
};
|
||||||
|
boundText: {
|
||||||
|
element: ExcalidrawTextElementWithContainer;
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"];
|
||||||
|
baseline: ExcalidrawTextElement["baseline"];
|
||||||
|
} | null;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
for (const { orig, latest } of targetElements) {
|
for (const { orig, latest } of targetElements) {
|
||||||
@ -810,39 +798,50 @@ export const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(orig)) {
|
let boundText: typeof elementsAndUpdates[0]["boundText"] = null;
|
||||||
const metrics = measureFontSizeFromWidth(orig, width, height);
|
|
||||||
|
const boundTextElement = getBoundTextElement(latest);
|
||||||
|
|
||||||
|
if (boundTextElement || isTextElement(orig)) {
|
||||||
|
const updatedElement = {
|
||||||
|
...latest,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
const metrics = measureFontSizeFromWidth(
|
||||||
|
boundTextElement ?? (orig as ExcalidrawTextElement),
|
||||||
|
boundTextElement
|
||||||
|
? getBoundTextMaxWidth(updatedElement)
|
||||||
|
: updatedElement.width,
|
||||||
|
boundTextElement
|
||||||
|
? getBoundTextMaxHeight(updatedElement, boundTextElement)
|
||||||
|
: updatedElement.height,
|
||||||
|
);
|
||||||
|
|
||||||
if (!metrics) {
|
if (!metrics) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTextElement(orig)) {
|
||||||
update.fontSize = metrics.size;
|
update.fontSize = metrics.size;
|
||||||
update.baseline = metrics.baseline;
|
update.baseline = metrics.baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = pointerDownState.originalElements.get(
|
|
||||||
getBoundTextElementId(orig) ?? "",
|
|
||||||
) as ExcalidrawTextElementWithContainer | undefined;
|
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
boundText = {
|
||||||
if (newFontSize < MIN_FONT_SIZE) {
|
element: boundTextElement,
|
||||||
return;
|
fontSize: metrics.size,
|
||||||
|
baseline: metrics.baseline,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
update.boundTextFontSize = newFontSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
elementsAndUpdates.push({
|
elementsAndUpdates.push({ element: latest, update, boundText });
|
||||||
element: latest,
|
|
||||||
update,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
|
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
|
||||||
|
|
||||||
for (const {
|
for (const { element, update, boundText } of elementsAndUpdates) {
|
||||||
element,
|
|
||||||
update: { boundTextFontSize, ...update },
|
|
||||||
} of elementsAndUpdates) {
|
|
||||||
const { width, height, angle } = update;
|
const { width, height, angle } = update;
|
||||||
|
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, update, false);
|
||||||
@ -852,17 +851,17 @@ export const resizeMultipleElements = (
|
|||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
if (boundText) {
|
||||||
if (boundTextElement && boundTextFontSize) {
|
const { element: boundTextElement, ...boundTextUpdates } = boundText;
|
||||||
mutateElement(
|
mutateElement(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
{
|
{
|
||||||
fontSize: boundTextFontSize,
|
...boundTextUpdates,
|
||||||
angle: isLinearElement(element) ? undefined : angle,
|
angle: isLinearElement(element) ? undefined : angle,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
handleBindTextResize(element, transformHandleType, true);
|
handleBindTextResize(element, transformHandleType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,32 +1,19 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import * as constants from "../constants";
|
import * as constants from "../constants";
|
||||||
|
|
||||||
const EPSILON_DIGITS = 3;
|
const EPSILON_DIGITS = 3;
|
||||||
// Needed so that we can mock the value of constants which is done in
|
|
||||||
// below tests. In Jest this wasn't needed as global override was possible
|
|
||||||
// but vite doesn't allow that hence we need to mock
|
|
||||||
vi.mock(
|
|
||||||
"../constants.ts",
|
|
||||||
//@ts-ignore
|
|
||||||
async (importOriginal) => {
|
|
||||||
const module: any = await importOriginal();
|
|
||||||
return { ...module };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
describe("getPerfectElementSize", () => {
|
describe("getPerfectElementSize", () => {
|
||||||
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("line", 149, 10);
|
const { height, width } = getPerfectElementSize("line", 149, 10);
|
||||||
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
|
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
|
||||||
const { height, width } = getPerfectElementSize("line", 10, 140);
|
const { height, width } = getPerfectElementSize("line", 10, 140);
|
||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
||||||
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
||||||
@ -37,19 +24,16 @@ describe("getPerfectElementSize", () => {
|
|||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return adjust height to be width * tan(locked angle)", () => {
|
it("should return adjust height to be width * tan(locked angle)", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return height equals to width if locked angle is 45 deg", () => {
|
it("should return height equals to width if locked angle is 45 deg", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
||||||
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return height:0 and width:0 when width and height are 0", () => {
|
it("should return height:0 and width:0 when width and height are 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
|
@ -10,8 +10,6 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO,
|
|
||||||
ARROW_LABEL_WIDTH_FRACTION,
|
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
@ -67,7 +65,7 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.text = textElement.text;
|
boundTextUpdates.text = textElement.text;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getBoundTextMaxWidth(container, textElement);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
boundTextUpdates.text = wrapText(
|
boundTextUpdates.text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
@ -85,27 +83,21 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.baseline = metrics.baseline;
|
boundTextUpdates.baseline = metrics.baseline;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
|
const containerDims = getContainerDims(container);
|
||||||
const maxContainerHeight = getBoundTextMaxHeight(
|
const maxContainerHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
);
|
);
|
||||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
|
||||||
|
|
||||||
|
let nextHeight = containerDims.height;
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (metrics.height > maxContainerHeight) {
|
||||||
const nextHeight = computeContainerDimensionForBoundText(
|
nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
}
|
}
|
||||||
if (metrics.width > maxContainerWidth) {
|
|
||||||
const nextWidth = computeContainerDimensionForBoundText(
|
|
||||||
metrics.width,
|
|
||||||
container.type,
|
|
||||||
);
|
|
||||||
mutateElement(container, { width: nextWidth });
|
|
||||||
}
|
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
@ -163,7 +155,6 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
shouldMaintainAspectRatio = false,
|
|
||||||
) => {
|
) => {
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId) {
|
if (!boundTextElementId) {
|
||||||
@ -184,17 +175,15 @@ export const handleBindTextResize = (
|
|||||||
let text = textElement.text;
|
let text = textElement.text;
|
||||||
let nextHeight = textElement.height;
|
let nextHeight = textElement.height;
|
||||||
let nextWidth = textElement.width;
|
let nextWidth = textElement.width;
|
||||||
|
const containerDims = getContainerDims(container);
|
||||||
const maxWidth = getBoundTextMaxWidth(container);
|
const maxWidth = getBoundTextMaxWidth(container);
|
||||||
const maxHeight = getBoundTextMaxHeight(
|
const maxHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
);
|
);
|
||||||
let containerHeight = container.height;
|
let containerHeight = containerDims.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
let nextBaseLine = textElement.baseline;
|
||||||
if (
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
shouldMaintainAspectRatio ||
|
|
||||||
(transformHandleType !== "n" && transformHandleType !== "s")
|
|
||||||
) {
|
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
@ -218,7 +207,7 @@ export const handleBindTextResize = (
|
|||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
const diff = containerHeight - container.height;
|
const diff = containerHeight - containerDims.height;
|
||||||
// fix the y coord when resizing from ne/nw/n
|
// fix the y coord when resizing from ne/nw/n
|
||||||
const updatedY =
|
const updatedY =
|
||||||
!isArrowElement(container) &&
|
!isArrowElement(container) &&
|
||||||
@ -698,6 +687,16 @@ export const getContainerElement = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getContainerDims = (element: ExcalidrawElement) => {
|
||||||
|
const MIN_WIDTH = 300;
|
||||||
|
if (isArrowElement(element)) {
|
||||||
|
const width = Math.max(element.width, MIN_WIDTH);
|
||||||
|
const height = element.height;
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
return { width: element.width, height: element.height };
|
||||||
|
};
|
||||||
|
|
||||||
export const getContainerCenter = (
|
export const getContainerCenter = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
@ -866,9 +865,8 @@ const VALID_CONTAINER_TYPES = new Set([
|
|||||||
"arrow",
|
"arrow",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const isValidTextContainer = (element: {
|
export const isValidTextContainer = (element: ExcalidrawElement) =>
|
||||||
type: ExcalidrawElement["type"];
|
VALID_CONTAINER_TYPES.has(element.type);
|
||||||
}) => VALID_CONTAINER_TYPES.has(element.type);
|
|
||||||
|
|
||||||
export const computeContainerDimensionForBoundText = (
|
export const computeContainerDimensionForBoundText = (
|
||||||
dimension: number,
|
dimension: number,
|
||||||
@ -889,19 +887,12 @@ export const computeContainerDimensionForBoundText = (
|
|||||||
return dimension + padding;
|
return dimension + padding;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextMaxWidth = (
|
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
|
||||||
container: ExcalidrawElement,
|
const width = getContainerDims(container).width;
|
||||||
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
|
|
||||||
container,
|
|
||||||
),
|
|
||||||
) => {
|
|
||||||
const { width } = container;
|
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const minWidth =
|
return width - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
|
|
||||||
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO;
|
|
||||||
return Math.max(ARROW_LABEL_WIDTH_FRACTION * width, minWidth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.type === "ellipse") {
|
if (container.type === "ellipse") {
|
||||||
// The width of the largest rectangle inscribed inside an ellipse is
|
// The width of the largest rectangle inscribed inside an ellipse is
|
||||||
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
||||||
@ -920,7 +911,7 @@ export const getBoundTextMaxHeight = (
|
|||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
) => {
|
) => {
|
||||||
const { height } = container;
|
const height = getContainerDims(container).height;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
if (containerHeight <= 0) {
|
if (containerHeight <= 0) {
|
||||||
|
@ -955,7 +955,7 @@ describe("textWysiwyg", () => {
|
|||||||
// should center align horizontally and vertically by default
|
// should center align horizontally and vertically by default
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
85,
|
85,
|
||||||
4.5,
|
4.5,
|
||||||
]
|
]
|
||||||
@ -979,7 +979,7 @@ describe("textWysiwyg", () => {
|
|||||||
// should left align horizontally and bottom vertically after resize
|
// should left align horizontally and bottom vertically after resize
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
15,
|
15,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
@ -1001,7 +1001,7 @@ describe("textWysiwyg", () => {
|
|||||||
// should right align horizontally and top vertically after resize
|
// should right align horizontally and top vertically after resize
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
375,
|
375,
|
||||||
-539,
|
-539,
|
||||||
]
|
]
|
||||||
@ -1279,7 +1279,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
15,
|
15,
|
||||||
25,
|
25,
|
||||||
]
|
]
|
||||||
@ -1290,7 +1290,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center"));
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
30,
|
30,
|
||||||
25,
|
25,
|
||||||
]
|
]
|
||||||
@ -1302,7 +1302,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
45,
|
45,
|
||||||
25,
|
25,
|
||||||
]
|
]
|
||||||
@ -1313,7 +1313,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
15,
|
15,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
@ -1325,7 +1325,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
30,
|
30,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
@ -1337,7 +1337,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
45,
|
45,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
@ -1349,7 +1349,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
15,
|
15,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
@ -1360,7 +1360,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center"));
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
30,
|
30,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
@ -1371,7 +1371,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Right"));
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
Array [
|
||||||
45,
|
45,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
|
@ -23,6 +23,7 @@ import { AppState } from "../types";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
@ -176,19 +177,20 @@ export const textWysiwyg = ({
|
|||||||
updatedTextElement,
|
updatedTextElement,
|
||||||
editable,
|
editable,
|
||||||
);
|
);
|
||||||
|
const containerDims = getContainerDims(container);
|
||||||
|
|
||||||
let originalContainerData;
|
let originalContainerData;
|
||||||
if (propertiesUpdated) {
|
if (propertiesUpdated) {
|
||||||
originalContainerData = updateOriginalContainerCache(
|
originalContainerData = updateOriginalContainerCache(
|
||||||
container.id,
|
container.id,
|
||||||
container.height,
|
containerDims.height,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
originalContainerData = originalContainerCache[container.id];
|
originalContainerData = originalContainerCache[container.id];
|
||||||
if (!originalContainerData) {
|
if (!originalContainerData) {
|
||||||
originalContainerData = updateOriginalContainerCache(
|
originalContainerData = updateOriginalContainerCache(
|
||||||
container.id,
|
container.id,
|
||||||
container.height,
|
containerDims.height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,7 +214,7 @@ export const textWysiwyg = ({
|
|||||||
// autoshrink container height until original container height
|
// autoshrink container height until original container height
|
||||||
// is reached when text is removed
|
// is reached when text is removed
|
||||||
!isArrowElement(container) &&
|
!isArrowElement(container) &&
|
||||||
container.height > originalContainerData.height &&
|
containerDims.height > originalContainerData.height &&
|
||||||
textElementHeight < maxHeight
|
textElementHeight < maxHeight
|
||||||
) {
|
) {
|
||||||
const targetContainerHeight = computeContainerDimensionForBoundText(
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
|
@ -86,15 +86,15 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
|||||||
|
|
||||||
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
|
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "embeddable";
|
|
||||||
/**
|
/**
|
||||||
* indicates whether the embeddable src (url) has been validated for rendering.
|
* indicates whether the embeddable src (url) has been validated for rendering.
|
||||||
* null value indicates that the validation is pending. We reset the
|
* nullish value indicates that the validation is pending. We reset the
|
||||||
* value on each restore (or url change) so that we can guarantee
|
* value on each restore (or url change) so that we can guarantee
|
||||||
* the validation came from a trusted source (the editor). Also because we
|
* the validation came from a trusted source (the editor). Also because we
|
||||||
* may not have access to host-app supplied url validator during restore.
|
* may not have access to host-app supplied url validator during restore.
|
||||||
*/
|
*/
|
||||||
validated: boolean | null;
|
validated?: boolean;
|
||||||
|
type: "embeddable";
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||||
@ -123,6 +123,7 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
|
|||||||
export type ExcalidrawGenericElement =
|
export type ExcalidrawGenericElement =
|
||||||
| ExcalidrawSelectionElement
|
| ExcalidrawSelectionElement
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement;
|
| ExcalidrawEllipseElement;
|
||||||
|
|
||||||
@ -137,8 +138,7 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawLinearElement
|
| ExcalidrawLinearElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawFrameElement
|
| ExcalidrawFrameElement;
|
||||||
| ExcalidrawEmbeddableElement;
|
|
||||||
|
|
||||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
@ -171,7 +171,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (
|
||||||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||||
|
) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
Object.defineProperties(window, {
|
Object.defineProperties(window, {
|
||||||
collab: {
|
collab: {
|
||||||
@ -857,7 +860,10 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (
|
||||||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||||
|
) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,9 +19,7 @@ export const AppWelcomeScreen: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
style={{ pointerEvents: "all" }}
|
style={{ pointerEvents: "all" }}
|
||||||
href={`${
|
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||||
import.meta.env.VITE_APP_PLUS_APP
|
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
|
||||||
key={idx}
|
key={idx}
|
||||||
>
|
>
|
||||||
Excalidraw+
|
Excalidraw+
|
||||||
|
@ -6,9 +6,7 @@ export const ExcalidrawPlusAppLink = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`${
|
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||||
import.meta.env.VITE_APP_PLUS_APP
|
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="plus-button"
|
className="plus-button"
|
||||||
|
@ -21,12 +21,10 @@ import { ResolutionType } from "../../utility-types";
|
|||||||
|
|
||||||
let FIREBASE_CONFIG: Record<string, any>;
|
let FIREBASE_CONFIG: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
FIREBASE_CONFIG = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Error JSON parsing firebase config. Supplied value: ${
|
`Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
|
||||||
import.meta.env.VITE_APP_FIREBASE_CONFIG
|
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
FIREBASE_CONFIG = {};
|
FIREBASE_CONFIG = {};
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
isSyncableElement(element),
|
isSyncableElement(element),
|
||||||
) as SyncableExcalidrawElement[];
|
) as SyncableExcalidrawElement[];
|
||||||
|
|
||||||
const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
|
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
||||||
const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
|
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
||||||
|
|
||||||
const generateRoomId = async () => {
|
const generateRoomId = async () => {
|
||||||
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
||||||
@ -67,16 +67,16 @@ export const getCollabServer = async (): Promise<{
|
|||||||
url: string;
|
url: string;
|
||||||
polling: boolean;
|
polling: boolean;
|
||||||
}> => {
|
}> => {
|
||||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
if (process.env.REACT_APP_WS_SERVER_URL) {
|
||||||
return {
|
return {
|
||||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
url: process.env.REACT_APP_WS_SERVER_URL,
|
||||||
polling: true,
|
polling: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
`${process.env.REACT_APP_PORTAL_URL}/collab-server`,
|
||||||
);
|
);
|
||||||
return await resp.json();
|
return await resp.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
31
src/excalidraw-app/pwa.ts
Normal file
31
src/excalidraw-app/pwa.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { register as registerServiceWorker } from "../serviceWorkerRegistration";
|
||||||
|
import { EVENT } from "../constants";
|
||||||
|
|
||||||
|
// On Apple mobile devices add the proprietary app icon and splashscreen markup.
|
||||||
|
// No one should have to do this manually, and eventually this annoyance will
|
||||||
|
// go away once https://bugs.webkit.org/show_bug.cgi?id=183937 is fixed.
|
||||||
|
if (
|
||||||
|
/\b(iPad|iPhone|iPod|Safari)\b/.test(navigator.userAgent) &&
|
||||||
|
!matchMedia("(display-mode: standalone)").matches
|
||||||
|
) {
|
||||||
|
import(/* webpackChunkName: "pwacompat" */ "pwacompat");
|
||||||
|
}
|
||||||
|
|
||||||
|
registerServiceWorker({
|
||||||
|
onUpdate: (registration) => {
|
||||||
|
const waitingServiceWorker = registration.waiting;
|
||||||
|
if (waitingServiceWorker) {
|
||||||
|
waitingServiceWorker.addEventListener(
|
||||||
|
EVENT.STATE_CHANGE,
|
||||||
|
(event: Event) => {
|
||||||
|
const target = event.target as ServiceWorker;
|
||||||
|
const state = target.state as ServiceWorkerState;
|
||||||
|
if (state === "activated") {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
waitingServiceWorker.postMessage({ type: "SKIP_WAITING" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
@ -6,11 +6,12 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
|
|||||||
"vercel.app": "staging",
|
"vercel.app": "staging",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SENTRY_DISABLED = import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
|
const REACT_APP_DISABLE_SENTRY =
|
||||||
|
process.env.REACT_APP_DISABLE_SENTRY === "true";
|
||||||
|
|
||||||
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy
|
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy
|
||||||
const onlineEnv =
|
const onlineEnv =
|
||||||
!SENTRY_DISABLED &&
|
!REACT_APP_DISABLE_SENTRY &&
|
||||||
Object.keys(SentryEnvHostnameMap).find(
|
Object.keys(SentryEnvHostnameMap).find(
|
||||||
(item) => window.location.hostname.indexOf(item) >= 0,
|
(item) => window.location.hostname.indexOf(item) >= 0,
|
||||||
);
|
);
|
||||||
@ -20,7 +21,7 @@ Sentry.init({
|
|||||||
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
||||||
: undefined,
|
: undefined,
|
||||||
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
|
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
|
||||||
release: import.meta.env.VITE_APP_GIT_SHA,
|
release: process.env.REACT_APP_GIT_SHA,
|
||||||
ignoreErrors: [
|
ignoreErrors: [
|
||||||
"undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
|
"undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
|
||||||
],
|
],
|
||||||
|
10
src/global.d.ts
vendored
10
src/global.d.ts
vendored
@ -38,6 +38,16 @@ interface CanvasRenderingContext2D {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
readonly REACT_APP_BACKEND_V2_GET_URL: string;
|
||||||
|
readonly REACT_APP_BACKEND_V2_POST_URL: string;
|
||||||
|
readonly REACT_APP_PORTAL_URL: string;
|
||||||
|
readonly REACT_APP_FIREBASE_CONFIG: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Clipboard extends EventTarget {
|
interface Clipboard extends EventTarget {
|
||||||
write(data: any[]): Promise<void>;
|
write(data: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import fallbackLangData from "./locales/en.json";
|
import fallbackLangData from "./locales/en.json";
|
||||||
import percentages from "./locales/percentages.json";
|
import percentages from "./locales/percentages.json";
|
||||||
|
import { ENV } from "./constants";
|
||||||
import { jotaiScope, jotaiStore } from "./jotai";
|
import { jotaiScope, jotaiStore } from "./jotai";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import { NestedKeyOf } from "./utility-types";
|
import { NestedKeyOf } from "./utility-types";
|
||||||
@ -73,7 +74,7 @@ export const languages: Language[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const TEST_LANG_CODE = "__test__";
|
const TEST_LANG_CODE = "__test__";
|
||||||
if (import.meta.env.DEV) {
|
if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
|
||||||
languages.unshift(
|
languages.unshift(
|
||||||
{ code: TEST_LANG_CODE, label: "test language" },
|
{ code: TEST_LANG_CODE, label: "test language" },
|
||||||
{
|
{
|
||||||
@ -144,7 +145,7 @@ export const t = (
|
|||||||
if (translation === undefined) {
|
if (translation === undefined) {
|
||||||
const errorMessage = `Can't find translation for ${path}`;
|
const errorMessage = `Can't find translation for ${path}`;
|
||||||
// in production, don't blow up the app on a missing translation key
|
// in production, don't blow up the app on a missing translation key
|
||||||
if (import.meta.env.PROD) {
|
if (process.env.NODE_ENV === "production") {
|
||||||
console.warn(errorMessage);
|
console.warn(errorMessage);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import ExcalidrawApp from "./excalidraw-app";
|
import ExcalidrawApp from "./excalidraw-app";
|
||||||
import { registerSW } from "virtual:pwa-register";
|
|
||||||
|
|
||||||
|
import "./excalidraw-app/pwa";
|
||||||
import "./excalidraw-app/sentry";
|
import "./excalidraw-app/sentry";
|
||||||
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
||||||
const rootElement = document.getElementById("root")!;
|
const rootElement = document.getElementById("root")!;
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
registerSW();
|
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ExcalidrawApp />
|
<ExcalidrawApp />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw.production.min.js",
|
"path": "dist/excalidraw.production.min.js",
|
||||||
"limit": "290 kB"
|
"limit": "298 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw-assets/locales",
|
"path": "dist/excalidraw-assets/locales",
|
||||||
|
@ -9,9 +9,9 @@ const parseEnvVariables = (filepath) => {
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
envVars.VITE_PKG_NAME = JSON.stringify(pkg.name);
|
envVars.PKG_NAME = JSON.stringify(pkg.name);
|
||||||
envVars.VITE_PKG_VERSION = JSON.stringify(pkg.version);
|
envVars.PKG_VERSION = JSON.stringify(pkg.version);
|
||||||
envVars.VITE_IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
||||||
return envVars;
|
return envVars;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,7 +75,6 @@ const {
|
|||||||
WelcomeScreen,
|
WelcomeScreen,
|
||||||
MainMenu,
|
MainMenu,
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
convertToExcalidrawElements,
|
|
||||||
} = window.ExcalidrawLib;
|
} = window.ExcalidrawLib;
|
||||||
|
|
||||||
const COMMENT_ICON_DIMENSION = 32;
|
const COMMENT_ICON_DIMENSION = 32;
|
||||||
@ -141,10 +140,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
initialStatePromiseRef.current.promise.resolve({
|
initialStatePromiseRef.current.promise.resolve(initialData);
|
||||||
...initialData,
|
|
||||||
elements: convertToExcalidrawElements(initialData.elements),
|
|
||||||
});
|
|
||||||
excalidrawAPI.addFiles(imagesArray);
|
excalidrawAPI.addFiles(imagesArray);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -188,40 +184,38 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
const updateScene = () => {
|
const updateScene = () => {
|
||||||
const sceneData = {
|
const sceneData = {
|
||||||
elements: restoreElements(
|
elements: restoreElements(
|
||||||
convertToExcalidrawElements([
|
[
|
||||||
{
|
{
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
id: "rect-1",
|
version: 141,
|
||||||
|
versionNonce: 361174001,
|
||||||
|
isDeleted: false,
|
||||||
|
id: "oDVXy8D6rom3H1-LLH2-f",
|
||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
x: 100.50390625,
|
x: 100.50390625,
|
||||||
y: 93.67578125,
|
y: 93.67578125,
|
||||||
strokeColor: "#c92a2a",
|
strokeColor: "#c92a2a",
|
||||||
|
backgroundColor: "transparent",
|
||||||
width: 186.47265625,
|
width: 186.47265625,
|
||||||
height: 141.9765625,
|
height: 141.9765625,
|
||||||
seed: 1968410350,
|
seed: 1968410350,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
boundElements: null,
|
||||||
|
locked: false,
|
||||||
|
link: null,
|
||||||
|
updated: 1,
|
||||||
roundness: {
|
roundness: {
|
||||||
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
||||||
value: 32,
|
value: 32,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
type: "arrow",
|
|
||||||
x: 300,
|
|
||||||
y: 150,
|
|
||||||
start: { id: "rect-1" },
|
|
||||||
end: { type: "ellipse" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
x: 300,
|
|
||||||
y: 100,
|
|
||||||
text: "HELLO WORLD!",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
appState: {
|
appState: {
|
||||||
@ -504,6 +498,52 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
const renderMenu = () => {
|
const renderMenu = () => {
|
||||||
return (
|
return (
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger
|
||||||
|
title="Custom trigger"
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zm-7.518-.267A8.25 8.25 0 1120.25 10.5M8.288 14.212A5.25 5.25 0 1117.25 10.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submenu trigger
|
||||||
|
</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
onSelect={() => window.alert("You clicked on sub item")}
|
||||||
|
>
|
||||||
|
Sub item
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
<MainMenu.DefaultItems.Export />
|
<MainMenu.DefaultItems.Export />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
@ -511,10 +551,57 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onSelect={() => window.alert("You clicked on collab button")}
|
onSelect={() => window.alert("You clicked on collab button")}
|
||||||
/>
|
/>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Trigger</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => window.alert("You clicked on sub item")}
|
||||||
|
>
|
||||||
|
Sub item
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
<MainMenu.Group title="Excalidraw links">
|
<MainMenu.Group title="Excalidraw links">
|
||||||
<MainMenu.DefaultItems.Socials />
|
<MainMenu.DefaultItems.Socials />
|
||||||
</MainMenu.Group>
|
</MainMenu.Group>
|
||||||
<MainMenu.Separator />
|
{/* <MainMenu.Separator /> */}
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger className="custom-classname">
|
||||||
|
Another submenu trigger
|
||||||
|
</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content className="custom-classname-for-content">
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
title="Sub item"
|
||||||
|
onSelect={() => window.alert("You clicked on sub item")}
|
||||||
|
>
|
||||||
|
Sub item
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Trigger me</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Trigger me inside</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => {
|
||||||
|
alert("wow, nested submenus!");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Item wow
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => {
|
||||||
|
alert("wow, nested submenus! very cool");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Another one
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
<MainMenu.ItemCustom>
|
<MainMenu.ItemCustom>
|
||||||
<button
|
<button
|
||||||
style={{ height: "2rem" }}
|
style={{ height: "2rem" }}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -253,4 +253,3 @@ export { LiveCollaborationTrigger };
|
|||||||
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
||||||
|
|
||||||
export { normalizeLink } from "../../data/url";
|
export { normalizeLink } from "../../data/url";
|
||||||
export { convertToExcalidrawElements } from "../../data/transform";
|
|
||||||
|
@ -59,7 +59,6 @@
|
|||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.7.1",
|
"css-loader": "6.7.1",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"import-meta-loader": "1.1.0",
|
|
||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "7.0.1",
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
|
@ -4,5 +4,5 @@ if (process.env.NODE_ENV !== ENV.TEST) {
|
|||||||
/* global __webpack_public_path__:writable */
|
/* global __webpack_public_path__:writable */
|
||||||
__webpack_public_path__ =
|
__webpack_public_path__ =
|
||||||
window.EXCALIDRAW_ASSET_PATH ||
|
window.EXCALIDRAW_ASSET_PATH ||
|
||||||
`https://unpkg.com/${process.env.VITE_PKG_NAME}@${process.env.VITE_PKG_VERSION}/dist/`;
|
`https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ const path = require("path");
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const autoprefixer = require("autoprefixer");
|
const autoprefixer = require("autoprefixer");
|
||||||
const { parseEnvVariables } = require("./env");
|
const { parseEnvVariables } = require("./env");
|
||||||
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
|
|
||||||
|
|
||||||
|
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "development",
|
mode: "development",
|
||||||
devtool: false,
|
devtool: false,
|
||||||
@ -17,6 +17,7 @@ module.exports = {
|
|||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
|
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
|
||||||
assetModuleFilename: "excalidraw-assets-dev/[name][ext]",
|
assetModuleFilename: "excalidraw-assets-dev/[name][ext]",
|
||||||
|
|
||||||
publicPath: "",
|
publicPath: "",
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -44,11 +45,8 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
use: [
|
use: [
|
||||||
{
|
|
||||||
loader: "import-meta-loader",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
options: {
|
options: {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const webpack = require("webpack");
|
|
||||||
const autoprefixer = require("autoprefixer");
|
|
||||||
const { parseEnvVariables } = require("./env");
|
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const BundleAnalyzerPlugin =
|
const BundleAnalyzerPlugin =
|
||||||
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
|
const autoprefixer = require("autoprefixer");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const { parseEnvVariables } = require("./env");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "production",
|
mode: "production",
|
||||||
@ -47,11 +47,9 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
|
|
||||||
use: [
|
use: [
|
||||||
{
|
|
||||||
loader: "import-meta-loader",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
options: {
|
options: {
|
||||||
|
@ -2916,11 +2916,6 @@ import-local@^3.0.2:
|
|||||||
pkg-dir "^4.2.0"
|
pkg-dir "^4.2.0"
|
||||||
resolve-cwd "^3.0.0"
|
resolve-cwd "^3.0.0"
|
||||||
|
|
||||||
import-meta-loader@1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/import-meta-loader/-/import-meta-loader-1.1.0.tgz#927060305f2d0f88b495f2754aa33387ca6579d7"
|
|
||||||
integrity sha512-f96r2o8xT+b2KVlOY4x+1KTJmJiapZlf77j1WebR8NQgMG1dpdqijjGl4i/2jMoXch2CVqcQoTMfh5BR7bR8wA==
|
|
||||||
|
|
||||||
indexes-of@^1.0.1:
|
indexes-of@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
||||||
|
@ -915,7 +915,7 @@ const drawElementFromCanvas = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
||||||
"true" &&
|
"true" &&
|
||||||
hasBoundTextElement(element)
|
hasBoundTextElement(element)
|
||||||
) {
|
) {
|
||||||
|
@ -162,52 +162,23 @@ const fillCircle = (
|
|||||||
const strokeGrid = (
|
const strokeGrid = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
gridSize: number,
|
gridSize: number,
|
||||||
scrollX: number,
|
offsetX: number,
|
||||||
scrollY: number,
|
offsetY: number,
|
||||||
zoom: Zoom,
|
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
) => {
|
) => {
|
||||||
const BOLD_LINE_FREQUENCY = 5;
|
|
||||||
|
|
||||||
enum GridLineColor {
|
|
||||||
Bold = "#cccccc",
|
|
||||||
Regular = "#e5e5e5",
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetX =
|
|
||||||
-Math.round(zoom.value / gridSize) * gridSize + (scrollX % gridSize);
|
|
||||||
const offsetY =
|
|
||||||
-Math.round(zoom.value / gridSize) * gridSize + (scrollY % gridSize);
|
|
||||||
|
|
||||||
const lineWidth = Math.min(1 / zoom.value, 1);
|
|
||||||
|
|
||||||
const spaceWidth = 1 / zoom.value;
|
|
||||||
const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)];
|
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.lineWidth = lineWidth;
|
context.strokeStyle = "rgba(0,0,0,0.1)";
|
||||||
|
|
||||||
for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
|
|
||||||
const isBold =
|
|
||||||
Math.round(x - scrollX) % (BOLD_LINE_FREQUENCY * gridSize) === 0;
|
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.setLineDash(isBold ? [] : lineDash);
|
for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
|
||||||
context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
|
|
||||||
context.moveTo(x, offsetY - gridSize);
|
context.moveTo(x, offsetY - gridSize);
|
||||||
context.lineTo(x, offsetY + height + gridSize * 2);
|
context.lineTo(x, offsetY + height + gridSize * 2);
|
||||||
context.stroke();
|
|
||||||
}
|
}
|
||||||
for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
|
for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
|
||||||
const isBold =
|
|
||||||
Math.round(y - scrollY) % (BOLD_LINE_FREQUENCY * gridSize) === 0;
|
|
||||||
context.beginPath();
|
|
||||||
context.setLineDash(isBold ? [] : lineDash);
|
|
||||||
context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
|
|
||||||
context.moveTo(offsetX - gridSize, y);
|
context.moveTo(offsetX - gridSize, y);
|
||||||
context.lineTo(offsetX + width + gridSize * 2, y);
|
context.lineTo(offsetX + width + gridSize * 2, y);
|
||||||
context.stroke();
|
|
||||||
}
|
}
|
||||||
|
context.stroke();
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -454,9 +425,12 @@ export const _renderScene = ({
|
|||||||
strokeGrid(
|
strokeGrid(
|
||||||
context,
|
context,
|
||||||
appState.gridSize,
|
appState.gridSize,
|
||||||
renderConfig.scrollX,
|
-Math.ceil(renderConfig.zoom.value / appState.gridSize) *
|
||||||
renderConfig.scrollY,
|
appState.gridSize +
|
||||||
renderConfig.zoom,
|
(renderConfig.scrollX % appState.gridSize),
|
||||||
|
-Math.ceil(renderConfig.zoom.value / appState.gridSize) *
|
||||||
|
appState.gridSize +
|
||||||
|
(renderConfig.scrollY % appState.gridSize),
|
||||||
normalizedCanvasWidth / renderConfig.zoom.value,
|
normalizedCanvasWidth / renderConfig.zoom.value,
|
||||||
normalizedCanvasHeight / renderConfig.zoom.value,
|
normalizedCanvasHeight / renderConfig.zoom.value,
|
||||||
);
|
);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user