Compare commits
209 Commits
aakansha-f
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
3ea07076ad | ||
|
ded0222e8d | ||
|
e7983bc493 | ||
|
083bcf802c | ||
|
bb985eba3a | ||
|
23c88a38d0 | ||
|
b85d5fa12b | ||
|
9391a09e54 | ||
|
50450a7dab | ||
|
fc9a9a2571 | ||
|
6126c34dc0 | ||
|
12e37e3dd2 | ||
|
9ca27c62c7 | ||
|
1acc646534 | ||
|
66bac50de3 | ||
|
5dd1efde8a | ||
|
a7c590d459 | ||
|
48924688c7 | ||
|
8af9ea3cf3 | ||
|
cbd908097f | ||
|
c1247742ea | ||
|
8104068bd5 | ||
|
dcc75ed007 | ||
|
e93bbc5776 | ||
|
2a2495175b | ||
|
b57b3b573d | ||
|
744e5b2ab3 | ||
|
d36c72c435 | ||
|
e57dc405fa | ||
|
41ed019bc2 | ||
|
f7c3644342 | ||
|
5e3550fc14 | ||
|
70888327a3 | ||
|
9fc15d81a0 | ||
|
a80ac4c748 | ||
|
9f76f8677b | ||
|
2e46e27490 | ||
|
cf0413338e | ||
|
49e4289878 | ||
|
3ddcc48e4c | ||
|
29a5e982c3 | ||
|
b33fa6d6f6 | ||
|
b7350f9707 | ||
|
8dfa2a98bb | ||
|
fb01ce2a00 | ||
|
3d57112480 | ||
|
7558a4e2be | ||
|
6d56634289 | ||
|
0aa1e66486 | ||
|
7f7128ec09 | ||
|
6de6a96abf | ||
|
28ab6531c9 | ||
|
81ebf82979 | ||
|
4d7d96eb7b | ||
|
1747e93957 | ||
|
3bd5d87cac | ||
|
74d2fc6406 | ||
|
ce9acfbc55 | ||
|
16c7945ca0 | ||
|
5ca3613cc3 | ||
|
b4abfad638 | ||
|
a39640ead1 | ||
|
84bd9bd4ff | ||
|
ae7ff76126 | ||
|
952aa63f86 | ||
|
a065ec67a9 | ||
|
079aa72475 | ||
|
644685a5a8 | ||
|
7bf4de5892 | ||
|
253c5c7866 | ||
|
82d8d02697 | ||
|
1e3c94a37a | ||
|
a91e401554 | ||
|
08563e7d7b | ||
|
6459ccda6a | ||
|
75bea48b54 | ||
|
13780f390a | ||
|
fecbde3f5c | ||
|
7340c70a06 | ||
|
a4f05339aa | ||
|
a8f0a14610 | ||
|
a89952e32f | ||
|
5b7596582f | ||
|
6977c32631 | ||
|
f6f9ed0396 | ||
|
b1b325b9a7 | ||
|
5bf27a463c | ||
|
306e133651 | ||
|
e0f2869374 | ||
|
2c511e30cd | ||
|
fff9d1522a | ||
|
e619e06055 | ||
|
d8965ee823 | ||
|
560231d365 | ||
|
026949204d | ||
|
1184a8c0e9 | ||
|
e9cae918a7 | ||
|
b1311a407a | ||
|
2a39d0b9a7 | ||
|
6b0218b012 | ||
|
45a57d70de | ||
|
da8dd389a9 | ||
|
dae81c0a2c | ||
|
1e9943323a | ||
|
1815cf3213 | ||
|
d35386755f | ||
|
9d5cfbbfb7 | ||
|
fee760d38c | ||
|
2a4799d8c8 | ||
|
c4445c181b | ||
|
d12a9fdd40 | ||
|
9368a9ce3e | ||
|
851b9b7aec | ||
|
5ddb28d378 | ||
|
404a79e241 | ||
|
eea30da05a | ||
|
98a77d7426 | ||
|
ff3c2e5a16 | ||
|
b64beaf5ba | ||
|
89304c9f66 | ||
|
1d0653ce50 | ||
|
c9c79646c5 | ||
|
979312f779 | ||
|
4d0d844e39 | ||
|
801412bf6b | ||
|
21726e22cc | ||
|
c3e8ddaf58 | ||
|
f640ddc2aa | ||
|
e7e54814e7 | ||
|
e9064a4a87 | ||
|
034113772d | ||
|
d34cd3072f | ||
|
e31230f78c | ||
|
399c92d882 | ||
|
b0b23353cf | ||
|
6164b5273c | ||
|
ca3be2c678 | ||
|
13b27afe0f | ||
|
372743f59f | ||
|
fc601347cf | ||
|
e4d8ba226f | ||
|
ec215362a1 | ||
|
0b8fc4f4b6 | ||
|
c170403b13 | ||
|
705ac9c1ab | ||
|
68692b9d4c | ||
|
d61b3cf83d | ||
|
d2b8f4d2f8 | ||
|
f8e65bb77e | ||
|
3030e96d62 | ||
|
44453b725d | ||
|
25bb6738ea | ||
|
9e52c30ce8 | ||
|
83383977f5 | ||
|
ac4c8b3ca7 | ||
|
5c8941467d | ||
|
0726911fa6 | ||
|
7e330c8ee1 | ||
|
7d21747644 | ||
|
e718136aea | ||
|
fe83e2922d | ||
|
20edddcd4e | ||
|
f6e8be399e | ||
|
ab49cad6b1 | ||
|
6aeb18b784 | ||
|
023313e92f | ||
|
1eee488dab | ||
|
dd4c333925 | ||
|
8542c95a7a | ||
|
cef6094d4c | ||
|
3322f0fa6f | ||
|
34a7d48b95 | ||
|
5c0b15ce2b | ||
|
9f9666110e | ||
|
05ffce62ef | ||
|
0f06fa3851 | ||
|
1ce933d2f5 | ||
|
15655acb5a | ||
|
d5b264c2d2 | ||
|
bd4424bbe3 | ||
|
38fc51b4e3 | ||
|
e1dc748aef | ||
|
0e95e2b386 | ||
|
9659254fd6 | ||
|
39b96cb011 | ||
|
04a8c22f39 | ||
|
e4506be3e8 | ||
|
1e816e87bf | ||
|
5368ddef74 | ||
|
88ff32e9b3 | ||
|
0fcbddda8e | ||
|
b9ba407f96 | ||
|
5acb99777a | ||
|
b107c9af2a | ||
|
c587b85b4e | ||
|
9686141113 | ||
|
0d7ee891e0 | ||
|
71fb60394a | ||
|
c9d18ecab6 | ||
|
8c1168ef33 | ||
|
c3c45a8c37 | ||
|
a8e6028c33 | ||
|
11e2f90ca1 | ||
|
4db87a0b6a | ||
|
4414069617 | ||
|
a9c5bdb878 | ||
|
5a0334f37f | ||
|
d8a4ca6911 | ||
|
eb9eeefc63 |
5
.codesandbox/Dockerfile
Normal file
5
.codesandbox/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
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,7 +27,10 @@
|
|||||||
"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",
|
||||||
@ -37,7 +40,11 @@
|
|||||||
"install-deps": {
|
"install-deps": {
|
||||||
"name": "Install Dependencies",
|
"name": "Install Dependencies",
|
||||||
"command": "yarn install",
|
"command": "yarn install",
|
||||||
"restartOn": { "files": ["yarn.lock"] }
|
"restartOn": {
|
||||||
|
"files": ["yarn.lock"],
|
||||||
|
"branch": false,
|
||||||
|
"resume": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,39 @@
|
|||||||
REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||||
REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||||
REACT_APP_WS_SERVER_URL=http://localhost:3002
|
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||||
|
|
||||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
# set this only if using the collaboration workflow we use on excalidraw.com
|
||||||
REACT_APP_PORTAL_URL=
|
VITE_APP_PORTAL_URL=
|
||||||
|
|
||||||
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"}'
|
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||||
|
|
||||||
# put these in your .env.local, or make sure you don't commit!
|
# put these in your .env.local, or make sure you don't commit!
|
||||||
# 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
|
||||||
REACT_APP_DEV_ENABLE_SW=
|
VITE_APP_DEV_ENABLE_SW=
|
||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
VITE_APP_DISABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
# The port the run the dev server
|
||||||
|
VITE_APP_PORT=3000
|
||||||
|
|
||||||
|
#Debug flags
|
||||||
|
|
||||||
|
# To enable bounding box for text containers
|
||||||
|
VITE_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,17 +1,15 @@
|
|||||||
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||||
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
REACT_APP_PORTAL_URL=https://portal.excalidraw.com
|
VITE_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 REACT_APP_PORTAL_URL flow
|
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
||||||
REACT_APP_WS_SERVER_URL=
|
VITE_APP_WS_SERVER_URL=
|
||||||
|
|
||||||
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_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"}'
|
||||||
|
|
||||||
# production-only vars
|
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
|
VITE_APP_DISABLE_TRACKING=
|
||||||
|
|
||||||
REACT_APP_PLUS_APP=https://app.excalidraw.com
|
|
||||||
|
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 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.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 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.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 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.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 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/semantic-pr-title.yml
vendored
2
.github/workflows/semantic-pr-title.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: Semantic PR title
|
name: Semantic PR title
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- edited
|
- edited
|
||||||
|
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 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
30
.github/workflows/size-limit.yml
vendored
Normal file
30
.github/workflows/size-limit.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: "Bundle Size check @excalidraw/excalidraw"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
size:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CI_JOB_NUMBER: 1
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js 18.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
- name: Install
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
- name: Install in src/packages/excalidraw
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
working-directory: src/packages/excalidraw
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- uses: andresz1/size-limit-action@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
build_script: build:umd
|
||||||
|
skip_step: install
|
||||||
|
directory: src/packages/excalidraw
|
26
.github/workflows/test-coverage-pr.yml
vendored
Normal file
26
.github/workflows/test-coverage-pr.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,3 +25,6 @@ src/packages/excalidraw/types
|
|||||||
src/packages/excalidraw/example/public/bundle.js
|
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
|
||||||
|
dev-dist
|
||||||
|
html
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
FROM node:14-alpine AS build
|
FROM node:18 AS build
|
||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN yarn --ignore-optional
|
RUN yarn --ignore-optional --network-timeout 600000
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
|
140
README.md
140
README.md
@ -1,29 +1,121 @@
|
|||||||
<div align="center" style="display:flex;flex-direction:column;"}>
|
<a href="https://excalidraw.com/" target="_blank" rel="noopener">
|
||||||
<a href="https://excalidraw.com">
|
<picture>
|
||||||
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams."/>
|
<source media="(prefers-color-scheme: dark)" alt="Excalidraw" srcset="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover_dark.png" />
|
||||||
</a>
|
<img alt="Excalidraw" src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover.png" />
|
||||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br/>Collaborative and end-to-end encrypted.</h3>
|
</picture>
|
||||||
<p>
|
</a>
|
||||||
<a href="https://twitter.com/excalidraw">
|
|
||||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
<h4 align="center">
|
||||||
</a>
|
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
||||||
<a href="https://discord.gg/UexuTaE">
|
<a href="https://blog.excalidraw.com">Blog</a> |
|
||||||
<img alt="Chat with us on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
<a href="https://docs.excalidraw.com">Documentation</a> |
|
||||||
</a>
|
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
||||||
</p>
|
</h4>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h2>
|
||||||
|
An open source virtual hand-drawn style whiteboard. </br>
|
||||||
|
Collaborative and end-to-end encrypted. </br>
|
||||||
|
<br />
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Try now
|
<br />
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||||
|
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
||||||
|
</a>
|
||||||
|
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||||
|
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/UexuTaE">
|
||||||
|
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/excalidraw">
|
||||||
|
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Visit [excalidraw.com](https://excalidraw.com) to start sketching.
|
<div align="center">
|
||||||
|
<figure>
|
||||||
|
<a href="https://excalidraw.com" target="_blank" rel="noopener">
|
||||||
|
<img src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2Fproduct_showcase.png" alt="Product showcase" />
|
||||||
|
</a>
|
||||||
|
<figcaption>
|
||||||
|
<p align="center">
|
||||||
|
Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
|
||||||
|
</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Community
|
## Features
|
||||||
|
|
||||||
For latest updates, follow us on [twitter](https://twitter.com/excalidraw). If you need help or want to chat, join us on [Discord](https://discord.gg/UexuTaE). For releases and deep dives, check out our [blog](https://blog.excalidraw.com). Report bugs on [GitHub](https://github.com/excalidraw/excalidraw/issues).
|
The Excalidraw editor (npm package) supports:
|
||||||
|
|
||||||
## Supporting Excalidraw
|
- 💯 Free & open-source.
|
||||||
|
- 🎨 Infinite, canvas-based whiteboard.
|
||||||
|
- ✍️ Hand-drawn like style.
|
||||||
|
- 🌓 Dark mode.
|
||||||
|
- 🏗️ Customizable.
|
||||||
|
- 📷 Image support.
|
||||||
|
- 😀 Shape libraries support.
|
||||||
|
- 👅 Localization (i18n) support.
|
||||||
|
- 🖼️ Export to PNG, SVG & clipboard.
|
||||||
|
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||||
|
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||||
|
- ➡️ Arrow-binding & labeled arrows.
|
||||||
|
- 🔙 Undo / Redo.
|
||||||
|
- 🔍 Zoom and panning support.
|
||||||
|
|
||||||
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
|
## Excalidraw.com
|
||||||
|
|
||||||
|
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||||
|
|
||||||
|
- 📡 PWA support (works offline).
|
||||||
|
- 🤼 Real-time collaboration.
|
||||||
|
- 🔒 End-to-end encryption.
|
||||||
|
- 💾 Local-first support (autosaves to the browser).
|
||||||
|
- 🔗 Shareable links (export to a readonly link you can share with others).
|
||||||
|
|
||||||
|
We'll be adding these features as drop-in plugins for the npm package in the future.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install react react-dom @excalidraw/excalidraw
|
||||||
|
```
|
||||||
|
|
||||||
|
or via yarn
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn add react react-dom @excalidraw/excalidraw
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues).
|
||||||
|
- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE).
|
||||||
|
- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating).
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
|
||||||
|
- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw)
|
||||||
|
|
||||||
|
## Who's integrating Excalidraw
|
||||||
|
|
||||||
|
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others
|
||||||
|
|
||||||
|
## Sponsors & support
|
||||||
|
|
||||||
|
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/).
|
||||||
|
|
||||||
|
## Thank you for supporting Excalidraw
|
||||||
|
|
||||||
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
|
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
|
||||||
|
|
||||||
@ -32,13 +124,3 @@ If you like the project, you can become a sponsor at [Open Collective](https://o
|
|||||||
Last but not least, we're thankful to these companies for offering their services for free:
|
Last but not least, we're thankful to these companies for offering their services for free:
|
||||||
|
|
||||||
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
||||||
|
|
||||||
## Developers
|
|
||||||
|
|
||||||
You can integrate Excalidraw into your app by installing our [npm component](https://npmjs.com/package/@excalidraw/excalidraw).
|
|
||||||
|
|
||||||
Visit our documentation on [https://docs.excalidraw.com](https://docs.excalidraw.com).
|
|
||||||
|
|
||||||
## Who's integrating Excalidraw
|
|
||||||
|
|
||||||
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/)
|
|
||||||
|
@ -16,7 +16,6 @@ function App() {
|
|||||||
className="custom-footer"
|
className="custom-footer"
|
||||||
onClick={() => alert("This is dummy footer")}
|
onClick={() => alert("This is dummy footer")}
|
||||||
>
|
>
|
||||||
{" "}
|
|
||||||
custom footer
|
custom footer
|
||||||
</button>
|
</button>
|
||||||
</Footer>
|
</Footer>
|
||||||
|
@ -14,8 +14,7 @@ function App() {
|
|||||||
Item1
|
Item1
|
||||||
</MainMenu.Item>
|
</MainMenu.Item>
|
||||||
<MainMenu.Item onSelect={() => window.alert("Item2")}>
|
<MainMenu.Item onSelect={() => window.alert("Item2")}>
|
||||||
{" "}
|
Item 2
|
||||||
Item 2{" "}
|
|
||||||
</MainMenu.Item>
|
</MainMenu.Item>
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
@ -93,7 +92,6 @@ function App() {
|
|||||||
style={{ height: "2rem" }}
|
style={{ height: "2rem" }}
|
||||||
onClick={() => window.alert("custom menu item")}
|
onClick={() => window.alert("custom menu item")}
|
||||||
>
|
>
|
||||||
{" "}
|
|
||||||
custom item
|
custom item
|
||||||
</button>
|
</button>
|
||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
|
@ -29,6 +29,8 @@ All `props` are *optional*.
|
|||||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
|
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
||||||
|
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||||
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
@ -215,7 +217,6 @@ Indicates whether to bind keyboard events to `document`. Disabled by default, me
|
|||||||
|
|
||||||
Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar).
|
Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar).
|
||||||
|
|
||||||
|
|
||||||
### autoFocus
|
### autoFocus
|
||||||
|
|
||||||
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
|
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
|
||||||
@ -228,3 +229,12 @@ Allows you to override `id` generation for files added on canvas (images). By de
|
|||||||
(file: File) => string | Promise<string>
|
(file: File) => string | Promise<string>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### validateEmbeddable
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
validateEmbeddable?: boolean | string[] | RegExp | RegExp[] | ((link: string) => boolean | undefined)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an optional property. By default we support a handful of well-known sites. You may allow additional sites or disallow the default ones by supplying a custom validator. If you pass `true`, all URLs will be allowed. You can also supply a list of hostnames, RegExp (or list of RegExp objects), or a function. If the function returns `undefined`, the built-in validator will be used.
|
||||||
|
|
||||||
|
Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.
|
@ -1,6 +1,19 @@
|
|||||||
# ref
|
# ref
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">createRef</a> | <a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a> | <a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">callbackRef</a> | <br/>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">resolvablePromise</a> } }
|
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">
|
||||||
|
createRef
|
||||||
|
</a>{" "}
|
||||||
|
|{" "}
|
||||||
|
<a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a>{" "}
|
||||||
|
|{" "}
|
||||||
|
<a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">
|
||||||
|
callbackRef
|
||||||
|
</a>{" "}
|
||||||
|
| <br />
|
||||||
|
{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
|
||||||
|
resolvablePromise
|
||||||
|
</a> } }
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
|
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
|
||||||
@ -139,7 +152,9 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: "500px" }}>
|
<div style={{ height: "500px" }}>
|
||||||
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
|
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
|
||||||
<button className="custom-button" onClick={updateScene}>Update Scene</button>
|
<button className="custom-button" onClick={updateScene}>
|
||||||
|
Update Scene
|
||||||
|
</button>
|
||||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -187,7 +202,8 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: "500px" }}>
|
<div style={{ height: "500px" }}>
|
||||||
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
|
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
|
||||||
<button className="custom-button"
|
<button
|
||||||
|
className="custom-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const libraryItems = [
|
const libraryItems = [
|
||||||
{
|
{
|
||||||
@ -205,10 +221,8 @@ function App() {
|
|||||||
];
|
];
|
||||||
excalidrawAPI.updateLibrary({
|
excalidrawAPI.updateLibrary({
|
||||||
libraryItems,
|
libraryItems,
|
||||||
openLibraryMenu: true
|
openLibraryMenu: true,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Update Library
|
Update Library
|
||||||
@ -250,7 +264,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
|
|||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
() =>{" "}
|
() =>{" "}
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
ExcalidrawElement[]
|
ExcalidrawElement[]
|
||||||
</a>
|
</a>
|
||||||
</pre>
|
</pre>
|
||||||
@ -261,7 +275,7 @@ Returns all the elements including the deleted in the scene.
|
|||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
() => NonDeleted<
|
() => NonDeleted<
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
</a>
|
</a>
|
||||||
[]>
|
[]>
|
||||||
@ -292,19 +306,34 @@ This is the history API. history.clear() will clear the history.
|
|||||||
|
|
||||||
## scrollToContent
|
## scrollToContent
|
||||||
|
|
||||||
<pre>
|
```tsx
|
||||||
(target?:{" "}
|
(
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
target?: ExcalidrawElement | ExcalidrawElement[],
|
||||||
ExcalidrawElement
|
opts?:
|
||||||
</a>{" "}
|
| {
|
||||||
|{" "}
|
fitToContent?: boolean;
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
animate?: boolean;
|
||||||
ExcalidrawElement
|
duration?: number;
|
||||||
</a>
|
}
|
||||||
[]) => void
|
| {
|
||||||
</pre>
|
fitToViewport?: boolean;
|
||||||
|
viewportZoomFactor?: number;
|
||||||
|
animate?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
) => void
|
||||||
|
```
|
||||||
|
|
||||||
Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene.
|
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
|
||||||
|
|
||||||
|
| Attribute | type | default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
|
||||||
|
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. |
|
||||||
|
| opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. |
|
||||||
|
| opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) |
|
||||||
|
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
|
||||||
|
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
|
||||||
|
|
||||||
## refresh
|
## refresh
|
||||||
|
|
||||||
@ -358,15 +387,18 @@ This API can be used to get the files present in the scene. It may contain files
|
|||||||
|
|
||||||
This API has the below signature. It sets the `tool` passed in param as the active tool.
|
This API has the below signature. It sets the `tool` passed in param as the active tool.
|
||||||
|
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(tool: <br/> { type: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">SHAPES</a>[number]["value"]| "eraser" } |<br/> { type: "custom"; customType: string }) => void
|
(tool: <br /> { type:{" "}
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">
|
||||||
|
SHAPES
|
||||||
|
</a>
|
||||||
|
[number]["value"]| "eraser" } |
|
||||||
|
<br /> { type: "custom"; customType: string }) => void
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## setCursor
|
## setCursor
|
||||||
|
|
||||||
This API can be used to customise the mouse cursor on the canvas and has the below signature.
|
This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param.
|
||||||
It sets the mouse cursor to the cursor passed in param.
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
(cursor: string) => void
|
(cursor: string) => void
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
## renderTopRightUI
|
## renderTopRightUI
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(isMobile: boolean, appState:{" "}
|
(isMobile: boolean, appState:
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||||
AppState
|
AppState
|
||||||
</a>
|
</a>
|
||||||
@ -29,8 +29,7 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
onClick={() => window.alert("This is dummy top right UI")}
|
onClick={() => window.alert("This is dummy top right UI")}
|
||||||
>
|
>
|
||||||
{" "}
|
Click me
|
||||||
Click me{" "}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -55,8 +54,7 @@ function App() {
|
|||||||
<Excalidraw
|
<Excalidraw
|
||||||
renderCustomStats={() => (
|
renderCustomStats={() => (
|
||||||
<p style={{ color: "#70b1ec", fontWeight: "bold" }}>
|
<p style={{ color: "#70b1ec", fontWeight: "bold" }}>
|
||||||
{" "}
|
Dummy stats will be shown here
|
||||||
Dummy stats will be shown here{" "}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -105,8 +103,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: "500px" }}>
|
<div style={{ height: "500px" }}>
|
||||||
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
|
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
|
||||||
{" "}
|
Toggle Custom Sidebar
|
||||||
Toggle Custom Sidebar{" "}
|
|
||||||
</button>
|
</button>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
UIOptions={{ dockedSidebarBreakpoint: 100 }}
|
UIOptions={{ dockedSidebarBreakpoint: 100 }}
|
||||||
@ -124,3 +121,16 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## renderEmbeddable
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
(element: NonDeleted<ExcalidrawEmbeddableElement>, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>) => JSX.Element | null
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
Allows you to replace the renderer for embeddable elements (which renders `<iframe>` elements).
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `element` | `NonDeleted<ExcalidrawEmbeddableElement>` | The embeddable element to be rendered. |
|
||||||
|
| `appState` | `AppState` | The current state of the UI. |
|
||||||
|
@ -31,10 +31,29 @@ You can pass `null` / `undefined` if not applicable.
|
|||||||
restoreElements(
|
restoreElements(
|
||||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
||||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||||
refreshDimensions?: boolean<br/>
|
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||||
)
|
)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
|
||||||
|
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||||
|
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
|
||||||
|
|
||||||
|
#### localElements
|
||||||
|
|
||||||
|
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
|
||||||
|
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
|
||||||
|
|
||||||
|
#### opts
|
||||||
|
The extra optional parameter to configure restored elements. It has the following attributes
|
||||||
|
|
||||||
|
| Prop | Type | Description|
|
||||||
|
| --- | --- | ------|
|
||||||
|
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
|
||||||
|
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||||
|
|
||||||
**_How to use_**
|
**_How to use_**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@ -43,9 +62,6 @@ import { restoreElements } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
|
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
|
||||||
|
|
||||||
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
|
|
||||||
Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates.
|
|
||||||
|
|
||||||
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
|
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
|
||||||
|
|
||||||
### restore
|
### restore
|
||||||
@ -53,10 +69,12 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
|
|||||||
**_Signature_**
|
**_Signature_**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
restoreElements(
|
restore(
|
||||||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
|
||||||
|
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||||
|
|
||||||
)
|
)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
@ -339,3 +339,47 @@ The `device` has the following `attributes`
|
|||||||
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
||||||
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
||||||
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
To help with localization, we export the following.
|
||||||
|
|
||||||
|
| name | type |
|
||||||
|
| --- | --- |
|
||||||
|
| `defaultLang` | `string` |
|
||||||
|
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||||
|
| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
#### defaultLang
|
||||||
|
|
||||||
|
Default language code, `en`.
|
||||||
|
|
||||||
|
#### languages
|
||||||
|
|
||||||
|
List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode).
|
||||||
|
|
||||||
|
#### useI18n
|
||||||
|
|
||||||
|
A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of `<Excalidraw>`.
|
||||||
|
|
||||||
|
```jsx live
|
||||||
|
function App() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
return (
|
||||||
|
<div style={{ height: "500px" }}>
|
||||||
|
<Excalidraw>
|
||||||
|
<button
|
||||||
|
style={{ position: "absolute", zIndex: 10, height: "2rem" }}
|
||||||
|
onClick={() => window.alert(t("labels.madeWithExcalidraw"))}
|
||||||
|
>
|
||||||
|
{t("buttons.confirm")}
|
||||||
|
</button>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -4,6 +4,34 @@
|
|||||||
|
|
||||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
||||||
|
|
||||||
|
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
||||||
|
|
||||||
|
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
|
||||||
|
|
||||||
|
We strongly recommend turning it off. You can follow the steps below on how to do so.
|
||||||
|
|
||||||
|
|
||||||
|
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
|
||||||
|

|
||||||
|
|
||||||
|
<div style={{width:'30rem'}}>
|
||||||
|
|
||||||
|
2. Once opened, look for **Aggressively Block Fingerprinting**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Switch to **Block Fingerprinting**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Thats all. All text elements should be fixed now 🎉
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Need help?
|
## Need help?
|
||||||
|
|
||||||
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
||||||
|
@ -34,14 +34,16 @@ function App() {
|
|||||||
|
|
||||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||||
|
|
||||||
|
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [Comp, setComp] = useState(null);
|
const [Excalidraw, setExcalidraw] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default));
|
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
|
||||||
}, []);
|
}, []);
|
||||||
return <>{Comp && <Comp />}</>;
|
return <>{Excalidraw && <Excalidraw />}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
BIN
dev-docs/docs/assets/aggressive-block-fingerprint.png
Normal file
BIN
dev-docs/docs/assets/aggressive-block-fingerprint.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 214 KiB |
BIN
dev-docs/docs/assets/block-fingerprint.png
Normal file
BIN
dev-docs/docs/assets/block-fingerprint.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 266 KiB |
BIN
dev-docs/docs/assets/brave-shield.png
Normal file
BIN
dev-docs/docs/assets/brave-shield.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
||||||
|
|
||||||
|
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
|
||||||
|
For new contributors we would recommend to start with *Easy* tasks.
|
||||||
|
|
||||||
|
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Option 1 - Manual
|
### Option 1 - Manual
|
||||||
@ -20,7 +25,7 @@ Pull requests are welcome. For major changes, please [open an issue](https://git
|
|||||||
|
|
||||||
### Option 2 - CodeSandbox
|
### Option 2 - CodeSandbox
|
||||||
|
|
||||||
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw
|
||||||
1. Connect your GitHub account
|
1. Connect your GitHub account
|
||||||
1. Go to Git tab on left side
|
1. Go to Git tab on left side
|
||||||
1. Tap on `Fork Sandbox`
|
1. Tap on `Fork Sandbox`
|
||||||
@ -64,6 +69,10 @@ 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
|
||||||
|
|
||||||
|
@ -30,7 +30,10 @@ const config = {
|
|||||||
docs: {
|
docs: {
|
||||||
sidebarPath: require.resolve("./sidebars.js"),
|
sidebarPath: require.resolve("./sidebars.js"),
|
||||||
// Please change this to your repo.
|
// Please change this to your repo.
|
||||||
editUrl: "https://github.com/excalidraw/docs/tree/master/",
|
editUrl:
|
||||||
|
"https://github.com/excalidraw/excalidraw/tree/master/dev-docs/",
|
||||||
|
showLastUpdateAuthor: true,
|
||||||
|
showLastUpdateTime: true,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
customCss: [
|
customCss: [
|
||||||
@ -129,6 +132,11 @@ const config = {
|
|||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
maxHeadingLevel: 4,
|
maxHeadingLevel: 4,
|
||||||
},
|
},
|
||||||
|
algolia: {
|
||||||
|
appId: "8FEAOD28DI",
|
||||||
|
apiKey: "4b07cca33ff2d2919bc95ff98f148e9e",
|
||||||
|
indexName: "excalidraw",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
themes: ["@docusaurus/theme-live-codeblock"],
|
themes: ["@docusaurus/theme-live-codeblock"],
|
||||||
plugins: ["docusaurus-plugin-sass"],
|
plugins: ["docusaurus-plugin-sass"],
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"@docusaurus/core": "2.2.0",
|
"@docusaurus/core": "2.2.0",
|
||||||
"@docusaurus/preset-classic": "2.2.0",
|
"@docusaurus/preset-classic": "2.2.0",
|
||||||
"@docusaurus/theme-live-codeblock": "2.2.0",
|
"@docusaurus/theme-live-codeblock": "2.2.0",
|
||||||
"@excalidraw/excalidraw": "0.14.2",
|
"@excalidraw/excalidraw": "0.15.2",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"docusaurus-plugin-sass": "0.2.3",
|
"docusaurus-plugin-sass": "0.2.3",
|
||||||
|
@ -24,6 +24,7 @@ const ExcalidrawScope = {
|
|||||||
Sidebar: ExcalidrawComp.Sidebar,
|
Sidebar: ExcalidrawComp.Sidebar,
|
||||||
exportToCanvas: ExcalidrawComp.exportToCanvas,
|
exportToCanvas: ExcalidrawComp.exportToCanvas,
|
||||||
initialData,
|
initialData,
|
||||||
|
useI18n: ExcalidrawComp.useI18n,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExcalidrawScope;
|
export default ExcalidrawScope;
|
||||||
|
@ -1631,10 +1631,10 @@
|
|||||||
url-loader "^4.1.1"
|
url-loader "^4.1.1"
|
||||||
webpack "^5.73.0"
|
webpack "^5.73.0"
|
||||||
|
|
||||||
"@excalidraw/excalidraw@0.14.2":
|
"@excalidraw/excalidraw@0.15.2":
|
||||||
version "0.14.2"
|
version "0.15.2"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375"
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
|
||||||
integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==
|
integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
|
||||||
|
|
||||||
"@hapi/hoek@^9.0.0":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "9.3.0"
|
version "9.3.0"
|
||||||
@ -1785,9 +1785,9 @@
|
|||||||
"@hapi/hoek" "^9.0.0"
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@sideway/formula@^3.0.0":
|
"@sideway/formula@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||||
|
|
||||||
"@sideway/pinpoint@^2.0.0":
|
"@sideway/pinpoint@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1:
|
|||||||
entities "^4.3.0"
|
entities "^4.3.0"
|
||||||
|
|
||||||
http-cache-semantics@^4.0.0:
|
http-cache-semantics@^4.0.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||||
|
|
||||||
http-deceiver@^1.2.7:
|
http-deceiver@^1.2.7:
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
@ -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.1"
|
version "5.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
|
||||||
|
|
||||||
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.0"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
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.3.7"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||||
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
|
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
@ -7159,9 +7159,9 @@ typescript@^4.7.4:
|
|||||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||||
|
|
||||||
ua-parser-js@^0.7.30:
|
ua-parser-js@^0.7.30:
|
||||||
version "0.7.31"
|
version "0.7.33"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||||
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
|
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
|
||||||
|
|
||||||
unescape@^1.0.1:
|
unescape@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
@ -7542,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
|
|||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@^5.73.0:
|
webpack@^5.73.0:
|
||||||
version "5.74.0"
|
version "5.76.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
|
||||||
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
|
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.3"
|
"@types/eslint-scope" "^3.7.3"
|
||||||
"@types/estree" "^0.0.51"
|
"@types/estree" "^0.0.51"
|
||||||
|
@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
|
<% if ("%PROD%" === "true") { %>
|
||||||
<script>
|
<script>
|
||||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||||
//
|
//
|
||||||
@ -97,42 +97,37 @@
|
|||||||
window.location.href = "https://app.excalidraw.com";
|
window.location.href = "https://app.excalidraw.com";
|
||||||
}
|
}
|
||||||
</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
|
<link rel="stylesheet" href="/fonts.css" type="text/css" />
|
||||||
rel="manifest"
|
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
||||||
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 process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
"[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return new _WebSocket(url);
|
return new _WebSocket(url);
|
||||||
@ -146,21 +141,6 @@
|
|||||||
// setting this so that libraries installation reuses this window tab.
|
// setting this so that libraries installation reuses this window tab.
|
||||||
window.name = "_excalidraw";
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true' &&
|
|
||||||
process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
|
||||||
<script
|
|
||||||
async
|
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
|
|
||||||
></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag() {
|
|
||||||
dataLayer.push(arguments);
|
|
||||||
}
|
|
||||||
gtag("js", new Date());
|
|
||||||
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
|
|
||||||
</script>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
||||||
<style>
|
<style>
|
||||||
@ -213,5 +193,40 @@
|
|||||||
<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 ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
|
||||||
|
<!-- 100% privacy friendly analytics -->
|
||||||
|
<script>
|
||||||
|
// need to load this script dynamically bcs. of iframe embed tracking
|
||||||
|
var scriptEle = document.createElement("script");
|
||||||
|
scriptEle.setAttribute(
|
||||||
|
"src",
|
||||||
|
"https://scripts.simpleanalyticscdn.com/latest.js",
|
||||||
|
);
|
||||||
|
scriptEle.setAttribute("type", "text/javascript");
|
||||||
|
scriptEle.setAttribute("defer", true);
|
||||||
|
scriptEle.setAttribute("async", true);
|
||||||
|
// if iframe
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
scriptEle.setAttribute("data-auto-collect", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(scriptEle);
|
||||||
|
|
||||||
|
// if iframe
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
scriptEle.addEventListener("load", () => {
|
||||||
|
if (window.sa_pageview) {
|
||||||
|
window.window.sa_event(action, {
|
||||||
|
category: "iframe",
|
||||||
|
label: "embed",
|
||||||
|
value: window.location.pathname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- end LEGACY GOOGLE ANALYTICS -->
|
||||||
|
<% } %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
79
package.json
79
package.json
@ -19,26 +19,26 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dwelle/tunnel-rat": "0.1.1",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
|
"@excalidraw/random-username": "1.0.0",
|
||||||
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@tldraw/vec": "1.7.1",
|
"@tldraw/vec": "1.7.1",
|
||||||
"@types/jest": "27.4.0",
|
|
||||||
"@types/pica": "5.1.3",
|
|
||||||
"@types/react": "18.0.15",
|
|
||||||
"@types/react-dom": "18.0.6",
|
|
||||||
"@types/socket.io-client": "1.4.36",
|
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.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",
|
||||||
"idb-keyval": "6.0.3",
|
"idb-keyval": "6.0.3",
|
||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "1.6.4",
|
"jotai": "1.13.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
@ -52,64 +52,60 @@
|
|||||||
"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.0",
|
"tunnel-rat": "0.1.2"
|
||||||
"typescript": "4.9.4",
|
|
||||||
"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.0",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
|
"@types/pica": "5.1.3",
|
||||||
|
"@types/react": "18.0.15",
|
||||||
|
"@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",
|
||||||
|
"@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",
|
||||||
"jest-canvas-mock": "2.4.0",
|
"jsdom": "22.1.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",
|
||||||
|
"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": ">=14.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"jest": {
|
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
|
||||||
],
|
|
||||||
"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 REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
|
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
||||||
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
||||||
"build:version": "node ./scripts/build-version.js",
|
"build:version": "node ./scripts/build-version.js",
|
||||||
"build": "yarn 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",
|
||||||
@ -117,18 +113,21 @@
|
|||||||
"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": "react-scripts start",
|
"start": "vite",
|
||||||
"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 --watchAll=false",
|
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
||||||
"test:app": "react-scripts test --passWithNoTests",
|
"test:app": "vitest --config vitest.config.ts",
|
||||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||||
"test: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 --updateSnapshot --watchAll=false",
|
"test:update": "yarn test:app --update --watch=false",
|
||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
|
"test:coverage": "vitest --coverage",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
public/service-worker.js
Normal file
20
public/service-worker.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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));
|
||||||
|
});
|
||||||
|
});
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
!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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
!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
|
|
@ -2,6 +2,9 @@ const fs = require("fs");
|
|||||||
|
|
||||||
const THRESSHOLD = 85;
|
const THRESSHOLD = 85;
|
||||||
|
|
||||||
|
// we're using BCP 47 language tags as keys
|
||||||
|
// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
|
||||||
|
|
||||||
const crowdinMap = {
|
const crowdinMap = {
|
||||||
"ar-SA": "en-ar",
|
"ar-SA": "en-ar",
|
||||||
"bg-BG": "en-bg",
|
"bg-BG": "en-bg",
|
||||||
@ -52,6 +55,7 @@ const crowdinMap = {
|
|||||||
"kk-KZ": "en-kk",
|
"kk-KZ": "en-kk",
|
||||||
"vi-VN": "en-vi",
|
"vi-VN": "en-vi",
|
||||||
"mr-IN": "en-mr",
|
"mr-IN": "en-mr",
|
||||||
|
"th-TH": "en-th",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@ -104,6 +108,7 @@ const flags = {
|
|||||||
"eu-ES": "🇪🇦",
|
"eu-ES": "🇪🇦",
|
||||||
"vi-VN": "🇻🇳",
|
"vi-VN": "🇻🇳",
|
||||||
"mr-IN": "🇮🇳",
|
"mr-IN": "🇮🇳",
|
||||||
|
"th-TH": "🇹🇭",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@ -156,6 +161,7 @@ const languages = {
|
|||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
"vi-VN": "Tiếng Việt",
|
"vi-VN": "Tiếng Việt",
|
||||||
"mr-IN": "मराठी",
|
"mr-IN": "मराठी",
|
||||||
|
"th-TH": "ภาษาไทย",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
@ -1,22 +1,9 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
const pkg = require(excalidrawPackage);
|
const pkg = require(excalidrawPackage);
|
||||||
|
|
||||||
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
|
|
||||||
|
|
||||||
const updateReadme = () => {
|
|
||||||
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
|
|
||||||
|
|
||||||
// remove note for stable readme
|
|
||||||
const data = originalReadMe.slice(excalidrawIndex);
|
|
||||||
|
|
||||||
// update readme
|
|
||||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
const publish = () => {
|
const publish = () => {
|
||||||
try {
|
try {
|
||||||
execSync(`yarn --frozen-lockfile`);
|
execSync(`yarn --frozen-lockfile`);
|
||||||
@ -30,15 +17,8 @@ const publish = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const release = () => {
|
const release = () => {
|
||||||
updateReadme();
|
|
||||||
console.info("Note for stable readme removed");
|
|
||||||
|
|
||||||
publish();
|
publish();
|
||||||
console.info(`Published ${pkg.version}!`);
|
console.info(`Published ${pkg.version}!`);
|
||||||
|
|
||||||
// revert readme after release
|
|
||||||
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
|
|
||||||
console.info("Readme reverted");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
release();
|
release();
|
||||||
|
@ -1,27 +1,29 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||||
|
|
||||||
export const actionAddToLibrary = register({
|
export const actionAddToLibrary = register({
|
||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
includeElementsInFrames: true,
|
||||||
);
|
});
|
||||||
if (selectedElements.some((element) => element.type === "image")) {
|
|
||||||
return {
|
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||||
commitToHistory: false,
|
if (selectedElements.some((element) => element.type === type)) {
|
||||||
appState: {
|
return {
|
||||||
...appState,
|
commitToHistory: false,
|
||||||
errorMessage: "Support for adding images to the library coming soon!",
|
appState: {
|
||||||
},
|
...appState,
|
||||||
};
|
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.library
|
return app.library
|
||||||
|
@ -10,44 +10,55 @@ import {
|
|||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (
|
const alignActionsPredicate = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
|
_: unknown,
|
||||||
|
app: AppClassProperties,
|
||||||
|
) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
return (
|
||||||
|
selectedElements.length > 1 &&
|
||||||
|
// TODO enable aligning frames when implemented properly
|
||||||
|
!selectedElements.some((el) => el.type === "frame")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const alignSelectedElements = (
|
const alignSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElements = alignElements(selectedElements, alignment);
|
const updatedElements = alignElements(selectedElements, alignment);
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
return elements.map(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
(element) => updatedElementsMap.get(element.id) || element,
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionAlignTop = register({
|
export const actionAlignTop = register({
|
||||||
name: "alignTop",
|
name: "alignTop",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
predicate: alignActionsPredicate,
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@ -56,9 +67,9 @@ export const actionAlignTop = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignTopIcon}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -74,10 +85,11 @@ export const actionAlignTop = register({
|
|||||||
export const actionAlignBottom = register({
|
export const actionAlignBottom = register({
|
||||||
name: "alignBottom",
|
name: "alignBottom",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
predicate: alignActionsPredicate,
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@ -86,9 +98,9 @@ export const actionAlignBottom = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignBottomIcon}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -104,10 +116,11 @@ export const actionAlignBottom = register({
|
|||||||
export const actionAlignLeft = register({
|
export const actionAlignLeft = register({
|
||||||
name: "alignLeft",
|
name: "alignLeft",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
predicate: alignActionsPredicate,
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@ -116,9 +129,9 @@ export const actionAlignLeft = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignLeftIcon}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -134,11 +147,11 @@ export const actionAlignLeft = register({
|
|||||||
export const actionAlignRight = register({
|
export const actionAlignRight = register({
|
||||||
name: "alignRight",
|
name: "alignRight",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@ -147,9 +160,9 @@ export const actionAlignRight = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignRightIcon}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -165,20 +178,20 @@ export const actionAlignRight = register({
|
|||||||
export const actionAlignVerticallyCentered = register({
|
export const actionAlignVerticallyCentered = register({
|
||||||
name: "alignVerticallyCentered",
|
name: "alignVerticallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterVerticallyIcon}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -192,19 +205,20 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
export const actionAlignHorizontallyCentered = register({
|
export const actionAlignHorizontallyCentered = register({
|
||||||
name: "alignHorizontallyCentered",
|
name: "alignHorizontallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
predicate: alignActionsPredicate,
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterHorizontallyIcon}
|
icon={CenterHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { VERTICAL_ALIGN } from "../constants";
|
import {
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
BOUND_TEXT_PADDING,
|
||||||
|
ROUNDNESS,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
|
TEXT_ALIGN,
|
||||||
|
} from "../constants";
|
||||||
|
import { isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
|
computeBoundTextPosition,
|
||||||
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@ -9,16 +16,21 @@ import {
|
|||||||
import {
|
import {
|
||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
|
updateOriginalContainerCache,
|
||||||
} from "../element/textWysiwyg";
|
} from "../element/textWysiwyg";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
|
isUsingAdaptiveRadius,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { AppState } from "../types";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@ -26,33 +38,34 @@ export const actionUnbindText = register({
|
|||||||
name: "unbindText",
|
name: "unbindText",
|
||||||
contextItemLabel: "labels.unbindText",
|
contextItemLabel: "labels.unbindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureText(
|
const { width, height, baseline } = measureText(
|
||||||
boundTextElement.originalText,
|
boundTextElement.originalText,
|
||||||
getFontString(boundTextElement),
|
getFontString(boundTextElement),
|
||||||
|
boundTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||||
element.id,
|
element.id,
|
||||||
);
|
);
|
||||||
resetOriginalContainerCache(element.id);
|
resetOriginalContainerCache(element.id);
|
||||||
|
const { x, y } = computeBoundTextPosition(element, boundTextElement);
|
||||||
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||||
containerId: null,
|
containerId: null,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
baseline,
|
baseline,
|
||||||
text: boundTextElement.originalText,
|
text: boundTextElement.originalText,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
@ -76,8 +89,8 @@ export const actionBindText = register({
|
|||||||
name: "bindText",
|
name: "bindText",
|
||||||
contextItemLabel: "labels.bindText",
|
contextItemLabel: "labels.bindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
if (selectedElements.length === 2) {
|
if (selectedElements.length === 2) {
|
||||||
const textElement =
|
const textElement =
|
||||||
@ -100,11 +113,8 @@ export const actionBindText = register({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
let container: ExcalidrawTextContainer;
|
let container: ExcalidrawTextContainer;
|
||||||
@ -122,6 +132,7 @@ export const actionBindText = register({
|
|||||||
mutateElement(textElement, {
|
mutateElement(textElement, {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
@ -129,20 +140,166 @@ export const actionBindText = register({
|
|||||||
id: textElement.id,
|
id: textElement.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
const originalContainerHeight = container.height;
|
||||||
redrawTextBoundingBox(textElement, container);
|
redrawTextBoundingBox(textElement, container);
|
||||||
const updatedElements = elements.slice();
|
// overwritting the cache with original container height so
|
||||||
const textElementIndex = updatedElements.findIndex(
|
// it can be restored when unbind
|
||||||
(ele) => ele.id === textElement.id,
|
updateOriginalContainerCache(container.id, originalContainerHeight);
|
||||||
);
|
|
||||||
updatedElements.splice(textElementIndex, 1);
|
|
||||||
const containerIndex = updatedElements.findIndex(
|
|
||||||
(ele) => ele.id === container.id,
|
|
||||||
);
|
|
||||||
updatedElements.splice(containerIndex + 1, 0, textElement);
|
|
||||||
return {
|
return {
|
||||||
elements: updatedElements,
|
elements: pushTextAboveContainer(elements, container, textElement),
|
||||||
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pushTextAboveContainer = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
) => {
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
const textElementIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === textElement.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(textElementIndex, 1);
|
||||||
|
|
||||||
|
const containerIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === container.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(containerIndex + 1, 0, textElement);
|
||||||
|
return updatedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushContainerBelowText = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
) => {
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
const containerIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === container.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(containerIndex, 1);
|
||||||
|
|
||||||
|
const textElementIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === textElement.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(textElementIndex, 0, container);
|
||||||
|
return updatedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionWrapTextInContainer = register({
|
||||||
|
name: "wrapTextInContainer",
|
||||||
|
contextItemLabel: "labels.createContainerFromText",
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
||||||
|
return selectedElements.length > 0 && areTextElements;
|
||||||
|
},
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||||
|
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
||||||
|
|
||||||
|
for (const textElement of selectedElements) {
|
||||||
|
if (isTextElement(textElement)) {
|
||||||
|
const container = newElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: appState.currentItemBackgroundColor,
|
||||||
|
boundElements: [
|
||||||
|
...(textElement.boundElements || []),
|
||||||
|
{ id: textElement.id, type: "text" },
|
||||||
|
],
|
||||||
|
angle: textElement.angle,
|
||||||
|
fillStyle: appState.currentItemFillStyle,
|
||||||
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
|
roughness: appState.currentItemRoughness,
|
||||||
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
|
roundness:
|
||||||
|
appState.currentItemRoundness === "round"
|
||||||
|
? {
|
||||||
|
type: isUsingAdaptiveRadius("rectangle")
|
||||||
|
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||||
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
opacity: 100,
|
||||||
|
locked: false,
|
||||||
|
x: textElement.x - BOUND_TEXT_PADDING,
|
||||||
|
y: textElement.y - BOUND_TEXT_PADDING,
|
||||||
|
width: computeContainerDimensionForBoundText(
|
||||||
|
textElement.width,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
height: computeContainerDimensionForBoundText(
|
||||||
|
textElement.height,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
groupIds: textElement.groupIds,
|
||||||
|
frameId: textElement.frameId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update bindings
|
||||||
|
if (textElement.boundElements?.length) {
|
||||||
|
const linearElementIds = textElement.boundElements
|
||||||
|
.filter((ele) => ele.type === "arrow")
|
||||||
|
.map((el) => el.id);
|
||||||
|
const linearElements = updatedElements.filter((ele) =>
|
||||||
|
linearElementIds.includes(ele.id),
|
||||||
|
) as ExcalidrawLinearElement[];
|
||||||
|
linearElements.forEach((ele) => {
|
||||||
|
let startBinding = ele.startBinding;
|
||||||
|
let endBinding = ele.endBinding;
|
||||||
|
|
||||||
|
if (startBinding?.elementId === textElement.id) {
|
||||||
|
startBinding = {
|
||||||
|
...startBinding,
|
||||||
|
elementId: container.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endBinding?.elementId === textElement.id) {
|
||||||
|
endBinding = { ...endBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startBinding || endBinding) {
|
||||||
|
mutateElement(ele, { startBinding, endBinding }, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(
|
||||||
|
textElement,
|
||||||
|
{
|
||||||
|
containerId: container.id,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
boundElements: null,
|
||||||
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
redrawTextBoundingBox(textElement, container);
|
||||||
|
|
||||||
|
updatedElements = pushContainerBelowText(
|
||||||
|
[...updatedElements, container],
|
||||||
|
container,
|
||||||
|
textElement,
|
||||||
|
);
|
||||||
|
containerIds[container.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: updatedElements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: containerIds,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||||
import { ZoomInIcon, ZoomOutIcon } from "../components/icons";
|
import { ZoomInIcon, ZoomOutIcon } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { CURSOR_TYPE, MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
import { CURSOR_TYPE, MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
||||||
@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
@ -19,6 +19,8 @@ import {
|
|||||||
isEraserActive,
|
isEraserActive,
|
||||||
isHandToolActive,
|
isHandToolActive,
|
||||||
} from "../appState";
|
} from "../appState";
|
||||||
|
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
|
import { Bounds } from "../element/bounds";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@ -35,24 +37,21 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
commitToHistory: !!value.viewBackgroundColor,
|
commitToHistory: !!value.viewBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData, appProps }) => {
|
||||||
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
|
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<ColorPicker
|
||||||
<ColorPicker
|
palette={null}
|
||||||
label={t("labels.canvasBackground")}
|
topPicks={DEFAULT_CANVAS_BACKGROUND_PICKS}
|
||||||
type="canvasBackground"
|
label={t("labels.canvasBackground")}
|
||||||
color={appState.viewBackgroundColor}
|
type="canvasBackground"
|
||||||
onChange={(color) => updateData({ viewBackgroundColor: color })}
|
color={appState.viewBackgroundColor}
|
||||||
isActive={appState.openPopup === "canvasColorPicker"}
|
onChange={(color) => updateData({ viewBackgroundColor: color })}
|
||||||
setActive={(active) =>
|
data-testid="canvas-background-picker"
|
||||||
updateData({ openPopup: active ? "canvasColorPicker" : null })
|
elements={elements}
|
||||||
}
|
appState={appState}
|
||||||
data-testid="canvas-background-picker"
|
updateData={updateData}
|
||||||
elements={elements}
|
/>
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -208,7 +207,7 @@ export const actionResetZoom = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const zoomValueToFitBoundsOnViewport = (
|
const zoomValueToFitBoundsOnViewport = (
|
||||||
bounds: [number, number, number, number],
|
bounds: Bounds,
|
||||||
viewportDimensions: { width: number; height: number },
|
viewportDimensions: { width: number; height: number },
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = bounds;
|
const [x1, y1, x2, y2] = bounds;
|
||||||
@ -226,50 +225,93 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoomToFitElements = (
|
export const zoomToFit = ({
|
||||||
elements: readonly ExcalidrawElement[],
|
targetElements,
|
||||||
appState: Readonly<AppState>,
|
appState,
|
||||||
zoomToSelection: boolean,
|
fitToViewport = false,
|
||||||
) => {
|
viewportZoomFactor = 0.7,
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
}: {
|
||||||
const selectedElements = getSelectedElements(nonDeletedElements, appState);
|
targetElements: readonly ExcalidrawElement[];
|
||||||
|
appState: Readonly<AppState>;
|
||||||
const commonBounds =
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
zoomToSelection && selectedElements.length > 0
|
fitToViewport: boolean;
|
||||||
? getCommonBounds(selectedElements)
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
: getCommonBounds(nonDeletedElements);
|
viewportZoomFactor?: number;
|
||||||
|
}) => {
|
||||||
const newZoom = {
|
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
||||||
value: zoomValueToFitBoundsOnViewport(commonBounds, {
|
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = commonBounds;
|
const [x1, y1, x2, y2] = commonBounds;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
|
||||||
|
let newZoomValue;
|
||||||
|
let scrollX;
|
||||||
|
let scrollY;
|
||||||
|
|
||||||
|
if (fitToViewport) {
|
||||||
|
const commonBoundsWidth = x2 - x1;
|
||||||
|
const commonBoundsHeight = y2 - y1;
|
||||||
|
|
||||||
|
newZoomValue =
|
||||||
|
Math.min(
|
||||||
|
appState.width / commonBoundsWidth,
|
||||||
|
appState.height / commonBoundsHeight,
|
||||||
|
) * Math.min(1, Math.max(viewportZoomFactor, 0.1));
|
||||||
|
|
||||||
|
// Apply clamping to newZoomValue to be between 10% and 3000%
|
||||||
|
newZoomValue = Math.min(
|
||||||
|
Math.max(newZoomValue, 0.1),
|
||||||
|
30.0,
|
||||||
|
) as NormalizedZoomValue;
|
||||||
|
|
||||||
|
scrollX = (appState.width / 2) * (1 / newZoomValue) - centerX;
|
||||||
|
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
||||||
|
} else {
|
||||||
|
newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerScroll = centerScrollOn({
|
||||||
|
scenePoint: { x: centerX, y: centerY },
|
||||||
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
zoom: { value: newZoomValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollX = centerScroll.scrollX;
|
||||||
|
scrollY = centerScroll.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...centerScrollOn({
|
scrollX,
|
||||||
scenePoint: { x: centerX, y: centerY },
|
scrollY,
|
||||||
viewportDimensions: {
|
zoom: { value: newZoomValue },
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
},
|
|
||||||
zoom: newZoom,
|
|
||||||
}),
|
|
||||||
zoom: newZoom,
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionZoomToSelected = register({
|
// Note, this action differs from actionZoomToFitSelection in that it doesn't
|
||||||
name: "zoomToSelection",
|
// zoom beyond 100%. In other words, if the content is smaller than viewport
|
||||||
|
// size, it won't be zoomed in.
|
||||||
|
export const actionZoomToFitSelectionInViewport = register({
|
||||||
|
name: "zoomToFitSelectionInViewport",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => zoomToFitElements(elements, appState, true),
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
return zoomToFit({
|
||||||
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
|
appState,
|
||||||
|
fitToViewport: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
||||||
|
// TBD on how proceed
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.TWO &&
|
event.code === CODES.TWO &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
@ -277,11 +319,31 @@ export const actionZoomToSelected = register({
|
|||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionZoomToFitSelection = register({
|
||||||
|
name: "zoomToFitSelection",
|
||||||
|
trackEvent: { category: "canvas" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
return zoomToFit({
|
||||||
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
|
appState,
|
||||||
|
fitToViewport: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// NOTE this action should use shift-2 per figma, alas
|
||||||
|
keyTest: (event) =>
|
||||||
|
event.code === CODES.THREE &&
|
||||||
|
event.shiftKey &&
|
||||||
|
!event.altKey &&
|
||||||
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
|
});
|
||||||
|
|
||||||
export const actionZoomToFit = register({
|
export const actionZoomToFit = register({
|
||||||
name: "zoomToFit",
|
name: "zoomToFit",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => zoomToFitElements(elements, appState, false),
|
perform: (elements, appState) =>
|
||||||
|
zoomToFit({ targetElements: elements, appState, fitToViewport: false }),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
@ -334,6 +396,7 @@ export const actionToggleEraserTool = register({
|
|||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
activeTool,
|
activeTool,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
@ -368,6 +431,7 @@ export const actionToggleHandTool = register({
|
|||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
activeTool,
|
activeTool,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { getSelectedElements } from "../scene/selection";
|
|
||||||
import { exportCanvas } from "../data/index";
|
import { exportCanvas } from "../data/index";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -16,9 +15,13 @@ export const actionCopy = register({
|
|||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
});
|
||||||
|
|
||||||
copyToClipboard(selectedElements, appState, app.files);
|
copyToClipboard(elementsToCopy, app.files);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
@ -72,11 +75,11 @@ export const actionCopyAsSvg = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
includeElementsInFrames: true,
|
||||||
);
|
});
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
@ -116,11 +119,11 @@ export const actionCopyAsPng = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
includeElementsInFrames: true,
|
||||||
);
|
});
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
@ -168,12 +171,11 @@ export const actionCopyAsPng = register({
|
|||||||
export const copyText = register({
|
export const copyText = register({
|
||||||
name: "copyText",
|
name: "copyText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const text = selectedElements
|
const text = selectedElements
|
||||||
.reduce((acc: string[], element) => {
|
.reduce((acc: string[], element) => {
|
||||||
@ -188,10 +190,15 @@ export const copyText = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
return (
|
return (
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
getSelectedElements(elements, appState, true).some(isTextElement)
|
app.scene
|
||||||
|
.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
})
|
||||||
|
.some(isTextElement)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyText",
|
contextItemLabel: "labels.copyText",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -18,11 +18,23 @@ const deleteSelectedElements = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
|
const framesToBeDeleted = new Set(
|
||||||
|
getSelectedElements(
|
||||||
|
elements.filter((el) => el.type === "frame"),
|
||||||
|
appState,
|
||||||
|
).map((el) => el.id),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: elements.map((el) => {
|
elements: elements.map((el) => {
|
||||||
if (appState.selectedElementIds[el.id]) {
|
if (appState.selectedElementIds[el.id]) {
|
||||||
return newElementWith(el, { isDeleted: true });
|
return newElementWith(el, { isDeleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
|
||||||
|
return newElementWith(el, { isDeleted: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isBoundToContainer(el) &&
|
isBoundToContainer(el) &&
|
||||||
appState.selectedElementIds[el.containerId]
|
appState.selectedElementIds[el.containerId]
|
||||||
@ -146,6 +158,7 @@ export const actionDeleteSelected = register({
|
|||||||
...nextAppState,
|
...nextAppState,
|
||||||
activeTool: updateActiveTool(appState, { type: "selection" }),
|
activeTool: updateActiveTool(appState, { type: "selection" }),
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
|
activeEmbeddable: null,
|
||||||
},
|
},
|
||||||
commitToHistory: isSomeElementSelected(
|
commitToHistory: isSomeElementSelected(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
@ -154,7 +167,9 @@ export const actionDeleteSelected = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.delete",
|
contextItemLabel: "labels.delete",
|
||||||
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
keyTest: (event, appState, elements) =>
|
||||||
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
|
||||||
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -6,44 +6,49 @@ import { ToolButton } from "../components/ToolButton";
|
|||||||
import { distributeElements, Distribution } from "../distribute";
|
import { distributeElements, Distribution } from "../distribute";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (
|
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
appState: AppState,
|
return (
|
||||||
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
|
selectedElements.length > 1 &&
|
||||||
|
// TODO enable distributing frames when implemented properly
|
||||||
|
!selectedElements.some((el) => el.type === "frame")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const distributeSelectedElements = (
|
const distributeSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
distribution: Distribution,
|
distribution: Distribution,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElements = distributeElements(selectedElements, distribution);
|
const updatedElements = distributeElements(selectedElements, distribution);
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
return elements.map(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
(element) => updatedElementsMap.get(element.id) || element,
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const distributeHorizontally = register({
|
export const distributeHorizontally = register({
|
||||||
name: "distributeHorizontally",
|
name: "distributeHorizontally",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, {
|
elements: distributeSelectedElements(elements, appState, app, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@ -52,9 +57,9 @@ export const distributeHorizontally = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeHorizontallyIcon}
|
icon={DistributeHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -70,10 +75,10 @@ export const distributeHorizontally = register({
|
|||||||
export const distributeVertically = register({
|
export const distributeVertically = register({
|
||||||
name: "distributeVertically",
|
name: "distributeVertically",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, {
|
elements: distributeSelectedElements(elements, appState, app, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@ -82,9 +87,9 @@ export const distributeVertically = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeVerticallyIcon}
|
icon={DistributeVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
@ -2,7 +2,7 @@ import { KEYS } from "../keys";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { duplicateElement, getNonDeletedElements } from "../element";
|
import { duplicateElement, getNonDeletedElements } from "../element";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
@ -16,9 +16,21 @@ import { AppState } from "../types";
|
|||||||
import { fixBindingsAfterDuplication } from "../element/binding";
|
import { fixBindingsAfterDuplication } from "../element/binding";
|
||||||
import { ActionResult } from "./types";
|
import { ActionResult } from "./types";
|
||||||
import { GRID_SIZE } from "../constants";
|
import { GRID_SIZE } from "../constants";
|
||||||
import { bindTextToShapeAfterDuplication } from "../element/textElement";
|
import {
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
bindTextToShapeAfterDuplication,
|
||||||
|
getBoundTextElement,
|
||||||
|
} from "../element/textElement";
|
||||||
|
import { isBoundToContainer, isFrameElement } from "../element/typeChecks";
|
||||||
|
import { normalizeElementOrder } from "../element/sortElements";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
import {
|
||||||
|
bindElementsToFramesAfterDuplication,
|
||||||
|
getFrameElements,
|
||||||
|
} from "../frame";
|
||||||
|
import {
|
||||||
|
excludeElementsInFramesFromSelection,
|
||||||
|
getSelectedElements,
|
||||||
|
} from "../scene/selection";
|
||||||
|
|
||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
name: "duplicateSelection",
|
name: "duplicateSelection",
|
||||||
@ -64,6 +76,11 @@ const duplicateElements = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): Partial<ActionResult> => {
|
): Partial<ActionResult> => {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// step (1)
|
||||||
|
|
||||||
|
const sortedElements = normalizeElementOrder(elements);
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const newElements: ExcalidrawElement[] = [];
|
const newElements: ExcalidrawElement[] = [];
|
||||||
const oldElements: ExcalidrawElement[] = [];
|
const oldElements: ExcalidrawElement[] = [];
|
||||||
@ -85,42 +102,160 @@ const duplicateElements = (
|
|||||||
return newElement;
|
return newElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalElements: ExcalidrawElement[] = [];
|
const idsOfElementsToDuplicate = arrayToMap(
|
||||||
|
getSelectedElements(sortedElements, appState, {
|
||||||
let index = 0;
|
includeBoundTextElement: true,
|
||||||
const selectedElementIds = arrayToMap(
|
includeElementsInFrames: true,
|
||||||
getSelectedElements(elements, appState, true),
|
}),
|
||||||
);
|
);
|
||||||
while (index < elements.length) {
|
|
||||||
const element = elements[index];
|
// Ids of elements that have already been processed so we don't push them
|
||||||
if (selectedElementIds.get(element.id)) {
|
// into the array twice if we end up backtracking when retrieving
|
||||||
if (element.groupIds.length) {
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
// cases such as a group containing deleted elements which were not selected).
|
||||||
|
//
|
||||||
|
// This is not enough to prevent duplicates, so we do a second loop afterwards
|
||||||
|
// to remove them.
|
||||||
|
//
|
||||||
|
// For convenience we mark even the newly created ones even though we don't
|
||||||
|
// loop over them.
|
||||||
|
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
|
|
||||||
|
const markAsProcessed = (elements: ExcalidrawElement[]) => {
|
||||||
|
for (const element of elements) {
|
||||||
|
processedIds.set(element.id, true);
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const elementsWithClones: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
|
||||||
|
while (++index < sortedElements.length) {
|
||||||
|
const element = sortedElements[index];
|
||||||
|
|
||||||
|
if (processedIds.get(element.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
const isElementAFrame = isFrameElement(element);
|
||||||
|
|
||||||
|
if (idsOfElementsToDuplicate.get(element.id)) {
|
||||||
|
// if a group or a container/bound-text or frame, duplicate atomically
|
||||||
|
if (element.groupIds.length || boundTextElement || isElementAFrame) {
|
||||||
const groupId = getSelectedGroupForElement(appState, element);
|
const groupId = getSelectedGroupForElement(appState, element);
|
||||||
// if group selected, duplicate it atomically
|
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
const groupElements = getElementsInGroup(elements, groupId);
|
// TODO:
|
||||||
finalElements.push(
|
// remove `.flatMap...`
|
||||||
...groupElements,
|
// if the elements in a frame are grouped when the frame is grouped
|
||||||
...groupElements.map((element) =>
|
const groupElements = getElementsInGroup(
|
||||||
duplicateAndOffsetElement(element),
|
sortedElements,
|
||||||
),
|
groupId,
|
||||||
|
).flatMap((element) =>
|
||||||
|
isFrameElement(element)
|
||||||
|
? [...getFrameElements(elements, element.id), element]
|
||||||
|
: [element],
|
||||||
);
|
);
|
||||||
index = index + groupElements.length;
|
|
||||||
|
elementsWithClones.push(
|
||||||
|
...markAsProcessed([
|
||||||
|
...groupElements,
|
||||||
|
...groupElements.map((element) =>
|
||||||
|
duplicateAndOffsetElement(element),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (boundTextElement) {
|
||||||
|
elementsWithClones.push(
|
||||||
|
...markAsProcessed([
|
||||||
|
element,
|
||||||
|
boundTextElement,
|
||||||
|
duplicateAndOffsetElement(element),
|
||||||
|
duplicateAndOffsetElement(boundTextElement),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isElementAFrame) {
|
||||||
|
const elementsInFrame = getFrameElements(sortedElements, element.id);
|
||||||
|
|
||||||
|
elementsWithClones.push(
|
||||||
|
...markAsProcessed([
|
||||||
|
...elementsInFrame,
|
||||||
|
element,
|
||||||
|
...elementsInFrame.map((e) => duplicateAndOffsetElement(e)),
|
||||||
|
duplicateAndOffsetElement(element),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalElements.push(element, duplicateAndOffsetElement(element));
|
// since elements in frames have a lower z-index than the frame itself,
|
||||||
|
// they will be looped first and if their frames are selected as well,
|
||||||
|
// they will have been copied along with the frame atomically in the
|
||||||
|
// above branch, so we must skip those elements here
|
||||||
|
//
|
||||||
|
// now, for elements do not belong any frames or elements whose frames
|
||||||
|
// are selected (or elements that are left out from the above
|
||||||
|
// steps for whatever reason) we (should at least) duplicate them here
|
||||||
|
if (!element.frameId || !idsOfElementsToDuplicate.has(element.frameId)) {
|
||||||
|
elementsWithClones.push(
|
||||||
|
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
finalElements.push(element);
|
elementsWithClones.push(...markAsProcessed([element]));
|
||||||
}
|
}
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// step (2)
|
||||||
|
|
||||||
|
// second pass to remove duplicates. We loop from the end as it's likelier
|
||||||
|
// that the last elements are in the correct order (contiguous or otherwise).
|
||||||
|
// Thus we need to reverse as the last step (3).
|
||||||
|
|
||||||
|
const finalElementsReversed: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
const finalElementIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
|
index = elementsWithClones.length;
|
||||||
|
|
||||||
|
while (--index >= 0) {
|
||||||
|
const element = elementsWithClones[index];
|
||||||
|
if (!finalElementIds.get(element.id)) {
|
||||||
|
finalElementIds.set(element.id, true);
|
||||||
|
finalElementsReversed.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step (3)
|
||||||
|
|
||||||
|
const finalElements = finalElementsReversed.reverse();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
bindTextToShapeAfterDuplication(
|
bindTextToShapeAfterDuplication(
|
||||||
|
elementsWithClones,
|
||||||
|
oldElements,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
fixBindingsAfterDuplication(
|
||||||
|
elementsWithClones,
|
||||||
|
oldElements,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
bindElementsToFramesAfterDuplication(
|
||||||
finalElements,
|
finalElements,
|
||||||
oldElements,
|
oldElements,
|
||||||
oldIdToDuplicatedId,
|
oldIdToDuplicatedId,
|
||||||
);
|
);
|
||||||
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
|
|
||||||
|
const nextElementsToSelect =
|
||||||
|
excludeElementsInFramesFromSelection(newElements);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: finalElements,
|
elements: finalElements,
|
||||||
@ -128,7 +263,7 @@ const duplicateElements = (
|
|||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedElementIds: newElements.reduce(
|
selectedElementIds: nextElementsToSelect.reduce(
|
||||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
if (!isBoundToContainer(element)) {
|
if (!isBoundToContainer(element)) {
|
||||||
acc[element.id] = true;
|
acc[element.id] = true;
|
||||||
@ -139,6 +274,8 @@ const duplicateElements = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
68
src/actions/actionElementLock.test.tsx
Normal file
68
src/actions/actionElementLock.test.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
|
import { queryByTestId, fireEvent } from "@testing-library/react";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { Pointer, UI } from "../tests/helpers/ui";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
describe("element locking", () => {
|
||||||
|
it("should not show unlockAllElements action in contextMenu if no elements locked", async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
|
||||||
|
mouse.rightClickAt(0, 0);
|
||||||
|
|
||||||
|
const item = queryByTestId(UI.queryContextMenu()!, "unlockAllElements");
|
||||||
|
expect(item).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unlock all elements and select them when using unlockAllElements action in contextMenu", async () => {
|
||||||
|
await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{
|
||||||
|
elements: [
|
||||||
|
API.createElement({
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
locked: true,
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
locked: true,
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
locked: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
mouse.rightClickAt(0, 0);
|
||||||
|
|
||||||
|
expect(Object.keys(h.state.selectedElementIds).length).toBe(0);
|
||||||
|
expect(h.elements.map((el) => el.locked)).toEqual([true, true, false]);
|
||||||
|
|
||||||
|
const item = queryByTestId(UI.queryContextMenu()!, "unlockAllElements");
|
||||||
|
expect(item).not.toBe(null);
|
||||||
|
|
||||||
|
fireEvent.click(item!.querySelector("button")!);
|
||||||
|
|
||||||
|
expect(h.elements.map((el) => el.locked)).toEqual([false, false, false]);
|
||||||
|
// should select the unlocked elements
|
||||||
|
expect(h.state.selectedElementIds).toEqual({
|
||||||
|
[h.elements[0].id]: true,
|
||||||
|
[h.elements[1].id]: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
104
src/actions/actionElementLock.ts
Normal file
104
src/actions/actionElementLock.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { arrayToMap } from "../utils";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
||||||
|
elements.every((el) => !el.locked);
|
||||||
|
|
||||||
|
export const actionToggleElementLock = register({
|
||||||
|
name: "toggleElementLock",
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
return !selectedElements.some(
|
||||||
|
(element) => element.locked && element.frameId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selectedElements.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextLockState = shouldLock(selectedElements);
|
||||||
|
const selectedElementsMap = arrayToMap(selectedElements);
|
||||||
|
return {
|
||||||
|
elements: elements.map((element) => {
|
||||||
|
if (!selectedElementsMap.has(element.id)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(element, { locked: nextLockState });
|
||||||
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedLinearElement: nextLockState
|
||||||
|
? null
|
||||||
|
: appState.selectedLinearElement,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: (elements, appState, app) => {
|
||||||
|
const selected = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: false,
|
||||||
|
});
|
||||||
|
if (selected.length === 1 && selected[0].type !== "frame") {
|
||||||
|
return selected[0].locked
|
||||||
|
? "labels.elementLock.unlock"
|
||||||
|
: "labels.elementLock.lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldLock(selected)
|
||||||
|
? "labels.elementLock.lockAll"
|
||||||
|
: "labels.elementLock.unlockAll";
|
||||||
|
},
|
||||||
|
keyTest: (event, appState, elements, app) => {
|
||||||
|
return (
|
||||||
|
event.key.toLocaleLowerCase() === KEYS.L &&
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.shiftKey &&
|
||||||
|
app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: false,
|
||||||
|
}).length > 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionUnlockAllElements = register({
|
||||||
|
name: "unlockAllElements",
|
||||||
|
trackEvent: { category: "canvas" },
|
||||||
|
viewMode: false,
|
||||||
|
predicate: (elements) => {
|
||||||
|
return elements.some((element) => element.locked);
|
||||||
|
},
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const lockedElements = elements.filter((el) => el.locked);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: elements.map((element) => {
|
||||||
|
if (element.locked) {
|
||||||
|
return newElementWith(element, { locked: false });
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: Object.fromEntries(
|
||||||
|
lockedElements.map((el) => [el.id, true]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: "labels.elementLock.unlockAll",
|
||||||
|
});
|
@ -26,7 +26,7 @@ export const actionChangeProjectName = register({
|
|||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value) => {
|
||||||
return { appState: { ...appState, name: value }, commitToHistory: false };
|
return { appState: { ...appState, name: value }, commitToHistory: false };
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, appProps }) => (
|
PanelComponent: ({ appState, updateData, appProps, data }) => (
|
||||||
<ProjectName
|
<ProjectName
|
||||||
label={t("labels.fileTitle")}
|
label={t("labels.fileTitle")}
|
||||||
value={appState.name || "Unnamed"}
|
value={appState.name || "Unnamed"}
|
||||||
@ -34,6 +34,7 @@ export const actionChangeProjectName = register({
|
|||||||
isNameEditable={
|
isNameEditable={
|
||||||
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
||||||
}
|
}
|
||||||
|
ignoreFocus={data?.ignoreFocus ?? false}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -64,7 +65,7 @@ export const actionChangeExportScale = register({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
const scaleButtonTitle = `${t(
|
||||||
"buttons.scale",
|
"imageExportDialog.label.scale",
|
||||||
)} ${s}x (${width}x${height})`;
|
)} ${s}x (${width}x${height})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -101,7 +102,7 @@ export const actionChangeExportBackground = register({
|
|||||||
checked={appState.exportBackground}
|
checked={appState.exportBackground}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.withBackground")}
|
{t("imageExportDialog.label.withBackground")}
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -120,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
checked={appState.exportEmbedScene}
|
checked={appState.exportEmbedScene}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.exportEmbedScene")}
|
{t("imageExportDialog.label.embedScene")}
|
||||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
@ -276,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
|||||||
onChange={(theme: Theme) => {
|
onChange={(theme: Theme) => {
|
||||||
updateData(theme === THEME.DARK);
|
updateData(theme === THEME.DARK);
|
||||||
}}
|
}}
|
||||||
title={t("labels.toggleExportColorScheme")}
|
title={t("imageExportDialog.label.darkMode")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
@ -125,13 +125,6 @@ export const actionFinalize = register({
|
|||||||
{ x, y },
|
{ x, y },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!appState.activeTool.locked &&
|
|
||||||
appState.activeTool.type !== "freedraw"
|
|
||||||
) {
|
|
||||||
appState.selectedElementIds[multiPointElement.id] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -167,6 +160,7 @@ export const actionFinalize = register({
|
|||||||
multiPointElement
|
multiPointElement
|
||||||
? appState.activeTool
|
? appState.activeTool
|
||||||
: activeTool,
|
: activeTool,
|
||||||
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
|
@ -1,74 +1,54 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||||
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
import { resizeMultipleElements } from "../element/resizeElements";
|
||||||
import { AppState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
|
||||||
import { updateBoundElements } from "../element/binding";
|
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import { CODES, KEYS } from "../keys";
|
||||||
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
bindOrUnbindSelectedElements,
|
||||||
getElementPointsCoords,
|
isBindingEnabled,
|
||||||
} from "../element/bounds";
|
unbindLinearElements,
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
} from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
|
||||||
|
|
||||||
const enableActionFlipHorizontal = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const eligibleElements = getSelectedElements(
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableActionFlipVertical = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const eligibleElements = getSelectedElements(
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return eligibleElements.length === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: flipSelectedElements(elements, appState, "horizontal"),
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
|
flipSelectedElements(elements, appState, "horizontal"),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.shiftKey && event.code === "KeyH",
|
keyTest: (event) => event.shiftKey && event.code === CODES.H,
|
||||||
contextItemLabel: "labels.flipHorizontal",
|
contextItemLabel: "labels.flipHorizontal",
|
||||||
predicate: (elements, appState) =>
|
|
||||||
enableActionFlipHorizontal(elements, appState),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionFlipVertical = register({
|
export const actionFlipVertical = register({
|
||||||
name: "flipVertical",
|
name: "flipVertical",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: flipSelectedElements(elements, appState, "vertical"),
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
|
flipSelectedElements(elements, appState, "vertical"),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
|
event.shiftKey && event.code === CODES.V && !event[KEYS.CTRL_OR_CMD],
|
||||||
contextItemLabel: "labels.flipVertical",
|
contextItemLabel: "labels.flipVertical",
|
||||||
predicate: (elements, appState) =>
|
|
||||||
enableActionFlipVertical(elements, appState),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flipSelectedElements = (
|
const flipSelectedElements = (
|
||||||
@ -79,13 +59,11 @@ const flipSelectedElements = (
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
|
{
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// remove once we allow for groups of elements to be flipped
|
|
||||||
if (selectedElements.length > 1) {
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedElements = flipElements(
|
const updatedElements = flipElements(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
appState,
|
appState,
|
||||||
@ -104,144 +82,20 @@ const flipElements = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
elements.forEach((element) => {
|
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
||||||
flipElement(element, appState);
|
|
||||||
// If vertical flip, rotate an extra 180
|
resizeMultipleElements(
|
||||||
if (flipDirection === "vertical") {
|
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
||||||
rotateElement(element, Math.PI);
|
elements,
|
||||||
}
|
"nw",
|
||||||
});
|
true,
|
||||||
|
flipDirection === "horizontal" ? maxX : minX,
|
||||||
|
flipDirection === "horizontal" ? minY : maxY,
|
||||||
|
);
|
||||||
|
|
||||||
|
(isBindingEnabled(appState)
|
||||||
|
? bindOrUnbindSelectedElements
|
||||||
|
: unbindLinearElements)(elements);
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
};
|
};
|
||||||
|
|
||||||
const flipElement = (
|
|
||||||
element: NonDeleted<ExcalidrawElement>,
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const originalX = element.x;
|
|
||||||
const originalY = element.y;
|
|
||||||
const width = element.width;
|
|
||||||
const height = element.height;
|
|
||||||
const originalAngle = normalizeAngle(element.angle);
|
|
||||||
|
|
||||||
// Rotate back to zero, if necessary
|
|
||||||
mutateElement(element, {
|
|
||||||
angle: normalizeAngle(0),
|
|
||||||
});
|
|
||||||
// Flip unrotated by pulling TransformHandle to opposite side
|
|
||||||
const transformHandles = getTransformHandles(element, appState.zoom);
|
|
||||||
let usingNWHandle = true;
|
|
||||||
let nHandle = transformHandles.nw;
|
|
||||||
if (!nHandle) {
|
|
||||||
// Use ne handle instead
|
|
||||||
usingNWHandle = false;
|
|
||||||
nHandle = transformHandles.ne;
|
|
||||||
if (!nHandle) {
|
|
||||||
mutateElement(element, {
|
|
||||||
angle: originalAngle,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalOffsetX = 0;
|
|
||||||
if (isLinearElement(element) && element.points.length < 3) {
|
|
||||||
finalOffsetX =
|
|
||||||
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
|
||||||
element.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialPointsCoords;
|
|
||||||
if (isLinearElement(element)) {
|
|
||||||
initialPointsCoords = getElementPointsCoords(element, element.points);
|
|
||||||
}
|
|
||||||
const initialElementAbsoluteCoords = getElementAbsoluteCoords(element);
|
|
||||||
|
|
||||||
if (isLinearElement(element) && element.points.length < 3) {
|
|
||||||
for (let index = 1; index < element.points.length; index++) {
|
|
||||||
LinearElementEditor.movePoints(element, [
|
|
||||||
{
|
|
||||||
index,
|
|
||||||
point: [-element.points[index][0], element.points[index][1]],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
LinearElementEditor.normalizePoints(element);
|
|
||||||
} else {
|
|
||||||
const elWidth = initialPointsCoords
|
|
||||||
? initialPointsCoords[2] - initialPointsCoords[0]
|
|
||||||
: initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0];
|
|
||||||
|
|
||||||
const startPoint = initialPointsCoords
|
|
||||||
? [initialPointsCoords[0], initialPointsCoords[1]]
|
|
||||||
: [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]];
|
|
||||||
|
|
||||||
resizeSingleElement(
|
|
||||||
new Map().set(element.id, element),
|
|
||||||
false,
|
|
||||||
element,
|
|
||||||
usingNWHandle ? "nw" : "ne",
|
|
||||||
true,
|
|
||||||
usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
|
|
||||||
startPoint[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotate by (360 degrees - original angle)
|
|
||||||
let angle = normalizeAngle(2 * Math.PI - originalAngle);
|
|
||||||
if (angle < 0) {
|
|
||||||
// check, probably unnecessary
|
|
||||||
angle = normalizeAngle(angle + 2 * Math.PI);
|
|
||||||
}
|
|
||||||
mutateElement(element, {
|
|
||||||
angle,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move back to original spot to appear "flipped in place"
|
|
||||||
mutateElement(element, {
|
|
||||||
x: originalX + finalOffsetX,
|
|
||||||
y: originalY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
|
|
||||||
updateBoundElements(element);
|
|
||||||
|
|
||||||
if (initialPointsCoords && isLinearElement(element)) {
|
|
||||||
// Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin.
|
|
||||||
// There's still room for improvement since when the line roughness is > 1
|
|
||||||
// we still have a small offset of the origin when fliipping the element.
|
|
||||||
const finalPointsCoords = getElementPointsCoords(element, element.points);
|
|
||||||
|
|
||||||
const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
|
|
||||||
const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];
|
|
||||||
|
|
||||||
const coordsDiff = topLeftCoordsDiff + topRightCoordDiff;
|
|
||||||
|
|
||||||
mutateElement(element, {
|
|
||||||
x: element.x + coordsDiff * 0.5,
|
|
||||||
y: element.y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
|
|
||||||
const originalX = element.x;
|
|
||||||
const originalY = element.y;
|
|
||||||
let angle = normalizeAngle(element.angle + rotationAngle);
|
|
||||||
if (angle < 0) {
|
|
||||||
// check, probably unnecessary
|
|
||||||
angle = normalizeAngle(2 * Math.PI + angle);
|
|
||||||
}
|
|
||||||
mutateElement(element, {
|
|
||||||
angle,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move back to original spot
|
|
||||||
mutateElement(element, {
|
|
||||||
x: originalX,
|
|
||||||
y: originalY,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
132
src/actions/actionFrame.ts
Normal file
132
src/actions/actionFrame.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { removeAllElementsFromFrame } from "../frame";
|
||||||
|
import { getFrameElements } from "../frame";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { AppClassProperties, AppState } from "../types";
|
||||||
|
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
|
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionSelectAllElementsInFrame = register({
|
||||||
|
name: "selectAllElementsInFrame",
|
||||||
|
trackEvent: { category: "canvas" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
|
|
||||||
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
|
const elementsInFrame = getFrameElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
selectedFrame.id,
|
||||||
|
).filter((element) => !(element.type === "text" && element.containerId));
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: elementsInFrame.reduce((acc, element) => {
|
||||||
|
acc[element.id] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: "labels.selectAllElementsInFrame",
|
||||||
|
predicate: (elements, appState, _, app) =>
|
||||||
|
isSingleFrameSelected(appState, app),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionRemoveAllElementsFromFrame = register({
|
||||||
|
name: "removeAllElementsFromFrame",
|
||||||
|
trackEvent: { category: "history" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
|
|
||||||
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
|
return {
|
||||||
|
elements: removeAllElementsFromFrame(elements, selectedFrame, appState),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: {
|
||||||
|
[selectedFrame.id]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: "labels.removeAllElementsFromFrame",
|
||||||
|
predicate: (elements, appState, _, app) =>
|
||||||
|
isSingleFrameSelected(appState, app),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionupdateFrameRendering = register({
|
||||||
|
name: "updateFrameRendering",
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: { category: "canvas" },
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
frameRendering: {
|
||||||
|
...appState.frameRendering,
|
||||||
|
enabled: !appState.frameRendering.enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: "labels.updateFrameRendering",
|
||||||
|
checked: (appState: AppState) => appState.frameRendering.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionSetFrameAsActiveTool = register({
|
||||||
|
name: "setFrameAsActiveTool",
|
||||||
|
trackEvent: { category: "toolbar" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "frame",
|
||||||
|
});
|
||||||
|
|
||||||
|
setCursorForShape(app.canvas, {
|
||||||
|
...appState,
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
activeTool: updateActiveTool(appState, {
|
||||||
|
type: "frame",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
!event.shiftKey &&
|
||||||
|
!event.altKey &&
|
||||||
|
event.key.toLocaleLowerCase() === KEYS.F,
|
||||||
|
});
|
@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { UngroupIcon, GroupIcon } from "../components/icons";
|
import { UngroupIcon, GroupIcon } from "../components/icons";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import {
|
import {
|
||||||
getSelectedGroupIds,
|
getSelectedGroupIds,
|
||||||
selectGroup,
|
selectGroup,
|
||||||
@ -17,9 +17,19 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import {
|
||||||
import { AppState } from "../types";
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import {
|
||||||
|
getElementsInResizingFrame,
|
||||||
|
groupByFrames,
|
||||||
|
removeElementsFromFrame,
|
||||||
|
replaceAllElementsInFrame,
|
||||||
|
} from "../frame";
|
||||||
|
|
||||||
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
||||||
if (elements.length >= 2) {
|
if (elements.length >= 2) {
|
||||||
@ -41,12 +51,12 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
const enableActionGroup = (
|
const enableActionGroup = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
});
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||||
);
|
);
|
||||||
@ -55,12 +65,11 @@ const enableActionGroup = (
|
|||||||
export const actionGroup = register({
|
export const actionGroup = register({
|
||||||
name: "group",
|
name: "group",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
});
|
||||||
);
|
|
||||||
if (selectedElements.length < 2) {
|
if (selectedElements.length < 2) {
|
||||||
// nothing to group
|
// nothing to group
|
||||||
return { appState, elements, commitToHistory: false };
|
return { appState, elements, commitToHistory: false };
|
||||||
@ -86,9 +95,31 @@ export const actionGroup = register({
|
|||||||
return { appState, elements, commitToHistory: false };
|
return { appState, elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nextElements = [...elements];
|
||||||
|
|
||||||
|
// this includes the case where we are grouping elements inside a frame
|
||||||
|
// and elements outside that frame
|
||||||
|
const groupingElementsFromDifferentFrames =
|
||||||
|
new Set(selectedElements.map((element) => element.frameId)).size > 1;
|
||||||
|
// when it happens, we want to remove elements that are in the frame
|
||||||
|
// and are going to be grouped from the frame (mouthful, I know)
|
||||||
|
if (groupingElementsFromDifferentFrames) {
|
||||||
|
const frameElementsMap = groupByFrames(selectedElements);
|
||||||
|
|
||||||
|
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
||||||
|
nextElements = removeElementsFromFrame(
|
||||||
|
nextElements,
|
||||||
|
elementsInFrame,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newGroupId = randomId();
|
const newGroupId = randomId();
|
||||||
const selectElementIds = arrayToMap(selectedElements);
|
const selectElementIds = arrayToMap(selectedElements);
|
||||||
const updatedElements = elements.map((element) => {
|
|
||||||
|
nextElements = nextElements.map((element) => {
|
||||||
if (!selectElementIds.get(element.id)) {
|
if (!selectElementIds.get(element.id)) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@ -102,17 +133,16 @@ export const actionGroup = register({
|
|||||||
});
|
});
|
||||||
// keep the z order within the group the same, but move them
|
// keep the z order within the group the same, but move them
|
||||||
// to the z order of the highest element in the layer stack
|
// to the z order of the highest element in the layer stack
|
||||||
const elementsInGroup = getElementsInGroup(updatedElements, newGroupId);
|
const elementsInGroup = getElementsInGroup(nextElements, newGroupId);
|
||||||
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
|
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
|
||||||
const lastGroupElementIndex =
|
const lastGroupElementIndex = nextElements.lastIndexOf(lastElementInGroup);
|
||||||
updatedElements.lastIndexOf(lastElementInGroup);
|
const elementsAfterGroup = nextElements.slice(lastGroupElementIndex + 1);
|
||||||
const elementsAfterGroup = updatedElements.slice(lastGroupElementIndex + 1);
|
const elementsBeforeGroup = nextElements
|
||||||
const elementsBeforeGroup = updatedElements
|
|
||||||
.slice(0, lastGroupElementIndex)
|
.slice(0, lastGroupElementIndex)
|
||||||
.filter(
|
.filter(
|
||||||
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
|
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
|
||||||
);
|
);
|
||||||
const updatedElementsInOrder = [
|
nextElements = [
|
||||||
...elementsBeforeGroup,
|
...elementsBeforeGroup,
|
||||||
...elementsInGroup,
|
...elementsInGroup,
|
||||||
...elementsAfterGroup,
|
...elementsAfterGroup,
|
||||||
@ -122,19 +152,20 @@ export const actionGroup = register({
|
|||||||
appState: selectGroup(
|
appState: selectGroup(
|
||||||
newGroupId,
|
newGroupId,
|
||||||
{ ...appState, selectedGroupIds: {} },
|
{ ...appState, selectedGroupIds: {} },
|
||||||
getNonDeletedElements(updatedElementsInOrder),
|
getNonDeletedElements(nextElements),
|
||||||
),
|
),
|
||||||
elements: updatedElementsInOrder,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.group",
|
contextItemLabel: "labels.group",
|
||||||
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
enableActionGroup(elements, appState, app),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<GroupIcon theme={appState.theme} />}
|
icon={<GroupIcon theme={appState.theme} />}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@ -148,14 +179,23 @@ export const actionGroup = register({
|
|||||||
export const actionUngroup = register({
|
export const actionUngroup = register({
|
||||||
name: "ungroup",
|
name: "ungroup",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const groupIds = getSelectedGroupIds(appState);
|
const groupIds = getSelectedGroupIds(appState);
|
||||||
if (groupIds.length === 0) {
|
if (groupIds.length === 0) {
|
||||||
return { appState, elements, commitToHistory: false };
|
return { appState, elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nextElements = [...elements];
|
||||||
|
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
const frames = selectedElements
|
||||||
|
.filter((element) => element.frameId)
|
||||||
|
.map((element) =>
|
||||||
|
app.scene.getElement(element.frameId!),
|
||||||
|
) as ExcalidrawFrameElement[];
|
||||||
|
|
||||||
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
||||||
const nextElements = elements.map((element) => {
|
nextElements = nextElements.map((element) => {
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
boundTextElementIds.push(element.id);
|
boundTextElementIds.push(element.id);
|
||||||
}
|
}
|
||||||
@ -174,15 +214,36 @@ export const actionUngroup = register({
|
|||||||
const updateAppState = selectGroupsForSelectedElements(
|
const updateAppState = selectGroupsForSelectedElements(
|
||||||
{ ...appState, selectedGroupIds: {} },
|
{ ...appState, selectedGroupIds: {} },
|
||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
frames.forEach((frame) => {
|
||||||
|
if (frame) {
|
||||||
|
nextElements = replaceAllElementsInFrame(
|
||||||
|
nextElements,
|
||||||
|
getElementsInResizingFrame(nextElements, frame, appState),
|
||||||
|
frame,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// remove binded text elements from selection
|
// remove binded text elements from selection
|
||||||
boundTextElementIds.forEach(
|
updateAppState.selectedElementIds = Object.entries(
|
||||||
(id) => (updateAppState.selectedElementIds[id] = false),
|
updateAppState.selectedElementIds,
|
||||||
|
).reduce(
|
||||||
|
(acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => {
|
||||||
|
if (selected && !boundTextElementIds.includes(id)) {
|
||||||
|
acc[id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: updateAppState,
|
appState: updateAppState,
|
||||||
|
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawLinearElement } from "../element/types";
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionToggleLinearEditor = register({
|
export const actionToggleLinearEditor = register({
|
||||||
@ -10,19 +8,18 @@ export const actionToggleLinearEditor = register({
|
|||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "element",
|
category: "element",
|
||||||
},
|
},
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform(elements, appState, _, app) {
|
perform(elements, appState, _, app) {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
})[0] as ExcalidrawLinearElement;
|
||||||
)[0] as ExcalidrawLinearElement;
|
|
||||||
|
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
@ -36,12 +33,11 @@ export const actionToggleLinearEditor = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState) => {
|
contextItemLabel: (elements, appState, app) => {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
true,
|
})[0] as ExcalidrawLinearElement;
|
||||||
)[0] as ExcalidrawLinearElement;
|
|
||||||
return appState.editingLinearElement?.elementId === selectedElement.id
|
return appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? "labels.lineEditor.exit"
|
? "labels.lineEditor.exit"
|
||||||
: "labels.lineEditor.edit";
|
: "labels.lineEditor.edit";
|
||||||
|
@ -67,7 +67,6 @@ export const actionFullScreen = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.key === KEYS.F && !event[KEYS.CTRL_OR_CMD],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getClientColors } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { Collaborator } from "../types";
|
import { Collaborator } from "../types";
|
||||||
@ -31,15 +31,14 @@ export const actionGoToCollaborator = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, data }) => {
|
PanelComponent: ({ updateData, data }) => {
|
||||||
const [clientId, collaborator] = data as [string, Collaborator];
|
const [clientId, collaborator] = data as [string, Collaborator];
|
||||||
|
|
||||||
const { background, stroke } = getClientColors(clientId, appState);
|
const background = getClientColor(clientId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
color={background}
|
color={background}
|
||||||
border={stroke}
|
|
||||||
onClick={() => updateData(collaborator.pointer)}
|
onClick={() => updateData(collaborator.pointer)}
|
||||||
name={collaborator.username || ""}
|
name={collaborator.username || ""}
|
||||||
src={collaborator.avatarUrl}
|
src={collaborator.avatarUrl}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
|
import {
|
||||||
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
|
DEFAULT_ELEMENT_STROKE_COLOR_PALETTE,
|
||||||
|
DEFAULT_ELEMENT_STROKE_PICKS,
|
||||||
|
} from "../colors";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||||
import { IconPicker } from "../components/IconPicker";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
// TODO barnabasmolnar/editor-redesign
|
// TODO barnabasmolnar/editor-redesign
|
||||||
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
|
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
|
||||||
@ -37,6 +44,7 @@ import {
|
|||||||
TextAlignLeftIcon,
|
TextAlignLeftIcon,
|
||||||
TextAlignCenterIcon,
|
TextAlignCenterIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
|
FillZigZagIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
@ -54,6 +62,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
|
|||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
@ -81,7 +90,7 @@ import {
|
|||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
@ -93,8 +102,11 @@ const changeProperty = (
|
|||||||
includeBoundText = false,
|
includeBoundText = false,
|
||||||
) => {
|
) => {
|
||||||
const selectedElementIds = arrayToMap(
|
const selectedElementIds = arrayToMap(
|
||||||
getSelectedElements(elements, appState, includeBoundText),
|
getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: includeBoundText,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return elements.map((element) => {
|
return elements.map((element) => {
|
||||||
if (
|
if (
|
||||||
selectedElementIds.get(element.id) ||
|
selectedElementIds.get(element.id) ||
|
||||||
@ -110,8 +122,8 @@ const getFormValue = function <T>(
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
defaultValue?: T,
|
defaultValue: T,
|
||||||
): T | null {
|
): T {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
return (
|
return (
|
||||||
@ -123,7 +135,7 @@ const getFormValue = function <T>(
|
|||||||
getAttribute,
|
getAttribute,
|
||||||
)
|
)
|
||||||
: defaultValue) ??
|
: defaultValue) ??
|
||||||
null
|
defaultValue
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -223,10 +235,12 @@ export const actionChangeStrokeColor = register({
|
|||||||
commitToHistory: !!value.currentItemStrokeColor,
|
commitToHistory: !!value.currentItemStrokeColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
||||||
<>
|
<>
|
||||||
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
||||||
|
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
||||||
type="elementStroke"
|
type="elementStroke"
|
||||||
label={t("labels.stroke")}
|
label={t("labels.stroke")}
|
||||||
color={getFormValue(
|
color={getFormValue(
|
||||||
@ -236,12 +250,9 @@ export const actionChangeStrokeColor = register({
|
|||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
isActive={appState.openPopup === "strokeColorPicker"}
|
|
||||||
setActive={(active) =>
|
|
||||||
updateData({ openPopup: active ? "strokeColorPicker" : null })
|
|
||||||
}
|
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
updateData={updateData}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@ -266,10 +277,12 @@ export const actionChangeBackgroundColor = register({
|
|||||||
commitToHistory: !!value.currentItemBackgroundColor,
|
commitToHistory: !!value.currentItemBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
||||||
<>
|
<>
|
||||||
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
||||||
|
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
||||||
type="elementBackground"
|
type="elementBackground"
|
||||||
label={t("labels.background")}
|
label={t("labels.background")}
|
||||||
color={getFormValue(
|
color={getFormValue(
|
||||||
@ -279,12 +292,9 @@ export const actionChangeBackgroundColor = register({
|
|||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
isActive={appState.openPopup === "backgroundColorPicker"}
|
|
||||||
setActive={(active) =>
|
|
||||||
updateData({ openPopup: active ? "backgroundColorPicker" : null })
|
|
||||||
}
|
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
updateData={updateData}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@ -293,7 +303,12 @@ export const actionChangeBackgroundColor = register({
|
|||||||
export const actionChangeFillStyle = register({
|
export const actionChangeFillStyle = register({
|
||||||
name: "changeFillStyle",
|
name: "changeFillStyle",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
|
trackEvent(
|
||||||
|
"element",
|
||||||
|
"changeFillStyle",
|
||||||
|
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
@ -304,40 +319,57 @@ export const actionChangeFillStyle = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
<fieldset>
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
<legend>{t("labels.fill")}</legend>
|
const allElementsZigZag =
|
||||||
<ButtonIconSelect
|
selectedElements.length > 0 &&
|
||||||
options={[
|
selectedElements.every((el) => el.fillStyle === "zigzag");
|
||||||
{
|
|
||||||
value: "hachure",
|
return (
|
||||||
text: t("labels.hachure"),
|
<fieldset>
|
||||||
icon: FillHachureIcon,
|
<legend>{t("labels.fill")}</legend>
|
||||||
},
|
<ButtonIconSelect
|
||||||
{
|
type="button"
|
||||||
value: "cross-hatch",
|
options={[
|
||||||
text: t("labels.crossHatch"),
|
{
|
||||||
icon: FillCrossHatchIcon,
|
value: "hachure",
|
||||||
},
|
text: `${
|
||||||
{
|
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
|
||||||
value: "solid",
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
text: t("labels.solid"),
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
icon: FillSolidIcon,
|
active: allElementsZigZag ? true : undefined,
|
||||||
},
|
},
|
||||||
]}
|
{
|
||||||
group="fill"
|
value: "cross-hatch",
|
||||||
value={getFormValue(
|
text: t("labels.crossHatch"),
|
||||||
elements,
|
icon: FillCrossHatchIcon,
|
||||||
appState,
|
},
|
||||||
(element) => element.fillStyle,
|
{
|
||||||
appState.currentItemFillStyle,
|
value: "solid",
|
||||||
)}
|
text: t("labels.solid"),
|
||||||
onChange={(value) => {
|
icon: FillSolidIcon,
|
||||||
updateData(value);
|
},
|
||||||
}}
|
]}
|
||||||
/>
|
value={getFormValue(
|
||||||
</fieldset>
|
elements,
|
||||||
),
|
appState,
|
||||||
|
(element) => element.fillStyle,
|
||||||
|
appState.currentItemFillStyle,
|
||||||
|
)}
|
||||||
|
onClick={(value, event) => {
|
||||||
|
const nextValue =
|
||||||
|
event.altKey &&
|
||||||
|
value === "hachure" &&
|
||||||
|
selectedElements.every((el) => el.fillStyle === "hachure")
|
||||||
|
? "zigzag"
|
||||||
|
: value;
|
||||||
|
|
||||||
|
updateData(nextValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeStrokeWidth = register({
|
export const actionChangeStrokeWidth = register({
|
||||||
@ -637,6 +669,7 @@ export const actionChangeFontFamily = register({
|
|||||||
oldElement,
|
oldElement,
|
||||||
{
|
{
|
||||||
fontFamily: value,
|
fontFamily: value,
|
||||||
|
lineHeight: getDefaultLineHeight(value),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||||
@ -745,16 +778,19 @@ export const actionChangeTextAlign = register({
|
|||||||
value: "left",
|
value: "left",
|
||||||
text: t("labels.left"),
|
text: t("labels.left"),
|
||||||
icon: TextAlignLeftIcon,
|
icon: TextAlignLeftIcon,
|
||||||
|
testId: "align-left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "center",
|
value: "center",
|
||||||
text: t("labels.center"),
|
text: t("labels.center"),
|
||||||
icon: TextAlignCenterIcon,
|
icon: TextAlignCenterIcon,
|
||||||
|
testId: "align-horizontal-center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "right",
|
value: "right",
|
||||||
text: t("labels.right"),
|
text: t("labels.right"),
|
||||||
icon: TextAlignRightIcon,
|
icon: TextAlignRightIcon,
|
||||||
|
testId: "align-right",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -778,6 +814,7 @@ export const actionChangeTextAlign = register({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeVerticalAlign = register({
|
export const actionChangeVerticalAlign = register({
|
||||||
name: "changeVerticalAlign",
|
name: "changeVerticalAlign",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
@ -832,16 +869,21 @@ export const actionChangeVerticalAlign = register({
|
|||||||
testId: "align-bottom",
|
testId: "align-bottom",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(elements, appState, (element) => {
|
value={getFormValue(
|
||||||
if (isTextElement(element) && element.containerId) {
|
elements,
|
||||||
return element.verticalAlign;
|
appState,
|
||||||
}
|
(element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
if (isTextElement(element) && element.containerId) {
|
||||||
if (boundTextElement) {
|
return element.verticalAlign;
|
||||||
return boundTextElement.verticalAlign;
|
}
|
||||||
}
|
const boundTextElement = getBoundTextElement(element);
|
||||||
return null;
|
if (boundTextElement) {
|
||||||
})}
|
return boundTextElement.verticalAlign;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
VERTICAL_ALIGN.MIDDLE,
|
||||||
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -5,6 +5,7 @@ import { getNonDeletedElements, isTextElement } from "../element";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
||||||
|
|
||||||
export const actionSelectAll = register({
|
export const actionSelectAll = register({
|
||||||
name: "selectAll",
|
name: "selectAll",
|
||||||
@ -13,19 +14,18 @@ export const actionSelectAll = register({
|
|||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const selectedElementIds = elements.reduce(
|
|
||||||
(map: Record<ExcalidrawElement["id"], true>, element) => {
|
const selectedElementIds = excludeElementsInFramesFromSelection(
|
||||||
if (
|
elements.filter(
|
||||||
|
(element) =>
|
||||||
!element.isDeleted &&
|
!element.isDeleted &&
|
||||||
!(isTextElement(element) && element.containerId) &&
|
!(isTextElement(element) && element.containerId) &&
|
||||||
!element.locked
|
!element.locked,
|
||||||
) {
|
),
|
||||||
map[element.id] = true;
|
).reduce((map: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
}
|
map[element.id] = true;
|
||||||
return map;
|
return map;
|
||||||
},
|
}, {});
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: selectGroupsForSelectedElements(
|
appState: selectGroupsForSelectedElements(
|
||||||
@ -41,6 +41,8 @@ export const actionSelectAll = register({
|
|||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
},
|
},
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { t } from "../i18n";
|
|
||||||
import { CODES } from "../keys";
|
import { CODES } from "../keys";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { fireEvent, render, screen } from "../tests/test-utils";
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
togglePopover,
|
||||||
|
} from "../tests/test-utils";
|
||||||
import { copiedStyles } from "./actionStyles";
|
import { copiedStyles } from "./actionStyles";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -14,7 +19,14 @@ describe("actionStyles", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
});
|
});
|
||||||
it("should copy & paste styles via keyboard", () => {
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// https://github.com/floating-ui/floating-ui/issues/1908#issuecomment-1301553793
|
||||||
|
// affects node v16+
|
||||||
|
await act(async () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should copy & paste styles via keyboard", async () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
@ -24,10 +36,10 @@ describe("actionStyles", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
// Change some styles of second rectangle
|
// Change some styles of second rectangle
|
||||||
UI.clickLabeledElement("Stroke");
|
togglePopover("Stroke");
|
||||||
UI.clickLabeledElement(t("colors.c92a2a"));
|
UI.clickOnTestId("color-red");
|
||||||
UI.clickLabeledElement("Background");
|
togglePopover("Background");
|
||||||
UI.clickLabeledElement(t("colors.e64980"));
|
UI.clickOnTestId("color-blue");
|
||||||
// Fill style
|
// Fill style
|
||||||
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
||||||
// Stroke width
|
// Stroke width
|
||||||
@ -60,8 +72,8 @@ describe("actionStyles", () => {
|
|||||||
|
|
||||||
const firstRect = API.getSelectedElement();
|
const firstRect = API.getSelectedElement();
|
||||||
expect(firstRect.id).toBe(h.elements[0].id);
|
expect(firstRect.id).toBe(h.elements[0].id);
|
||||||
expect(firstRect.strokeColor).toBe("#c92a2a");
|
expect(firstRect.strokeColor).toBe("#e03131");
|
||||||
expect(firstRect.backgroundColor).toBe("#e64980");
|
expect(firstRect.backgroundColor).toBe("#a5d8ff");
|
||||||
expect(firstRect.fillStyle).toBe("cross-hatch");
|
expect(firstRect.fillStyle).toBe("cross-hatch");
|
||||||
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
|
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
|
||||||
expect(firstRect.strokeStyle).toBe("dotted");
|
expect(firstRect.strokeStyle).toBe("dotted");
|
||||||
|
@ -12,11 +12,15 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getBoundTextElement } from "../element/textElement";
|
import {
|
||||||
|
getBoundTextElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
getDefaultRoundnessTypeForElement,
|
getDefaultRoundnessTypeForElement,
|
||||||
|
isFrameElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
@ -61,7 +65,9 @@ export const actionPasteStyles = register({
|
|||||||
return { elements, commitToHistory: false };
|
return { elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const selectedElements = getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
});
|
||||||
const selectedElementIds = selectedElements.map((element) => element.id);
|
const selectedElementIds = selectedElements.map((element) => element.id);
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
@ -92,12 +98,18 @@ export const actionPasteStyles = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isTextElement(newElement)) {
|
if (isTextElement(newElement)) {
|
||||||
|
const fontSize =
|
||||||
|
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
|
||||||
|
const fontFamily =
|
||||||
|
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
|
fontSize,
|
||||||
fontFamily:
|
fontFamily,
|
||||||
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
textAlign:
|
textAlign:
|
||||||
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
|
lineHeight:
|
||||||
|
elementStylesToCopyFrom.lineHeight ||
|
||||||
|
getDefaultLineHeight(fontFamily),
|
||||||
});
|
});
|
||||||
let container = null;
|
let container = null;
|
||||||
if (newElement.containerId) {
|
if (newElement.containerId) {
|
||||||
@ -118,6 +130,13 @@ export const actionPasteStyles = register({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFrameElement(element)) {
|
||||||
|
newElement = newElementWith(newElement, {
|
||||||
|
roundness: null,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
import { newElementWith } from "../element/mutateElement";
|
|
||||||
import { ExcalidrawElement } from "../element/types";
|
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
import { register } from "./register";
|
|
||||||
|
|
||||||
export const actionToggleLock = register({
|
|
||||||
name: "toggleLock",
|
|
||||||
trackEvent: { category: "element" },
|
|
||||||
perform: (elements, appState) => {
|
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const operation = getOperation(selectedElements);
|
|
||||||
const selectedElementsMap = arrayToMap(selectedElements);
|
|
||||||
const lock = operation === "lock";
|
|
||||||
return {
|
|
||||||
elements: elements.map((element) => {
|
|
||||||
if (!selectedElementsMap.has(element.id)) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newElementWith(element, { locked: lock });
|
|
||||||
}),
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
selectedLinearElement: lock ? null : appState.selectedLinearElement,
|
|
||||||
},
|
|
||||||
commitToHistory: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
contextItemLabel: (elements, appState) => {
|
|
||||||
const selected = getSelectedElements(elements, appState, false);
|
|
||||||
if (selected.length === 1) {
|
|
||||||
return selected[0].locked
|
|
||||||
? "labels.elementLock.unlock"
|
|
||||||
: "labels.elementLock.lock";
|
|
||||||
}
|
|
||||||
|
|
||||||
return getOperation(selected) === "lock"
|
|
||||||
? "labels.elementLock.lockAll"
|
|
||||||
: "labels.elementLock.unlockAll";
|
|
||||||
},
|
|
||||||
keyTest: (event, appState, elements) => {
|
|
||||||
return (
|
|
||||||
event.key.toLocaleLowerCase() === KEYS.L &&
|
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
|
||||||
event.shiftKey &&
|
|
||||||
getSelectedElements(elements, appState, false).length > 0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOperation = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
): "lock" | "unlock" => (elements.some((el) => !el.locked) ? "lock" : "unlock");
|
|
@ -84,5 +84,5 @@ export { actionToggleZenMode } from "./actionToggleZenMode";
|
|||||||
export { actionToggleStats } from "./actionToggleStats";
|
export { actionToggleStats } from "./actionToggleStats";
|
||||||
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
||||||
export { actionLink } from "../element/Hyperlink";
|
export { actionLink } from "../element/Hyperlink";
|
||||||
export { actionToggleLock } from "./actionToggleLock";
|
export { actionToggleElementLock } from "./actionElementLock";
|
||||||
export { actionToggleLinearEditor } from "./actionLinearEditor";
|
export { actionToggleLinearEditor } from "./actionLinearEditor";
|
||||||
|
@ -90,6 +90,7 @@ export class ActionManager {
|
|||||||
event,
|
event,
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -118,10 +119,13 @@ export class ActionManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction(action: Action, source: ActionSource = "api") {
|
executeAction(
|
||||||
|
action: Action,
|
||||||
|
source: ActionSource = "api",
|
||||||
|
value: any = null,
|
||||||
|
) {
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
const value = null;
|
|
||||||
|
|
||||||
trackAction(action, source, appState, elements, this.app, value);
|
trackAction(action, source, appState, elements, this.app, value);
|
||||||
|
|
||||||
@ -165,6 +169,7 @@ export class ActionManager {
|
|||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
|
app={this.app}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { isDarwin } from "../constants";
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { SubtypeOf } from "../utility-types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ export type ShortcutName =
|
|||||||
ActionName,
|
ActionName,
|
||||||
| "toggleTheme"
|
| "toggleTheme"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
|
| "clearCanvas"
|
||||||
| "cut"
|
| "cut"
|
||||||
| "copy"
|
| "copy"
|
||||||
| "paste"
|
| "paste"
|
||||||
@ -32,7 +34,7 @@ export type ShortcutName =
|
|||||||
| "flipHorizontal"
|
| "flipHorizontal"
|
||||||
| "flipVertical"
|
| "flipVertical"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "toggleLock"
|
| "toggleElementLock"
|
||||||
>
|
>
|
||||||
| "saveScene"
|
| "saveScene"
|
||||||
| "imageExport";
|
| "imageExport";
|
||||||
@ -41,6 +43,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
loadScene: [getShortcutKey("CtrlOrCmd+O")],
|
loadScene: [getShortcutKey("CtrlOrCmd+O")],
|
||||||
|
clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")],
|
||||||
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
|
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
copy: [getShortcutKey("CtrlOrCmd+C")],
|
copy: [getShortcutKey("CtrlOrCmd+C")],
|
||||||
@ -77,7 +80,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
flipVertical: [getShortcutKey("Shift+V")],
|
flipVertical: [getShortcutKey("Shift+V")],
|
||||||
viewMode: [getShortcutKey("Alt+R")],
|
viewMode: [getShortcutKey("Alt+R")],
|
||||||
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
|
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
|
||||||
toggleLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { MarkOptional } from "../utility-types";
|
||||||
|
|
||||||
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
||||||
|
|
||||||
@ -81,7 +82,8 @@ export type ActionName =
|
|||||||
| "zoomOut"
|
| "zoomOut"
|
||||||
| "resetZoom"
|
| "resetZoom"
|
||||||
| "zoomToFit"
|
| "zoomToFit"
|
||||||
| "zoomToSelection"
|
| "zoomToFitSelection"
|
||||||
|
| "zoomToFitSelectionInViewport"
|
||||||
| "changeFontFamily"
|
| "changeFontFamily"
|
||||||
| "changeTextAlign"
|
| "changeTextAlign"
|
||||||
| "changeVerticalAlign"
|
| "changeVerticalAlign"
|
||||||
@ -110,10 +112,18 @@ export type ActionName =
|
|||||||
| "unbindText"
|
| "unbindText"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "bindText"
|
| "bindText"
|
||||||
| "toggleLock"
|
| "unlockAllElements"
|
||||||
|
| "toggleElementLock"
|
||||||
| "toggleLinearEditor"
|
| "toggleLinearEditor"
|
||||||
| "toggleEraserTool"
|
| "toggleEraserTool"
|
||||||
| "toggleHandTool";
|
| "toggleHandTool"
|
||||||
|
| "selectAllElementsInFrame"
|
||||||
|
| "removeAllElementsFromFrame"
|
||||||
|
| "updateFrameRendering"
|
||||||
|
| "setFrameAsActiveTool"
|
||||||
|
| "setEmbeddableAsActiveTool"
|
||||||
|
| "createContainerFromText"
|
||||||
|
| "wrapTextInContainer";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@ -121,6 +131,7 @@ export type PanelComponentProps = {
|
|||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
appProps: ExcalidrawProps;
|
appProps: ExcalidrawProps;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
|
app: AppClassProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
@ -132,12 +143,14 @@ export interface Action {
|
|||||||
event: React.KeyboardEvent | KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
app: AppClassProperties,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
contextItemLabel?:
|
contextItemLabel?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
) => string);
|
) => string);
|
||||||
predicate?: (
|
predicate?: (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { Box, getCommonBoundingBox } from "./element/bounds";
|
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
@ -33,7 +33,7 @@ export const alignElements = (
|
|||||||
|
|
||||||
const calculateTranslation = (
|
const calculateTranslation = (
|
||||||
group: ExcalidrawElement[],
|
group: ExcalidrawElement[],
|
||||||
selectionBoundingBox: Box,
|
selectionBoundingBox: BoundingBox,
|
||||||
{ axis, position }: Alignment,
|
{ axis, position }: Alignment,
|
||||||
): { x: number; y: number } => {
|
): { x: number; y: number } => {
|
||||||
const groupBoundingBox = getCommonBoundingBox(group);
|
const groupBoundingBox = getCommonBoundingBox(group);
|
||||||
|
@ -1,22 +1,32 @@
|
|||||||
export const trackEvent =
|
export const trackEvent = (
|
||||||
typeof process !== "undefined" &&
|
category: string,
|
||||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
action: string,
|
||||||
typeof window !== "undefined" &&
|
label?: string,
|
||||||
window.gtag
|
value?: number,
|
||||||
? (category: string, action: string, label?: string, value?: number) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
window.gtag("event", action, {
|
// place here categories that you want to track as events
|
||||||
event_category: category,
|
// KEEP IN MIND THE PRICING
|
||||||
event_label: label,
|
const ALLOWED_CATEGORIES_TO_TRACK = [] as string[];
|
||||||
value,
|
// Uncomment the next line to track locally
|
||||||
});
|
// console.log("Track Event", { category, action, label, value });
|
||||||
} catch (error) {
|
|
||||||
console.error("error logging to ga", error);
|
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
|
||||||
? (category: string, action: string, label?: string, value?: number) => {}
|
if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
|
||||||
: (category: string, action: string, label?: string, value?: number) => {
|
return;
|
||||||
// Uncomment the next line to track locally
|
}
|
||||||
// console.log("Track Event", { category, action, label, value });
|
|
||||||
};
|
if (window.sa_event) {
|
||||||
|
window.sa_event(action, {
|
||||||
|
category,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error during analytics", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import oc from "open-color";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_ELEMENT_PROPS,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
@ -23,20 +24,21 @@ export const getDefaultAppState = (): Omit<
|
|||||||
theme: THEME.LIGHT,
|
theme: THEME.LIGHT,
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
currentChartType: "bar",
|
currentChartType: "bar",
|
||||||
currentItemBackgroundColor: "transparent",
|
currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||||
currentItemEndArrowhead: "arrow",
|
currentItemEndArrowhead: "arrow",
|
||||||
currentItemFillStyle: "hachure",
|
currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
|
||||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||||
currentItemOpacity: 100,
|
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
|
||||||
currentItemRoughness: 1,
|
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
currentItemStartArrowhead: null,
|
currentItemStartArrowhead: null,
|
||||||
currentItemStrokeColor: oc.black,
|
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
currentItemRoundness: "round",
|
currentItemRoundness: "round",
|
||||||
currentItemStrokeStyle: "solid",
|
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
currentItemStrokeWidth: 1,
|
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
@ -44,7 +46,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
activeTool: {
|
activeTool: {
|
||||||
type: "selection",
|
type: "selection",
|
||||||
customType: null,
|
customType: null,
|
||||||
locked: false,
|
locked: DEFAULT_ELEMENT_PROPS.locked,
|
||||||
lastActiveTool: null,
|
lastActiveTool: null,
|
||||||
},
|
},
|
||||||
penMode: false,
|
penMode: false,
|
||||||
@ -57,7 +59,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
gridSize: null,
|
gridSize: null,
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
isSidebarDocked: false,
|
defaultSidebarDockedPreference: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
@ -77,13 +79,18 @@ export const getDefaultAppState = (): Omit<
|
|||||||
scrollY: 0,
|
scrollY: 0,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
selectedElementsAreBeingDragged: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
|
frameToHighlight: null,
|
||||||
|
editingFrame: null,
|
||||||
|
elementsToHighlight: null,
|
||||||
toast: null,
|
toast: null,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: COLOR_PALETTE.white,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
zoom: {
|
zoom: {
|
||||||
value: 1 as NormalizedZoomValue,
|
value: 1 as NormalizedZoomValue,
|
||||||
@ -133,6 +140,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||||
currentItemTextAlign: { browser: true, export: false, server: false },
|
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||||
cursorButton: { browser: true, export: false, server: false },
|
cursorButton: { browser: true, export: false, server: false },
|
||||||
|
activeEmbeddable: { browser: false, export: false, server: false },
|
||||||
draggingElement: { browser: false, export: false, server: false },
|
draggingElement: { browser: false, export: false, server: false },
|
||||||
editingElement: { browser: false, export: false, server: false },
|
editingElement: { browser: false, export: false, server: false },
|
||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
@ -149,7 +157,11 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
gridSize: { browser: true, export: true, server: true },
|
gridSize: { browser: true, export: true, server: true },
|
||||||
height: { browser: false, export: false, server: false },
|
height: { browser: false, export: false, server: false },
|
||||||
isBindingEnabled: { browser: false, export: false, server: false },
|
isBindingEnabled: { browser: false, export: false, server: false },
|
||||||
isSidebarDocked: { browser: true, export: false, server: false },
|
defaultSidebarDockedPreference: {
|
||||||
|
browser: true,
|
||||||
|
export: false,
|
||||||
|
server: false,
|
||||||
|
},
|
||||||
isLoading: { browser: false, export: false, server: false },
|
isLoading: { browser: false, export: false, server: false },
|
||||||
isResizing: { browser: false, export: false, server: false },
|
isResizing: { browser: false, export: false, server: false },
|
||||||
isRotating: { browser: false, export: false, server: false },
|
isRotating: { browser: false, export: false, server: false },
|
||||||
@ -171,11 +183,20 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
scrollY: { browser: true, export: false, server: false },
|
scrollY: { browser: true, export: false, server: false },
|
||||||
selectedElementIds: { browser: true, export: false, server: false },
|
selectedElementIds: { browser: true, export: false, server: false },
|
||||||
selectedGroupIds: { browser: true, export: false, server: false },
|
selectedGroupIds: { browser: true, export: false, server: false },
|
||||||
|
selectedElementsAreBeingDragged: {
|
||||||
|
browser: false,
|
||||||
|
export: false,
|
||||||
|
server: false,
|
||||||
|
},
|
||||||
selectionElement: { browser: false, export: false, server: false },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
showStats: { browser: true, export: false, server: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false, server: false },
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
|
frameToHighlight: { browser: false, export: false, server: false },
|
||||||
|
editingFrame: { browser: false, export: false, server: false },
|
||||||
|
elementsToHighlight: { browser: false, export: false, server: false },
|
||||||
toast: { browser: false, export: false, server: false },
|
toast: { browser: false, export: false, server: false },
|
||||||
viewBackgroundColor: { browser: true, export: true, server: true },
|
viewBackgroundColor: { browser: true, export: true, server: true },
|
||||||
width: { browser: false, export: false, server: false },
|
width: { browser: false, export: false, server: false },
|
||||||
|
20
src/assets/lock.svg
Normal file
20
src/assets/lock.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<svg width="178" height="162" viewBox="0 0 178 162" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.3329 54.3823L38.5547 94.3134L39.7731 111.754L40.1282 118.907L41.0832 123.59L44.3502 131.942L48.9438 137.693L52.5472 143.333L58.5544 147.755L62.5364 150.239L72.3634 154.486L83.15 156.361L91.1212 158.708L101.174 157.525L110.808 156.719L115.983 154.049L124.511 151.377L129.276 148.71L133.701 143.947L139.666 135.877L142.001 128.136L145.746 118.192L145.188 111.065L145.489 94.3675L145.873 75.2546L143.227 59.7779L142.022 47.4695L138.595 46.8345L102.952 45.4703L56.9173 46.7498L46.0719 49.1207L41.9323 50.6825L39.5684 53.4297" fill="#E3E2FE"/>
|
||||||
|
<path d="M41.0014 54.2859C41.0861 64.8796 38.3765 102.581 40.9779 117.876C43.5793 133.17 48.2646 139.346 56.6121 146.047C64.9596 152.746 79.1214 157.662 91.0653 158.078C103.009 158.492 119.347 155.242 128.277 148.543C137.206 141.842 142.112 133.527 144.641 117.874C147.169 102.221 146.061 66.4132 143.446 54.6222C140.83 42.8289 143.97 48.2857 128.948 47.1238C113.925 45.9619 67.9608 46.477 53.3051 47.6483C38.6493 48.8197 43.2053 53.0675 41.0155 54.1518M40.5263 53.9801C40.5404 64.6138 37.9249 103.418 40.5921 118.587C43.257 133.755 48.147 138.325 56.5251 144.991C64.9008 151.655 78.7263 157.935 90.8536 158.577C102.981 159.221 120.212 155.413 129.289 148.844C138.368 142.277 142.872 134.995 145.321 119.168C147.767 103.343 146.805 65.8698 143.977 53.8837C141.148 41.8975 143.615 48.4292 128.348 47.2508C113.081 46.0724 67.14 45.6726 52.3737 46.8157C37.6074 47.9564 41.7776 53.091 39.7524 54.1024" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.7935 45.726L66.36 36.6964L65.5979 34.4501L67.3384 30.7668L70.4197 26.5048L74.8157 21.0598L81.9095 16.9131L89.9419 14.4951L95.6127 12.8534L97.7555 13.2133L103.819 15.269L106.552 18.5807L109.967 22.3793L114.45 27.2387L114.904 34.3937L117.153 38.9214L116.031 44.5005L116.765 45.5448L118.863 47.3559L126.164 47.6782L127.744 46.7797L128.76 44.7052L123.882 25.8227L120.641 20.4835L116.351 15.8758L112.809 11.0729L108.617 7.36601L101.352 5.43025L93.263 3.31104L84.2451 5.1433L77.7299 7.86229L76.0011 8.0975L62.1168 19.8532L59.5366 24.5597L55.7733 32.3215L55.0371 39.5282L56.0626 44.8933L56.5636 47.1725L59.6401 46.8644L66.1648 46.0388" fill="#E3E2FE"/>
|
||||||
|
<path d="M65.0037 46.0106C65.1166 43.8231 64.9237 36.8492 66.1421 33.2412C67.3605 29.6307 69.0799 27.2857 72.314 24.3527C75.5481 21.422 81.273 17.6093 85.5444 15.6453C89.8157 13.6813 94.3317 12.1148 97.9421 12.5664C101.555 13.0157 104.544 15.857 107.219 18.3478C109.893 20.8387 112.356 24.0869 113.986 27.5115C115.618 30.9338 116.389 35.8684 117.006 38.8861C117.622 41.9038 115.992 44.2888 117.69 45.6178C119.388 46.949 125.617 48.1721 127.195 46.8667C128.773 45.5613 127.717 41.1888 127.157 37.7877C126.597 34.3866 125.494 30.1247 123.838 26.4601C122.183 22.7956 119.746 18.9029 117.222 15.7982C114.698 12.6934 112.791 9.9086 108.696 7.83642C104.601 5.76189 97.8081 3.42863 92.6547 3.35337C87.5013 3.2781 81.5529 5.74308 77.7708 7.38717C73.991 9.03127 72.8879 10.6166 69.9666 13.218C67.043 15.8217 62.4306 19.6768 60.2384 23.0026C58.0486 26.3284 57.4818 29.252 56.8185 33.1753C56.1529 37.0962 54.6499 44.39 56.2517 46.5327C57.8511 48.6731 64.7756 45.98 66.4267 46.0247M65.9704 45.5096C65.9845 43.348 64.2652 37.5525 65.5423 33.8456C66.8172 30.1364 70.2959 26.4789 73.6264 23.259C76.9546 20.039 81.3177 16.3015 85.5208 14.5281C89.7216 12.7523 95.1079 11.7903 98.8383 12.6111C102.569 13.432 105.283 16.8072 107.903 19.4486C110.526 22.09 113.146 25.3029 114.567 28.4664C115.987 31.6276 116.03 35.4051 116.425 38.4204C116.82 41.4334 115.124 45.1426 116.937 46.5515C118.751 47.9604 125.539 48.2968 127.31 46.8761C129.081 45.4578 127.978 41.4428 127.562 38.0347C127.145 34.6265 126.501 30.1646 124.81 26.4296C123.116 22.6921 120.195 18.7594 117.413 15.6194C114.63 12.4818 112.247 9.39349 108.117 7.58945C103.987 5.78541 97.5776 5.02099 92.6335 4.79519C87.6895 4.56939 82.3503 4.78813 78.4505 6.23466C74.5484 7.68118 72.0882 10.6542 69.228 13.4696C66.3679 16.2851 63.4725 19.7873 61.2898 23.1319C59.1071 26.4789 56.9761 29.4896 56.1293 33.5469C55.285 37.6043 54.577 45.2132 56.2117 47.4759C57.8487 49.7409 64.2675 47.3418 65.9445 47.1301" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M140.37 54.8958C137.884 58.1322 127.704 71.2286 125.185 74.5427M139.697 54.209C137.098 57.5466 127.005 71.7884 124.51 75.3565" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M141.663 63.1765C139.661 66.0413 131.311 77.1501 129.077 79.9726M141.065 62.5908C139.021 65.2792 130.631 76.1364 128.717 78.8625" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M141.888 72.9917C139.475 75.8589 130.268 86.8478 127.966 89.7455M141.02 72.726C138.503 75.6496 129.775 87.2476 127.58 90.3242" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M141.948 82.215C139.815 85.1057 130.308 96.8214 127.961 99.7709M141.459 81.7375C139.298 84.4119 129.816 95.9888 127.479 98.8606" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M141.357 91.7838C138.885 95.2484 128.808 108.535 126.428 111.76M142.474 91.4757C139.917 94.7921 128.38 107.493 125.781 110.883" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M142.568 101.479C140.028 104.403 129.867 115.528 127.195 118.356M141.811 101.018C139.212 104.055 129.477 115.975 126.828 118.97" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M141.023 112.172C138.591 114.775 128.028 125.905 125.422 128.664M140.51 113.465C138.008 116.147 127.36 125.233 124.742 127.582" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M139.004 123.69C136.501 126.275 125.952 137.248 123.287 140.108M138.343 124.817C135.805 127.454 125.487 138.261 122.848 140.75" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M132.192 139.862C129.854 141.624 120.87 148.168 118.574 150.012M131.39 139.496C128.97 141.333 120.524 148.89 118.322 150.621" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.6351 92.3124L78.2767 89.0148L78.6718 88.8784L75.6282 79.6865L74.4922 76.0525L75.0379 74.1074L78.6248 69.5444L83.6182 65.186L86.6924 64.0711L93.7768 63.9864L99.9181 63.9276L103.905 64.4215L106.038 66.068L109.333 67.6392L110.251 69.4479L112.438 73.1877L112.702 81.928L111.674 82.93L110.907 85.5573L107.828 89.2336L101.273 92.9193L102.785 120.401L99.5488 125.521L98.0059 127.838L96.1313 129.414L93.17 130.237L92.2198 130.033L90.1358 129.233L88.8328 126.594L87.8378 95.2549L88.9386 93.3215L86.0409 91.294L80.9533 91.1552" fill="white"/>
|
||||||
|
<path d="M82.8214 92.0607C82.0664 91.4327 79.291 90.7201 77.8539 88.2033C76.4167 85.6866 73.5284 80.4438 74.1964 76.9581C74.862 73.4723 78.6959 69.6384 81.8524 67.2887C85.0089 64.939 88.9227 63.1138 93.1353 62.8574C97.3478 62.6034 103.957 63.9888 107.132 65.7575C110.31 67.5263 111.416 70.5651 112.196 73.4747C112.977 76.3842 112.606 80.6626 111.82 83.2122C111.035 85.7642 109.078 87.1661 107.481 88.7749C105.883 90.3837 103.106 91.2751 102.233 92.8651C101.363 94.4551 102.327 95.3254 102.25 98.3125C102.172 101.3 101.76 107.227 101.767 110.788C101.772 114.349 102.487 116.981 102.285 119.676C102.085 122.374 101.52 125.126 100.556 126.965C99.5917 128.805 98.077 130.256 96.5011 130.715C94.9275 131.171 92.4485 130.36 91.1101 129.713C89.7742 129.066 89.0144 128.341 88.4805 126.836C87.9489 125.331 87.9678 123.964 87.9137 120.681C87.8596 117.397 88.1159 111.599 88.1583 107.14C88.203 102.68 89.2779 96.445 88.1724 93.9236C87.0693 91.4022 82.7791 92.4347 81.5325 92.0137M82.0194 91.6068C81.222 90.7624 78.4536 89.7886 77.3623 87.1567C76.2733 84.5247 74.6621 79.3125 75.4783 75.815C76.2921 72.3151 79.1428 68.3166 82.2522 66.1597C85.3617 64.0029 90.1693 63.062 94.1302 62.8739C98.0911 62.6857 102.925 63.0832 106.02 65.0331C109.118 66.9853 111.834 71.5836 112.705 74.5801C113.572 77.5743 111.949 80.7731 111.234 83.0076C110.519 85.2444 109.835 86.3711 108.417 87.9916C107.001 89.6146 103.738 90.9623 102.732 92.7358C101.725 94.5092 102.351 95.6382 102.377 98.6301C102.405 101.62 102.866 106.949 102.894 110.682C102.922 114.417 102.955 118.291 102.544 121.038C102.13 123.783 101.408 125.54 100.42 127.161C99.4318 128.781 98.1005 130.233 96.6163 130.759C95.1322 131.286 92.9353 130.893 91.51 130.322C90.0846 129.753 88.7769 128.889 88.0618 127.335C87.3468 125.78 87.0128 124.317 87.2198 120.998C87.4268 117.68 89.0874 112.046 89.299 107.422C89.5107 102.798 89.8494 95.9322 88.4946 93.2509C87.1398 90.5695 82.4804 91.4845 81.1679 91.3316" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M28.1943 139.31C26.7936 139.432 25.332 140.402 23.8703 140.523C23.1395 140.766 22.5914 140.16 21.9824 139.735C21.5561 139.553 21.008 138.461 20.8253 138.219C20.5817 136.884 19.9118 134.276 20.0336 133.002C19.7291 131.364 21.5561 129.787 23.0786 129.727C23.2613 129.727 23.8094 129.787 23.8703 129.787C25.7583 130.151 27.5853 131.546 29.5341 131.728C29.595 131.728 29.6559 131.728 29.6559 131.668C30.4476 130.333 30.204 126.937 30.813 125.542C30.813 125.36 31.1784 123.54 31.1784 123.237C31.6048 122.327 32.1529 121.781 33.1273 122.084C33.7972 122.266 34.6498 122.388 34.6498 123.237V128.635C34.8325 129.242 36.1114 128.999 36.5986 128.999C38.7911 128.028 40.8617 127.422 43.3586 127.058C45.6729 127.179 46.7082 129.242 46.5864 131.304C46.6473 132.396 45.4293 133.245 44.6985 133.973C44.4549 134.094 43.4804 134.519 43.115 134.397C42.2624 133.791 41.1662 134.033 40.1309 134.094C40.1309 134.155 40.0091 134.337 40.07 134.397C41.288 135.853 43.5413 136.096 45.0639 137.066C46.1601 138.34 47.4999 138.643 47.1345 140.341C47.0736 141.191 47.1345 142.1 46.221 142.404C45.9774 142.586 44.5767 142.828 44.2722 142.828C43.9677 142.768 43.3586 142.343 43.115 142.04C40.9835 141.13 38.6693 140.402 36.2332 140.159V145.133C35.9896 146.468 35.6851 147.923 34.6498 148.955C34.2844 149.015 33.1273 149.015 32.7619 148.955C32.4574 148.773 31.4221 147.741 31.1784 147.438C30.5694 145.133 30.4476 142.404 29.6559 140.159C29.1687 139.553 28.986 139.25 28.1943 139.31Z" fill="#6965DB"/>
|
||||||
|
<path d="M59.5964 139.31C58.1956 139.432 56.734 140.402 55.2724 140.523C54.5416 140.766 53.9935 140.16 53.3845 139.735C52.9582 139.553 52.41 138.461 52.2273 138.219C51.9837 136.884 51.3138 134.276 51.4356 133.002C51.1311 131.364 52.9582 129.787 54.4807 129.727C54.6634 129.727 55.2115 129.787 55.2724 129.787C57.1603 130.151 58.9874 131.546 60.9362 131.728C60.9971 131.728 61.058 131.728 61.058 131.668C61.8497 130.333 61.6061 126.937 62.2151 125.542C62.2151 125.36 62.5805 123.54 62.5805 123.237C63.0068 122.327 63.5549 121.781 64.5293 122.084C65.1992 122.266 66.0519 122.388 66.0519 123.237V128.635C66.2346 129.242 67.5135 128.999 68.0007 128.999C70.1931 128.028 72.2638 127.422 74.7607 127.058C77.0749 127.179 78.1103 129.242 77.9885 131.304C78.0494 132.396 76.8313 133.245 76.1005 133.973C75.8569 134.094 74.8825 134.519 74.5171 134.397C73.6645 133.791 72.5683 134.033 71.5329 134.094C71.5329 134.155 71.4112 134.337 71.4721 134.397C72.6901 135.853 74.9434 136.096 76.4659 137.066C77.5621 138.34 78.902 138.643 78.5366 140.341C78.4757 141.191 78.5366 142.1 77.623 142.404C77.3794 142.586 75.9787 142.828 75.6742 142.828C75.3697 142.768 74.7607 142.343 74.5171 142.04C72.3856 141.13 70.0713 140.402 67.6353 140.159V145.133C67.3917 146.468 67.0872 147.923 66.0519 148.955C65.6865 149.015 64.5293 149.015 64.1639 148.955C63.8594 148.773 62.8241 147.741 62.5805 147.438C61.9715 145.133 61.8497 142.404 61.058 140.159C60.5708 139.553 60.3881 139.25 59.5964 139.31Z" fill="#6965DB"/>
|
||||||
|
<path d="M90.9984 139.31C89.5977 139.432 88.1361 140.402 86.6745 140.523C85.9436 140.766 85.3955 140.16 84.7865 139.735C84.3602 139.553 83.8121 138.461 83.6294 138.219C83.3858 136.884 82.7159 134.276 82.8377 133.002C82.5332 131.364 84.3602 129.787 85.8827 129.727C86.0654 129.727 86.6136 129.787 86.6745 129.787C88.5624 130.151 90.3894 131.546 92.3382 131.728C92.3991 131.728 92.46 131.728 92.46 131.668C93.2518 130.333 93.0082 126.937 93.6172 125.542C93.6172 125.36 93.9826 123.54 93.9826 123.237C94.4089 122.327 94.957 121.781 95.9314 122.084C96.6013 122.266 97.4539 122.388 97.4539 123.237V128.635C97.6366 129.242 98.9155 128.999 99.4028 128.999C101.595 128.028 103.666 127.422 106.163 127.058C108.477 127.179 109.512 129.242 109.391 131.304C109.451 132.396 108.233 133.245 107.503 133.973C107.259 134.094 106.285 134.519 105.919 134.397C105.067 133.791 103.97 134.033 102.935 134.094C102.935 134.155 102.813 134.337 102.874 134.397C104.092 135.853 106.345 136.096 107.868 137.066C108.964 138.34 110.304 138.643 109.939 140.341C109.878 141.191 109.939 142.1 109.025 142.404C108.782 142.586 107.381 142.828 107.076 142.828C106.772 142.768 106.163 142.343 105.919 142.04C103.788 141.13 101.473 140.402 99.0373 140.159V145.133C98.7937 146.468 98.4892 147.923 97.4539 148.955C97.0885 149.015 95.9314 149.015 95.566 148.955C95.2615 148.773 94.2262 147.741 93.9826 147.438C93.3736 145.133 93.2518 142.404 92.46 140.159C91.9728 139.553 91.7901 139.25 90.9984 139.31Z" fill="#6965DB"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 13 KiB |
@ -1,8 +1,11 @@
|
|||||||
import colors from "./colors";
|
import {
|
||||||
|
COLOR_PALETTE,
|
||||||
|
DEFAULT_CHART_COLOR_INDEX,
|
||||||
|
getAllColorsSpecificShade,
|
||||||
|
} from "./colors";
|
||||||
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";
|
||||||
@ -158,10 +161,7 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const bgColors = colors.elementBackground.slice(
|
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
|
||||||
2,
|
|
||||||
colors.elementBackground.length,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Put all the common properties here so when the whole chart is selected
|
// Put all the common properties here so when the whole chart is selected
|
||||||
// the properties dialog shows the correct selected values
|
// the properties dialog shows the correct selected values
|
||||||
@ -171,7 +171,7 @@ const commonProps = {
|
|||||||
fontSize: DEFAULT_FONT_SIZE,
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
strokeColor: colors.elementStroke[0],
|
strokeColor: COLOR_PALETTE.black,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
@ -179,7 +179,7 @@ const commonProps = {
|
|||||||
locked: false,
|
locked: false,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const getChartDimentions = (spreadsheet: Spreadsheet) => {
|
const getChartDimensions = (spreadsheet: Spreadsheet) => {
|
||||||
const chartWidth =
|
const chartWidth =
|
||||||
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
||||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
||||||
@ -249,7 +249,7 @@ const chartLines = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
|
||||||
const xLine = newLinearElement({
|
const xLine = newLinearElement({
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
groupIds: [groupId],
|
groupIds: [groupId],
|
||||||
@ -312,7 +312,7 @@ const chartBaseElements = (
|
|||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
debug?: boolean,
|
debug?: boolean,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
|
||||||
|
|
||||||
const title = spreadsheet.title
|
const title = spreadsheet.title
|
||||||
? newTextElement({
|
? newTextElement({
|
||||||
@ -323,7 +323,6 @@ const chartBaseElements = (
|
|||||||
x: x + chartWidth / 2,
|
x: x + chartWidth / 2,
|
||||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
strokeStyle: "solid",
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
@ -338,7 +337,7 @@ const chartBaseElements = (
|
|||||||
y: y - chartHeight,
|
y: y - chartHeight,
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
strokeColor: colors.elementStroke[0],
|
strokeColor: COLOR_PALETTE.black,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
opacity: 6,
|
opacity: 6,
|
||||||
})
|
})
|
||||||
@ -384,7 +383,7 @@ const chartTypeBar = (
|
|||||||
y,
|
y,
|
||||||
groupId,
|
groupId,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
import.meta.env.DEV,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@ -473,7 +472,7 @@ const chartTypeLine = (
|
|||||||
y,
|
y,
|
||||||
groupId,
|
groupId,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
import.meta.env.DEV,
|
||||||
),
|
),
|
||||||
line,
|
line,
|
||||||
...lines,
|
...lines,
|
||||||
|
@ -1,28 +1,40 @@
|
|||||||
import colors from "./colors";
|
function hashToInteger(id: string) {
|
||||||
import { AppState } from "./types";
|
let hash = 0;
|
||||||
|
if (id.length === 0) {
|
||||||
export const getClientColors = (clientId: string, appState: AppState) => {
|
return hash;
|
||||||
if (appState?.collaborators) {
|
|
||||||
const currentUser = appState.collaborators.get(clientId);
|
|
||||||
if (currentUser?.color) {
|
|
||||||
return currentUser.color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Naive way of getting an integer out of the clientId
|
for (let i = 0; i < id.length; i++) {
|
||||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
const char = id.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + char;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip transparent & gray colors
|
export const getClientColor = (
|
||||||
const backgrounds = colors.elementBackground.slice(3);
|
/**
|
||||||
const strokes = colors.elementStroke.slice(3);
|
* any uniquely identifying key, such as user id or socket id
|
||||||
return {
|
*/
|
||||||
background: backgrounds[sum % backgrounds.length],
|
id: string,
|
||||||
stroke: strokes[sum % strokes.length],
|
) => {
|
||||||
};
|
// to get more even distribution in case `id` is not uniformly distributed to
|
||||||
|
// begin with, we hash it
|
||||||
|
const hash = Math.abs(hashToInteger(id));
|
||||||
|
// we want to get a multiple of 10 number in the range of 0-360 (in other
|
||||||
|
// words a hue value of step size 10). There are 37 such values including 0.
|
||||||
|
const hue = (hash % 37) * 10;
|
||||||
|
const saturation = 100;
|
||||||
|
const lightness = 83;
|
||||||
|
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClientInitials = (userName?: string | null) => {
|
/**
|
||||||
if (!userName) {
|
* returns first char, capitalized
|
||||||
return "?";
|
*/
|
||||||
}
|
export const getNameInitial = (name?: string | null) => {
|
||||||
return userName.trim()[0].toUpperCase();
|
// first char can be a surrogate pair, hence using codePointAt
|
||||||
|
const firstCodePoint = name?.trim()?.codePointAt(0);
|
||||||
|
return (
|
||||||
|
firstCodePoint ? String.fromCodePoint(firstCodePoint) : "?"
|
||||||
|
).toUpperCase();
|
||||||
};
|
};
|
||||||
|
@ -2,12 +2,15 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { AppState, BinaryFiles } from "./types";
|
import { BinaryFiles } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { isInitializedImageElement } from "./element/typeChecks";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
import { isPromiseLike } from "./utils";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
|
import { mutateElement } from "./element/mutateElement";
|
||||||
|
import { getContainingFrame } from "./frame";
|
||||||
|
import { isPromiseLike, isTestEnv } from "./utils";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@ -21,6 +24,7 @@ export interface ClipboardData {
|
|||||||
files?: BinaryFiles;
|
files?: BinaryFiles;
|
||||||
text?: string;
|
text?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
programmaticAPI?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
let CLIPBOARD = "";
|
||||||
@ -45,6 +49,7 @@ 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)
|
||||||
) {
|
) {
|
||||||
@ -55,24 +60,56 @@ const clipboardContainsElements = (
|
|||||||
|
|
||||||
export const copyToClipboard = async (
|
export const copyToClipboard = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles | null,
|
files: BinaryFiles | null,
|
||||||
) => {
|
) => {
|
||||||
|
const framesToCopy = new Set(
|
||||||
|
elements.filter((element) => element.type === "frame"),
|
||||||
|
);
|
||||||
|
let foundFile = false;
|
||||||
|
|
||||||
|
const _files = elements.reduce((acc, element) => {
|
||||||
|
if (isInitializedImageElement(element)) {
|
||||||
|
foundFile = true;
|
||||||
|
if (files && files[element.fileId]) {
|
||||||
|
acc[element.fileId] = files[element.fileId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as BinaryFiles);
|
||||||
|
|
||||||
|
if (foundFile && !files) {
|
||||||
|
console.warn(
|
||||||
|
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// select binded text elements when copying
|
// select binded text elements when copying
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements,
|
elements: elements.map((element) => {
|
||||||
files: files
|
if (
|
||||||
? elements.reduce((acc, element) => {
|
getContainingFrame(element) &&
|
||||||
if (isInitializedImageElement(element) && files[element.fileId]) {
|
!framesToCopy.has(getContainingFrame(element)!)
|
||||||
acc[element.fileId] = files[element.fileId];
|
) {
|
||||||
}
|
const copiedElement = deepCopyElement(element);
|
||||||
return acc;
|
mutateElement(copiedElement, {
|
||||||
}, {} as BinaryFiles)
|
frameId: null,
|
||||||
: undefined,
|
});
|
||||||
|
return copiedElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}),
|
||||||
|
files: files ? _files : undefined,
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
|
|
||||||
|
if (isTestEnv()) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
CLIPBOARD = json;
|
CLIPBOARD = json;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PREFER_APP_CLIPBOARD = false;
|
PREFER_APP_CLIPBOARD = false;
|
||||||
await copyTextToSystemClipboard(json);
|
await copyTextToSystemClipboard(json);
|
||||||
@ -156,6 +193,8 @@ 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,
|
||||||
@ -163,6 +202,7 @@ 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) {}
|
||||||
|
186
src/colors.ts
186
src/colors.ts
@ -1,22 +1,170 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
import { Merge } from "./utility-types";
|
||||||
|
|
||||||
const shades = (index: number) => [
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
oc.red[index],
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
oc.pink[index],
|
source: R,
|
||||||
oc.grape[index],
|
keys: K,
|
||||||
oc.violet[index],
|
) => {
|
||||||
oc.indigo[index],
|
return keys.reduce((acc, key: K[number]) => {
|
||||||
oc.blue[index],
|
if (key in source) {
|
||||||
oc.cyan[index],
|
acc[key] = source[key];
|
||||||
oc.teal[index],
|
}
|
||||||
oc.green[index],
|
return acc;
|
||||||
oc.lime[index],
|
}, {} as Pick<R, K[number]>) as Pick<R, K[number]>;
|
||||||
oc.yellow[index],
|
|
||||||
oc.orange[index],
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
canvasBackground: [oc.white, oc.gray[0], oc.gray[1], ...shades(0)],
|
|
||||||
elementBackground: ["transparent", oc.gray[4], oc.gray[6], ...shades(6)],
|
|
||||||
elementStroke: [oc.black, oc.gray[8], oc.gray[7], ...shades(9)],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ColorPickerColor =
|
||||||
|
| Exclude<keyof oc, "indigo" | "lime">
|
||||||
|
| "transparent"
|
||||||
|
| "bronze";
|
||||||
|
export type ColorTuple = readonly [string, string, string, string, string];
|
||||||
|
export type ColorPalette = Merge<
|
||||||
|
Record<ColorPickerColor, ColorTuple>,
|
||||||
|
{ black: "#1e1e1e"; white: "#ffffff"; transparent: "transparent" }
|
||||||
|
>;
|
||||||
|
|
||||||
|
// used general type instead of specific type (ColorPalette) to support custom colors
|
||||||
|
export type ColorPaletteCustom = { [key: string]: ColorTuple | string };
|
||||||
|
export type ColorShadesIndexes = [number, number, number, number, number];
|
||||||
|
|
||||||
|
export const MAX_CUSTOM_COLORS_USED_IN_CANVAS = 5;
|
||||||
|
export const COLORS_PER_ROW = 5;
|
||||||
|
|
||||||
|
export const DEFAULT_CHART_COLOR_INDEX = 4;
|
||||||
|
|
||||||
|
export const DEFAULT_ELEMENT_STROKE_COLOR_INDEX = 4;
|
||||||
|
export const DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX = 1;
|
||||||
|
export const ELEMENTS_PALETTE_SHADE_INDEXES = [0, 2, 4, 6, 8] as const;
|
||||||
|
export const CANVAS_PALETTE_SHADE_INDEXES = [0, 1, 2, 3, 4] as const;
|
||||||
|
|
||||||
|
export const getSpecificColorShades = (
|
||||||
|
color: Exclude<
|
||||||
|
ColorPickerColor,
|
||||||
|
"transparent" | "white" | "black" | "bronze"
|
||||||
|
>,
|
||||||
|
indexArr: Readonly<ColorShadesIndexes>,
|
||||||
|
) => {
|
||||||
|
return indexArr.map((index) => oc[color][index]) as any as ColorTuple;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COLOR_PALETTE = {
|
||||||
|
transparent: "transparent",
|
||||||
|
black: "#1e1e1e",
|
||||||
|
white: "#ffffff",
|
||||||
|
// open-colors
|
||||||
|
gray: getSpecificColorShades("gray", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
red: getSpecificColorShades("red", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
pink: getSpecificColorShades("pink", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
grape: getSpecificColorShades("grape", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
violet: getSpecificColorShades("violet", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
blue: getSpecificColorShades("blue", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
cyan: getSpecificColorShades("cyan", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
teal: getSpecificColorShades("teal", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
green: getSpecificColorShades("green", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
yellow: getSpecificColorShades("yellow", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
orange: getSpecificColorShades("orange", ELEMENTS_PALETTE_SHADE_INDEXES),
|
||||||
|
// radix bronze shades 3,5,7,9,11
|
||||||
|
bronze: ["#f8f1ee", "#eaddd7", "#d2bab0", "#a18072", "#846358"],
|
||||||
|
} as ColorPalette;
|
||||||
|
|
||||||
|
const COMMON_ELEMENT_SHADES = pick(COLOR_PALETTE, [
|
||||||
|
"cyan",
|
||||||
|
"blue",
|
||||||
|
"violet",
|
||||||
|
"grape",
|
||||||
|
"pink",
|
||||||
|
"green",
|
||||||
|
"teal",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"red",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// quick picks defaults
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ORDER matters for positioning in quick picker
|
||||||
|
export const DEFAULT_ELEMENT_STROKE_PICKS = [
|
||||||
|
COLOR_PALETTE.black,
|
||||||
|
COLOR_PALETTE.red[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
|
||||||
|
COLOR_PALETTE.green[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
|
||||||
|
COLOR_PALETTE.blue[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
|
||||||
|
COLOR_PALETTE.yellow[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
|
||||||
|
] as ColorTuple;
|
||||||
|
|
||||||
|
// ORDER matters for positioning in quick picker
|
||||||
|
export const DEFAULT_ELEMENT_BACKGROUND_PICKS = [
|
||||||
|
COLOR_PALETTE.transparent,
|
||||||
|
COLOR_PALETTE.red[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
|
||||||
|
COLOR_PALETTE.green[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
|
||||||
|
COLOR_PALETTE.blue[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
|
||||||
|
COLOR_PALETTE.yellow[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
|
||||||
|
] as ColorTuple;
|
||||||
|
|
||||||
|
// ORDER matters for positioning in quick picker
|
||||||
|
export const DEFAULT_CANVAS_BACKGROUND_PICKS = [
|
||||||
|
COLOR_PALETTE.white,
|
||||||
|
// radix slate2
|
||||||
|
"#f8f9fa",
|
||||||
|
// radix blue2
|
||||||
|
"#f5faff",
|
||||||
|
// radix yellow2
|
||||||
|
"#fffce8",
|
||||||
|
// radix bronze2
|
||||||
|
"#fdf8f6",
|
||||||
|
] as ColorTuple;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// palette defaults
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const DEFAULT_ELEMENT_STROKE_COLOR_PALETTE = {
|
||||||
|
// 1st row
|
||||||
|
transparent: COLOR_PALETTE.transparent,
|
||||||
|
white: COLOR_PALETTE.white,
|
||||||
|
gray: COLOR_PALETTE.gray,
|
||||||
|
black: COLOR_PALETTE.black,
|
||||||
|
bronze: COLOR_PALETTE.bronze,
|
||||||
|
// rest
|
||||||
|
...COMMON_ELEMENT_SHADES,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ORDER matters for positioning in pallete (5x3 grid)s
|
||||||
|
export const DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE = {
|
||||||
|
transparent: COLOR_PALETTE.transparent,
|
||||||
|
white: COLOR_PALETTE.white,
|
||||||
|
gray: COLOR_PALETTE.gray,
|
||||||
|
black: COLOR_PALETTE.black,
|
||||||
|
bronze: COLOR_PALETTE.bronze,
|
||||||
|
|
||||||
|
...COMMON_ELEMENT_SHADES,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// !!!MUST BE WITHOUT GRAY, TRANSPARENT AND BLACK!!!
|
||||||
|
export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) =>
|
||||||
|
[
|
||||||
|
// 2nd row
|
||||||
|
COLOR_PALETTE.cyan[index],
|
||||||
|
COLOR_PALETTE.blue[index],
|
||||||
|
COLOR_PALETTE.violet[index],
|
||||||
|
COLOR_PALETTE.grape[index],
|
||||||
|
COLOR_PALETTE.pink[index],
|
||||||
|
|
||||||
|
// 3rd row
|
||||||
|
COLOR_PALETTE.green[index],
|
||||||
|
COLOR_PALETTE.teal[index],
|
||||||
|
COLOR_PALETTE.yellow[index],
|
||||||
|
COLOR_PALETTE.orange[index],
|
||||||
|
COLOR_PALETTE.red[index],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const rgbToHex = (r: number, g: number, b: number) =>
|
||||||
|
`#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement, PointerType } from "../element/types";
|
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
hasText,
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppState, Zoom } from "../types";
|
import { UIAppState, Zoom } from "../types";
|
||||||
import {
|
import {
|
||||||
capitalizeString,
|
capitalizeString,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
@ -28,16 +28,23 @@ import { trackEvent } from "../analytics";
|
|||||||
import { hasBoundTextElement } from "../element/typeChecks";
|
import { hasBoundTextElement } from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import "./Actions.scss";
|
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { shouldAllowVerticalAlign } from "../element/textElement";
|
import {
|
||||||
|
shouldAllowVerticalAlign,
|
||||||
|
suppportsHorizontalAlign,
|
||||||
|
} from "../element/textElement";
|
||||||
|
|
||||||
|
import "./Actions.scss";
|
||||||
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
|
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
renderAction,
|
renderAction,
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: UIAppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
}) => {
|
}) => {
|
||||||
@ -85,7 +92,8 @@ export const SelectedShapeActions = ({
|
|||||||
<div>
|
<div>
|
||||||
{((hasStrokeColor(appState.activeTool.type) &&
|
{((hasStrokeColor(appState.activeTool.type) &&
|
||||||
appState.activeTool.type !== "image" &&
|
appState.activeTool.type !== "image" &&
|
||||||
commonSelectedType !== "image") ||
|
commonSelectedType !== "image" &&
|
||||||
|
commonSelectedType !== "frame") ||
|
||||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||||
renderAction("changeStrokeColor")}
|
renderAction("changeStrokeColor")}
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +130,8 @@ export const SelectedShapeActions = ({
|
|||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{renderAction("changeTextAlign")}
|
{suppportsHorizontalAlign(targetElements) &&
|
||||||
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -211,65 +220,189 @@ export const ShapesSwitcher = ({
|
|||||||
appState,
|
appState,
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
activeTool: AppState["activeTool"];
|
activeTool: UIAppState["activeTool"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
appState: AppState;
|
appState: UIAppState;
|
||||||
}) => (
|
}) => {
|
||||||
<>
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
const device = useDevice();
|
||||||
const label = t(`toolBar.${value}`);
|
return (
|
||||||
const letter =
|
<>
|
||||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
const shortcut = letter
|
const label = t(`toolBar.${value}`);
|
||||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
const letter =
|
||||||
: `${numericKey}`;
|
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||||
return (
|
const shortcut = letter
|
||||||
<ToolButton
|
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||||
className={clsx("Shape", { fillable })}
|
: `${numericKey}`;
|
||||||
key={value}
|
return (
|
||||||
type="radio"
|
<ToolButton
|
||||||
icon={icon}
|
className={clsx("Shape", { fillable })}
|
||||||
checked={activeTool.type === value}
|
key={value}
|
||||||
name="editor-current-shape"
|
type="radio"
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
icon={icon}
|
||||||
keyBindingLabel={numericKey || letter}
|
checked={activeTool.type === value}
|
||||||
aria-label={capitalizeString(label)}
|
name="editor-current-shape"
|
||||||
aria-keyshortcuts={shortcut}
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
data-testid={`toolbar-${value}`}
|
keyBindingLabel={numericKey || letter}
|
||||||
onPointerDown={({ pointerType }) => {
|
aria-label={capitalizeString(label)}
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
aria-keyshortcuts={shortcut}
|
||||||
setAppState({
|
data-testid={`toolbar-${value}`}
|
||||||
penDetected: true,
|
onPointerDown={({ pointerType }) => {
|
||||||
penMode: true,
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
|
setAppState({
|
||||||
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={({ pointerType }) => {
|
||||||
|
if (appState.activeTool.type !== value) {
|
||||||
|
trackEvent("toolbar", value, "ui");
|
||||||
|
}
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: value,
|
||||||
});
|
});
|
||||||
}
|
setAppState({
|
||||||
}}
|
activeTool: nextActiveTool,
|
||||||
onChange={({ pointerType }) => {
|
activeEmbeddable: null,
|
||||||
if (appState.activeTool.type !== value) {
|
multiElement: null,
|
||||||
trackEvent("toolbar", value, "ui");
|
selectedElementIds: {},
|
||||||
}
|
});
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
setCursorForShape(canvas, {
|
||||||
type: value,
|
...appState,
|
||||||
});
|
activeTool: nextActiveTool,
|
||||||
setAppState({
|
});
|
||||||
activeTool: nextActiveTool,
|
if (value === "image") {
|
||||||
multiElement: null,
|
onImageAction({ pointerType });
|
||||||
selectedElementIds: {},
|
}
|
||||||
});
|
}}
|
||||||
setCursorForShape(canvas, {
|
/>
|
||||||
...appState,
|
);
|
||||||
activeTool: nextActiveTool,
|
})}
|
||||||
});
|
<div className="App-toolbar__divider" />
|
||||||
if (value === "image") {
|
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
||||||
onImageAction({ pointerType });
|
{device.isMobile ? (
|
||||||
}
|
<>
|
||||||
}}
|
<ToolButton
|
||||||
/>
|
className={clsx("Shape", { fillable: false })}
|
||||||
);
|
type="radio"
|
||||||
})}
|
icon={frameToolIcon}
|
||||||
</>
|
checked={activeTool.type === "frame"}
|
||||||
);
|
name="editor-current-shape"
|
||||||
|
title={`${capitalizeString(
|
||||||
|
t("toolBar.frame"),
|
||||||
|
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||||
|
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||||
|
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||||
|
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||||
|
data-testid={`toolbar-frame`}
|
||||||
|
onPointerDown={({ pointerType }) => {
|
||||||
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
|
setAppState({
|
||||||
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={({ pointerType }) => {
|
||||||
|
trackEvent("toolbar", "frame", "ui");
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "frame",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
|
icon={EmbedIcon}
|
||||||
|
checked={activeTool.type === "embeddable"}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={capitalizeString(t("toolBar.embeddable"))}
|
||||||
|
aria-label={capitalizeString(t("toolBar.embeddable"))}
|
||||||
|
data-testid={`toolbar-embeddable`}
|
||||||
|
onPointerDown={({ pointerType }) => {
|
||||||
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
|
setAppState({
|
||||||
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={({ pointerType }) => {
|
||||||
|
trackEvent("toolbar", "embeddable", "ui");
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "embeddable",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
className="App-toolbar__extra-tools-trigger"
|
||||||
|
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||||
|
title={t("toolBar.extraTools")}
|
||||||
|
>
|
||||||
|
{extraToolsIcon}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
|
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||||
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "frame",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={frameToolIcon}
|
||||||
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
|
data-testid="toolbar-frame"
|
||||||
|
>
|
||||||
|
{t("toolBar.frame")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "embeddable",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={EmbedIcon}
|
||||||
|
data-testid="toolbar-embeddable"
|
||||||
|
>
|
||||||
|
{t("toolBar.embeddable")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ZoomActions = ({
|
export const ZoomActions = ({
|
||||||
renderAction,
|
renderAction,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { actionClearCanvas } from "../actions";
|
import { actionClearCanvas } from "../actions";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
import { useExcalidrawActionManager } from "./App";
|
import { useExcalidrawActionManager } from "./App";
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
|||||||
export const ActiveConfirmDialog = () => {
|
export const ActiveConfirmDialog = () => {
|
||||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||||
activeConfirmDialogAtom,
|
activeConfirmDialogAtom,
|
||||||
|
jotaiScope,
|
||||||
);
|
);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
46
src/components/App.test.tsx
Normal file
46
src/components/App.test.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import * as Renderer from "../renderer/renderScene";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
import { render, queryByTestId } from "../tests/test-utils";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||||
|
|
||||||
|
describe("Test <App/>", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Unmount ReactDOM from root
|
||||||
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
localStorage.clear();
|
||||||
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error modal when using brave and measureText API is not working", async () => {
|
||||||
|
(global.navigator as any).brave = {
|
||||||
|
isBrave: {
|
||||||
|
name: "isBrave",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalContext = global.HTMLCanvasElement.prototype.getContext("2d");
|
||||||
|
//@ts-ignore
|
||||||
|
global.HTMLCanvasElement.prototype.getContext = (contextId) => {
|
||||||
|
return {
|
||||||
|
...originalContext,
|
||||||
|
measureText: () => ({
|
||||||
|
width: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
expect(
|
||||||
|
queryByTestId(
|
||||||
|
document.querySelector(".excalidraw-modal-container")!,
|
||||||
|
"brave-measure-text-error",
|
||||||
|
),
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@ -10,10 +10,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $oc-white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.625rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 800;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
&-img {
|
&-img {
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import "./Avatar.scss";
|
import "./Avatar.scss";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { getClientInitials } from "../clients";
|
import { getNameInitial } from "../clients";
|
||||||
|
|
||||||
type AvatarProps = {
|
type AvatarProps = {
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
color: string;
|
color: string;
|
||||||
border: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
src?: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
|
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
|
||||||
const shortName = getClientInitials(name);
|
const shortName = getNameInitial(name);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const loadImg = !error && src;
|
const loadImg = !error && src;
|
||||||
const style = loadImg ? undefined : { background: color };
|
const style = loadImg ? undefined : { background: color };
|
||||||
|
43
src/components/BraveMeasureTextError.tsx
Normal file
43
src/components/BraveMeasureTextError.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Trans from "./Trans";
|
||||||
|
|
||||||
|
const BraveMeasureTextError = () => {
|
||||||
|
return (
|
||||||
|
<div data-testid="brave-measure-text-error">
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="errors.brave_measure_text_error.line1"
|
||||||
|
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="errors.brave_measure_text_error.line2"
|
||||||
|
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="errors.brave_measure_text_error.line3"
|
||||||
|
link={(el) => (
|
||||||
|
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
|
||||||
|
{el}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="errors.brave_measure_text_error.line4"
|
||||||
|
issueLink={(el) => (
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/issues/new">
|
||||||
|
{el}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
discordLink={(el) => <a href="https://discord.gg/UexuTaE">{el}.</a>}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BraveMeasureTextError;
|
@ -1,8 +1,12 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { composeEventHandlers } from "../utils";
|
||||||
import "./Button.scss";
|
import "./Button.scss";
|
||||||
|
|
||||||
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
onSelect: () => any;
|
onSelect: () => any;
|
||||||
|
/** whether button is in active state */
|
||||||
|
selected?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|||||||
export const Button = ({
|
export const Button = ({
|
||||||
type = "button",
|
type = "button",
|
||||||
onSelect,
|
onSelect,
|
||||||
|
selected,
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
...rest
|
...rest
|
||||||
}: ButtonProps) => {
|
}: ButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={(event) => {
|
onClick={composeEventHandlers(rest.onClick, (event) => {
|
||||||
onSelect();
|
onSelect();
|
||||||
rest.onClick?.(event);
|
})}
|
||||||
}}
|
|
||||||
type={type}
|
type={type}
|
||||||
className={`excalidraw-button ${className}`}
|
className={clsx("excalidraw-button", className, { selected })}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,33 +1,59 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||||
export const ButtonIconSelect = <T extends Object>({
|
export const ButtonIconSelect = <T extends Object>(
|
||||||
options,
|
props: {
|
||||||
value,
|
options: {
|
||||||
onChange,
|
value: T;
|
||||||
group,
|
text: string;
|
||||||
}: {
|
icon: JSX.Element;
|
||||||
options: { value: T; text: string; icon: JSX.Element; testId?: string }[];
|
testId?: string;
|
||||||
value: T | null;
|
/** if not supplied, defaults to value identity check */
|
||||||
onChange: (value: T) => void;
|
active?: boolean;
|
||||||
group: string;
|
}[];
|
||||||
}) => (
|
value: T | null;
|
||||||
|
type?: "radio" | "button";
|
||||||
|
} & (
|
||||||
|
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
||||||
|
| {
|
||||||
|
type: "button";
|
||||||
|
onClick: (
|
||||||
|
value: T,
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) => (
|
||||||
<div className="buttonList buttonListIcon">
|
<div className="buttonList buttonListIcon">
|
||||||
{options.map((option) => (
|
{props.options.map((option) =>
|
||||||
<label
|
props.type === "button" ? (
|
||||||
key={option.text}
|
<button
|
||||||
className={clsx({ active: value === option.value })}
|
key={option.text}
|
||||||
title={option.text}
|
onClick={(event) => props.onClick(option.value, event)}
|
||||||
>
|
className={clsx({
|
||||||
<input
|
active: option.active ?? props.value === option.value,
|
||||||
type="radio"
|
})}
|
||||||
name={group}
|
|
||||||
onChange={() => onChange(option.value)}
|
|
||||||
checked={value === option.value}
|
|
||||||
data-testid={option.testId}
|
data-testid={option.testId}
|
||||||
/>
|
title={option.text}
|
||||||
{option.icon}
|
>
|
||||||
</label>
|
{option.icon}
|
||||||
))}
|
</button>
|
||||||
|
) : (
|
||||||
|
<label
|
||||||
|
key={option.text}
|
||||||
|
className={clsx({ active: props.value === option.value })}
|
||||||
|
title={option.text}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={props.group}
|
||||||
|
onChange={() => props.onChange(option.value)}
|
||||||
|
checked={props.value === option.value}
|
||||||
|
data-testid={option.testId}
|
||||||
|
/>
|
||||||
|
{option.icon}
|
||||||
|
</label>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
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