Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| add75b8c93 | |||
| 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 | |||
| 1e48aafb9c | |||
| 34761200bf | |||
| a0899966ff | |||
| c2b40dff92 | |||
| 9733ecb3df | |||
| 189b721eed | |||
| 90fd4a95df | |||
| 5d3e98fa04 | |||
| 422c25449f | |||
| 67289ef4ce | |||
| 233576628c | |||
| c54a099010 | |||
| 3b976613ba | |||
| bee59747d1 | |||
| 2e1352f3fa | |||
| 6b65db7b68 | |||
| e4c5ebf867 | |||
| 0602f3cfe4 | |||
| eade72b744 | |||
| ef5c9002ad | |||
| aa9e1c4566 | |||
| edc7f7bf47 | |||
| 1310256dcc | |||
| 4ac1841d92 | |||
| bdf6e53289 | |||
| a6706cff20 | |||
| c739ac5c61 | |||
| 0d818f3810 | |||
| 58a7568c9f | |||
| 722e5ca845 | |||
| bb568a9670 | |||
| 0f5b0d1d1d | |||
| 25fd275158 | |||
| 3d047d57a7 | |||
| 26a6f9e76d | |||
| 1c11bb5b41 | |||
| aced1cc6f5 | |||
| f3f85b4c90 | |||
| 86781f09dd | |||
| a94b44440e | |||
| 77bf553ed8 | |||
| fce7047199 | |||
| 9905deb4b4 | |||
| fee84f3807 | |||
| 9020ab3761 | |||
| 136f8b2279 | |||
| 8670b2d587 | |||
| b081a09962 | |||
| 10a23a10a5 | |||
| 30ae4b8bf2 | |||
| cf9e29834d | |||
| 5d26c15daf | |||
| b0d7ff290f | |||
| 458e6d6c24 | |||
| a21db08cae | |||
| 1b626175de | |||
| 5ffdd3f32d | |||
| 77b873251a | |||
| b50b8f7b0d | |||
| 40656c70d1 | |||
| c5b4b04d6b | |||
| 1ad212677b | |||
| 32427c355c | |||
| 402a812159 | |||
| 0480753581 | |||
| f7e17a28fa | |||
| 78f3a92dd1 | |||
| c8743a8c02 | |||
| 127c1be6ad | |||
| 86bf2d697d | |||
| 7ee8de0a46 | |||
| 981f327b48 | |||
| eeea8406c9 | |||
| 0f249e3b26 | |||
| 2c7c80bd75 | |||
| 94ad8eaa19 | |||
| 13d9374cde | |||
| efb6d0825b | |||
| 80a61db72f | |||
| 9a13dd8836 | |||
| cf6a5ff16b | |||
| fa8c7abf50 | |||
| c3ecbcb3ab |
@@ -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
|
||||||
@@ -20,3 +22,4 @@ package-lock.json
|
|||||||
static
|
static
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
src/packages/excalidraw/types
|
||||||
|
|||||||
+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
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,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 +102,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
-19
@@ -19,34 +19,35 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "6.2.2",
|
"@sentry/browser": "6.2.5",
|
||||||
"@sentry/integrations": "6.2.1",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.11.9",
|
"@testing-library/jest-dom": "5.11.10",
|
||||||
"@testing-library/react": "11.2.5",
|
"@testing-library/react": "11.2.6",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.22",
|
||||||
"@types/react": "17.0.2",
|
"@types/react": "17.0.3",
|
||||||
"@types/react-dom": "17.0.1",
|
"@types/react-dom": "17.0.3",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.14.2",
|
"browser-fs-access": "0.18.0",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.2.10",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.1.0",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.1.21",
|
"nanoid": "3.1.22",
|
||||||
"open-color": "1.8.0",
|
"open-color": "1.8.0",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
"perfect-freehand": "0.4.7",
|
||||||
"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",
|
||||||
"points-on-curve": "0.2.0",
|
"points-on-curve": "0.2.0",
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "17.0.1",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.1",
|
"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 +55,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",
|
||||||
@@ -103,6 +104,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.
+27
-5
@@ -51,8 +51,7 @@
|
|||||||
name="twitter:description"
|
name="twitter:description"
|
||||||
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<!-- OG tags require absolute url for images -->
|
|
||||||
<meta name="twitter:image" content="https://excalidraw.com/og-image.png" />
|
|
||||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
||||||
|
|
||||||
<!-- Excalidraw version -->
|
<!-- Excalidraw version -->
|
||||||
@@ -88,6 +87,8 @@
|
|||||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||||
|
// setting this so that libraries installation reuses this window tab.
|
||||||
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
||||||
<script
|
<script
|
||||||
@@ -106,15 +107,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 {
|
||||||
@@ -124,6 +127,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 {
|
||||||
@@ -146,6 +150,24 @@
|
|||||||
color: var(--popup-text-color);
|
color: var(--popup-text-color);
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
}
|
}
|
||||||
|
#root {
|
||||||
|
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>
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -39,5 +39,37 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/screenshots/virtual-whiteboard.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "462x945"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/wireframe.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "462x945"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/illustration.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "462x945"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/shapes.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "462x945"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/collaboration.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "462x945"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/export.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "462x945"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
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 && !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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { getDefaultAppState } from "../appState";
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { ZOOM_STEP } from "../constants";
|
import { ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
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 { useIsMobile } from "../components/App";
|
||||||
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";
|
||||||
@@ -21,8 +22,8 @@ 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,12 @@ 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -53,7 +59,6 @@ export const actionClearCanvas = register({
|
|||||||
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,
|
||||||
},
|
},
|
||||||
@@ -72,6 +77,7 @@ export const actionClearCanvas = register({
|
|||||||
updateData(null);
|
updateData(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
data-testid="clear-canvas-button"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -258,3 +264,27 @@ 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 === "light" ? "dark" : "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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const actionCopyAsSvg = register({
|
|||||||
? selectedElements
|
? selectedElements
|
||||||
: getNonDeletedElements(elements),
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.canvas,
|
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -89,7 +88,6 @@ export const actionCopyAsPng = register({
|
|||||||
? selectedElements
|
? selectedElements
|
||||||
: getNonDeletedElements(elements),
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.canvas,
|
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
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";
|
||||||
@@ -8,9 +8,16 @@ import { Tooltip } from "../components/Tooltip";
|
|||||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
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 as fsSupported } from "browser-fs-access";
|
||||||
|
import { CheckboxItem } from "../components/CheckboxItem";
|
||||||
|
import { getExportSize } from "../scene/export";
|
||||||
|
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants";
|
||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { ActiveFile } from "../components/ActiveFile";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@@ -18,15 +25,66 @@ export const actionChangeProjectName = register({
|
|||||||
trackEvent("change", "title");
|
trackEvent("change", "title");
|
||||||
return { appState: { ...appState, name: value }, commitToHistory: false };
|
return { appState: { ...appState, name: value }, commitToHistory: false };
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData, appProps }) => (
|
||||||
<ProjectName
|
<ProjectName
|
||||||
label={t("labels.fileTitle")}
|
label={t("labels.fileTitle")}
|
||||||
value={appState.name || "Unnamed"}
|
value={appState.name || "Unnamed"}
|
||||||
onChange={(name: string) => updateData(name)}
|
onChange={(name: string) => updateData(name)}
|
||||||
|
isNameEditable={
|
||||||
|
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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="s"
|
||||||
|
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) => {
|
||||||
@@ -36,14 +94,12 @@ export const actionChangeExportBackground = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<label>
|
<CheckboxItem
|
||||||
<input
|
checked={appState.exportBackground}
|
||||||
type="checkbox"
|
onChange={(checked) => updateData(checked)}
|
||||||
checked={appState.exportBackground}
|
>
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.withBackground")}
|
{t("labels.withBackground")}
|
||||||
</label>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,46 +112,20 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<label style={{ display: "flex" }}>
|
<CheckboxItem
|
||||||
<input
|
checked={appState.exportEmbedScene}
|
||||||
type="checkbox"
|
onChange={(checked) => updateData(checked)}
|
||||||
checked={appState.exportEmbedScene}
|
>
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.exportEmbedScene")}
|
{t("labels.exportEmbedScene")}
|
||||||
<Tooltip
|
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||||
label={t("labels.exportEmbedScene_details")}
|
<div className="Tooltip-icon">{questionCircle}</div>
|
||||||
position="above"
|
|
||||||
long={true}
|
|
||||||
>
|
|
||||||
<div className="TooltipIcon">{questionCircle}</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeShouldAddWatermark = register({
|
export const actionSaveToActiveFile = register({
|
||||||
name: "changeShouldAddWatermark",
|
name: "saveToActiveFile",
|
||||||
perform: (_elements, appState, value) => {
|
|
||||||
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) => {
|
perform: async (elements, appState, value) => {
|
||||||
const fileHandleExists = !!appState.fileHandle;
|
const fileHandleExists = !!appState.fileHandle;
|
||||||
try {
|
try {
|
||||||
@@ -124,20 +154,16 @@ 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)}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSaveAsScene = register({
|
export const actionSaveFileToDisk = register({
|
||||||
name: "saveAsScene",
|
name: "saveFileToDisk",
|
||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value) => {
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, {
|
const { fileHandle } = await saveAsJSON(elements, {
|
||||||
@@ -161,10 +187,9 @@ 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={
|
hidden={!fsSupported}
|
||||||
!("chooseFileSystemEntries" in window || "showOpenFilePicker" in window)
|
|
||||||
}
|
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
data-testid="save-as-button"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -176,7 +201,7 @@ export const actionLoadScene = register({
|
|||||||
const {
|
const {
|
||||||
elements: loadedElements,
|
elements: loadedElements,
|
||||||
appState: loadedAppState,
|
appState: loadedAppState,
|
||||||
} = await loadFromJSON(appState);
|
} = await loadFromJSON(appState, elements);
|
||||||
return {
|
return {
|
||||||
elements: loadedElements,
|
elements: loadedElements,
|
||||||
appState: loadedAppState,
|
appState: loadedAppState,
|
||||||
@@ -202,6 +227,7 @@ export const actionLoadScene = register({
|
|||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
|
data-testid="load-button"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,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,
|
||||||
@@ -51,19 +51,19 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
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 +86,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 +118,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 +147,14 @@ 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,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.elementType === "draw",
|
commitToHistory: appState.elementType === "freedraw",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event, appState) =>
|
keyTest: (event, appState) =>
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { register } from "./register";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { getElementMap, getNonDeletedElements } from "../element";
|
||||||
|
import { mutateElement } from "../element/mutateElement";
|
||||||
|
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||||
|
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { getTransformHandles } from "../element/transformHandles";
|
||||||
|
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
|
||||||
|
import { updateBoundElements } from "../element/binding";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
|
const enableActionFlipHorizontal = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const eligibleElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableActionFlipVertical = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const eligibleElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
return eligibleElements.length === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionFlipHorizontal = register({
|
||||||
|
name: "flipHorizontal",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
elements: flipSelectedElements(elements, appState, "horizontal"),
|
||||||
|
appState,
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => event.shiftKey && event.code === "KeyH",
|
||||||
|
contextItemLabel: "labels.flipHorizontal",
|
||||||
|
contextItemPredicate: (elements, appState) =>
|
||||||
|
enableActionFlipHorizontal(elements, appState),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionFlipVertical = register({
|
||||||
|
name: "flipVertical",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
elements: flipSelectedElements(elements, appState, "vertical"),
|
||||||
|
appState,
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => event.shiftKey && event.code === "KeyV",
|
||||||
|
contextItemLabel: "labels.flipVertical",
|
||||||
|
contextItemPredicate: (elements, appState) =>
|
||||||
|
enableActionFlipVertical(elements, appState),
|
||||||
|
});
|
||||||
|
|
||||||
|
const flipSelectedElements = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: Readonly<AppState>,
|
||||||
|
flipDirection: "horizontal" | "vertical",
|
||||||
|
) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
// remove once we allow for groups of elements to be flipped
|
||||||
|
if (selectedElements.length > 1) {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedElements = flipElements(
|
||||||
|
selectedElements,
|
||||||
|
appState,
|
||||||
|
flipDirection,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedElementsMap = getElementMap(updatedElements);
|
||||||
|
|
||||||
|
return elements.map((element) => updatedElementsMap[element.id] || element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flipElements = (
|
||||||
|
elements: NonDeleted<ExcalidrawElement>[],
|
||||||
|
appState: AppState,
|
||||||
|
flipDirection: "horizontal" | "vertical",
|
||||||
|
): ExcalidrawElement[] => {
|
||||||
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
flipElement(elements[i], appState);
|
||||||
|
// If vertical flip, rotate an extra 180
|
||||||
|
if (flipDirection === "vertical") {
|
||||||
|
rotateElement(elements[i], Math.PI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flipElement = (
|
||||||
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const originalX = element.x;
|
||||||
|
const originalY = element.y;
|
||||||
|
const width = element.width;
|
||||||
|
const height = element.height;
|
||||||
|
const originalAngle = normalizeAngle(element.angle);
|
||||||
|
|
||||||
|
let finalOffsetX = 0;
|
||||||
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
|
finalOffsetX =
|
||||||
|
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
||||||
|
element.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate back to zero, if necessary
|
||||||
|
mutateElement(element, {
|
||||||
|
angle: normalizeAngle(0),
|
||||||
|
});
|
||||||
|
// Flip unrotated by pulling TransformHandle to opposite side
|
||||||
|
const transformHandles = getTransformHandles(element, appState.zoom);
|
||||||
|
let usingNWHandle = true;
|
||||||
|
let newNCoordsX = 0;
|
||||||
|
let nHandle = transformHandles.nw;
|
||||||
|
if (!nHandle) {
|
||||||
|
// Use ne handle instead
|
||||||
|
usingNWHandle = false;
|
||||||
|
nHandle = transformHandles.ne;
|
||||||
|
if (!nHandle) {
|
||||||
|
mutateElement(element, {
|
||||||
|
angle: originalAngle,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinearElement(element)) {
|
||||||
|
for (let i = 1; i < element.points.length; i++) {
|
||||||
|
LinearElementEditor.movePoint(element, i, [
|
||||||
|
-element.points[i][0],
|
||||||
|
element.points[i][1],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
LinearElementEditor.normalizePoints(element);
|
||||||
|
} else {
|
||||||
|
// calculate new x-coord for transformation
|
||||||
|
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
|
||||||
|
resizeSingleElement(
|
||||||
|
element,
|
||||||
|
true,
|
||||||
|
element,
|
||||||
|
usingNWHandle ? "nw" : "ne",
|
||||||
|
false,
|
||||||
|
newNCoordsX,
|
||||||
|
nHandle[1],
|
||||||
|
);
|
||||||
|
// fix the size to account for handle sizes
|
||||||
|
mutateElement(element, {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate by (360 degrees - original angle)
|
||||||
|
let angle = normalizeAngle(2 * Math.PI - originalAngle);
|
||||||
|
if (angle < 0) {
|
||||||
|
// check, probably unnecessary
|
||||||
|
angle = normalizeAngle(angle + 2 * Math.PI);
|
||||||
|
}
|
||||||
|
mutateElement(element, {
|
||||||
|
angle,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move back to original spot to appear "flipped in place"
|
||||||
|
mutateElement(element, {
|
||||||
|
x: originalX + finalOffsetX,
|
||||||
|
y: originalY,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateBoundElements(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
|
||||||
|
const originalX = element.x;
|
||||||
|
const originalY = element.y;
|
||||||
|
let angle = normalizeAngle(element.angle + rotationAngle);
|
||||||
|
if (angle < 0) {
|
||||||
|
// check, probably unnecessary
|
||||||
|
angle = normalizeAngle(2 * Math.PI + angle);
|
||||||
|
}
|
||||||
|
mutateElement(element, {
|
||||||
|
angle,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move back to original spot
|
||||||
|
mutateElement(element, {
|
||||||
|
x: originalX,
|
||||||
|
y: originalY,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ 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 +59,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",
|
||||||
|
|||||||
@@ -70,7 +70,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,7 +1,6 @@
|
|||||||
import React from "react";
|
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 { ButtonSelect } from "../components/ButtonSelect";
|
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { IconPicker } from "../components/IconPicker";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +13,13 @@ import {
|
|||||||
FillCrossHatchIcon,
|
FillCrossHatchIcon,
|
||||||
FillHachureIcon,
|
FillHachureIcon,
|
||||||
FillSolidIcon,
|
FillSolidIcon,
|
||||||
|
FontFamilyCodeIcon,
|
||||||
|
FontFamilyHandDrawnIcon,
|
||||||
|
FontFamilyNormalIcon,
|
||||||
|
FontSizeExtraLargeIcon,
|
||||||
|
FontSizeLargeIcon,
|
||||||
|
FontSizeMediumIcon,
|
||||||
|
FontSizeSmallIcon,
|
||||||
SloppinessArchitectIcon,
|
SloppinessArchitectIcon,
|
||||||
SloppinessArtistIcon,
|
SloppinessArtistIcon,
|
||||||
SloppinessCartoonistIcon,
|
SloppinessCartoonistIcon,
|
||||||
@@ -21,8 +27,15 @@ import {
|
|||||||
StrokeStyleDottedIcon,
|
StrokeStyleDottedIcon,
|
||||||
StrokeStyleSolidIcon,
|
StrokeStyleSolidIcon,
|
||||||
StrokeWidthIcon,
|
StrokeWidthIcon,
|
||||||
|
TextAlignCenterIcon,
|
||||||
|
TextAlignLeftIcon,
|
||||||
|
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,
|
||||||
@@ -35,7 +48,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";
|
||||||
@@ -90,13 +103,18 @@ 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,
|
newElementWith(el, {
|
||||||
}),
|
strokeColor: value.currentItemStrokeColor,
|
||||||
),
|
}),
|
||||||
appState: { ...appState, currentItemStrokeColor: value },
|
),
|
||||||
commitToHistory: true,
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemStrokeColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -111,7 +129,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 })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -121,13 +143,18 @@ export const actionChangeBackgroundColor = register({
|
|||||||
name: "changeBackgroundColor",
|
name: "changeBackgroundColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
...(value.currentItemBackgroundColor && {
|
||||||
newElementWith(el, {
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
backgroundColor: value,
|
newElementWith(el, {
|
||||||
}),
|
backgroundColor: value.currentItemBackgroundColor,
|
||||||
),
|
}),
|
||||||
appState: { ...appState, currentItemBackgroundColor: value },
|
),
|
||||||
commitToHistory: true,
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -142,7 +169,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 })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -413,13 +444,29 @@ export const actionChangeFontSize = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontSize")}</legend>
|
<legend>{t("labels.fontSize")}</legend>
|
||||||
<ButtonSelect
|
<ButtonIconSelect
|
||||||
group="font-size"
|
group="font-size"
|
||||||
options={[
|
options={[
|
||||||
{ value: 16, text: t("labels.small") },
|
{
|
||||||
{ value: 20, text: t("labels.medium") },
|
value: 16,
|
||||||
{ value: 28, text: t("labels.large") },
|
text: t("labels.small"),
|
||||||
{ value: 36, text: t("labels.veryLarge") },
|
icon: <FontSizeSmallIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 20,
|
||||||
|
text: t("labels.medium"),
|
||||||
|
icon: <FontSizeMediumIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 28,
|
||||||
|
text: t("labels.large"),
|
||||||
|
icon: <FontSizeLargeIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 36,
|
||||||
|
text: t("labels.veryLarge"),
|
||||||
|
icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
@@ -456,16 +503,32 @@ export const actionChangeFontFamily = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
const options: { value: FontFamily; text: string }[] = [
|
const options: {
|
||||||
{ value: 1, text: t("labels.handDrawn") },
|
value: FontFamilyValues;
|
||||||
{ value: 2, text: t("labels.normal") },
|
text: string;
|
||||||
{ value: 3, text: t("labels.code") },
|
icon: JSX.Element;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
value: FONT_FAMILY.Virgil,
|
||||||
|
text: t("labels.handDrawn"),
|
||||||
|
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: FONT_FAMILY.Helvetica,
|
||||||
|
text: t("labels.normal"),
|
||||||
|
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: FONT_FAMILY.Cascadia,
|
||||||
|
text: t("labels.code"),
|
||||||
|
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
<legend>{t("labels.fontFamily")}</legend>
|
||||||
<ButtonSelect<FontFamily | false>
|
<ButtonIconSelect<FontFamilyValues | false>
|
||||||
group="font-family"
|
group="font-family"
|
||||||
options={options}
|
options={options}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@@ -506,12 +569,24 @@ export const actionChangeTextAlign = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.textAlign")}</legend>
|
<legend>{t("labels.textAlign")}</legend>
|
||||||
<ButtonSelect<TextAlign | false>
|
<ButtonIconSelect<TextAlign | false>
|
||||||
group="text-align"
|
group="text-align"
|
||||||
options={[
|
options={[
|
||||||
{ value: "left", text: t("labels.left") },
|
{
|
||||||
{ value: "center", text: t("labels.center") },
|
value: "left",
|
||||||
{ value: "right", text: t("labels.right") },
|
text: t("labels.left"),
|
||||||
|
icon: <TextAlignLeftIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "center",
|
||||||
|
text: t("labels.center"),
|
||||||
|
icon: <TextAlignCenterIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "right",
|
||||||
|
text: t("labels.right"),
|
||||||
|
icon: <TextAlignRightIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ export {
|
|||||||
distributeVertically,
|
distributeVertically,
|
||||||
} from "./actionDistribute";
|
} from "./actionDistribute";
|
||||||
|
|
||||||
|
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
actionCopy,
|
actionCopy,
|
||||||
actionCut,
|
actionCut,
|
||||||
|
|||||||
+23
-4
@@ -7,12 +7,18 @@ import {
|
|||||||
ActionResult,
|
ActionResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppProps, AppState } from "../types";
|
||||||
import { MODES } from "../constants";
|
import { MODES } from "../constants";
|
||||||
|
import Library from "../data/library";
|
||||||
|
|
||||||
// This is the <App> component, but for now we don't care about anything but its
|
// This is the <App> component, but for now we don't care about anything but its
|
||||||
// `canvas` state.
|
// `canvas` state.
|
||||||
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
|
type App = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
focusContainer: () => void;
|
||||||
|
props: AppProps;
|
||||||
|
library: Library;
|
||||||
|
};
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@@ -51,11 +57,15 @@ 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 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))
|
||||||
.filter(
|
.filter(
|
||||||
(action) =>
|
(action) =>
|
||||||
|
(action.name in canvasActions
|
||||||
|
? canvasActions[action.name as keyof typeof canvasActions]
|
||||||
|
: true) &&
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
@@ -102,7 +112,15 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
// like the user list. We can use this key to extract more
|
// like the user list. We can use this key to extract more
|
||||||
// data from app state. This is an alternative to generic prop hell!
|
// data from app state. This is an alternative to generic prop hell!
|
||||||
renderAction = (name: ActionName, id?: string) => {
|
renderAction = (name: ActionName, id?: string) => {
|
||||||
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.actions[name] &&
|
||||||
|
"PanelComponent" in this.actions[name] &&
|
||||||
|
(name in canvasActions
|
||||||
|
? canvasActions[name as keyof typeof canvasActions]
|
||||||
|
: true)
|
||||||
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
const updateData = (formState?: any) => {
|
const updateData = (formState?: any) => {
|
||||||
@@ -122,6 +140,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
id={id}
|
id={id}
|
||||||
|
appProps={this.app.props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export type ShortcutName =
|
|||||||
| "zenMode"
|
| "zenMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary"
|
| "addToLibrary"
|
||||||
| "viewMode";
|
| "viewMode"
|
||||||
|
| "flipHorizontal"
|
||||||
|
| "flipVertical";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
@@ -55,8 +57,10 @@ 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")],
|
||||||
|
flipVertical: [getShortcutKey("Shift+V")],
|
||||||
viewMode: [getShortcutKey("Alt+R")],
|
viewMode: [getShortcutKey("Alt+R")],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+25
-9
@@ -1,22 +1,32 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
|
import Library from "../data/library";
|
||||||
|
|
||||||
/** if false, the action should be prevented */
|
/** if false, the action should be prevented */
|
||||||
export type ActionResult =
|
export type ActionResult =
|
||||||
| {
|
| {
|
||||||
elements?: readonly ExcalidrawElement[] | null;
|
elements?: readonly ExcalidrawElement[] | null;
|
||||||
appState?: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
|
appState?: MarkOptional<
|
||||||
|
AppState,
|
||||||
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
|
> | null;
|
||||||
commitToHistory: boolean;
|
commitToHistory: boolean;
|
||||||
syncHistory?: boolean;
|
syncHistory?: boolean;
|
||||||
}
|
}
|
||||||
| false;
|
| false;
|
||||||
|
|
||||||
|
type AppAPI = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
focusContainer(): void;
|
||||||
|
library: Library;
|
||||||
|
};
|
||||||
|
|
||||||
type ActionFn = (
|
type ActionFn = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
formData: any,
|
formData: any,
|
||||||
app: { canvas: HTMLCanvasElement | null },
|
app: AppAPI,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
@@ -42,6 +52,7 @@ export type ActionName =
|
|||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
| "changeFillStyle"
|
| "changeFillStyle"
|
||||||
| "changeStrokeWidth"
|
| "changeStrokeWidth"
|
||||||
|
| "changeStrokeShape"
|
||||||
| "changeSloppiness"
|
| "changeSloppiness"
|
||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
@@ -55,9 +66,9 @@ export type ActionName =
|
|||||||
| "changeProjectName"
|
| "changeProjectName"
|
||||||
| "changeExportBackground"
|
| "changeExportBackground"
|
||||||
| "changeExportEmbedScene"
|
| "changeExportEmbedScene"
|
||||||
| "changeShouldAddWatermark"
|
| "changeExportScale"
|
||||||
| "saveScene"
|
| "saveToActiveFile"
|
||||||
| "saveAsScene"
|
| "saveFileToDisk"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
| "duplicateSelection"
|
| "duplicateSelection"
|
||||||
| "deleteSelectedElements"
|
| "deleteSelectedElements"
|
||||||
@@ -85,8 +96,11 @@ export type ActionName =
|
|||||||
| "alignHorizontallyCentered"
|
| "alignHorizontallyCentered"
|
||||||
| "distributeHorizontally"
|
| "distributeHorizontally"
|
||||||
| "distributeVertically"
|
| "distributeVertically"
|
||||||
|
| "flipHorizontal"
|
||||||
|
| "flipVertical"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
| "exportWithDarkMode";
|
| "exportWithDarkMode"
|
||||||
|
| "toggleTheme";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
@@ -94,12 +108,13 @@ export interface Action {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
|
appProps: ExcalidrawProps;
|
||||||
id?: string;
|
id?: string;
|
||||||
}>;
|
}>;
|
||||||
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;
|
||||||
@@ -114,6 +129,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;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -3,14 +3,19 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
|
EXPORT_SCALES,
|
||||||
} 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"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
> => {
|
> => {
|
||||||
return {
|
return {
|
||||||
theme: "light",
|
theme: "light",
|
||||||
@@ -39,11 +44,11 @@ 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,
|
||||||
gridSize: null,
|
gridSize: null,
|
||||||
height: window.innerHeight,
|
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
isLibraryOpen: false,
|
isLibraryOpen: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -53,6 +58,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,
|
||||||
@@ -62,7 +68,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,
|
||||||
@@ -70,7 +75,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
toastMessage: null,
|
toastMessage: null,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
width: window.innerWidth,
|
|
||||||
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,
|
||||||
@@ -119,6 +123,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
errorMessage: { browser: false, export: false },
|
errorMessage: { browser: false, export: false },
|
||||||
exportBackground: { browser: true, export: false },
|
exportBackground: { browser: true, export: false },
|
||||||
exportEmbedScene: { browser: true, export: false },
|
exportEmbedScene: { browser: true, export: false },
|
||||||
|
exportScale: { browser: true, export: false },
|
||||||
exportWithDarkMode: { browser: true, export: false },
|
exportWithDarkMode: { browser: true, export: false },
|
||||||
fileHandle: { browser: false, export: false },
|
fileHandle: { browser: false, export: false },
|
||||||
gridSize: { browser: true, export: true },
|
gridSize: { browser: true, export: true },
|
||||||
@@ -134,6 +139,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
offsetLeft: { browser: false, export: false },
|
offsetLeft: { browser: false, export: false },
|
||||||
offsetTop: { browser: false, export: false },
|
offsetTop: { browser: false, export: false },
|
||||||
openMenu: { browser: true, export: false },
|
openMenu: { browser: true, export: false },
|
||||||
|
openPopup: { browser: false, export: false },
|
||||||
pasteDialog: { browser: false, export: false },
|
pasteDialog: { browser: false, export: false },
|
||||||
previousSelectedElementIds: { browser: true, export: false },
|
previousSelectedElementIds: { browser: true, export: false },
|
||||||
resizingElement: { browser: false, export: false },
|
resizingElement: { browser: false, export: false },
|
||||||
@@ -143,7 +149,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
selectedElementIds: { browser: true, export: false },
|
selectedElementIds: { browser: true, export: false },
|
||||||
selectedGroupIds: { browser: true, export: false },
|
selectedGroupIds: { browser: true, export: false },
|
||||||
selectionElement: { browser: false, export: false },
|
selectionElement: { browser: false, export: false },
|
||||||
shouldAddWatermark: { browser: true, export: false },
|
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false },
|
||||||
showHelpDialog: { browser: false, export: false },
|
showHelpDialog: { browser: false, export: false },
|
||||||
showStats: { browser: true, export: false },
|
showStats: { browser: true, export: false },
|
||||||
|
|||||||
+23
-24
@@ -6,16 +6,20 @@ import { getSelectedElements } from "./scene";
|
|||||||
import { AppState } from "./types";
|
import { AppState } 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 } from "./constants";
|
||||||
|
|
||||||
const TYPE_ELEMENTS = "excalidraw/elements";
|
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof TYPE_ELEMENTS;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
created: number;
|
|
||||||
elements: ExcalidrawElement[];
|
elements: ExcalidrawElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ClipboardData {
|
||||||
|
spreadsheet?: Spreadsheet;
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
text?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
let CLIPBOARD = "";
|
||||||
let PREFER_APP_CLIPBOARD = false;
|
let PREFER_APP_CLIPBOARD = false;
|
||||||
|
|
||||||
@@ -31,8 +35,16 @@ export const probablySupportsClipboardBlob =
|
|||||||
"ClipboardItem" in window &&
|
"ClipboardItem" in window &&
|
||||||
"toBlob" in HTMLCanvasElement.prototype;
|
"toBlob" in HTMLCanvasElement.prototype;
|
||||||
|
|
||||||
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
|
const clipboardContainsElements = (
|
||||||
if (contents?.type === TYPE_ELEMENTS) {
|
contents: any,
|
||||||
|
): contents is { elements: ExcalidrawElement[] } => {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
EXPORT_DATA_TYPES.excalidraw,
|
||||||
|
EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
|
].includes(contents?.type) &&
|
||||||
|
Array.isArray(contents.elements)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -43,8 +55,7 @@ export const copyToClipboard = async (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: TYPE_ELEMENTS,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
created: Date.now(),
|
|
||||||
elements: getSelectedElements(elements, appState),
|
elements: getSelectedElements(elements, appState),
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
@@ -105,12 +116,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
|
||||||
@@ -131,15 +137,9 @@ export const parseClipboard = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
// system clipboard elements are newer than in-app clipboard
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
if (
|
|
||||||
isElementsClipboard(systemClipboardData) &&
|
|
||||||
(!appClipboardData?.created ||
|
|
||||||
appClipboardData.created < systemClipboardData.created)
|
|
||||||
) {
|
|
||||||
return { elements: systemClipboardData.elements };
|
return { elements: systemClipboardData.elements };
|
||||||
}
|
}
|
||||||
// in-app clipboard is newer than system clipboard
|
|
||||||
return appClipboardData;
|
return appClipboardData;
|
||||||
} catch {
|
} catch {
|
||||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||||
@@ -151,8 +151,7 @@ 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({ "image/png": blob }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
+13
-27
@@ -3,13 +3,14 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } 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";
|
||||||
@@ -53,10 +54,17 @@ export const SelectedShapeActions = ({
|
|||||||
{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,23 +151,14 @@ 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,
|
|
||||||
}: {
|
}: {
|
||||||
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;
|
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key }, index) => {
|
{SHAPES.map(({ value, icon, key }, index) => {
|
||||||
@@ -193,19 +192,6 @@ export const ShapesSwitcher = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<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 });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
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>
|
||||||
|
);
|
||||||
+881
-559
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";
|
||||||
|
|
||||||
export const ButtonIconCycle = <T extends any>({
|
export const ButtonIconCycle = <T extends any>({
|
||||||
@@ -14,11 +13,11 @@ export const ButtonIconCycle = <T extends any>({
|
|||||||
}) => {
|
}) => {
|
||||||
const current = options.find((op) => op.value === value);
|
const current = options.find((op) => op.value === value);
|
||||||
|
|
||||||
function cycle() {
|
const cycle = () => {
|
||||||
const index = options.indexOf(current!);
|
const index = options.indexOf(current!);
|
||||||
const next = (index + 1) % options.length;
|
const next = (index + 1) % options.length;
|
||||||
onChange(options[next].value);
|
onChange(options[next].value);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label key={group} className={clsx({ active: current!.value !== null })}>
|
<label key={group} className={clsx({ active: current!.value !== null })}>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ const Picker = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -237,13 +238,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;
|
||||||
|
|
||||||
|
|||||||
@@ -32,67 +32,63 @@ const ContextMenu = ({
|
|||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
}: ContextMenuProps) => {
|
}: ContextMenuProps) => {
|
||||||
const isDarkTheme = !!document
|
|
||||||
.querySelector(".excalidraw")
|
|
||||||
?.classList.contains("theme--dark");
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Popover
|
||||||
className={clsx("excalidraw", {
|
onCloseRequest={onCloseRequest}
|
||||||
"theme--dark theme--dark-background-none": isDarkTheme,
|
top={top}
|
||||||
})}
|
left={left}
|
||||||
|
fitInViewport={true}
|
||||||
>
|
>
|
||||||
<Popover
|
<ul
|
||||||
onCloseRequest={onCloseRequest}
|
className="context-menu"
|
||||||
top={top}
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
left={left}
|
|
||||||
fitInViewport={true}
|
|
||||||
>
|
>
|
||||||
<ul
|
{options.map((option, idx) => {
|
||||||
className="context-menu"
|
if (option === "separator") {
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
return <hr key={idx} className="context-menu-option-separator" />;
|
||||||
>
|
}
|
||||||
{options.map((option, idx) => {
|
|
||||||
if (option === "separator") {
|
|
||||||
return <hr key={idx} className="context-menu-option-separator" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionName = option.name;
|
const actionName = option.name;
|
||||||
const label = option.contextItemLabel
|
const label = option.contextItemLabel
|
||||||
? t(option.contextItemLabel)
|
? t(option.contextItemLabel)
|
||||||
: "";
|
: "";
|
||||||
return (
|
return (
|
||||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||||
<button
|
<button
|
||||||
className={clsx("context-menu-option", {
|
className={clsx("context-menu-option", {
|
||||||
dangerous: actionName === "deleteSelectedElements",
|
dangerous: actionName === "deleteSelectedElements",
|
||||||
checkmark: option.checked?.(appState),
|
checkmark: option.checked?.(appState),
|
||||||
})}
|
})}
|
||||||
onClick={() => actionManager.executeAction(option)}
|
onClick={() => actionManager.executeAction(option)}
|
||||||
>
|
>
|
||||||
<div className="context-menu-option__label">{label}</div>
|
<div className="context-menu-option__label">{label}</div>
|
||||||
<kbd className="context-menu-option__shortcut">
|
<kbd className="context-menu-option__shortcut">
|
||||||
{actionName
|
{actionName
|
||||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||||
: ""}
|
: ""}
|
||||||
</kbd>
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let contextMenuNode: HTMLDivElement;
|
const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
|
||||||
const getContextMenuNode = (): HTMLDivElement => {
|
|
||||||
|
const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
|
||||||
|
let contextMenuNode = contextMenuNodeByContainer.get(container);
|
||||||
if (contextMenuNode) {
|
if (contextMenuNode) {
|
||||||
return contextMenuNode;
|
return contextMenuNode;
|
||||||
}
|
}
|
||||||
const div = document.createElement("div");
|
contextMenuNode = document.createElement("div");
|
||||||
document.body.appendChild(div);
|
container
|
||||||
return (contextMenuNode = div);
|
.querySelector(".excalidraw-contextMenuContainer")!
|
||||||
|
.appendChild(contextMenuNode);
|
||||||
|
contextMenuNodeByContainer.set(container, contextMenuNode);
|
||||||
|
return contextMenuNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextMenuParams = {
|
type ContextMenuParams = {
|
||||||
@@ -101,10 +97,16 @@ type ContextMenuParams = {
|
|||||||
left: ContextMenuProps["left"];
|
left: ContextMenuProps["left"];
|
||||||
actionManager: ContextMenuProps["actionManager"];
|
actionManager: ContextMenuProps["actionManager"];
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
|
container: HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = (container: HTMLElement) => {
|
||||||
unmountComponentAtNode(getContextMenuNode());
|
const contextMenuNode = contextMenuNodeByContainer.get(container);
|
||||||
|
if (contextMenuNode) {
|
||||||
|
unmountComponentAtNode(contextMenuNode);
|
||||||
|
contextMenuNode.remove();
|
||||||
|
contextMenuNodeByContainer.delete(container);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -121,11 +123,11 @@ export default {
|
|||||||
top={params.top}
|
top={params.top}
|
||||||
left={params.left}
|
left={params.left}
|
||||||
options={options}
|
options={options}
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={() => handleClose(params.container)}
|
||||||
actionManager={params.actionManager}
|
actionManager={params.actionManager}
|
||||||
appState={params.appState}
|
appState={params.appState}
|
||||||
/>,
|
/>,
|
||||||
getContextMenuNode(),
|
getContextMenuNode(params.container),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
export type Appearence = "light" | "dark";
|
export type Appearence = "light" | "dark";
|
||||||
|
|
||||||
@@ -12,31 +13,19 @@ export const DarkModeToggle = (props: {
|
|||||||
onChange: (value: Appearence) => void;
|
onChange: (value: Appearence) => 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 === "light" ? ICONS.MOON : ICONS.SUN}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
aria-label={title}
|
||||||
<input
|
onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
|
||||||
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
data-testid="toggle-dark-mode"
|
||||||
type="checkbox"
|
/>
|
||||||
onChange={(event) =>
|
|
||||||
props.onChange(event.target.checked ? "dark" : "light")
|
|
||||||
}
|
|
||||||
checked={props.value === "dark"}
|
|
||||||
aria-label={title}
|
|
||||||
/>
|
|
||||||
<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,14 +32,7 @@ export const ErrorDialog = ({
|
|||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("errorDialog.title")}
|
title={t("errorDialog.title")}
|
||||||
>
|
>
|
||||||
<div>
|
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div>
|
||||||
{message.split("\n").map((line) => (
|
|
||||||
<>
|
|
||||||
{line}
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -28,16 +28,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ExportDialog__name {
|
@include isMobile {
|
||||||
grid-column: project-name;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
.TextInput {
|
|
||||||
height: calc(1rem - 3px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
|
||||||
.ExportDialog {
|
.ExportDialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -66,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,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,17 @@ 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("helpDialog.editSelectedShape")}
|
||||||
|
shortcuts={[
|
||||||
|
getShortcutKey("Enter"),
|
||||||
|
t("helpDialog.doubleClick"),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.textNewLine")}
|
label={t("helpDialog.textNewLine")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
@@ -231,6 +238,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>
|
||||||
@@ -349,6 +364,22 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.ungroup")}
|
label={t("labels.ungroup")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.flipHorizontal")}
|
||||||
|
shortcuts={[getShortcutKey("Shift+H")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.flipVertical")}
|
||||||
|
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>
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ type HelpIconProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HelpIcon = (props: HelpIconProps) => (
|
export const HelpIcon = (props: HelpIconProps) => (
|
||||||
<label title={`${props.title} — ?`} className="help-icon">
|
<button
|
||||||
<div onClick={props.onClick}>{questionCircle}</div>
|
className="help-icon"
|
||||||
</label>
|
onClick={props.onClick}
|
||||||
|
type="button"
|
||||||
|
title={`${props.title} — ?`}
|
||||||
|
aria-label={props.title}
|
||||||
|
>
|
||||||
|
{questionCircle}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getSelectedElements } from "../scene";
|
|||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement, isTextElement } from "../element/typeChecks";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
|
||||||
interface Hint {
|
interface Hint {
|
||||||
@@ -23,7 +23,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,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 (
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import { useIsMobile } from "./App";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
import { exportToCanvas } from "../scene/export";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import "./ExportDialog.scss";
|
import { clipboard, exportImage } from "./icons";
|
||||||
import { clipboard, exportFile, link } from "./icons";
|
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import "./ExportDialog.scss";
|
||||||
const scales = [1, 2, 3];
|
import { supported as fsSupported } from "browser-fs-access";
|
||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
import OpenColor from "open-color";
|
||||||
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
|
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||||
|
|
||||||
const supportsContextFilters =
|
const supportsContextFilters =
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
@@ -52,15 +53,37 @@ export type ExportCB = (
|
|||||||
scale?: number,
|
scale?: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
const ExportModal = ({
|
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,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
exportPadding = 10,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportToPng,
|
onExportToPng,
|
||||||
onExportToSvg,
|
onExportToSvg,
|
||||||
onExportToClipboard,
|
onExportToClipboard,
|
||||||
onExportToBackend,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
@@ -69,18 +92,12 @@ const ExportModal = ({
|
|||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
onExportToBackend?: ExportCB;
|
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
const [scale, setScale] = useState(defaultScale);
|
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const {
|
const { exportBackground, viewBackgroundColor } = appState;
|
||||||
exportBackground,
|
|
||||||
viewBackgroundColor,
|
|
||||||
shouldAddWatermark,
|
|
||||||
} = appState;
|
|
||||||
|
|
||||||
const exportedElements = exportSelected
|
const exportedElements = exportSelected
|
||||||
? getSelectedElements(elements, appState)
|
? getSelectedElements(elements, appState)
|
||||||
@@ -100,8 +117,6 @@ const ExportModal = ({
|
|||||||
exportBackground,
|
exportBackground,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
|
||||||
shouldAddWatermark,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// if converting to blob fails, there's some problem that will
|
// if converting to blob fails, there's some problem that will
|
||||||
@@ -124,8 +139,6 @@ const ExportModal = ({
|
|||||||
exportBackground,
|
exportBackground,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
scale,
|
|
||||||
shouldAddWatermark,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -133,106 +146,84 @@ const ExportModal = ({
|
|||||||
<div className="ExportDialog__preview" ref={previewRef} />
|
<div className="ExportDialog__preview" ref={previewRef} />
|
||||||
{supportsContextFilters &&
|
{supportsContextFilters &&
|
||||||
actionManager.renderAction("exportWithDarkMode")}
|
actionManager.renderAction("exportWithDarkMode")}
|
||||||
<Stack.Col gap={2} align="center">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
|
||||||
<div className="ExportDialog__actions">
|
<div
|
||||||
<Stack.Row gap={2}>
|
style={{
|
||||||
<ToolButton
|
display: "grid",
|
||||||
type="button"
|
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
|
||||||
label="PNG"
|
// dunno why this is needed, but when the items wrap it creates
|
||||||
title={t("buttons.exportToPng")}
|
// an overflow
|
||||||
aria-label={t("buttons.exportToPng")}
|
overflow: "hidden",
|
||||||
onClick={() => onExportToPng(exportedElements, scale)}
|
}}
|
||||||
/>
|
>
|
||||||
<ToolButton
|
{actionManager.renderAction("changeExportBackground")}
|
||||||
type="button"
|
{someElementIsSelected && (
|
||||||
label="SVG"
|
<CheckboxItem
|
||||||
title={t("buttons.exportToSvg")}
|
checked={exportSelected}
|
||||||
aria-label={t("buttons.exportToSvg")}
|
onChange={(checked) => setExportSelected(checked)}
|
||||||
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")}
|
{t("labels.onlySelected")}
|
||||||
</label>
|
</CheckboxItem>
|
||||||
</div>
|
)}
|
||||||
|
{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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!fsSupported && 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>
|
||||||
)}
|
)}
|
||||||
{actionManager.renderAction("changeExportEmbedScene")}
|
</Stack.Row>
|
||||||
{actionManager.renderAction("changeShouldAddWatermark")}
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExportDialog = ({
|
export const ImageExportDialog = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
exportPadding = 10,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportToPng,
|
onExportToPng,
|
||||||
onExportToSvg,
|
onExportToSvg,
|
||||||
onExportToClipboard,
|
onExportToClipboard,
|
||||||
onExportToBackend,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
@@ -241,14 +232,11 @@ export const ExportDialog = ({
|
|||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
onExportToBackend?: ExportCB;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(false);
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
const triggerButton = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setModalIsShown(false);
|
setModalIsShown(false);
|
||||||
triggerButton.current?.focus();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -257,16 +245,16 @@ export const ExportDialog = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalIsShown(true);
|
setModalIsShown(true);
|
||||||
}}
|
}}
|
||||||
icon={exportFile}
|
data-testid="image-export-button"
|
||||||
|
icon={exportImage}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t("buttons.export")}
|
aria-label={t("buttons.exportImage")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
title={t("buttons.export")}
|
title={t("buttons.exportImage")}
|
||||||
ref={triggerButton}
|
|
||||||
/>
|
/>
|
||||||
{modalIsShown && (
|
{modalIsShown && (
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
||||||
<ExportModal
|
<ImageExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
exportPadding={exportPadding}
|
exportPadding={exportPadding}
|
||||||
@@ -274,7 +262,6 @@ export const ExportDialog = ({
|
|||||||
onExportToPng={onExportToPng}
|
onExportToPng={onExportToPng}
|
||||||
onExportToSvg={onExportToSvg}
|
onExportToSvg={onExportToSvg}
|
||||||
onExportToClipboard={onExportToClipboard}
|
onExportToClipboard={onExportToClipboard}
|
||||||
onExportToBackend={onExportToBackend}
|
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -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,127 @@
|
|||||||
|
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 } 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 { supported as fsSupported } from "browser-fs-access";
|
||||||
|
|
||||||
|
export type ExportCB = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scale?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const JSONExportModal = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager,
|
||||||
|
exportOpts,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
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")}
|
||||||
|
{!fsSupported && 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, canvas)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{exportOpts.renderCustomUI &&
|
||||||
|
exportOpts.renderCustomUI(elements, appState, canvas)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JSONExportDialog = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager,
|
||||||
|
exportOpts,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
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}
|
||||||
|
actionManager={actionManager}
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
exportOpts={exportOpts}
|
||||||
|
canvas={canvas}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
-40
@@ -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%;
|
||||||
z-index: 100;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
&-right {
|
||||||
right: 0;
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 190px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zen-mode-transition {
|
.zen-mode-transition {
|
||||||
@@ -105,12 +72,16 @@
|
|||||||
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(-92px, 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(92px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.layer-ui__wrapper__footer-left--transition-bottom {
|
||||||
|
transform: translate(0, 92px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disable-zen-mode {
|
.disable-zen-mode {
|
||||||
@@ -137,5 +108,17 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+254
-191
@@ -10,29 +10,33 @@ 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 { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
|
import {
|
||||||
|
AppProps,
|
||||||
|
AppState,
|
||||||
|
ExcalidrawProps,
|
||||||
|
LibraryItem,
|
||||||
|
LibraryItems,
|
||||||
|
} from "../types";
|
||||||
import { muteFSAbortError } from "../utils";
|
import { muteFSAbortError } from "../utils";
|
||||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
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";
|
||||||
@@ -41,6 +45,9 @@ 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";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -57,14 +64,14 @@ interface LayerUIProps {
|
|||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
onExportToBackend?: (
|
renderTopRightUI?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
appState: AppState,
|
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
) => void;
|
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
|
||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@@ -96,35 +103,44 @@ 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,
|
||||||
|
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"];
|
||||||
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 = [];
|
||||||
let addedPendingElements = false;
|
let addedPendingElements = false;
|
||||||
|
|
||||||
const referrer = libraryReturnUrl || window.location.origin;
|
const referrer =
|
||||||
|
libraryReturnUrl || window.location.origin + window.location.pathname;
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
<div className="layer-ui__library-header">
|
<div className="layer-ui__library-header" key="library-header">
|
||||||
<ToolButton
|
<ToolButton
|
||||||
key="import"
|
key="import"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -132,11 +148,11 @@ const LibraryMenuItems = ({
|
|||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
icon={load}
|
icon={load}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
importLibraryFromJSON()
|
importLibraryFromJSON(library)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Maybe we should close and open the menu so that the items get updated.
|
// Close and then open to get the libraries updated
|
||||||
// But for now we just close the menu.
|
|
||||||
setAppState({ isLibraryOpen: false });
|
setAppState({ isLibraryOpen: false });
|
||||||
|
setAppState({ isLibraryOpen: true });
|
||||||
})
|
})
|
||||||
.catch(muteFSAbortError)
|
.catch(muteFSAbortError)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -144,36 +160,42 @@ const LibraryMenuItems = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ToolButton
|
{!!libraryItems.length && (
|
||||||
key="export"
|
<>
|
||||||
type="button"
|
<ToolButton
|
||||||
title={t("buttons.export")}
|
key="export"
|
||||||
aria-label={t("buttons.export")}
|
type="button"
|
||||||
icon={exportFile}
|
title={t("buttons.export")}
|
||||||
onClick={() => {
|
aria-label={t("buttons.export")}
|
||||||
saveLibraryAsJSON()
|
icon={exportFile}
|
||||||
.catch(muteFSAbortError)
|
onClick={() => {
|
||||||
.catch((error) => {
|
saveLibraryAsJSON(library)
|
||||||
setAppState({ errorMessage: error.message });
|
.catch(muteFSAbortError)
|
||||||
});
|
.catch((error) => {
|
||||||
}}
|
setAppState({ errorMessage: error.message });
|
||||||
/>
|
});
|
||||||
<ToolButton
|
}}
|
||||||
key="reset"
|
/>
|
||||||
type="button"
|
<ToolButton
|
||||||
title={t("buttons.resetLibrary")}
|
key="reset"
|
||||||
aria-label={t("buttons.resetLibrary")}
|
type="button"
|
||||||
icon={trash}
|
title={t("buttons.resetLibrary")}
|
||||||
onClick={() => {
|
aria-label={t("buttons.resetLibrary")}
|
||||||
if (window.confirm(t("alerts.resetLibrary"))) {
|
icon={trash}
|
||||||
Library.resetLibrary();
|
onClick={() => {
|
||||||
setLibraryItems([]);
|
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||||
}
|
library.resetLibrary();
|
||||||
}}
|
setLibraryItems([]);
|
||||||
/>
|
focusContainer();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href={`https://libraries.excalidraw.com?referrer=${referrer}`}
|
href={`https://libraries.excalidraw.com?target=${
|
||||||
|
window.name || "_blank"
|
||||||
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`}
|
||||||
target="_excalidraw_libraries"
|
target="_excalidraw_libraries"
|
||||||
>
|
>
|
||||||
{t("labels.libraries")}
|
{t("labels.libraries")}
|
||||||
@@ -188,13 +210,13 @@ 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]}
|
||||||
pendingElements={
|
pendingElements={
|
||||||
shouldAddPendingElements ? pendingElements : undefined
|
shouldAddPendingElements ? pendingElements : undefined
|
||||||
}
|
}
|
||||||
@@ -202,7 +224,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>,
|
||||||
@@ -227,15 +249,23 @@ const LibraryMenu = ({
|
|||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
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"];
|
||||||
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) => {
|
||||||
@@ -261,7 +291,7 @@ const LibraryMenu = ({
|
|||||||
resolve("loading");
|
resolve("loading");
|
||||||
}, 100);
|
}, 100);
|
||||||
}),
|
}),
|
||||||
Library.loadLibrary().then((items) => {
|
library.loadLibrary().then((items) => {
|
||||||
setLibraryItems(items);
|
setLibraryItems(items);
|
||||||
setIsLoading("ready");
|
setIsLoading("ready");
|
||||||
}),
|
}),
|
||||||
@@ -273,24 +303,33 @@ 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 nextItems = items.filter((_, index) => index !== indexToRemove);
|
const items = await library.loadLibrary();
|
||||||
Library.saveLibrary(nextItems);
|
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
||||||
setLibraryItems(nextItems);
|
library.saveLibrary(nextItems).catch((error) => {
|
||||||
}, []);
|
setLibraryItems(items);
|
||||||
|
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||||
|
});
|
||||||
|
setLibraryItems(nextItems);
|
||||||
|
},
|
||||||
|
[library, setAppState],
|
||||||
|
);
|
||||||
|
|
||||||
const addToLibrary = useCallback(
|
const addToLibrary = useCallback(
|
||||||
async (elements: LibraryItem) => {
|
async (elements: LibraryItem) => {
|
||||||
const items = await Library.loadLibrary();
|
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 : (
|
||||||
@@ -301,7 +340,7 @@ const LibraryMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LibraryMenuItems
|
<LibraryMenuItems
|
||||||
library={libraryItems}
|
libraryItems={libraryItems}
|
||||||
onRemoveFromLibrary={removeFromLibrary}
|
onRemoveFromLibrary={removeFromLibrary}
|
||||||
onAddToLibrary={addToLibrary}
|
onAddToLibrary={addToLibrary}
|
||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertShape}
|
||||||
@@ -309,6 +348,10 @@ const LibraryMenu = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
setLibraryItems={setLibraryItems}
|
setLibraryItems={setLibraryItems}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
|
library={library}
|
||||||
|
theme={theme}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Island>
|
</Island>
|
||||||
@@ -329,69 +372,69 @@ const LayerUI = ({
|
|||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
toggleZenMode,
|
toggleZenMode,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
onExportToBackend,
|
renderTopRightUI,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
UIOptions,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const renderEncryptedIcon = () => (
|
const renderJSONExportDialog = () => {
|
||||||
<a
|
if (!UIOptions.canvasActions.export) {
|
||||||
className={clsx("encrypted-icon tooltip zen-mode-visibility", {
|
return null;
|
||||||
"zen-mode-visibility--hidden": zenModeEnabled,
|
}
|
||||||
})}
|
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
return (
|
||||||
target="_blank"
|
<JSONExportDialog
|
||||||
rel="noopener noreferrer"
|
elements={elements}
|
||||||
>
|
appState={appState}
|
||||||
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
|
actionManager={actionManager}
|
||||||
{shield}
|
exportOpts={UIOptions.canvasActions.export}
|
||||||
</Tooltip>
|
canvas={canvas}
|
||||||
</a>
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderImageExportDialog = () => {
|
||||||
|
if (!UIOptions.canvasActions.saveAsImage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const renderExportDialog = () => {
|
|
||||||
const createExporter = (type: ExportType): ExportCB => async (
|
const createExporter = (type: ExportType): ExportCB => async (
|
||||||
exportedElements,
|
exportedElements,
|
||||||
scale,
|
|
||||||
) => {
|
) => {
|
||||||
if (canvas) {
|
await exportCanvas(type, exportedElements, appState, {
|
||||||
await exportCanvas(type, exportedElements, appState, canvas, {
|
exportBackground: appState.exportBackground,
|
||||||
exportBackground: appState.exportBackground,
|
name: appState.name,
|
||||||
name: appState.name,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
})
|
||||||
scale,
|
.catch(muteFSAbortError)
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
.catch((error) => {
|
||||||
})
|
console.error(error);
|
||||||
.catch(muteFSAbortError)
|
setAppState({ errorMessage: error.message });
|
||||||
.catch((error) => {
|
});
|
||||||
console.error(error);
|
|
||||||
setAppState({ errorMessage: error.message });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExportDialog
|
<ImageExportDialog
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
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
|
||||||
@@ -405,9 +448,8 @@ 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>
|
||||||
@@ -426,11 +468,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}
|
||||||
@@ -445,6 +488,9 @@ const LayerUI = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
/>
|
/>
|
||||||
|
{appState.fileHandle && (
|
||||||
|
<>{actionManager.renderAction("saveToActiveFile")}</>
|
||||||
|
)}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -463,7 +509,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
|
||||||
@@ -498,6 +545,10 @@ const LayerUI = ({
|
|||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
|
library={library}
|
||||||
|
theme={appState.theme}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -524,6 +575,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 })}
|
||||||
@@ -535,15 +592,12 @@ const LayerUI = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
|
||||||
/>
|
/>
|
||||||
</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}
|
||||||
@@ -551,24 +605,30 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
<UserList
|
<div
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx(
|
||||||
"transition-right": zenModeEnabled,
|
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||||
})}
|
{
|
||||||
|
"transition-right": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{appState.collaborators.size > 0 &&
|
<UserList>
|
||||||
Array.from(appState.collaborators)
|
{appState.collaborators.size > 0 &&
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
Array.from(appState.collaborators)
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
// Collaborator is either not initialized or is actually the current user.
|
||||||
.map(([clientId, client]) => (
|
.filter(([_, client]) => Object.keys(client).length !== 0)
|
||||||
<Tooltip
|
.map(([clientId, client]) => (
|
||||||
label={client.username || "Unknown user"}
|
<Tooltip
|
||||||
key={clientId}
|
label={client.username || "Unknown user"}
|
||||||
>
|
key={clientId}
|
||||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
>
|
||||||
</Tooltip>
|
{actionManager.renderAction("goToCollaborator", clientId)}
|
||||||
))}
|
</Tooltip>
|
||||||
</UserList>
|
))}
|
||||||
|
</UserList>
|
||||||
|
{renderTopRightUI?.(isMobile, appState)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
);
|
);
|
||||||
@@ -576,61 +636,61 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const renderBottomAppMenu = () => {
|
const renderBottomAppMenu = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<footer
|
||||||
className={clsx("App-menu App-menu_bottom zen-mode-transition", {
|
role="contentinfo"
|
||||||
"App-menu_bottom--transition-left": zenModeEnabled,
|
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Stack.Col gap={2}>
|
<div
|
||||||
<Section heading="canvasActions">
|
className={clsx(
|
||||||
<Island padding={1}>
|
"layer-ui__wrapper__footer-left zen-mode-transition",
|
||||||
<ZoomActions
|
{
|
||||||
renderAction={actionManager.renderAction}
|
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
||||||
zoom={appState.zoom}
|
},
|
||||||
/>
|
)}
|
||||||
</Island>
|
>
|
||||||
{renderEncryptedIcon()}
|
<Stack.Col gap={2}>
|
||||||
</Section>
|
<Section heading="canvasActions">
|
||||||
</Stack.Col>
|
<Island padding={1}>
|
||||||
</div>
|
<ZoomActions
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
zoom={appState.zoom}
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
</Section>
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||||
|
{
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderCustomFooter?.(false, appState)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-right zen-mode-transition",
|
||||||
|
{
|
||||||
|
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("toggleShortcuts")}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={clsx("disable-zen-mode", {
|
||||||
|
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||||
|
})}
|
||||||
|
onClick={toggleZenMode}
|
||||||
|
>
|
||||||
|
{t("buttons.exitZenMode")}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGitHubCorner = () => {
|
|
||||||
return (
|
|
||||||
<aside
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
|
||||||
{
|
|
||||||
"transition-right": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<GitHubCorner theme={appState.theme} />
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderFooter = () => (
|
|
||||||
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
|
||||||
<div
|
|
||||||
className={clsx("zen-mode-transition", {
|
|
||||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{renderCustomFooter?.(false)}
|
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={clsx("disable-zen-mode", {
|
|
||||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
|
||||||
})}
|
|
||||||
onClick={toggleZenMode}
|
|
||||||
>
|
|
||||||
{t("buttons.exitZenMode")}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
|
|
||||||
const dialogs = (
|
const dialogs = (
|
||||||
<>
|
<>
|
||||||
{appState.isLoading && <LoadingMessage />}
|
{appState.isLoading && <LoadingMessage />}
|
||||||
@@ -641,7 +701,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
|
||||||
@@ -666,7 +730,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}
|
||||||
@@ -689,8 +754,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_m`,
|
||||||
|
{
|
||||||
|
"zen-mode-visibility--hidden": appState.zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
||||||
|
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="9"
|
||||||
|
/>
|
||||||
|
<div className="ToolIcon__icon">{LIBRARY_ICON}</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import React, { 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 { LibraryItem } from "../types";
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
@@ -36,22 +36,27 @@ export const LibraryUnit = ({
|
|||||||
if (!elementsToRender) {
|
if (!elementsToRender) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const svg = exportToSvg(elementsToRender, {
|
let svg: SVGSVGElement;
|
||||||
exportBackground: false,
|
|
||||||
viewBackgroundColor: oc.white,
|
|
||||||
shouldAddWatermark: false,
|
|
||||||
});
|
|
||||||
for (const child of ref.current!.children) {
|
|
||||||
if (child.tagName !== "svg") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ref.current!.removeChild(child);
|
|
||||||
}
|
|
||||||
ref.current!.appendChild(svg);
|
|
||||||
|
|
||||||
const current = ref.current!;
|
const current = ref.current!;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
svg = await exportToSvg(elementsToRender, {
|
||||||
|
exportBackground: false,
|
||||||
|
viewBackgroundColor: oc.white,
|
||||||
|
});
|
||||||
|
for (const child of ref.current!.children) {
|
||||||
|
if (child.tagName !== "svg") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current!.removeChild(child);
|
||||||
|
}
|
||||||
|
current!.appendChild(svg);
|
||||||
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
current.removeChild(svg);
|
if (svg) {
|
||||||
|
current.removeChild(svg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [elements, pendingElements]);
|
}, [elements, pendingElements]);
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,12 +39,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 +55,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,7 +30,7 @@ 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;
|
||||||
};
|
};
|
||||||
@@ -38,7 +40,8 @@ export const MobileMenu = ({
|
|||||||
elements,
|
elements,
|
||||||
libraryMenu,
|
libraryMenu,
|
||||||
actionManager,
|
actionManager,
|
||||||
exportButton,
|
renderJSONExportDialog,
|
||||||
|
renderImageExportDialog,
|
||||||
setAppState,
|
setAppState,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
@@ -62,15 +65,15 @@ export const MobileMenu = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<LockIcon
|
<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 +110,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 +156,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>
|
||||||
|
|||||||
@@ -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,11 @@
|
|||||||
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";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -11,8 +13,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 = "light" } = props;
|
||||||
|
const modalRoot = useBodyRoot(theme);
|
||||||
|
|
||||||
if (!modalRoot) {
|
if (!modalRoot) {
|
||||||
return null;
|
return null;
|
||||||
@@ -21,6 +25,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 +42,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 +51,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 +86,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,21 @@ const ChartPreviewBtn = (props: {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
setChartElements(elements);
|
setChartElements(elements);
|
||||||
|
let svg: SVGSVGElement;
|
||||||
const svg = exportToSvg(elements, {
|
|
||||||
exportBackground: false,
|
|
||||||
viewBackgroundColor: oc.white,
|
|
||||||
shouldAddWatermark: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const previewNode = previewRef.current!;
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
previewNode.appendChild(svg);
|
(async () => {
|
||||||
|
svg = await exportToSvg(elements, {
|
||||||
|
exportBackground: false,
|
||||||
|
viewBackgroundColor: oc.white,
|
||||||
|
});
|
||||||
|
|
||||||
if (props.selected) {
|
previewNode.appendChild(svg);
|
||||||
(previewNode.parentNode as HTMLDivElement).focus();
|
|
||||||
}
|
if (props.selected) {
|
||||||
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode.removeChild(svg);
|
previewNode.removeChild(svg);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.popover {
|
.popover {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,28 +1,31 @@
|
|||||||
import "./TextInput.scss";
|
import "./TextInput.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
import { selectNode, removeSelection } from "../utils";
|
import { focusNearestParent } from "../utils";
|
||||||
|
|
||||||
|
import "./ProjectName.scss";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
|
isNameEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ProjectName extends Component<Props> {
|
export const ProjectName = (props: Props) => {
|
||||||
private handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
const { id } = useExcalidrawContainer();
|
||||||
selectNode(event.currentTarget);
|
const [fileName, setFileName] = useState<string>(props.value);
|
||||||
};
|
|
||||||
|
|
||||||
private handleBlur = (event: React.FocusEvent<HTMLElement>) => {
|
const handleBlur = (event: any) => {
|
||||||
const value = event.currentTarget.innerText.trim();
|
focusNearestParent(event.target);
|
||||||
if (value !== this.props.value) {
|
const value = event.target.value;
|
||||||
this.props.onChange(value);
|
if (value !== props.value) {
|
||||||
|
props.onChange(value);
|
||||||
}
|
}
|
||||||
removeSelection();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
||||||
@@ -31,32 +34,26 @@ export class ProjectName extends Component<Props> {
|
|||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private makeEditable = (editable: HTMLSpanElement | null) => {
|
|
||||||
if (!editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
editable.contentEditable = "plaintext-only";
|
|
||||||
} catch {
|
|
||||||
editable.contentEditable = "true";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
return (
|
||||||
return (
|
<div className="ProjectName">
|
||||||
<span
|
<label className="ProjectName-label" htmlFor="filename">
|
||||||
suppressContentEditableWarning
|
{`${props.label}${props.isNameEditable ? "" : ":"}`}
|
||||||
ref={this.makeEditable}
|
</label>
|
||||||
data-type="wysiwyg"
|
{props.isNameEditable ? (
|
||||||
className="TextInput"
|
<input
|
||||||
role="textbox"
|
className="TextInput"
|
||||||
aria-label={this.props.label}
|
onBlur={handleBlur}
|
||||||
onBlur={this.handleBlur}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
id={`${id}-filename`}
|
||||||
onFocus={this.handleFocus}
|
value={fileName}
|
||||||
>
|
onChange={(event) => setFileName(event.target.value)}
|
||||||
{this.props.value}
|
/>
|
||||||
</span>
|
) : (
|
||||||
);
|
<span className="TextInput TextInput--readonly" id={`${id}-filename`}>
|
||||||
}
|
{props.value}
|
||||||
}
|
</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)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+10
-95
@@ -1,49 +1,22 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { copyTextToSystemClipboard } from "../clipboard";
|
|
||||||
import { DEFAULT_VERSION } from "../constants";
|
|
||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import {
|
|
||||||
getElementsStorageSize,
|
|
||||||
getTotalStorageSize,
|
|
||||||
} from "../excalidraw-app/data/localStorage";
|
|
||||||
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 } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { debounce, getVersion, nFormatter } from "../utils";
|
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import "./Stats.scss";
|
import "./Stats.scss";
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
|
||||||
|
|
||||||
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
|
||||||
cb({
|
|
||||||
scene: getElementsStorageSize(),
|
|
||||||
total: getTotalStorageSize(),
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
export const Stats = (props: {
|
export const Stats = (props: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
|
|
||||||
scene: 0,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getStorageSizes((sizes) => {
|
|
||||||
setStorageSizes(sizes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => () => getStorageSizes.cancel(), []);
|
|
||||||
|
|
||||||
const boundingBox = getCommonBounds(props.elements);
|
const boundingBox = getCommonBounds(props.elements);
|
||||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||||
@@ -53,17 +26,6 @@ export const Stats = (props: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = getVersion();
|
|
||||||
let hash;
|
|
||||||
let timestamp;
|
|
||||||
|
|
||||||
if (version !== DEFAULT_VERSION) {
|
|
||||||
timestamp = version.slice(0, 16).replace("T", " ");
|
|
||||||
hash = version.slice(21);
|
|
||||||
} else {
|
|
||||||
timestamp = t("stats.versionNotAvailable");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Stats">
|
<div className="Stats">
|
||||||
<Island padding={2}>
|
<Island padding={2}>
|
||||||
@@ -88,17 +50,7 @@ export const Stats = (props: {
|
|||||||
<td>{t("stats.height")}</td>
|
<td>{t("stats.height")}</td>
|
||||||
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
|
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th colSpan={2}>{t("stats.storage")}</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("stats.scene")}</td>
|
|
||||||
<td>{nFormatter(storageSizes.scene, 1)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("stats.total")}</td>
|
|
||||||
<td>{nFormatter(storageSizes.total, 1)}</td>
|
|
||||||
</tr>
|
|
||||||
{selectedElements.length === 1 && (
|
{selectedElements.length === 1 && (
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2}>{t("stats.element")}</th>
|
<th colSpan={2}>{t("stats.element")}</th>
|
||||||
@@ -120,31 +72,17 @@ export const Stats = (props: {
|
|||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{"x"}</td>
|
<td>{"x"}</td>
|
||||||
<td>
|
<td>{Math.round(selectedBoundingBox[0])}</td>
|
||||||
{Math.round(
|
|
||||||
selectedElements.length === 1
|
|
||||||
? selectedElements[0].x
|
|
||||||
: selectedBoundingBox[0],
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{"y"}</td>
|
<td>{"y"}</td>
|
||||||
<td>
|
<td>{Math.round(selectedBoundingBox[1])}</td>
|
||||||
{Math.round(
|
|
||||||
selectedElements.length === 1
|
|
||||||
? selectedElements[0].y
|
|
||||||
: selectedBoundingBox[1],
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("stats.width")}</td>
|
<td>{t("stats.width")}</td>
|
||||||
<td>
|
<td>
|
||||||
{Math.round(
|
{Math.round(
|
||||||
selectedElements.length === 1
|
selectedBoundingBox[2] - selectedBoundingBox[0],
|
||||||
? selectedElements[0].width
|
|
||||||
: selectedBoundingBox[2] - selectedBoundingBox[0],
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -152,9 +90,7 @@ export const Stats = (props: {
|
|||||||
<td>{t("stats.height")}</td>
|
<td>{t("stats.height")}</td>
|
||||||
<td>
|
<td>
|
||||||
{Math.round(
|
{Math.round(
|
||||||
selectedElements.length === 1
|
selectedBoundingBox[3] - selectedBoundingBox[1],
|
||||||
? selectedElements[0].height
|
|
||||||
: selectedBoundingBox[3] - selectedBoundingBox[1],
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -170,28 +106,7 @@ export const Stats = (props: {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr>
|
{props.renderCustomStats?.(props.elements, props.appState)}
|
||||||
<th colSpan={2}>{t("stats.version")}</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={2}
|
|
||||||
style={{ textAlign: "center", cursor: "pointer" }}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await copyTextToSystemClipboard(getVersion());
|
|
||||||
props.setAppState({
|
|
||||||
toastMessage: t("toast.copyToClipboard"),
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}}
|
|
||||||
title={t("stats.versionCopy")}
|
|
||||||
>
|
|
||||||
{timestamp}
|
|
||||||
<br />
|
|
||||||
{hash}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
type ToolIconSize = "s" | "m";
|
type ToolIconSize = "s" | "m";
|
||||||
|
|
||||||
@@ -29,9 +30,13 @@ type ToolButtonProps =
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
})
|
})
|
||||||
|
| (ToolButtonBaseProps & {
|
||||||
|
type: "icon";
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick?(): void;
|
||||||
|
})
|
||||||
| (ToolButtonBaseProps & {
|
| (ToolButtonBaseProps & {
|
||||||
type: "radio";
|
type: "radio";
|
||||||
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
});
|
});
|
||||||
@@ -39,11 +44,12 @@ type ToolButtonProps =
|
|||||||
const DEFAULT_SIZE: ToolIconSize = "m";
|
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 || DEFAULT_SIZE}`;
|
||||||
|
|
||||||
if (props.type === "button") {
|
if (props.type === "button" || props.type === "icon") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -56,8 +62,10 @@ 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"]}
|
||||||
hidden={props.hidden}
|
hidden={props.hidden}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
aria-label={props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
@@ -65,14 +73,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
>
|
>
|
||||||
<div className="ToolIcon__icon" aria-hidden="true">
|
{(props.icon || props.label) && (
|
||||||
{props.icon || props.label}
|
<div className="ToolIcon__icon" aria-hidden="true">
|
||||||
{props.keyBindingLabel && (
|
{props.icon || props.label}
|
||||||
<span className="ToolIcon__keybinding">
|
{props.keyBindingLabel && (
|
||||||
{props.keyBindingLabel}
|
<span className="ToolIcon__keybinding">
|
||||||
</span>
|
{props.keyBindingLabel}
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{props.showAriaLabel && (
|
{props.showAriaLabel && (
|
||||||
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
||||||
)}
|
)}
|
||||||
@@ -90,7 +100,7 @@ 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}
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -57,14 +74,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 +86,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
&--show {
|
&--show {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -94,6 +111,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 +141,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 +186,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 +213,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-50
@@ -1,58 +1,39 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
.excalidraw {
|
.excalidraw-tooltip {
|
||||||
.Tooltip {
|
position: absolute;
|
||||||
position: relative;
|
z-index: 1000;
|
||||||
}
|
|
||||||
|
|
||||||
.Tooltip__label {
|
padding: 8px;
|
||||||
--arrow-size: 4px;
|
border-radius: 6px;
|
||||||
visibility: hidden;
|
box-sizing: border-box;
|
||||||
background: $oc-black;
|
pointer-events: none;
|
||||||
color: $oc-white;
|
word-wrap: break-word;
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 500;
|
|
||||||
// extra pixel offset for unknown reasons
|
|
||||||
left: calc(50% + var(--arrow-size) / 2 - 1px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--below {
|
&.excalidraw-tooltip--visible {
|
||||||
top: calc(100% + var(--arrow-size) + 3px);
|
display: block;
|
||||||
|
}
|
||||||
&::after {
|
}
|
||||||
border-bottom-color: $oc-black;
|
|
||||||
bottom: 100%;
|
.excalidraw {
|
||||||
}
|
.Tooltip-icon {
|
||||||
}
|
width: 0.9em;
|
||||||
}
|
height: 0.9em;
|
||||||
|
margin-left: 5px;
|
||||||
.Tooltip:hover .Tooltip__label {
|
margin-top: 1px;
|
||||||
visibility: visible;
|
display: flex;
|
||||||
}
|
|
||||||
|
@include isMobile {
|
||||||
.Tooltip__label:hover {
|
display: none;
|
||||||
visibility: visible;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-20
@@ -1,31 +1,92 @@
|
|||||||
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(() => {
|
||||||
label,
|
return () =>
|
||||||
position = "below",
|
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
|
||||||
long = false,
|
}, []);
|
||||||
}: TooltipProps) => (
|
|
||||||
<div className="Tooltip">
|
return (
|
||||||
<span
|
<div
|
||||||
className={
|
onPointerEnter={(event) =>
|
||||||
position === "above"
|
updateTooltip(
|
||||||
? "Tooltip__label Tooltip__label--above"
|
event.currentTarget as HTMLDivElement,
|
||||||
: "Tooltip__label Tooltip__label--below"
|
getTooltipDiv(),
|
||||||
|
label,
|
||||||
|
long,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPointerLeave={() =>
|
||||||
|
getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
|
||||||
}
|
}
|
||||||
style={{ width: long ? "50ch" : "10ch" }}
|
|
||||||
>
|
>
|
||||||
{label}
|
{children}
|
||||||
</span>
|
</div>
|
||||||
{children}
|
);
|
||||||
</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;
|
||||||
|
|||||||
+194
-99
@@ -24,7 +24,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 +44,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 +91,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 },
|
||||||
@@ -123,6 +147,22 @@ export const shareIOS = createIcon(
|
|||||||
{ width: 24, height: 24 },
|
{ width: 24, height: 24 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const shareWindows = createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M40 5.6v6.1l-4.1.7c-8.9 1.4-16.5 6.9-20.6 15C13 32 10.9 43 12.4 43c.4 0 2.4-1.3 4.4-3 5-3.9 12.1-7 18.2-7.7l5-.6v12.8l11.2-11.3L62.5 22 51.2 10.8 40-.5v6.1zm10.2 22.6L44 34.5v-6.8l-6.9.6c-3.9.3-9.8 1.7-13.2 3.1-3.5 1.4-6.5 2.4-6.7 2.2-.9-1 3-7.5 6.4-10.8C28 18.6 34.4 16 40.1 16c3.7 0 3.9-.1 3.9-3.2V9.5l6.2 6.3 6.3 6.2-6.3 6.2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M0 36v20h48v-6.2c0-6 0-6.1-2-4.3-1.1 1-2 2.9-2 4.2V52H4V34c0-17.3-.1-18-2-18s-2 .7-2 20z"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 64, height: 64 },
|
||||||
|
);
|
||||||
|
|
||||||
// Icon imported form Storybook
|
// Icon imported form Storybook
|
||||||
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
|
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
|
||||||
export const resetZoom = createIcon(
|
export const resetZoom = createIcon(
|
||||||
@@ -206,14 +246,12 @@ export const SendToBackIcon = React.memo(
|
|||||||
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"
|
||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
@@ -319,7 +357,6 @@ export const DistributeHorizontallyIcon = React.memo(
|
|||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M5 5V19Z" fill="black" />
|
|
||||||
<path
|
<path
|
||||||
d="M19 5V19M5 5V19"
|
d="M19 5V19M5 5V19"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
@@ -337,14 +374,6 @@ 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: "light" | "dark" }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
@@ -448,6 +477,11 @@ export const shield = createIcon(
|
|||||||
{ width: 24 },
|
{ width: 24 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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: "light" | "dark" }) =>
|
export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
@@ -463,42 +497,16 @@ 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 },
|
||||||
),
|
),
|
||||||
@@ -520,60 +528,18 @@ 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 },
|
||||||
),
|
),
|
||||||
@@ -615,9 +581,10 @@ export const StrokeWidthIcon = React.memo(
|
|||||||
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
|
({ theme, strokeWidth }: { theme: "light" | "dark"; 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 },
|
||||||
@@ -632,6 +599,7 @@ export const StrokeStyleSolidIcon = React.memo(
|
|||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -649,6 +617,7 @@ 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 },
|
||||||
),
|
),
|
||||||
@@ -658,11 +627,12 @@ export const StrokeStyleDottedIcon = React.memo(
|
|||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ 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 },
|
||||||
),
|
),
|
||||||
@@ -675,6 +645,7 @@ export const SloppinessArchitectIcon = React.memo(
|
|||||||
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 },
|
||||||
@@ -688,6 +659,7 @@ export const SloppinessArtistIcon = React.memo(
|
|||||||
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 },
|
||||||
@@ -701,6 +673,7 @@ export const SloppinessCartoonistIcon = React.memo(
|
|||||||
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 },
|
||||||
@@ -714,6 +687,7 @@ export const EdgeSharpIcon = React.memo(
|
|||||||
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 },
|
||||||
@@ -727,6 +701,7 @@ export const EdgeRoundIcon = React.memo(
|
|||||||
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 },
|
||||||
@@ -794,3 +769,123 @@ export const ArrowheadBarIcon = React.memo(
|
|||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const FontSizeSmallIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 0 69.092 L 0 55.03 A 124.24 124.24 0 0 0 4.706 57.02 Q 6.826 57.863 8.708 58.5 A 53.466 53.466 0 0 0 12.231 59.571 Q 17.236 60.889 21.387 60.889 A 20.909 20.909 0 0 0 24.265 60.704 Q 25.719 60.502 26.903 60.077 A 8.649 8.649 0 0 0 29.028 58.985 Q 31.689 57.08 31.689 53.321 Q 31.689 51.221 30.518 49.585 A 10.126 10.126 0 0 0 29.282 48.177 Q 28.352 47.287 27.075 46.436 A 23.719 23.719 0 0 0 25.752 45.627 Q 23.774 44.492 20.176 42.735 A 254.44 254.44 0 0 0 17.822 41.602 Q 11.503 38.631 8.236 35.888 A 19.742 19.742 0 0 1 8.008 35.694 A 22.18 22.18 0 0 1 2.783 29.102 Q 0.83 25.342 0.83 20.313 A 22.471 22.471 0 0 1 1.733 13.778 A 17.283 17.283 0 0 1 7.251 5.42 A 21.486 21.486 0 0 1 15.177 1.272 Q 18.361 0.338 22.166 0.09 A 43.573 43.573 0 0 1 25 0 A 42.399 42.399 0 0 1 34.349 1.01 A 39.075 39.075 0 0 1 35.62 1.319 A 67.407 67.407 0 0 1 42.108 3.382 A 83.357 83.357 0 0 1 46.191 5.03 L 41.309 16.797 Q 35.596 14.453 31.86 13.526 A 30.762 30.762 0 0 0 25.417 12.612 A 28.337 28.337 0 0 0 24.512 12.598 A 14.846 14.846 0 0 0 22.022 12.793 Q 19.498 13.224 17.92 14.6 Q 15.625 16.602 15.625 19.824 Q 15.625 21.826 16.553 23.316 Q 17.48 24.805 19.507 26.197 A 18.343 18.343 0 0 0 20.659 26.912 Q 22.596 28.035 26.516 29.953 A 299.99 299.99 0 0 0 29.102 31.201 Q 37.91 35.412 41.841 39.642 A 16.553 16.553 0 0 1 42.822 40.796 A 17.675 17.675 0 0 1 46.301 49.233 A 23.517 23.517 0 0 1 46.533 52.588 A 21.581 21.581 0 0 1 45.471 59.515 A 17.733 17.733 0 0 1 39.575 67.823 Q 33.745 72.486 24.094 73.243 A 49.683 49.683 0 0 1 20.215 73.389 A 51.712 51.712 0 0 1 9.448 72.315 A 40.672 40.672 0 0 1 0 69.092 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 47, height: 77 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontSizeMediumIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 44.092 71.387 L 30.225 71.387 L 13.037 15.381 L 12.598 15.381 A 1505.093 1505.093 0 0 1 12.959 22.313 Q 13.426 31.715 13.508 36.4 A 102.991 102.991 0 0 1 13.525 38.184 L 13.525 71.387 L 0 71.387 L 0 0 L 20.605 0 L 37.5 54.59 L 37.793 54.59 L 55.713 0 L 76.318 0 L 76.318 71.387 L 62.207 71.387 L 62.207 37.598 Q 62.207 35.205 62.28 32.08 A 160.703 160.703 0 0 1 62.326 30.544 Q 62.452 26.754 62.866 17.168 A 5390.536 5390.536 0 0 1 62.939 15.479 L 62.5 15.479 L 44.092 71.387 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 77, height: 75 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontSizeLargeIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 44.092 71.387 L 0 71.387 L 0 0 L 15.137 0 L 15.137 58.887 L 44.092 58.887 L 44.092 71.387 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 45, height: 75 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontSizeExtraLargeIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 42.578 35.4 L 66.699 71.387 L 49.414 71.387 L 32.813 44.385 L 16.211 71.387 L 0 71.387 L 23.682 34.57 L 1.514 0 L 18.213 0 L 33.594 25.684 L 48.682 0 L 64.99 0 L 42.578 35.4 Z M 119.775 71.387 L 75.684 71.387 L 75.684 0 L 90.82 0 L 90.82 58.887 L 119.775 58.887 L 119.775 71.387 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 120, height: 75 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontFamilyHandDrawnIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontFamilyNormalIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 63.818 71.68 L 54.492 71.68 L 45.898 49.561 L 17.578 49.561 L 9.082 71.68 L 0 71.68 L 27.881 0 L 35.986 0 L 63.818 71.68 Z M 20.605 41.602 L 43.213 41.602 L 35.205 19.971 L 31.787 9.277 Q 30.322 15.137 28.711 19.971 L 20.605 41.602 Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 68.994 71.68 L 52.686 71.68 L 47.51 54.688 L 21.484 54.688 L 16.309 71.68 L 0 71.68 L 25.195 0 L 43.701 0 L 68.994 71.68 Z M 25.293 41.992 L 43.896 41.992 A 27590.463 27590.463 0 0 1 42.2 36.532 Q 36.965 19.676 35.937 16.273 A 120.932 120.932 0 0 1 35.815 15.869 A 131.65 131.65 0 0 1 35.396 14.435 Q 34.951 12.879 34.675 11.741 A 34.866 34.866 0 0 1 34.521 11.084 A 141.762 141.762 0 0 1 33.706 14.075 Q 31.482 21.957 25.293 41.992 Z"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 70, height: 78 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontFamilyCodeIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M278.9 511.5l-61-17.7c-6.4-1.8-10-8.5-8.2-14.9L346.2 8.7c1.8-6.4 8.5-10 14.9-8.2l61 17.7c6.4 1.8 10 8.5 8.2 14.9L293.8 503.3c-1.9 6.4-8.5 10.1-14.9 8.2zm-114-112.2l43.5-46.4c4.6-4.9 4.3-12.7-.8-17.2L117 256l90.6-79.7c5.1-4.5 5.5-12.3.8-17.2l-43.5-46.4c-4.5-4.8-12.1-5.1-17-.5L3.8 247.2c-5.1 4.7-5.1 12.8 0 17.5l144.1 135.1c4.9 4.6 12.5 4.4 17-.5zm327.2.6l144.1-135.1c5.1-4.7 5.1-12.8 0-17.5L492.1 112.1c-4.8-4.5-12.4-4.3-17 .5L431.6 159c-4.6 4.9-4.3 12.7.8 17.2L523 256l-90.6 79.7c-5.1 4.5-5.5 12.3-.8 17.2l43.5 46.4c4.5 4.9 12.1 5.1 17 .6z"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 640, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignLeftIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<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"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignCenterIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<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"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignRightIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<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"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
+46
-8
@@ -1,5 +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 { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
@@ -13,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: "",
|
||||||
@@ -62,15 +64,15 @@ 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 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}";
|
||||||
@@ -84,9 +86,17 @@ export const MIME_TYPES = {
|
|||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EXPORT_DATA_TYPES = {
|
||||||
|
excalidraw: "excalidraw",
|
||||||
|
excalidrawClipboard: "excalidraw/clipboard",
|
||||||
|
excalidrawLibrary: "excalidrawlib",
|
||||||
|
} 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;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
@@ -95,7 +105,6 @@ 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
|
||||||
@@ -110,3 +119,32 @@ export const MODES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const THEME_FILTER = cssVariables.themeFilter;
|
export const THEME_FILTER = cssVariables.themeFilter;
|
||||||
|
|
||||||
|
export const URL_QUERY_KEYS = {
|
||||||
|
addLibrary: "addLibrary",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const URL_HASH_KEYS = {
|
||||||
|
addLibrary: "addLibrary",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||||
|
canvasActions: {
|
||||||
|
changeViewBackgroundColor: true,
|
||||||
|
clearCanvas: true,
|
||||||
|
export: { saveFileToDisk: true },
|
||||||
|
loadScene: true,
|
||||||
|
saveToActiveFile: 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
|
||||||
|
|||||||
@@ -5,6 +5,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 {
|
||||||
|
|||||||
+29
-28
@@ -16,6 +16,12 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
// serves 2 purposes:
|
// serves 2 purposes:
|
||||||
// 1. prevent selecting text outside the component when double-clicking or
|
// 1. prevent selecting text outside the component when double-clicking or
|
||||||
@@ -45,6 +51,13 @@
|
|||||||
image-rendering: -moz-crisp-edges; // FF
|
image-rendering: -moz-crisp-edges; // FF
|
||||||
|
|
||||||
z-index: var(--zIndex-canvas);
|
z-index: var(--zIndex-canvas);
|
||||||
|
|
||||||
|
// Remove the main canvas from document flow to avoid resizeObserver
|
||||||
|
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
||||||
|
}
|
||||||
|
|
||||||
|
&__canvas {
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme--dark {
|
&.theme--dark {
|
||||||
@@ -222,7 +235,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
svg {
|
svg {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 18px;
|
height: 14px;
|
||||||
|
padding: 2px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
&.active svg {
|
&.active svg {
|
||||||
@@ -319,8 +333,8 @@
|
|||||||
.App-menu_bottom {
|
.App-menu_bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: min-content auto min-content;
|
||||||
grid-gap: 4px;
|
grid-gap: 15px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
@@ -345,10 +359,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-menu_bottom > *:first-child {
|
.App-menu_bottom > *:first-child {
|
||||||
justify-self: flex-start;
|
justify-self: flex-start;
|
||||||
}
|
}
|
||||||
@@ -406,13 +416,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.dropdown-select--floating {
|
&.dropdown-select--floating {
|
||||||
position: absolute;
|
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-select__language.dropdown-select--floating {
|
.dropdown-select__language.dropdown-select--floating {
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
@@ -448,22 +456,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.help-icon {
|
.help-icon {
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: $oc-gray-6;
|
fill: $oc-gray-6;
|
||||||
bottom: 14px;
|
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
background: none;
|
||||||
|
color: var(--icon-fill-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
right: 14px;
|
margin-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
:root[dir="rtl"] & {
|
||||||
left: 14px;
|
margin-left: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
aside {
|
aside {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -479,20 +494,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.github-corner {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.zen-mode-visibility {
|
.zen-mode-visibility {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
+4
-2
@@ -14,11 +14,12 @@
|
|||||||
--focus-highlight-color: #{$oc-blue-2};
|
--focus-highlight-color: #{$oc-blue-2};
|
||||||
--icon-fill-color: #{$oc-black};
|
--icon-fill-color: #{$oc-black};
|
||||||
--icon-green-fill-color: #{$oc-green-9};
|
--icon-green-fill-color: #{$oc-green-9};
|
||||||
|
--default-bg-color: #{$oc-white};
|
||||||
--input-bg-color: #{$oc-white};
|
--input-bg-color: #{$oc-white};
|
||||||
--input-border-color: #{$oc-gray-3};
|
--input-border-color: #{$oc-gray-3};
|
||||||
--input-hover-bg-color: #{$oc-gray-1};
|
--input-hover-bg-color: #{$oc-gray-1};
|
||||||
--input-label-color: #{$oc-gray-7};
|
--input-label-color: #{$oc-gray-7};
|
||||||
--island-bg-color: rgba(255, 255, 255, 0.9);
|
--island-bg-color: rgba(255, 255, 255, 0.96);
|
||||||
--keybinding-color: #{$oc-gray-5};
|
--keybinding-color: #{$oc-gray-5};
|
||||||
--link-color: #{$oc-blue-7};
|
--link-color: #{$oc-blue-7};
|
||||||
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
||||||
@@ -56,11 +57,12 @@
|
|||||||
--focus-highlight-color: #{$oc-blue-6};
|
--focus-highlight-color: #{$oc-blue-6};
|
||||||
--icon-fill-color: #{$oc-gray-4};
|
--icon-fill-color: #{$oc-gray-4};
|
||||||
--icon-green-fill-color: #{$oc-green-4};
|
--icon-green-fill-color: #{$oc-green-4};
|
||||||
|
--default-bg-color: #121212;
|
||||||
--input-bg-color: #121212;
|
--input-bg-color: #121212;
|
||||||
--input-border-color: #2e2e2e;
|
--input-border-color: #2e2e2e;
|
||||||
--input-hover-bg-color: #181818;
|
--input-hover-bg-color: #181818;
|
||||||
--input-label-color: #{$oc-gray-2};
|
--input-label-color: #{$oc-gray-2};
|
||||||
--island-bg-color: #1e1e1e;
|
--island-bg-color: rgba(30, 30, 30, 0.98);
|
||||||
--keybinding-color: #{$oc-gray-6};
|
--keybinding-color: #{$oc-gray-6};
|
||||||
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
||||||
--popup-bg-color: #2c2c2c;
|
--popup-bg-color: #2c2c2c;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
@import "open-color/open-color.scss";
|
@import "open-color/open-color.scss";
|
||||||
|
|
||||||
// keep up to date with is-mobile.tsx
|
@mixin isMobile() {
|
||||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
@at-root .excalidraw--mobile#{&} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
isMobileQuery: unquote($is-mobile-query);
|
|
||||||
themeFilter: unquote($theme-filter);
|
themeFilter: unquote($theme-filter);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-11
@@ -1,13 +1,14 @@
|
|||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isValidExcalidrawData } from "./json";
|
import { isValidExcalidrawData } from "./json";
|
||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { LibraryData } from "./types";
|
import { ImportedLibraryData } from "./types";
|
||||||
|
|
||||||
const parseFileContents = async (blob: Blob | File) => {
|
const parseFileContents = async (blob: Blob | File) => {
|
||||||
let contents: string;
|
let contents: string;
|
||||||
@@ -83,6 +84,7 @@ export const loadFromBlob = async (
|
|||||||
blob: Blob,
|
blob: Blob,
|
||||||
/** @see restore.localAppState */
|
/** @see restore.localAppState */
|
||||||
localAppState: AppState | null,
|
localAppState: AppState | null,
|
||||||
|
localElements: readonly ExcalidrawElement[] | null,
|
||||||
) => {
|
) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
try {
|
try {
|
||||||
@@ -95,13 +97,7 @@ export const loadFromBlob = async (
|
|||||||
elements: clearElementsForExport(data.elements || []),
|
elements: clearElementsForExport(data.elements || []),
|
||||||
appState: {
|
appState: {
|
||||||
theme: localAppState?.theme,
|
theme: localAppState?.theme,
|
||||||
fileHandle:
|
fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null,
|
||||||
blob.handle &&
|
|
||||||
["application/json", MIME_TYPES.excalidraw].includes(
|
|
||||||
getMimeType(blob),
|
|
||||||
)
|
|
||||||
? blob.handle
|
|
||||||
: null,
|
|
||||||
...cleanAppStateForExport(data.appState || {}),
|
...cleanAppStateForExport(data.appState || {}),
|
||||||
...(localAppState
|
...(localAppState
|
||||||
? calculateScrollCenter(data.elements || [], localAppState, null)
|
? calculateScrollCenter(data.elements || [], localAppState, null)
|
||||||
@@ -109,6 +105,7 @@ export const loadFromBlob = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
|
localElements,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -120,8 +117,8 @@ export const loadFromBlob = async (
|
|||||||
|
|
||||||
export const loadLibraryFromBlob = async (blob: Blob) => {
|
export const loadLibraryFromBlob = async (blob: Blob) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
const data: LibraryData = JSON.parse(contents);
|
const data: ImportedLibraryData = JSON.parse(contents);
|
||||||
if (data.type !== "excalidrawlib") {
|
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
+9
-3
@@ -2,7 +2,7 @@ import decodePng from "png-chunks-extract";
|
|||||||
import tEXt from "png-chunk-text";
|
import tEXt from "png-chunk-text";
|
||||||
import encodePng from "png-chunks-encode";
|
import encodePng from "png-chunks-encode";
|
||||||
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// PNG
|
// PNG
|
||||||
@@ -67,7 +67,10 @@ export const decodePngMetadata = async (blob: Blob) => {
|
|||||||
const encodedData = JSON.parse(metadata.text);
|
const encodedData = JSON.parse(metadata.text);
|
||||||
if (!("encoded" in encodedData)) {
|
if (!("encoded" in encodedData)) {
|
||||||
// legacy, un-encoded scene JSON
|
// legacy, un-encoded scene JSON
|
||||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
return metadata.text;
|
return metadata.text;
|
||||||
}
|
}
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
@@ -115,7 +118,10 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
|
|||||||
const encodedData = JSON.parse(json);
|
const encodedData = JSON.parse(json);
|
||||||
if (!("encoded" in encodedData)) {
|
if (!("encoded" in encodedData)) {
|
||||||
// legacy, un-encoded scene JSON
|
// legacy, un-encoded scene JSON
|
||||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
|
|||||||
+10
-28
@@ -1,8 +1,9 @@
|
|||||||
import { fileSave } from "browser-fs-access";
|
import { fileSave } from "browser-fs-access";
|
||||||
import {
|
import {
|
||||||
copyCanvasToClipboardAsPng,
|
copyBlobToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
|
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
@@ -18,42 +19,29 @@ export const exportCanvas = async (
|
|||||||
type: ExportType,
|
type: ExportType,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement,
|
|
||||||
{
|
{
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportPadding = 10,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
name,
|
name,
|
||||||
scale = 1,
|
|
||||||
shouldAddWatermark,
|
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
name: string;
|
name: string;
|
||||||
scale?: number;
|
|
||||||
shouldAddWatermark: boolean;
|
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
if (type === "svg" || type === "clipboard-svg") {
|
if (type === "svg" || type === "clipboard-svg") {
|
||||||
const tempSvg = exportToSvg(elements, {
|
const tempSvg = await exportToSvg(elements, {
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportWithDarkMode: appState.exportWithDarkMode,
|
exportWithDarkMode: appState.exportWithDarkMode,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
exportScale: appState.exportScale,
|
||||||
shouldAddWatermark,
|
exportEmbedScene: appState.exportEmbedScene && type === "svg",
|
||||||
metadata:
|
|
||||||
appState.exportEmbedScene && type === "svg"
|
|
||||||
? await (
|
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
|
||||||
).encodeSvgMetadata({
|
|
||||||
text: serializeAsJSON(elements, appState),
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
if (type === "svg") {
|
if (type === "svg") {
|
||||||
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
|
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
|
||||||
@@ -62,7 +50,7 @@ export const exportCanvas = async (
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else if (type === "clipboard-svg") {
|
} else if (type === "clipboard-svg") {
|
||||||
copyTextToSystemClipboard(tempSvg.outerHTML);
|
await copyTextToSystemClipboard(tempSvg.outerHTML);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,15 +59,14 @@ export const exportCanvas = async (
|
|||||||
exportBackground,
|
exportBackground,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
|
||||||
shouldAddWatermark,
|
|
||||||
});
|
});
|
||||||
tempCanvas.style.display = "none";
|
tempCanvas.style.display = "none";
|
||||||
document.body.appendChild(tempCanvas);
|
document.body.appendChild(tempCanvas);
|
||||||
|
let blob = await canvasToBlob(tempCanvas);
|
||||||
|
tempCanvas.remove();
|
||||||
|
|
||||||
if (type === "png") {
|
if (type === "png") {
|
||||||
const fileName = `${name}.png`;
|
const fileName = `${name}.png`;
|
||||||
let blob = await canvasToBlob(tempCanvas);
|
|
||||||
if (appState.exportEmbedScene) {
|
if (appState.exportEmbedScene) {
|
||||||
blob = await (
|
blob = await (
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
await import(/* webpackChunkName: "image" */ "./image")
|
||||||
@@ -95,7 +82,7 @@ export const exportCanvas = async (
|
|||||||
});
|
});
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
try {
|
try {
|
||||||
await copyCanvasToClipboardAsPng(tempCanvas);
|
await copyBlobToClipboardAsPng(blob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -103,9 +90,4 @@ export const exportCanvas = async (
|
|||||||
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up the DOM
|
|
||||||
if (tempCanvas !== canvas) {
|
|
||||||
tempCanvas.remove();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-33
@@ -1,28 +1,32 @@
|
|||||||
import { fileOpen, fileSave } from "browser-fs-access";
|
import { fileOpen, fileSave } from "browser-fs-access";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { loadFromBlob } from "./blob";
|
import { loadFromBlob } from "./blob";
|
||||||
import { Library } from "./library";
|
|
||||||
import { ImportedDataState } from "./types";
|
import {
|
||||||
|
ExportedDataState,
|
||||||
|
ImportedDataState,
|
||||||
|
ExportedLibraryData,
|
||||||
|
} from "./types";
|
||||||
|
import Library from "./library";
|
||||||
|
|
||||||
export const serializeAsJSON = (
|
export const serializeAsJSON = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: Partial<AppState>,
|
||||||
): string =>
|
): string => {
|
||||||
JSON.stringify(
|
const data: ExportedDataState = {
|
||||||
{
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
type: "excalidraw",
|
version: 2,
|
||||||
version: 2,
|
source: EXPORT_SOURCE,
|
||||||
source: window.location.origin,
|
elements: clearElementsForExport(elements),
|
||||||
elements: clearElementsForExport(elements),
|
appState: cleanAppStateForExport(appState),
|
||||||
appState: cleanAppStateForExport(appState),
|
};
|
||||||
},
|
|
||||||
null,
|
return JSON.stringify(data, null, 2);
|
||||||
2,
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export const saveAsJSON = async (
|
export const saveAsJSON = async (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -45,7 +49,10 @@ export const saveAsJSON = async (
|
|||||||
return { fileHandle };
|
return { fileHandle };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadFromJSON = async (localAppState: AppState) => {
|
export const loadFromJSON = async (
|
||||||
|
localAppState: AppState,
|
||||||
|
localElements: readonly ExcalidrawElement[] | null,
|
||||||
|
) => {
|
||||||
const blob = await fileOpen({
|
const blob = await fileOpen({
|
||||||
description: "Excalidraw files",
|
description: "Excalidraw files",
|
||||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||||
@@ -60,7 +67,7 @@ export const loadFromJSON = async (localAppState: AppState) => {
|
|||||||
],
|
],
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
return loadFromBlob(blob, localAppState);
|
return loadFromBlob(blob, localAppState, localElements);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidExcalidrawData = (data?: {
|
export const isValidExcalidrawData = (data?: {
|
||||||
@@ -69,7 +76,7 @@ export const isValidExcalidrawData = (data?: {
|
|||||||
appState?: any;
|
appState?: any;
|
||||||
}): data is ImportedDataState => {
|
}): data is ImportedDataState => {
|
||||||
return (
|
return (
|
||||||
data?.type === "excalidraw" &&
|
data?.type === EXPORT_DATA_TYPES.excalidraw &&
|
||||||
(!data.elements ||
|
(!data.elements ||
|
||||||
(Array.isArray(data.elements) &&
|
(Array.isArray(data.elements) &&
|
||||||
(!data.appState || typeof data.appState === "object")))
|
(!data.appState || typeof data.appState === "object")))
|
||||||
@@ -80,22 +87,20 @@ export const isValidLibrary = (json: any) => {
|
|||||||
return (
|
return (
|
||||||
typeof json === "object" &&
|
typeof json === "object" &&
|
||||||
json &&
|
json &&
|
||||||
json.type === "excalidrawlib" &&
|
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
|
||||||
json.version === 1
|
json.version === 1
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveLibraryAsJSON = async () => {
|
export const saveLibraryAsJSON = async (library: Library) => {
|
||||||
const library = await Library.loadLibrary();
|
const libraryItems = await library.loadLibrary();
|
||||||
const serialized = JSON.stringify(
|
const data: ExportedLibraryData = {
|
||||||
{
|
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||||
type: "excalidrawlib",
|
version: 1,
|
||||||
version: 1,
|
source: EXPORT_SOURCE,
|
||||||
library,
|
library: libraryItems,
|
||||||
},
|
};
|
||||||
null,
|
const serialized = JSON.stringify(data, null, 2);
|
||||||
2,
|
|
||||||
);
|
|
||||||
const fileName = "library.excalidrawlib";
|
const fileName = "library.excalidrawlib";
|
||||||
const blob = new Blob([serialized], {
|
const blob = new Blob([serialized], {
|
||||||
type: MIME_TYPES.excalidrawlib,
|
type: MIME_TYPES.excalidrawlib,
|
||||||
@@ -107,7 +112,7 @@ export const saveLibraryAsJSON = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importLibraryFromJSON = async () => {
|
export const importLibraryFromJSON = async (library: Library) => {
|
||||||
const blob = await fileOpen({
|
const blob = await fileOpen({
|
||||||
description: "Excalidraw library files",
|
description: "Excalidraw library files",
|
||||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||||
@@ -116,5 +121,5 @@ export const importLibraryFromJSON = async () => {
|
|||||||
extensions: [".json", ".excalidrawlib"],
|
extensions: [".json", ".excalidrawlib"],
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
Library.importLibrary(blob);
|
await library.importLibrary(blob);
|
||||||
};
|
};
|
||||||
|
|||||||
+45
-30
@@ -1,20 +1,29 @@
|
|||||||
import { loadLibraryFromBlob } from "./blob";
|
import { loadLibraryFromBlob } from "./blob";
|
||||||
import { LibraryItems, LibraryItem } from "../types";
|
import { LibraryItems, LibraryItem } from "../types";
|
||||||
import { restoreElements } from "./restore";
|
import { restoreElements } from "./restore";
|
||||||
import { STORAGE_KEYS } from "../constants";
|
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { NonDeleted, ExcalidrawElement } from "../element/types";
|
import type App from "../components/App";
|
||||||
|
|
||||||
export class Library {
|
class Library {
|
||||||
private static libraryCache: LibraryItems | null = null;
|
private libraryCache: LibraryItems | null = null;
|
||||||
|
private app: App;
|
||||||
|
|
||||||
static resetLibrary = () => {
|
constructor(app: App) {
|
||||||
Library.libraryCache = null;
|
this.app = app;
|
||||||
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
}
|
||||||
|
|
||||||
|
resetLibrary = async () => {
|
||||||
|
await this.app.props.onLibraryChange?.([]);
|
||||||
|
this.libraryCache = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
|
||||||
|
const elements = getNonDeletedElements(restoreElements(libraryItem, null));
|
||||||
|
return elements.length ? elements : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** imports library (currently merges, removing duplicates) */
|
/** imports library (currently merges, removing duplicates) */
|
||||||
static async importLibrary(blob: Blob) {
|
async importLibrary(blob: Blob) {
|
||||||
const libraryFile = await loadLibraryFromBlob(blob);
|
const libraryFile = await loadLibraryFromBlob(blob);
|
||||||
if (!libraryFile || !libraryFile.library) {
|
if (!libraryFile || !libraryFile.library) {
|
||||||
return;
|
return;
|
||||||
@@ -44,37 +53,41 @@ export class Library {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingLibraryItems = await Library.loadLibrary();
|
const existingLibraryItems = await this.loadLibrary();
|
||||||
|
|
||||||
const filtered = libraryFile.library!.reduce((acc, libraryItem) => {
|
const filtered = libraryFile.library!.reduce((acc, libraryItem) => {
|
||||||
const restored = getNonDeletedElements(restoreElements(libraryItem));
|
const restoredItem = this.restoreLibraryItem(libraryItem);
|
||||||
if (isUniqueitem(existingLibraryItems, restored)) {
|
if (restoredItem && isUniqueitem(existingLibraryItems, restoredItem)) {
|
||||||
acc.push(restored);
|
acc.push(restoredItem);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as (readonly NonDeleted<ExcalidrawElement>[])[]);
|
}, [] as Mutable<LibraryItems>);
|
||||||
|
|
||||||
Library.saveLibrary([...existingLibraryItems, ...filtered]);
|
await this.saveLibrary([...existingLibraryItems, ...filtered]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadLibrary = (): Promise<LibraryItems> => {
|
loadLibrary = (): Promise<LibraryItems> => {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
if (Library.libraryCache) {
|
if (this.libraryCache) {
|
||||||
return resolve(JSON.parse(JSON.stringify(Library.libraryCache)));
|
return resolve(JSON.parse(JSON.stringify(this.libraryCache)));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
const libraryItems = this.app.libraryItemsFromStorage;
|
||||||
if (!data) {
|
if (!libraryItems) {
|
||||||
return resolve([]);
|
return resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = (JSON.parse(data) as LibraryItems).map((elements) =>
|
const items = libraryItems.reduce((acc, item) => {
|
||||||
restoreElements(elements),
|
const restoredItem = this.restoreLibraryItem(item);
|
||||||
) as Mutable<LibraryItems>;
|
if (restoredItem) {
|
||||||
|
acc.push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as Mutable<LibraryItems>);
|
||||||
|
|
||||||
// clone to ensure we don't mutate the cached library elements in the app
|
// clone to ensure we don't mutate the cached library elements in the app
|
||||||
Library.libraryCache = JSON.parse(JSON.stringify(items));
|
this.libraryCache = JSON.parse(JSON.stringify(items));
|
||||||
|
|
||||||
resolve(items);
|
resolve(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -84,17 +97,19 @@ export class Library {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static saveLibrary = (items: LibraryItems) => {
|
saveLibrary = async (items: LibraryItems) => {
|
||||||
const prevLibraryItems = Library.libraryCache;
|
const prevLibraryItems = this.libraryCache;
|
||||||
try {
|
try {
|
||||||
const serializedItems = JSON.stringify(items);
|
const serializedItems = JSON.stringify(items);
|
||||||
// cache optimistically so that consumers have access to the latest
|
// cache optimistically so that the app has access to the latest
|
||||||
// immediately
|
// immediately
|
||||||
Library.libraryCache = JSON.parse(serializedItems);
|
this.libraryCache = JSON.parse(serializedItems);
|
||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
await this.app.props.onLibraryChange?.(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Library.libraryCache = prevLibraryItems;
|
this.libraryCache = prevLibraryItems;
|
||||||
console.error(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Library;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user