Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8946b2637f | |||
| a834a4fda0 | |||
| 7dbd0c5e0a | |||
| ba35eb8f8c | |||
| 163ad1f4c4 | |||
| 0f0244224d | |||
| 6eecadce60 | |||
| bc88cf5002 | |||
| 571be9c0fe | |||
| 5d925c7d3f | |||
| 45c520341f | |||
| c6ffc06541 | |||
| ff29780760 | |||
| 463857ad9a | |||
| be2da9539e | |||
| bb7829ef90 | |||
| 1104f6891e | |||
| a97e172070 | |||
| 39d45afc06 | |||
| 00c6940851 | |||
| 982cba2035 | |||
| 54739cd2df | |||
| 75aeaa6c38 | |||
| bea4a1e066 | |||
| e8b462cc31 | |||
| c86c176e10 | |||
| b09c11bb14 | |||
| 7199d13f48 | |||
| 7d1fddc144 | |||
| 5da3207633 | |||
| 8c9786e026 | |||
| f0f13ed694 | |||
| 850d8eb47e | |||
| f287f9c002 | |||
| 78df5bc852 | |||
| f0073c7e26 | |||
| fa7a313412 | |||
| 8b3f236cd8 | |||
| 621812d0eb | |||
| d607249205 | |||
| df28c3299f | |||
| b00a57b4be | |||
| 9277e839db | |||
| 0d5d60944f | |||
| 489a652d73 | |||
| 2b85d96121 | |||
| 6ce535d3a4 | |||
| da43cf5635 | |||
| 603ecfba34 | |||
| a589708737 | |||
| 4df401d012 | |||
| b2c4552416 | |||
| 5cae218f1b | |||
| 4be726d405 | |||
| 99623334d1 | |||
| 685abac81a | |||
| 9581c45522 | |||
| 0749d2c1f3 | |||
| 8787f3dc60 | |||
| 5fabc57277 | |||
| e7cbb859f0 | |||
| aa860251c7 | |||
| 380aaa30e6 | |||
| 2e61fec7a6 | |||
| 3c295559c7 | |||
| 55d3287abf | |||
| e3e967421e | |||
| 77aae63006 | |||
| ee64a7e264 | |||
| 097362662d | |||
| 038e9c13dd | |||
| bc8ba08ad0 | |||
| f861a9fdd0 | |||
| 62303b8a22 | |||
| 9cc741ab3a | |||
| 2d279cbb02 | |||
| 57ea4fdf9a | |||
| 44402f42bf | |||
| bdead4d164 | |||
| bfc0656475 | |||
| a33a3334f7 | |||
| 969d3c694a | |||
| 5cd921549a | |||
| 437afcbea4 | |||
| 6dee02e320 | |||
| 74a2f16501 | |||
| fd4460be37 | |||
| e82d0493cf | |||
| 083cb4c656 | |||
| d067365c1d | |||
| 273cac6b60 | |||
| b9337b8a36 | |||
| 0e0025921b | |||
| efc01ddab1 | |||
| 7bce22b114 | |||
| aab4965bbb | |||
| 486a9a3da8 | |||
| 2425c06082 | |||
| 79ea844901 | |||
| 6690215cd1 | |||
| 7f5e783fe8 | |||
| 9325109836 | |||
| 69b6fbb3f4 | |||
| 6b6002baae | |||
| 54dcb73701 | |||
| b595d3fcba | |||
| d0867d1c3b | |||
| 0d19e9210c | |||
| 4249de41d4 | |||
| 15f02ba191 | |||
| a2e1199907 | |||
| c08e9c4172 | |||
| abfc58eb24 | |||
| 035c7affff | |||
| c819b653bf | |||
| 60cea7a0c2 | |||
| d63b6a3469 | |||
| 0912fe1c93 | |||
| 360310de31 | |||
| 716c78e930 | |||
| ba48974351 | |||
| 6c3e4417e1 | |||
| bc0b6e1888 | |||
| 99a22e8445 | |||
| e6d9797167 | |||
| a1e8fdfb1b | |||
| 1cce63b07b | |||
| e9c2a09c21 | |||
| 55e0812680 | |||
| 0f32278a7e | |||
| 1bdb8da1c3 | |||
| 9c9787e0a0 | |||
| c2fe24c562 | |||
| 52faa52091 | |||
| dd12abc583 | |||
| abebf9aff8 | |||
| 790c9fd02e | |||
| 357266e9ab | |||
| 0bbb4535cf | |||
| d201d0be1b | |||
| 5662c5141d | |||
| 044614dcf3 | |||
| 9ec15989ab | |||
| 08aafcd248 | |||
| ea5602457f | |||
| 3c58d19d45 | |||
| fcfcdebc99 | |||
| aa97c074a7 | |||
| d65d2c5279 | |||
| 6d40039f08 | |||
| f4e10c93e1 | |||
| 82c6df0e1f | |||
| c37bd59ddd | |||
| 198a5e3b53 | |||
| a78e1fa99b | |||
| fc5db9248c | |||
| ebf64036fd | |||
| 6271a031a3 | |||
| 78da4c075e | |||
| f1cf28a84e | |||
| 3b9290831a | |||
| bec34f2d57 | |||
| 07839f8d20 | |||
| 8068d1f853 | |||
| 92c7d3257f | |||
| a8a5e7b6ff | |||
| 45a4a00b69 | |||
| 436e539d3a | |||
| ff19167063 | |||
| 3fc531ed6e | |||
| 6f55c00814 | |||
| a7eb6e1168 | |||
| 641bbdd2da | |||
| 42b0f7a614 | |||
| c11e3818ac | |||
| 4b6aa5c53b | |||
| ebd0408d7d | |||
| f4fefbcee8 | |||
| 11b8cc2caa | |||
| 6bebfe63be | |||
| 91ab7f36e2 | |||
| 5ee8e8249c | |||
| 49c6bdd520 | |||
| 198800136e | |||
| 178ee04d82 | |||
| 18cdafbcbe | |||
| 286e9a1524 | |||
| bac76778ce | |||
| f28f7ffb6e | |||
| 12e8cc853f | |||
| 81108bf580 | |||
| 23030a15f2 | |||
| 4ef7cb7365 | |||
| 5cc3f7db80 | |||
| 5c42cb5be4 | |||
| 004d3180b5 | |||
| c12119278a | |||
| 4d628844de | |||
| 946a209927 | |||
| 811437724b | |||
| 9dcde502aa | |||
| d3106495b2 | |||
| 891ac82447 | |||
| 354976e08e | |||
| 5c73c5813c | |||
| 3a0b6fb41b | |||
| 37d513ad59 | |||
| 46624cc953 | |||
| 0d23c8dd76 | |||
| 51ef4cd97b | |||
| b558d19d37 | |||
| b8fb6580ef | |||
| 6730eb41c2 | |||
| 87c42cb327 | |||
| 8cfd05aa95 | |||
| 3ed8271344 | |||
| 73515b5a83 | |||
| 63d3da9a54 | |||
| 215fb5e357 | |||
| 886177816b | |||
| 7d29351d66 | |||
| c0047269c1 | |||
| 793b69e592 | |||
| e0a449aa40 | |||
| d5a270f643 | |||
| d126d04d17 | |||
| 153ca6a7c6 | |||
| 2618ac9f6e | |||
| f64fd80493 | |||
| a884351137 | |||
| e546a85a8d | |||
| 29e630086c | |||
| a82165cb50 | |||
| 4dc0159a05 | |||
| 458787d1d7 | |||
| 815977296e | |||
| 58f840aa93 | |||
| 422149c249 | |||
| a7cbe68ae8 | |||
| c19c8ecd27 | |||
| d91950bd03 | |||
| 89472c14ed | |||
| 09dfd16b17 | |||
| 016e69b9f2 | |||
| bb1f979718 | |||
| 5fda8400f3 | |||
| 96beaa4354 | |||
| 7183f1c83e | |||
| 24ae9dca2e | |||
| f6ac3ea7c6 | |||
| b88e0253cc |
@@ -5,3 +5,4 @@ package-lock.json
|
|||||||
firebase/
|
firebase/
|
||||||
dist/
|
dist/
|
||||||
public/workbox
|
public/workbox
|
||||||
|
src/packages/excalidraw/types
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Auto release @excalidraw/excalidraw-next
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Auto-release-excalidraw-next:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: Setup Node.js 14.x
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 14.x
|
||||||
|
- name: Set up publish access
|
||||||
|
run: |
|
||||||
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
- name: Auto release
|
||||||
|
run: |
|
||||||
|
yarn autorelease
|
||||||
@@ -5,9 +5,11 @@
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.envrc
|
.envrc
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.history
|
||||||
.idea
|
.idea
|
||||||
.vercel
|
.vercel
|
||||||
.vscode
|
.vscode
|
||||||
|
.yarn
|
||||||
*.log
|
*.log
|
||||||
*.tgz
|
*.tgz
|
||||||
build
|
build
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ ARG NODE_ENV=production
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN yarn build:app:docker
|
RUN yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.17-alpine
|
FROM nginx:1.21-alpine
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ The first set of digits is the room. This is visible from the server that’s go
|
|||||||
|
|
||||||
The second set of digits is the encryption key. The Excalidraw server doesn’t know about it. This is what all the participants use to encrypt/decrypt the messages.
|
The second set of digits is the encryption key. The Excalidraw server doesn’t know about it. This is what all the participants use to encrypt/decrypt the messages.
|
||||||
|
|
||||||
|
> Note: Please ensure that the encryption key is 22 characters long.
|
||||||
|
|
||||||
## Shape libraries
|
## Shape libraries
|
||||||
|
|
||||||
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
||||||
@@ -93,7 +95,7 @@ These instructions will get you a copy of the project up and running on your loc
|
|||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/)
|
- [Node.js](https://nodejs.org/en/)
|
||||||
- [Yarn](https://yarnpkg.com/getting-started/install)
|
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
#### Clone the repo
|
#### Clone the repo
|
||||||
@@ -102,6 +104,20 @@ These instructions will get you a copy of the project up and running on your loc
|
|||||||
git clone https://github.com/excalidraw/excalidraw.git
|
git clone https://github.com/excalidraw/excalidraw.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Start the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
|
||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
@@ -2,5 +2,8 @@
|
|||||||
"firestore": {
|
"firestore": {
|
||||||
"rules": "firestore.rules",
|
"rules": "firestore.rules",
|
||||||
"indexes": "firestore.indexes.json"
|
"indexes": "firestore.indexes.json"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"rules": "storage.rules"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
service firebase.storage {
|
||||||
|
match /b/{bucket}/o {
|
||||||
|
match /{migrations} {
|
||||||
|
match /{scenes}/{scene} {
|
||||||
|
allow get, write: if true;
|
||||||
|
// redundant, but let's be explicit'
|
||||||
|
allow list: if false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-14
@@ -19,22 +19,28 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "6.2.2",
|
"@dwelle/browser-fs-access": "0.21.3",
|
||||||
"@sentry/integrations": "6.2.1",
|
"@sentry/browser": "6.2.5",
|
||||||
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.11.10",
|
"@testing-library/jest-dom": "5.11.10",
|
||||||
"@testing-library/react": "11.2.5",
|
"@testing-library/react": "11.2.6",
|
||||||
|
"@tldraw/vec": "0.0.106",
|
||||||
"@types/jest": "26.0.22",
|
"@types/jest": "26.0.22",
|
||||||
|
"@types/pica": "5.1.3",
|
||||||
"@types/react": "17.0.3",
|
"@types/react": "17.0.3",
|
||||||
"@types/react-dom": "17.0.2",
|
"@types/react-dom": "17.0.3",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.16.2",
|
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.2.10",
|
"fake-indexeddb": "3.1.3",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"firebase": "8.3.3",
|
||||||
|
"i18next-browser-languagedetector": "6.1.0",
|
||||||
|
"idb-keyval": "5.1.3",
|
||||||
|
"image-blob-reduce": "3.0.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.1.22",
|
"nanoid": "3.1.22",
|
||||||
"open-color": "1.8.0",
|
"open-color": "1.8.0",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
"perfect-freehand": "1.0.15",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
@@ -43,10 +49,10 @@
|
|||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"roughjs": "4.3.1",
|
"roughjs": "4.4.1",
|
||||||
"sass": "1.32.8",
|
"sass": "1.32.10",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"typescript": "4.2.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@excalidraw/eslint-config": "1.0.0",
|
"@excalidraw/eslint-config": "1.0.0",
|
||||||
@@ -54,9 +60,9 @@
|
|||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.6",
|
||||||
"@types/pako": "1.0.1",
|
"@types/pako": "1.0.1",
|
||||||
"@types/resize-observer-browser": "0.1.5",
|
"@types/resize-observer-browser": "0.1.5",
|
||||||
"eslint-config-prettier": "8.1.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"firebase-tools": "9.6.1",
|
"firebase-tools": "9.9.0",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"lint-staged": "10.5.4",
|
"lint-staged": "10.5.4",
|
||||||
@@ -75,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|@dwelle/browser-fs-access)/)"
|
||||||
],
|
],
|
||||||
"resetMocks": false
|
"resetMocks": false
|
||||||
},
|
},
|
||||||
@@ -103,6 +109,7 @@
|
|||||||
"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 --updateSnapshot --watchAll=false",
|
||||||
"test": "yarn test:app"
|
"test": "yarn test:app",
|
||||||
|
"autorelease": "node scripts/autorelease.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+33
-3
@@ -13,6 +13,18 @@
|
|||||||
|
|
||||||
<meta name="theme-color" content="#000" />
|
<meta name="theme-color" content="#000" />
|
||||||
|
|
||||||
|
<!-- Declarative Link Capturing (https://web.dev/declarative-link-capturing/) -->
|
||||||
|
<meta
|
||||||
|
http-equiv="origin-trial"
|
||||||
|
content="Ak3VyzTheARtX2CnxBZ3ZKxImB0mNyvDakmMxeAChgxrWFMZ3IGN64VP+uj36VxM5OegsbLmrP258b1xvqp7+Q8AAABbeyJvcmlnaW4iOiJodHRwczovL2V4Y2FsaWRyYXcuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJBcHBMaW5rQ2FwdHVyaW5nIiwiZXhwaXJ5IjoxNjM0MDgzMTk5fQ=="
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- File Handling (https://web.dev/file-handling/) -->
|
||||||
|
<meta
|
||||||
|
http-equiv="origin-trial"
|
||||||
|
content="AkMQsAnFmKfRfPKQHNCv2WmZREqgwkqhyt2M7aOwQiCStB+hPYnGnv+mNbkPDAsGXrwsj/waFi76wPzTDUaEeQ0AAABUeyJvcmlnaW4iOiJodHRwczovL2V4Y2FsaWRyYXcuY29tOjQ0MyIsImZlYXR1cmUiOiJGaWxlSGFuZGxpbmciLCJleHBpcnkiOjE2MzQwODMxOTl9"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- General tags -->
|
<!-- General tags -->
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
@@ -107,15 +119,17 @@
|
|||||||
|
|
||||||
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
||||||
<style>
|
<style>
|
||||||
body {
|
body,
|
||||||
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
--ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
--ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
||||||
Roboto, Helvetica, Arial, sans-serif;
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
@@ -125,6 +139,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
white-space: nowrap; /* added line */
|
white-space: nowrap; /* added line */
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoadingMessage {
|
.LoadingMessage {
|
||||||
@@ -149,6 +164,21 @@
|
|||||||
}
|
}
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1200px) {
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
-webkit-user-select: auto;
|
||||||
|
-khtml-user-select: auto;
|
||||||
|
-moz-user-select: auto;
|
||||||
|
-ms-user-select: auto;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"capture_links": "new_client",
|
"capture_links": "new-client",
|
||||||
"share_target": {
|
"share_target": {
|
||||||
"action": "/web-share-target",
|
"action": "/web-share-target",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { exec, execSync } = require("child_process");
|
||||||
|
|
||||||
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
|
const pkg = require(excalidrawPackage);
|
||||||
|
|
||||||
|
const getShortCommitHash = () => {
|
||||||
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish = () => {
|
||||||
|
try {
|
||||||
|
execSync(`yarn --frozen-lockfile`);
|
||||||
|
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||||
|
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
||||||
|
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get files changed between prev and head commit
|
||||||
|
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||||
|
if (error || stderr) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedFiles = stdout.trim().split("\n");
|
||||||
|
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/;
|
||||||
|
|
||||||
|
const excalidrawPackageFiles = changedFiles.filter((file) => {
|
||||||
|
return (
|
||||||
|
(file.indexOf("src") >= 0 || file.indexOf("package.json")) >= 0 &&
|
||||||
|
!filesToIgnoreRegex.test(file)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (!excalidrawPackageFiles.length) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update package.json
|
||||||
|
pkg.version = `${pkg.version}-${getShortCommitHash()}`;
|
||||||
|
pkg.name = "@excalidraw/excalidraw-next";
|
||||||
|
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
|
||||||
|
|
||||||
|
// update readme
|
||||||
|
const data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||||
|
publish();
|
||||||
|
});
|
||||||
@@ -37,6 +37,9 @@ const crowdinMap = {
|
|||||||
"uk-UA": "en-uk",
|
"uk-UA": "en-uk",
|
||||||
"zh-CN": "en-zhcn",
|
"zh-CN": "en-zhcn",
|
||||||
"zh-TW": "en-zhtw",
|
"zh-TW": "en-zhtw",
|
||||||
|
"lv-LV": "en-lv",
|
||||||
|
"cs-CZ": "en-cs",
|
||||||
|
"kk-KZ": "en-kk",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -74,6 +77,9 @@ const flags = {
|
|||||||
"uk-UA": "🇺🇦",
|
"uk-UA": "🇺🇦",
|
||||||
"zh-CN": "🇨🇳",
|
"zh-CN": "🇨🇳",
|
||||||
"zh-TW": "🇹🇼",
|
"zh-TW": "🇹🇼",
|
||||||
|
"lv-LV": "🇱🇻",
|
||||||
|
"cs-CZ": "🇨🇿",
|
||||||
|
"kk-KZ": "🇰🇿",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@@ -111,6 +117,9 @@ const languages = {
|
|||||||
"uk-UA": "Українська",
|
"uk-UA": "Українська",
|
||||||
"zh-CN": "简体中文",
|
"zh-CN": "简体中文",
|
||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
|
"lv-LV": "Latviešu",
|
||||||
|
"cs-CZ": "Česky",
|
||||||
|
"kk-KZ": "Қазақ тілі",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const util = require("util");
|
||||||
|
const exec = util.promisify(require("child_process").exec);
|
||||||
|
const updateReadme = require("./updateReadme");
|
||||||
|
const updateChangelog = require("./updateChangelog");
|
||||||
|
|
||||||
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
|
|
||||||
|
const updatePackageVersion = (nextVersion) => {
|
||||||
|
const pkg = require(excalidrawPackage);
|
||||||
|
pkg.version = nextVersion;
|
||||||
|
const content = `${JSON.stringify(pkg, null, 2)}\n`;
|
||||||
|
fs.writeFileSync(excalidrawPackage, content, "utf-8");
|
||||||
|
};
|
||||||
|
|
||||||
|
const release = async (nextVersion) => {
|
||||||
|
try {
|
||||||
|
updateReadme();
|
||||||
|
await updateChangelog(nextVersion);
|
||||||
|
updatePackageVersion(nextVersion);
|
||||||
|
await exec(`git add -u`);
|
||||||
|
await exec(
|
||||||
|
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||||
|
);
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.log("Done!");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextVersion = process.argv.slice(2)[0];
|
||||||
|
if (!nextVersion) {
|
||||||
|
console.error("Pass the next version to release!");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
release(nextVersion);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const util = require("util");
|
||||||
|
const exec = util.promisify(require("child_process").exec);
|
||||||
|
|
||||||
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
|
const pkg = require(excalidrawPackage);
|
||||||
|
const lastVersion = pkg.version;
|
||||||
|
const existingChangeLog = fs.readFileSync(
|
||||||
|
`${excalidrawDir}/CHANGELOG.md`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const supportedTypes = ["feat", "fix", "style", "refactor", "perf", "build"];
|
||||||
|
const headerForType = {
|
||||||
|
feat: "Features",
|
||||||
|
fix: "Fixes",
|
||||||
|
style: "Styles",
|
||||||
|
refactor: " Refactor",
|
||||||
|
perf: "Performance",
|
||||||
|
build: "Build",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommitHashForLastVersion = async () => {
|
||||||
|
try {
|
||||||
|
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
|
||||||
|
const { stdout } = await exec(
|
||||||
|
`git log --format=format:"%H" --grep=${commitMessage}`,
|
||||||
|
);
|
||||||
|
return stdout;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLibraryCommitsSinceLastRelease = async () => {
|
||||||
|
const commitHash = await getCommitHashForLastVersion();
|
||||||
|
const { stdout } = await exec(
|
||||||
|
`git log --pretty=format:%s ${commitHash}...master`,
|
||||||
|
);
|
||||||
|
const commitsSinceLastRelease = stdout.split("\n");
|
||||||
|
const commitList = {};
|
||||||
|
supportedTypes.forEach((type) => {
|
||||||
|
commitList[type] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
commitsSinceLastRelease.forEach((commit) => {
|
||||||
|
const indexOfColon = commit.indexOf(":");
|
||||||
|
const type = commit.slice(0, indexOfColon);
|
||||||
|
if (!supportedTypes.includes(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
|
||||||
|
const messageWithCapitalizeFirst =
|
||||||
|
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
|
||||||
|
const prNumber = commit.match(/\(#([0-9]*)\)/)[1];
|
||||||
|
|
||||||
|
// return if the changelog already contains the pr number which would happen for package updates
|
||||||
|
if (existingChangeLog.includes(prNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
|
||||||
|
const messageWithPRLink = messageWithCapitalizeFirst.replace(
|
||||||
|
/\(#[0-9]*\)/,
|
||||||
|
prMarkdown,
|
||||||
|
);
|
||||||
|
commitList[type].push(messageWithPRLink);
|
||||||
|
});
|
||||||
|
return commitList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChangelog = async (nextVersion) => {
|
||||||
|
const commitList = await getLibraryCommitsSinceLastRelease();
|
||||||
|
let changelogForLibrary =
|
||||||
|
"## Excalidraw Library\n\n**_This section lists the updates made to the excalidraw library and will not affect the integration._**\n\n";
|
||||||
|
supportedTypes.forEach((type) => {
|
||||||
|
if (commitList[type].length) {
|
||||||
|
changelogForLibrary += `### ${headerForType[type]}\n\n`;
|
||||||
|
const commits = commitList[type];
|
||||||
|
commits.forEach((commit) => {
|
||||||
|
changelogForLibrary += `- ${commit}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
changelogForLibrary += "---\n";
|
||||||
|
const lastVersionIndex = existingChangeLog.indexOf(`## ${lastVersion}`);
|
||||||
|
let updatedContent =
|
||||||
|
existingChangeLog.slice(0, lastVersionIndex) +
|
||||||
|
changelogForLibrary +
|
||||||
|
existingChangeLog.slice(lastVersionIndex);
|
||||||
|
const currentDate = new Date().toISOString().slice(0, 10);
|
||||||
|
const newVersion = `## ${nextVersion} (${currentDate})`;
|
||||||
|
updatedContent = updatedContent.replace(`## Unreleased`, newVersion);
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/CHANGELOG.md`, updatedContent, "utf8");
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = updateChangelog;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const updateReadme = () => {
|
||||||
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
|
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
||||||
|
|
||||||
|
// remove note for unstable release
|
||||||
|
data = data.replace(
|
||||||
|
/<!-- unstable-readme-start-->[\s\S]*?<!-- unstable-readme-end-->/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
// replace "excalidraw-next" with "excalidraw"
|
||||||
|
data = data.replace(/excalidraw-next/g, "excalidraw");
|
||||||
|
data = data.trim();
|
||||||
|
|
||||||
|
const demoIndex = data.indexOf("### Demo");
|
||||||
|
const excalidrawNextNote =
|
||||||
|
"#### Note\n\n**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**\n\n";
|
||||||
|
// Add excalidraw next note to try out for unreleased changes
|
||||||
|
data = data.slice(0, demoIndex) + excalidrawNextNote + data.slice(demoIndex);
|
||||||
|
|
||||||
|
// update readme
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = updateReadme;
|
||||||
@@ -2,18 +2,20 @@ import { register } from "./register";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { Library } from "../data/library";
|
|
||||||
|
|
||||||
export const actionAddToLibrary = register({
|
export const actionAddToLibrary = register({
|
||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
Library.loadLibrary().then((items) => {
|
app.library.loadLibrary().then((items) => {
|
||||||
Library.saveLibrary([...items, selectedElements.map(deepCopyElement)]);
|
app.library.saveLibrary([
|
||||||
|
...items,
|
||||||
|
selectedElements.map(deepCopyElement),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { alignElements, Alignment } from "../align";
|
import { alignElements, Alignment } from "../align";
|
||||||
import {
|
import {
|
||||||
AlignBottomIcon,
|
AlignBottomIcon,
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import { getDefaultAppState } from "../appState";
|
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
import { zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { ZOOM_STEP } from "../constants";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
|
import { THEME, ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
@@ -16,13 +13,17 @@ import { getNewZoom } from "../scene/zoom";
|
|||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { Tooltip } from "../components/Tooltip";
|
||||||
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import ClearCanvas from "../components/ClearCanvas";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
perform: (_, appState, value) => {
|
perform: (_, appState, value) => {
|
||||||
return {
|
return {
|
||||||
appState: { ...appState, viewBackgroundColor: value },
|
appState: { ...appState, ...value },
|
||||||
commitToHistory: true,
|
commitToHistory: !!value.viewBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => {
|
PanelComponent: ({ appState, updateData }) => {
|
||||||
@@ -32,7 +33,11 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
label={t("labels.canvasBackground")}
|
label={t("labels.canvasBackground")}
|
||||||
type="canvasBackground"
|
type="canvasBackground"
|
||||||
color={appState.viewBackgroundColor}
|
color={appState.viewBackgroundColor}
|
||||||
onChange={(color) => updateData(color)}
|
onChange={(color) => updateData({ viewBackgroundColor: color })}
|
||||||
|
isActive={appState.openPopup === "canvasColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openPopup: active ? "canvasColorPicker" : null })
|
||||||
|
}
|
||||||
data-testid="canvas-background-picker"
|
data-testid="canvas-background-picker"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,40 +47,28 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
|
|
||||||
export const actionClearCanvas = register({
|
export const actionClearCanvas = register({
|
||||||
name: "clearCanvas",
|
name: "clearCanvas",
|
||||||
perform: (elements, appState: AppState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
|
app.imageCache.clear();
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) =>
|
elements: elements.map((element) =>
|
||||||
newElementWith(element, { isDeleted: true }),
|
newElementWith(element, { isDeleted: true }),
|
||||||
),
|
),
|
||||||
appState: {
|
appState: {
|
||||||
...getDefaultAppState(),
|
...getDefaultAppState(),
|
||||||
|
files: {},
|
||||||
theme: appState.theme,
|
theme: appState.theme,
|
||||||
elementLocked: appState.elementLocked,
|
elementLocked: appState.elementLocked,
|
||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
exportEmbedScene: appState.exportEmbedScene,
|
exportEmbedScene: appState.exportEmbedScene,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize,
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
|
||||||
showStats: appState.showStats,
|
showStats: appState.showStats,
|
||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ updateData }) => (
|
|
||||||
<ToolButton
|
PanelComponent: ({ updateData }) => <ClearCanvas onConfirm={updateData} />,
|
||||||
type="button"
|
|
||||||
icon={trash}
|
|
||||||
title={t("buttons.clearReset")}
|
|
||||||
aria-label={t("buttons.clearReset")}
|
|
||||||
showAriaLabel={useIsMobile()}
|
|
||||||
onClick={() => {
|
|
||||||
if (window.confirm(t("alerts.clearReset"))) {
|
|
||||||
updateData(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
data-testid="clear-canvas-button"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionZoomIn = register({
|
export const actionZoomIn = register({
|
||||||
@@ -104,6 +97,7 @@ export const actionZoomIn = register({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@@ -138,6 +132,7 @@ export const actionZoomOut = register({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@@ -164,16 +159,21 @@ export const actionResetZoom = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, appState }) => (
|
||||||
|
<Tooltip label={t("buttons.resetZoom")}>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={resetZoom}
|
className="reset-zoom-button"
|
||||||
title={t("buttons.resetZoom")}
|
title={t("buttons.resetZoom")}
|
||||||
aria-label={t("buttons.resetZoom")}
|
aria-label={t("buttons.resetZoom")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
/>
|
size="small"
|
||||||
|
>
|
||||||
|
{(appState.zoom.value * 100).toFixed(0)}%
|
||||||
|
</ToolButton>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
(event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) &&
|
(event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) &&
|
||||||
@@ -260,3 +260,28 @@ export const actionZoomToFit = register({
|
|||||||
!event.altKey &&
|
!event.altKey &&
|
||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionToggleTheme = register({
|
||||||
|
name: "toggleTheme",
|
||||||
|
perform: (_, appState, value) => {
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
theme:
|
||||||
|
value || (appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT),
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
|
<div style={{ marginInlineStart: "0.25rem" }}>
|
||||||
|
<DarkModeToggle
|
||||||
|
value={appState.theme}
|
||||||
|
onChange={(theme) => {
|
||||||
|
updateData(theme);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { t } from "../i18n";
|
|||||||
|
|
||||||
export const actionCopy = register({
|
export const actionCopy = register({
|
||||||
name: "copy",
|
name: "copy",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
copyToClipboard(getNonDeletedElements(elements), appState);
|
copyToClipboard(getNonDeletedElements(elements), appState, app.files);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
@@ -50,7 +50,7 @@ export const actionCopyAsSvg = register({
|
|||||||
? selectedElements
|
? selectedElements
|
||||||
: getNonDeletedElements(elements),
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.canvas,
|
app.files,
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -89,7 +89,7 @@ export const actionCopyAsPng = register({
|
|||||||
? selectedElements
|
? selectedElements
|
||||||
: getNonDeletedElements(elements),
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.canvas,
|
app.files,
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import React from "react";
|
|
||||||
import { trash } from "../components/icons";
|
import { trash } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
DistributeHorizontallyIcon,
|
DistributeHorizontallyIcon,
|
||||||
DistributeVerticallyIcon,
|
DistributeVerticallyIcon,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
|||||||
+102
-68
@@ -1,17 +1,25 @@
|
|||||||
import React from "react";
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { load, questionCircle, save, saveAs } from "../components/icons";
|
import { load, questionCircle, saveAs } from "../components/icons";
|
||||||
import { ProjectName } from "../components/ProjectName";
|
import { ProjectName } from "../components/ProjectName";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import "../components/ToolIcon.scss";
|
import "../components/ToolIcon.scss";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
|
import { resaveAsImageWithScene } from "../data/resave";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { supported } from "browser-fs-access";
|
import { CheckboxItem } from "../components/CheckboxItem";
|
||||||
|
import { getExportSize } from "../scene/export";
|
||||||
|
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
|
||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { ActiveFile } from "../components/ActiveFile";
|
||||||
|
import { isImageFileHandle } from "../data/blob";
|
||||||
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@@ -31,6 +39,54 @@ export const actionChangeProjectName = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionChangeExportScale = register({
|
||||||
|
name: "changeExportScale",
|
||||||
|
perform: (_elements, appState, value) => {
|
||||||
|
return {
|
||||||
|
appState: { ...appState, exportScale: value },
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements: allElements, appState, updateData }) => {
|
||||||
|
const elements = getNonDeletedElements(allElements);
|
||||||
|
const exportSelected = isSomeElementSelected(elements, appState);
|
||||||
|
const exportedElements = exportSelected
|
||||||
|
? getSelectedElements(elements, appState)
|
||||||
|
: elements;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{EXPORT_SCALES.map((s) => {
|
||||||
|
const [width, height] = getExportSize(
|
||||||
|
exportedElements,
|
||||||
|
DEFAULT_EXPORT_PADDING,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
|
||||||
|
const scaleButtonTitle = `${t(
|
||||||
|
"buttons.scale",
|
||||||
|
)} ${s}x (${width}x${height})`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolButton
|
||||||
|
key={s}
|
||||||
|
size="small"
|
||||||
|
type="radio"
|
||||||
|
icon={`${s}x`}
|
||||||
|
name="export-canvas-scale"
|
||||||
|
title={scaleButtonTitle}
|
||||||
|
aria-label={scaleButtonTitle}
|
||||||
|
id="export-canvas-scale"
|
||||||
|
checked={s === appState.exportScale}
|
||||||
|
onChange={() => updateData(s)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const actionChangeExportBackground = register({
|
export const actionChangeExportBackground = register({
|
||||||
name: "changeExportBackground",
|
name: "changeExportBackground",
|
||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value) => {
|
||||||
@@ -40,14 +96,12 @@ export const actionChangeExportBackground = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<label>
|
<CheckboxItem
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={appState.exportBackground}
|
checked={appState.exportBackground}
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
/>{" "}
|
>
|
||||||
{t("labels.withBackground")}
|
{t("labels.withBackground")}
|
||||||
</label>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,57 +114,35 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<label style={{ display: "flex" }}>
|
<CheckboxItem
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={appState.exportEmbedScene}
|
checked={appState.exportEmbedScene}
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
/>{" "}
|
|
||||||
{t("labels.exportEmbedScene")}
|
|
||||||
<Tooltip
|
|
||||||
label={t("labels.exportEmbedScene_details")}
|
|
||||||
position="above"
|
|
||||||
long={true}
|
|
||||||
>
|
>
|
||||||
<div className="TooltipIcon">{questionCircle}</div>
|
{t("labels.exportEmbedScene")}
|
||||||
|
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||||
|
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeShouldAddWatermark = register({
|
export const actionSaveToActiveFile = register({
|
||||||
name: "changeShouldAddWatermark",
|
name: "saveToActiveFile",
|
||||||
perform: (_elements, appState, value) => {
|
perform: async (elements, appState, value, app) => {
|
||||||
return {
|
|
||||||
appState: { ...appState, shouldAddWatermark: value },
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={appState.shouldAddWatermark}
|
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.addWatermark")}
|
|
||||||
</label>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionSaveScene = register({
|
|
||||||
name: "saveScene",
|
|
||||||
perform: async (elements, appState, value) => {
|
|
||||||
const fileHandleExists = !!appState.fileHandle;
|
const fileHandleExists = !!appState.fileHandle;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, appState);
|
const { fileHandle } = isImageFileHandle(appState.fileHandle)
|
||||||
|
? await resaveAsImageWithScene(elements, appState, app.files)
|
||||||
|
: await saveAsJSON(elements, appState, app.files);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
fileHandle,
|
fileHandle,
|
||||||
toastMessage: fileHandleExists
|
toastMessage: fileHandleExists
|
||||||
? fileHandle.name
|
? fileHandle?.name
|
||||||
? t("toast.fileSavedToFilename").replace(
|
? t("toast.fileSavedToFilename").replace(
|
||||||
"{filename}",
|
"{filename}",
|
||||||
`"${fileHandle.name}"`,
|
`"${fileHandle.name}"`,
|
||||||
@@ -128,27 +160,26 @@ export const actionSaveScene = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, appState }) => (
|
||||||
<ToolButton
|
<ActiveFile
|
||||||
type="button"
|
onSave={() => updateData(null)}
|
||||||
icon={save}
|
fileName={appState.fileHandle?.name}
|
||||||
title={t("buttons.save")}
|
|
||||||
aria-label={t("buttons.save")}
|
|
||||||
showAriaLabel={useIsMobile()}
|
|
||||||
onClick={() => updateData(null)}
|
|
||||||
data-testid="save-button"
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSaveAsScene = register({
|
export const actionSaveFileToDisk = register({
|
||||||
name: "saveAsScene",
|
name: "saveFileToDisk",
|
||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value, app) => {
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, {
|
const { fileHandle } = await saveAsJSON(
|
||||||
|
elements,
|
||||||
|
{
|
||||||
...appState,
|
...appState,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
});
|
},
|
||||||
|
app.files,
|
||||||
|
);
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
@@ -166,7 +197,7 @@ export const actionSaveAsScene = register({
|
|||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
hidden={!supported}
|
hidden={!nativeFileSystemSupported}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-as-button"
|
data-testid="save-as-button"
|
||||||
/>
|
/>
|
||||||
@@ -175,15 +206,17 @@ export const actionSaveAsScene = register({
|
|||||||
|
|
||||||
export const actionLoadScene = register({
|
export const actionLoadScene = register({
|
||||||
name: "loadScene",
|
name: "loadScene",
|
||||||
perform: async (elements, appState) => {
|
perform: async (elements, appState, _, app) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
elements: loadedElements,
|
elements: loadedElements,
|
||||||
appState: loadedAppState,
|
appState: loadedAppState,
|
||||||
} = await loadFromJSON(appState);
|
files,
|
||||||
|
} = await loadFromJSON(appState, elements);
|
||||||
return {
|
return {
|
||||||
elements: loadedElements,
|
elements: loadedElements,
|
||||||
appState: loadedAppState,
|
appState: loadedAppState,
|
||||||
|
files,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -193,6 +226,7 @@ export const actionLoadScene = register({
|
|||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
appState: { ...appState, errorMessage: error.message },
|
appState: { ...appState, errorMessage: error.message },
|
||||||
|
files: app.files,
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -229,9 +263,9 @@ export const actionExportWithDarkMode = register({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DarkModeToggle
|
<DarkModeToggle
|
||||||
value={appState.exportWithDarkMode ? "dark" : "light"}
|
value={appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT}
|
||||||
onChange={(theme: Appearence) => {
|
onChange={(theme: Theme) => {
|
||||||
updateData(theme === "dark");
|
updateData(theme === THEME.DARK);
|
||||||
}}
|
}}
|
||||||
title={t("labels.toggleExportColorScheme")}
|
title={t("labels.toggleExportColorScheme")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { isInvisiblySmallElement } from "../element";
|
import { isInvisiblySmallElement } from "../element";
|
||||||
import { resetCursor } from "../utils";
|
import { resetCursor } from "../utils";
|
||||||
import React from "react";
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { done } from "../components/icons";
|
import { done } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -18,7 +17,7 @@ import { isBindingElement } from "../element/typeChecks";
|
|||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
perform: (elements, appState, _, { canvas }) => {
|
perform: (elements, appState, _, { canvas, focusContainer }) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const {
|
const {
|
||||||
elementId,
|
elementId,
|
||||||
@@ -50,20 +49,25 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
|
|
||||||
|
if (appState.pendingImageElement) {
|
||||||
|
mutateElement(appState.pendingImageElement, { isDeleted: true }, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
window.document.activeElement.blur();
|
focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
const multiPointElement = appState.multiElement
|
||||||
? appState.multiElement
|
? appState.multiElement
|
||||||
: appState.editingElement?.type === "draw"
|
: appState.editingElement?.type === "freedraw"
|
||||||
? appState.editingElement
|
? appState.editingElement
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (multiPointElement) {
|
if (multiPointElement) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (
|
if (
|
||||||
multiPointElement.type !== "draw" &&
|
multiPointElement.type !== "freedraw" &&
|
||||||
appState.lastPointerDownWith !== "touch"
|
appState.lastPointerDownWith !== "touch"
|
||||||
) {
|
) {
|
||||||
const { points, lastCommittedPoint } = multiPointElement;
|
const { points, lastCommittedPoint } = multiPointElement;
|
||||||
@@ -86,7 +90,7 @@ export const actionFinalize = register({
|
|||||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
||||||
if (
|
if (
|
||||||
multiPointElement.type === "line" ||
|
multiPointElement.type === "line" ||
|
||||||
multiPointElement.type === "draw"
|
multiPointElement.type === "freedraw"
|
||||||
) {
|
) {
|
||||||
if (isLoop) {
|
if (isLoop) {
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = multiPointElement.points;
|
||||||
@@ -118,22 +122,24 @@ export const actionFinalize = register({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appState.elementLocked && appState.elementType !== "draw") {
|
if (!appState.elementLocked && appState.elementType !== "freedraw") {
|
||||||
appState.selectedElementIds[multiPointElement.id] = true;
|
appState.selectedElementIds[multiPointElement.id] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!appState.elementLocked && appState.elementType !== "draw") ||
|
(!appState.elementLocked && appState.elementType !== "freedraw") ||
|
||||||
!multiPointElement
|
!multiPointElement
|
||||||
) {
|
) {
|
||||||
resetCursor(canvas);
|
resetCursor(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
elementType:
|
elementType:
|
||||||
(appState.elementLocked || appState.elementType === "draw") &&
|
(appState.elementLocked || appState.elementType === "freedraw") &&
|
||||||
multiPointElement
|
multiPointElement
|
||||||
? appState.elementType
|
? appState.elementType
|
||||||
: "selection",
|
: "selection",
|
||||||
@@ -145,14 +151,15 @@ export const actionFinalize = register({
|
|||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
multiPointElement &&
|
multiPointElement &&
|
||||||
!appState.elementLocked &&
|
!appState.elementLocked &&
|
||||||
appState.elementType !== "draw"
|
appState.elementType !== "freedraw"
|
||||||
? {
|
? {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
[multiPointElement.id]: true,
|
[multiPointElement.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
|
pendingImageElement: null,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.elementType === "draw",
|
commitToHistory: appState.elementType === "freedraw",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event, appState) =>
|
keyTest: (event, appState) =>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ExcalidrawElement, NonDeleted } from "../element/types";
|
|||||||
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
import { getTransformHandles } from "../element/transformHandles";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { updateBoundElements } from "../element/binding";
|
import { updateBoundElements } from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@@ -93,13 +93,13 @@ const flipElements = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
for (let i = 0; i < elements.length; i++) {
|
elements.forEach((element) => {
|
||||||
flipElement(elements[i], appState);
|
flipElement(element, appState);
|
||||||
// If vertical flip, rotate an extra 180
|
// If vertical flip, rotate an extra 180
|
||||||
if (flipDirection === "vertical") {
|
if (flipDirection === "vertical") {
|
||||||
rotateElement(elements[i], Math.PI);
|
rotateElement(element, Math.PI);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return elements;
|
return elements;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ const flipElement = (
|
|||||||
const originalAngle = normalizeAngle(element.angle);
|
const originalAngle = normalizeAngle(element.angle);
|
||||||
|
|
||||||
let finalOffsetX = 0;
|
let finalOffsetX = 0;
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
finalOffsetX =
|
finalOffsetX =
|
||||||
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
||||||
element.width;
|
element.width;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Action, ActionResult } from "./types";
|
import { Action, ActionResult } from "./types";
|
||||||
import React from "react";
|
|
||||||
import { undo, redo } from "../components/icons";
|
import { undo, redo } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { SceneHistory, HistoryEntry } from "../history";
|
import History, { HistoryEntry } from "../history";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isWindows, KEYS } from "../keys";
|
import { isWindows, KEYS } from "../keys";
|
||||||
@@ -59,7 +58,7 @@ const writeData = (
|
|||||||
return { commitToHistory };
|
return { commitToHistory };
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActionCreator = (history: SceneHistory) => Action;
|
type ActionCreator = (history: History) => Action;
|
||||||
|
|
||||||
export const createUndoAction: ActionCreator = (history) => ({
|
export const createUndoAction: ActionCreator = (history) => ({
|
||||||
name: "undo",
|
name: "undo",
|
||||||
@@ -69,12 +68,13 @@ export const createUndoAction: ActionCreator = (history) => ({
|
|||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.key.toLowerCase() === KEYS.Z &&
|
event.key.toLowerCase() === KEYS.Z &&
|
||||||
!event.shiftKey,
|
!event.shiftKey,
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, data }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={undo}
|
icon={undo}
|
||||||
aria-label={t("buttons.undo")}
|
aria-label={t("buttons.undo")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
|
size={data?.size || "medium"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
commitToHistory: () => false,
|
commitToHistory: () => false,
|
||||||
@@ -89,12 +89,13 @@ export const createRedoAction: ActionCreator = (history) => ({
|
|||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.key.toLowerCase() === KEYS.Z) ||
|
event.key.toLowerCase() === KEYS.Z) ||
|
||||||
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
|
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, data }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={redo}
|
icon={redo}
|
||||||
aria-label={t("buttons.redo")}
|
aria-label={t("buttons.redo")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
|
size={data?.size || "medium"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
commitToHistory: () => false,
|
commitToHistory: () => false,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { menu, palette } from "../components/icons";
|
import { menu, palette } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -70,7 +69,10 @@ export const actionFullScreen = register({
|
|||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
perform: (_elements, appState) => {
|
perform: (_elements, appState, _, { focusContainer }) => {
|
||||||
|
if (appState.showHelpDialog) {
|
||||||
|
focusContainer();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { getClientColors, getClientInitials } from "../clients";
|
import { getClientColors, getClientInitials } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
@@ -30,8 +29,8 @@ export const actionGoToCollaborator = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, id }) => {
|
PanelComponent: ({ appState, updateData, data }) => {
|
||||||
const clientId = id;
|
const clientId: string | undefined = data?.id;
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
@@ -13,6 +12,13 @@ import {
|
|||||||
FillCrossHatchIcon,
|
FillCrossHatchIcon,
|
||||||
FillHachureIcon,
|
FillHachureIcon,
|
||||||
FillSolidIcon,
|
FillSolidIcon,
|
||||||
|
FontFamilyCodeIcon,
|
||||||
|
FontFamilyHandDrawnIcon,
|
||||||
|
FontFamilyNormalIcon,
|
||||||
|
FontSizeExtraLargeIcon,
|
||||||
|
FontSizeLargeIcon,
|
||||||
|
FontSizeMediumIcon,
|
||||||
|
FontSizeSmallIcon,
|
||||||
SloppinessArchitectIcon,
|
SloppinessArchitectIcon,
|
||||||
SloppinessArtistIcon,
|
SloppinessArtistIcon,
|
||||||
SloppinessCartoonistIcon,
|
SloppinessCartoonistIcon,
|
||||||
@@ -20,18 +26,15 @@ import {
|
|||||||
StrokeStyleDottedIcon,
|
StrokeStyleDottedIcon,
|
||||||
StrokeStyleSolidIcon,
|
StrokeStyleSolidIcon,
|
||||||
StrokeWidthIcon,
|
StrokeWidthIcon,
|
||||||
FontSizeSmallIcon,
|
|
||||||
FontSizeMediumIcon,
|
|
||||||
FontSizeLargeIcon,
|
|
||||||
FontSizeExtraLargeIcon,
|
|
||||||
FontFamilyHandDrawnIcon,
|
|
||||||
FontFamilyNormalIcon,
|
|
||||||
FontFamilyCodeIcon,
|
|
||||||
TextAlignLeftIcon,
|
|
||||||
TextAlignCenterIcon,
|
TextAlignCenterIcon,
|
||||||
|
TextAlignLeftIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
import {
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
FONT_FAMILY,
|
||||||
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
getNonDeletedElements,
|
getNonDeletedElements,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
@@ -44,7 +47,7 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FontFamily,
|
FontFamilyValues,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
@@ -56,6 +59,7 @@ import {
|
|||||||
getTargetElements,
|
getTargetElements,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const changeProperty = (
|
const changeProperty = (
|
||||||
@@ -99,13 +103,20 @@ export const actionChangeStrokeColor = register({
|
|||||||
name: "changeStrokeColor",
|
name: "changeStrokeColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
...(value.currentItemStrokeColor && {
|
||||||
newElementWith(el, {
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
strokeColor: value,
|
return hasStrokeColor(el.type)
|
||||||
|
? newElementWith(el, {
|
||||||
|
strokeColor: value.currentItemStrokeColor,
|
||||||
|
})
|
||||||
|
: el;
|
||||||
}),
|
}),
|
||||||
),
|
}),
|
||||||
appState: { ...appState, currentItemStrokeColor: value },
|
appState: {
|
||||||
commitToHistory: true,
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemStrokeColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -120,7 +131,11 @@ export const actionChangeStrokeColor = register({
|
|||||||
(element) => element.strokeColor,
|
(element) => element.strokeColor,
|
||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={updateData}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
|
isActive={appState.openPopup === "strokeColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openPopup: active ? "strokeColorPicker" : null })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -130,13 +145,18 @@ export const actionChangeBackgroundColor = register({
|
|||||||
name: "changeBackgroundColor",
|
name: "changeBackgroundColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
|
...(value.currentItemBackgroundColor && {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
backgroundColor: value,
|
backgroundColor: value.currentItemBackgroundColor,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
appState: { ...appState, currentItemBackgroundColor: value },
|
}),
|
||||||
commitToHistory: true,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -151,7 +171,11 @@ export const actionChangeBackgroundColor = register({
|
|||||||
(element) => element.backgroundColor,
|
(element) => element.backgroundColor,
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={updateData}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
|
isActive={appState.openPopup === "backgroundColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openPopup: active ? "backgroundColorPicker" : null })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -481,19 +505,23 @@ export const actionChangeFontFamily = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
const options: { value: FontFamily; text: string; icon: JSX.Element }[] = [
|
const options: {
|
||||||
|
value: FontFamilyValues;
|
||||||
|
text: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
value: 1,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 3,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
||||||
},
|
},
|
||||||
@@ -502,7 +530,7 @@ export const actionChangeFontFamily = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
<legend>{t("labels.fontFamily")}</legend>
|
||||||
<ButtonIconSelect<FontFamily | false>
|
<ButtonIconSelect<FontFamilyValues | false>
|
||||||
group="font-family"
|
group="font-family"
|
||||||
options={options}
|
options={options}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { CODES, KEYS } from "../keys";
|
||||||
|
|
||||||
export const actionToggleStats = register({
|
export const actionToggleStats = register({
|
||||||
name: "stats",
|
name: "stats",
|
||||||
@@ -13,4 +14,6 @@ export const actionToggleStats = register({
|
|||||||
},
|
},
|
||||||
checked: (appState) => appState.showStats,
|
checked: (appState) => appState.showStats,
|
||||||
contextItemLabel: "stats.title",
|
contextItemLabel: "stats.title",
|
||||||
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const actionToggleViewMode = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
viewModeEnabled: !this.checked!(appState),
|
viewModeEnabled: !this.checked!(appState),
|
||||||
selectedElementIds: {},
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export {
|
|||||||
actionZoomOut,
|
actionZoomOut,
|
||||||
actionResetZoom,
|
actionResetZoom,
|
||||||
actionZoomToFit,
|
actionZoomToFit,
|
||||||
|
actionToggleTheme,
|
||||||
} from "./actionCanvas";
|
} from "./actionCanvas";
|
||||||
|
|
||||||
export { actionFinalize } from "./actionFinalize";
|
export { actionFinalize } from "./actionFinalize";
|
||||||
@@ -33,8 +34,8 @@ export { actionFinalize } from "./actionFinalize";
|
|||||||
export {
|
export {
|
||||||
actionChangeProjectName,
|
actionChangeProjectName,
|
||||||
actionChangeExportBackground,
|
actionChangeExportBackground,
|
||||||
actionSaveScene,
|
actionSaveToActiveFile,
|
||||||
actionSaveAsScene,
|
actionSaveFileToDisk,
|
||||||
actionLoadScene,
|
actionLoadScene,
|
||||||
} from "./actionExport";
|
} from "./actionExport";
|
||||||
|
|
||||||
|
|||||||
+10
-14
@@ -5,15 +5,12 @@ import {
|
|||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionName,
|
ActionName,
|
||||||
ActionResult,
|
ActionResult,
|
||||||
|
PanelComponentProps,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppProps, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { MODES } from "../constants";
|
import { MODES } from "../constants";
|
||||||
|
|
||||||
// This is the <App> component, but for now we don't care about anything but its
|
|
||||||
// `canvas` state.
|
|
||||||
type App = { canvas: HTMLCanvasElement | null; props: AppProps };
|
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
|
|
||||||
@@ -21,13 +18,13 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
|
|
||||||
getAppState: () => Readonly<AppState>;
|
getAppState: () => Readonly<AppState>;
|
||||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
||||||
app: App;
|
app: AppClassProperties;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
updater: UpdaterFn,
|
updater: UpdaterFn,
|
||||||
getAppState: () => AppState,
|
getAppState: () => AppState,
|
||||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
|
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
|
||||||
app: App,
|
app: AppClassProperties,
|
||||||
) {
|
) {
|
||||||
this.updater = (actionResult) => {
|
this.updater = (actionResult) => {
|
||||||
if (actionResult && "then" in actionResult) {
|
if (actionResult && "then" in actionResult) {
|
||||||
@@ -51,7 +48,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
actions.forEach((action) => this.registerAction(action));
|
actions.forEach((action) => this.registerAction(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
const data = Object.values(this.actions)
|
const data = Object.values(this.actions)
|
||||||
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
||||||
@@ -101,11 +98,10 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id is an attribute that we can use to pass in data like keys.
|
/**
|
||||||
// This is needed for dynamically generated action components
|
* @param data additional data sent to the PanelComponent
|
||||||
// like the user list. We can use this key to extract more
|
*/
|
||||||
// data from app state. This is an alternative to generic prop hell!
|
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||||
renderAction = (name: ActionName, id?: string) => {
|
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -133,8 +129,8 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
elements={this.getElementsIncludingDeleted()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
id={id}
|
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
stats: [],
|
stats: [getShortcutKey("Alt+/")],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
flipVertical: [getShortcutKey("Shift+V")],
|
flipVertical: [getShortcutKey("Shift+V")],
|
||||||
|
|||||||
+26
-13
@@ -1,6 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
ExcalidrawProps,
|
||||||
|
BinaryFiles,
|
||||||
|
} from "../types";
|
||||||
|
import { ToolButtonSize } from "../components/ToolButton";
|
||||||
|
|
||||||
/** if false, the action should be prevented */
|
/** if false, the action should be prevented */
|
||||||
export type ActionResult =
|
export type ActionResult =
|
||||||
@@ -10,8 +16,10 @@ export type ActionResult =
|
|||||||
AppState,
|
AppState,
|
||||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
> | null;
|
> | null;
|
||||||
|
files?: BinaryFiles | null;
|
||||||
commitToHistory: boolean;
|
commitToHistory: boolean;
|
||||||
syncHistory?: boolean;
|
syncHistory?: boolean;
|
||||||
|
replaceFiles?: boolean;
|
||||||
}
|
}
|
||||||
| false;
|
| false;
|
||||||
|
|
||||||
@@ -19,7 +27,7 @@ type ActionFn = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
formData: any,
|
formData: any,
|
||||||
app: { canvas: HTMLCanvasElement | null },
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
@@ -45,6 +53,7 @@ export type ActionName =
|
|||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
| "changeFillStyle"
|
| "changeFillStyle"
|
||||||
| "changeStrokeWidth"
|
| "changeStrokeWidth"
|
||||||
|
| "changeStrokeShape"
|
||||||
| "changeSloppiness"
|
| "changeSloppiness"
|
||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
@@ -58,9 +67,9 @@ export type ActionName =
|
|||||||
| "changeProjectName"
|
| "changeProjectName"
|
||||||
| "changeExportBackground"
|
| "changeExportBackground"
|
||||||
| "changeExportEmbedScene"
|
| "changeExportEmbedScene"
|
||||||
| "changeShouldAddWatermark"
|
| "changeExportScale"
|
||||||
| "saveScene"
|
| "saveToActiveFile"
|
||||||
| "saveAsScene"
|
| "saveFileToDisk"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
| "duplicateSelection"
|
| "duplicateSelection"
|
||||||
| "deleteSelectedElements"
|
| "deleteSelectedElements"
|
||||||
@@ -91,21 +100,24 @@ export type ActionName =
|
|||||||
| "flipHorizontal"
|
| "flipHorizontal"
|
||||||
| "flipVertical"
|
| "flipVertical"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
| "exportWithDarkMode";
|
| "exportWithDarkMode"
|
||||||
|
| "toggleTheme";
|
||||||
|
|
||||||
export interface Action {
|
export type PanelComponentProps = {
|
||||||
name: ActionName;
|
|
||||||
PanelComponent?: React.FC<{
|
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
appProps: ExcalidrawProps;
|
appProps: ExcalidrawProps;
|
||||||
id?: string;
|
data?: Partial<{ id: string; size: ToolButtonSize }>;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
name: ActionName;
|
||||||
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
event: KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => boolean;
|
) => boolean;
|
||||||
@@ -120,6 +132,7 @@ export interface Action {
|
|||||||
export interface ActionsManagerInterface {
|
export interface ActionsManagerInterface {
|
||||||
actions: Record<ActionName, Action>;
|
actions: Record<ActionName, Action>;
|
||||||
registerAction: (action: Action) => void;
|
registerAction: (action: Action) => void;
|
||||||
handleKeyDown: (event: KeyboardEvent) => boolean;
|
handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean;
|
||||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||||
|
executeAction: (action: Action) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-68
@@ -3,17 +3,23 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
|
EXPORT_SCALES,
|
||||||
|
THEME,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { t } from "./i18n";
|
import { t } from "./i18n";
|
||||||
import { AppState, NormalizedZoomValue } from "./types";
|
import { AppState, NormalizedZoomValue } from "./types";
|
||||||
import { getDateTime } from "./utils";
|
import { getDateTime } from "./utils";
|
||||||
|
|
||||||
|
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
|
||||||
|
? devicePixelRatio
|
||||||
|
: 1;
|
||||||
|
|
||||||
export const getDefaultAppState = (): Omit<
|
export const getDefaultAppState = (): Omit<
|
||||||
AppState,
|
AppState,
|
||||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
> => {
|
> => {
|
||||||
return {
|
return {
|
||||||
theme: "light",
|
theme: THEME.LIGHT,
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
currentChartType: "bar",
|
currentChartType: "bar",
|
||||||
currentItemBackgroundColor: "transparent",
|
currentItemBackgroundColor: "transparent",
|
||||||
@@ -39,6 +45,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
elementType: "selection",
|
elementType: "selection",
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
|
exportScale: defaultExportScale,
|
||||||
exportEmbedScene: false,
|
exportEmbedScene: false,
|
||||||
exportWithDarkMode: false,
|
exportWithDarkMode: false,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
@@ -52,6 +59,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
multiElement: null,
|
multiElement: null,
|
||||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||||
openMenu: null,
|
openMenu: null,
|
||||||
|
openPopup: null,
|
||||||
pasteDialog: { shown: false, data: null },
|
pasteDialog: { shown: false, data: null },
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
@@ -61,7 +69,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldAddWatermark: false,
|
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showHelpDialog: false,
|
showHelpDialog: false,
|
||||||
showStats: false,
|
showStats: false,
|
||||||
@@ -72,6 +79,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||||
viewModeEnabled: false,
|
viewModeEnabled: false,
|
||||||
|
pendingImageElement: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,77 +93,87 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
browser: boolean;
|
browser: boolean;
|
||||||
/** whether to keep when exporting to file/database */
|
/** whether to keep when exporting to file/database */
|
||||||
export: boolean;
|
export: boolean;
|
||||||
|
/** server (shareLink/collab/...) */
|
||||||
|
server: boolean;
|
||||||
},
|
},
|
||||||
T extends Record<keyof AppState, Values>
|
T extends Record<keyof AppState, Values>
|
||||||
>(
|
>(
|
||||||
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
|
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
|
||||||
) => config)({
|
) => config)({
|
||||||
theme: { browser: true, export: false },
|
theme: { browser: true, export: false, server: false },
|
||||||
collaborators: { browser: false, export: false },
|
collaborators: { browser: false, export: false, server: false },
|
||||||
currentChartType: { browser: true, export: false },
|
currentChartType: { browser: true, export: false, server: false },
|
||||||
currentItemBackgroundColor: { browser: true, export: false },
|
currentItemBackgroundColor: { browser: true, export: false, server: false },
|
||||||
currentItemEndArrowhead: { browser: true, export: false },
|
currentItemEndArrowhead: { browser: true, export: false, server: false },
|
||||||
currentItemFillStyle: { browser: true, export: false },
|
currentItemFillStyle: { browser: true, export: false, server: false },
|
||||||
currentItemFontFamily: { browser: true, export: false },
|
currentItemFontFamily: { browser: true, export: false, server: false },
|
||||||
currentItemFontSize: { browser: true, export: false },
|
currentItemFontSize: { browser: true, export: false, server: false },
|
||||||
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
currentItemLinearStrokeSharpness: {
|
||||||
currentItemOpacity: { browser: true, export: false },
|
browser: true,
|
||||||
currentItemRoughness: { browser: true, export: false },
|
export: false,
|
||||||
currentItemStartArrowhead: { browser: true, export: false },
|
server: false,
|
||||||
currentItemStrokeColor: { browser: true, export: false },
|
},
|
||||||
currentItemStrokeSharpness: { browser: true, export: false },
|
currentItemOpacity: { browser: true, export: false, server: false },
|
||||||
currentItemStrokeStyle: { browser: true, export: false },
|
currentItemRoughness: { browser: true, export: false, server: false },
|
||||||
currentItemStrokeWidth: { browser: true, export: false },
|
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
||||||
currentItemTextAlign: { browser: true, export: false },
|
currentItemStrokeColor: { browser: true, export: false, server: false },
|
||||||
cursorButton: { browser: true, export: false },
|
currentItemStrokeSharpness: { browser: true, export: false, server: false },
|
||||||
draggingElement: { browser: false, export: false },
|
currentItemStrokeStyle: { browser: true, export: false, server: false },
|
||||||
editingElement: { browser: false, export: false },
|
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||||
editingGroupId: { browser: true, export: false },
|
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||||
editingLinearElement: { browser: false, export: false },
|
cursorButton: { browser: true, export: false, server: false },
|
||||||
elementLocked: { browser: true, export: false },
|
draggingElement: { browser: false, export: false, server: false },
|
||||||
elementType: { browser: true, export: false },
|
editingElement: { browser: false, export: false, server: false },
|
||||||
errorMessage: { browser: false, export: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
exportBackground: { browser: true, export: false },
|
editingLinearElement: { browser: false, export: false, server: false },
|
||||||
exportEmbedScene: { browser: true, export: false },
|
elementLocked: { browser: true, export: false, server: false },
|
||||||
exportWithDarkMode: { browser: true, export: false },
|
elementType: { browser: true, export: false, server: false },
|
||||||
fileHandle: { browser: false, export: false },
|
errorMessage: { browser: false, export: false, server: false },
|
||||||
gridSize: { browser: true, export: true },
|
exportBackground: { browser: true, export: false, server: false },
|
||||||
height: { browser: false, export: false },
|
exportEmbedScene: { browser: true, export: false, server: false },
|
||||||
isBindingEnabled: { browser: false, export: false },
|
exportScale: { browser: true, export: false, server: false },
|
||||||
isLibraryOpen: { browser: false, export: false },
|
exportWithDarkMode: { browser: true, export: false, server: false },
|
||||||
isLoading: { browser: false, export: false },
|
fileHandle: { browser: false, export: false, server: false },
|
||||||
isResizing: { browser: false, export: false },
|
gridSize: { browser: true, export: true, server: true },
|
||||||
isRotating: { browser: false, export: false },
|
height: { browser: false, export: false, server: false },
|
||||||
lastPointerDownWith: { browser: true, export: false },
|
isBindingEnabled: { browser: false, export: false, server: false },
|
||||||
multiElement: { browser: false, export: false },
|
isLibraryOpen: { browser: false, export: false, server: false },
|
||||||
name: { browser: true, export: false },
|
isLoading: { browser: false, export: false, server: false },
|
||||||
offsetLeft: { browser: false, export: false },
|
isResizing: { browser: false, export: false, server: false },
|
||||||
offsetTop: { browser: false, export: false },
|
isRotating: { browser: false, export: false, server: false },
|
||||||
openMenu: { browser: true, export: false },
|
lastPointerDownWith: { browser: true, export: false, server: false },
|
||||||
pasteDialog: { browser: false, export: false },
|
multiElement: { browser: false, export: false, server: false },
|
||||||
previousSelectedElementIds: { browser: true, export: false },
|
name: { browser: true, export: false, server: false },
|
||||||
resizingElement: { browser: false, export: false },
|
offsetLeft: { browser: false, export: false, server: false },
|
||||||
scrolledOutside: { browser: true, export: false },
|
offsetTop: { browser: false, export: false, server: false },
|
||||||
scrollX: { browser: true, export: false },
|
openMenu: { browser: true, export: false, server: false },
|
||||||
scrollY: { browser: true, export: false },
|
openPopup: { browser: false, export: false, server: false },
|
||||||
selectedElementIds: { browser: true, export: false },
|
pasteDialog: { browser: false, export: false, server: false },
|
||||||
selectedGroupIds: { browser: true, export: false },
|
previousSelectedElementIds: { browser: true, export: false, server: false },
|
||||||
selectionElement: { browser: false, export: false },
|
resizingElement: { browser: false, export: false, server: false },
|
||||||
shouldAddWatermark: { browser: true, export: false },
|
scrolledOutside: { browser: true, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
scrollX: { browser: true, export: false, server: false },
|
||||||
showHelpDialog: { browser: false, export: false },
|
scrollY: { browser: true, export: false, server: false },
|
||||||
showStats: { browser: true, export: false },
|
selectedElementIds: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false },
|
selectedGroupIds: { browser: true, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
toastMessage: { browser: false, export: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
viewBackgroundColor: { browser: true, export: true },
|
showHelpDialog: { browser: false, export: false, server: false },
|
||||||
width: { browser: false, export: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
zenModeEnabled: { browser: true, export: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
zoom: { browser: true, export: false },
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
viewModeEnabled: { browser: false, export: false },
|
toastMessage: { browser: false, export: false, server: false },
|
||||||
|
viewBackgroundColor: { browser: true, export: true, server: true },
|
||||||
|
width: { browser: false, export: false, server: false },
|
||||||
|
zenModeEnabled: { browser: true, export: false, server: false },
|
||||||
|
zoom: { browser: true, export: false, server: false },
|
||||||
|
viewModeEnabled: { browser: false, export: false, server: false },
|
||||||
|
pendingImageElement: { browser: false, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
const _clearAppStateForStorage = <
|
||||||
|
ExportType extends "export" | "browser" | "server"
|
||||||
|
>(
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
exportType: ExportType,
|
exportType: ExportType,
|
||||||
) => {
|
) => {
|
||||||
@@ -168,8 +186,10 @@ const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
|||||||
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
|
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
|
||||||
const propConfig = APP_STATE_STORAGE_CONF[key];
|
const propConfig = APP_STATE_STORAGE_CONF[key];
|
||||||
if (propConfig?.[exportType]) {
|
if (propConfig?.[exportType]) {
|
||||||
// @ts-ignore see https://github.com/microsoft/TypeScript/issues/31445
|
const nextValue = appState[key];
|
||||||
stateForExport[key] = appState[key];
|
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/31445
|
||||||
|
(stateForExport as any)[key] = nextValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stateForExport;
|
return stateForExport;
|
||||||
@@ -182,3 +202,7 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
|
|||||||
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
|
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
|
||||||
return _clearAppStateForStorage(appState, "export");
|
return _clearAppStateForStorage(appState, "export");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
|
||||||
|
return _clearAppStateForStorage(appState, "server");
|
||||||
|
};
|
||||||
|
|||||||
+29
-15
@@ -3,17 +3,26 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { AppState } from "./types";
|
import { AppState, 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 { canvasToBlob } from "./data/blob";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { EXPORT_DATA_TYPES } from "./constants";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
elements: ExcalidrawElement[];
|
elements: ExcalidrawElement[];
|
||||||
|
files: BinaryFiles | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ClipboardData {
|
||||||
|
spreadsheet?: Spreadsheet;
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
files?: BinaryFiles;
|
||||||
|
text?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
let CLIPBOARD = "";
|
||||||
let PREFER_APP_CLIPBOARD = false;
|
let PREFER_APP_CLIPBOARD = false;
|
||||||
|
|
||||||
@@ -31,7 +40,7 @@ export const probablySupportsClipboardBlob =
|
|||||||
|
|
||||||
const clipboardContainsElements = (
|
const clipboardContainsElements = (
|
||||||
contents: any,
|
contents: any,
|
||||||
): contents is { elements: ExcalidrawElement[] } => {
|
): contents is { elements: ExcalidrawElement[]; files?: BinaryFiles } => {
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
EXPORT_DATA_TYPES.excalidraw,
|
EXPORT_DATA_TYPES.excalidraw,
|
||||||
@@ -47,10 +56,18 @@ const clipboardContainsElements = (
|
|||||||
export const copyToClipboard = async (
|
export const copyToClipboard = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
files: BinaryFiles,
|
||||||
) => {
|
) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements: getSelectedElements(elements, appState),
|
elements: selectedElements,
|
||||||
|
files: selectedElements.reduce((acc, element) => {
|
||||||
|
if (isInitializedImageElement(element) && files[element.fileId]) {
|
||||||
|
acc[element.fileId] = files[element.fileId];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as BinaryFiles),
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
CLIPBOARD = json;
|
CLIPBOARD = json;
|
||||||
@@ -110,12 +127,7 @@ const getSystemClipboard = async (
|
|||||||
*/
|
*/
|
||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent | null,
|
||||||
): Promise<{
|
): Promise<ClipboardData> => {
|
||||||
spreadsheet?: Spreadsheet;
|
|
||||||
elements?: readonly ExcalidrawElement[];
|
|
||||||
text?: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
}> => {
|
|
||||||
const systemClipboard = await getSystemClipboard(event);
|
const systemClipboard = await getSystemClipboard(event);
|
||||||
|
|
||||||
// if system clipboard empty, couldn't be resolved, or contains previously
|
// if system clipboard empty, couldn't be resolved, or contains previously
|
||||||
@@ -137,7 +149,10 @@ export const parseClipboard = async (
|
|||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
return { elements: systemClipboardData.elements };
|
return {
|
||||||
|
elements: systemClipboardData.elements,
|
||||||
|
files: systemClipboardData.files,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return appClipboardData;
|
return appClipboardData;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -150,10 +165,9 @@ export const parseClipboard = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob) => {
|
||||||
const blob = await canvasToBlob(canvas);
|
|
||||||
await navigator.clipboard.write([
|
await navigator.clipboard.write([
|
||||||
new window.ClipboardItem({ "image/png": blob }),
|
new window.ClipboardItem({ [MIME_TYPES.png]: blob }),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+40
-39
@@ -1,15 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import {
|
import {
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getTargetElements,
|
getTargetElements,
|
||||||
hasBackground,
|
hasBackground,
|
||||||
hasStroke,
|
hasStrokeStyle,
|
||||||
|
hasStrokeWidth,
|
||||||
hasText,
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
@@ -17,6 +18,7 @@ import { AppState, Zoom } from "../types";
|
|||||||
import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -47,16 +49,36 @@ export const SelectedShapeActions = ({
|
|||||||
hasBackground(elementType) ||
|
hasBackground(elementType) ||
|
||||||
targetElements.some((element) => hasBackground(element.type));
|
targetElements.some((element) => hasBackground(element.type));
|
||||||
|
|
||||||
|
let commonSelectedType: string | null = targetElements[0]?.type || null;
|
||||||
|
|
||||||
|
for (const element of targetElements) {
|
||||||
|
if (element.type !== commonSelectedType) {
|
||||||
|
commonSelectedType = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
{renderAction("changeStrokeColor")}
|
{((hasStrokeColor(elementType) &&
|
||||||
|
elementType !== "image" &&
|
||||||
|
commonSelectedType !== "image") ||
|
||||||
|
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||||
|
renderAction("changeStrokeColor")}
|
||||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStroke(elementType) ||
|
{(hasStrokeWidth(elementType) ||
|
||||||
targetElements.some((element) => hasStroke(element.type))) && (
|
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||||
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
|
{(elementType === "freedraw" ||
|
||||||
|
targetElements.some((element) => element.type === "freedraw")) &&
|
||||||
|
renderAction("changeStrokeShape")}
|
||||||
|
|
||||||
|
{(hasStrokeStyle(elementType) ||
|
||||||
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeStrokeWidth")}
|
|
||||||
{renderAction("changeStrokeStyle")}
|
{renderAction("changeStrokeStyle")}
|
||||||
{renderAction("changeSloppiness")}
|
{renderAction("changeSloppiness")}
|
||||||
</>
|
</>
|
||||||
@@ -143,31 +165,24 @@ export const SelectedShapeActions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIBRARY_ICON = (
|
|
||||||
// fa-th-large
|
|
||||||
<svg viewBox="0 0 512 512">
|
|
||||||
<path d="M296 32h192c13.255 0 24 10.745 24 24v160c0 13.255-10.745 24-24 24H296c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24zm-80 0H24C10.745 32 0 42.745 0 56v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zM0 296v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm296 184h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H296c-13.255 0-24 10.745-24 24v160c0 13.255 10.745 24 24 24z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
canvas,
|
canvas,
|
||||||
elementType,
|
elementType,
|
||||||
setAppState,
|
setAppState,
|
||||||
isLibraryOpen,
|
onImageAction,
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elementType: ExcalidrawElement["type"];
|
elementType: ExcalidrawElement["type"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
isLibraryOpen: boolean;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key }, index) => {
|
{SHAPES.map(({ value, icon, key }, index) => {
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const letter = typeof key === "string" ? key : key[0];
|
const letter = key && (typeof key === "string" ? key : key[0]);
|
||||||
const shortcut = `${capitalizeString(letter)} ${t("helpDialog.or")} ${
|
const shortcut = letter
|
||||||
index + 1
|
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
|
||||||
}`;
|
: `${index + 1}`;
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className="Shape"
|
className="Shape"
|
||||||
@@ -181,31 +196,20 @@ export const ShapesSwitcher = ({
|
|||||||
aria-label={capitalizeString(label)}
|
aria-label={capitalizeString(label)}
|
||||||
aria-keyshortcuts={shortcut}
|
aria-keyshortcuts={shortcut}
|
||||||
data-testid={value}
|
data-testid={value}
|
||||||
onChange={() => {
|
onChange={({ pointerType }) => {
|
||||||
setAppState({
|
setAppState({
|
||||||
elementType: value,
|
elementType: value,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
});
|
});
|
||||||
setCursorForShape(canvas, value);
|
setCursorForShape(canvas, value);
|
||||||
setAppState({});
|
if (value === "image") {
|
||||||
|
onImageAction({ pointerType });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<ToolButton
|
|
||||||
className="Shape ToolIcon_type_button__library"
|
|
||||||
type="button"
|
|
||||||
icon={LIBRARY_ICON}
|
|
||||||
name="editor-library"
|
|
||||||
keyBindingLabel="9"
|
|
||||||
aria-keyshortcuts="9"
|
|
||||||
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
|
||||||
aria-label={capitalizeString(t("toolBar.library"))}
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({ isLibraryOpen: !isLibraryOpen });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -218,12 +222,9 @@ export const ZoomActions = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<Stack.Col gap={1}>
|
<Stack.Col gap={1}>
|
||||||
<Stack.Row gap={1} align="center">
|
<Stack.Row gap={1} align="center">
|
||||||
{renderAction("zoomIn")}
|
|
||||||
{renderAction("zoomOut")}
|
{renderAction("zoomOut")}
|
||||||
|
{renderAction("zoomIn")}
|
||||||
{renderAction("resetZoom")}
|
{renderAction("resetZoom")}
|
||||||
<div style={{ marginInlineStart: 4 }}>
|
|
||||||
{(zoom.value * 100).toFixed(0)}%
|
|
||||||
</div>
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.excalidraw {
|
||||||
|
.ActiveFile {
|
||||||
|
.ActiveFile__fileName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 9.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.15em;
|
||||||
|
margin-inline-end: 0.3em;
|
||||||
|
transform: scaleY(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import Stack from "../components/Stack";
|
||||||
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { save, file } from "../components/icons";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
|
import "./ActiveFile.scss";
|
||||||
|
|
||||||
|
type ActiveFileProps = {
|
||||||
|
fileName?: string;
|
||||||
|
onSave: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
|
||||||
|
<Stack.Row className="ActiveFile" gap={1} align="center">
|
||||||
|
<span className="ActiveFile__fileName">
|
||||||
|
{file}
|
||||||
|
<span>{fileName}</span>
|
||||||
|
</span>
|
||||||
|
<ToolButton
|
||||||
|
type="icon"
|
||||||
|
icon={save}
|
||||||
|
title={t("buttons.save")}
|
||||||
|
aria-label={t("buttons.save")}
|
||||||
|
onClick={onSave}
|
||||||
|
data-testid="save-button"
|
||||||
|
/>
|
||||||
|
</Stack.Row>
|
||||||
|
);
|
||||||
+1163
-302
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { DarkModeToggle } from "./DarkModeToggle";
|
|
||||||
|
|
||||||
export const BackgroundPickerAndDarkModeToggle = ({
|
export const BackgroundPickerAndDarkModeToggle = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -16,15 +15,6 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
{showThemeBtn && (
|
{showThemeBtn && actionManager.renderAction("toggleTheme")}
|
||||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
|
||||||
<DarkModeToggle
|
|
||||||
value={appState.theme}
|
|
||||||
onChange={(theme) => {
|
|
||||||
setAppState({ theme });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
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 />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const ButtonSelect = <T extends Object>({
|
export const ButtonSelect = <T extends Object>({
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.Card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
max-width: 290px;
|
||||||
|
|
||||||
|
margin: 1em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.Card-icon {
|
||||||
|
font-size: 2.6em;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 1.4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--card-color);
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 2.8rem;
|
||||||
|
height: 2.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Card-details {
|
||||||
|
font-size: 0.96em;
|
||||||
|
min-height: 90px;
|
||||||
|
padding: 0 1em;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .Card-button.ToolIcon_type_button {
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
background-color: var(--card-color);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--card-color-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--card-color-darkest);
|
||||||
|
}
|
||||||
|
.ToolIcon__label {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Spinner {
|
||||||
|
--spinner-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import OpenColor from "open-color";
|
||||||
|
|
||||||
|
import "./Card.scss";
|
||||||
|
|
||||||
|
export const Card: React.FC<{
|
||||||
|
color: keyof OpenColor;
|
||||||
|
}> = ({ children, color }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="Card"
|
||||||
|
style={{
|
||||||
|
["--card-color" as any]: OpenColor[color][7],
|
||||||
|
["--card-color-darker" as any]: OpenColor[color][8],
|
||||||
|
["--card-color-darkest" as any]: OpenColor[color][9],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.Checkbox {
|
||||||
|
margin: 4px 0.3em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:hover:not(.is-checked) .Checkbox-box:not(:focus) {
|
||||||
|
box-shadow: 0 0 0 2px #{$oc-blue-4};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.is-checked) .Checkbox-box:not(:focus) {
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.Checkbox-box {
|
||||||
|
box-shadow: 0 0 2px 1px inset #{$oc-blue-7} !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.Checkbox-box {
|
||||||
|
background-color: fade-out($oc-blue-1, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked {
|
||||||
|
.Checkbox-box {
|
||||||
|
background-color: #{$oc-blue-1};
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .Checkbox-box {
|
||||||
|
background-color: #{$oc-blue-2};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Checkbox-box {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
margin: 0 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
box-shadow: 0 0 0 2px #{$oc-blue-7};
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
color: #{$oc-blue-7};
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px #{$oc-blue-7};
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excalidraw-tooltip-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { checkIcon } from "./icons";
|
||||||
|
|
||||||
|
import "./CheckboxItem.scss";
|
||||||
|
|
||||||
|
export const CheckboxItem: React.FC<{
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}> = ({ children, checked, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("Checkbox", { "is-checked": checked })}
|
||||||
|
onClick={(event) => {
|
||||||
|
onChange(!checked);
|
||||||
|
((event.currentTarget as HTMLDivElement).querySelector(
|
||||||
|
".Checkbox-box",
|
||||||
|
) as HTMLButtonElement).focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
|
||||||
|
{checkIcon}
|
||||||
|
</button>
|
||||||
|
<div className="Checkbox-label">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.clear-canvas {
|
||||||
|
&-buttons {
|
||||||
|
display: flex;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
min-width: 2.5rem;
|
||||||
|
width: auto;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon_type_button {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--confirm.ToolIcon_type_button {
|
||||||
|
background-color: $oc-red-6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $oc-red-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--cancel.ToolIcon_type_button {
|
||||||
|
background-color: $oc-gray-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { useIsMobile } from "./App";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { trash } from "./icons";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
|
import "./ClearCanvas.scss";
|
||||||
|
|
||||||
|
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||||
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const toggleDialog = () => {
|
||||||
|
setShowDialog(!showDialog);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
icon={trash}
|
||||||
|
title={t("buttons.clearReset")}
|
||||||
|
aria-label={t("buttons.clearReset")}
|
||||||
|
showAriaLabel={useIsMobile()}
|
||||||
|
onClick={toggleDialog}
|
||||||
|
data-testid="clear-canvas-button"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showDialog && (
|
||||||
|
<Dialog
|
||||||
|
onCloseRequest={toggleDialog}
|
||||||
|
title={t("clearCanvasDialog.title")}
|
||||||
|
className="clear-canvas"
|
||||||
|
small={true}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
||||||
|
<div className="clear-canvas-buttons">
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.clear")}
|
||||||
|
aria-label={t("buttons.clear")}
|
||||||
|
label={t("buttons.clear")}
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
toggleDialog();
|
||||||
|
}}
|
||||||
|
data-testid="confirm-clear-canvas-button"
|
||||||
|
className="clear-canvas--confirm"
|
||||||
|
/>
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.cancel")}
|
||||||
|
aria-label={t("buttons.cancel")}
|
||||||
|
label={t("buttons.cancel")}
|
||||||
|
onClick={toggleDialog}
|
||||||
|
data-testid="cancel-clear-canvas-button"
|
||||||
|
className="clear-canvas--cancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClearCanvas;
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { users } from "./icons";
|
import { users } from "./icons";
|
||||||
|
|
||||||
import "./CollabButton.scss";
|
import "./CollabButton.scss";
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-input {
|
.color-picker-input {
|
||||||
width: 12ch; /* length of `transparent` + 1 */
|
width: 11ch; /* length of `transparent` */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: var(--input-bg-color);
|
background-color: var(--input-bg-color);
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
|
import { isTransparent } from "../utils";
|
||||||
|
|
||||||
import "./ColorPicker.scss";
|
import "./ColorPicker.scss";
|
||||||
import { isArrowKey, KEYS } from "../keys";
|
import { isArrowKey, KEYS } from "../keys";
|
||||||
@@ -14,7 +15,7 @@ const isValidColor = (color: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getColor = (color: string): string | null => {
|
const getColor = (color: string): string | null => {
|
||||||
if (color === "transparent") {
|
if (isTransparent(color)) {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ const Picker = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -136,15 +138,19 @@ const Picker = ({
|
|||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{colors.map((_color, i) => (
|
{colors.map((_color, i) => {
|
||||||
|
const _colorWithoutHash = _color.replace("#", "");
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
className="color-picker-swatch"
|
className="color-picker-swatch"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
(event.currentTarget as HTMLButtonElement).focus();
|
(event.currentTarget as HTMLButtonElement).focus();
|
||||||
onChange(_color);
|
onChange(_color);
|
||||||
}}
|
}}
|
||||||
title={`${_color} — ${keyBindings[i].toUpperCase()}`}
|
title={`${t(`colors.${_colorWithoutHash}`)}${
|
||||||
aria-label={_color}
|
!isTransparent(_color) ? ` (${_color})` : ""
|
||||||
|
} — ${keyBindings[i].toUpperCase()}`}
|
||||||
|
aria-label={t(`colors.${_colorWithoutHash}`)}
|
||||||
aria-keyshortcuts={keyBindings[i]}
|
aria-keyshortcuts={keyBindings[i]}
|
||||||
style={{ color: _color }}
|
style={{ color: _color }}
|
||||||
key={_color}
|
key={_color}
|
||||||
@@ -160,12 +166,13 @@ const Picker = ({
|
|||||||
onChange(_color);
|
onChange(_color);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{_color === "transparent" ? (
|
{isTransparent(_color) ? (
|
||||||
<div className="color-picker-transparent"></div>
|
<div className="color-picker-transparent"></div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<span className="color-picker-keybinding">{keyBindings[i]}</span>
|
<span className="color-picker-keybinding">{keyBindings[i]}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<ColorInput
|
<ColorInput
|
||||||
color={color}
|
color={color}
|
||||||
@@ -237,13 +244,16 @@ export const ColorPicker = ({
|
|||||||
color,
|
color,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
|
isActive,
|
||||||
|
setActive,
|
||||||
}: {
|
}: {
|
||||||
type: "canvasBackground" | "elementBackground" | "elementStroke";
|
type: "canvasBackground" | "elementBackground" | "elementStroke";
|
||||||
color: string | null;
|
color: string | null;
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
|
isActive: boolean;
|
||||||
|
setActive: (active: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [isActive, setActive] = React.useState(false);
|
|
||||||
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.context-menu-option {
|
.context-menu-option {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { render, unmountComponentAtNode } from "react-dom";
|
import { render, unmountComponentAtNode } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
|
|||||||
@@ -1,42 +1,32 @@
|
|||||||
import "./ToolIcon.scss";
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
export type Appearence = "light" | "dark";
|
import { THEME } from "../constants";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
|
||||||
// We chose to use only explicit toggle and not a third option for system value,
|
// We chose to use only explicit toggle and not a third option for system value,
|
||||||
// but this could be added in the future.
|
// but this could be added in the future.
|
||||||
export const DarkModeToggle = (props: {
|
export const DarkModeToggle = (props: {
|
||||||
value: Appearence;
|
value: Theme;
|
||||||
onChange: (value: Appearence) => void;
|
onChange: (value: Theme) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const title = props.title
|
const title =
|
||||||
? props.title
|
props.title ||
|
||||||
: props.value === "dark"
|
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
|
||||||
? t("buttons.lightMode")
|
|
||||||
: t("buttons.darkMode");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<ToolButton
|
||||||
className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
type="icon"
|
||||||
data-testid="toggle-dark-mode"
|
icon={props.value === THEME.LIGHT ? ICONS.MOON : ICONS.SUN}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
|
||||||
type="checkbox"
|
|
||||||
onChange={(event) =>
|
|
||||||
props.onChange(event.target.checked ? "dark" : "light")
|
|
||||||
}
|
|
||||||
checked={props.value === "dark"}
|
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
|
onClick={() =>
|
||||||
|
props.onChange(props.value === THEME.DARK ? THEME.LIGHT : THEME.DARK)
|
||||||
|
}
|
||||||
|
data-testid="toggle-dark-mode"
|
||||||
/>
|
/>
|
||||||
<div className="ToolIcon__icon">
|
|
||||||
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.Dialog {
|
.Dialog {
|
||||||
--metric: calc(var(--space-factor) * 4);
|
--metric: calc(var(--space-factor) * 4);
|
||||||
--inset-left: #{"max(var(--metric), var(--sal))"};
|
--inset-left: #{"max(var(--metric), var(--sal))"};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useExcalidrawContainer, useIsMobile } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import "./Dialog.scss";
|
import "./Dialog.scss";
|
||||||
import { back, close } from "./icons";
|
import { back, close } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const Dialog = (props: {
|
export const Dialog = (props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -16,8 +17,11 @@ export const Dialog = (props: {
|
|||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
theme?: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
|
const { id } = useExcalidrawContainer();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
@@ -65,19 +69,25 @@ export const Dialog = (props: {
|
|||||||
return focusableElements ? Array.from(focusableElements) : [];
|
return focusableElements ? Array.from(focusableElements) : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
(lastActiveElement as HTMLElement).focus();
|
||||||
|
props.onCloseRequest();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className)}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={props.small ? 550 : 800}
|
maxWidth={props.small ? 550 : 800}
|
||||||
onCloseRequest={props.onCloseRequest}
|
onCloseRequest={onClose}
|
||||||
|
theme={props.theme}
|
||||||
>
|
>
|
||||||
<Island ref={setIslandNode}>
|
<Island ref={setIslandNode}>
|
||||||
<h2 id="dialog-title" className="Dialog__title">
|
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
||||||
<span className="Dialog__titleContent">{props.title}</span>
|
<span className="Dialog__titleContent">{props.title}</span>
|
||||||
<button
|
<button
|
||||||
className="Modal__close"
|
className="Modal__close"
|
||||||
onClick={props.onCloseRequest}
|
onClick={onClose}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{useIsMobile() ? back : close}
|
{useIsMobile() ? back : close}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
export const ErrorDialog = ({
|
export const ErrorDialog = ({
|
||||||
message,
|
message,
|
||||||
@@ -11,6 +12,7 @@ export const ErrorDialog = ({
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(!!message);
|
const [modalIsShown, setModalIsShown] = useState(!!message);
|
||||||
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setModalIsShown(false);
|
setModalIsShown(false);
|
||||||
@@ -18,7 +20,9 @@ export const ErrorDialog = ({
|
|||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [onClose]);
|
// TODO: Fix the A11y issues so this is never needed since we should always focus on last active element
|
||||||
|
excalidrawContainer?.focus();
|
||||||
|
}, [onClose, excalidrawContainer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -28,34 +28,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ExportDialog__name {
|
@include isMobile {
|
||||||
grid-column: project-name;
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.TextInput {
|
|
||||||
height: calc(1rem - 3px);
|
|
||||||
width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
margin-left: 8px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&--readonly {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
width: auto;
|
|
||||||
max-width: 200px;
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
|
||||||
.ExportDialog {
|
.ExportDialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -84,4 +57,63 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ExportDialog--json {
|
||||||
|
.ExportDialog-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
justify-items: center;
|
||||||
|
row-gap: 2em;
|
||||||
|
|
||||||
|
@media (max-width: 460px) {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
.Card-details {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProjectName {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 1em auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.TextInput {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProjectName-label {
|
||||||
|
margin: 0.625em 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.ExportDialog-imageExportButton {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
margin: 0 0.2em;
|
||||||
|
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: var(--button-color);
|
||||||
|
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.28),
|
||||||
|
0 6px 10px 0 rgba(0, 0, 0, 0.14);
|
||||||
|
|
||||||
|
font-family: Cascadia;
|
||||||
|
font-size: 1.8em;
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-color-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-color-darkest);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { render, unmountComponentAtNode } from "react-dom";
|
|
||||||
import { ActionsManagerInterface } from "../actions/types";
|
|
||||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
|
||||||
import { canvasToBlob } from "../data/blob";
|
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
|
||||||
import { CanvasError } from "../errors";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import { useIsMobile } from "../is-mobile";
|
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
|
||||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { Dialog } from "./Dialog";
|
|
||||||
import "./ExportDialog.scss";
|
|
||||||
import { clipboard, exportFile, link } from "./icons";
|
|
||||||
import Stack from "./Stack";
|
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
|
|
||||||
const scales = [1, 2, 3];
|
|
||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
|
||||||
|
|
||||||
const supportsContextFilters =
|
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
|
||||||
|
|
||||||
export const ErrorCanvasPreview = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3>{t("canvasError.cannotShowPreview")}</h3>
|
|
||||||
<p>
|
|
||||||
<span>{t("canvasError.canvasTooBig")}</span>
|
|
||||||
</p>
|
|
||||||
<em>({t("canvasError.canvasTooBigTip")})</em>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPreview = (
|
|
||||||
content: HTMLCanvasElement | Error,
|
|
||||||
previewNode: HTMLDivElement,
|
|
||||||
) => {
|
|
||||||
unmountComponentAtNode(previewNode);
|
|
||||||
previewNode.innerHTML = "";
|
|
||||||
if (content instanceof HTMLCanvasElement) {
|
|
||||||
previewNode.appendChild(content);
|
|
||||||
} else {
|
|
||||||
render(<ErrorCanvasPreview />, previewNode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportCB = (
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scale?: number,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
const ExportModal = ({
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
exportPadding = 10,
|
|
||||||
actionManager,
|
|
||||||
onExportToPng,
|
|
||||||
onExportToSvg,
|
|
||||||
onExportToClipboard,
|
|
||||||
onExportToBackend,
|
|
||||||
}: {
|
|
||||||
appState: AppState;
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
exportPadding?: number;
|
|
||||||
actionManager: ActionsManagerInterface;
|
|
||||||
onExportToPng: ExportCB;
|
|
||||||
onExportToSvg: ExportCB;
|
|
||||||
onExportToClipboard: ExportCB;
|
|
||||||
onExportToBackend?: ExportCB;
|
|
||||||
onCloseRequest: () => void;
|
|
||||||
}) => {
|
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
|
||||||
const [scale, setScale] = useState(defaultScale);
|
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
|
||||||
const {
|
|
||||||
exportBackground,
|
|
||||||
viewBackgroundColor,
|
|
||||||
shouldAddWatermark,
|
|
||||||
} = appState;
|
|
||||||
|
|
||||||
const exportedElements = exportSelected
|
|
||||||
? getSelectedElements(elements, appState)
|
|
||||||
: elements;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setExportSelected(someElementIsSelected);
|
|
||||||
}, [someElementIsSelected]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const previewNode = previewRef.current;
|
|
||||||
if (!previewNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const canvas = exportToCanvas(exportedElements, appState, {
|
|
||||||
exportBackground,
|
|
||||||
viewBackgroundColor,
|
|
||||||
exportPadding,
|
|
||||||
scale,
|
|
||||||
shouldAddWatermark,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if converting to blob fails, there's some problem that will
|
|
||||||
// likely prevent preview and export (e.g. canvas too big)
|
|
||||||
canvasToBlob(canvas)
|
|
||||||
.then(() => {
|
|
||||||
renderPreview(canvas, previewNode);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
renderPreview(new CanvasError(), previewNode);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
renderPreview(new CanvasError(), previewNode);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
appState,
|
|
||||||
exportedElements,
|
|
||||||
exportBackground,
|
|
||||||
exportPadding,
|
|
||||||
viewBackgroundColor,
|
|
||||||
scale,
|
|
||||||
shouldAddWatermark,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ExportDialog">
|
|
||||||
<div className="ExportDialog__preview" ref={previewRef} />
|
|
||||||
{supportsContextFilters &&
|
|
||||||
actionManager.renderAction("exportWithDarkMode")}
|
|
||||||
<Stack.Col gap={2} align="center">
|
|
||||||
<div className="ExportDialog__actions">
|
|
||||||
<Stack.Row gap={2}>
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
label="PNG"
|
|
||||||
title={t("buttons.exportToPng")}
|
|
||||||
aria-label={t("buttons.exportToPng")}
|
|
||||||
onClick={() => onExportToPng(exportedElements, scale)}
|
|
||||||
/>
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
label="SVG"
|
|
||||||
title={t("buttons.exportToSvg")}
|
|
||||||
aria-label={t("buttons.exportToSvg")}
|
|
||||||
onClick={() => onExportToSvg(exportedElements, scale)}
|
|
||||||
/>
|
|
||||||
{probablySupportsClipboardBlob && (
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
icon={clipboard}
|
|
||||||
title={t("buttons.copyPngToClipboard")}
|
|
||||||
aria-label={t("buttons.copyPngToClipboard")}
|
|
||||||
onClick={() => onExportToClipboard(exportedElements, scale)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{onExportToBackend && (
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
icon={link}
|
|
||||||
title={t("buttons.getShareableLink")}
|
|
||||||
aria-label={t("buttons.getShareableLink")}
|
|
||||||
onClick={() => onExportToBackend(exportedElements)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack.Row>
|
|
||||||
<div className="ExportDialog__name">
|
|
||||||
{actionManager.renderAction("changeProjectName")}
|
|
||||||
</div>
|
|
||||||
<Stack.Row gap={2}>
|
|
||||||
{scales.map((s) => {
|
|
||||||
const [width, height] = getExportSize(
|
|
||||||
exportedElements,
|
|
||||||
exportPadding,
|
|
||||||
shouldAddWatermark,
|
|
||||||
s,
|
|
||||||
);
|
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
|
||||||
"buttons.scale",
|
|
||||||
)} ${s}x (${width}x${height})`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolButton
|
|
||||||
key={s}
|
|
||||||
size="s"
|
|
||||||
type="radio"
|
|
||||||
icon={`${s}x`}
|
|
||||||
name="export-canvas-scale"
|
|
||||||
title={scaleButtonTitle}
|
|
||||||
aria-label={scaleButtonTitle}
|
|
||||||
id="export-canvas-scale"
|
|
||||||
checked={s === scale}
|
|
||||||
onChange={() => setScale(s)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack.Row>
|
|
||||||
</div>
|
|
||||||
{actionManager.renderAction("changeExportBackground")}
|
|
||||||
{someElementIsSelected && (
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={exportSelected}
|
|
||||||
onChange={(event) =>
|
|
||||||
setExportSelected(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.onlySelected")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{actionManager.renderAction("changeExportEmbedScene")}
|
|
||||||
{actionManager.renderAction("changeShouldAddWatermark")}
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ExportDialog = ({
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
exportPadding = 10,
|
|
||||||
actionManager,
|
|
||||||
onExportToPng,
|
|
||||||
onExportToSvg,
|
|
||||||
onExportToClipboard,
|
|
||||||
onExportToBackend,
|
|
||||||
}: {
|
|
||||||
appState: AppState;
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
exportPadding?: number;
|
|
||||||
actionManager: ActionsManagerInterface;
|
|
||||||
onExportToPng: ExportCB;
|
|
||||||
onExportToSvg: ExportCB;
|
|
||||||
onExportToClipboard: ExportCB;
|
|
||||||
onExportToBackend?: ExportCB;
|
|
||||||
}) => {
|
|
||||||
const [modalIsShown, setModalIsShown] = useState(false);
|
|
||||||
const triggerButton = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
|
||||||
setModalIsShown(false);
|
|
||||||
triggerButton.current?.focus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ToolButton
|
|
||||||
onClick={() => {
|
|
||||||
setModalIsShown(true);
|
|
||||||
}}
|
|
||||||
data-testid="export-button"
|
|
||||||
icon={exportFile}
|
|
||||||
type="button"
|
|
||||||
aria-label={t("buttons.export")}
|
|
||||||
showAriaLabel={useIsMobile()}
|
|
||||||
title={t("buttons.export")}
|
|
||||||
ref={triggerButton}
|
|
||||||
/>
|
|
||||||
{modalIsShown && (
|
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
|
||||||
<ExportModal
|
|
||||||
elements={elements}
|
|
||||||
appState={appState}
|
|
||||||
exportPadding={exportPadding}
|
|
||||||
actionManager={actionManager}
|
|
||||||
onExportToPng={onExportToPng}
|
|
||||||
onExportToSvg={onExportToSvg}
|
|
||||||
onExportToClipboard={onExportToClipboard}
|
|
||||||
onExportToBackend={onExportToBackend}
|
|
||||||
onCloseRequest={handleClose}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.FixedSideContainer {
|
.FixedSideContainer {
|
||||||
--margin: 0.25rem;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -10,9 +9,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_top {
|
.FixedSideContainer_side_top {
|
||||||
left: var(--margin);
|
left: var(--space-factor);
|
||||||
top: var(--margin);
|
top: var(--space-factor);
|
||||||
right: var(--margin);
|
right: var(--space-factor);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,16 +22,16 @@
|
|||||||
|
|
||||||
/* TODO: if these are used, make sure to implement RTL support
|
/* TODO: if these are used, make sure to implement RTL support
|
||||||
.FixedSideContainer_side_left {
|
.FixedSideContainer_side_left {
|
||||||
left: var(--margin);
|
left: var(--space-factor);
|
||||||
top: var(--margin);
|
top: var(--space-factor);
|
||||||
bottom: var(--margin);
|
bottom: var(--space-factor);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_right {
|
.FixedSideContainer_side_right {
|
||||||
right: var(--margin);
|
right: var(--space-factor);
|
||||||
top: var(--margin);
|
top: var(--space-factor);
|
||||||
bottom: var(--margin);
|
bottom: var(--space-factor);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -153,10 +153,19 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("toolBar.draw")}
|
label={t("toolBar.freedraw")}
|
||||||
shortcuts={["Shift+P", "7"]}
|
shortcuts={["Shift+P", "7"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||||
|
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
|
||||||
|
<Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
|
||||||
|
<Shortcut
|
||||||
|
label={t("helpDialog.editSelectedShape")}
|
||||||
|
shortcuts={[
|
||||||
|
getShortcutKey("Enter"),
|
||||||
|
t("helpDialog.doubleClick"),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.textNewLine")}
|
label={t("helpDialog.textNewLine")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
@@ -231,6 +240,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.viewMode")}
|
label={t("labels.viewMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+R")]}
|
shortcuts={[getShortcutKey("Alt+R")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.toggleTheme")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+Shift+D")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("stats.title")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+/")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
<Column>
|
||||||
@@ -357,6 +374,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.flipVertical")}
|
label={t("labels.flipVertical")}
|
||||||
shortcuts={[getShortcutKey("Shift+V")]}
|
shortcuts={[getShortcutKey("Shift+V")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.showStroke")}
|
||||||
|
shortcuts={[getShortcutKey("S")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.showBackground")}
|
||||||
|
shortcuts={[getShortcutKey("G")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
</Columns>
|
</Columns>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { questionCircle } from "../components/icons";
|
import { questionCircle } from "../components/icons";
|
||||||
|
|
||||||
type HelpIconProps = {
|
type HelpIconProps = {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ $wide-viewport-width: 1000px;
|
|||||||
color: $oc-gray-6;
|
color: $oc-gray-6;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
position: static;
|
position: static;
|
||||||
padding-right: 2em;
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from "react";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import {
|
||||||
|
isImageElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
|
||||||
interface Hint {
|
interface Hint {
|
||||||
@@ -23,7 +26,7 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.linearElementMulti");
|
return t("hints.linearElementMulti");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elementType === "draw") {
|
if (elementType === "freedraw") {
|
||||||
return t("hints.freeDraw");
|
return t("hints.freeDraw");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +34,10 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.text");
|
return t("hints.text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appState.elementType === "image" && appState.pendingImageElement) {
|
||||||
|
return t("hints.placeImage");
|
||||||
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
if (
|
if (
|
||||||
isResizing &&
|
isResizing &&
|
||||||
@@ -41,7 +48,9 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
|
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
|
||||||
return t("hints.lockAngle");
|
return t("hints.lockAngle");
|
||||||
}
|
}
|
||||||
return t("hints.resize");
|
return isImageElement(targetElement)
|
||||||
|
? t("hints.resizeImage")
|
||||||
|
: t("hints.resize");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRotating && lastPointerDownWith === "mouse") {
|
if (isRotating && lastPointerDownWith === "mouse") {
|
||||||
@@ -57,6 +66,14 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.lineEditor_info");
|
return t("hints.lineEditor_info");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||||
|
return t("hints.text_selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appState.editingElement && isTextElement(appState.editingElement)) {
|
||||||
|
return t("hints.text_editing");
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
:root[dir="rtl"] & {
|
:root[dir="rtl"] & {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ function Picker<T>({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { render, unmountComponentAtNode } from "react-dom";
|
||||||
|
import { ActionsManagerInterface } from "../actions/types";
|
||||||
|
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||||
|
import { canvasToBlob } from "../data/blob";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { CanvasError } from "../errors";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { useIsMobile } from "./App";
|
||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
|
import { exportToCanvas } from "../scene/export";
|
||||||
|
import { AppState, BinaryFiles } from "../types";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { clipboard, exportImage } from "./icons";
|
||||||
|
import Stack from "./Stack";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import "./ExportDialog.scss";
|
||||||
|
import OpenColor from "open-color";
|
||||||
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
|
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||||
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
|
|
||||||
|
const supportsContextFilters =
|
||||||
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
|
|
||||||
|
export const ErrorCanvasPreview = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>{t("canvasError.cannotShowPreview")}</h3>
|
||||||
|
<p>
|
||||||
|
<span>{t("canvasError.canvasTooBig")}</span>
|
||||||
|
</p>
|
||||||
|
<em>({t("canvasError.canvasTooBigTip")})</em>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPreview = (
|
||||||
|
content: HTMLCanvasElement | Error,
|
||||||
|
previewNode: HTMLDivElement,
|
||||||
|
) => {
|
||||||
|
unmountComponentAtNode(previewNode);
|
||||||
|
previewNode.innerHTML = "";
|
||||||
|
if (content instanceof HTMLCanvasElement) {
|
||||||
|
previewNode.appendChild(content);
|
||||||
|
} else {
|
||||||
|
render(<ErrorCanvasPreview />, previewNode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExportCB = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scale?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const ExportButton: React.FC<{
|
||||||
|
color: keyof OpenColor;
|
||||||
|
onClick: () => void;
|
||||||
|
title: string;
|
||||||
|
shade?: number;
|
||||||
|
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="ExportDialog-imageExportButton"
|
||||||
|
style={{
|
||||||
|
["--button-color" as any]: OpenColor[color][shade],
|
||||||
|
["--button-color-darker" as any]: OpenColor[color][shade + 1],
|
||||||
|
["--button-color-darkest" as any]: OpenColor[color][shade + 2],
|
||||||
|
}}
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageExportModal = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
|
actionManager,
|
||||||
|
onExportToPng,
|
||||||
|
onExportToSvg,
|
||||||
|
onExportToClipboard,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
files: BinaryFiles;
|
||||||
|
exportPadding?: number;
|
||||||
|
actionManager: ActionsManagerInterface;
|
||||||
|
onExportToPng: ExportCB;
|
||||||
|
onExportToSvg: ExportCB;
|
||||||
|
onExportToClipboard: ExportCB;
|
||||||
|
onCloseRequest: () => void;
|
||||||
|
}) => {
|
||||||
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { exportBackground, viewBackgroundColor } = appState;
|
||||||
|
|
||||||
|
const exportedElements = exportSelected
|
||||||
|
? getSelectedElements(elements, appState)
|
||||||
|
: elements;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExportSelected(someElementIsSelected);
|
||||||
|
}, [someElementIsSelected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const previewNode = previewRef.current;
|
||||||
|
if (!previewNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exportToCanvas(exportedElements, appState, files, {
|
||||||
|
exportBackground,
|
||||||
|
viewBackgroundColor,
|
||||||
|
exportPadding,
|
||||||
|
})
|
||||||
|
.then((canvas) => {
|
||||||
|
// if converting to blob fails, there's some problem that will
|
||||||
|
// likely prevent preview and export (e.g. canvas too big)
|
||||||
|
return canvasToBlob(canvas).then(() => {
|
||||||
|
renderPreview(canvas, previewNode);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
renderPreview(new CanvasError(), previewNode);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
exportedElements,
|
||||||
|
exportBackground,
|
||||||
|
exportPadding,
|
||||||
|
viewBackgroundColor,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ExportDialog">
|
||||||
|
<div className="ExportDialog__preview" ref={previewRef} />
|
||||||
|
{supportsContextFilters &&
|
||||||
|
actionManager.renderAction("exportWithDarkMode")}
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
|
||||||
|
// dunno why this is needed, but when the items wrap it creates
|
||||||
|
// an overflow
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("changeExportBackground")}
|
||||||
|
{someElementIsSelected && (
|
||||||
|
<CheckboxItem
|
||||||
|
checked={exportSelected}
|
||||||
|
onChange={(checked) => setExportSelected(checked)}
|
||||||
|
>
|
||||||
|
{t("labels.onlySelected")}
|
||||||
|
</CheckboxItem>
|
||||||
|
)}
|
||||||
|
{actionManager.renderAction("changeExportEmbedScene")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
||||||
|
<Stack.Row gap={2}>
|
||||||
|
{actionManager.renderAction("changeExportScale")}
|
||||||
|
</Stack.Row>
|
||||||
|
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: ".6em 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!nativeFileSystemSupported &&
|
||||||
|
actionManager.renderAction("changeProjectName")}
|
||||||
|
</div>
|
||||||
|
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
|
||||||
|
<ExportButton
|
||||||
|
color="indigo"
|
||||||
|
title={t("buttons.exportToPng")}
|
||||||
|
aria-label={t("buttons.exportToPng")}
|
||||||
|
onClick={() => onExportToPng(exportedElements)}
|
||||||
|
>
|
||||||
|
PNG
|
||||||
|
</ExportButton>
|
||||||
|
<ExportButton
|
||||||
|
color="red"
|
||||||
|
title={t("buttons.exportToSvg")}
|
||||||
|
aria-label={t("buttons.exportToSvg")}
|
||||||
|
onClick={() => onExportToSvg(exportedElements)}
|
||||||
|
>
|
||||||
|
SVG
|
||||||
|
</ExportButton>
|
||||||
|
{probablySupportsClipboardBlob && (
|
||||||
|
<ExportButton
|
||||||
|
title={t("buttons.copyPngToClipboard")}
|
||||||
|
onClick={() => onExportToClipboard(exportedElements)}
|
||||||
|
color="gray"
|
||||||
|
shade={7}
|
||||||
|
>
|
||||||
|
{clipboard}
|
||||||
|
</ExportButton>
|
||||||
|
)}
|
||||||
|
</Stack.Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageExportDialog = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
|
actionManager,
|
||||||
|
onExportToPng,
|
||||||
|
onExportToSvg,
|
||||||
|
onExportToClipboard,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
files: BinaryFiles;
|
||||||
|
exportPadding?: number;
|
||||||
|
actionManager: ActionsManagerInterface;
|
||||||
|
onExportToPng: ExportCB;
|
||||||
|
onExportToSvg: ExportCB;
|
||||||
|
onExportToClipboard: ExportCB;
|
||||||
|
}) => {
|
||||||
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
setModalIsShown(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => {
|
||||||
|
setModalIsShown(true);
|
||||||
|
}}
|
||||||
|
data-testid="image-export-button"
|
||||||
|
icon={exportImage}
|
||||||
|
type="button"
|
||||||
|
aria-label={t("buttons.exportImage")}
|
||||||
|
showAriaLabel={useIsMobile()}
|
||||||
|
title={t("buttons.exportImage")}
|
||||||
|
/>
|
||||||
|
{modalIsShown && (
|
||||||
|
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
||||||
|
<ImageExportModal
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
files={files}
|
||||||
|
exportPadding={exportPadding}
|
||||||
|
actionManager={actionManager}
|
||||||
|
onExportToPng={onExportToPng}
|
||||||
|
onExportToSvg={onExportToSvg}
|
||||||
|
onExportToClipboard={onExportToClipboard}
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,30 +1,25 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
|
children: React.ReactElement;
|
||||||
}
|
}
|
||||||
interface State {
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
export class InitializeApp extends React.Component<Props, State> {
|
|
||||||
public state: { isLoading: boolean } = {
|
|
||||||
isLoading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
async componentDidMount() {
|
export const InitializeApp = (props: Props) => {
|
||||||
const currentLang =
|
const [loading, setLoading] = useState(true);
|
||||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
|
||||||
defaultLang;
|
useEffect(() => {
|
||||||
|
const updateLang = async () => {
|
||||||
await setLanguage(currentLang);
|
await setLanguage(currentLang);
|
||||||
this.setState({
|
};
|
||||||
isLoading: false,
|
const currentLang =
|
||||||
});
|
languages.find((lang) => lang.code === props.langCode) || defaultLang;
|
||||||
}
|
updateLang();
|
||||||
|
setLoading(false);
|
||||||
|
}, [props.langCode]);
|
||||||
|
|
||||||
public render() {
|
return loading ? <LoadingMessage /> : props.children;
|
||||||
return this.state.isLoading ? <LoadingMessage /> : this.props.children;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
.Island {
|
.Island {
|
||||||
--padding: 0;
|
--padding: 0;
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
backdrop-filter: saturate(100%) blur(10px);
|
|
||||||
box-shadow: var(--shadow-island);
|
box-shadow: var(--shadow-island);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: calc(var(--padding) * var(--space-factor));
|
padding: calc(var(--padding) * var(--space-factor));
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ActionsManagerInterface } from "../actions/types";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { useIsMobile } from "./App";
|
||||||
|
import { AppState, ExportOpts, BinaryFiles } from "../types";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { exportFile, exportToFileIcon, link } from "./icons";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { actionSaveFileToDisk } from "../actions/actionExport";
|
||||||
|
import { Card } from "./Card";
|
||||||
|
|
||||||
|
import "./ExportDialog.scss";
|
||||||
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
|
|
||||||
|
export type ExportCB = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scale?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const JSONExportModal = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
actionManager,
|
||||||
|
exportOpts,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
files: BinaryFiles;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
actionManager: ActionsManagerInterface;
|
||||||
|
onCloseRequest: () => void;
|
||||||
|
exportOpts: ExportOpts;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
}) => {
|
||||||
|
const { onExportToBackend } = exportOpts;
|
||||||
|
return (
|
||||||
|
<div className="ExportDialog ExportDialog--json">
|
||||||
|
<div className="ExportDialog-cards">
|
||||||
|
{exportOpts.saveFileToDisk && (
|
||||||
|
<Card color="lime">
|
||||||
|
<div className="Card-icon">{exportToFileIcon}</div>
|
||||||
|
<h2>{t("exportDialog.disk_title")}</h2>
|
||||||
|
<div className="Card-details">
|
||||||
|
{t("exportDialog.disk_details")}
|
||||||
|
{!nativeFileSystemSupported &&
|
||||||
|
actionManager.renderAction("changeProjectName")}
|
||||||
|
</div>
|
||||||
|
<ToolButton
|
||||||
|
className="Card-button"
|
||||||
|
type="button"
|
||||||
|
title={t("exportDialog.disk_button")}
|
||||||
|
aria-label={t("exportDialog.disk_button")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={() => {
|
||||||
|
actionManager.executeAction(actionSaveFileToDisk);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{onExportToBackend && (
|
||||||
|
<Card color="pink">
|
||||||
|
<div className="Card-icon">{link}</div>
|
||||||
|
<h2>{t("exportDialog.link_title")}</h2>
|
||||||
|
<div className="Card-details">{t("exportDialog.link_details")}</div>
|
||||||
|
<ToolButton
|
||||||
|
className="Card-button"
|
||||||
|
type="button"
|
||||||
|
title={t("exportDialog.link_button")}
|
||||||
|
aria-label={t("exportDialog.link_button")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={() =>
|
||||||
|
onExportToBackend(elements, appState, files, canvas)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{exportOpts.renderCustomUI &&
|
||||||
|
exportOpts.renderCustomUI(elements, appState, files, canvas)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JSONExportDialog = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
actionManager,
|
||||||
|
exportOpts,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
appState: AppState;
|
||||||
|
files: BinaryFiles;
|
||||||
|
actionManager: ActionsManagerInterface;
|
||||||
|
exportOpts: ExportOpts;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
}) => {
|
||||||
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
setModalIsShown(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => {
|
||||||
|
setModalIsShown(true);
|
||||||
|
}}
|
||||||
|
data-testid="json-export-button"
|
||||||
|
icon={exportFile}
|
||||||
|
type="button"
|
||||||
|
aria-label={t("buttons.export")}
|
||||||
|
showAriaLabel={useIsMobile()}
|
||||||
|
title={t("buttons.export")}
|
||||||
|
/>
|
||||||
|
{modalIsShown && (
|
||||||
|
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||||
|
<JSONExportModal
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
files={files}
|
||||||
|
actionManager={actionManager}
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
exportOpts={exportOpts}
|
||||||
|
canvas={canvas}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+35
-42
@@ -40,50 +40,17 @@
|
|||||||
.layer-ui__wrapper {
|
.layer-ui__wrapper {
|
||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
.encrypted-icon {
|
&__top-right {
|
||||||
position: relative;
|
|
||||||
margin-inline-start: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--space-factor);
|
|
||||||
color: $oc-green-9;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1.2rem;
|
|
||||||
height: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__github-corner {
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
width: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
|
|
||||||
|
&-right {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
bottom: 0;
|
display: flex;
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 190px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zen-mode-transition {
|
.zen-mode-transition {
|
||||||
@@ -105,11 +72,15 @@
|
|||||||
transform: translate(-999px, 0);
|
transform: translate(-999px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="ltr"] &.App-menu_bottom--transition-left {
|
:root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
|
||||||
transform: translate(-92px, 0);
|
transform: translate(-76px, 0);
|
||||||
}
|
}
|
||||||
:root[dir="rtl"] &.App-menu_bottom--transition-left {
|
:root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
|
||||||
transform: translate(92px, 0);
|
transform: translate(76px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layer-ui__wrapper__footer-left--transition-bottom {
|
||||||
|
transform: translate(0, 92px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,5 +108,27 @@
|
|||||||
transition-delay: 0.8s;
|
transition-delay: 0.8s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layer-ui__wrapper__footer-center {
|
||||||
|
pointer-events: none;
|
||||||
|
& > * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layer-ui__wrapper__footer-left,
|
||||||
|
.layer-ui__wrapper__footer-right,
|
||||||
|
.disable-zen-mode--visible {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__wrapper__footer-left {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__wrapper__footer-right {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-inline-end: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+212
-110
@@ -10,17 +10,17 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { CLASSES } from "../constants";
|
import { CLASSES } from "../constants";
|
||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
||||||
import { Library } from "../data/library";
|
|
||||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import {
|
import {
|
||||||
AppProps,
|
AppProps,
|
||||||
AppState,
|
AppState,
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
|
BinaryFiles,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -29,16 +29,15 @@ import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
|||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
import { ExportCB, ExportDialog } from "./ExportDialog";
|
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { GitHubCorner } from "./GitHubCorner";
|
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
import { exportFile, load, shield, trash } from "./icons";
|
import { exportFile, load, trash } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockButton } from "./LockButton";
|
||||||
import { MobileMenu } from "./MobileMenu";
|
import { MobileMenu } from "./MobileMenu";
|
||||||
import { PasteChartDialog } from "./PasteChartDialog";
|
import { PasteChartDialog } from "./PasteChartDialog";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
@@ -47,10 +46,15 @@ import Stack from "./Stack";
|
|||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
|
import Library from "../data/library";
|
||||||
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
|
import { LibraryButton } from "./LibraryButton";
|
||||||
|
import { isImageFileHandle } from "../data/blob";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
|
files: BinaryFiles;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
@@ -63,15 +67,18 @@ interface LayerUIProps {
|
|||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
onExportToBackend?: (
|
renderTopRightUI?: (
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement | null,
|
) => JSX.Element | null;
|
||||||
) => void;
|
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
|
||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@@ -103,26 +110,36 @@ const useOnClickOutside = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LibraryMenuItems = ({
|
const LibraryMenuItems = ({
|
||||||
library,
|
libraryItems,
|
||||||
onRemoveFromLibrary,
|
onRemoveFromLibrary,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
setLibraryItems,
|
setLibraryItems,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
files,
|
||||||
|
id,
|
||||||
}: {
|
}: {
|
||||||
library: LibraryItems;
|
libraryItems: LibraryItems;
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
onRemoveFromLibrary: (index: number) => void;
|
onRemoveFromLibrary: (index: number) => void;
|
||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: (elements: LibraryItem) => void;
|
onAddToLibrary: (elements: LibraryItem) => void;
|
||||||
|
theme: AppState["theme"];
|
||||||
|
files: BinaryFiles;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
setLibraryItems: (library: LibraryItems) => void;
|
setLibraryItems: (library: LibraryItems) => void;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
const numCells = libraryItems.length + (pendingElements.length > 0 ? 1 : 0);
|
||||||
const CELLS_PER_ROW = isMobile ? 4 : 6;
|
const CELLS_PER_ROW = isMobile ? 4 : 6;
|
||||||
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
|
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
|
||||||
const rows = [];
|
const rows = [];
|
||||||
@@ -140,7 +157,7 @@ const LibraryMenuItems = ({
|
|||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
icon={load}
|
icon={load}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
importLibraryFromJSON()
|
importLibraryFromJSON(library)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Close and then open to get the libraries updated
|
// Close and then open to get the libraries updated
|
||||||
setAppState({ isLibraryOpen: false });
|
setAppState({ isLibraryOpen: false });
|
||||||
@@ -152,7 +169,7 @@ const LibraryMenuItems = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!!library.length && (
|
{!!libraryItems.length && (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
key="export"
|
key="export"
|
||||||
@@ -161,7 +178,7 @@ const LibraryMenuItems = ({
|
|||||||
aria-label={t("buttons.export")}
|
aria-label={t("buttons.export")}
|
||||||
icon={exportFile}
|
icon={exportFile}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
saveLibraryAsJSON()
|
saveLibraryAsJSON(library)
|
||||||
.catch(muteFSAbortError)
|
.catch(muteFSAbortError)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setAppState({ errorMessage: error.message });
|
setAppState({ errorMessage: error.message });
|
||||||
@@ -176,8 +193,9 @@ const LibraryMenuItems = ({
|
|||||||
icon={trash}
|
icon={trash}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm(t("alerts.resetLibrary"))) {
|
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||||
Library.resetLibrary();
|
library.resetLibrary();
|
||||||
setLibraryItems([]);
|
setLibraryItems([]);
|
||||||
|
focusContainer();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -186,7 +204,7 @@ const LibraryMenuItems = ({
|
|||||||
<a
|
<a
|
||||||
href={`https://libraries.excalidraw.com?target=${
|
href={`https://libraries.excalidraw.com?target=${
|
||||||
window.name || "_blank"
|
window.name || "_blank"
|
||||||
}&referrer=${referrer}&useHash=true&token=${Library.csrfToken}`}
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`}
|
||||||
target="_excalidraw_libraries"
|
target="_excalidraw_libraries"
|
||||||
>
|
>
|
||||||
{t("labels.libraries")}
|
{t("labels.libraries")}
|
||||||
@@ -201,13 +219,14 @@ const LibraryMenuItems = ({
|
|||||||
const shouldAddPendingElements: boolean =
|
const shouldAddPendingElements: boolean =
|
||||||
pendingElements.length > 0 &&
|
pendingElements.length > 0 &&
|
||||||
!addedPendingElements &&
|
!addedPendingElements &&
|
||||||
y + x >= library.length;
|
y + x >= libraryItems.length;
|
||||||
addedPendingElements = addedPendingElements || shouldAddPendingElements;
|
addedPendingElements = addedPendingElements || shouldAddPendingElements;
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
<Stack.Col key={x}>
|
<Stack.Col key={x}>
|
||||||
<LibraryUnit
|
<LibraryUnit
|
||||||
elements={library[y + x]}
|
elements={libraryItems[y + x]}
|
||||||
|
files={files}
|
||||||
pendingElements={
|
pendingElements={
|
||||||
shouldAddPendingElements ? pendingElements : undefined
|
shouldAddPendingElements ? pendingElements : undefined
|
||||||
}
|
}
|
||||||
@@ -215,7 +234,7 @@ const LibraryMenuItems = ({
|
|||||||
onClick={
|
onClick={
|
||||||
shouldAddPendingElements
|
shouldAddPendingElements
|
||||||
? onAddToLibrary.bind(null, pendingElements)
|
? onAddToLibrary.bind(null, pendingElements)
|
||||||
: onInsertShape.bind(null, library[y + x])
|
: onInsertShape.bind(null, libraryItems[y + x])
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Stack.Col>,
|
</Stack.Col>,
|
||||||
@@ -240,15 +259,25 @@ const LibraryMenu = ({
|
|||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
|
files,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
}: {
|
}: {
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
onClickOutside: (event: MouseEvent) => void;
|
onClickOutside: (event: MouseEvent) => void;
|
||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
|
theme: AppState["theme"];
|
||||||
|
files: BinaryFiles;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useOnClickOutside(ref, (event) => {
|
useOnClickOutside(ref, (event) => {
|
||||||
@@ -265,16 +294,16 @@ const LibraryMenu = ({
|
|||||||
"preloading" | "loading" | "ready"
|
"preloading" | "loading" | "ready"
|
||||||
>("preloading");
|
>("preloading");
|
||||||
|
|
||||||
const loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const loadingTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.race([
|
Promise.race([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
loadingTimerRef.current = setTimeout(() => {
|
loadingTimerRef.current = window.setTimeout(() => {
|
||||||
resolve("loading");
|
resolve("loading");
|
||||||
}, 100);
|
}, 100);
|
||||||
}),
|
}),
|
||||||
Library.loadLibrary().then((items) => {
|
library.loadLibrary().then((items) => {
|
||||||
setLibraryItems(items);
|
setLibraryItems(items);
|
||||||
setIsLoading("ready");
|
setIsLoading("ready");
|
||||||
}),
|
}),
|
||||||
@@ -286,24 +315,39 @@ const LibraryMenu = ({
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(loadingTimerRef.current!);
|
clearTimeout(loadingTimerRef.current!);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [library]);
|
||||||
|
|
||||||
const removeFromLibrary = useCallback(async (indexToRemove) => {
|
const removeFromLibrary = useCallback(
|
||||||
const items = await Library.loadLibrary();
|
async (indexToRemove) => {
|
||||||
|
const items = await library.loadLibrary();
|
||||||
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
||||||
Library.saveLibrary(nextItems);
|
library.saveLibrary(nextItems).catch((error) => {
|
||||||
|
setLibraryItems(items);
|
||||||
|
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||||
|
});
|
||||||
setLibraryItems(nextItems);
|
setLibraryItems(nextItems);
|
||||||
}, []);
|
},
|
||||||
|
[library, setAppState],
|
||||||
|
);
|
||||||
|
|
||||||
const addToLibrary = useCallback(
|
const addToLibrary = useCallback(
|
||||||
async (elements: LibraryItem) => {
|
async (elements: LibraryItem) => {
|
||||||
const items = await Library.loadLibrary();
|
if (elements.some((element) => element.type === "image")) {
|
||||||
|
return setAppState({
|
||||||
|
errorMessage: "Support for adding images to the library coming soon!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await library.loadLibrary();
|
||||||
const nextItems = [...items, elements];
|
const nextItems = [...items, elements];
|
||||||
onAddToLibrary();
|
onAddToLibrary();
|
||||||
Library.saveLibrary(nextItems);
|
library.saveLibrary(nextItems).catch((error) => {
|
||||||
|
setLibraryItems(items);
|
||||||
|
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||||
|
});
|
||||||
setLibraryItems(nextItems);
|
setLibraryItems(nextItems);
|
||||||
},
|
},
|
||||||
[onAddToLibrary],
|
[onAddToLibrary, library, setAppState],
|
||||||
);
|
);
|
||||||
|
|
||||||
return loadingState === "preloading" ? null : (
|
return loadingState === "preloading" ? null : (
|
||||||
@@ -314,7 +358,7 @@ const LibraryMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LibraryMenuItems
|
<LibraryMenuItems
|
||||||
library={libraryItems}
|
libraryItems={libraryItems}
|
||||||
onRemoveFromLibrary={removeFromLibrary}
|
onRemoveFromLibrary={removeFromLibrary}
|
||||||
onAddToLibrary={addToLibrary}
|
onAddToLibrary={addToLibrary}
|
||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertShape}
|
||||||
@@ -322,6 +366,11 @@ const LibraryMenu = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
setLibraryItems={setLibraryItems}
|
setLibraryItems={setLibraryItems}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
|
library={library}
|
||||||
|
theme={theme}
|
||||||
|
files={files}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Island>
|
</Island>
|
||||||
@@ -331,6 +380,7 @@ const LibraryMenu = ({
|
|||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
|
files,
|
||||||
setAppState,
|
setAppState,
|
||||||
canvas,
|
canvas,
|
||||||
elements,
|
elements,
|
||||||
@@ -342,75 +392,86 @@ const LayerUI = ({
|
|||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
toggleZenMode,
|
toggleZenMode,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
onExportToBackend,
|
renderTopRightUI,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
|
onImageAction,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const renderEncryptedIcon = () => (
|
const renderJSONExportDialog = () => {
|
||||||
<a
|
|
||||||
className={clsx("encrypted-icon tooltip zen-mode-visibility", {
|
|
||||||
"zen-mode-visibility--hidden": zenModeEnabled,
|
|
||||||
})}
|
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label={t("encrypted.link")}
|
|
||||||
>
|
|
||||||
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
|
|
||||||
{shield}
|
|
||||||
</Tooltip>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderExportDialog = () => {
|
|
||||||
if (!UIOptions.canvasActions.export) {
|
if (!UIOptions.canvasActions.export) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JSONExportDialog
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
files={files}
|
||||||
|
actionManager={actionManager}
|
||||||
|
exportOpts={UIOptions.canvasActions.export}
|
||||||
|
canvas={canvas}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderImageExportDialog = () => {
|
||||||
|
if (!UIOptions.canvasActions.saveAsImage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const createExporter = (type: ExportType): ExportCB => async (
|
const createExporter = (type: ExportType): ExportCB => async (
|
||||||
exportedElements,
|
exportedElements,
|
||||||
scale,
|
|
||||||
) => {
|
) => {
|
||||||
if (canvas) {
|
const fileHandle = await exportCanvas(
|
||||||
await exportCanvas(type, exportedElements, appState, canvas, {
|
type,
|
||||||
|
exportedElements,
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
{
|
||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
name: appState.name,
|
name: appState.name,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
scale,
|
},
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
)
|
||||||
})
|
|
||||||
.catch(muteFSAbortError)
|
.catch(muteFSAbortError)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setAppState({ errorMessage: error.message });
|
setAppState({ errorMessage: error.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.exportEmbedScene &&
|
||||||
|
fileHandle &&
|
||||||
|
isImageFileHandle(fileHandle)
|
||||||
|
) {
|
||||||
|
setAppState({ fileHandle });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExportDialog
|
<ImageExportDialog
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportToPng={createExporter("png")}
|
onExportToPng={createExporter("png")}
|
||||||
onExportToSvg={createExporter("svg")}
|
onExportToSvg={createExporter("svg")}
|
||||||
onExportToClipboard={createExporter("clipboard")}
|
onExportToClipboard={createExporter("clipboard")}
|
||||||
onExportToBackend={
|
|
||||||
onExportToBackend
|
|
||||||
? (elements) => {
|
|
||||||
onExportToBackend &&
|
|
||||||
onExportToBackend(elements, appState, canvas);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Separator = () => {
|
||||||
|
return <div style={{ width: ".625em" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
const renderViewModeCanvasActions = () => {
|
const renderViewModeCanvasActions = () => {
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
@@ -424,15 +485,15 @@ const LayerUI = ({
|
|||||||
<Island padding={2} style={{ zIndex: 1 }}>
|
<Island padding={2} style={{ zIndex: 1 }}>
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
<Stack.Row gap={1} justifyContent="space-between">
|
<Stack.Row gap={1} justifyContent="space-between">
|
||||||
{actionManager.renderAction("saveScene")}
|
{renderJSONExportDialog()}
|
||||||
{actionManager.renderAction("saveAsScene")}
|
{renderImageExportDialog()}
|
||||||
{renderExportDialog()}
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCanvasActions = () => (
|
const renderCanvasActions = () => (
|
||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
@@ -445,11 +506,12 @@ const LayerUI = ({
|
|||||||
<Island padding={2} style={{ zIndex: 1 }}>
|
<Island padding={2} style={{ zIndex: 1 }}>
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
<Stack.Row gap={1} justifyContent="space-between">
|
<Stack.Row gap={1} justifyContent="space-between">
|
||||||
{actionManager.renderAction("loadScene")}
|
|
||||||
{actionManager.renderAction("saveScene")}
|
|
||||||
{actionManager.renderAction("saveAsScene")}
|
|
||||||
{renderExportDialog()}
|
|
||||||
{actionManager.renderAction("clearCanvas")}
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
<Separator />
|
||||||
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{renderJSONExportDialog()}
|
||||||
|
{renderImageExportDialog()}
|
||||||
|
<Separator />
|
||||||
{onCollabButtonClick && (
|
{onCollabButtonClick && (
|
||||||
<CollabButton
|
<CollabButton
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@@ -464,6 +526,9 @@ const LayerUI = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
/>
|
/>
|
||||||
|
{appState.fileHandle && (
|
||||||
|
<>{actionManager.renderAction("saveToActiveFile")}</>
|
||||||
|
)}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -482,7 +547,8 @@ const LayerUI = ({
|
|||||||
style={{
|
style={{
|
||||||
// we want to make sure this doesn't overflow so substracting 200
|
// we want to make sure this doesn't overflow so substracting 200
|
||||||
// which is approximately height of zoom footer and top left menu items with some buffer
|
// which is approximately height of zoom footer and top left menu items with some buffer
|
||||||
maxHeight: `${appState.height - 200}px`,
|
// if active file name is displayed, subtracting 248 to account for its height
|
||||||
|
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
@@ -517,6 +583,11 @@ const LayerUI = ({
|
|||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
|
library={library}
|
||||||
|
theme={appState.theme}
|
||||||
|
files={files}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -543,6 +614,12 @@ const LayerUI = ({
|
|||||||
{(heading) => (
|
{(heading) => (
|
||||||
<Stack.Col gap={4} align="start">
|
<Stack.Col gap={4} align="start">
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
|
<LockButton
|
||||||
|
zenModeEnabled={zenModeEnabled}
|
||||||
|
checked={appState.elementLocked}
|
||||||
|
onChange={onLockToggle}
|
||||||
|
title={t("toolBar.lock")}
|
||||||
|
/>
|
||||||
<Island
|
<Island
|
||||||
padding={1}
|
padding={1}
|
||||||
className={clsx({ "zen-mode": zenModeEnabled })}
|
className={clsx({ "zen-mode": zenModeEnabled })}
|
||||||
@@ -554,15 +631,17 @@ const LayerUI = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
onImageAction={({ pointerType }) => {
|
||||||
|
onImageAction({
|
||||||
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<LockIcon
|
<LibraryButton
|
||||||
zenModeEnabled={zenModeEnabled}
|
appState={appState}
|
||||||
checked={appState.elementLocked}
|
setAppState={setAppState}
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{libraryMenu}
|
{libraryMenu}
|
||||||
@@ -570,11 +649,15 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
<UserList
|
<div
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||||
|
{
|
||||||
"transition-right": zenModeEnabled,
|
"transition-right": zenModeEnabled,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
|
<UserList>
|
||||||
{appState.collaborators.size > 0 &&
|
{appState.collaborators.size > 0 &&
|
||||||
Array.from(appState.collaborators)
|
Array.from(appState.collaborators)
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
// Collaborator is either not initialized or is actually the current user.
|
||||||
@@ -584,10 +667,14 @@ const LayerUI = ({
|
|||||||
label={client.username || "Unknown user"}
|
label={client.username || "Unknown user"}
|
||||||
key={clientId}
|
key={clientId}
|
||||||
>
|
>
|
||||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
{actionManager.renderAction("goToCollaborator", {
|
||||||
|
id: clientId,
|
||||||
|
})}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</UserList>
|
</UserList>
|
||||||
|
{renderTopRightUI?.(isMobile, appState)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
);
|
);
|
||||||
@@ -595,10 +682,17 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const renderBottomAppMenu = () => {
|
const renderBottomAppMenu = () => {
|
||||||
return (
|
return (
|
||||||
|
<footer
|
||||||
|
role="contentinfo"
|
||||||
|
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx("App-menu App-menu_bottom zen-mode-transition", {
|
className={clsx(
|
||||||
"App-menu_bottom--transition-left": zenModeEnabled,
|
"layer-ui__wrapper__footer-left zen-mode-transition",
|
||||||
})}
|
{
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Stack.Col gap={2}>
|
<Stack.Col gap={2}>
|
||||||
<Section heading="canvasActions">
|
<Section heading="canvasActions">
|
||||||
@@ -608,35 +702,37 @@ const LayerUI = ({
|
|||||||
zoom={appState.zoom}
|
zoom={appState.zoom}
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
{renderEncryptedIcon()}
|
{!viewModeEnabled && (
|
||||||
|
<div
|
||||||
|
className={clsx("undo-redo-buttons zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("undo", { size: "small" })}
|
||||||
|
{actionManager.renderAction("redo", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div
|
||||||
};
|
|
||||||
|
|
||||||
const renderGitHubCorner = () => {
|
|
||||||
return (
|
|
||||||
<aside
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||||
{
|
{
|
||||||
"transition-right": zenModeEnabled,
|
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<GitHubCorner theme={appState.theme} />
|
{renderCustomFooter?.(false, appState)}
|
||||||
</aside>
|
</div>
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderFooter = () => (
|
|
||||||
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
|
||||||
<div
|
<div
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-right zen-mode-transition",
|
||||||
|
{
|
||||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{renderCustomFooter?.(false)}
|
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
{actionManager.renderAction("toggleShortcuts")}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -649,6 +745,7 @@ const LayerUI = ({
|
|||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const dialogs = (
|
const dialogs = (
|
||||||
<>
|
<>
|
||||||
@@ -660,7 +757,11 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.showHelpDialog && (
|
{appState.showHelpDialog && (
|
||||||
<HelpDialog onClose={() => setAppState({ showHelpDialog: false })} />
|
<HelpDialog
|
||||||
|
onClose={() => {
|
||||||
|
setAppState({ showHelpDialog: false });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
<PasteChartDialog
|
<PasteChartDialog
|
||||||
@@ -685,7 +786,8 @@ const LayerUI = ({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
libraryMenu={libraryMenu}
|
libraryMenu={libraryMenu}
|
||||||
exportButton={renderExportDialog()}
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
@@ -694,6 +796,8 @@ const LayerUI = ({
|
|||||||
renderCustomFooter={renderCustomFooter}
|
renderCustomFooter={renderCustomFooter}
|
||||||
viewModeEnabled={viewModeEnabled}
|
viewModeEnabled={viewModeEnabled}
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
|
onImageAction={onImageAction}
|
||||||
|
renderTopRightUI={renderTopRightUI}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -708,8 +812,6 @@ const LayerUI = ({
|
|||||||
{dialogs}
|
{dialogs}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
{renderBottomAppMenu()}
|
{renderBottomAppMenu()}
|
||||||
{renderGitHubCorner()}
|
|
||||||
{renderFooter()}
|
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { capitalizeString } from "../utils";
|
||||||
|
|
||||||
|
const LIBRARY_ICON = (
|
||||||
|
<svg viewBox="0 0 576 512">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LibraryButton: React.FC<{
|
||||||
|
appState: AppState;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}> = ({ appState, setAppState }) => {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
"ToolIcon ToolIcon_type_floating ToolIcon__library zen-mode-visibility",
|
||||||
|
`ToolIcon_size_medium`,
|
||||||
|
{
|
||||||
|
"zen-mode-visibility--hidden": appState.zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
title={`${capitalizeString(t("toolBar.library"))} — 0`}
|
||||||
|
style={{ marginInlineStart: "var(--space-factor)" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="ToolIcon_type_checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
name="editor-library"
|
||||||
|
onChange={(event) => {
|
||||||
|
setAppState({ isLibraryOpen: event.target.checked });
|
||||||
|
}}
|
||||||
|
checked={appState.isLibraryOpen}
|
||||||
|
aria-label={capitalizeString(t("toolBar.library"))}
|
||||||
|
aria-keyshortcuts="0"
|
||||||
|
/>
|
||||||
|
<div className="ToolIcon__icon">{LIBRARY_ICON}</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { close } from "../components/icons";
|
import { close } from "../components/icons";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import { LibraryItem } from "../types";
|
import { BinaryFiles, LibraryItem } from "../types";
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
|
|
||||||
// fa-plus
|
// fa-plus
|
||||||
@@ -21,39 +21,44 @@ const PLUS_ICON = (
|
|||||||
|
|
||||||
export const LibraryUnit = ({
|
export const LibraryUnit = ({
|
||||||
elements,
|
elements,
|
||||||
|
files,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onRemoveFromLibrary,
|
onRemoveFromLibrary,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
elements?: LibraryItem;
|
elements?: LibraryItem;
|
||||||
|
files: BinaryFiles;
|
||||||
pendingElements?: LibraryItem;
|
pendingElements?: LibraryItem;
|
||||||
onRemoveFromLibrary: () => void;
|
onRemoveFromLibrary: () => void;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
const elementsToRender = elements || pendingElements;
|
const elementsToRender = elements || pendingElements;
|
||||||
if (!elementsToRender) {
|
if (!elementsToRender) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const svg = exportToSvg(elementsToRender, {
|
const svg = await exportToSvg(
|
||||||
|
elementsToRender,
|
||||||
|
{
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
shouldAddWatermark: false,
|
},
|
||||||
});
|
files,
|
||||||
for (const child of ref.current!.children) {
|
);
|
||||||
if (child.tagName !== "svg") {
|
node.innerHTML = svg.outerHTML;
|
||||||
continue;
|
})();
|
||||||
}
|
|
||||||
ref.current!.removeChild(child);
|
|
||||||
}
|
|
||||||
ref.current!.appendChild(svg);
|
|
||||||
|
|
||||||
const current = ref.current!;
|
|
||||||
return () => {
|
return () => {
|
||||||
current.removeChild(svg);
|
node.innerHTML = "";
|
||||||
};
|
};
|
||||||
}, [elements, pendingElements]);
|
}, [elements, pendingElements, files]);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
export const LoadingMessage = () => {
|
export const LoadingMessage = () => {
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { ToolButtonSize } from "./ToolButton";
|
||||||
type LockIconSize = "s" | "m";
|
|
||||||
|
|
||||||
type LockIconProps = {
|
type LockIconProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
size?: LockIconSize;
|
|
||||||
zenModeEnabled?: boolean;
|
zenModeEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SIZE: LockIconSize = "m";
|
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
CHECKED: (
|
CHECKED: (
|
||||||
@@ -41,12 +38,12 @@ const ICONS = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LockIcon = (props: LockIconProps) => {
|
export const LockButton = (props: LockIconProps) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
|
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
|
||||||
`ToolIcon_size_${props.size || DEFAULT_SIZE}`,
|
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||||
{
|
{
|
||||||
"zen-mode-visibility--hidden": props.zenModeEnabled,
|
"zen-mode-visibility--hidden": props.zenModeEnabled,
|
||||||
},
|
},
|
||||||
@@ -57,7 +54,6 @@ export const LockIcon = (props: LockIconProps) => {
|
|||||||
className="ToolIcon_type_checkbox"
|
className="ToolIcon_type_checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={props.name}
|
name={props.name}
|
||||||
id={props.id}
|
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
aria-label={props.title}
|
aria-label={props.title}
|
||||||
@@ -13,14 +13,16 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
|||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockButton } from "./LockButton";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
|
import { LibraryButton } from "./LibraryButton";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
exportButton: React.ReactNode;
|
renderJSONExportDialog: () => React.ReactNode;
|
||||||
|
renderImageExportDialog: () => React.ReactNode;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
libraryMenu: JSX.Element | null;
|
libraryMenu: JSX.Element | null;
|
||||||
@@ -28,9 +30,14 @@ type MobileMenuProps = {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
showThemeBtn: boolean;
|
showThemeBtn: boolean;
|
||||||
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
|
renderTopRightUI?: (
|
||||||
|
isMobile: boolean,
|
||||||
|
appState: AppState,
|
||||||
|
) => JSX.Element | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -38,7 +45,8 @@ export const MobileMenu = ({
|
|||||||
elements,
|
elements,
|
||||||
libraryMenu,
|
libraryMenu,
|
||||||
actionManager,
|
actionManager,
|
||||||
exportButton,
|
renderJSONExportDialog,
|
||||||
|
renderImageExportDialog,
|
||||||
setAppState,
|
setAppState,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
@@ -47,6 +55,8 @@ export const MobileMenu = ({
|
|||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
|
onImageAction,
|
||||||
|
renderTopRightUI,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
@@ -62,15 +72,21 @@ export const MobileMenu = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
onImageAction={({ pointerType }) => {
|
||||||
|
onImageAction({
|
||||||
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<LockIcon
|
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||||
|
<LockButton
|
||||||
checked={appState.elementLocked}
|
checked={appState.elementLocked}
|
||||||
onChange={onLockToggle}
|
onChange={onLockToggle}
|
||||||
title={t("toolBar.lock")}
|
title={t("toolBar.lock")}
|
||||||
/>
|
/>
|
||||||
|
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{libraryMenu}
|
{libraryMenu}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
@@ -107,19 +123,17 @@ export const MobileMenu = ({
|
|||||||
if (viewModeEnabled) {
|
if (viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{actionManager.renderAction("saveScene")}
|
{renderJSONExportDialog()}
|
||||||
{actionManager.renderAction("saveAsScene")}
|
{renderImageExportDialog()}
|
||||||
{exportButton}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{actionManager.renderAction("loadScene")}
|
|
||||||
{actionManager.renderAction("saveScene")}
|
|
||||||
{actionManager.renderAction("saveAsScene")}
|
|
||||||
{exportButton}
|
|
||||||
{actionManager.renderAction("clearCanvas")}
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{renderJSONExportDialog()}
|
||||||
|
{renderImageExportDialog()}
|
||||||
{onCollabButtonClick && (
|
{onCollabButtonClick && (
|
||||||
<CollabButton
|
<CollabButton
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@@ -155,7 +169,7 @@ export const MobileMenu = ({
|
|||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{renderCustomFooter?.(true)}
|
{renderCustomFooter?.(true, appState)}
|
||||||
{appState.collaborators.size > 0 && (
|
{appState.collaborators.size > 0 && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
@@ -167,10 +181,9 @@ export const MobileMenu = ({
|
|||||||
)
|
)
|
||||||
.map(([clientId, client]) => (
|
.map(([clientId, client]) => (
|
||||||
<React.Fragment key={clientId}>
|
<React.Fragment key={clientId}>
|
||||||
{actionManager.renderAction(
|
{actionManager.renderAction("goToCollaborator", {
|
||||||
"goToCollaborator",
|
id: clientId,
|
||||||
clientId,
|
})}
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</UserList>
|
</UserList>
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: transparentize($oc-black, 0.7);
|
background-color: transparentize($oc-black, 0.3);
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal__content {
|
.Modal__content {
|
||||||
@@ -45,14 +44,17 @@
|
|||||||
|
|
||||||
// for modals, reset blurry bg
|
// for modals, reset blurry bg
|
||||||
background: var(--island-bg-color);
|
background: var(--island-bg-color);
|
||||||
backdrop-filter: none;
|
|
||||||
|
|
||||||
border: 1px solid var(--dialog-border-color);
|
border: 1px solid var(--dialog-border-color);
|
||||||
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -82,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.Modal {
|
.Modal {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import "./Modal.scss";
|
import "./Modal.scss";
|
||||||
|
|
||||||
import React, { useState, useLayoutEffect } from "react";
|
import React, { useState, useLayoutEffect, useRef } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { useExcalidrawContainer, useIsMobile } from "./App";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -11,8 +14,10 @@ export const Modal = (props: {
|
|||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
labelledBy: string;
|
labelledBy: string;
|
||||||
|
theme?: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
const modalRoot = useBodyRoot();
|
const { theme = THEME.LIGHT } = props;
|
||||||
|
const modalRoot = useBodyRoot(theme);
|
||||||
|
|
||||||
if (!modalRoot) {
|
if (!modalRoot) {
|
||||||
return null;
|
return null;
|
||||||
@@ -21,6 +26,7 @@ export const Modal = (props: {
|
|||||||
const handleKeydown = (event: React.KeyboardEvent) => {
|
const handleKeydown = (event: React.KeyboardEvent) => {
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
props.onCloseRequest();
|
props.onCloseRequest();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -37,6 +43,7 @@ export const Modal = (props: {
|
|||||||
<div
|
<div
|
||||||
className="Modal__content"
|
className="Modal__content"
|
||||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,16 +52,29 @@ export const Modal = (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useBodyRoot = () => {
|
const useBodyRoot = (theme: AppState["theme"]) => {
|
||||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const isMobileRef = useRef(isMobile);
|
||||||
|
isMobileRef.current = isMobile;
|
||||||
|
|
||||||
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const isDarkTheme = !!document
|
if (div) {
|
||||||
.querySelector(".excalidraw")
|
div.classList.toggle("excalidraw--mobile", isMobile);
|
||||||
?.classList.contains("theme--dark");
|
}
|
||||||
|
}, [div, isMobile]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const isDarkTheme =
|
||||||
|
!!excalidrawContainer?.classList.contains("theme--dark") ||
|
||||||
|
theme === "dark";
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||||
|
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||||
|
|
||||||
if (isDarkTheme) {
|
if (isDarkTheme) {
|
||||||
div.classList.add("theme--dark");
|
div.classList.add("theme--dark");
|
||||||
@@ -67,7 +87,7 @@ const useBodyRoot = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
document.body.removeChild(div);
|
document.body.removeChild(div);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [excalidrawContainer, theme]);
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.PasteChartDialog {
|
.PasteChartDialog {
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.Island {
|
.Island {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,20 +34,25 @@ const ChartPreviewBtn = (props: {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
setChartElements(elements);
|
setChartElements(elements);
|
||||||
|
let svg: SVGSVGElement;
|
||||||
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
const svg = exportToSvg(elements, {
|
(async () => {
|
||||||
|
svg = await exportToSvg(
|
||||||
|
elements,
|
||||||
|
{
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
shouldAddWatermark: false,
|
},
|
||||||
});
|
null, // files
|
||||||
|
);
|
||||||
const previewNode = previewRef.current!;
|
|
||||||
|
|
||||||
previewNode.appendChild(svg);
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
if (props.selected) {
|
if (props.selected) {
|
||||||
(previewNode.parentNode as HTMLDivElement).focus();
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode.removeChild(svg);
|
previewNode.removeChild(svg);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.ProjectName {
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.TextInput {
|
||||||
|
height: calc(1rem - 3px);
|
||||||
|
width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&--readonly {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
width: auto;
|
||||||
|
max-width: 200px;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import "./TextInput.scss";
|
import "./TextInput.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { focusNearestParent } from "../utils";
|
||||||
|
|
||||||
|
import "./ProjectName.scss";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -9,21 +13,19 @@ type Props = {
|
|||||||
isNameEditable: boolean;
|
isNameEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
export const ProjectName = (props: Props) => {
|
||||||
fileName: string;
|
const { id } = useExcalidrawContainer();
|
||||||
};
|
const [fileName, setFileName] = useState<string>(props.value);
|
||||||
export class ProjectName extends Component<Props, State> {
|
|
||||||
state = {
|
const handleBlur = (event: any) => {
|
||||||
fileName: this.props.value,
|
focusNearestParent(event.target);
|
||||||
};
|
|
||||||
private handleBlur = (event: any) => {
|
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value !== this.props.value) {
|
if (value !== props.value) {
|
||||||
this.props.onChange(value);
|
props.onChange(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.nativeEvent.isComposing || event.keyCode === 229) {
|
if (event.nativeEvent.isComposing || event.keyCode === 229) {
|
||||||
@@ -33,29 +35,25 @@ export class ProjectName extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="ProjectName">
|
||||||
<label htmlFor="file-name">
|
<label className="ProjectName-label" htmlFor="filename">
|
||||||
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
{`${props.label}${props.isNameEditable ? "" : ":"}`}
|
||||||
</label>
|
</label>
|
||||||
{this.props.isNameEditable ? (
|
{props.isNameEditable ? (
|
||||||
<input
|
<input
|
||||||
className="TextInput"
|
className="TextInput"
|
||||||
onBlur={this.handleBlur}
|
onBlur={handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
id="file-name"
|
id={`${id}-filename`}
|
||||||
value={this.state.fileName}
|
value={fileName}
|
||||||
onChange={(event) =>
|
onChange={(event) => setFileName(event.target.value)}
|
||||||
this.setState({ fileName: event.target.value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="TextInput TextInput--readonly" id="file-name">
|
<span className="TextInput TextInput--readonly" id={`${id}-filename`}>
|
||||||
{this.props.value}
|
{props.value}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
||||||
heading: string;
|
heading: string;
|
||||||
@@ -7,13 +8,14 @@ interface SectionProps extends React.HTMLProps<HTMLElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
||||||
|
const { id } = useExcalidrawContainer();
|
||||||
const header = (
|
const header = (
|
||||||
<h2 className="visually-hidden" id={`${heading}-title`}>
|
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
||||||
{t(`headings.${heading}`)}
|
{t(`headings.${heading}`)}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<section {...props} aria-labelledby={`${heading}-title`}>
|
<section {...props} aria-labelledby={`${id}-${heading}-title`}>
|
||||||
{typeof children === "function" ? (
|
{typeof children === "function" ? (
|
||||||
children(header)
|
children(header)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@import "open-color/open-color.scss";
|
||||||
|
|
||||||
|
$duration: 1.6s;
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.Spinner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
--spinner-color: var(--icon-fill-color);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
animation: rotate $duration linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
circle {
|
||||||
|
stroke: var(--spinner-color);
|
||||||
|
animation: dash $duration linear 0s infinite;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 300;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 150, 300;
|
||||||
|
stroke-dashoffset: -200;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 1, 300;
|
||||||
|
stroke-dashoffset: -280;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import "./Spinner.scss";
|
||||||
|
|
||||||
|
const Spinner = ({
|
||||||
|
size = "1em",
|
||||||
|
circleWidth = 8,
|
||||||
|
}: {
|
||||||
|
size?: string | number;
|
||||||
|
circleWidth?: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="Spinner">
|
||||||
|
<svg viewBox="0 0 100 100" style={{ width: size, height: size }}>
|
||||||
|
<circle
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r={50 - circleWidth / 2}
|
||||||
|
strokeWidth={circleWidth}
|
||||||
|
fill="none"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
top: 64px;
|
top: 64px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
z-index: 999;
|
z-index: 10;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 24px 8px 0;
|
margin: 0 24px 8px 0;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { TOAST_TIMEOUT } from "../constants";
|
import { TOAST_TIMEOUT } from "../constants";
|
||||||
import "./Toast.scss";
|
import "./Toast.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import "./ToolIcon.scss";
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
import { AbortError } from "../errors";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import { PointerType } from "../element/types";
|
||||||
|
|
||||||
type ToolIconSize = "s" | "m";
|
export type ToolButtonSize = "small" | "medium";
|
||||||
|
|
||||||
type ToolButtonBaseProps = {
|
type ToolButtonBaseProps = {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
@@ -14,7 +18,7 @@ type ToolButtonBaseProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
size?: ToolIconSize;
|
size?: ToolButtonSize;
|
||||||
keyBindingLabel?: string;
|
keyBindingLabel?: string;
|
||||||
showAriaLabel?: boolean;
|
showAriaLabel?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
@@ -27,23 +31,58 @@ type ToolButtonProps =
|
|||||||
| (ToolButtonBaseProps & {
|
| (ToolButtonBaseProps & {
|
||||||
type: "button";
|
type: "button";
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
onClick?(event: React.MouseEvent): void;
|
||||||
|
})
|
||||||
|
| (ToolButtonBaseProps & {
|
||||||
|
type: "icon";
|
||||||
|
children?: React.ReactNode;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
})
|
})
|
||||||
| (ToolButtonBaseProps & {
|
| (ToolButtonBaseProps & {
|
||||||
type: "radio";
|
type: "radio";
|
||||||
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(data: { pointerType: PointerType | null }): void;
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_SIZE: ToolIconSize = "m";
|
|
||||||
|
|
||||||
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||||
|
const { id: excalId } = useExcalidrawContainer();
|
||||||
const innerRef = React.useRef(null);
|
const innerRef = React.useRef(null);
|
||||||
React.useImperativeHandle(ref, () => innerRef.current);
|
React.useImperativeHandle(ref, () => innerRef.current);
|
||||||
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
|
const sizeCn = `ToolIcon_size_${props.size}`;
|
||||||
|
|
||||||
if (props.type === "button") {
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
|
const onClick = async (event: React.MouseEvent) => {
|
||||||
|
const ret = "onClick" in props && props.onClick?.(event);
|
||||||
|
|
||||||
|
if (ret && "then" in ret) {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await ret;
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof AbortError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
||||||
|
|
||||||
|
if (props.type === "button" || props.type === "icon") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -56,6 +95,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
{
|
{
|
||||||
ToolIcon: !props.hidden,
|
ToolIcon: !props.hidden,
|
||||||
"ToolIcon--selected": props.selected,
|
"ToolIcon--selected": props.selected,
|
||||||
|
"ToolIcon--plain": props.type === "icon",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
data-testid={props["data-testid"]}
|
data-testid={props["data-testid"]}
|
||||||
@@ -63,9 +103,11 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
title={props.title}
|
title={props.title}
|
||||||
aria-label={props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={props.onClick}
|
onClick={onClick}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
|
{(props.icon || props.label) && (
|
||||||
<div className="ToolIcon__icon" aria-hidden="true">
|
<div className="ToolIcon__icon" aria-hidden="true">
|
||||||
{props.icon || props.label}
|
{props.icon || props.label}
|
||||||
{props.keyBindingLabel && (
|
{props.keyBindingLabel && (
|
||||||
@@ -74,8 +116,11 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{props.showAriaLabel && (
|
{props.showAriaLabel && (
|
||||||
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
<div className="ToolIcon__label">
|
||||||
|
{props["aria-label"]} {isLoading && <Spinner />}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
@@ -83,7 +128,18 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={clsx("ToolIcon", props.className)} title={props.title}>
|
<label
|
||||||
|
className={clsx("ToolIcon", props.className)}
|
||||||
|
title={props.title}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
lastPointerTypeRef.current = event.pointerType || null;
|
||||||
|
}}
|
||||||
|
onPointerUp={() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
lastPointerTypeRef.current = null;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
className={`ToolIcon_type_radio ${sizeCn}`}
|
className={`ToolIcon_type_radio ${sizeCn}`}
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -91,8 +147,10 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
aria-label={props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
||||||
data-testid={props["data-testid"]}
|
data-testid={props["data-testid"]}
|
||||||
id={props.id}
|
id={`${excalId}-${props.id}`}
|
||||||
onChange={props.onChange}
|
onChange={() => {
|
||||||
|
props.onChange?.({ pointerType: lastPointerTypeRef.current });
|
||||||
|
}}
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
/>
|
/>
|
||||||
@@ -109,4 +167,5 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
ToolButton.defaultProps = {
|
ToolButton.defaultProps = {
|
||||||
visible: true,
|
visible: true,
|
||||||
className: "",
|
className: "",
|
||||||
|
size: "medium",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,26 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
font-family: Cascadia;
|
font-family: Cascadia;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--button-gray-1);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
background-color: var(--button-gray-1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon--plain {
|
||||||
|
background-color: transparent;
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
@@ -37,15 +54,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__label {
|
.ToolIcon__label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
color: var(--icon-fill-color);
|
color: var(--icon-fill-color);
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
margin: 0 0.8em;
|
margin: 0 0.8em;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.Spinner {
|
||||||
|
margin-left: 0.6em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon_size_s .ToolIcon__icon {
|
.ToolIcon_size_small .ToolIcon__icon {
|
||||||
width: 1.4rem;
|
width: 2rem;
|
||||||
height: 1.4rem;
|
height: 2rem;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,14 +80,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--button-gray-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--button-gray-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||||
}
|
}
|
||||||
@@ -77,6 +92,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
&--show {
|
&--show {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -94,6 +117,9 @@
|
|||||||
|
|
||||||
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
|
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
|
||||||
background-color: var(--button-gray-2);
|
background-color: var(--button-gray-2);
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus + .ToolIcon__icon {
|
&:focus + .ToolIcon__icon {
|
||||||
@@ -121,12 +147,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
|
background-color: var(--button-gray-1);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon.ToolIcon__lock {
|
.ToolIcon.ToolIcon__lock {
|
||||||
|
margin-inline-end: var(--space-factor);
|
||||||
&.ToolIcon_type_floating {
|
&.ToolIcon_type_floating {
|
||||||
margin-left: 0.1rem;
|
margin-left: 0.1rem;
|
||||||
}
|
}
|
||||||
@@ -157,10 +192,9 @@
|
|||||||
// move the lock button out of the way on small viewports
|
// move the lock button out of the way on small viewports
|
||||||
// it begins to collide with the GitHub icon before we switch to mobile mode
|
// it begins to collide with the GitHub icon before we switch to mobile mode
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.ToolIcon.ToolIcon__lock {
|
.ToolIcon.ToolIcon_type_floating {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 60px;
|
|
||||||
right: -8px;
|
right: -8px;
|
||||||
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@@ -185,16 +219,13 @@
|
|||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ToolIcon.ToolIcon__library {
|
||||||
|
top: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TooltipIcon {
|
.ToolIcon.ToolIcon__lock {
|
||||||
width: 0.9em;
|
margin-inline-end: 0;
|
||||||
height: 0.9em;
|
top: 60px;
|
||||||
margin-left: 5px;
|
|
||||||
margin-top: 1px;
|
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-43
@@ -1,58 +1,45 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
.excalidraw {
|
|
||||||
.Tooltip {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Tooltip__label {
|
// container in body where the actual tooltip is appended to
|
||||||
--arrow-size: 4px;
|
.excalidraw-tooltip {
|
||||||
visibility: hidden;
|
|
||||||
background: $oc-black;
|
|
||||||
color: $oc-white;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 1000;
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
padding: 8px;
|
||||||
font-weight: 500;
|
border-radius: 6px;
|
||||||
// extra pixel offset for unknown reasons
|
box-sizing: border-box;
|
||||||
left: calc(50% + var(--arrow-size) / 2 - 1px);
|
pointer-events: none;
|
||||||
transform: translateX(-50%);
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
&::after {
|
background: $oc-black;
|
||||||
content: "";
|
|
||||||
border: var(--arrow-size) solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
left: calc(50% - var(--arrow-size));
|
|
||||||
}
|
|
||||||
|
|
||||||
&--above {
|
line-height: 1.5;
|
||||||
bottom: calc(100% + var(--arrow-size) + 3px);
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
&::after {
|
display: none;
|
||||||
border-top-color: $oc-black;
|
|
||||||
top: 100%;
|
&.excalidraw-tooltip--visible {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--below {
|
// wraps the element we want to apply the tooltip to
|
||||||
top: calc(100% + var(--arrow-size) + 3px);
|
.excalidraw-tooltip-wrapper {
|
||||||
|
display: flex;
|
||||||
&::after {
|
height: 100%;
|
||||||
border-bottom-color: $oc-black;
|
|
||||||
bottom: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tooltip:hover .Tooltip__label {
|
.excalidraw-tooltip-icon {
|
||||||
visibility: visible;
|
width: 0.9em;
|
||||||
}
|
height: 0.9em;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 1px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
.Tooltip__label:hover {
|
@include isMobile {
|
||||||
visibility: visible;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-16
@@ -1,31 +1,93 @@
|
|||||||
import "./Tooltip.scss";
|
import "./Tooltip.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
const getTooltipDiv = () => {
|
||||||
|
const existingDiv = document.querySelector<HTMLDivElement>(
|
||||||
|
".excalidraw-tooltip",
|
||||||
|
);
|
||||||
|
if (existingDiv) {
|
||||||
|
return existingDiv;
|
||||||
|
}
|
||||||
|
const div = document.createElement("div");
|
||||||
|
document.body.appendChild(div);
|
||||||
|
div.classList.add("excalidraw-tooltip");
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTooltip = (
|
||||||
|
item: HTMLDivElement,
|
||||||
|
tooltip: HTMLDivElement,
|
||||||
|
label: string,
|
||||||
|
long: boolean,
|
||||||
|
) => {
|
||||||
|
tooltip.classList.add("excalidraw-tooltip--visible");
|
||||||
|
tooltip.style.minWidth = long ? "50ch" : "10ch";
|
||||||
|
tooltip.style.maxWidth = long ? "50ch" : "15ch";
|
||||||
|
|
||||||
|
tooltip.textContent = label;
|
||||||
|
|
||||||
|
const {
|
||||||
|
x: itemX,
|
||||||
|
bottom: itemBottom,
|
||||||
|
top: itemTop,
|
||||||
|
width: itemWidth,
|
||||||
|
} = item.getBoundingClientRect();
|
||||||
|
|
||||||
|
const {
|
||||||
|
width: labelWidth,
|
||||||
|
height: labelHeight,
|
||||||
|
} = tooltip.getBoundingClientRect();
|
||||||
|
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
const margin = 5;
|
||||||
|
|
||||||
|
const left = itemX + itemWidth / 2 - labelWidth / 2;
|
||||||
|
const offsetLeft =
|
||||||
|
left + labelWidth >= viewportWidth ? left + labelWidth - viewportWidth : 0;
|
||||||
|
|
||||||
|
const top = itemBottom + margin;
|
||||||
|
const offsetTop =
|
||||||
|
top + labelHeight >= viewportHeight
|
||||||
|
? itemBottom - itemTop + labelHeight + margin * 2
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
Object.assign(tooltip.style, {
|
||||||
|
top: `${top - offsetTop}px`,
|
||||||
|
left: `${left - offsetLeft}px`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
type TooltipProps = {
|
type TooltipProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
position?: "above" | "below";
|
|
||||||
long?: boolean;
|
long?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tooltip = ({
|
export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
|
||||||
children,
|
useEffect(() => {
|
||||||
|
return () =>
|
||||||
|
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="excalidraw-tooltip-wrapper"
|
||||||
|
onPointerEnter={(event) =>
|
||||||
|
updateTooltip(
|
||||||
|
event.currentTarget as HTMLDivElement,
|
||||||
|
getTooltipDiv(),
|
||||||
label,
|
label,
|
||||||
position = "below",
|
long,
|
||||||
long = false,
|
)
|
||||||
}: TooltipProps) => (
|
}
|
||||||
<div className="Tooltip">
|
onPointerLeave={() =>
|
||||||
<span
|
getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
|
||||||
className={
|
|
||||||
position === "above"
|
|
||||||
? "Tooltip__label Tooltip__label--above"
|
|
||||||
: "Tooltip__label Tooltip__label--below"
|
|
||||||
}
|
}
|
||||||
style={{ width: long ? "50ch" : "10ch" }}
|
|
||||||
>
|
>
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
.UserList {
|
.UserList {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/*github corner*/
|
/*github corner*/
|
||||||
padding: var(--space-factor) 40px var(--space-factor) var(--space-factor);
|
padding: var(--space-factor) var(--space-factor) var(--space-factor)
|
||||||
|
var(--space-factor);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
+108
-173
@@ -10,13 +10,15 @@ import React from "react";
|
|||||||
|
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
const activeElementColor = (theme: "light" | "dark") =>
|
const activeElementColor = (theme: Theme) =>
|
||||||
theme === "light" ? oc.orange[4] : oc.orange[9];
|
theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9];
|
||||||
const iconFillColor = (theme: "light" | "dark") =>
|
const iconFillColor = (theme: Theme) =>
|
||||||
theme === "light" ? oc.black : oc.gray[4];
|
theme === THEME.LIGHT ? oc.black : oc.gray[4];
|
||||||
const handlerColor = (theme: "light" | "dark") =>
|
const handlerColor = (theme: Theme) =>
|
||||||
theme === "light" ? oc.white : "#1e1e1e";
|
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
width?: number;
|
width?: number;
|
||||||
@@ -24,7 +26,10 @@ type Opts = {
|
|||||||
mirror?: true;
|
mirror?: true;
|
||||||
} & React.SVGProps<SVGSVGElement>;
|
} & React.SVGProps<SVGSVGElement>;
|
||||||
|
|
||||||
const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
|
export const createIcon = (
|
||||||
|
d: string | React.ReactNode,
|
||||||
|
opts: number | Opts = 512,
|
||||||
|
) => {
|
||||||
const { width = 512, height = width, mirror, style } =
|
const { width = 512, height = width, mirror, style } =
|
||||||
typeof opts === "number" ? ({ width: opts } as Opts) : opts;
|
typeof opts === "number" ? ({ width: opts } as Opts) : opts;
|
||||||
return (
|
return (
|
||||||
@@ -41,6 +46,14 @@ const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkIcon = createIcon(
|
||||||
|
<polyline fill="none" stroke="currentColor" points="20 6 9 17 4 12" />,
|
||||||
|
{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const link = createIcon(
|
export const link = createIcon(
|
||||||
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
|
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
|
||||||
{ mirror: true },
|
{ mirror: true },
|
||||||
@@ -80,6 +93,19 @@ export const exportFile = createIcon(
|
|||||||
{ width: 576, height: 512, mirror: true },
|
{ width: 576, height: 512, mirror: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const exportImage = createIcon(
|
||||||
|
<>
|
||||||
|
<path d="M571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-187 44v-64 64z" />
|
||||||
|
<path d="M384 121.941V128H256V0h6.059c6.362 0 12.471 2.53 16.97 7.029l97.941 97.941a24.01 24.01 0 017.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z" />
|
||||||
|
</>,
|
||||||
|
{ width: 576, height: 512, mirror: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const exportToFileIcon = createIcon(
|
||||||
|
"M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z",
|
||||||
|
{ width: 512, height: 512 },
|
||||||
|
);
|
||||||
|
|
||||||
export const zoomIn = createIcon(
|
export const zoomIn = createIcon(
|
||||||
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
|
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
@@ -151,8 +177,7 @@ export const resetZoom = createIcon(
|
|||||||
{ width: 1024 },
|
{ width: 1024 },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BringForwardIcon = React.memo(
|
export const BringForwardIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -172,8 +197,7 @@ export const BringForwardIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SendBackwardIcon = React.memo(
|
export const SendBackwardIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -193,8 +217,7 @@ export const SendBackwardIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BringToFrontIcon = React.memo(
|
export const BringToFrontIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -214,22 +237,19 @@ export const BringToFrontIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SendToBackIcon = React.memo(
|
export const SendToBackIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
|
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
|
||||||
fill={activeElementColor(theme)}
|
fill={activeElementColor(theme)}
|
||||||
stroke={activeElementColor(theme)}
|
stroke={activeElementColor(theme)}
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h8V3zM22 14a1 1 0 00-1-1h-7a1 1 0 00-1 1v7a1 1 0 001 1h8v-8z"
|
d="M11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h8V3zM22 14a1 1 0 00-1-1h-7a1 1 0 00-1 1v7a1 1 0 001 1h8v-8z"
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
@@ -243,8 +263,7 @@ export const SendToBackIcon = React.memo(
|
|||||||
// first one the user sees. Horizontal align icons should not be flipped since
|
// first one the user sees. Horizontal align icons should not be flipped since
|
||||||
// that would make them lie about their function.
|
// that would make them lie about their function.
|
||||||
//
|
//
|
||||||
export const AlignTopIcon = React.memo(
|
export const AlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -265,8 +284,7 @@ export const AlignTopIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AlignBottomIcon = React.memo(
|
export const AlignBottomIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -287,8 +305,7 @@ export const AlignBottomIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AlignLeftIcon = React.memo(
|
export const AlignLeftIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -309,8 +326,7 @@ export const AlignLeftIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AlignRightIcon = React.memo(
|
export const AlignRightIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -332,10 +348,9 @@ export const AlignRightIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DistributeHorizontallyIcon = React.memo(
|
export const DistributeHorizontallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M5 5V19Z" fill="black" />
|
|
||||||
<path
|
<path
|
||||||
d="M19 5V19M5 5V19"
|
d="M19 5V19M5 5V19"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
@@ -353,16 +368,8 @@ export const DistributeHorizontallyIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
></svg>;
|
|
||||||
|
|
||||||
export const DistributeVerticallyIcon = React.memo(
|
export const DistributeVerticallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -383,8 +390,7 @@ export const DistributeVerticallyIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CenterVerticallyIcon = React.memo(
|
export const CenterVerticallyIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -407,7 +413,7 @@ export const CenterVerticallyIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const CenterHorizontallyIcon = React.memo(
|
export const CenterHorizontallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -464,7 +470,12 @@ export const shield = createIcon(
|
|||||||
{ width: 24 },
|
{ width: 24 },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
|
export const file = createIcon(
|
||||||
|
"M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm32-48h224V288l-23.5-23.5c-4.7-4.7-12.3-4.7-17 0L176 352l-39.5-39.5c-4.7-4.7-12.3-4.7-17 0L80 352v64zm48-240c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z",
|
||||||
|
{ width: 384, height: 512 },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GroupIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
||||||
@@ -479,49 +490,22 @@ export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
|
|||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<rect
|
<g
|
||||||
x="2.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
fill={handlerColor(theme)}
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="6"
|
strokeWidth="6"
|
||||||
/>
|
>
|
||||||
<rect
|
<rect x="2.5" y="2.5" width="30" height="30" />
|
||||||
x="2.5"
|
<rect x="2.5" y="149.5" width="30" height="30" />
|
||||||
y="149.5"
|
<rect x="147.5" y="149.5" width="30" height="30" />
|
||||||
width="30"
|
<rect x="147.5" y="2.5" width="30" height="30" />
|
||||||
height="30"
|
</g>
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="149.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
</>,
|
</>,
|
||||||
{ width: 182, height: 182, mirror: true },
|
{ width: 182, height: 182, mirror: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UngroupIcon = React.memo(
|
export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
||||||
@@ -536,67 +520,24 @@ export const UngroupIcon = React.memo(
|
|||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<rect
|
<g
|
||||||
x="2.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
fill={handlerColor(theme)}
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="6"
|
strokeWidth="6"
|
||||||
/>
|
>
|
||||||
<rect
|
<rect x="2.5" y="2.5" width="30" height="30" />
|
||||||
x="78.5"
|
<rect x="78.5" y="149.5" width="30" height="30" />
|
||||||
y="149.5"
|
<rect x="147.5" y="149.5" width="30" height="30" />
|
||||||
width="30"
|
<rect x="147.5" y="78.5" width="30" height="30" />
|
||||||
height="30"
|
<rect x="105.5" y="2.5" width="30" height="30" />
|
||||||
fill={handlerColor(theme)}
|
<rect x="2.5" y="102.5" width="30" height="30" />
|
||||||
stroke={iconFillColor(theme)}
|
</g>
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="149.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="78.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="105.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="2.5"
|
|
||||||
y="102.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
</>,
|
</>,
|
||||||
{ width: 182, height: 182, mirror: true },
|
{ width: 182, height: 182, mirror: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FillHachureIcon = React.memo(
|
export const FillHachureIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@@ -608,8 +549,7 @@ export const FillHachureIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FillCrossHatchIcon = React.memo(
|
export const FillCrossHatchIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<g fill={iconFillColor(theme)} fillRule="evenodd" clipRule="evenodd">
|
<g fill={iconFillColor(theme)} fillRule="evenodd" clipRule="evenodd">
|
||||||
<path d="M20.101 16H28.0934L36 8.95989V4H33.5779L20.101 16ZM30.5704 4L17.0935 16H9.10101L22.5779 4H30.5704ZM19.5704 4L6.09349 16H4V10.7475L11.5779 4H19.5704ZM8.57036 4H4V8.06952L8.57036 4ZM36 11.6378L31.101 16H36V11.6378ZM2 2V18H38V2H2Z" />
|
<path d="M20.101 16H28.0934L36 8.95989V4H33.5779L20.101 16ZM30.5704 4L17.0935 16H9.10101L22.5779 4H30.5704ZM19.5704 4L6.09349 16H4V10.7475L11.5779 4H19.5704ZM8.57036 4H4V8.06952L8.57036 4ZM36 11.6378L31.101 16H36V11.6378ZM2 2V18H38V2H2Z" />
|
||||||
@@ -619,8 +559,7 @@ export const FillCrossHatchIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FillSolidIcon = React.memo(
|
export const FillSolidIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(<path d="M2 2H38V18H2V2Z" fill={iconFillColor(theme)} />, {
|
createIcon(<path d="M2 2H38V18H2V2Z" fill={iconFillColor(theme)} />, {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 20,
|
height: 20,
|
||||||
@@ -628,26 +567,27 @@ export const FillSolidIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeWidthIcon = React.memo(
|
export const StrokeWidthIcon = React.memo(
|
||||||
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
|
({ theme, strokeWidth }: { theme: Theme; strokeWidth: number }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H32"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeStyleSolidIcon = React.memo(
|
export const StrokeStyleSolidIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H34"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -656,8 +596,7 @@ export const StrokeStyleSolidIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeStyleDashedIcon = React.memo(
|
export const StrokeStyleDashedIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H34"
|
||||||
@@ -665,45 +604,47 @@ export const StrokeStyleDashedIcon = React.memo(
|
|||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
strokeDasharray={"10, 8"}
|
strokeDasharray={"10, 8"}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeStyleDottedIcon = React.memo(
|
export const StrokeStyleDottedIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H36"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
strokeDasharray={"4, 4"}
|
strokeDasharray={"2, 4.5"}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SloppinessArchitectIcon = React.memo(
|
export const SloppinessArchitectIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M3.00098 16.1691C6.28774 13.9744 19.6399 2.8905 22.7215 3.00082C25.8041 3.11113 19.1158 15.5488 21.4962 16.8309C23.8757 18.1131 34.4155 11.7148 37.0001 10.6919"
|
d="M3.00098 16.1691C6.28774 13.9744 19.6399 2.8905 22.7215 3.00082C25.8041 3.11113 19.1158 15.5488 21.4962 16.8309C23.8757 18.1131 34.4155 11.7148 37.0001 10.6919"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SloppinessArtistIcon = React.memo(
|
export const SloppinessArtistIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M3 17C6.68158 14.8752 16.1296 9.09849 22.0648 6.54922C28 3.99995 22.2896 13.3209 25 14C27.7104 14.6791 36.3757 9.6471 36.3757 9.6471M6.40706 15C13 11.1918 20.0468 1.51045 23.0234 3.0052C26 4.49995 20.457 12.8659 22.7285 16.4329C25 20 36.3757 13 36.3757 13"
|
d="M3 17C6.68158 14.8752 16.1296 9.09849 22.0648 6.54922C28 3.99995 22.2896 13.3209 25 14C27.7104 14.6791 36.3757 9.6471 36.3757 9.6471M6.40706 15C13 11.1918 20.0468 1.51045 23.0234 3.0052C26 4.49995 20.457 12.8659 22.7285 16.4329C25 20 36.3757 13 36.3757 13"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
@@ -711,46 +652,46 @@ export const SloppinessArtistIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const SloppinessCartoonistIcon = React.memo(
|
export const SloppinessCartoonistIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M3 15.6468C6.93692 13.5378 22.5544 2.81528 26.6206 3.00242C30.6877 3.18956 25.6708 15.3346 27.4009 16.7705C29.1309 18.2055 35.4001 12.4762 37 11.6177M3.97143 10.4917C6.61158 9.24563 16.3706 2.61886 19.8104 3.01724C23.2522 3.41472 22.0773 12.2013 24.6181 12.8783C27.1598 13.5536 33.3179 8.04068 35.0571 7.07244"
|
d="M3 15.6468C6.93692 13.5378 22.5544 2.81528 26.6206 3.00242C30.6877 3.18956 25.6708 15.3346 27.4009 16.7705C29.1309 18.2055 35.4001 12.4762 37 11.6177M3.97143 10.4917C6.61158 9.24563 16.3706 2.61886 19.8104 3.01724C23.2522 3.41472 22.0773 12.2013 24.6181 12.8783C27.1598 13.5536 33.3179 8.04068 35.0571 7.07244"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EdgeSharpIcon = React.memo(
|
export const EdgeSharpIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M10 17L10 5L35 5"
|
d="M10 17L10 5L35 5"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EdgeRoundIcon = React.memo(
|
export const EdgeRoundIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M10 17V15C10 8 13 5 21 5L33.5 5"
|
d="M10 17V15C10 8 13 5 21 5L33.5 5"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadNoneIcon = React.memo(
|
export const ArrowheadNoneIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H34"
|
||||||
@@ -766,7 +707,7 @@ export const ArrowheadNoneIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadArrowIcon = React.memo(
|
export const ArrowheadArrowIcon = React.memo(
|
||||||
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
|
({ theme, flip = false }: { theme: Theme; flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g
|
<g
|
||||||
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
@@ -782,7 +723,7 @@ export const ArrowheadArrowIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadDotIcon = React.memo(
|
export const ArrowheadDotIcon = React.memo(
|
||||||
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
|
({ theme, flip = false }: { theme: Theme; flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g
|
<g
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
@@ -797,7 +738,7 @@ export const ArrowheadDotIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadBarIcon = React.memo(
|
export const ArrowheadBarIcon = React.memo(
|
||||||
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
|
({ theme, flip = false }: { theme: Theme; flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
|
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
|
||||||
<path
|
<path
|
||||||
@@ -811,8 +752,7 @@ export const ArrowheadBarIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeSmallIcon = React.memo(
|
export const FontSizeSmallIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -822,8 +762,7 @@ export const FontSizeSmallIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeMediumIcon = React.memo(
|
export const FontSizeMediumIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -833,8 +772,7 @@ export const FontSizeMediumIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeLargeIcon = React.memo(
|
export const FontSizeLargeIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -845,7 +783,7 @@ export const FontSizeLargeIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeExtraLargeIcon = React.memo(
|
export const FontSizeExtraLargeIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -856,7 +794,7 @@ export const FontSizeExtraLargeIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const FontFamilyHandDrawnIcon = React.memo(
|
export const FontFamilyHandDrawnIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -866,8 +804,7 @@ export const FontFamilyHandDrawnIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontFamilyNormalIcon = React.memo(
|
export const FontFamilyNormalIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -883,8 +820,7 @@ export const FontFamilyNormalIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontFamilyCodeIcon = React.memo(
|
export const FontFamilyCodeIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -896,19 +832,18 @@ export const FontFamilyCodeIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TextAlignLeftIcon = React.memo(
|
export const TextAlignLeftIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
|
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TextAlignCenterIcon = React.memo(
|
export const TextAlignCenterIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
|
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
|
||||||
@@ -918,12 +853,12 @@ export const TextAlignCenterIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TextAlignRightIcon = React.memo(
|
export const TextAlignRightIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
|
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
),
|
),
|
||||||
|
|||||||
+48
-11
@@ -1,6 +1,6 @@
|
|||||||
import { FontFamily } from "./element/types";
|
|
||||||
import cssVariables from "./css/variables.module.scss";
|
import cssVariables from "./css/variables.module.scss";
|
||||||
import { AppProps } from "./types";
|
import { AppProps } from "./types";
|
||||||
|
import { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ export const CURSOR_TYPE = {
|
|||||||
TEXT: "text",
|
TEXT: "text",
|
||||||
CROSSHAIR: "crosshair",
|
CROSSHAIR: "crosshair",
|
||||||
GRABBING: "grabbing",
|
GRABBING: "grabbing",
|
||||||
|
GRAB: "grab",
|
||||||
POINTER: "pointer",
|
POINTER: "pointer",
|
||||||
MOVE: "move",
|
MOVE: "move",
|
||||||
AUTO: "",
|
AUTO: "",
|
||||||
@@ -34,6 +35,7 @@ export enum EVENT {
|
|||||||
MOUSE_MOVE = "mousemove",
|
MOUSE_MOVE = "mousemove",
|
||||||
RESIZE = "resize",
|
RESIZE = "resize",
|
||||||
UNLOAD = "unload",
|
UNLOAD = "unload",
|
||||||
|
FOCUS = "focus",
|
||||||
BLUR = "blur",
|
BLUR = "blur",
|
||||||
DRAG_OVER = "dragover",
|
DRAG_OVER = "dragover",
|
||||||
DROP = "drop",
|
DROP = "drop",
|
||||||
@@ -63,15 +65,20 @@ export const CLASSES = {
|
|||||||
|
|
||||||
// 1-based in case we ever do `if(element.fontFamily)`
|
// 1-based in case we ever do `if(element.fontFamily)`
|
||||||
export const FONT_FAMILY = {
|
export const FONT_FAMILY = {
|
||||||
1: "Virgil",
|
Virgil: 1,
|
||||||
2: "Helvetica",
|
Helvetica: 2,
|
||||||
3: "Cascadia",
|
Cascadia: 3,
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
|
export const THEME = {
|
||||||
|
LIGHT: "light",
|
||||||
|
DARK: "dark",
|
||||||
|
};
|
||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamily = 1;
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
export const DEFAULT_VERSION = "{version}";
|
||||||
@@ -83,7 +90,13 @@ export const GRID_SIZE = 20; // TODO make it configurable?
|
|||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
excalidraw: "application/vnd.excalidraw+json",
|
excalidraw: "application/vnd.excalidraw+json",
|
||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
};
|
json: "application/json",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
png: "image/png",
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
gif: "image/gif",
|
||||||
|
binary: "application/octet-stream",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_DATA_TYPES = {
|
export const EXPORT_DATA_TYPES = {
|
||||||
excalidraw: "excalidraw",
|
excalidraw: "excalidraw",
|
||||||
@@ -91,18 +104,20 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
excalidrawLibrary: "excalidrawlib",
|
excalidrawLibrary: "excalidrawlib",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const EXPORT_SOURCE = window.location.origin;
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
|
export const IMAGE_RENDER_TIMEOUT = 500;
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||||
export const TITLE_TIMEOUT = 10000;
|
export const TITLE_TIMEOUT = 10000;
|
||||||
export const TOAST_TIMEOUT = 5000;
|
export const TOAST_TIMEOUT = 5000;
|
||||||
export const VERSION_TIMEOUT = 30000;
|
export const VERSION_TIMEOUT = 30000;
|
||||||
export const SCROLL_TIMEOUT = 100;
|
export const SCROLL_TIMEOUT = 100;
|
||||||
|
|
||||||
export const ZOOM_STEP = 0.1;
|
export const ZOOM_STEP = 0.1;
|
||||||
|
|
||||||
// Report a user inactive after IDLE_THRESHOLD milliseconds
|
// Report a user inactive after IDLE_THRESHOLD milliseconds
|
||||||
@@ -130,10 +145,32 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
canvasActions: {
|
canvasActions: {
|
||||||
changeViewBackgroundColor: true,
|
changeViewBackgroundColor: true,
|
||||||
clearCanvas: true,
|
clearCanvas: true,
|
||||||
export: true,
|
export: { saveFileToDisk: true },
|
||||||
loadScene: true,
|
loadScene: true,
|
||||||
saveAsScene: true,
|
saveToActiveFile: true,
|
||||||
saveScene: true,
|
|
||||||
theme: true,
|
theme: true,
|
||||||
|
saveAsImage: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||||
|
|
||||||
|
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
|
||||||
|
|
||||||
|
export const EXPORT_SCALES = [1, 2, 3];
|
||||||
|
export const DEFAULT_EXPORT_PADDING = 10; // px
|
||||||
|
|
||||||
|
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
|
|
||||||
|
export const ALLOWED_IMAGE_MIME_TYPES = [
|
||||||
|
MIME_TYPES.png,
|
||||||
|
MIME_TYPES.jpg,
|
||||||
|
MIME_TYPES.svg,
|
||||||
|
MIME_TYPES.gif,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user