Merge
This commit is contained in:
commit
3c86b014de
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
4
.github/workflows/build-packages.yml
vendored
4
.github/workflows/build-packages.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
8
.github/workflows/cancel.yml
vendored
8
.github/workflows/cancel.yml
vendored
@ -1,9 +1,11 @@
|
||||
name: Cancel
|
||||
on: [push]
|
||||
name: Cancel previous runs
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
name: "Cancel Previous Runs"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: styfle/cancel-workflow-action@0.6.0
|
||||
|
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@ -1,10 +1,6 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@ -13,10 +9,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and lint
|
||||
run: |
|
||||
@ -24,5 +20,3 @@ jobs:
|
||||
npm run test:other
|
||||
npm run test:code
|
||||
npm run test:typecheck
|
||||
env:
|
||||
CI: true
|
||||
|
10
.github/workflows/locales-coverage.yml
vendored
10
.github/workflows/locales-coverage.yml
vendored
@ -14,18 +14,18 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Create report file
|
||||
run: |
|
||||
npm run locales-coverage
|
||||
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
||||
if [ ! -z "${FILE_CHANGED}" ]; then
|
||||
git config --global user.name 'Kostas Bariotis'
|
||||
git config --global user.email 'konmpar@gmail.com'
|
||||
git config --global user.name 'Excalidraw Bot'
|
||||
git config --global user.email 'bot@excalidraw.com'
|
||||
git add src/locales/percentages.json
|
||||
git commit -am "Auto commit: Calculate translation coverage"
|
||||
git push
|
||||
@ -43,5 +43,5 @@ jobs:
|
||||
uses: kt3k/update-pr-description@v1.0.1
|
||||
with:
|
||||
pr_body: ${{ steps.getCommentBody.outputs.body }}
|
||||
pr_title: "chore: New Crowdin updates"
|
||||
pr_title: "chore: Update translations from Crowdin"
|
||||
github_token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
1
.github/workflows/semantic-pr-title.yml
vendored
1
.github/workflows/semantic-pr-title.yml
vendored
@ -10,6 +10,7 @@ on:
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
||||
env:
|
||||
|
5
.github/workflows/sentry-production.yml
vendored
5
.github/workflows/sentry-production.yml
vendored
@ -8,13 +8,14 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1.0.0
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and build
|
||||
run: |
|
||||
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@ -1,10 +1,6 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@ -13,14 +9,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and test
|
||||
run: |
|
||||
npm ci
|
||||
npm run test:app
|
||||
env:
|
||||
CI: true
|
||||
|
89
README.md
89
README.md
@ -1,8 +1,8 @@
|
||||
<div align="center" style="display:flex;flex-direction:column;">
|
||||
<a href="https://excalidraw.com">
|
||||
<img src="./public/og-image.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
||||
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
||||
</a>
|
||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.</h3>
|
||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br>Collaborative and end to end encrypted.</h3>
|
||||
<p>
|
||||
<a href="https://twitter.com/Excalidraw">
|
||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+excalidraw&style=social&logo=twitter">
|
||||
@ -10,9 +10,6 @@
|
||||
<a target="_blank" href="https://crowdin.com/project/excalidraw">
|
||||
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
|
||||
</a>
|
||||
<a target="_blank" href="https://hub.docker.com/r/excalidraw/excalidraw">
|
||||
<img src="https://img.shields.io/docker/pulls/excalidraw/excalidraw">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -20,13 +17,51 @@
|
||||
|
||||
Go to [excalidraw.com](https://excalidraw.com) to start sketching.
|
||||
|
||||
Read our [blog](https://blog.excalidraw.com) and follow the [guides](https://howto.excalidraw.com) to learn more about Excalidraw and how to use it effectively.
|
||||
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
|
||||
|
||||
## Documentation
|
||||
|
||||
### Shortcuts
|
||||
|
||||
You can almost do anything with shortcuts. Click on the help icon on the bottom right corner to see them all.
|
||||
|
||||
### Curved lines and arrows
|
||||
|
||||
Choose line or arrow and click click click instead of drag.
|
||||
|
||||
### Charts
|
||||
|
||||
You can easily create charts by copy pasting data from Excel or just plain comma separated text.
|
||||
|
||||
### Translating
|
||||
|
||||
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
|
||||
|
||||
Translations will be available on the app if they exceed a certain threshold of completion (currently 85%).
|
||||
|
||||
### Create a collaboration session manually
|
||||
|
||||
In order to create a session manually you just need to generate a link of this form:
|
||||
|
||||
```
|
||||
https://excalidraw.com/#room=[0-9a-f]{20},[a-zA-Z0-9_-]{22}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```
|
||||
https://excalidraw.com/#room=91bd46ae3aa84dff9d20,pfLqgEoY1c2ioq8LmGwsFA
|
||||
```
|
||||
|
||||
The first set of digits is the room. This is visible from the server that’s going to dispatch messages to everyone that knows this number.
|
||||
|
||||
The second set of digits is the encryption key. The Excalidraw server doesn’t know about it. This is what all the participants use to encrypt/decrypt the messages.
|
||||
|
||||
## Shape libraries
|
||||
|
||||
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
||||
|
||||
## Run the code
|
||||
## Developement
|
||||
|
||||
### Code Sandbox
|
||||
|
||||
@ -63,7 +98,7 @@ You can use docker-compose to work on excalidraw locally if you don't want to se
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
## Self hosting
|
||||
### Self hosting
|
||||
|
||||
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self host your own client under your own domain, on Kubernetes, AWS ECS, etc.
|
||||
|
||||
@ -82,45 +117,11 @@ We are working towards providing a full-fledged solution for self hosting your o
|
||||
|
||||
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
||||
|
||||
## Translating
|
||||
## Notable used tools
|
||||
|
||||
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
|
||||
|
||||
Translations will be available on the app if they exceed a certain threshold of completion (currently 85%).
|
||||
|
||||
## Excalidraw is built using these awesome tools
|
||||
|
||||
- [React](https://reactjs.org)
|
||||
- [Create React App](https://github.com/facebook/create-react-app)
|
||||
- [Rough.js](https://roughjs.com)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [Vercel](https://vercel.com)
|
||||
|
||||
And the main source of inspiration for starting the project is the awesome [Zwibbler](https://zwibbler.com/demo/) app.
|
||||
|
||||
## Testimonials
|
||||
|
||||
<a href="https://twitter.com/Lissy_Sykes/status/1213813117177729026"><img width="398" src="https://user-images.githubusercontent.com/197597/71783813-dbf8a600-2fa0-11ea-9c0d-bb3cc45969e6.png"></a> <a href="https://twitter.com/dan_abramov/status/1213762494428262400"><img width="398" src="https://user-images.githubusercontent.com/197597/71783990-4d395880-2fa3-11ea-9ad7-186138db5003.png"></a>
|
||||
|
||||
<a href="https://twitter.com/kyehohenberger/status/1214288572037025792"><img width="423" src="https://user-images.githubusercontent.com/197597/71851802-34f13880-308c-11ea-9416-191099e6349c.png"></a> <a href="https://twitter.com/lucasazzola/status/1215126440330416128"><img width="429" src="https://user-images.githubusercontent.com/197597/72039003-48e99580-3258-11ea-8daa-85dd055f2a82.png">
|
||||
|
||||
<a href="https://twitter.com/jordwalke/status/1214858186789806080"><img width="434" src="https://user-images.githubusercontent.com/197597/72036874-07a1b780-3251-11ea-99e8-6bafd93483a0.png"></a>
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. <a href="https://github.com/excalidraw/excalidraw/graphs/contributors"><img src="https://opencollective.com/excalidraw/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/excalidraw/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/excalidraw"><img src="https://opencollective.com/excalidraw/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/excalidraw/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/excalidraw/organization/0/website"><img src="https://opencollective.com/excalidraw/organization/0/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/1/website"><img src="https://opencollective.com/excalidraw/organization/1/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/2/website"><img src="https://opencollective.com/excalidraw/organization/2/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/3/website"><img src="https://opencollective.com/excalidraw/organization/3/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/4/website"><img src="https://opencollective.com/excalidraw/organization/4/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/5/website"><img src="https://opencollective.com/excalidraw/organization/5/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/6/website"><img src="https://opencollective.com/excalidraw/organization/6/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/7/website"><img src="https://opencollective.com/excalidraw/organization/7/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/8/website"><img src="https://opencollective.com/excalidraw/organization/8/avatar.svg"></a> <a href="https://opencollective.com/excalidraw/organization/9/website"><img src="https://opencollective.com/excalidraw/organization/9/avatar.svg"></a>
|
||||
|
280
package-lock.json
generated
280
package-lock.json
generated
@ -4,9 +4,9 @@
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz",
|
||||
"integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==",
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz",
|
||||
"integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
@ -1308,9 +1308,9 @@
|
||||
"integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA=="
|
||||
},
|
||||
"@firebase/app": {
|
||||
"version": "0.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.13.tgz",
|
||||
"integrity": "sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ==",
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.14.tgz",
|
||||
"integrity": "sha512-ZQKuiJ+fzr4tULgWoXbW+AZVTGsejOkSrlQ+zx78WiGKIubpFJLklnP3S0oYr/1nHzr4vaKuM4G8IL1Wv/+MpQ==",
|
||||
"requires": {
|
||||
"@firebase/app-types": "0.6.1",
|
||||
"@firebase/component": "0.1.21",
|
||||
@ -1334,9 +1334,9 @@
|
||||
"integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg=="
|
||||
},
|
||||
"@firebase/auth": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.1.tgz",
|
||||
"integrity": "sha512-7juD7D/kaxNti/xa5G+ZGJJs+bdJUWOW0MlNBtXwiG+TjMh69EDmwJnQmmc9h/32QVvXt1qo1OGWOoMMpF/2Gg==",
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.2.tgz",
|
||||
"integrity": "sha512-68TlDL0yh3kF8PiCzI8m8RWd/bf/xCLUsdz1NZ2Dwea0sp6e2WAhu0sem1GfhwuEwL+Ns4jCdX7qbe/OQlkVEA==",
|
||||
"requires": {
|
||||
"@firebase/auth-types": "0.10.1"
|
||||
}
|
||||
@ -1368,9 +1368,9 @@
|
||||
}
|
||||
},
|
||||
"@firebase/database": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.2.tgz",
|
||||
"integrity": "sha512-E86yrom0Ii+61UScG44y1q3H3NuozzGGTGbYmiyTe1qK8Qvzuiu7yyfdDnqFW2fkeKvTRLoDeCpgZy27FgEndQ==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.9.1.tgz",
|
||||
"integrity": "sha512-JdxgNvniSZiAx+lrdAQxkCZOTv+UfdmhRm9JA4RTs4XOpvwzmRtJTAIGBn+9CWXUAkWkjt5CYHLmYysD7NGj6g==",
|
||||
"requires": {
|
||||
"@firebase/auth-interop-types": "0.1.5",
|
||||
"@firebase/component": "0.1.21",
|
||||
@ -1405,9 +1405,9 @@
|
||||
}
|
||||
},
|
||||
"@firebase/firestore": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.2.tgz",
|
||||
"integrity": "sha512-8yUdBLLr6UhE+IjPR+fxLBD0bDnEqF9GalohfURZeLQPaL3b+LtqqGCLvvXC4MKT0lJAHOV8J9LA6rHj8vI0/Q==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.4.tgz",
|
||||
"integrity": "sha512-chSOvJyVoS7HmH7YOyqQP66wMwmsYNo2nPbFkrmQM/fRGXntNxXD1Greu1uts2hNyNeDLNrFHW5y7PlE3LAbwQ==",
|
||||
"requires": {
|
||||
"@firebase/component": "0.1.21",
|
||||
"@firebase/firestore-types": "2.1.0",
|
||||
@ -1649,17 +1649,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"@google-cloud/pubsub": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-2.7.0.tgz",
|
||||
"integrity": "sha512-wc/XOo5Ibo3GWmuaLu80EBIhXSdu2vf99HUqBbdsSSkmRNIka2HqoIhLlOFnnncQn0lZnGL7wtKGIDLoH9LiBg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-2.8.0.tgz",
|
||||
"integrity": "sha512-AoSKAbpHCoLq6jO9vMX+K6hJhkayafan24Rs2RKHU8Y0qF6IGSm1+ly0OG12TgziHWg818/6dljWWKgwDcp8KA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@google-cloud/paginator": "^3.0.0",
|
||||
"@google-cloud/precise-date": "^2.0.0",
|
||||
"@google-cloud/projectify": "^2.0.0",
|
||||
"@google-cloud/promisify": "^2.0.0",
|
||||
"@opentelemetry/api": "^0.11.0",
|
||||
"@opentelemetry/tracing": "^0.11.0",
|
||||
"@opentelemetry/api": "^0.12.0",
|
||||
"@opentelemetry/tracing": "^0.12.0",
|
||||
"@types/duplexify": "^3.6.0",
|
||||
"@types/long": "^4.0.0",
|
||||
"arrify": "^2.0.0",
|
||||
@ -2460,28 +2460,28 @@
|
||||
}
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.11.0.tgz",
|
||||
"integrity": "sha512-K+1ADLMxduhsXoZ0GRfi9Pw162FvzBQLDQlHru1lg86rpIU+4XqdJkSGo6y3Kg+GmOWq1HNHOA/ydw/rzHQkRg==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.12.0.tgz",
|
||||
"integrity": "sha512-Dn4vU5GlaBrIWzLpsM6xbJwKHdlpwBQ4Bd+cL9ofJP3hKT8jBXpBpribmyaqAzrajzzl2Yt8uTa9rFVLfjDAvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@opentelemetry/context-base": "^0.11.0"
|
||||
"@opentelemetry/context-base": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"@opentelemetry/context-base": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.11.0.tgz",
|
||||
"integrity": "sha512-ESRk+572bftles7CVlugAj5Azrz61VO0MO0TS2pE9MLVL/zGmWuUBQryART6/nsrFqo+v9HPt37GPNcECTZR1w==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.12.0.tgz",
|
||||
"integrity": "sha512-UXwSsXo3F3yZ1dIBOG9ID8v2r9e+bqLWoizCtTb8rXtwF+N5TM7hzzvQz72o3nBU+zrI/D5e+OqAYK8ZgDd3DA==",
|
||||
"dev": true
|
||||
},
|
||||
"@opentelemetry/core": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-0.11.0.tgz",
|
||||
"integrity": "sha512-ZEKjBXeDGBqzouz0uJmrbEKNExEsQOhsZ3tJDCLcz5dUNoVw642oIn2LYWdQK2YdIfZbEmltiF65/csGsaBtFA==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-0.12.0.tgz",
|
||||
"integrity": "sha512-oLZIkmTNWTJXzo1eA4dGu/S7wOVtylsgnEsCmhSJGhrJVDXm1eW/aGuNs3DVBeuxp0ZvQLAul3/PThsC3YrnzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@opentelemetry/api": "^0.11.0",
|
||||
"@opentelemetry/context-base": "^0.11.0",
|
||||
"@opentelemetry/api": "^0.12.0",
|
||||
"@opentelemetry/context-base": "^0.12.0",
|
||||
"semver": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -2506,32 +2506,32 @@
|
||||
}
|
||||
},
|
||||
"@opentelemetry/resources": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-0.11.0.tgz",
|
||||
"integrity": "sha512-o7DwV1TcezqBtS5YW2AWBcn01nVpPptIbTr966PLlVBcS//w8LkjeOShiSZxQ0lmV4b2en0FiSouSDoXk/5qIQ==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-0.12.0.tgz",
|
||||
"integrity": "sha512-8cYvIKB68cyupc7D6SWzkLtt13mbjgxMahL4JKCM6hWPyiGSJlPFEAey4XFXI5LLpPZRYTPHLVoLqI/xwCFZZA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@opentelemetry/api": "^0.11.0",
|
||||
"@opentelemetry/core": "^0.11.0"
|
||||
"@opentelemetry/api": "^0.12.0",
|
||||
"@opentelemetry/core": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"@opentelemetry/semantic-conventions": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-0.11.0.tgz",
|
||||
"integrity": "sha512-xsthnI/J+Cx0YVDGgUzvrH0ZTtfNtl866M454NarYwDrc0JvC24sYw+XS5PJyk2KDzAHtb0vlrumUc1OAut/Fw==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-0.12.0.tgz",
|
||||
"integrity": "sha512-BuCcDW0uLNYYTns0/LwXkJ8lp8aDm7kpS+WunEmPAPRSCe6ciOYRvzn5reqJfX93rf+6A3U2SgrBnCTH+0qoQQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@opentelemetry/tracing": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/tracing/-/tracing-0.11.0.tgz",
|
||||
"integrity": "sha512-QweFmxzl32BcyzwdWCNjVXZT1WeENNS/RWETq/ohqu+fAsTcMyGcr6cOq/yDdFmtBy+bm5WVVdeByEjNS+c4/w==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/tracing/-/tracing-0.12.0.tgz",
|
||||
"integrity": "sha512-2TUGhTGkhgnxTciHCNAILPSeyXageJewRqfP9wOrx65sKd/jgvNYoY8nYf4EVWVMirDOxKDsmYgUkjdQrwb2dg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@opentelemetry/api": "^0.11.0",
|
||||
"@opentelemetry/context-base": "^0.11.0",
|
||||
"@opentelemetry/core": "^0.11.0",
|
||||
"@opentelemetry/resources": "^0.11.0",
|
||||
"@opentelemetry/semantic-conventions": "^0.11.0"
|
||||
"@opentelemetry/api": "^0.12.0",
|
||||
"@opentelemetry/context-base": "^0.12.0",
|
||||
"@opentelemetry/core": "^0.12.0",
|
||||
"@opentelemetry/resources": "^0.12.0",
|
||||
"@opentelemetry/semantic-conventions": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"@pmmmwh/react-refresh-webpack-plugin": {
|
||||
@ -2663,70 +2663,86 @@
|
||||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.2.tgz",
|
||||
"integrity": "sha512-uxZ7y7rp85tJll+RZtXRhXPbnFnOaxZqJEv05vJlXBtBNLQtlczV5iCtU9mZRLVHDtmZ5VVKUV8IKXntEqqDpQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.3.tgz",
|
||||
"integrity": "sha512-Ukxh83Twql4UmUgds9wPWllE62NG71cYvm5AM6daTojvM8wFR2jh7G6GiA0WYfgMb2fw6SlbevB2xb6RDG5DzQ==",
|
||||
"requires": {
|
||||
"@sentry/core": "5.29.2",
|
||||
"@sentry/types": "5.29.2",
|
||||
"@sentry/utils": "5.29.2",
|
||||
"@sentry/core": "6.0.3",
|
||||
"@sentry/types": "6.0.3",
|
||||
"@sentry/utils": "6.0.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.2.tgz",
|
||||
"integrity": "sha512-7WYkoxB5IdlNEbwOwqSU64erUKH4laavPsM0/yQ+jojM76ErxlgEF0u//p5WaLPRzh3iDSt6BH+9TL45oNZeZw==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.3.tgz",
|
||||
"integrity": "sha512-UykB/4/98y2DkNvwTiL2ofFPuK3KDHc7rIRNsdj6dg6D+Cf7FRexgmWUUkZrpC/y+QBj0TPqkcFDcZAuQDa3Ag==",
|
||||
"requires": {
|
||||
"@sentry/hub": "5.29.2",
|
||||
"@sentry/minimal": "5.29.2",
|
||||
"@sentry/types": "5.29.2",
|
||||
"@sentry/utils": "5.29.2",
|
||||
"@sentry/hub": "6.0.3",
|
||||
"@sentry/minimal": "6.0.3",
|
||||
"@sentry/types": "6.0.3",
|
||||
"@sentry/utils": "6.0.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.2.tgz",
|
||||
"integrity": "sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.3.tgz",
|
||||
"integrity": "sha512-BfV32tE09rjTWM9W0kk8gzxUC2k1h57Z5dNWJ35na79+LguNNtCcI6fHlFQ3PkJca6ITYof9FI8iQHUfsHFZnw==",
|
||||
"requires": {
|
||||
"@sentry/types": "5.29.2",
|
||||
"@sentry/utils": "5.29.2",
|
||||
"@sentry/types": "6.0.3",
|
||||
"@sentry/utils": "6.0.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/integrations": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.29.2.tgz",
|
||||
"integrity": "sha512-bH50B0xubbHrJFq8xZRxOc5BgXe1PXKfC0OqQkhhSd+Bu2WDLCHcn0CEzV+8thZTYkipAoFAFJNdEWcsM2Wcew==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.0.3.tgz",
|
||||
"integrity": "sha512-SE/rQ+ttfoC6FlHDibB4e9lV95j78YkjQ6PvYNUe+zGkGIretCJREqgaS+W3qTNYvOdbUViuiiqtdfyvW9nM2g==",
|
||||
"requires": {
|
||||
"@sentry/types": "5.29.2",
|
||||
"@sentry/utils": "5.29.2",
|
||||
"localforage": "1.8.1",
|
||||
"@sentry/types": "6.0.3",
|
||||
"@sentry/utils": "6.0.3",
|
||||
"localforage": "^1.8.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/types": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.3.tgz",
|
||||
"integrity": "sha512-266aBQbk9AGedhG2dzXshWbn23LYLElXqlI74DLku48UrU2v7TGKdyik/8/nfOfquCoRSp0GFGYHbItwU124XQ=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.3.tgz",
|
||||
"integrity": "sha512-lvuBFvZHYs1zYwI8dkC8Z8ryb0aYnwPFUl1rbZiMwJpYI2Dgl1jpqqZWv9luux2rSRYOMid74uGedV708rvEgA==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.0.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.2.tgz",
|
||||
"integrity": "sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.3.tgz",
|
||||
"integrity": "sha512-YsW+nw0SMyyb7UQdjZeKlZjxbGsJFpXNLh9iIp6fHKnoLTTv17YPm2ej9sOikDsQuVotaPg/xn/Qt5wySGHIxw==",
|
||||
"requires": {
|
||||
"@sentry/hub": "5.29.2",
|
||||
"@sentry/types": "5.29.2",
|
||||
"@sentry/hub": "6.0.3",
|
||||
"@sentry/types": "6.0.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.2.tgz",
|
||||
"integrity": "sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA=="
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.3.tgz",
|
||||
"integrity": "sha512-266aBQbk9AGedhG2dzXshWbn23LYLElXqlI74DLku48UrU2v7TGKdyik/8/nfOfquCoRSp0GFGYHbItwU124XQ=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.2.tgz",
|
||||
"integrity": "sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.3.tgz",
|
||||
"integrity": "sha512-lvuBFvZHYs1zYwI8dkC8Z8ryb0aYnwPFUl1rbZiMwJpYI2Dgl1jpqqZWv9luux2rSRYOMid74uGedV708rvEgA==",
|
||||
"requires": {
|
||||
"@sentry/types": "5.29.2",
|
||||
"@sentry/types": "6.0.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
@ -2992,9 +3008,9 @@
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.8.tgz",
|
||||
"integrity": "sha512-ScyKrWQM5xNcr79PkSewnA79CLaoxVskE+f7knTOhDD9ftZSA1Jw8mj+pneqhEu3x37ncNfW84NUr7lqK+mXjA==",
|
||||
"version": "5.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz",
|
||||
"integrity": "sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
@ -3298,14 +3314,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
|
||||
},
|
||||
"@types/nanoid": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/nanoid/-/nanoid-2.1.0.tgz",
|
||||
"integrity": "sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.5.1.tgz",
|
||||
@ -3368,9 +3376,9 @@
|
||||
}
|
||||
},
|
||||
"@types/socket.io-client": {
|
||||
"version": "1.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.34.tgz",
|
||||
"integrity": "sha512-Lzia5OTQFJZJ5R4HsEEldywiiqT9+W2rDbyHJiiTGqOcju89sCsQ8aUXDljY6Ls33wKZZGC0bfMhr/VpOyjtXg=="
|
||||
"version": "1.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.35.tgz",
|
||||
"integrity": "sha512-MI8YmxFS+jMkIziycT5ickBWK1sZwDwy16mgH/j99Mcom6zRG/NimNGQ3vJV0uX5G6g/hEw0FG3w3b3sT5OUGw=="
|
||||
},
|
||||
"@types/source-list-map": {
|
||||
"version": "0.1.2",
|
||||
@ -5108,10 +5116,10 @@
|
||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
|
||||
},
|
||||
"browser-nativefs": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.12.0.tgz",
|
||||
"integrity": "sha512-ZCHJcQI6bBm9YjB+6wMT1nWg+/mnWnz7r3gJ8sx7RjgLtWROFq+BuD12cAncD6y45MIbUqFM8eMKXoHXOxSFxA=="
|
||||
"browser-fs-access": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.13.0.tgz",
|
||||
"integrity": "sha512-qP8zFVhRQThxYgBXdlFHbzIrWb1us0G5kL2ZL0vW4BO5llKE4qBAcQsQrw4KN+6vjw8sKeWaGWJtzijfRT4N0Q=="
|
||||
},
|
||||
"browser-process-hrtime": {
|
||||
"version": "1.0.0",
|
||||
@ -7940,9 +7948,9 @@
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz",
|
||||
"integrity": "sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz",
|
||||
"integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-config-react-app": {
|
||||
@ -8505,9 +8513,9 @@
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==",
|
||||
"version": "6.9.6",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
|
||||
"integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
@ -9020,16 +9028,16 @@
|
||||
}
|
||||
},
|
||||
"firebase": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.2.tgz",
|
||||
"integrity": "sha512-a07aW2TTAA9S7p4mx5pu8hvtVokJEjAQlAocHKOWwmRJRIduE9Vvr/3i50FtujT5gGNr0Qm+EyWyB+/7TJiwnw==",
|
||||
"version": "8.2.5",
|
||||
"resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.5.tgz",
|
||||
"integrity": "sha512-x9KUJR8PvqLUNzNKWHjAnO7rJVgK546G0F+vjlJTNl+J/8oFTdWh8X4PvYda0z0XM68A2Y9xPGf3blz5qHCn0A==",
|
||||
"requires": {
|
||||
"@firebase/analytics": "0.6.2",
|
||||
"@firebase/app": "0.6.13",
|
||||
"@firebase/app": "0.6.14",
|
||||
"@firebase/app-types": "0.6.1",
|
||||
"@firebase/auth": "0.16.1",
|
||||
"@firebase/database": "0.8.2",
|
||||
"@firebase/firestore": "2.1.2",
|
||||
"@firebase/auth": "0.16.2",
|
||||
"@firebase/database": "0.9.1",
|
||||
"@firebase/firestore": "2.1.4",
|
||||
"@firebase/functions": "0.6.1",
|
||||
"@firebase/installations": "0.4.19",
|
||||
"@firebase/messaging": "0.7.3",
|
||||
@ -9041,9 +9049,9 @@
|
||||
}
|
||||
},
|
||||
"firebase-tools": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.1.2.tgz",
|
||||
"integrity": "sha512-YUiqMuQ+nbdCNpahSO0eyKxxVfT0nDdijkUEUplTGArkDwqdOKPIxVqHj1edq7GEPXTRWlk7zibnbOnCCHaedw==",
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.2.tgz",
|
||||
"integrity": "sha512-AFjf7S9NjEM+u8ZByJEKASxRG1g+LLg/A0CrzA3V91P92MN+8cyrCigEs7mCdtFknLaShrCgzROyo/OEwd4xdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@google-cloud/pubsub": "^2.7.0",
|
||||
@ -10146,9 +10154,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "13.13.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.39.tgz",
|
||||
"integrity": "sha512-wct+WgRTTkBm2R3vbrFOqyZM5w0g+D8KnhstG9463CJBVC3UVZHMToge7iMBR1vDl/I+NWFHUeK9X+JcF0rWKw==",
|
||||
"version": "13.13.40",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.40.tgz",
|
||||
"integrity": "sha512-eKaRo87lu1yAXrzEJl0zcJxfUMDT5/mZalFyOkT44rnQps41eS2pfWzbaulSPpQLFNy29bFqn+Y5lOTL8ATlEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"duplexify": {
|
||||
@ -10784,9 +10792,9 @@
|
||||
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="
|
||||
},
|
||||
"husky": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.7.tgz",
|
||||
"integrity": "sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw==",
|
||||
"version": "4.3.8",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz",
|
||||
"integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.0.0",
|
||||
@ -11515,9 +11523,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ip-regex": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz",
|
||||
"integrity": "sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
|
||||
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -14254,9 +14262,9 @@
|
||||
}
|
||||
},
|
||||
"localforage": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.8.1.tgz",
|
||||
"integrity": "sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
|
||||
"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
|
||||
"requires": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
@ -15263,9 +15271,9 @@
|
||||
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
|
||||
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
|
||||
"version": "3.1.20",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
|
||||
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@ -17653,9 +17661,9 @@
|
||||
}
|
||||
},
|
||||
"proxy-agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-4.0.0.tgz",
|
||||
"integrity": "sha512-8P0Y2SkwvKjiGU1IkEfYuTteioMIDFxPL4/j49zzt5Mz3pG1KO+mIrDG1qH0PQUHTTczjwGcYl+EzfXiFj5vUQ==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-4.0.1.tgz",
|
||||
"integrity": "sha512-ODnQnW2jc/FUVwHHuaZEfN5otg/fMbvMxz9nMSUQfJ9JU7q2SZvSULSsjLloVgJOiv9yhc8GlNMKc4GkFmcVEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"agent-base": "^6.0.0",
|
||||
|
25
package.json
25
package.json
@ -19,21 +19,20 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "5.29.2",
|
||||
"@sentry/integrations": "5.29.2",
|
||||
"@testing-library/jest-dom": "5.11.8",
|
||||
"@sentry/browser": "6.0.3",
|
||||
"@sentry/integrations": "6.0.3",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/nanoid": "2.1.0",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/socket.io-client": "1.4.34",
|
||||
"browser-nativefs": "0.12.0",
|
||||
"@types/socket.io-client": "1.4.35",
|
||||
"browser-fs-access": "0.13.0",
|
||||
"clsx": "1.1.1",
|
||||
"firebase": "8.2.2",
|
||||
"firebase": "8.2.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "2.1.11",
|
||||
"nanoid": "3.1.20",
|
||||
"node-sass": "4.14.1",
|
||||
"open-color": "1.8.0",
|
||||
"pako": "1.0.11",
|
||||
@ -52,10 +51,10 @@
|
||||
"devDependencies": {
|
||||
"@types/lodash.throttle": "4.1.6",
|
||||
"@types/pako": "1.0.1",
|
||||
"eslint-config-prettier": "7.1.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"firebase-tools": "9.1.2",
|
||||
"husky": "4.3.7",
|
||||
"firebase-tools": "9.2.2",
|
||||
"husky": "4.3.8",
|
||||
"jest-canvas-mock": "2.3.0",
|
||||
"lint-staged": "10.5.3",
|
||||
"pepjs": "0.5.3",
|
||||
@ -73,7 +72,7 @@
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-nativefs)/)"
|
||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
||||
],
|
||||
"resetMocks": false
|
||||
},
|
||||
@ -82,7 +81,7 @@
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||
"build:app": "REACT_APP_GIT_SHA=$NOW_GITHUB_COMMIT_SHA react-scripts build",
|
||||
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||
"build:version": "node ./scripts/build-version.js",
|
||||
"build": "npm run build:app && npm run build:version",
|
||||
"eject": "react-scripts eject",
|
||||
|
BIN
public/og-image-sm.png
Normal file
BIN
public/og-image-sm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
@ -18,6 +18,7 @@ const crowdinMap = {
|
||||
"id-ID": "en-id",
|
||||
"it-IT": "en-it",
|
||||
"ja-JP": "en-ja",
|
||||
"kab-KAB": "en-kab",
|
||||
"ko-KR": "en-ko",
|
||||
"my-MM": "en-my",
|
||||
"nb-NO": "en-nb",
|
||||
@ -40,7 +41,7 @@ const crowdinMap = {
|
||||
const flags = {
|
||||
"ar-SA": "🇸🇦",
|
||||
"bg-BG": "🇧🇬",
|
||||
"ca-ES": "🇪🇸",
|
||||
"ca-ES": "🏳",
|
||||
"de-DE": "🇩🇪",
|
||||
"el-GR": "🇬🇷",
|
||||
"es-ES": "🇪🇸",
|
||||
@ -53,6 +54,7 @@ const flags = {
|
||||
"id-ID": "🇮🇩",
|
||||
"it-IT": "🇮🇹",
|
||||
"ja-JP": "🇯🇵",
|
||||
"kab-KAB": "🏳",
|
||||
"ko-KR": "🇰🇷",
|
||||
"my-MM": "🇲🇲",
|
||||
"nb-NO": "🇳🇴",
|
||||
@ -88,6 +90,7 @@ const languages = {
|
||||
"id-ID": "Bahasa Indonesia",
|
||||
"it-IT": "Italiano",
|
||||
"ja-JP": "日本語",
|
||||
"kab-KAB": "Taqbaylit",
|
||||
"ko-KR": "한국어",
|
||||
"my-MM": "Burmese",
|
||||
"nb-NO": "Norsk bokmål",
|
||||
|
@ -17,6 +17,5 @@ export const actionAddToLibrary = register({
|
||||
});
|
||||
return false;
|
||||
},
|
||||
contextMenuOrder: 6,
|
||||
contextItemLabel: "labels.addToLibrary",
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { getDefaultAppState } from "../appState";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { GRID_SIZE } from "../constants";
|
||||
import { GRID_SIZE, ZOOM_STEP } from "../constants";
|
||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
@ -76,8 +76,6 @@ export const actionClearCanvas = register({
|
||||
),
|
||||
});
|
||||
|
||||
const ZOOM_STEP = 0.1;
|
||||
|
||||
export const actionZoomIn = register({
|
||||
name: "zoomIn",
|
||||
perform: (_elements, appState) => {
|
||||
|
114
src/actions/actionClipboard.tsx
Normal file
114
src/actions/actionClipboard.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { copyToClipboard } from "../clipboard";
|
||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||
import { getSelectedElements } from "../scene/selection";
|
||||
import { exportCanvas } from "../data/index";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { t } from "../i18n";
|
||||
|
||||
export const actionCopy = register({
|
||||
name: "copy",
|
||||
perform: (elements, appState) => {
|
||||
copyToClipboard(getNonDeletedElements(elements), appState);
|
||||
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.copy",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.C,
|
||||
});
|
||||
|
||||
export const actionCut = register({
|
||||
name: "cut",
|
||||
perform: (elements, appState, data, app) => {
|
||||
actionCopy.perform(elements, appState, data, app);
|
||||
return actionDeleteSelected.perform(elements, appState, data, app);
|
||||
},
|
||||
contextItemLabel: "labels.cut",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X,
|
||||
});
|
||||
|
||||
export const actionCopyAsSvg = register({
|
||||
name: "copyAsSvg",
|
||||
perform: async (elements, appState, _data, app) => {
|
||||
if (!app.canvas) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard-svg",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: getNonDeletedElements(elements),
|
||||
appState,
|
||||
app.canvas,
|
||||
appState,
|
||||
);
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
contextItemLabel: "labels.copyAsSvg",
|
||||
});
|
||||
|
||||
export const actionCopyAsPng = register({
|
||||
name: "copyAsPng",
|
||||
perform: async (elements, appState, _data, app) => {
|
||||
if (!app.canvas) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: getNonDeletedElements(elements),
|
||||
appState,
|
||||
app.canvas,
|
||||
appState,
|
||||
);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyToClipboardAsPng"),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
contextItemLabel: "labels.copyAsPng",
|
||||
keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey,
|
||||
});
|
@ -136,7 +136,6 @@ export const actionDeleteSelected = register({
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.delete",
|
||||
contextMenuOrder: 999999,
|
||||
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
|
@ -125,7 +125,6 @@ export const actionGroup = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
contextMenuOrder: 4,
|
||||
contextItemLabel: "labels.group",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
enableActionGroup(elements, appState),
|
||||
@ -174,7 +173,6 @@ export const actionUngroup = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
||||
contextMenuOrder: 5,
|
||||
contextItemLabel: "labels.ungroup",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
getSelectedGroupIds(appState).length > 0,
|
||||
|
@ -6,7 +6,7 @@ import { t } from "../i18n";
|
||||
import { SceneHistory, HistoryEntry } from "../history";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { KEYS } from "../keys";
|
||||
import { isWindows, KEYS } from "../keys";
|
||||
import { getElementMap } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||
@ -59,16 +59,16 @@ const writeData = (
|
||||
return { commitToHistory };
|
||||
};
|
||||
|
||||
const testUndo = (shift: boolean) => (event: KeyboardEvent) =>
|
||||
event[KEYS.CTRL_OR_CMD] && /z/i.test(event.key) && event.shiftKey === shift;
|
||||
|
||||
type ActionCreator = (history: SceneHistory) => Action;
|
||||
|
||||
export const createUndoAction: ActionCreator = (history) => ({
|
||||
name: "undo",
|
||||
perform: (elements, appState) =>
|
||||
writeData(elements, appState, () => history.undoOnce()),
|
||||
keyTest: testUndo(false),
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.key.toLowerCase() === KEYS.Z &&
|
||||
!event.shiftKey,
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
@ -84,7 +84,11 @@ export const createRedoAction: ActionCreator = (history) => ({
|
||||
name: "redo",
|
||||
perform: (elements, appState) =>
|
||||
writeData(elements, appState, () => history.redoOnce()),
|
||||
keyTest: testUndo(true),
|
||||
keyTest: (event) =>
|
||||
(event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === KEYS.Z) ||
|
||||
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
|
@ -74,13 +74,13 @@ export const actionShortcuts = register({
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
showShortcutsDialog: true,
|
||||
showHelpDialog: !appState.showHelpDialog,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<HelpIcon title={t("shortcutsDialog.title")} onClick={updateData} />
|
||||
<HelpIcon title={t("helpDialog.title")} onClick={updateData} />
|
||||
),
|
||||
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
redrawTextBoundingBox,
|
||||
} from "../element";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
@ -23,13 +24,16 @@ export const actionCopyStyles = register({
|
||||
copiedStyles = JSON.stringify(element);
|
||||
}
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyStyles"),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.copyStyles",
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C,
|
||||
contextMenuOrder: 0,
|
||||
});
|
||||
|
||||
export const actionPasteStyles = register({
|
||||
@ -69,5 +73,4 @@ export const actionPasteStyles = register({
|
||||
contextItemLabel: "labels.pasteStyles",
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||
contextMenuOrder: 1,
|
||||
});
|
||||
|
22
src/actions/actionToggleGridMode.tsx
Normal file
22
src/actions/actionToggleGridMode.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { GRID_SIZE } from "../constants";
|
||||
import { AppState } from "../types";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleGridMode = register({
|
||||
name: "gridMode",
|
||||
perform(elements, appState) {
|
||||
trackEvent("view", "mode", "grid");
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState: AppState) => appState.gridSize !== null,
|
||||
contextItemLabel: "labels.gridMode",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
|
||||
});
|
16
src/actions/actionToggleStats.tsx
Normal file
16
src/actions/actionToggleStats.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleStats = register({
|
||||
name: "stats",
|
||||
perform(elements, appState) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
showStats: !this.checked!(appState),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.showStats,
|
||||
contextItemLabel: "stats.title",
|
||||
});
|
22
src/actions/actionToggleViewMode.tsx
Normal file
22
src/actions/actionToggleViewMode.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleViewMode = register({
|
||||
name: "viewMode",
|
||||
perform(elements, appState) {
|
||||
trackEvent("view", "mode", "view");
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
viewModeEnabled: !this.checked!(appState),
|
||||
selectedElementIds: {},
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.viewModeEnabled,
|
||||
contextItemLabel: "labels.viewMode",
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R,
|
||||
});
|
22
src/actions/actionToggleZenMode.tsx
Normal file
22
src/actions/actionToggleZenMode.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleZenMode = register({
|
||||
name: "zenMode",
|
||||
perform(elements, appState) {
|
||||
trackEvent("view", "mode", "zen");
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
zenModeEnabled: !this.checked!(appState),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.zenModeEnabled,
|
||||
contextItemLabel: "buttons.zenMode",
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z,
|
||||
});
|
@ -65,3 +65,15 @@ export {
|
||||
distributeHorizontally,
|
||||
distributeVertically,
|
||||
} from "./actionDistribute";
|
||||
|
||||
export {
|
||||
actionCopy,
|
||||
actionCut,
|
||||
actionCopyAsPng,
|
||||
actionCopyAsSvg,
|
||||
} from "./actionClipboard";
|
||||
|
||||
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||
|
||||
export { actionToggleStats } from "./actionToggleStats";
|
||||
|
@ -3,14 +3,15 @@ import {
|
||||
Action,
|
||||
ActionsManagerInterface,
|
||||
UpdaterFn,
|
||||
ActionFilterFn,
|
||||
ActionName,
|
||||
ActionResult,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { t } from "../i18n";
|
||||
import { ShortcutName } from "./shortcuts";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
|
||||
// This is the <App> component, but for now we don't care about anything but its
|
||||
// `canvas` state.
|
||||
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
|
||||
|
||||
export class ActionManager implements ActionsManagerInterface {
|
||||
actions = {} as ActionsManagerInterface["actions"];
|
||||
@ -18,13 +19,14 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||
|
||||
getAppState: () => Readonly<AppState>;
|
||||
|
||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
||||
app: App;
|
||||
|
||||
constructor(
|
||||
updater: UpdaterFn,
|
||||
getAppState: () => AppState,
|
||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
|
||||
app: App,
|
||||
) {
|
||||
this.updater = (actionResult) => {
|
||||
if (actionResult && "then" in actionResult) {
|
||||
@ -37,6 +39,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
};
|
||||
this.getAppState = getAppState;
|
||||
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
registerAction(action: Action) {
|
||||
@ -63,6 +66,12 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
if (data.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const { viewModeEnabled } = this.getAppState();
|
||||
if (viewModeEnabled) {
|
||||
if (data[0].name !== "viewMode") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.updater(
|
||||
@ -70,6 +79,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
@ -81,43 +91,11 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
|
||||
return Object.values(this.actions)
|
||||
.filter(actionFilter)
|
||||
.filter((action) => "contextItemLabel" in action)
|
||||
.filter((action) =>
|
||||
action.contextItemPredicate
|
||||
? action.contextItemPredicate(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
)
|
||||
: true,
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) -
|
||||
(b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
|
||||
)
|
||||
.map((action) => ({
|
||||
// take last bit of the label "labels.<shortcutName>"
|
||||
shortcutName: action.contextItemLabel?.split(".").pop() as ShortcutName,
|
||||
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
|
||||
action: () => {
|
||||
this.updater(
|
||||
action.perform(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
),
|
||||
);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// Id is an attribute that we can use to pass in data like keys.
|
||||
// This is needed for dynamically generated action components
|
||||
// like the user list. We can use this key to extract more
|
||||
@ -132,6 +110,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
formState,
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ export type ShortcutName =
|
||||
| "copyStyles"
|
||||
| "pasteStyles"
|
||||
| "selectAll"
|
||||
| "delete"
|
||||
| "deleteSelectedElements"
|
||||
| "duplicateSelection"
|
||||
| "sendBackward"
|
||||
| "bringForward"
|
||||
@ -22,7 +22,8 @@ export type ShortcutName =
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "addToLibrary";
|
||||
| "addToLibrary"
|
||||
| "viewMode";
|
||||
|
||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||
@ -31,10 +32,10 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
|
||||
pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
|
||||
selectAll: [getShortcutKey("CtrlOrCmd+A")],
|
||||
delete: [getShortcutKey("Del")],
|
||||
deleteSelectedElements: [getShortcutKey("Del")],
|
||||
duplicateSelection: [
|
||||
getShortcutKey("CtrlOrCmd+D"),
|
||||
getShortcutKey(`Alt+${t("shortcutsDialog.drag")}`),
|
||||
getShortcutKey(`Alt+${t("helpDialog.drag")}`),
|
||||
],
|
||||
sendBackward: [getShortcutKey("CtrlOrCmd+[")],
|
||||
bringForward: [getShortcutKey("CtrlOrCmd+]")],
|
||||
@ -56,6 +57,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
zenMode: [getShortcutKey("Alt+Z")],
|
||||
stats: [],
|
||||
addToLibrary: [],
|
||||
viewMode: [getShortcutKey("Alt+R")],
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||
|
@ -16,12 +16,18 @@ type ActionFn = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
formData: any,
|
||||
app: { canvas: HTMLCanvasElement | null },
|
||||
) => ActionResult | Promise<ActionResult>;
|
||||
|
||||
export type UpdaterFn = (res: ActionResult) => void;
|
||||
export type ActionFilterFn = (action: Action) => void;
|
||||
|
||||
export type ActionName =
|
||||
| "copy"
|
||||
| "cut"
|
||||
| "paste"
|
||||
| "copyAsPng"
|
||||
| "copyAsSvg"
|
||||
| "sendBackward"
|
||||
| "bringForward"
|
||||
| "sendToBack"
|
||||
@ -29,6 +35,9 @@ export type ActionName =
|
||||
| "copyStyles"
|
||||
| "selectAll"
|
||||
| "pasteStyles"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "changeStrokeColor"
|
||||
| "changeBackgroundColor"
|
||||
| "changeFillStyle"
|
||||
@ -75,7 +84,8 @@ export type ActionName =
|
||||
| "alignVerticallyCentered"
|
||||
| "alignHorizontallyCentered"
|
||||
| "distributeHorizontally"
|
||||
| "distributeVertically";
|
||||
| "distributeVertically"
|
||||
| "viewMode";
|
||||
|
||||
export interface Action {
|
||||
name: ActionName;
|
||||
@ -93,19 +103,16 @@ export interface Action {
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => boolean;
|
||||
contextItemLabel?: string;
|
||||
contextMenuOrder?: number;
|
||||
contextItemPredicate?: (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => boolean;
|
||||
checked?: (appState: Readonly<AppState>) => boolean;
|
||||
}
|
||||
|
||||
export interface ActionsManagerInterface {
|
||||
actions: Record<ActionName, Action>;
|
||||
registerAction: (action: Action) => void;
|
||||
handleKeyDown: (event: KeyboardEvent) => boolean;
|
||||
getContextMenuItems: (
|
||||
actionFilter: ActionFilterFn,
|
||||
) => { label: string; action: () => void }[];
|
||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export const trackEvent =
|
||||
process.env.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
||||
typeof process !== "undefined" &&
|
||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
||||
typeof window !== "undefined" &&
|
||||
window.gtag
|
||||
? (category: string, name: string, label?: string, value?: number) => {
|
||||
@ -9,7 +10,7 @@ export const trackEvent =
|
||||
value,
|
||||
});
|
||||
}
|
||||
: typeof process !== "undefined" && process?.env?.JEST_WORKER_ID
|
||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
||||
? (category: string, name: string, label?: string, value?: number) => {}
|
||||
: (category: string, name: string, label?: string, value?: number) => {
|
||||
// Uncomment the next line to track locally
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
GRID_SIZE,
|
||||
} from "./constants";
|
||||
import { t } from "./i18n";
|
||||
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
||||
import { AppState, NormalizedZoomValue } from "./types";
|
||||
import { getDateTime } from "./utils";
|
||||
|
||||
export const getDefaultAppState = (): Omit<
|
||||
@ -57,22 +57,24 @@ export const getDefaultAppState = (): Omit<
|
||||
previousSelectedElementIds: {},
|
||||
resizingElement: null,
|
||||
scrolledOutside: false,
|
||||
scrollX: 0 as FlooredNumber,
|
||||
scrollY: 0 as FlooredNumber,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
selectionElement: null,
|
||||
shouldAddWatermark: false,
|
||||
shouldCacheIgnoreZoom: false,
|
||||
showGrid: false,
|
||||
showShortcutsDialog: false,
|
||||
showHelpDialog: false,
|
||||
showStats: false,
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
toastMessage: null,
|
||||
viewBackgroundColor: oc.white,
|
||||
width: window.innerWidth,
|
||||
zenModeEnabled: false,
|
||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||
viewModeEnabled: false,
|
||||
};
|
||||
};
|
||||
|
||||
@ -144,14 +146,16 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
selectionElement: { browser: false, export: false },
|
||||
shouldAddWatermark: { browser: true, export: false },
|
||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
||||
showShortcutsDialog: { browser: false, export: false },
|
||||
showHelpDialog: { browser: false, export: false },
|
||||
showStats: { browser: true, export: false },
|
||||
startBoundElement: { browser: false, export: false },
|
||||
suggestedBindings: { browser: false, export: false },
|
||||
toastMessage: { browser: false, export: false },
|
||||
viewBackgroundColor: { browser: true, export: true },
|
||||
width: { browser: false, export: false },
|
||||
zenModeEnabled: { browser: true, export: false },
|
||||
zoom: { browser: true, export: false },
|
||||
viewModeEnabled: { browser: false, export: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { trackEvent } from "./analytics";
|
||||
import colors from "./colors";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants";
|
||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||
@ -473,7 +472,6 @@ export const renderSpreadsheet = (
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
trackEvent("magic", "chart", chartType, spreadsheet.values.length);
|
||||
if (chartType === "line") {
|
||||
return chartTypeLine(spreadsheet, x, y);
|
||||
}
|
||||
|
@ -163,9 +163,9 @@ export const ShapesSwitcher = ({
|
||||
{SHAPES.map(({ value, icon, key }, index) => {
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter = typeof key === "string" ? key : key[0];
|
||||
const shortcut = `${capitalizeString(letter)} ${t(
|
||||
"shortcutsDialog.or",
|
||||
)} ${index + 1}`;
|
||||
const shortcut = `${capitalizeString(letter)} ${t("helpDialog.or")} ${
|
||||
index + 1
|
||||
}`;
|
||||
return (
|
||||
<ToolButton
|
||||
className="Shape"
|
||||
|
@ -2,8 +2,31 @@ import { Point, simplify } from "points-on-curve";
|
||||
import React from "react";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "../actions";
|
||||
import { actionDeleteSelected, actionFinalize } from "../actions";
|
||||
import {
|
||||
actionAddToLibrary,
|
||||
actionBringForward,
|
||||
actionBringToFront,
|
||||
actionCopy,
|
||||
actionCopyAsPng,
|
||||
actionCopyAsSvg,
|
||||
actionCopyStyles,
|
||||
actionCut,
|
||||
actionDeleteSelected,
|
||||
actionDuplicateSelection,
|
||||
actionFinalize,
|
||||
actionGroup,
|
||||
actionPasteStyles,
|
||||
actionSelectAll,
|
||||
actionSendBackward,
|
||||
actionSendToBack,
|
||||
actionToggleGridMode,
|
||||
actionToggleStats,
|
||||
actionToggleZenMode,
|
||||
actionUngroup,
|
||||
} from "../actions";
|
||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { actions } from "../actions/register";
|
||||
@ -18,7 +41,6 @@ import {
|
||||
} from "../clipboard";
|
||||
import {
|
||||
APP_NAME,
|
||||
CANVAS_ONLY_ACTIONS,
|
||||
CURSOR_TYPE,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
DRAGGING_THRESHOLD,
|
||||
@ -32,8 +54,9 @@ import {
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
ZOOM_STEP,
|
||||
} from "../constants";
|
||||
import { exportCanvas, loadFromBlob } from "../data";
|
||||
import { loadFromBlob } from "../data";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import { Library } from "../data/library";
|
||||
import { restore } from "../data/restore";
|
||||
@ -126,7 +149,6 @@ import {
|
||||
getSelectedElements,
|
||||
isOverScrollBars,
|
||||
isSomeElementSelected,
|
||||
normalizeScroll,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { SceneState, ScrollBars } from "../scene/types";
|
||||
@ -154,9 +176,12 @@ import {
|
||||
viewportCoordsToSceneCoords,
|
||||
withBatchedUpdates,
|
||||
} from "../utils";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import { isMobile } from "../is-mobile";
|
||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Stats } from "./Stats";
|
||||
import { Toast } from "./Toast";
|
||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||
|
||||
const { history } = createHistory();
|
||||
|
||||
@ -246,6 +271,7 @@ export type ExcalidrawImperativeAPI = {
|
||||
};
|
||||
setScrollToCenter: InstanceType<typeof App>["setScrollToCenter"];
|
||||
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
||||
getAppState: () => InstanceType<typeof App>["state"];
|
||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
||||
ready: true;
|
||||
};
|
||||
@ -272,6 +298,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
excalidrawRef,
|
||||
viewModeEnabled = false,
|
||||
} = props;
|
||||
this.state = {
|
||||
...defaultAppState,
|
||||
@ -279,6 +306,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
width,
|
||||
height,
|
||||
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
|
||||
viewModeEnabled,
|
||||
};
|
||||
if (excalidrawRef) {
|
||||
const readyPromise =
|
||||
@ -296,6 +324,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
},
|
||||
setScrollToCenter: this.setScrollToCenter,
|
||||
getSceneElements: this.getSceneElements,
|
||||
getAppState: () => this.state,
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@ -310,6 +339,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.syncActionResult,
|
||||
() => this.state,
|
||||
() => this.scene.getElementsIncludingDeleted(),
|
||||
this,
|
||||
);
|
||||
this.actionManager.registerAll(actions);
|
||||
|
||||
@ -317,6 +347,62 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.actionManager.registerAction(createRedoAction(history));
|
||||
}
|
||||
|
||||
private renderCanvas() {
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
const {
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
viewModeEnabled,
|
||||
} = this.state;
|
||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||
const canvasHeight = canvasDOMHeight * canvasScale;
|
||||
if (viewModeEnabled) {
|
||||
return (
|
||||
<canvas
|
||||
id="canvas"
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
cursor: "grabbing",
|
||||
}}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
ref={this.handleCanvasRef}
|
||||
onContextMenu={this.handleCanvasContextMenu}
|
||||
onPointerMove={this.handleCanvasPointerMove}
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerCancel={this.removePointer}
|
||||
onTouchMove={this.handleTouchMove}
|
||||
onPointerDown={this.handleCanvasPointerDown}
|
||||
>
|
||||
{t("labels.drawingCanvas")}
|
||||
</canvas>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<canvas
|
||||
id="canvas"
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
}}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
ref={this.handleCanvasRef}
|
||||
onContextMenu={this.handleCanvasContextMenu}
|
||||
onPointerDown={this.handleCanvasPointerDown}
|
||||
onDoubleClick={this.handleCanvasDoubleClick}
|
||||
onPointerMove={this.handleCanvasPointerMove}
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerCancel={this.removePointer}
|
||||
onTouchMove={this.handleTouchMove}
|
||||
onDrop={this.handleCanvasOnDrop}
|
||||
>
|
||||
{t("labels.drawingCanvas")}
|
||||
</canvas>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
zenModeEnabled,
|
||||
@ -324,20 +410,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
height: canvasDOMHeight,
|
||||
offsetTop,
|
||||
offsetLeft,
|
||||
viewModeEnabled,
|
||||
} = this.state;
|
||||
|
||||
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
|
||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||
const canvasHeight = canvasDOMHeight * canvasScale;
|
||||
|
||||
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
|
||||
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="excalidraw"
|
||||
className={clsx("excalidraw", {
|
||||
"excalidraw--view-mode": viewModeEnabled,
|
||||
})}
|
||||
ref={this.excalidrawContainerRef}
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
@ -367,6 +452,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
isCollaborating={this.props.isCollaborating || false}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderCustomFooter={renderFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
/>
|
||||
{this.state.showStats && (
|
||||
<Stats
|
||||
@ -376,28 +462,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
onClose={this.toggleStats}
|
||||
/>
|
||||
)}
|
||||
<main>
|
||||
<canvas
|
||||
id="canvas"
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
}}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
ref={this.handleCanvasRef}
|
||||
onContextMenu={this.handleCanvasContextMenu}
|
||||
onPointerDown={this.handleCanvasPointerDown}
|
||||
onDoubleClick={this.handleCanvasDoubleClick}
|
||||
onPointerMove={this.handleCanvasPointerMove}
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerCancel={this.removePointer}
|
||||
onTouchMove={this.handleTouchMove}
|
||||
onDrop={this.handleCanvasOnDrop}
|
||||
>
|
||||
{t("labels.drawingCanvas")}
|
||||
</canvas>
|
||||
</main>
|
||||
{this.state.toastMessage !== null && (
|
||||
<Toast
|
||||
message={this.state.toastMessage}
|
||||
clearToast={this.clearToast}
|
||||
/>
|
||||
)}
|
||||
<main>{this.renderCanvas()}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -437,6 +508,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
if (actionResult.commitToHistory) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
|
||||
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
||||
|
||||
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||
viewModeEnabled = this.props.viewModeEnabled;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
(state) => ({
|
||||
...actionResult.appState,
|
||||
@ -446,6 +524,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
height: state.height,
|
||||
offsetTop: state.offsetTop,
|
||||
offsetLeft: state.offsetLeft,
|
||||
viewModeEnabled,
|
||||
}),
|
||||
() => {
|
||||
if (actionResult.syncHistory) {
|
||||
@ -628,7 +707,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
|
||||
this.scene.addCallback(this.onSceneUpdated);
|
||||
|
||||
this.addEventListeners();
|
||||
|
||||
// optim to avoid extra render on init
|
||||
@ -695,25 +773,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
|
||||
private addEventListeners() {
|
||||
this.removeEventListeners();
|
||||
document.addEventListener(EVENT.COPY, this.onCopy);
|
||||
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
||||
document.addEventListener(EVENT.CUT, this.onCut);
|
||||
|
||||
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
||||
document.addEventListener(
|
||||
EVENT.MOUSE_MOVE,
|
||||
this.updateCurrentCursorPosition,
|
||||
);
|
||||
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
||||
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
||||
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
||||
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
||||
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
||||
|
||||
// rerender text elements on font load to fix #637 && #1553
|
||||
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
||||
|
||||
// Safari-only desktop pinch zoom
|
||||
document.addEventListener(
|
||||
EVENT.GESTURE_START,
|
||||
@ -730,6 +799,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.onGestureEnd as any,
|
||||
false,
|
||||
);
|
||||
if (this.state.viewModeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
||||
document.addEventListener(EVENT.CUT, this.onCut);
|
||||
|
||||
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
||||
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
||||
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
||||
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
||||
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
||||
@ -752,6 +833,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
|
||||
this.setState(
|
||||
{ viewModeEnabled: !!this.props.viewModeEnabled },
|
||||
this.addEventListeners,
|
||||
);
|
||||
}
|
||||
|
||||
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(".excalidraw")
|
||||
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
|
||||
@ -899,43 +991,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
copyToClipboard(this.scene.getElements(), this.state);
|
||||
};
|
||||
|
||||
private copyToClipboardAsPng = async () => {
|
||||
const elements = this.scene.getElements();
|
||||
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length ? selectedElements : elements,
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private copyToClipboardAsSvg = async () => {
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getElements(),
|
||||
this.state,
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard-svg",
|
||||
selectedElements.length ? selectedElements : this.scene.getElements(),
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private static resetTapTwice() {
|
||||
didTapTwice = false;
|
||||
}
|
||||
@ -1143,9 +1198,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
};
|
||||
|
||||
toggleZenMode = () => {
|
||||
this.setState({
|
||||
zenModeEnabled: !this.state.zenModeEnabled,
|
||||
});
|
||||
this.actionManager.executeAction(actionToggleZenMode);
|
||||
};
|
||||
|
||||
toggleGridMode = () => {
|
||||
@ -1158,9 +1211,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
if (!this.state.showStats) {
|
||||
trackEvent("dialog", "stats");
|
||||
}
|
||||
this.setState({
|
||||
showStats: !this.state.showStats,
|
||||
});
|
||||
this.actionManager.executeAction(actionToggleStats);
|
||||
};
|
||||
|
||||
setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
|
||||
@ -1173,6 +1224,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
clearToast = () => {
|
||||
this.setState({ toastMessage: null });
|
||||
};
|
||||
|
||||
public updateScene = withBatchedUpdates((sceneData: SceneData) => {
|
||||
if (sceneData.commitToHistory) {
|
||||
history.resumeRecording();
|
||||
@ -1242,31 +1297,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
|
||||
if (event.key === KEYS.QUESTION_MARK) {
|
||||
this.setState({
|
||||
showShortcutsDialog: true,
|
||||
showHelpDialog: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z) {
|
||||
this.toggleZenMode();
|
||||
}
|
||||
|
||||
if (event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE) {
|
||||
this.toggleGridMode();
|
||||
}
|
||||
if (event[KEYS.CTRL_OR_CMD]) {
|
||||
this.setState({ isBindingEnabled: false });
|
||||
}
|
||||
|
||||
if (event.code === CODES.C && event.altKey && event.shiftKey) {
|
||||
this.copyToClipboardAsPng();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.actionManager.handleKeyDown(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.viewModeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event[KEYS.CTRL_OR_CMD]) {
|
||||
this.setState({ isBindingEnabled: false });
|
||||
}
|
||||
|
||||
if (event.code === CODES.NINE) {
|
||||
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
||||
}
|
||||
@ -1771,8 +1817,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const scaleFactor = distance / gesture.initialDistance;
|
||||
|
||||
this.setState(({ zoom, scrollX, scrollY, offsetLeft, offsetTop }) => ({
|
||||
scrollX: normalizeScroll(scrollX + deltaX / zoom.value),
|
||||
scrollY: normalizeScroll(scrollY + deltaY / zoom.value),
|
||||
scrollX: scrollX + deltaX / zoom.value,
|
||||
scrollY: scrollY + deltaY / zoom.value,
|
||||
zoom: getNewZoom(
|
||||
getNormalizedZoom(initialScale * scaleFactor),
|
||||
zoom,
|
||||
@ -2074,6 +2120,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
|
||||
lastPointerUp = onPointerUp;
|
||||
|
||||
if (!this.state.viewModeEnabled) {
|
||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
||||
@ -2082,6 +2129,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
pointerDownState.eventListeners.onUp = onPointerUp;
|
||||
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
||||
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
||||
}
|
||||
};
|
||||
|
||||
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
||||
@ -2131,7 +2179,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
!(
|
||||
gesture.pointers.size === 0 &&
|
||||
(event.button === POINTER_BUTTON.WHEEL ||
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||
this.state.viewModeEnabled)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
@ -2184,12 +2233,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
scrollX: normalizeScroll(
|
||||
this.state.scrollX - deltaX / this.state.zoom.value,
|
||||
),
|
||||
scrollY: normalizeScroll(
|
||||
this.state.scrollY - deltaY / this.state.zoom.value,
|
||||
),
|
||||
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
|
||||
scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
|
||||
});
|
||||
});
|
||||
const teardown = withBatchedUpdates(
|
||||
@ -3013,9 +3058,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const x = event.clientX;
|
||||
const dx = x - pointerDownState.lastCoords.x;
|
||||
this.setState({
|
||||
scrollX: normalizeScroll(
|
||||
this.state.scrollX - dx / this.state.zoom.value,
|
||||
),
|
||||
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
||||
});
|
||||
pointerDownState.lastCoords.x = x;
|
||||
return true;
|
||||
@ -3025,9 +3068,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const y = event.clientY;
|
||||
const dy = y - pointerDownState.lastCoords.y;
|
||||
this.setState({
|
||||
scrollY: normalizeScroll(
|
||||
this.state.scrollY - dy / this.state.zoom.value,
|
||||
),
|
||||
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
||||
});
|
||||
pointerDownState.lastCoords.y = y;
|
||||
return true;
|
||||
@ -3593,9 +3634,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
transformElements(
|
||||
pointerDownState,
|
||||
transformHandleType,
|
||||
(newTransformHandle) => {
|
||||
pointerDownState.resize.handleType = newTransformHandle;
|
||||
},
|
||||
selectedElements,
|
||||
pointerDownState.resize.arrowDirection,
|
||||
getRotateWithDiscreteAngleKey(event),
|
||||
@ -3625,52 +3663,87 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.state,
|
||||
);
|
||||
|
||||
const maybeGroupAction = actionGroup.contextItemPredicate!(
|
||||
this.actionManager.getElementsIncludingDeleted(),
|
||||
this.actionManager.getAppState(),
|
||||
);
|
||||
|
||||
const maybeUngroupAction = actionUngroup.contextItemPredicate!(
|
||||
this.actionManager.getElementsIncludingDeleted(),
|
||||
this.actionManager.getAppState(),
|
||||
);
|
||||
|
||||
const separator = "separator";
|
||||
|
||||
const _isMobile = isMobile();
|
||||
|
||||
const elements = this.scene.getElements();
|
||||
const element = this.getElementAtPosition(x, y);
|
||||
const options: ContextMenuOption[] = [];
|
||||
if (probablySupportsClipboardBlob && elements.length > 0) {
|
||||
options.push(actionCopyAsPng);
|
||||
}
|
||||
|
||||
if (probablySupportsClipboardWriteText && elements.length > 0) {
|
||||
options.push(actionCopyAsSvg);
|
||||
}
|
||||
if (!element) {
|
||||
const viewModeOptions: ContextMenuOption[] = [
|
||||
...options,
|
||||
actionToggleStats,
|
||||
];
|
||||
|
||||
if (typeof this.props.viewModeEnabled === "undefined") {
|
||||
viewModeOptions.push(actionToggleViewMode);
|
||||
}
|
||||
|
||||
ContextMenu.push({
|
||||
options: viewModeOptions,
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
});
|
||||
|
||||
if (this.state.viewModeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContextMenu.push({
|
||||
options: [
|
||||
_isMobile &&
|
||||
navigator.clipboard && {
|
||||
shortcutName: "paste",
|
||||
label: t("labels.paste"),
|
||||
action: () => this.pasteFromClipboard(null),
|
||||
name: "paste",
|
||||
perform: (elements, appStates) => {
|
||||
this.pasteFromClipboard(null);
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.paste",
|
||||
},
|
||||
_isMobile && navigator.clipboard && separator,
|
||||
probablySupportsClipboardBlob &&
|
||||
elements.length > 0 && {
|
||||
shortcutName: "copyAsPng",
|
||||
label: t("labels.copyAsPng"),
|
||||
action: this.copyToClipboardAsPng,
|
||||
},
|
||||
elements.length > 0 &&
|
||||
actionCopyAsPng,
|
||||
probablySupportsClipboardWriteText &&
|
||||
elements.length > 0 && {
|
||||
shortcutName: "copyAsSvg",
|
||||
label: t("labels.copyAsSvg"),
|
||||
action: this.copyToClipboardAsSvg,
|
||||
},
|
||||
...this.actionManager.getContextMenuItems((action) =>
|
||||
CANVAS_ONLY_ACTIONS.includes(action.name),
|
||||
),
|
||||
{
|
||||
checked: this.state.showGrid,
|
||||
shortcutName: "gridMode",
|
||||
label: t("labels.gridMode"),
|
||||
action: this.toggleGridMode,
|
||||
},
|
||||
{
|
||||
checked: this.state.zenModeEnabled,
|
||||
shortcutName: "zenMode",
|
||||
label: t("buttons.zenMode"),
|
||||
action: this.toggleZenMode,
|
||||
},
|
||||
{
|
||||
checked: this.state.showStats,
|
||||
shortcutName: "stats",
|
||||
label: t("stats.title"),
|
||||
action: this.toggleStats,
|
||||
},
|
||||
elements.length > 0 &&
|
||||
actionCopyAsSvg,
|
||||
((probablySupportsClipboardBlob && elements.length > 0) ||
|
||||
(probablySupportsClipboardWriteText && elements.length > 0)) &&
|
||||
separator,
|
||||
actionSelectAll,
|
||||
separator,
|
||||
actionToggleGridMode,
|
||||
actionToggleZenMode,
|
||||
typeof this.props.viewModeEnabled === "undefined" &&
|
||||
actionToggleViewMode,
|
||||
actionToggleStats,
|
||||
],
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -3679,39 +3752,55 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
}
|
||||
|
||||
if (this.state.viewModeEnabled) {
|
||||
ContextMenu.push({
|
||||
options: [navigator.clipboard && actionCopy, ...options],
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ContextMenu.push({
|
||||
options: [
|
||||
{
|
||||
shortcutName: "cut",
|
||||
label: t("labels.cut"),
|
||||
action: this.cutAll,
|
||||
},
|
||||
_isMobile && actionCut,
|
||||
_isMobile && navigator.clipboard && actionCopy,
|
||||
_isMobile &&
|
||||
navigator.clipboard && {
|
||||
shortcutName: "copy",
|
||||
label: t("labels.copy"),
|
||||
action: this.copyAll,
|
||||
name: "paste",
|
||||
perform: (elements, appStates) => {
|
||||
this.pasteFromClipboard(null);
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
navigator.clipboard && {
|
||||
shortcutName: "paste",
|
||||
label: t("labels.paste"),
|
||||
action: () => this.pasteFromClipboard(null),
|
||||
contextItemLabel: "labels.paste",
|
||||
},
|
||||
probablySupportsClipboardBlob && {
|
||||
shortcutName: "copyAsPng",
|
||||
label: t("labels.copyAsPng"),
|
||||
action: this.copyToClipboardAsPng,
|
||||
},
|
||||
probablySupportsClipboardWriteText && {
|
||||
shortcutName: "copyAsSvg",
|
||||
label: t("labels.copyAsSvg"),
|
||||
action: this.copyToClipboardAsSvg,
|
||||
},
|
||||
...this.actionManager.getContextMenuItems(
|
||||
(action) => !CANVAS_ONLY_ACTIONS.includes(action.name),
|
||||
),
|
||||
_isMobile && separator,
|
||||
...options,
|
||||
separator,
|
||||
actionCopyStyles,
|
||||
actionPasteStyles,
|
||||
separator,
|
||||
maybeGroupAction && actionGroup,
|
||||
maybeUngroupAction && actionUngroup,
|
||||
(maybeGroupAction || maybeUngroupAction) && separator,
|
||||
actionAddToLibrary,
|
||||
separator,
|
||||
actionSendBackward,
|
||||
actionBringForward,
|
||||
actionSendToBack,
|
||||
actionBringToFront,
|
||||
separator,
|
||||
actionDuplicateSelection,
|
||||
actionDeleteSelected,
|
||||
],
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
});
|
||||
};
|
||||
|
||||
@ -3742,9 +3831,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
let newZoom = this.state.zoom.value - delta / 100;
|
||||
// increase zoom steps the more zoomed-in we are (applies to >100% only)
|
||||
newZoom += Math.log10(Math.max(1, this.state.zoom.value)) * -sign;
|
||||
// round to nearest step
|
||||
newZoom = Math.round(newZoom * ZOOM_STEP * 100) / (ZOOM_STEP * 100);
|
||||
|
||||
this.setState(({ zoom, offsetLeft, offsetTop }) => ({
|
||||
zoom: getNewZoom(
|
||||
getNormalizedZoom(zoom.value - delta / 100),
|
||||
getNormalizedZoom(newZoom),
|
||||
zoom,
|
||||
{ left: offsetLeft, top: offsetTop },
|
||||
{
|
||||
@ -3767,14 +3862,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
if (event.shiftKey) {
|
||||
this.setState(({ zoom, scrollX }) => ({
|
||||
// on Mac, shift+wheel tends to result in deltaX
|
||||
scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom.value),
|
||||
scrollX: scrollX - (deltaY || deltaX) / zoom.value,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(({ zoom, scrollX, scrollY }) => ({
|
||||
scrollX: normalizeScroll(scrollX - deltaX / zoom.value),
|
||||
scrollY: normalizeScroll(scrollY - deltaY / zoom.value),
|
||||
scrollX: scrollX - deltaX / zoom.value,
|
||||
scrollY: scrollY - deltaY / zoom.value,
|
||||
}));
|
||||
});
|
||||
|
||||
@ -3834,7 +3929,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
};
|
||||
|
||||
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
|
||||
if (!this.unmounted) {
|
||||
this.setState({ shouldCacheIgnoreZoom: false });
|
||||
}
|
||||
}, 300);
|
||||
|
||||
private getCanvasOffsets(offsets?: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Avatar {
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.CollabButton.is-collaborating {
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.color-picker {
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.context-menu {
|
||||
@ -9,9 +9,10 @@
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
margin: -0.25rem 0 0 0.125rem;
|
||||
padding: 0.25rem 0;
|
||||
padding: 0.5rem 0;
|
||||
background-color: var(--popup-secondary-background-color);
|
||||
border: 1px solid var(--button-gray-3);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.context-menu button {
|
||||
@ -54,6 +55,7 @@
|
||||
.context-menu-option__shortcut {
|
||||
justify-self: end;
|
||||
opacity: 0.6;
|
||||
font-family: inherit;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
@ -87,4 +89,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-option-separator {
|
||||
border: none;
|
||||
border-top: 1px solid $oc-gray-5;
|
||||
}
|
||||
}
|
||||
|
@ -2,28 +2,36 @@ import React from "react";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import clsx from "clsx";
|
||||
import { Popover } from "./Popover";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import "./ContextMenu.scss";
|
||||
import {
|
||||
getShortcutFromShortcutName,
|
||||
ShortcutName,
|
||||
} from "../actions/shortcuts";
|
||||
import { Action } from "../actions/types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { AppState } from "../types";
|
||||
|
||||
type ContextMenuOption = {
|
||||
checked?: boolean;
|
||||
shortcutName: ShortcutName;
|
||||
label: string;
|
||||
action(): void;
|
||||
};
|
||||
export type ContextMenuOption = "separator" | Action;
|
||||
|
||||
type Props = {
|
||||
type ContextMenuProps = {
|
||||
options: ContextMenuOption[];
|
||||
onCloseRequest?(): void;
|
||||
top: number;
|
||||
left: number;
|
||||
actionManager: ActionManager;
|
||||
appState: Readonly<AppState>;
|
||||
};
|
||||
|
||||
const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
||||
const ContextMenu = ({
|
||||
options,
|
||||
onCloseRequest,
|
||||
top,
|
||||
left,
|
||||
actionManager,
|
||||
appState,
|
||||
}: ContextMenuProps) => {
|
||||
const isDarkTheme = !!document
|
||||
.querySelector(".excalidraw")
|
||||
?.classList.contains("Appearance_dark");
|
||||
@ -43,23 +51,34 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
||||
className="context-menu"
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{options.map(({ action, checked, shortcutName, label }, idx) => (
|
||||
<li data-testid={shortcutName} key={idx} onClick={onCloseRequest}>
|
||||
{options.map((option, idx) => {
|
||||
if (option === "separator") {
|
||||
return <hr key={idx} className="context-menu-option-separator" />;
|
||||
}
|
||||
|
||||
const actionName = option.name;
|
||||
const label = option.contextItemLabel
|
||||
? t(option.contextItemLabel)
|
||||
: "";
|
||||
return (
|
||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||
<button
|
||||
className={`context-menu-option
|
||||
${shortcutName === "delete" ? "dangerous" : ""}
|
||||
${checked ? "checkmark" : ""}`}
|
||||
onClick={action}
|
||||
className={clsx("context-menu-option", {
|
||||
dangerous: actionName === "deleteSelectedElements",
|
||||
checkmark: option.checked?.(appState),
|
||||
})}
|
||||
onClick={() => actionManager.executeAction(option)}
|
||||
>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<div className="context-menu-option__shortcut">
|
||||
{shortcutName
|
||||
? getShortcutFromShortcutName(shortcutName)
|
||||
<kbd className="context-menu-option__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</div>
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Popover>
|
||||
</div>
|
||||
@ -78,8 +97,10 @@ const getContextMenuNode = (): HTMLDivElement => {
|
||||
|
||||
type ContextMenuParams = {
|
||||
options: (ContextMenuOption | false | null | undefined)[];
|
||||
top: number;
|
||||
left: number;
|
||||
top: ContextMenuProps["top"];
|
||||
left: ContextMenuProps["left"];
|
||||
actionManager: ContextMenuProps["actionManager"];
|
||||
appState: Readonly<AppState>;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@ -101,6 +122,8 @@ export default {
|
||||
left={params.left}
|
||||
options={options}
|
||||
onCloseRequest={handleClose}
|
||||
actionManager={params.actionManager}
|
||||
appState={params.appState}
|
||||
/>,
|
||||
getContextMenuNode(),
|
||||
);
|
||||
|
@ -1,6 +1,11 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Dialog {
|
||||
user-select: text;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.Dialog__title {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
@ -10,6 +15,7 @@
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
text-align: center;
|
||||
font-variant: small-caps;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.Dialog__titleContent {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { KEYS } from "../keys";
|
||||
@ -8,14 +9,6 @@ import { back, close } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
const useRefState = <T,>() => {
|
||||
const [refValue, setRefValue] = useState<T | null>(null);
|
||||
const refCallback = useCallback((value: T) => {
|
||||
setRefValue(value);
|
||||
}, []);
|
||||
return [refValue, refCallback] as const;
|
||||
};
|
||||
|
||||
export const Dialog = (props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
@ -24,7 +17,7 @@ export const Dialog = (props: {
|
||||
title: React.ReactNode;
|
||||
autofocus?: boolean;
|
||||
}) => {
|
||||
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!islandNode) {
|
||||
@ -80,7 +73,7 @@ export const Dialog = (props: {
|
||||
onCloseRequest={props.onCloseRequest}
|
||||
>
|
||||
<Island ref={setIslandNode}>
|
||||
<h3 id="dialog-title" className="Dialog__title">
|
||||
<h2 id="dialog-title" className="Dialog__title">
|
||||
<span className="Dialog__titleContent">{props.title}</span>
|
||||
<button
|
||||
className="Modal__close"
|
||||
@ -89,7 +82,7 @@ export const Dialog = (props: {
|
||||
>
|
||||
{useIsMobile() ? back : close}
|
||||
</button>
|
||||
</h3>
|
||||
</h2>
|
||||
<div className="Dialog__content">{props.children}</div>
|
||||
</Island>
|
||||
</Modal>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.ExportDialog__preview {
|
||||
|
@ -1,23 +1,28 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.ShortcutsDialog-island {
|
||||
.HelpDialog h3 {
|
||||
border-bottom: 1px solid var(--button-gray-2);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.HelpDialog--island {
|
||||
border: 1px solid var(--button-gray-2);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ShortcutsDialog-island-title {
|
||||
.HelpDialog--island-title {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
background-color: var(--button-gray-1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ShorcutsDialog-shortcut {
|
||||
.HelpDialog--shortcut {
|
||||
border-top: 1px solid var(--button-gray-2);
|
||||
}
|
||||
|
||||
.ShorcutsDialog-key {
|
||||
.HelpDialog--key {
|
||||
word-break: keep-all;
|
||||
border: 1px solid var(--button-gray-2);
|
||||
padding: 2px 8px;
|
||||
@ -29,14 +34,23 @@
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ShortcutsDialog-footer {
|
||||
.HelpDialog--header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
border-top: 1px solid var(--button-gray-2);
|
||||
margin-top: 8px;
|
||||
padding-top: 16px;
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.HelpDialog--btn {
|
||||
border: 1px solid var(--link-color);
|
||||
padding: 8px 32px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.HelpDialog--btn:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
359
src/components/HelpDialog.tsx
Normal file
359
src/components/HelpDialog.tsx
Normal file
@ -0,0 +1,359 @@
|
||||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { isDarwin, isWindows } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./HelpDialog.scss";
|
||||
|
||||
const Header = () => (
|
||||
<div className="HelpDialog--header">
|
||||
<a
|
||||
className="HelpDialog--btn"
|
||||
href="https://github.com/excalidraw/excalidraw#documentation"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("helpDialog.documentation")}
|
||||
</a>
|
||||
<a
|
||||
className="HelpDialog--btn"
|
||||
href="https://blog.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("helpDialog.blog")}
|
||||
</a>
|
||||
<a
|
||||
className="HelpDialog--btn"
|
||||
href="https://github.com/excalidraw/excalidraw/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("helpDialog.github")}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Section = (props: { title: string; children: React.ReactNode }) => (
|
||||
<>
|
||||
<h3>{props.title}</h3>
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
|
||||
const Columns = (props: { children: React.ReactNode }) => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Column = (props: { children: React.ReactNode }) => (
|
||||
<div style={{ width: "49%" }}>{props.children}</div>
|
||||
);
|
||||
|
||||
const ShortcutIsland = (props: {
|
||||
caption: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="HelpDialog--island">
|
||||
<h3 className="HelpDialog--island-title">{props.caption}</h3>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Shortcut = (props: {
|
||||
label: string;
|
||||
shortcuts: string[];
|
||||
isOr: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="HelpDialog--shortcut">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
margin: "0",
|
||||
padding: "4px 8px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: "0 0 auto",
|
||||
justifyContent: "flex-end",
|
||||
marginInlineStart: "auto",
|
||||
minWidth: "30%",
|
||||
}}
|
||||
>
|
||||
{props.shortcuts.map((shortcut, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ShortcutKey>{shortcut}</ShortcutKey>
|
||||
{props.isOr &&
|
||||
index !== props.shortcuts.length - 1 &&
|
||||
t("helpDialog.or")}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Shortcut.defaultProps = {
|
||||
isOr: true,
|
||||
};
|
||||
|
||||
const ShortcutKey = (props: { children: React.ReactNode }) => (
|
||||
<kbd className="HelpDialog--key" {...props} />
|
||||
);
|
||||
|
||||
export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
onCloseRequest={handleClose}
|
||||
title={t("helpDialog.title")}
|
||||
className={"HelpDialog"}
|
||||
>
|
||||
<Header />
|
||||
<Section title={t("helpDialog.shortcuts")}>
|
||||
<Columns>
|
||||
<Column>
|
||||
<ShortcutIsland caption={t("helpDialog.shapes")}>
|
||||
<Shortcut
|
||||
label={t("toolBar.selection")}
|
||||
shortcuts={["V", "1"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.rectangle")}
|
||||
shortcuts={["R", "2"]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
|
||||
<Shortcut label={t("toolBar.ellipse")} shortcuts={["E", "4"]} />
|
||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.draw")}
|
||||
shortcuts={["Shift+P", "7"]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||
<Shortcut
|
||||
label={t("helpDialog.textNewLine")}
|
||||
shortcuts={[
|
||||
getShortcutKey("Enter"),
|
||||
getShortcutKey("Shift+Enter"),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.textFinish")}
|
||||
shortcuts={[
|
||||
getShortcutKey("Esc"),
|
||||
getShortcutKey("CtrlOrCmd+Enter"),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.curvedArrow")}
|
||||
shortcuts={[
|
||||
"A",
|
||||
t("helpDialog.click"),
|
||||
t("helpDialog.click"),
|
||||
t("helpDialog.click"),
|
||||
]}
|
||||
isOr={false}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.curvedLine")}
|
||||
shortcuts={[
|
||||
"L",
|
||||
t("helpDialog.click"),
|
||||
t("helpDialog.click"),
|
||||
t("helpDialog.click"),
|
||||
]}
|
||||
isOr={false}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
|
||||
<Shortcut
|
||||
label={t("helpDialog.preventBinding")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
<ShortcutIsland caption={t("helpDialog.view")}>
|
||||
<Shortcut
|
||||
label={t("buttons.zoomIn")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd++")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.zoomOut")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.resetZoom")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.zoomToFit")}
|
||||
shortcuts={["Shift+1"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.zoomToSelection")}
|
||||
shortcuts={["Shift+2"]}
|
||||
/>
|
||||
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
|
||||
<Shortcut
|
||||
label={t("buttons.zenMode")}
|
||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.gridMode")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.viewMode")}
|
||||
shortcuts={[getShortcutKey("Alt+R")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShortcutIsland caption={t("helpDialog.editor")}>
|
||||
<Shortcut
|
||||
label={t("labels.selectAll")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.multiSelect")}
|
||||
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.moveCanvas")}
|
||||
shortcuts={[
|
||||
getShortcutKey(`Space+${t("helpDialog.drag")}`),
|
||||
getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
|
||||
]}
|
||||
isOr={true}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.cut")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copy")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.paste")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.pasteStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.delete")}
|
||||
shortcuts={[getShortcutKey("Del")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.sendToBack")}
|
||||
shortcuts={[
|
||||
isDarwin
|
||||
? getShortcutKey("CtrlOrCmd+Alt+[")
|
||||
: getShortcutKey("CtrlOrCmd+Shift+["),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.bringToFront")}
|
||||
shortcuts={[
|
||||
isDarwin
|
||||
? getShortcutKey("CtrlOrCmd+Alt+]")
|
||||
: getShortcutKey("CtrlOrCmd+Shift+]"),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.sendBackward")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.bringForward")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignTop")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignBottom")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignLeft")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignRight")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.duplicateSelection")}
|
||||
shortcuts={[
|
||||
getShortcutKey("CtrlOrCmd+D"),
|
||||
getShortcutKey(`Alt+${t("helpDialog.drag")}`),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.undo")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.redo")}
|
||||
shortcuts={
|
||||
isWindows
|
||||
? [
|
||||
getShortcutKey("CtrlOrCmd+Y"),
|
||||
getShortcutKey("CtrlOrCmd+Shift+Z"),
|
||||
]
|
||||
: [getShortcutKey("CtrlOrCmd+Shift+Z")]
|
||||
}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.group")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.ungroup")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
</Column>
|
||||
</Columns>
|
||||
</Section>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { questionCircle } from "../components/icons";
|
||||
|
||||
type HelpIconProps = {
|
||||
title?: string;
|
||||
@ -7,19 +8,8 @@ type HelpIconProps = {
|
||||
onClick?(): void;
|
||||
};
|
||||
|
||||
const ICON = (
|
||||
<svg
|
||||
width="30"
|
||||
height="22"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const HelpIcon = (props: HelpIconProps) => (
|
||||
<label title={`${props.title} — ?`} className="help-icon">
|
||||
<div onClick={props.onClick}>{ICON}</div>
|
||||
<div onClick={props.onClick}>{questionCircle}</div>
|
||||
</label>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
// this is loosely based on the longest hint text
|
||||
$wide-viewport-width: 1000px;
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.picker-container {
|
||||
|
@ -36,7 +36,7 @@ import { LockIcon } from "./LockIcon";
|
||||
import { MobileMenu } from "./MobileMenu";
|
||||
import { PasteChartDialog } from "./PasteChartDialog";
|
||||
import { Section } from "./Section";
|
||||
import { ShortcutsDialog } from "./ShortcutsDialog";
|
||||
import { HelpDialog } from "./HelpDialog";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
@ -61,6 +61,7 @@ interface LayerUIProps {
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => void;
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
viewModeEnabled: boolean;
|
||||
}
|
||||
|
||||
const useOnClickOutside = (
|
||||
@ -299,6 +300,7 @@ const LayerUI = ({
|
||||
isCollaborating,
|
||||
onExportToBackend,
|
||||
renderCustomFooter,
|
||||
viewModeEnabled,
|
||||
}: LayerUIProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@ -358,6 +360,28 @@ const LayerUI = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderViewModeCanvasActions = () => {
|
||||
return (
|
||||
<Section
|
||||
heading="canvasActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
<Island padding={2} style={{ zIndex: 1 }}>
|
||||
<Stack.Col gap={4}>
|
||||
<Stack.Row gap={1} justifyContent="space-between">
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{renderExportDialog()}
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
</Island>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
const renderCanvasActions = () => (
|
||||
<Section
|
||||
heading="canvasActions"
|
||||
@ -448,9 +472,12 @@ const LayerUI = ({
|
||||
gap={4}
|
||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
||||
>
|
||||
{renderCanvasActions()}
|
||||
{viewModeEnabled
|
||||
? renderViewModeCanvasActions()
|
||||
: renderCanvasActions()}
|
||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||
</Stack.Col>
|
||||
{!viewModeEnabled && (
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
<Stack.Col gap={4} align="start">
|
||||
@ -480,6 +507,7 @@ const LayerUI = ({
|
||||
</Stack.Col>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
<UserList
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-right": zenModeEnabled,
|
||||
@ -524,6 +552,20 @@ const LayerUI = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderGitHubCorner = () => {
|
||||
return (
|
||||
<aside
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
||||
{
|
||||
"transition-right": zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<GitHubCorner appearance={appState.appearance} />
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
const renderFooter = () => (
|
||||
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
||||
<div
|
||||
@ -566,10 +608,8 @@ const LayerUI = ({
|
||||
onClose={() => setAppState({ errorMessage: null })}
|
||||
/>
|
||||
)}
|
||||
{appState.showShortcutsDialog && (
|
||||
<ShortcutsDialog
|
||||
onClose={() => setAppState({ showShortcutsDialog: false })}
|
||||
/>
|
||||
{appState.showHelpDialog && (
|
||||
<HelpDialog onClose={() => setAppState({ showHelpDialog: false })} />
|
||||
)}
|
||||
{appState.pasteDialog.shown && (
|
||||
<PasteChartDialog
|
||||
@ -601,25 +641,19 @@ const LayerUI = ({
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="layer-ui__wrapper">
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper", {
|
||||
"disable-pointerEvents": appState.cursorButton === "down",
|
||||
})}
|
||||
>
|
||||
{dialogs}
|
||||
{renderFixedSideContainer()}
|
||||
{renderBottomAppMenu()}
|
||||
{
|
||||
<aside
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
||||
{
|
||||
"transition-right": zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<GitHubCorner appearance={appState.appearance} />
|
||||
</aside>
|
||||
}
|
||||
{renderGitHubCorner()}
|
||||
{renderFooter()}
|
||||
</div>
|
||||
);
|
||||
|
@ -29,6 +29,7 @@ type MobileMenuProps = {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
viewModeEnabled: boolean;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@ -43,8 +44,10 @@ export const MobileMenu = ({
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
}: MobileMenuProps) => (
|
||||
<>
|
||||
viewModeEnabled,
|
||||
}: MobileMenuProps) => {
|
||||
const renderFixedSideContainer = () => {
|
||||
return (
|
||||
<FixedSideContainer side="top">
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
@ -72,6 +75,68 @@ export const MobileMenu = ({
|
||||
</Section>
|
||||
<HintViewer appState={appState} elements={elements} />
|
||||
</FixedSideContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAppToolbar = () => {
|
||||
if (viewModeEnabled) {
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
{actionManager.renderAction("toggleEditMenu")}
|
||||
{actionManager.renderAction("undo")}
|
||||
{actionManager.renderAction("redo")}
|
||||
{actionManager.renderAction(
|
||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||
)}
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCanvasActions = () => {
|
||||
if (viewModeEnabled) {
|
||||
return (
|
||||
<>
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{exportButton}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{exportButton}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
collaboratorCount={appState.collaborators.size}
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<BackgroundPickerAndDarkModeToggle
|
||||
actionManager={actionManager}
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!viewModeEnabled && renderFixedSideContainer()}
|
||||
<div
|
||||
className="App-bottom-bar"
|
||||
style={{
|
||||
@ -85,30 +150,16 @@ export const MobileMenu = ({
|
||||
<Section className="App-mobile-menu" heading="canvasActions">
|
||||
<div className="panelColumn">
|
||||
<Stack.Col gap={4}>
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{exportButton}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
collaboratorCount={appState.collaborators.size}
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
<BackgroundPickerAndDarkModeToggle
|
||||
actionManager={actionManager}
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
{renderCanvasActions()}
|
||||
{renderCustomFooter?.(true)}
|
||||
<fieldset>
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList mobile>
|
||||
{Array.from(appState.collaborators)
|
||||
// Collaborator is either not initialized or is actually the current user.
|
||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
||||
.filter(
|
||||
([_, client]) => Object.keys(client).length !== 0,
|
||||
)
|
||||
.map(([clientId, client]) => (
|
||||
<React.Fragment key={clientId}>
|
||||
{actionManager.renderAction(
|
||||
@ -123,6 +174,7 @@ export const MobileMenu = ({
|
||||
</div>
|
||||
</Section>
|
||||
) : appState.openMenu === "shape" &&
|
||||
!viewModeEnabled &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
@ -134,16 +186,7 @@ export const MobileMenu = ({
|
||||
</Section>
|
||||
) : null}
|
||||
<footer className="App-toolbar">
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
{actionManager.renderAction("toggleEditMenu")}
|
||||
{actionManager.renderAction("undo")}
|
||||
{actionManager.renderAction("redo")}
|
||||
{actionManager.renderAction(
|
||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||
)}
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
</div>
|
||||
{renderAppToolbar()}
|
||||
{appState.scrolledOutside && !appState.openMenu && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
@ -161,3 +204,4 @@ export const MobileMenu = ({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Modal {
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.PasteChartDialog {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import oc from "open-color";
|
||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||
import { ChartType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
@ -86,6 +87,7 @@ export const PasteChartDialog = ({
|
||||
|
||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||
onInsertChart(elements);
|
||||
trackEvent("magic", "chart", chartType);
|
||||
setAppState({
|
||||
currentChartType: chartType,
|
||||
pasteDialog: {
|
||||
|
@ -1,328 +0,0 @@
|
||||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { isDarwin } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./ShortcutsDialog.scss";
|
||||
|
||||
const Columns = (props: { children: React.ReactNode }) => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Column = (props: { children: React.ReactNode }) => (
|
||||
<div style={{ width: "49%" }}>{props.children}</div>
|
||||
);
|
||||
|
||||
const ShortcutIsland = (props: {
|
||||
caption: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="ShortcutsDialog-island">
|
||||
<h3 className="ShortcutsDialog-island-title">{props.caption}</h3>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Shortcut = (props: {
|
||||
label: string;
|
||||
shortcuts: string[];
|
||||
isOr: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="ShorcutsDialog-shortcut">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
margin: "0",
|
||||
padding: "4px 8px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: "0 0 auto",
|
||||
justifyContent: "flex-end",
|
||||
marginInlineStart: "auto",
|
||||
minWidth: "30%",
|
||||
}}
|
||||
>
|
||||
{props.shortcuts.map((shortcut, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ShortcutKey>{shortcut}</ShortcutKey>
|
||||
{props.isOr &&
|
||||
index !== props.shortcuts.length - 1 &&
|
||||
t("shortcutsDialog.or")}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Shortcut.defaultProps = {
|
||||
isOr: true,
|
||||
};
|
||||
|
||||
const ShortcutKey = (props: { children: React.ReactNode }) => (
|
||||
<span className="ShorcutsDialog-key" {...props} />
|
||||
);
|
||||
|
||||
const Footer = () => (
|
||||
<div className="ShortcutsDialog-footer">
|
||||
<a
|
||||
href="https://blog.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("shortcutsDialog.blog")}
|
||||
</a>
|
||||
<a
|
||||
href="https://howto.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("shortcutsDialog.howto")}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/excalidraw/excalidraw/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("shortcutsDialog.github")}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog onCloseRequest={handleClose} title={t("shortcutsDialog.title")}>
|
||||
<Columns>
|
||||
<Column>
|
||||
<ShortcutIsland caption={t("shortcutsDialog.shapes")}>
|
||||
<Shortcut label={t("toolBar.selection")} shortcuts={["V", "1"]} />
|
||||
<Shortcut label={t("toolBar.rectangle")} shortcuts={["R", "2"]} />
|
||||
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
|
||||
<Shortcut label={t("toolBar.ellipse")} shortcuts={["E", "4"]} />
|
||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.draw")}
|
||||
shortcuts={["Shift+P", "7"]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.textNewLine")}
|
||||
shortcuts={[
|
||||
getShortcutKey("Enter"),
|
||||
getShortcutKey("Shift+Enter"),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.textFinish")}
|
||||
shortcuts={[
|
||||
getShortcutKey("Esc"),
|
||||
getShortcutKey("CtrlOrCmd+Enter"),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.curvedArrow")}
|
||||
shortcuts={[
|
||||
"A",
|
||||
t("shortcutsDialog.click"),
|
||||
t("shortcutsDialog.click"),
|
||||
t("shortcutsDialog.click"),
|
||||
]}
|
||||
isOr={false}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.curvedLine")}
|
||||
shortcuts={[
|
||||
"L",
|
||||
t("shortcutsDialog.click"),
|
||||
t("shortcutsDialog.click"),
|
||||
t("shortcutsDialog.click"),
|
||||
]}
|
||||
isOr={false}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.preventBinding")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
<ShortcutIsland caption={t("shortcutsDialog.view")}>
|
||||
<Shortcut
|
||||
label={t("buttons.zoomIn")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd++")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.zoomOut")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.resetZoom")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.zoomToFit")}
|
||||
shortcuts={["Shift+1"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("shortcutsDialog.zoomToSelection")}
|
||||
shortcuts={["Shift+2"]}
|
||||
/>
|
||||
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
|
||||
<Shortcut
|
||||
label={t("buttons.zenMode")}
|
||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.gridMode")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShortcutIsland caption={t("shortcutsDialog.editor")}>
|
||||
<Shortcut
|
||||
label={t("labels.selectAll")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.multiSelect")}
|
||||
shortcuts={[
|
||||
getShortcutKey(`Shift+${t("shortcutsDialog.click")}`),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.moveCanvas")}
|
||||
shortcuts={[
|
||||
getShortcutKey(`Space+${t("shortcutsDialog.drag")}`),
|
||||
getShortcutKey(`Wheel+${t("shortcutsDialog.drag")}`),
|
||||
]}
|
||||
isOr={true}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.cut")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copy")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.paste")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.pasteStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.delete")}
|
||||
shortcuts={[getShortcutKey("Del")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.sendToBack")}
|
||||
shortcuts={[
|
||||
isDarwin
|
||||
? getShortcutKey("CtrlOrCmd+Alt+[")
|
||||
: getShortcutKey("CtrlOrCmd+Shift+["),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.bringToFront")}
|
||||
shortcuts={[
|
||||
isDarwin
|
||||
? getShortcutKey("CtrlOrCmd+Alt+]")
|
||||
: getShortcutKey("CtrlOrCmd+Shift+]"),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.sendBackward")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.bringForward")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignTop")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignBottom")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignLeft")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.alignRight")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.duplicateSelection")}
|
||||
shortcuts={[
|
||||
getShortcutKey("CtrlOrCmd+D"),
|
||||
getShortcutKey(`Alt+${t("shortcutsDialog.drag")}`),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.undo")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.redo")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.group")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.ungroup")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
</Column>
|
||||
</Columns>
|
||||
<Footer />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.Stats {
|
||||
position: fixed;
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables.scss";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.TextInput {
|
||||
|
32
src/components/Toast.scss
Normal file
32
src/components/Toast.scss
Normal file
@ -0,0 +1,32 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Toast {
|
||||
animation: fade-in 0.5s;
|
||||
background-color: var(--button-gray-1);
|
||||
border-radius: 4px;
|
||||
bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
left: 50%;
|
||||
margin-left: -150px;
|
||||
padding: 4px 0;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.Toast__message {
|
||||
color: var(--popup-text-color);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
34
src/components/Toast.tsx
Normal file
34
src/components/Toast.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { TOAST_TIMEOUT } from "../constants";
|
||||
import "./Toast.scss";
|
||||
|
||||
export const Toast = ({
|
||||
message,
|
||||
clearToast,
|
||||
}: {
|
||||
message: string;
|
||||
clearToast: () => void;
|
||||
}) => {
|
||||
const timerRef = useRef<number>(0);
|
||||
|
||||
const scheduleTimeout = useCallback(
|
||||
() =>
|
||||
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
|
||||
[clearToast],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
scheduleTimeout();
|
||||
return () => clearTimeout(timerRef.current);
|
||||
}, [scheduleTimeout, message]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="Toast"
|
||||
onMouseEnter={() => clearTimeout(timerRef?.current)}
|
||||
onMouseLeave={scheduleTimeout}
|
||||
>
|
||||
<p className="Toast__message">{message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
@import "open-color/open-color.scss";
|
||||
@import "../css/variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.ToolIcon {
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
.excalidraw {
|
||||
.Tooltip {
|
||||
position: relative;
|
||||
@ -48,15 +48,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// the following 3 rules ensure that the tooltip doesn't show (nor affect
|
||||
// the cursor) when you drag over when you draw on canvas, but at the same
|
||||
// time it still works when clicking on the link/shield
|
||||
|
||||
body:active & .Tooltip:not(:hover) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body:not(:active) & .Tooltip:hover .Tooltip__label {
|
||||
.Tooltip:hover .Tooltip__label {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
@ -89,3 +89,7 @@ export const STORAGE_KEYS = {
|
||||
export const TAP_TWICE_TIMEOUT = 300;
|
||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||
export const TITLE_TIMEOUT = 10000;
|
||||
export const TOAST_TIMEOUT = 5000;
|
||||
export const VERSION_TIMEOUT = 30000;
|
||||
|
||||
export const ZOOM_STEP = 0.1;
|
||||
|
42
src/createInverseContext.tsx
Normal file
42
src/createInverseContext.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
|
||||
export const createInverseContext = <T extends unknown = null>(
|
||||
initialValue: T,
|
||||
) => {
|
||||
const Context = React.createContext(initialValue) as React.Context<T> & {
|
||||
_updateProviderValue?: (value: T) => void;
|
||||
};
|
||||
|
||||
class InverseConsumer extends React.Component {
|
||||
state = { value: initialValue };
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
Context._updateProviderValue = (value: T) => this.setState({ value });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Context.Provider value={this.state.value}>
|
||||
{this.props.children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InverseProvider extends React.Component<{ value: T }> {
|
||||
componentDidMount() {
|
||||
Context._updateProviderValue?.(this.props.value);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
Context._updateProviderValue?.(this.props.value);
|
||||
}
|
||||
render() {
|
||||
return <Context.Consumer>{() => this.props.children}</Context.Consumer>;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Context,
|
||||
Consumer: InverseConsumer,
|
||||
Provider: InverseProvider,
|
||||
};
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
@import "./_variables";
|
||||
@import "./variables.module";
|
||||
@import "./theme";
|
||||
|
||||
.excalidraw {
|
||||
@ -13,7 +13,7 @@
|
||||
a {
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: $oc-blue-7; /* OC Blue 7 */
|
||||
color: var(--link-color);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@ -282,7 +282,7 @@
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.App-menu_top > * {
|
||||
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@ -323,7 +323,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.App-menu_bottom > * {
|
||||
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@ -431,6 +431,7 @@
|
||||
cursor: pointer;
|
||||
fill: $oc-gray-6;
|
||||
bottom: 14px;
|
||||
width: 1.5rem;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: 14px;
|
||||
@ -491,6 +492,13 @@
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&.excalidraw--view-mode {
|
||||
.App-menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.App-bottom-bar,
|
||||
.FixedSideContainer,
|
||||
|
@ -32,6 +32,7 @@
|
||||
--popup-text-color: #{$oc-black};
|
||||
--popup-text-inverted-color: #{$oc-white};
|
||||
--dialog-border: #{$oc-gray-6};
|
||||
--link-color: #{$oc-blue-7};
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
|
@ -2,3 +2,7 @@
|
||||
|
||||
// keep up to date with is-mobile.tsx
|
||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||
|
||||
:export {
|
||||
isMobileQuery: unquote($is-mobile-query);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { fileSave } from "browser-nativefs";
|
||||
import { fileSave } from "browser-fs-access";
|
||||
import {
|
||||
copyCanvasToClipboardAsPng,
|
||||
copyTextToSystemClipboard,
|
||||
@ -36,7 +36,7 @@ export const exportCanvas = async (
|
||||
},
|
||||
) => {
|
||||
if (elements.length === 0) {
|
||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||
}
|
||||
if (type === "svg" || type === "clipboard-svg") {
|
||||
const tempSvg = exportToSvg(elements, {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { fileOpen, fileSave } from "browser-nativefs";
|
||||
import { fileOpen, fileSave } from "browser-fs-access";
|
||||
import { cleanAppStateForExport } from "../appState";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { clearElementsForExport } from "../element";
|
||||
|
@ -34,7 +34,6 @@ export {
|
||||
export {
|
||||
resizeTest,
|
||||
getCursorForResizingElement,
|
||||
normalizeTransformHandleType,
|
||||
getElementWithTransformHandleType,
|
||||
getTransformHandleTypeFromCoords,
|
||||
} from "./resizeTest";
|
||||
|
@ -4,7 +4,6 @@ import { rescalePoints } from "../points";
|
||||
import {
|
||||
rotate,
|
||||
adjustXYWithRotation,
|
||||
getFlipAdjustment,
|
||||
centerPoint,
|
||||
rotatePoint,
|
||||
} from "../math";
|
||||
@ -13,21 +12,16 @@ import {
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawElement,
|
||||
} from "./types";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getCommonBounds,
|
||||
getResizedElementAbsoluteCoords,
|
||||
} from "./bounds";
|
||||
import { isGenericElement, isLinearElement, isTextElement } from "./typeChecks";
|
||||
import { isLinearElement, isTextElement } from "./typeChecks";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import {
|
||||
getCursorForResizingElement,
|
||||
normalizeTransformHandleType,
|
||||
} from "./resizeTest";
|
||||
import { getCursorForResizingElement } from "./resizeTest";
|
||||
import { measureText, getFontString } from "../utils";
|
||||
import { updateBoundElements } from "./binding";
|
||||
import {
|
||||
@ -49,7 +43,6 @@ const normalizeAngle = (angle: number): number => {
|
||||
export const transformElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
setTransformHandle: (nextTransformHandle: MaybeTransformHandleType) => void,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
resizeArrowDirection: "origin" | "end",
|
||||
isRotateWithDiscreteAngle: boolean,
|
||||
@ -101,8 +94,7 @@ export const transformElements = (
|
||||
);
|
||||
updateBoundElements(element);
|
||||
} else if (transformHandleType) {
|
||||
if (isGenericElement(element)) {
|
||||
resizeSingleGenericElement(
|
||||
resizeSingleElement(
|
||||
pointerDownState.originalElements.get(element.id) as typeof element,
|
||||
shouldKeepSidesRatio,
|
||||
element,
|
||||
@ -111,26 +103,6 @@ export const transformElements = (
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
} else {
|
||||
const keepSquareAspectRatio = shouldKeepSidesRatio;
|
||||
resizeSingleNonGenericElement(
|
||||
element,
|
||||
transformHandleType,
|
||||
isResizeCenterPoint,
|
||||
keepSquareAspectRatio,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
setTransformHandle(
|
||||
normalizeTransformHandleType(element, transformHandleType),
|
||||
);
|
||||
if (element.width < 0) {
|
||||
mutateElement(element, { width: -element.width });
|
||||
}
|
||||
if (element.height < 0) {
|
||||
mutateElement(element, { height: -element.height });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update cursor
|
||||
@ -414,8 +386,8 @@ const resizeSingleTextElement = (
|
||||
}
|
||||
};
|
||||
|
||||
const resizeSingleGenericElement = (
|
||||
stateAtResizeStart: NonDeleted<ExcalidrawGenericElement>,
|
||||
const resizeSingleElement = (
|
||||
stateAtResizeStart: NonDeletedExcalidrawElement,
|
||||
shouldKeepSidesRatio: boolean,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
transformHandleDirection: TransformHandleDirection,
|
||||
@ -423,251 +395,184 @@ const resizeSingleGenericElement = (
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(stateAtResizeStart);
|
||||
// Gets bounds corners
|
||||
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
|
||||
stateAtResizeStart,
|
||||
stateAtResizeStart.width,
|
||||
stateAtResizeStart.height,
|
||||
);
|
||||
const startTopLeft: Point = [x1, y1];
|
||||
const startBottomRight: Point = [x2, y2];
|
||||
const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
|
||||
|
||||
// Calculate new dimensions based on cursor position
|
||||
let newWidth = stateAtResizeStart.width;
|
||||
let newHeight = stateAtResizeStart.height;
|
||||
const rotatedPointer = rotatePoint(
|
||||
[pointerX, pointerY],
|
||||
startCenter,
|
||||
-stateAtResizeStart.angle,
|
||||
);
|
||||
|
||||
//Get bounds corners rendered on screen
|
||||
const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
|
||||
element,
|
||||
element.width,
|
||||
element.height,
|
||||
);
|
||||
const boundsCurrentWidth = esx2 - esx1;
|
||||
const boundsCurrentHeight = esy2 - esy1;
|
||||
|
||||
// It's important we set the initial scale value based on the width and height at resize start,
|
||||
// otherwise previous dimensions affected by modifiers will be taken into account.
|
||||
const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
|
||||
const atStartBoundsHeight = startBottomRight[1] - startTopLeft[1];
|
||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
newWidth = rotatedPointer[0] - startTopLeft[0];
|
||||
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
||||
}
|
||||
if (transformHandleDirection.includes("s")) {
|
||||
newHeight = rotatedPointer[1] - startTopLeft[1];
|
||||
scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
|
||||
}
|
||||
if (transformHandleDirection.includes("w")) {
|
||||
newWidth = startBottomRight[0] - rotatedPointer[0];
|
||||
scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
|
||||
}
|
||||
if (transformHandleDirection.includes("n")) {
|
||||
newHeight = startBottomRight[1] - rotatedPointer[1];
|
||||
scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
|
||||
}
|
||||
// Linear elements dimensions differ from bounds dimensions
|
||||
const eleInitialWidth = stateAtResizeStart.width;
|
||||
const eleInitialHeight = stateAtResizeStart.height;
|
||||
// We have to use dimensions of element on screen, otherwise the scaling of the
|
||||
// dimensions won't match the cursor for linear elements.
|
||||
let eleNewWidth = element.width * scaleX;
|
||||
let eleNewHeight = element.height * scaleY;
|
||||
|
||||
// adjust dimensions for resizing from center
|
||||
if (isResizeFromCenter) {
|
||||
newWidth = 2 * newWidth - stateAtResizeStart.width;
|
||||
newHeight = 2 * newHeight - stateAtResizeStart.height;
|
||||
eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
|
||||
eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
|
||||
}
|
||||
|
||||
// adjust dimensions to keep sides ratio
|
||||
if (shouldKeepSidesRatio) {
|
||||
const widthRatio = Math.abs(newWidth) / stateAtResizeStart.width;
|
||||
const heightRatio = Math.abs(newHeight) / stateAtResizeStart.height;
|
||||
const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
|
||||
const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
|
||||
if (transformHandleDirection.length === 1) {
|
||||
newHeight *= widthRatio;
|
||||
newWidth *= heightRatio;
|
||||
eleNewHeight *= widthRatio;
|
||||
eleNewWidth *= heightRatio;
|
||||
}
|
||||
if (transformHandleDirection.length === 2) {
|
||||
const ratio = Math.max(widthRatio, heightRatio);
|
||||
newWidth = stateAtResizeStart.width * ratio * Math.sign(newWidth);
|
||||
newHeight = stateAtResizeStart.height * ratio * Math.sign(newHeight);
|
||||
eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth);
|
||||
eleNewHeight = eleInitialHeight * ratio * Math.sign(eleNewHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
newBoundsX1,
|
||||
newBoundsY1,
|
||||
newBoundsX2,
|
||||
newBoundsY2,
|
||||
] = getResizedElementAbsoluteCoords(
|
||||
stateAtResizeStart,
|
||||
eleNewWidth,
|
||||
eleNewHeight,
|
||||
);
|
||||
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
||||
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
||||
|
||||
// Calculate new topLeft based on fixed corner during resize
|
||||
let newTopLeft = startTopLeft as [number, number];
|
||||
let newTopLeft = [...startTopLeft] as [number, number];
|
||||
if (["n", "w", "nw"].includes(transformHandleDirection)) {
|
||||
newTopLeft = [
|
||||
startBottomRight[0] - Math.abs(newWidth),
|
||||
startBottomRight[1] - Math.abs(newHeight),
|
||||
startBottomRight[0] - Math.abs(newBoundsWidth),
|
||||
startBottomRight[1] - Math.abs(newBoundsHeight),
|
||||
];
|
||||
}
|
||||
if (transformHandleDirection === "ne") {
|
||||
const bottomLeft = [
|
||||
stateAtResizeStart.x,
|
||||
stateAtResizeStart.y + stateAtResizeStart.height,
|
||||
];
|
||||
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newHeight)];
|
||||
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
|
||||
}
|
||||
if (transformHandleDirection === "sw") {
|
||||
const topRight = [
|
||||
stateAtResizeStart.x + stateAtResizeStart.width,
|
||||
stateAtResizeStart.y,
|
||||
];
|
||||
newTopLeft = [topRight[0] - Math.abs(newWidth), topRight[1]];
|
||||
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||
newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
|
||||
}
|
||||
|
||||
// Keeps opposite handle fixed during resize
|
||||
if (shouldKeepSidesRatio) {
|
||||
if (["s", "n"].includes(transformHandleDirection)) {
|
||||
newTopLeft[0] = startCenter[0] - newWidth / 2;
|
||||
newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
|
||||
}
|
||||
if (["e", "w"].includes(transformHandleDirection)) {
|
||||
newTopLeft[1] = startCenter[1] - newHeight / 2;
|
||||
newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Flip horizontally
|
||||
if (newWidth < 0) {
|
||||
if (eleNewWidth < 0) {
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
newTopLeft[0] -= Math.abs(newWidth);
|
||||
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
||||
}
|
||||
if (transformHandleDirection.includes("w")) {
|
||||
newTopLeft[0] += Math.abs(newWidth);
|
||||
newTopLeft[0] += Math.abs(newBoundsWidth);
|
||||
}
|
||||
}
|
||||
// Flip vertically
|
||||
if (newHeight < 0) {
|
||||
if (eleNewHeight < 0) {
|
||||
if (transformHandleDirection.includes("s")) {
|
||||
newTopLeft[1] -= Math.abs(newHeight);
|
||||
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
||||
}
|
||||
if (transformHandleDirection.includes("n")) {
|
||||
newTopLeft[1] += Math.abs(newHeight);
|
||||
newTopLeft[1] += Math.abs(newBoundsHeight);
|
||||
}
|
||||
}
|
||||
|
||||
if (isResizeFromCenter) {
|
||||
newTopLeft[0] = startCenter[0] - Math.abs(newWidth) / 2;
|
||||
newTopLeft[1] = startCenter[1] - Math.abs(newHeight) / 2;
|
||||
newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2;
|
||||
newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2;
|
||||
}
|
||||
|
||||
// adjust topLeft to new rotation point
|
||||
const angle = stateAtResizeStart.angle;
|
||||
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
|
||||
const newCenter: Point = [
|
||||
newTopLeft[0] + Math.abs(newWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(newHeight) / 2,
|
||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||
];
|
||||
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
||||
|
||||
// Readjust points for linear elements
|
||||
const rescaledPoints = rescalePointsInElement(
|
||||
stateAtResizeStart,
|
||||
eleNewWidth,
|
||||
eleNewHeight,
|
||||
);
|
||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||
// So we need to readjust (x,y) to be where the first point should be
|
||||
const newOrigin = [...newTopLeft];
|
||||
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
|
||||
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
|
||||
|
||||
const resizedElement = {
|
||||
width: Math.abs(newWidth),
|
||||
height: Math.abs(newHeight),
|
||||
x: newTopLeft[0],
|
||||
y: newTopLeft[1],
|
||||
width: Math.abs(eleNewWidth),
|
||||
height: Math.abs(eleNewHeight),
|
||||
x: newOrigin[0],
|
||||
y: newOrigin[1],
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
if (
|
||||
resizedElement.width !== 0 &&
|
||||
resizedElement.height !== 0 &&
|
||||
Number.isFinite(resizedElement.x) &&
|
||||
Number.isFinite(resizedElement.y)
|
||||
) {
|
||||
updateBoundElements(element, {
|
||||
newSize: { width: resizedElement.width, height: resizedElement.height },
|
||||
});
|
||||
mutateElement(element, resizedElement);
|
||||
};
|
||||
|
||||
const resizeSingleNonGenericElement = (
|
||||
element: NonDeleted<Exclude<ExcalidrawElement, ExcalidrawGenericElement>>,
|
||||
transformHandleType: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||
isResizeFromCenter: boolean,
|
||||
keepSquareAspectRatio: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
|
||||
// rotation pointer with reverse angle
|
||||
const [rotatedX, rotatedY] = rotate(
|
||||
pointerX,
|
||||
pointerY,
|
||||
cx,
|
||||
cy,
|
||||
-element.angle,
|
||||
);
|
||||
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
if (
|
||||
transformHandleType === "e" ||
|
||||
transformHandleType === "ne" ||
|
||||
transformHandleType === "se"
|
||||
) {
|
||||
scaleX = (rotatedX - x1) / (x2 - x1);
|
||||
}
|
||||
if (
|
||||
transformHandleType === "s" ||
|
||||
transformHandleType === "sw" ||
|
||||
transformHandleType === "se"
|
||||
) {
|
||||
scaleY = (rotatedY - y1) / (y2 - y1);
|
||||
}
|
||||
if (
|
||||
transformHandleType === "w" ||
|
||||
transformHandleType === "nw" ||
|
||||
transformHandleType === "sw"
|
||||
) {
|
||||
scaleX = (x2 - rotatedX) / (x2 - x1);
|
||||
}
|
||||
if (
|
||||
transformHandleType === "n" ||
|
||||
transformHandleType === "nw" ||
|
||||
transformHandleType === "ne"
|
||||
) {
|
||||
scaleY = (y2 - rotatedY) / (y2 - y1);
|
||||
}
|
||||
let nextWidth = element.width * scaleX;
|
||||
let nextHeight = element.height * scaleY;
|
||||
if (keepSquareAspectRatio) {
|
||||
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
|
||||
}
|
||||
|
||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||
element,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
);
|
||||
const deltaX1 = (x1 - nextX1) / 2;
|
||||
const deltaY1 = (y1 - nextY1) / 2;
|
||||
const deltaX2 = (x2 - nextX2) / 2;
|
||||
const deltaY2 = (y2 - nextY2) / 2;
|
||||
|
||||
const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
|
||||
|
||||
updateBoundElements(element, {
|
||||
newSize: { width: nextWidth, height: nextHeight },
|
||||
});
|
||||
const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
|
||||
{
|
||||
...element,
|
||||
...rescaledPoints,
|
||||
},
|
||||
Math.abs(nextWidth),
|
||||
Math.abs(nextHeight),
|
||||
);
|
||||
const [flipDiffX, flipDiffY] = getFlipAdjustment(
|
||||
transformHandleType,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
nextX1,
|
||||
nextY1,
|
||||
nextX2,
|
||||
nextY2,
|
||||
finalX1,
|
||||
finalY1,
|
||||
finalX2,
|
||||
finalY2,
|
||||
isLinearElement(element),
|
||||
element.angle,
|
||||
);
|
||||
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
||||
getSidesForTransformHandle(transformHandleType, isResizeFromCenter),
|
||||
element.x - flipDiffX,
|
||||
element.y - flipDiffY,
|
||||
element.angle,
|
||||
deltaX1,
|
||||
deltaY1,
|
||||
deltaX2,
|
||||
deltaY2,
|
||||
);
|
||||
|
||||
if (
|
||||
nextWidth !== 0 &&
|
||||
nextHeight !== 0 &&
|
||||
Number.isFinite(nextElementX) &&
|
||||
Number.isFinite(nextElementY)
|
||||
) {
|
||||
mutateElement(element, {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
x: nextElementX,
|
||||
y: nextElementY,
|
||||
...rescaledPoints,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -173,57 +173,3 @@ export const getCursorForResizingElement = (resizingElement: {
|
||||
|
||||
return cursor ? `${cursor}-resize` : "";
|
||||
};
|
||||
|
||||
export const normalizeTransformHandleType = (
|
||||
element: ExcalidrawElement,
|
||||
transformHandleType: TransformHandleType,
|
||||
): TransformHandleType => {
|
||||
if (element.width >= 0 && element.height >= 0) {
|
||||
return transformHandleType;
|
||||
}
|
||||
|
||||
if (element.width < 0 && element.height < 0) {
|
||||
switch (transformHandleType) {
|
||||
case "nw":
|
||||
return "se";
|
||||
case "ne":
|
||||
return "sw";
|
||||
case "se":
|
||||
return "nw";
|
||||
case "sw":
|
||||
return "ne";
|
||||
}
|
||||
} else if (element.width < 0) {
|
||||
switch (transformHandleType) {
|
||||
case "nw":
|
||||
return "ne";
|
||||
case "ne":
|
||||
return "nw";
|
||||
case "se":
|
||||
return "sw";
|
||||
case "sw":
|
||||
return "se";
|
||||
case "e":
|
||||
return "w";
|
||||
case "w":
|
||||
return "e";
|
||||
}
|
||||
} else {
|
||||
switch (transformHandleType) {
|
||||
case "nw":
|
||||
return "sw";
|
||||
case "ne":
|
||||
return "se";
|
||||
case "se":
|
||||
return "ne";
|
||||
case "sw":
|
||||
return "nw";
|
||||
case "n":
|
||||
return "s";
|
||||
case "s":
|
||||
return "n";
|
||||
}
|
||||
}
|
||||
|
||||
return transformHandleType;
|
||||
};
|
||||
|
@ -7,7 +7,8 @@ export const showSelectedShapeActions = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
) =>
|
||||
Boolean(
|
||||
appState.editingElement ||
|
||||
!appState.viewModeEnabled &&
|
||||
(appState.editingElement ||
|
||||
getSelectedElements(elements, appState).length ||
|
||||
appState.elementType !== "selection",
|
||||
appState.elementType !== "selection"),
|
||||
);
|
||||
|
@ -6,10 +6,11 @@ import { APP_NAME, ENV, EVENT } from "../../constants";
|
||||
import { ImportedDataState } from "../../data/types";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import {
|
||||
getElementMap,
|
||||
getSceneVersion,
|
||||
getSyncableElements,
|
||||
} from "../../packages/excalidraw/index";
|
||||
import { AppState, Collaborator, Gesture } from "../../types";
|
||||
import { Collaborator, Gesture } from "../../types";
|
||||
import { resolvablePromise, withBatchedUpdates } from "../../utils";
|
||||
import {
|
||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||
@ -31,6 +32,7 @@ import {
|
||||
} from "../data/localStorage";
|
||||
import Portal from "./Portal";
|
||||
import RoomDialog from "./RoomDialog";
|
||||
import { createInverseContext } from "../../createInverseContext";
|
||||
|
||||
interface CollabState {
|
||||
isCollaborating: boolean;
|
||||
@ -56,17 +58,21 @@ type ReconciledElements = readonly ExcalidrawElement[] & {
|
||||
};
|
||||
|
||||
interface Props {
|
||||
children: (collab: CollabAPI) => React.ReactNode;
|
||||
// NOTE not type-safe because the refObject may in fact not be initialized
|
||||
// with ExcalidrawImperativeAPI yet
|
||||
excalidrawRef: React.MutableRefObject<ExcalidrawImperativeAPI>;
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}
|
||||
|
||||
const {
|
||||
Context: CollabContext,
|
||||
Consumer: CollabContextConsumer,
|
||||
Provider: CollabContextProvider,
|
||||
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
|
||||
|
||||
export { CollabContext, CollabContextConsumer };
|
||||
|
||||
class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
portal: Portal;
|
||||
excalidrawAPI: Props["excalidrawAPI"];
|
||||
private socketInitializationTimer?: NodeJS.Timeout;
|
||||
private excalidrawRef: Props["excalidrawRef"];
|
||||
excalidrawAppState?: AppState;
|
||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||
private collaborators = new Map<string, Collaborator>();
|
||||
|
||||
@ -80,7 +86,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
activeRoomLink: "",
|
||||
};
|
||||
this.portal = new Portal(this);
|
||||
this.excalidrawRef = props.excalidrawRef;
|
||||
this.excalidrawAPI = props.excalidrawAPI;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -142,7 +148,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
saveCollabRoomToFirebase = async (
|
||||
syncableElements: ExcalidrawElement[] = getSyncableElements(
|
||||
this.excalidrawRef.current!.getSceneElementsIncludingDeleted(),
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
),
|
||||
) => {
|
||||
try {
|
||||
@ -154,13 +160,13 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
openPortal = async () => {
|
||||
window.history.pushState({}, APP_NAME, await generateCollaborationLink());
|
||||
const elements = this.excalidrawRef.current!.getSceneElements();
|
||||
const elements = this.excalidrawAPI.getSceneElements();
|
||||
// remove deleted elements from elements array & history to ensure we don't
|
||||
// expose potentially sensitive user data in case user manually deletes
|
||||
// existing elements (or clears scene), which would otherwise be persisted
|
||||
// to database even if deleted before creating the room.
|
||||
this.excalidrawRef.current!.history.clear();
|
||||
this.excalidrawRef.current!.updateScene({
|
||||
this.excalidrawAPI.history.clear();
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
commitToHistory: true,
|
||||
});
|
||||
@ -175,7 +181,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
private destroySocketClient = () => {
|
||||
this.collaborators = new Map();
|
||||
this.excalidrawRef.current!.updateScene({
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: this.collaborators,
|
||||
});
|
||||
this.setState({
|
||||
@ -265,7 +271,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
user.selectedElementIds = selectedElementIds;
|
||||
user.username = username;
|
||||
collaborators.set(socketId, user);
|
||||
this.excalidrawRef.current!.updateScene({
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators,
|
||||
});
|
||||
break;
|
||||
@ -300,7 +306,55 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
private reconcileElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): ReconciledElements => {
|
||||
const newElements = this.portal.reconcileElements(elements);
|
||||
const currentElements = this.getSceneElementsIncludingDeleted();
|
||||
// create a map of ids so we don't have to iterate
|
||||
// over the array more than once.
|
||||
const localElementMap = getElementMap(currentElements);
|
||||
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
|
||||
// Reconcile
|
||||
const newElements: readonly ExcalidrawElement[] = elements
|
||||
.reduce((elements, element) => {
|
||||
// if the remote element references one that's currently
|
||||
// edited on local, skip it (it'll be added in the next step)
|
||||
if (
|
||||
element.id === appState.editingElement?.id ||
|
||||
element.id === appState.resizingElement?.id ||
|
||||
element.id === appState.draggingElement?.id
|
||||
) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version > element.version
|
||||
) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
delete localElementMap[element.id];
|
||||
} else if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version === element.version &&
|
||||
localElementMap[element.id].versionNonce !== element.versionNonce
|
||||
) {
|
||||
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
|
||||
if (localElementMap[element.id].versionNonce < element.versionNonce) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
} else {
|
||||
// it should be highly unlikely that the two versionNonces are the same. if we are
|
||||
// really worried about this, we can replace the versionNonce with the socket id.
|
||||
elements.push(element);
|
||||
}
|
||||
delete localElementMap[element.id];
|
||||
} else {
|
||||
elements.push(element);
|
||||
delete localElementMap[element.id];
|
||||
}
|
||||
|
||||
return elements;
|
||||
}, [] as Mutable<typeof elements>)
|
||||
// add local elements that weren't deleted or on remote
|
||||
.concat(...Object.values(localElementMap));
|
||||
|
||||
// Avoid broadcasting to the rest of the collaborators the scene
|
||||
// we just received!
|
||||
@ -319,10 +373,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
}: { init?: boolean; initFromSnapshot?: boolean } = {},
|
||||
) => {
|
||||
if (init || initFromSnapshot) {
|
||||
this.excalidrawRef.current!.setScrollToCenter(elements);
|
||||
this.excalidrawAPI.setScrollToCenter(elements);
|
||||
}
|
||||
|
||||
this.excalidrawRef.current!.updateScene({
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
commitToHistory: !!init,
|
||||
});
|
||||
@ -331,7 +385,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
// when we receive any messages from another peer. This UX can be pretty rough -- if you
|
||||
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
|
||||
// right now we think this is the right tradeoff.
|
||||
this.excalidrawRef.current!.history.clear();
|
||||
this.excalidrawAPI.history.clear();
|
||||
};
|
||||
|
||||
setCollaborators(sockets: string[]) {
|
||||
@ -347,7 +401,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
}
|
||||
this.collaborators = collaborators;
|
||||
this.excalidrawRef.current!.updateScene({ collaborators });
|
||||
this.excalidrawAPI.updateScene({ collaborators });
|
||||
});
|
||||
}
|
||||
|
||||
@ -360,7 +414,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
};
|
||||
|
||||
public getSceneElementsIncludingDeleted = () => {
|
||||
return this.excalidrawRef.current!.getSceneElementsIncludingDeleted();
|
||||
return this.excalidrawAPI.getSceneElementsIncludingDeleted();
|
||||
};
|
||||
|
||||
onPointerUpdate = (payload: {
|
||||
@ -373,11 +427,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.portal.broadcastMouseLocation(payload);
|
||||
};
|
||||
|
||||
broadcastElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
state: AppState,
|
||||
) => {
|
||||
this.excalidrawAppState = state;
|
||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
if (
|
||||
getSceneVersion(elements) >
|
||||
this.getLastBroadcastedOrReceivedSceneVersion()
|
||||
@ -396,7 +446,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.portal.broadcastScene(
|
||||
SCENE.UPDATE,
|
||||
getSyncableElements(
|
||||
this.excalidrawRef.current!.getSceneElementsIncludingDeleted(),
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
),
|
||||
true,
|
||||
);
|
||||
@ -425,8 +475,23 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
});
|
||||
};
|
||||
|
||||
/** PRIVATE. Use `this.getContextValue()` instead. */
|
||||
private contextValue: CollabAPI | null = null;
|
||||
|
||||
/** Getter of context value. Returned object is stable. */
|
||||
getContextValue = (): CollabAPI => {
|
||||
this.contextValue = this.contextValue || ({} as CollabAPI);
|
||||
|
||||
this.contextValue.isCollaborating = this.state.isCollaborating;
|
||||
this.contextValue.username = this.state.username;
|
||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
||||
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
|
||||
this.contextValue.broadcastElements = this.broadcastElements;
|
||||
return this.contextValue;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
|
||||
|
||||
return (
|
||||
@ -450,14 +515,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
onClose={() => this.setState({ errorMessage: "" })}
|
||||
/>
|
||||
)}
|
||||
{children({
|
||||
isCollaborating: this.state.isCollaborating,
|
||||
username: this.state.username,
|
||||
onPointerUpdate: this.onPointerUpdate,
|
||||
initializeSocketClient: this.initializeSocketClient,
|
||||
onCollabButtonClick: this.onCollabButtonClick,
|
||||
broadcastElements: this.broadcastElements,
|
||||
})}
|
||||
<CollabContextProvider
|
||||
value={{
|
||||
api: this.getContextValue(),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -6,23 +6,20 @@ import {
|
||||
|
||||
import CollabWrapper from "./CollabWrapper";
|
||||
|
||||
import {
|
||||
getElementMap,
|
||||
getSyncableElements,
|
||||
} from "../../packages/excalidraw/index";
|
||||
import { getSyncableElements } from "../../packages/excalidraw/index";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import { BROADCAST, SCENE } from "../app_constants";
|
||||
|
||||
class Portal {
|
||||
app: CollabWrapper;
|
||||
collab: CollabWrapper;
|
||||
socket: SocketIOClient.Socket | null = null;
|
||||
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
||||
roomId: string | null = null;
|
||||
roomKey: string | null = null;
|
||||
broadcastedElementVersions: Map<string, number> = new Map();
|
||||
|
||||
constructor(app: CollabWrapper) {
|
||||
this.app = app;
|
||||
constructor(collab: CollabWrapper) {
|
||||
this.collab = collab;
|
||||
}
|
||||
|
||||
open(socket: SocketIOClient.Socket, id: string, key: string) {
|
||||
@ -30,7 +27,7 @@ class Portal {
|
||||
this.roomId = id;
|
||||
this.roomKey = key;
|
||||
|
||||
// Initialize socket listeners (moving from App)
|
||||
// Initialize socket listeners
|
||||
this.socket.on("init-room", () => {
|
||||
if (this.socket) {
|
||||
this.socket.emit("join-room", this.roomId);
|
||||
@ -39,12 +36,12 @@ class Portal {
|
||||
this.socket.on("new-user", async (_socketId: string) => {
|
||||
this.broadcastScene(
|
||||
SCENE.INIT,
|
||||
getSyncableElements(this.app.getSceneElementsIncludingDeleted()),
|
||||
getSyncableElements(this.collab.getSceneElementsIncludingDeleted()),
|
||||
/* syncAll */ true,
|
||||
);
|
||||
});
|
||||
this.socket.on("room-user-change", (clients: string[]) => {
|
||||
this.app.setCollaborators(clients);
|
||||
this.collab.setCollaborators(clients);
|
||||
});
|
||||
}
|
||||
|
||||
@ -125,10 +122,10 @@ class Portal {
|
||||
data as SocketUpdateData,
|
||||
);
|
||||
|
||||
if (syncAll && this.app.state.isCollaborating) {
|
||||
if (syncAll && this.collab.state.isCollaborating) {
|
||||
await Promise.all([
|
||||
broadcastPromise,
|
||||
this.app.saveCollabRoomToFirebase(syncableElements),
|
||||
this.collab.saveCollabRoomToFirebase(syncableElements),
|
||||
]);
|
||||
} else {
|
||||
await broadcastPromise;
|
||||
@ -146,9 +143,9 @@ class Portal {
|
||||
socketId: this.socket.id,
|
||||
pointer: payload.pointer,
|
||||
button: payload.button || "up",
|
||||
selectedElementIds:
|
||||
this.app.excalidrawAppState?.selectedElementIds || {},
|
||||
username: this.app.state.username,
|
||||
selectedElementIds: this.collab.excalidrawAPI.getAppState()
|
||||
.selectedElementIds,
|
||||
username: this.collab.state.username,
|
||||
},
|
||||
};
|
||||
return this._broadcastSocketData(
|
||||
@ -157,62 +154,6 @@ class Portal {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
reconcileElements = (
|
||||
sceneElements: readonly ExcalidrawElement[],
|
||||
): readonly ExcalidrawElement[] => {
|
||||
const currentElements = this.app.getSceneElementsIncludingDeleted();
|
||||
// create a map of ids so we don't have to iterate
|
||||
// over the array more than once.
|
||||
const localElementMap = getElementMap(currentElements);
|
||||
|
||||
// Reconcile
|
||||
return (
|
||||
sceneElements
|
||||
.reduce((elements, element) => {
|
||||
// if the remote element references one that's currently
|
||||
// edited on local, skip it (it'll be added in the next step)
|
||||
if (
|
||||
element.id === this.app.excalidrawAppState?.editingElement?.id ||
|
||||
element.id === this.app.excalidrawAppState?.resizingElement?.id ||
|
||||
element.id === this.app.excalidrawAppState?.draggingElement?.id
|
||||
) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version > element.version
|
||||
) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
delete localElementMap[element.id];
|
||||
} else if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version === element.version &&
|
||||
localElementMap[element.id].versionNonce !== element.versionNonce
|
||||
) {
|
||||
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
|
||||
if (
|
||||
localElementMap[element.id].versionNonce < element.versionNonce
|
||||
) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
} else {
|
||||
// it should be highly unlikely that the two versionNonces are the same. if we are
|
||||
// really worried about this, we can replace the versionNonce with the socket id.
|
||||
elements.push(element);
|
||||
}
|
||||
delete localElementMap[element.id];
|
||||
} else {
|
||||
elements.push(element);
|
||||
delete localElementMap[element.id];
|
||||
}
|
||||
|
||||
return elements;
|
||||
}, [] as Mutable<typeof sceneElements>)
|
||||
// add local elements that weren't deleted or on remote
|
||||
.concat(...Object.values(localElementMap))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Portal;
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../../css/_variables";
|
||||
@import "../../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.RoomDialog-linkContainer {
|
||||
|
@ -25,7 +25,6 @@ export const LanguageList = ({
|
||||
<option key={i18n.defaultLang.code} value={i18n.defaultLang.code}>
|
||||
{i18n.defaultLang.label}
|
||||
</option>
|
||||
<option disabled>{"──────────"}</option>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.label}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
@ -11,18 +12,19 @@ import { getDefaultAppState } from "../appState";
|
||||
import { ExcalidrawImperativeAPI } from "../components/App";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import { APP_NAME, EVENT, TITLE_TIMEOUT } from "../constants";
|
||||
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
||||
import { ImportedDataState } from "../data/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { Language, t } from "../i18n";
|
||||
import Excalidraw, {
|
||||
defaultLang,
|
||||
languages,
|
||||
} from "../packages/excalidraw/index";
|
||||
import { AppState, ExcalidrawAPIRefValue } from "../types";
|
||||
import { AppState } from "../types";
|
||||
import {
|
||||
debounce,
|
||||
getVersion,
|
||||
@ -30,7 +32,11 @@ import {
|
||||
resolvablePromise,
|
||||
} from "../utils";
|
||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants";
|
||||
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper";
|
||||
import CollabWrapper, {
|
||||
CollabAPI,
|
||||
CollabContext,
|
||||
CollabContextConsumer,
|
||||
} from "./collab/CollabWrapper";
|
||||
import { LanguageList } from "./components/LanguageList";
|
||||
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
||||
import { loadFromFirebase } from "./data/firebase";
|
||||
@ -49,15 +55,6 @@ languageDetector.init({
|
||||
checkWhitelist: false,
|
||||
});
|
||||
|
||||
const excalidrawRef: React.MutableRefObject<
|
||||
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
|
||||
> = {
|
||||
current: {
|
||||
readyPromise: resolvablePromise(),
|
||||
ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
const saveDebounced = debounce(
|
||||
(elements: readonly ExcalidrawElement[], state: AppState) => {
|
||||
saveToLocalStorage(elements, state);
|
||||
@ -191,7 +188,7 @@ const initializeScene = async (opts: {
|
||||
return null;
|
||||
};
|
||||
|
||||
function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
function ExcalidrawWrapper() {
|
||||
// dimensions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -226,31 +223,40 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
initialStatePromiseRef.current.promise = resolvablePromise<ImportedDataState | null>();
|
||||
}
|
||||
|
||||
const { collab } = props;
|
||||
useEffect(() => {
|
||||
// Delayed so that the app has a time to load the latest SW
|
||||
setTimeout(() => {
|
||||
trackEvent("load", "version", getVersion());
|
||||
}, VERSION_TIMEOUT);
|
||||
}, []);
|
||||
|
||||
const [
|
||||
excalidrawAPI,
|
||||
excalidrawRefCallback,
|
||||
] = useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||
|
||||
const collabAPI = useContext(CollabContext)?.api;
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent("load", "version", getVersion());
|
||||
excalidrawRef.current!.readyPromise.then((excalidrawApi) => {
|
||||
if (!collabAPI || !excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeScene({
|
||||
resetScene: excalidrawApi.resetScene,
|
||||
initializeSocketClient: collab.initializeSocketClient,
|
||||
resetScene: excalidrawAPI.resetScene,
|
||||
initializeSocketClient: collabAPI.initializeSocketClient,
|
||||
}).then((scene) => {
|
||||
initialStatePromiseRef.current.promise.resolve(scene);
|
||||
});
|
||||
});
|
||||
|
||||
const onHashChange = (_: HashChangeEvent) => {
|
||||
const api = excalidrawRef.current!;
|
||||
if (!api.ready) {
|
||||
return;
|
||||
}
|
||||
if (window.location.hash.length > 1) {
|
||||
initializeScene({
|
||||
resetScene: api.resetScene,
|
||||
initializeSocketClient: collab.initializeSocketClient,
|
||||
resetScene: excalidrawAPI.resetScene,
|
||||
initializeSocketClient: collabAPI.initializeSocketClient,
|
||||
}).then((scene) => {
|
||||
if (scene) {
|
||||
api.updateScene(scene);
|
||||
excalidrawAPI.updateScene(scene);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -269,7 +275,7 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
||||
clearTimeout(titleTimeout);
|
||||
};
|
||||
}, [collab.initializeSocketClient]);
|
||||
}, [collabAPI, excalidrawAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
languageDetector.cacheUserLanguage(langCode);
|
||||
@ -280,8 +286,8 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
appState: AppState,
|
||||
) => {
|
||||
saveDebounced(elements, appState);
|
||||
if (collab.isCollaborating) {
|
||||
collab.broadcastElements(elements, appState);
|
||||
if (collabAPI?.isCollaborating) {
|
||||
collabAPI.broadcastElements(elements);
|
||||
}
|
||||
};
|
||||
|
||||
@ -339,19 +345,20 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
return (
|
||||
<>
|
||||
<Excalidraw
|
||||
ref={excalidrawRef}
|
||||
ref={excalidrawRefCallback}
|
||||
onChange={onChange}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
user={{ name: collab.username }}
|
||||
onCollabButtonClick={collab.onCollabButtonClick}
|
||||
isCollaborating={collab.isCollaborating}
|
||||
onPointerUpdate={collab.onPointerUpdate}
|
||||
user={{ name: collabAPI?.username }}
|
||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||
isCollaborating={collabAPI?.isCollaborating}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
/>
|
||||
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
<ErrorDialog
|
||||
message={errorMessage}
|
||||
@ -365,13 +372,9 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
export default function ExcalidrawApp() {
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<CollabWrapper
|
||||
excalidrawRef={
|
||||
excalidrawRef as React.MutableRefObject<ExcalidrawImperativeAPI>
|
||||
}
|
||||
>
|
||||
{(collab) => <ExcalidrawWrapper collab={collab} />}
|
||||
</CollabWrapper>
|
||||
<CollabContextConsumer>
|
||||
<ExcalidrawWrapper />
|
||||
</CollabContextConsumer>
|
||||
</TopErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { PointerCoords } from "./types";
|
||||
import { normalizeScroll } from "./scene";
|
||||
|
||||
export const getCenter = (pointers: Map<number, PointerCoords>) => {
|
||||
const allCoords = Array.from(pointers.values());
|
||||
return {
|
||||
x: normalizeScroll(sum(allCoords, (coords) => coords.x) / allCoords.length),
|
||||
y: normalizeScroll(sum(allCoords, (coords) => coords.y) / allCoords.length),
|
||||
x: sum(allCoords, (coords) => coords.x) / allCoords.length,
|
||||
y: sum(allCoords, (coords) => coords.y) / allCoords.length,
|
||||
};
|
||||
};
|
||||
|
||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -85,6 +85,6 @@ type ForwardRef<T, P = any> = Parameters<
|
||||
// --------------------------------------------------------------------------—
|
||||
|
||||
interface Blob {
|
||||
handle?: import("browser-nativefs").FileSystemHandle;
|
||||
handle?: import("browser-fs-acces").FileSystemHandle;
|
||||
name?: string;
|
||||
}
|
||||
|
7
src/hooks/useCallbackRefState.ts
Normal file
7
src/hooks/useCallbackRefState.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export const useCallbackRefState = <T>() => {
|
||||
const [refValue, setRefValue] = useState<T | null>(null);
|
||||
const refCallback = useCallback((value: T | null) => setRefValue(value), []);
|
||||
return [refValue, refCallback] as const;
|
||||
};
|
@ -27,6 +27,7 @@ const allLanguages: Language[] = [
|
||||
{ code: "id-ID", label: "Bahasa Indonesia" },
|
||||
{ code: "it-IT", label: "Italiano" },
|
||||
{ code: "ja-JP", label: "日本語" },
|
||||
{ code: "kab-KAB", label: "Taqbaylit" },
|
||||
{ code: "ko-KR", label: "한국어" },
|
||||
{ code: "my-MM", label: "Burmese" },
|
||||
{ code: "nb-NO", label: "Norsk bokmål" },
|
||||
|
@ -1,7 +1,18 @@
|
||||
import React, { useState, useEffect, useRef, useContext } from "react";
|
||||
import variables from "./css/variables.module.scss";
|
||||
|
||||
const context = React.createContext(false);
|
||||
|
||||
const getIsMobileMatcher = () => {
|
||||
return window.matchMedia
|
||||
? window.matchMedia(variables.isMobileQuery)
|
||||
: (({
|
||||
matches: false,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
} as any) as MediaQueryList);
|
||||
};
|
||||
|
||||
export const IsMobileProvider = ({
|
||||
children,
|
||||
}: {
|
||||
@ -9,16 +20,7 @@ export const IsMobileProvider = ({
|
||||
}) => {
|
||||
const query = useRef<MediaQueryList>();
|
||||
if (!query.current) {
|
||||
query.current = window.matchMedia
|
||||
? window.matchMedia(
|
||||
// keep up to date with _variables.scss
|
||||
"(max-width: 640px), (max-height: 500px) and (max-width: 1000px)",
|
||||
)
|
||||
: (({
|
||||
matches: false,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
} as any) as MediaQueryList);
|
||||
query.current = getIsMobileMatcher();
|
||||
}
|
||||
const [isMobile, setMobile] = useState(query.current.matches);
|
||||
|
||||
@ -31,6 +33,8 @@ export const IsMobileProvider = ({
|
||||
return <context.Provider value={isMobile}>{children}</context.Provider>;
|
||||
};
|
||||
|
||||
export const isMobile = () => getIsMobileMatcher().matches;
|
||||
|
||||
export default function useIsMobile() {
|
||||
return useContext(context);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
||||
export const isWindows = /^Win/.test(window.navigator.platform);
|
||||
|
||||
export const CODES = {
|
||||
EQUAL: "Equal",
|
||||
@ -18,7 +19,9 @@ export const CODES = {
|
||||
F: "KeyF",
|
||||
H: "KeyH",
|
||||
V: "KeyV",
|
||||
X: "KeyX",
|
||||
Z: "KeyZ",
|
||||
R: "KeyR",
|
||||
} as const;
|
||||
|
||||
export const KEYS = {
|
||||
@ -48,6 +51,7 @@ export const KEYS = {
|
||||
T: "t",
|
||||
V: "v",
|
||||
X: "x",
|
||||
Y: "y",
|
||||
Z: "z",
|
||||
} as const;
|
||||
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "وضع الشبكة",
|
||||
"addToLibrary": "أضف إلى المكتبة",
|
||||
"removeFromLibrary": "حذف من المكتبة",
|
||||
"libraryLoadingMessage": "جارٍ تحميل المكتبة...",
|
||||
"libraryLoadingMessage": "جارٍ تحميل المكتبة…",
|
||||
"libraries": "تصفح المكتبات",
|
||||
"loadingScene": "جاري تحميل المشهد...",
|
||||
"loadingScene": "جاري تحميل المشهد…",
|
||||
"align": "محاذاة",
|
||||
"alignTop": "محاذاة إلى اﻷعلى",
|
||||
"alignBottom": "محاذاة إلى اﻷسفل",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "توسيط عمودي",
|
||||
"centerHorizontally": "توسيط أفقي",
|
||||
"distributeHorizontally": "التوزيع الأفقي",
|
||||
"distributeVertically": "التوزيع عمودياً"
|
||||
"distributeVertically": "التوزيع عمودياً",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "إعادة تعيين اللوحة",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "خطأ"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "اختصارات لوحة المفاتيح",
|
||||
"shapes": "الأشكال",
|
||||
"or": "أو",
|
||||
"click": "انقر",
|
||||
"drag": "اسحب",
|
||||
"curvedArrow": "سهم منحنى",
|
||||
"curvedLine": "خط منحنى",
|
||||
"editor": "المحرر",
|
||||
"view": "المشهد",
|
||||
"blog": "اقرأ مدونتنا",
|
||||
"howto": "اتبع دليلنا",
|
||||
"github": "عثرت على مشكلة؟ إرسال",
|
||||
"textNewLine": "إضافة سطر جديد (نص)",
|
||||
"textFinish": "الانتهاء من تحرير (النص)",
|
||||
"zoomToFit": "تكبير لتلائم جميع العناصر",
|
||||
"zoomToSelection": "تقريب للمحدد",
|
||||
"preventBinding": "منع ربط السهم"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "إحصائيات للمهووسين",
|
||||
"total": "المجموع",
|
||||
"width": "العرض"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Решетъчен режим",
|
||||
"addToLibrary": "Добавяне към библиотеката",
|
||||
"removeFromLibrary": "Премахване от библиотеката",
|
||||
"libraryLoadingMessage": "Зареждане на библиотеката...",
|
||||
"libraryLoadingMessage": "Зареждане на библиотеката…",
|
||||
"libraries": "Разглеждане на библиотеките",
|
||||
"loadingScene": "Зареждане на сцена...",
|
||||
"loadingScene": "Зареждане на сцена…",
|
||||
"align": "Подравняване",
|
||||
"alignTop": "Подравняване отгоре",
|
||||
"alignBottom": "Подравняване отдолу",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Центрирай вертикално",
|
||||
"centerHorizontally": "Центрирай хоризонтално",
|
||||
"distributeHorizontally": "Разпредели хоризонтално",
|
||||
"distributeVertically": "Разпредели вертикално"
|
||||
"distributeVertically": "Разпредели вертикално",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Нулиране на платно",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Грешка"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Клавиши за бърз достъп",
|
||||
"shapes": "Фигури",
|
||||
"or": "или",
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "клик",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "плъзнете",
|
||||
"curvedArrow": "Извита стрелка",
|
||||
"curvedLine": "Извита линия",
|
||||
"editor": "Редактор",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "или",
|
||||
"preventBinding": "",
|
||||
"shapes": "Фигури",
|
||||
"shortcuts": "Клавиши за бърз достъп",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "Преглед",
|
||||
"blog": "Прочетете нашия блог",
|
||||
"howto": "Следвайте нашите ръководства",
|
||||
"github": "Намерихте проблем? Изпратете",
|
||||
"textNewLine": "Добавяне на нов ред (текст)",
|
||||
"textFinish": "Завършете редактиране (текст)",
|
||||
"zoomToFit": "Приближи докато се виждат всички елементи",
|
||||
"zoomToSelection": "Приближи селекцията",
|
||||
"preventBinding": "Спри прилепяне на стрелките"
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": "Приближи селекцията"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Статистика за хакери",
|
||||
"total": "Общо",
|
||||
"width": "Широчина"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -30,17 +30,17 @@
|
||||
"edges": "Vores",
|
||||
"sharp": "Agut",
|
||||
"round": "Arrodonit",
|
||||
"arrowheads": "Punta de fletxa",
|
||||
"arrowheads": "Puntes de fletxa",
|
||||
"arrowhead_none": "Cap",
|
||||
"arrowhead_arrow": "Fletxa",
|
||||
"arrowhead_bar": "Línia",
|
||||
"arrowhead_bar": "Barra",
|
||||
"arrowhead_dot": "Punt",
|
||||
"fontSize": "Mida de lletra",
|
||||
"fontFamily": "Tipus de lletra",
|
||||
"onlySelected": "Només seleccionats",
|
||||
"withBackground": "Amb fons",
|
||||
"exportEmbedScene": "Incrustar escena al fitxer exportat",
|
||||
"exportEmbedScene_details": "Les dades de l’escena es desaran al fitxer PNG/SVG exportat de manera que es pugui restaurar l’escena.\nAugmentarà la mida del fitxer exportat.",
|
||||
"exportEmbedScene_details": "Les dades de l’escena es desaran al fitxer PNG/SVG de manera que es pugui restaurar l’escena.\nAugmentarà la mida del fitxer exportat.",
|
||||
"addWatermark": "Afegir \"Fet amb Excalidraw\"",
|
||||
"handDrawn": "Dibuixat a mà",
|
||||
"normal": "Normal",
|
||||
@ -61,7 +61,7 @@
|
||||
"architect": "Arquitecte",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Dibuixant",
|
||||
"fileTitle": "Títol de fitxer",
|
||||
"fileTitle": "Títol del fitxer",
|
||||
"colorPicker": "Selector de colors",
|
||||
"canvasBackground": "Fons del llenç",
|
||||
"drawingCanvas": "Llenç de dibuix",
|
||||
@ -82,7 +82,7 @@
|
||||
"removeFromLibrary": "Eliminar de la biblioteca",
|
||||
"libraryLoadingMessage": "Carregant la biblioteca...",
|
||||
"libraries": "Explorar biblioteques",
|
||||
"loadingScene": "Carregant escena...",
|
||||
"loadingScene": "Carregant escena…",
|
||||
"align": "Alinear",
|
||||
"alignTop": "Alinear a dalt",
|
||||
"alignBottom": "Alinear a baix",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Centrar verticalment",
|
||||
"centerHorizontally": "Centrar horitzontalment",
|
||||
"distributeHorizontally": "Distribuir horitzontalment",
|
||||
"distributeVertically": "Distribuir verticalment"
|
||||
"distributeVertically": "Distribuir verticalment",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Netejar el llenç",
|
||||
@ -127,7 +128,7 @@
|
||||
"alerts": {
|
||||
"clearReset": "Tot el llenç s'esborrarà. Estàs segur?",
|
||||
"couldNotCreateShareableLink": "No s'ha pogut crear un enllaç per compartir.",
|
||||
"couldNotCreateShareableLinkTooBig": "No s’ha pogut crear un enllaç compartible: l’escena és massa gran",
|
||||
"couldNotCreateShareableLinkTooBig": "No s’ha pogut crear un enllaç per compartir: l’escena és massa gran",
|
||||
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
|
||||
"importBackendFailed": "Importació fallida.",
|
||||
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
|
||||
@ -162,7 +163,7 @@
|
||||
"freeDraw": "Fer clic i arrosegar, deixar anar al punt final",
|
||||
"text": "Consell: també pots afegir text fent doble clic a qualsevol lloc amb l'eina de selecció",
|
||||
"linearElementMulti": "Fer clic a l'ultim punt, o polsar Escape o Enter per acabar",
|
||||
"lockAngle": "Pots restringir l’angle mantenint premuda MAJÚS",
|
||||
"lockAngle": "Per restringir els angles, mantenir premut el majúscul (SHIFT)",
|
||||
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
|
||||
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
||||
"lineEditor_info": "Fes doble clic o premi Enter per editar punts",
|
||||
@ -171,7 +172,7 @@
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "No es pot mostrar la vista prèvia",
|
||||
"canvasTooBig": "El llenç pot ser massa gran.",
|
||||
"canvasTooBig": "Pot ser que el llenç sigui massa gran.",
|
||||
"canvasTooBigTip": "Consell: prova d’acostar una mica els elements més allunyats."
|
||||
},
|
||||
"errorSplash": {
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Error"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Dreceres de teclat",
|
||||
"shapes": "Formes",
|
||||
"or": "o",
|
||||
"click": "fer clic",
|
||||
"drag": "arrosegar",
|
||||
"curvedArrow": "Fletxa curva",
|
||||
"curvedLine": "Línea curva",
|
||||
"editor": "Editor",
|
||||
"view": "Vista",
|
||||
"blog": "Llegir el nostre blog",
|
||||
"howto": "Seguir els nostres guies",
|
||||
"github": "Has trobat un problema? Enviar-ho",
|
||||
"textNewLine": "Afegir línea nova (text)",
|
||||
"textFinish": "Acabar d'editar (text)",
|
||||
"zoomToFit": "Zoom per veure tots els elements",
|
||||
"zoomToSelection": "Amplia la selecció",
|
||||
"preventBinding": "Prevenir vinculació de la fletxa"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Estadístiques per nerds",
|
||||
"total": "Total",
|
||||
"width": "Amplada"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Rastermodus",
|
||||
"addToLibrary": "Zur Bibliothek hinzufügen",
|
||||
"removeFromLibrary": "Aus Bibliothek entfernen",
|
||||
"libraryLoadingMessage": "Lade Bibliothek...",
|
||||
"libraryLoadingMessage": "Lade Bibliothek…",
|
||||
"libraries": "Bibliotheken durchsuchen",
|
||||
"loadingScene": "Lade Zeichnung...",
|
||||
"loadingScene": "Lade Zeichnung…",
|
||||
"align": "Ausrichten",
|
||||
"alignTop": "Obere Kanten",
|
||||
"alignBottom": "Untere Kanten",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Vertikal zentrieren",
|
||||
"centerHorizontally": "Horizontal zentrieren",
|
||||
"distributeHorizontally": "Horizontal verteilen",
|
||||
"distributeVertically": "Vertikal verteilen"
|
||||
"distributeVertically": "Vertikal verteilen",
|
||||
"viewMode": "Ansichtsmodus"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Fehler"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Tastaturkürzel",
|
||||
"shapes": "Formen",
|
||||
"or": "oder",
|
||||
"helpDialog": {
|
||||
"blog": "Lies unseren Blog",
|
||||
"click": "klicken",
|
||||
"drag": "ziehen",
|
||||
"curvedArrow": "Gebogener Pfeil",
|
||||
"curvedLine": "Gebogene Linie",
|
||||
"documentation": "Dokumentation",
|
||||
"drag": "ziehen",
|
||||
"editor": "Editor",
|
||||
"view": "Ansicht",
|
||||
"blog": "Unseren Blog lesen",
|
||||
"howto": "Folge unseren Anleitungen",
|
||||
"github": "Ein Problem gefunden? Informiere uns",
|
||||
"textNewLine": "Neue Zeile hinzufügen (Text)",
|
||||
"howto": "Folge unseren Anleitungen",
|
||||
"or": "oder",
|
||||
"preventBinding": "Pfeil-Bindung verhindern",
|
||||
"shapes": "Formen",
|
||||
"shortcuts": "Tastaturkürzel",
|
||||
"textFinish": "Bearbeiten beenden (Text)",
|
||||
"textNewLine": "Neue Zeile hinzufügen (Text)",
|
||||
"title": "Hilfe",
|
||||
"view": "Ansicht",
|
||||
"zoomToFit": "Zoomen um alle Elemente einzupassen",
|
||||
"zoomToSelection": "Zoomauswahl",
|
||||
"preventBinding": "Pfeil-Bindung verhindern"
|
||||
"zoomToSelection": "Auf Auswahl zoomen"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Statistiken für Nerds",
|
||||
"total": "Gesamt",
|
||||
"width": "Breite"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Formatierung kopiert.",
|
||||
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert."
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Εμφάνιση σε πλέγμα",
|
||||
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
|
||||
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
|
||||
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης...",
|
||||
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης…",
|
||||
"libraries": "Άλλες βιβλιοθήκες",
|
||||
"loadingScene": "Φόρτωση σκηνής...",
|
||||
"loadingScene": "Φόρτωση σκηνής…",
|
||||
"align": "Στοίχιση",
|
||||
"alignTop": "Στοίχιση πάνω",
|
||||
"alignBottom": "Στοίχιση κάτω",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Κέντρο κάθετα",
|
||||
"centerHorizontally": "Κέντρο οριζόντια",
|
||||
"distributeHorizontally": "Οριζόντια κατανομή",
|
||||
"distributeVertically": "Κατακόρυφη κατανομή"
|
||||
"distributeVertically": "Κατακόρυφη κατανομή",
|
||||
"viewMode": "Λειτουργία προβολής"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Επαναφορά του καμβά",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Σφάλμα"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Συντομεύσεις πληκτρολογίου",
|
||||
"shapes": "Σχήματα",
|
||||
"or": "ή",
|
||||
"helpDialog": {
|
||||
"blog": "Διαβάστε το Blog μας",
|
||||
"click": "κλικ",
|
||||
"drag": "σύρε",
|
||||
"curvedArrow": "Κυρτό βέλος",
|
||||
"curvedLine": "Κυρτή γραμμή",
|
||||
"documentation": "Εγχειρίδιο",
|
||||
"drag": "σύρε",
|
||||
"editor": "Επεξεργαστής",
|
||||
"view": "Προβολή",
|
||||
"blog": "Διαβάστε το ιστολόγιο μας",
|
||||
"howto": "Ακολουθήστε τους οδηγούς μας",
|
||||
"github": "Βρήκατε πρόβλημα; Υποβάλετε το",
|
||||
"textNewLine": "Προσθήκη νέας γραμμής (κείμενο)",
|
||||
"howto": "Ακολουθήστε τους οδηγούς μας",
|
||||
"or": "ή",
|
||||
"preventBinding": "Αποτροπή δέσμευσης βέλων",
|
||||
"shapes": "Σχήματα",
|
||||
"shortcuts": "Συντομεύσεις πληκτρολογίου",
|
||||
"textFinish": "Ολοκλήρωση επεξεργασίας (κείμενο)",
|
||||
"textNewLine": "Προσθήκη νέας γραμμής (κείμενο)",
|
||||
"title": "Βοήθεια",
|
||||
"view": "Προβολή",
|
||||
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
||||
"zoomToSelection": "Εστίαση στην επιλογή",
|
||||
"preventBinding": "Αποτροπή δέσμευσης βέλων"
|
||||
"zoomToSelection": "Ζουμ στην επιλογή"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα έιναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Στατιστικά για σπασίκλες",
|
||||
"total": "Σύνολο ",
|
||||
"width": "Πλάτος"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Αντιγράφηκαν στυλ.",
|
||||
"copyToClipboardAsPng": "Αντιγράφτηκε στο πρόχειρο ως PNG."
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Grid mode",
|
||||
"addToLibrary": "Add to library",
|
||||
"removeFromLibrary": "Remove from library",
|
||||
"libraryLoadingMessage": "Loading library...",
|
||||
"libraryLoadingMessage": "Loading library…",
|
||||
"libraries": "Browse libraries",
|
||||
"loadingScene": "Loading scene...",
|
||||
"loadingScene": "Loading scene…",
|
||||
"align": "Align",
|
||||
"alignTop": "Align top",
|
||||
"alignBottom": "Align bottom",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Center vertically",
|
||||
"centerHorizontally": "Center horizontally",
|
||||
"distributeHorizontally": "Distribute horizontally",
|
||||
"distributeVertically": "Distribute vertically"
|
||||
"distributeVertically": "Distribute vertically",
|
||||
"viewMode": "View mode"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Error"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Keyboard shortcuts",
|
||||
"shapes": "Shapes",
|
||||
"or": "or",
|
||||
"helpDialog": {
|
||||
"blog": "Read our blog",
|
||||
"click": "click",
|
||||
"drag": "drag",
|
||||
"curvedArrow": "Curved arrow",
|
||||
"curvedLine": "Curved line",
|
||||
"documentation": "Documentation",
|
||||
"drag": "drag",
|
||||
"editor": "Editor",
|
||||
"view": "View",
|
||||
"blog": "Read our blog",
|
||||
"howto": "Follow our guides",
|
||||
"github": "Found an issue? Submit",
|
||||
"textNewLine": "Add new line (text)",
|
||||
"howto": "Follow our guides",
|
||||
"or": "or",
|
||||
"preventBinding": "Prevent arrow binding",
|
||||
"shapes": "Shapes",
|
||||
"shortcuts": "Keyboard shortcuts",
|
||||
"textFinish": "Finish editing (text)",
|
||||
"textNewLine": "Add new line (text)",
|
||||
"title": "Help",
|
||||
"view": "View",
|
||||
"zoomToFit": "Zoom to fit all elements",
|
||||
"zoomToSelection": "Zoom to selection",
|
||||
"preventBinding": "Prevent arrow binding"
|
||||
"zoomToSelection": "Zoom to selection"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Stats for nerds",
|
||||
"total": "Total",
|
||||
"width": "Width"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Copied styles.",
|
||||
"copyToClipboardAsPng": "Copied to clipboard as PNG."
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Centrar verticalmente",
|
||||
"centerHorizontally": "Centrar horizontalmente",
|
||||
"distributeHorizontally": "Distribuir horizontalmente",
|
||||
"distributeVertically": "Distribuir verticalmente"
|
||||
"distributeVertically": "Distribuir verticalmente",
|
||||
"viewMode": "Modo presentación"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Error"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Atajos del teclado",
|
||||
"shapes": "Formas",
|
||||
"or": "o",
|
||||
"click": "clic",
|
||||
"drag": "arrastrar",
|
||||
"helpDialog": {
|
||||
"blog": "Lee nuestro blog",
|
||||
"click": "click",
|
||||
"curvedArrow": "Flecha curvada",
|
||||
"curvedLine": "Línea curva",
|
||||
"documentation": "Documentación",
|
||||
"drag": "arrastrar",
|
||||
"editor": "Editor",
|
||||
"view": "Vista",
|
||||
"blog": "Lee nuestro blog",
|
||||
"howto": "Siga nuestras guías",
|
||||
"github": "¿Has encontrado un problema? Envíalo",
|
||||
"textNewLine": "Añadir nueva línea (texto)",
|
||||
"howto": "Siga nuestras guías",
|
||||
"or": "o",
|
||||
"preventBinding": "Evitar yuxtaposición de flechas",
|
||||
"shapes": "Formas",
|
||||
"shortcuts": "Atajos del teclado",
|
||||
"textFinish": "Finalizar edición (texto)",
|
||||
"textNewLine": "Añadir nueva línea (texto)",
|
||||
"title": "Ayuda",
|
||||
"view": "Vista",
|
||||
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
|
||||
"zoomToSelection": "Hacer zoom a la selección",
|
||||
"preventBinding": "Evitar yuxtaposición de flechas"
|
||||
"zoomToSelection": "Hacer zoom a la selección"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Estadísticas para nerds",
|
||||
"total": "Total",
|
||||
"width": "Ancho"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Estilos copiados.",
|
||||
"copyToClipboardAsPng": "Copiado al portapapeles como PNG."
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "حالت شبکه ای",
|
||||
"addToLibrary": "افزودن به کتابخانه",
|
||||
"removeFromLibrary": "حذف از کتابخانه",
|
||||
"libraryLoadingMessage": "بارگذاری کتابخانه...",
|
||||
"libraryLoadingMessage": "بارگذاری کتابخانه…",
|
||||
"libraries": "مرور کردن کتابخانه ها",
|
||||
"loadingScene": "باگذاری صحنه...",
|
||||
"loadingScene": "باگذاری صحنه…",
|
||||
"align": "تراز",
|
||||
"alignTop": "تراز به بالا",
|
||||
"alignBottom": "تراز به پایین",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "وسط قرار دادن به صورت عمودی",
|
||||
"centerHorizontally": "وسط قرار دادن به صورت افقی",
|
||||
"distributeHorizontally": "توزیع کردن به صورت افقی",
|
||||
"distributeVertically": "توزیع کردن به صورت عمودی"
|
||||
"distributeVertically": "توزیع کردن به صورت عمودی",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "پاکسازی بوم نقاشی",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "خطا"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "میانبرهای صفحه کلید",
|
||||
"shapes": "شکلها",
|
||||
"or": "یا",
|
||||
"click": "کلیک",
|
||||
"drag": "کشیدن",
|
||||
"helpDialog": {
|
||||
"blog": "بلاگ ما را بخوانید",
|
||||
"click": "",
|
||||
"curvedArrow": "فلش خمیده",
|
||||
"curvedLine": "منحنی",
|
||||
"documentation": "مستندات",
|
||||
"drag": "",
|
||||
"editor": "ویرایشگر",
|
||||
"view": "نمایش",
|
||||
"blog": "بلاگ ما را بخوانید",
|
||||
"howto": "راهنمای ما را دنبال کنید",
|
||||
"github": "اشکالی می بینید؟ گزارش دهید",
|
||||
"howto": "راهنمای ما را دنبال کنید",
|
||||
"or": "یا",
|
||||
"preventBinding": "مانع شدن از چسبیدن فلش ها",
|
||||
"shapes": "شکلها",
|
||||
"shortcuts": "میانبرهای صفحه کلید",
|
||||
"textFinish": "",
|
||||
"textNewLine": "یک خط جدید اضافه کنید (متن)",
|
||||
"textFinish": "پایان ویرایش (متن)",
|
||||
"title": "راهنما",
|
||||
"view": "مشاهده",
|
||||
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
|
||||
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
|
||||
"preventBinding": "مانع شدن از چسبیدن فلش ها"
|
||||
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "آمار برای نردها",
|
||||
"total": "مجموع",
|
||||
"width": "عرض"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "کپی سبک.",
|
||||
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG."
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Ruudukkotila",
|
||||
"addToLibrary": "Lisää kirjastoon",
|
||||
"removeFromLibrary": "Poista kirjastosta",
|
||||
"libraryLoadingMessage": "Ladataan kirjastoa...",
|
||||
"libraryLoadingMessage": "Ladataan kirjastoa…",
|
||||
"libraries": "Selaa kirjastoja",
|
||||
"loadingScene": "Ladataan työtä...",
|
||||
"loadingScene": "Ladataan työtä…",
|
||||
"align": "Tasaa",
|
||||
"alignTop": "Tasaa ylös",
|
||||
"alignBottom": "Tasaa alas",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Keskitä pystysuunnassa",
|
||||
"centerHorizontally": "Keskitä vaakasuunnassa",
|
||||
"distributeHorizontally": "Jaa vaakasuunnassa",
|
||||
"distributeVertically": "Jaa pystysuunnassa"
|
||||
"distributeVertically": "Jaa pystysuunnassa",
|
||||
"viewMode": "Katselutila"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Tyhjennä piirtoalue",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Virhe"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Pikanäppäimet",
|
||||
"shapes": "Muodot",
|
||||
"or": "tai",
|
||||
"helpDialog": {
|
||||
"blog": "Lue blogiamme",
|
||||
"click": "klikkaa",
|
||||
"drag": "vedä",
|
||||
"curvedArrow": "Kaareva nuoli",
|
||||
"curvedLine": "Kaareva viiva",
|
||||
"editor": "Editori",
|
||||
"view": "Näkymä",
|
||||
"blog": "Lue blogiamme",
|
||||
"howto": "Seuraa oppaitamme",
|
||||
"documentation": "Käyttöohjeet",
|
||||
"drag": "vedä",
|
||||
"editor": "Muokkausohjelma",
|
||||
"github": "Löysitkö ongelman? Kerro meille",
|
||||
"textNewLine": "Lisää uusi rivi (teksti)",
|
||||
"howto": "Seuraa oppaitamme",
|
||||
"or": "tai",
|
||||
"preventBinding": "Estä nuolten kiinnitys",
|
||||
"shapes": "Muodot",
|
||||
"shortcuts": "Pikanäppäimet",
|
||||
"textFinish": "Lopeta muokkaus (teksti)",
|
||||
"zoomToFit": "Zoomaa kaikki elementit näkyviin",
|
||||
"zoomToSelection": "Zoomaa valintaan",
|
||||
"preventBinding": "Estä nuolten sitominen"
|
||||
"textNewLine": "Lisää uusi rivi (teksti)",
|
||||
"title": "Ohjeet",
|
||||
"view": "Näkymä",
|
||||
"zoomToFit": "Näytä kaikki elementit",
|
||||
"zoomToSelection": "Näytä valinta"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Piirroksesi ovat päästä päähän salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Nörttien tilastot",
|
||||
"total": "Yhteensä",
|
||||
"width": "Leveys"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Tyylit kopioitu.",
|
||||
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona."
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"stroke": "Contour",
|
||||
"background": "Arrière-plan",
|
||||
"fill": "Remplissage",
|
||||
"strokeWidth": "Largeur du contour",
|
||||
"strokeWidth": "Largeur du trait",
|
||||
"strokeStyle": "Style du trait",
|
||||
"strokeStyle_solid": "Plein",
|
||||
"strokeStyle_dashed": "Tirets",
|
||||
@ -28,10 +28,10 @@
|
||||
"opacity": "Opacité",
|
||||
"textAlign": "Alignement du texte",
|
||||
"edges": "Angles",
|
||||
"sharp": "Aigu",
|
||||
"round": "Rond",
|
||||
"arrowheads": "Extrémités de ligne",
|
||||
"arrowhead_none": "Aucun",
|
||||
"sharp": "Pointus",
|
||||
"round": "Arrondis",
|
||||
"arrowheads": "Extrémités de flèche",
|
||||
"arrowhead_none": "Aucune",
|
||||
"arrowhead_arrow": "Flèche",
|
||||
"arrowhead_bar": "Barre",
|
||||
"arrowhead_dot": "Point",
|
||||
@ -42,7 +42,7 @@
|
||||
"exportEmbedScene": "Intégrer la scène au fichier exporté",
|
||||
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
|
||||
"addWatermark": "Ajouter \"Fait avec Excalidraw\"",
|
||||
"handDrawn": "Manuscrite",
|
||||
"handDrawn": "À main levée",
|
||||
"normal": "Normale",
|
||||
"code": "Code",
|
||||
"small": "Petit",
|
||||
@ -64,7 +64,7 @@
|
||||
"fileTitle": "Titre du fichier",
|
||||
"colorPicker": "Sélecteur de couleur",
|
||||
"canvasBackground": "Arrière-plan du canevas",
|
||||
"drawingCanvas": "Canvas de dessin",
|
||||
"drawingCanvas": "Zone de dessin",
|
||||
"layers": "Calques",
|
||||
"actions": "Actions",
|
||||
"language": "Langue",
|
||||
@ -81,9 +81,9 @@
|
||||
"addToLibrary": "Ajouter à la bibliothèque",
|
||||
"removeFromLibrary": "Supprimer de la bibliothèque",
|
||||
"libraryLoadingMessage": "Chargement de la bibliothèque...",
|
||||
"libraries": "Explorer les bibliothèques",
|
||||
"libraries": "Parcourir les bibliothèques",
|
||||
"loadingScene": "Chargement de la scène...",
|
||||
"align": "Alignement",
|
||||
"align": "Aligner",
|
||||
"alignTop": "Aligner en haut",
|
||||
"alignBottom": "Aligner en bas",
|
||||
"alignLeft": "Aligner à gauche",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Centrer verticalement",
|
||||
"centerHorizontally": "Centrer horizontalement",
|
||||
"distributeHorizontally": "Distribuer horizontalement",
|
||||
"distributeVertically": "Distribuer verticalement"
|
||||
"distributeVertically": "Distribuer verticalement",
|
||||
"viewMode": "Mode présentation"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Réinitialiser le canevas",
|
||||
@ -99,7 +100,7 @@
|
||||
"exportToPng": "Exporter en PNG",
|
||||
"exportToSvg": "Exporter en SVG",
|
||||
"copyToClipboard": "Copier dans le presse-papier",
|
||||
"copyPngToClipboard": "Copier le PNG dans le presse-papier",
|
||||
"copyPngToClipboard": "Copier le PNG vers le presse-papier",
|
||||
"scale": "Échelle",
|
||||
"save": "Sauvegarder",
|
||||
"saveAs": "Enregistrer sous",
|
||||
@ -116,12 +117,12 @@
|
||||
"edit": "Modifier",
|
||||
"undo": "Annuler",
|
||||
"redo": "Rétablir",
|
||||
"roomDialog": "Démarrer le collaboration en temps réel",
|
||||
"createNewRoom": "Créer un nouveau salon",
|
||||
"roomDialog": "Démarrer la collaboration en direct",
|
||||
"createNewRoom": "Créer une nouvelle salle",
|
||||
"fullScreen": "Plein écran",
|
||||
"darkMode": "Mode sombre",
|
||||
"lightMode": "Mode Clair",
|
||||
"zenMode": "Mode Zen",
|
||||
"lightMode": "Mode clair",
|
||||
"zenMode": "Mode zen",
|
||||
"exitZenMode": "Quitter le mode zen"
|
||||
},
|
||||
"alerts": {
|
||||
@ -136,8 +137,8 @@
|
||||
"uploadedSecurly": "Le téléchargement a été sécurisé avec un chiffrement de bout en bout, ce qui signifie que ni Excalidraw ni personne d'autre ne peut en lire le contenu.",
|
||||
"loadSceneOverridePrompt": "Le chargement d'un dessin externe remplacera votre contenu actuel. Souhaitez-vous continuer ?",
|
||||
"errorLoadingLibrary": "Une erreur s'est produite lors du chargement de la bibliothèque tierce.",
|
||||
"confirmAddLibrary": "Cela va ajouter {{numShapes}} forme(s) à votre bibliothèque. Êtes-vous sûr(e) ?",
|
||||
"imageDoesNotContainScene": "L'importation des images n'est pas prise en charge pour le moment.\n\nVoulez-vous importer une scène ? Cette image ne semble pas contenir de données de scène. Avez-vous activé cette option lors de l'exportation ?",
|
||||
"confirmAddLibrary": "Cela va ajouter {{numShapes}} forme(s) à votre bibliothèque. Êtes-vous sûr·e ?",
|
||||
"imageDoesNotContainScene": "L'importation d'images n'est pas prise en charge pour le moment.\n\nVouliez-vous importer une scène ? Cette image ne semble pas contenir de données de scène. Avez-vous activé cette option lors de l'exportation ?",
|
||||
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image"
|
||||
},
|
||||
"toolBar": {
|
||||
@ -160,63 +161,65 @@
|
||||
"hints": {
|
||||
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
|
||||
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
|
||||
"text": "Astuce : vous pouvez également ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
|
||||
"text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
|
||||
"linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer",
|
||||
"lockAngle": "Vous pouvez contraindre l'angle en maintenant SHIFT",
|
||||
"resize": "Vous pouvez conserver les proportions en maintenant la touche SHIFT pendant le redimensionnement,\nen maintenant la touche ALT pour redimensionner par rapport au centre",
|
||||
"rotate": "Vous pouvez contraindre les angles en maintenant MAJ enfoncé pendant la rotation",
|
||||
"lockAngle": "Vous pouvez restreindre l'angle en maintenant MAJ",
|
||||
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement,\nmaintenez la touche ALT pour redimensionner par rapport au centre",
|
||||
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
|
||||
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
|
||||
"lineEditor_pointSelected": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le déplacer",
|
||||
"lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou à supprimer, ou maintenez Alt enfoncé et cliquez pour ajouter de nouveaux points"
|
||||
"lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou supprimer, ou maintenez Alt et cliquez pour ajouter de nouveaux points"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Impossible d’afficher l’aperçu",
|
||||
"canvasTooBig": "Le canevas est peut-être trop grand.",
|
||||
"canvasTooBigTip": "Conseil : essayez de rapprocher un peu plus les éléments les plus éloignés."
|
||||
"canvasTooBigTip": "Astuce : essayez de rapprocher un peu les éléments les plus éloignés."
|
||||
},
|
||||
"errorSplash": {
|
||||
"headingMain_pre": "Une erreur est survenue. Essayez ",
|
||||
"headingMain_button": "rechargement de la page.",
|
||||
"headingMain_button": "de recharger la page.",
|
||||
"clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez ",
|
||||
"clearCanvasMessage_button": "effacement du canevas.",
|
||||
"clearCanvasCaveat": " Cela entraînera une perte du travail ",
|
||||
"trackedToSentry_pre": "L'erreur avec l'identifiant ",
|
||||
"trackedToSentry_post": " a été enregistrée dans notre système.",
|
||||
"openIssueMessage_pre": "Nous avons été très prudents de ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre ",
|
||||
"openIssueMessage_pre": "Nous avons fait très attention à ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre ",
|
||||
"openIssueMessage_button": "outil de suivi des bugs.",
|
||||
"openIssueMessage_post": " Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.",
|
||||
"sceneContent": "Contenu de la scène :"
|
||||
},
|
||||
"roomDialog": {
|
||||
"desc_intro": "Vous pouvez inviter des personnes dans votre scène actuelle à collaborer avec vous.",
|
||||
"desc_privacy": "Ne vous inquiétez pas, la session utilise le chiffrement de bout en bout, donc tout ce que vous dessinez restera privé. Même notre serveur ne sera pas en mesure de voir ce que vous faites.",
|
||||
"desc_intro": "Vous pouvez inviter des personnes à collaborer avec vous sur votre scène actuelle.",
|
||||
"desc_privacy": "Pas d'inquiétude, la session utilise le chiffrement de bout en bout, donc tout ce que vous dessinez restera privé. Même notre serveur ne pourra voir ce que vous faites.",
|
||||
"button_startSession": "Démarrer la session",
|
||||
"button_stopSession": "Arrêter la session",
|
||||
"desc_inProgressIntro": "La session de collaboration en direct est maintenant en cours.",
|
||||
"desc_shareLink": "Partagez ce lien avec ceux avec qui vous souhaitez collaborer :",
|
||||
"desc_exitSession": "Arrêter la session vous déconnectera du salon, mais vous pourrez continuer à travailler avec la scène, localement. Notez que cela n'affectera pas les autres personnes, et ils seront toujours en mesure de collaborer sur leur version."
|
||||
"desc_shareLink": "Partagez ce lien avec les personnes avec lesquelles vous souhaitez collaborer :",
|
||||
"desc_exitSession": "Arrêter la session vous déconnectera de la salle, mais vous pourrez continuer à travailler avec la scène, localement. Notez que cela n'affectera pas les autres personnes, et ils pourront toujours collaborer sur leur version."
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Erreur"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Raccourcis clavier",
|
||||
"shapes": "Formes",
|
||||
"or": "ou",
|
||||
"click": "cliquer",
|
||||
"drag": "glisser",
|
||||
"helpDialog": {
|
||||
"blog": "Lire notre blog",
|
||||
"click": "clic",
|
||||
"curvedArrow": "Flèche courbée",
|
||||
"curvedLine": "Ligne courbée",
|
||||
"documentation": "Documentation",
|
||||
"drag": "glisser",
|
||||
"editor": "Éditeur",
|
||||
"view": "Afficher",
|
||||
"blog": "Lisez notre blog",
|
||||
"github": "Problème trouvé ? Soumettre",
|
||||
"howto": "Suivez nos guides",
|
||||
"github": "Vous avez trouvé un problème ? Envoyer",
|
||||
"textNewLine": "Ajouter une nouvelle ligne (texte)",
|
||||
"or": "ou",
|
||||
"preventBinding": "Empêcher la liaison de flèche",
|
||||
"shapes": "Formes",
|
||||
"shortcuts": "Raccourcis clavier",
|
||||
"textFinish": "Terminer l'édition (texte)",
|
||||
"zoomToFit": "Zoomer pour visualiser tous les éléments",
|
||||
"zoomToSelection": "Zoomer sur la sélection",
|
||||
"preventBinding": "Empêcher la liaison de la flèche"
|
||||
"textNewLine": "Ajouter une nouvelle ligne (texte)",
|
||||
"title": "Aide",
|
||||
"view": "Affichage",
|
||||
"zoomToFit": "Zoomer pour voir tous les éléments",
|
||||
"zoomToSelection": "Zoomer sur la sélection"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais."
|
||||
@ -227,10 +230,14 @@
|
||||
"elements": "Éléments",
|
||||
"height": "Hauteur",
|
||||
"scene": "Scène",
|
||||
"selected": "Sélectionné",
|
||||
"selected": "Sélection",
|
||||
"storage": "Stockage",
|
||||
"title": "Stats pour les nerds",
|
||||
"total": "Total",
|
||||
"width": "Largeur"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Styles copiés.",
|
||||
"copyToClipboardAsPng": "Copié vers le presse-papier en PNG."
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "מצב רשת",
|
||||
"addToLibrary": "הוסף לספריה",
|
||||
"removeFromLibrary": "הסר מספריה",
|
||||
"libraryLoadingMessage": "טוען ספריה...",
|
||||
"libraryLoadingMessage": "טוען ספריה…",
|
||||
"libraries": "דפדף בספריות",
|
||||
"loadingScene": "טוען תצוגה...",
|
||||
"loadingScene": "טוען תצוגה…",
|
||||
"align": "יישר",
|
||||
"alignTop": "יישר למעלה",
|
||||
"alignBottom": "יישר למטה",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "מרכז אנכית",
|
||||
"centerHorizontally": "מרכז אופקית",
|
||||
"distributeHorizontally": "חלוקה אופקית",
|
||||
"distributeVertically": "חלוקה אנכית"
|
||||
"distributeVertically": "חלוקה אנכית",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "אפס את הלוח",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "שגיאה"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "קיצורי מקלדת",
|
||||
"shapes": "צורות",
|
||||
"or": "או",
|
||||
"click": "לחץ",
|
||||
"drag": "גרור",
|
||||
"curvedArrow": "חץ מעוקל",
|
||||
"curvedLine": "קו מעוקל",
|
||||
"editor": "עורך",
|
||||
"view": "תצוגה",
|
||||
"blog": "קרא את הבלוג שלנו",
|
||||
"howto": "עקוב אחר המדריכים שלנו",
|
||||
"github": "מצאת בעיה? דווח",
|
||||
"textNewLine": "הוסף שורה חדשה (טקסט)",
|
||||
"textFinish": "סיים עריכה (טקסט)",
|
||||
"zoomToFit": "זום להתאמת כל האלמנטים למסך",
|
||||
"zoomToSelection": "התמקד בבחירה",
|
||||
"preventBinding": "מנע השתלבות חצים"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "סטטיסטיקות לחנונים",
|
||||
"total": "סה״כ",
|
||||
"width": "רוחב"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,8 @@
|
||||
"centerVertically": "लंबवत केन्द्रित",
|
||||
"centerHorizontally": "क्षैतिज केन्द्रित",
|
||||
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
||||
"distributeVertically": "खड़ी रूप से वितरित करें"
|
||||
"distributeVertically": "खड़ी रूप से वितरित करें",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "कैनवास रीसेट करें",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "गलती"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "कीबोर्ड के शॉर्टकट्स",
|
||||
"shapes": "आकृतियाँ",
|
||||
"or": "या",
|
||||
"click": "क्लिक करें",
|
||||
"drag": "खींचें",
|
||||
"curvedArrow": "घुमावदार तीर",
|
||||
"curvedLine": "घुमावदार रेखा",
|
||||
"editor": "संपादक",
|
||||
"view": "दृश्य",
|
||||
"blog": "हमारा ब्लॉग पढे",
|
||||
"howto": "हमारे गाइड का पालन करें",
|
||||
"github": "एक मुद्दा मिला? प्रस्तुत करे",
|
||||
"textNewLine": "नई पंक्ति (पाठ) जोड़ें",
|
||||
"textFinish": "संपादन समाप्त करें (पाठ)",
|
||||
"zoomToFit": "सभी तत्वों को फिट करने के लिए ज़ूम करें",
|
||||
"zoomToSelection": "सिलेक्शन तक ज़ूम करे",
|
||||
"preventBinding": "तीर बंधन रोकें"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "बेवकूफ के लिए आँकड़े",
|
||||
"total": "कुल",
|
||||
"width": "चौड़ाई"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Hálómód",
|
||||
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
||||
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
||||
"libraryLoadingMessage": "Könyvtár betöltése...",
|
||||
"libraryLoadingMessage": "Könyvtár betöltése…",
|
||||
"libraries": "Könyvtárak böngészése",
|
||||
"loadingScene": "Jelenet betöltése...",
|
||||
"loadingScene": "Jelenet betöltése…",
|
||||
"align": "Igazítás",
|
||||
"alignTop": "Felülre igazítás",
|
||||
"alignBottom": "Alulra igazítás",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Függőlegesen középre igazított",
|
||||
"centerHorizontally": "Vízszintesen középre igazított",
|
||||
"distributeHorizontally": "Vízszintes elosztás",
|
||||
"distributeVertically": "Függőleges elosztás"
|
||||
"distributeVertically": "Függőleges elosztás",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Vászon törlése",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Hiba"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Gyorsbillentyűk",
|
||||
"shapes": "Formák",
|
||||
"or": "vagy",
|
||||
"click": "klikk",
|
||||
"drag": "húzd",
|
||||
"curvedArrow": "Ívelt nyíl",
|
||||
"curvedLine": "Ívelt vonal",
|
||||
"editor": "Szerkesztő",
|
||||
"view": "Nézet",
|
||||
"blog": "Olvasd a blogunkat",
|
||||
"howto": "Kövesd az útmutatóinkat",
|
||||
"github": "Hibát találtál? Küld be",
|
||||
"textNewLine": "Új sor hozzáadása (szöveg)",
|
||||
"textFinish": "Szerkesztés befejezése (szöveg)",
|
||||
"zoomToFit": "Az összes elem látótérbe hozása",
|
||||
"zoomToSelection": "Kijelölésre nagyítás",
|
||||
"preventBinding": "A nyíl ne ragadjon"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Statisztikák",
|
||||
"total": "Összesen",
|
||||
"width": "Szélesség"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Mode grid",
|
||||
"addToLibrary": "Tambahkan ke pustaka",
|
||||
"removeFromLibrary": "Hapus dari pustaka",
|
||||
"libraryLoadingMessage": "Memuat pustaka...",
|
||||
"libraryLoadingMessage": "Memuat pustaka…",
|
||||
"libraries": "Telusur pustaka",
|
||||
"loadingScene": "Memuat pemandangan...",
|
||||
"loadingScene": "Memuat pemandangan…",
|
||||
"align": "Perataan",
|
||||
"alignTop": "Rata atas",
|
||||
"alignBottom": "Rata bawah",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Pusatkan secara vertikal",
|
||||
"centerHorizontally": "Pusatkan secara horizontal",
|
||||
"distributeHorizontally": "Distribusikan horizontal",
|
||||
"distributeVertically": "Distribusikan vertikal"
|
||||
"distributeVertically": "Distribusikan vertikal",
|
||||
"viewMode": "Mode tampilan"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Setel Ulang Kanvas",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Kesalahan"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Pintasan keyboard",
|
||||
"shapes": "Bentuk",
|
||||
"or": "atau",
|
||||
"helpDialog": {
|
||||
"blog": "Baca blog kami",
|
||||
"click": "klik",
|
||||
"drag": "seret",
|
||||
"curvedArrow": "Panah lengkung",
|
||||
"curvedLine": "Garis lengkung",
|
||||
"documentation": "Dokumentasi",
|
||||
"drag": "seret",
|
||||
"editor": "Editor",
|
||||
"view": "Tampilan",
|
||||
"blog": "Baca blog kami",
|
||||
"github": "Menemukan masalah? Kirimkan",
|
||||
"howto": "Ikuti panduan kami",
|
||||
"github": "Menemukan sebuah masalah? Kirimkan",
|
||||
"textNewLine": "Tambahkan baris baru (teks)",
|
||||
"or": "atau",
|
||||
"preventBinding": "Cegah pengikatan panah",
|
||||
"shapes": "Bentuk",
|
||||
"shortcuts": "Pintasan keyboard",
|
||||
"textFinish": "Selesai mengedit (teks)",
|
||||
"textNewLine": "Tambahkan baris baru (teks)",
|
||||
"title": "Bantuan",
|
||||
"view": "Tampilan",
|
||||
"zoomToFit": "Perbesar agar sesuai dengan semua elemen",
|
||||
"zoomToSelection": "Perbesar ke seleksi",
|
||||
"preventBinding": "Cegah pengikatan panah"
|
||||
"zoomToSelection": "Perbesar ke seleksi"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Statistik untuk nerd",
|
||||
"total": "Total",
|
||||
"width": "Lebar"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Gaya tersalin.",
|
||||
"copyToClipboardAsPng": "Tersalin ke clipboard sebagai PNG."
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "Modalità griglia",
|
||||
"addToLibrary": "Aggiungi alla libreria",
|
||||
"removeFromLibrary": "Rimuovi dalla libreria",
|
||||
"libraryLoadingMessage": "Caricamento della biblioteca...",
|
||||
"libraryLoadingMessage": "Caricamento della biblioteca…",
|
||||
"libraries": "Sfoglia librerie",
|
||||
"loadingScene": "Caricamento della scena...",
|
||||
"loadingScene": "Caricamento della scena…",
|
||||
"align": "Allinea",
|
||||
"alignTop": "Allinea in alto",
|
||||
"alignBottom": "Allinea in basso",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "Centra Verticalmente",
|
||||
"centerHorizontally": "Centra orizzontalmente",
|
||||
"distributeHorizontally": "Distribuisci orizzontalmente",
|
||||
"distributeVertically": "Distribuisci verticalmente"
|
||||
"distributeVertically": "Distribuisci verticalmente",
|
||||
"viewMode": "Modalità visualizzazione"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Svuota la tela",
|
||||
@ -163,7 +164,7 @@
|
||||
"text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione",
|
||||
"linearElementMulti": "Clicca sull'ultimo punto o premi Esc o Invio per finire",
|
||||
"lockAngle": "Puoi limitare l'angolo tenendo premuto SHIFT",
|
||||
"resize": "Per vincolare le proporzioni, tenir premuto MAIUSC durante il ridimensionamento;\nper ridimensionare dal centro, tenir premuto ALT",
|
||||
"resize": "Per vincolare le proporzioni, tieni premuto MAIUSC durante il ridimensionamento;\nper ridimensionare dal centro, tieni premuto ALT",
|
||||
"rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione",
|
||||
"lineEditor_info": "Fai doppio click o premi invio per modificare i punti",
|
||||
"lineEditor_pointSelected": "Premere Elimina per rimuovere il punto, CtrlOrCmd+D per duplicare o trascinare per spostare",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "Errore"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "Scorciatoie da tastiera",
|
||||
"shapes": "Forme",
|
||||
"or": "oppure",
|
||||
"helpDialog": {
|
||||
"blog": "Leggi il nostro blog",
|
||||
"click": "click",
|
||||
"drag": "trascina",
|
||||
"curvedArrow": "Freccia curva",
|
||||
"curvedLine": "Linea curva",
|
||||
"documentation": "Documentazione",
|
||||
"drag": "trascina",
|
||||
"editor": "Editor",
|
||||
"view": "Vista",
|
||||
"blog": "Leggi il nostro blog",
|
||||
"github": "Trovato un problema? Segnalalo",
|
||||
"howto": "Segui le nostre guide",
|
||||
"github": "Hai trovato un problema? Segnalalo",
|
||||
"or": "oppure",
|
||||
"preventBinding": "Impedisci legame della freccia",
|
||||
"shapes": "Forme",
|
||||
"shortcuts": "Scorciatoie da tastiera",
|
||||
"textFinish": "Termina la modifica (testo)",
|
||||
"textNewLine": "Aggiungi nuova riga (testo)",
|
||||
"textFinish": "Completa la modifica (testo)",
|
||||
"title": "Guida",
|
||||
"view": "Vista",
|
||||
"zoomToFit": "Adatta zoom per mostrare tutti gli elementi",
|
||||
"zoomToSelection": "Zoom alla selezione",
|
||||
"preventBinding": "Prevenire l'associazione freccia"
|
||||
"zoomToSelection": "Zoom alla selezione"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "Statistiche per nerd",
|
||||
"total": "Totale",
|
||||
"width": "Larghezza"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Stili copiati.",
|
||||
"copyToClipboardAsPng": "Copiato negli appunti come PNG."
|
||||
}
|
||||
}
|
||||
|
@ -30,11 +30,11 @@
|
||||
"edges": "角",
|
||||
"sharp": "四角",
|
||||
"round": "丸",
|
||||
"arrowheads": "",
|
||||
"arrowhead_none": "",
|
||||
"arrowhead_arrow": "",
|
||||
"arrowhead_bar": "",
|
||||
"arrowhead_dot": "",
|
||||
"arrowheads": "線の終点",
|
||||
"arrowhead_none": "なし",
|
||||
"arrowhead_arrow": "矢印",
|
||||
"arrowhead_bar": "バー",
|
||||
"arrowhead_dot": "ドット",
|
||||
"fontSize": "フォントの大きさ",
|
||||
"fontFamily": "フォントの種類",
|
||||
"onlySelected": "選択中のみ",
|
||||
@ -80,9 +80,9 @@
|
||||
"gridMode": "",
|
||||
"addToLibrary": "ライブラリに追加",
|
||||
"removeFromLibrary": "ライブラリから削除",
|
||||
"libraryLoadingMessage": "ライブラリを読み込み中...",
|
||||
"libraryLoadingMessage": "ライブラリを読み込み中…",
|
||||
"libraries": "",
|
||||
"loadingScene": "シーンを読み込み中...",
|
||||
"loadingScene": "シーンを読み込み中…",
|
||||
"align": "整列",
|
||||
"alignTop": "上揃え",
|
||||
"alignBottom": "下揃え",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "縦方向に中央揃え",
|
||||
"centerHorizontally": "横方向に中央揃え",
|
||||
"distributeHorizontally": "",
|
||||
"distributeVertically": ""
|
||||
"distributeVertically": "",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "キャンバスのリセット",
|
||||
@ -118,9 +119,9 @@
|
||||
"redo": "やり直し",
|
||||
"roomDialog": "共同編集を開始する",
|
||||
"createNewRoom": "新しい部屋を作成する",
|
||||
"fullScreen": "",
|
||||
"darkMode": "",
|
||||
"lightMode": "",
|
||||
"fullScreen": "全画面表示",
|
||||
"darkMode": "ダークモード",
|
||||
"lightMode": "ライトモード",
|
||||
"zenMode": "",
|
||||
"exitZenMode": "集中モードをやめる"
|
||||
},
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "エラー"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "キーボードショートカット",
|
||||
"shapes": "図形",
|
||||
"or": "または",
|
||||
"click": "クリック",
|
||||
"drag": "ドラッグ",
|
||||
"curvedArrow": "曲がった矢印",
|
||||
"curvedLine": "曲線",
|
||||
"editor": "エディタ",
|
||||
"view": "表示",
|
||||
"blog": "公式ブログを読む",
|
||||
"howto": "ヘルプ・マニュアル",
|
||||
"github": "不具合報告はこちら",
|
||||
"textNewLine": "テキストの改行",
|
||||
"textFinish": "テキストの編集を終える",
|
||||
"zoomToFit": "すべての図形が収まるよう拡大/縮小",
|
||||
"zoomToSelection": "",
|
||||
"preventBinding": "矢印を結合しない"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
|
||||
@ -225,12 +228,16 @@
|
||||
"angle": "",
|
||||
"element": "",
|
||||
"elements": "",
|
||||
"height": "",
|
||||
"height": "高さ",
|
||||
"scene": "",
|
||||
"selected": "",
|
||||
"storage": "",
|
||||
"title": "",
|
||||
"total": "",
|
||||
"width": ""
|
||||
"total": "合計",
|
||||
"width": "幅"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
243
src/locales/kab-KAB.json
Normal file
243
src/locales/kab-KAB.json
Normal file
@ -0,0 +1,243 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Senṭeḍ",
|
||||
"pasteCharts": "Senṭeḍ udlifen",
|
||||
"selectAll": "Fren akk",
|
||||
"multiSelect": "Rnu aferdis ɣer tefrayt",
|
||||
"moveCanvas": "Smutti taɣzut n usuneɣ",
|
||||
"cut": "Gzem",
|
||||
"copy": "Nɣel",
|
||||
"copyAsPng": "Nɣel ɣer tecfawit am PNG",
|
||||
"copyAsSvg": "Nɣel ɣer tecfawit am SVG",
|
||||
"bringForward": "Awi ɣer sdat",
|
||||
"sendToBack": "Awi s agilal",
|
||||
"bringToFront": "Err ɣer deffir",
|
||||
"sendBackward": "Awi ɣer deffir",
|
||||
"delete": "Kkes",
|
||||
"copyStyles": "Nɣel iɣunab",
|
||||
"pasteStyles": "Senṭeḍ iɣunab",
|
||||
"stroke": "Azizdew",
|
||||
"background": "Agilal",
|
||||
"fill": "Taččart",
|
||||
"strokeWidth": "Tehri n yizirig",
|
||||
"strokeStyle": "Aɣanib n tizirig",
|
||||
"strokeStyle_solid": "Aččuran",
|
||||
"strokeStyle_dashed": "S tjerriḍin",
|
||||
"strokeStyle_dotted": "S tenqiḍin",
|
||||
"sloppiness": "",
|
||||
"opacity": "Tiḍullest",
|
||||
"textAlign": "Areyyec n uḍris",
|
||||
"edges": "Leryuf",
|
||||
"sharp": "Yemsed",
|
||||
"round": "Imdewer",
|
||||
"arrowheads": "Ixfawen n tenccabt",
|
||||
"arrowhead_none": "Ulac",
|
||||
"arrowhead_arrow": "Taneccabt",
|
||||
"arrowhead_bar": "Afeggag",
|
||||
"arrowhead_dot": "Tanqiḍt",
|
||||
"fontSize": "Tiddi n tsefsit",
|
||||
"fontFamily": "Tawacult n tsefsiyin",
|
||||
"onlySelected": "Tafrayt kan",
|
||||
"withBackground": "S ugilal",
|
||||
"exportEmbedScene": "Seddu asayes deg ufaylu yettwasifḍen",
|
||||
"exportEmbedScene_details": "Asayes ad yettwasekles deg ufaylu n usifeḍ PNG/SVG akken akken ad yili wamek ara d-yettwarr seg-s usayes. Ayagi ad isimɣur tiddi n ufaylu n usifeḍ.",
|
||||
"addWatermark": "Seddu \"Yettwaxdem s Excalidraw\"",
|
||||
"handDrawn": "Asuneɣ s ufus",
|
||||
"normal": "Amagnu",
|
||||
"code": "Tangalt",
|
||||
"small": "Meẓẓi",
|
||||
"medium": "Alemmas",
|
||||
"large": "Ameqran",
|
||||
"veryLarge": "Meqqer aṭas",
|
||||
"solid": "Aččuran",
|
||||
"hachure": "Azerreg",
|
||||
"crossHatch": "Azerreg anmidag",
|
||||
"thin": "Arqaq",
|
||||
"bold": "Azuran",
|
||||
"left": "Azelmaḍ",
|
||||
"center": "Talemmast",
|
||||
"right": "Ayfus",
|
||||
"extraBold": "Azuran aṭas",
|
||||
"architect": "Amasdag",
|
||||
"artist": "Anaẓur",
|
||||
"cartoonist": "",
|
||||
"fileTitle": "Azwel n ufaylu",
|
||||
"colorPicker": "Amafran n yini",
|
||||
"canvasBackground": "Agilal n teɣzut n usuneɣ",
|
||||
"drawingCanvas": "Taɣzut n usuneɣ",
|
||||
"layers": "Tissiyin",
|
||||
"actions": "Tigawin",
|
||||
"language": "Tutlayt",
|
||||
"createRoom": "Bḍu tiɣimit n umɛawen s srid",
|
||||
"duplicateSelection": "Sisleg",
|
||||
"untitled": "War azwel",
|
||||
"name": "Isem",
|
||||
"yourName": "Isem-ik (im)",
|
||||
"madeWithExcalidraw": "Yettwaxdem s Excalidraw",
|
||||
"group": "Segrew tafrayt",
|
||||
"ungroup": "Kkess asegrew i tefrayt",
|
||||
"collaborators": "Imɛiwnen",
|
||||
"gridMode": "Askar n uferrug",
|
||||
"addToLibrary": "Rnu ɣer temkarḍit",
|
||||
"removeFromLibrary": "Kkes si temkarḍit",
|
||||
"libraryLoadingMessage": "Asali n temkarḍit…",
|
||||
"libraries": "Snirem timkarḍiyin",
|
||||
"loadingScene": "Asali n usayes…",
|
||||
"align": "Reyyec",
|
||||
"alignTop": "Areyyec uksawen",
|
||||
"alignBottom": "Areyyec ukessar",
|
||||
"alignLeft": "Reyyec s azelmaḍ",
|
||||
"alignRight": "Areyyec s ayfus",
|
||||
"centerVertically": "Di tlemmast s ibeddi",
|
||||
"centerHorizontally": "Di tlemmast s uglawi",
|
||||
"distributeHorizontally": "Freq s uglawi",
|
||||
"distributeVertically": "Freq s yibeddi",
|
||||
"viewMode": "Askar n tmuɣli"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Ales awennez n teɣzut n usuneɣ",
|
||||
"export": "Sifeḍ",
|
||||
"exportToPng": "Sifeḍ ɣer PNG",
|
||||
"exportToSvg": "Sifeḍ ɣer SVG",
|
||||
"copyToClipboard": "Nɣel ɣer tecfawit",
|
||||
"copyPngToClipboard": "Nɣel PNG ɣer tecfawit",
|
||||
"scale": "Taskala",
|
||||
"save": "Sekles",
|
||||
"saveAs": "Sekles am",
|
||||
"load": "Sali-d",
|
||||
"getShareableLink": "Awi-d aseɣwen n beṭṭu",
|
||||
"close": "Mdel",
|
||||
"selectLanguage": "Fren tutlayt",
|
||||
"scrollBackToContent": "Uɣal s agbur",
|
||||
"zoomIn": "Simɣur",
|
||||
"zoomOut": "Simẓi",
|
||||
"resetZoom": "Ales awennez n usemɣer",
|
||||
"menu": "Umuɣ",
|
||||
"done": "Ifukk",
|
||||
"edit": "Ẓreg",
|
||||
"undo": "Sefsex",
|
||||
"redo": "Err-d",
|
||||
"roomDialog": "Bdu amɛawen s srid",
|
||||
"createNewRoom": "Snulfu-d taxxamt tamaynutt",
|
||||
"fullScreen": "Agdil aččuran",
|
||||
"darkMode": "Askar imsulles",
|
||||
"lightMode": "Askar afaw",
|
||||
"zenMode": "Askar Zen",
|
||||
"exitZenMode": "Ffeɣ seg uskar Zen"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?",
|
||||
"couldNotCreateShareableLink": "D awezɣi asnulfu n useɣwen n beṭṭu.",
|
||||
"couldNotCreateShareableLinkTooBig": "D awezɣi asnulfu n useɣwen n beṭṭu. Asayes ɣezzif aṭas",
|
||||
"couldNotLoadInvalidFile": "D awezɣi asali n ufaylu armeɣtu",
|
||||
"importBackendFailed": "",
|
||||
"cannotExportEmptyCanvas": "D awezɣi asifeḍ n teɣzut n usuneɣ tilemt.",
|
||||
"couldNotCopyToClipboard": "D awezɣi anɣal ɣer tecfawit. Eɛreḍ ad tesqedceḍ iminig Chrome.",
|
||||
"decryptFailed": "D awezɣi tukksa n uwgelhen i yisefka.",
|
||||
"uploadedSecurly": "Asili yettwasɣelles s uwgelhen ixef s ixef, ayagi yebɣa ad d-yini belli aqeddac n Excalidraw akked medden ur zmiren ara ad ɣren agbur.",
|
||||
"loadSceneOverridePrompt": "Asali n wunuɣ uffiɣ ad isemselsi agbur-inek (m) yellan. Tebɣiḍ ad tkemmeleḍ?",
|
||||
"errorLoadingLibrary": "Teḍra-d tuccḍa deg usali n temkarḍit n wis kraḍ.",
|
||||
"confirmAddLibrary": "Ayagi adirnu talɣa (win) {{numShapes}} ɣer temkarḍit-inek (m). Tetḥeqqeḍ?",
|
||||
"imageDoesNotContainScene": "Taktert n tugniwin ur tettwadhel ara akka tura.\nTebɣiḍ ad tketreḍ asayes? Tugna-agi tettban-d ur tegbir ara isefka n usnas. Tesremdeḍ ayagi deg usifeḍ?",
|
||||
"cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Tafrayt",
|
||||
"draw": "Unuɣ ilelli",
|
||||
"rectangle": "Asrem",
|
||||
"diamond": "Ameɣṛun",
|
||||
"ellipse": "Taglayt",
|
||||
"arrow": "Taneccabt",
|
||||
"line": "Izirig",
|
||||
"text": "Aḍris",
|
||||
"library": "Tamkarḍit",
|
||||
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Tigawin n teɣzut n usuneɣ",
|
||||
"selectedShapeActions": "Tigawin n talɣa yettwafernen",
|
||||
"shapes": "Talɣiwin"
|
||||
},
|
||||
"hints": {
|
||||
"linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig",
|
||||
"freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ",
|
||||
"text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt",
|
||||
"linearElementMulti": "Ssit ɣef tenqiḍt taneggarut neɣ ssed taqeffalt Escape neɣ taqeffalt Kcem akken ad tfakkeḍ",
|
||||
"lockAngle": "Tzemreḍ ad tḥettmeḍ tiɣmert s tuṭṭfa n tqeffalt SHIFT",
|
||||
"resize": "Tzemreḍ ad tḥettemeḍ assaɣ s tuṭṭfa n tqeffalt SHIFT mi ara tettbeddileḍ tiddi,\nma teṭṭfeḍ ALT abeddel n tiddi ad yili si tlemmast",
|
||||
"rotate": "Tzemreḍ ad tḥettemeḍ tiɣemmar s tuṭṭfa n SHIFT di tuzzya",
|
||||
"lineEditor_info": "Ssit snat n tikkal neɣ ssed taqeffalt Kcem akken ad tẓergeḍ tinqiḍin",
|
||||
"lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍt, CtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ",
|
||||
"lineEditor_nothingSelected": "Fren tanqiḍt ara tesmuttiḍ neɣ ara tekkseḍ, neɣ ṭṭef taqeffalt Alt akken ad ternuḍ tinqiḍin timaynutin"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Ulamek abeqqeḍ n teskant",
|
||||
"canvasTooBig": "Taɣzut n usuneɣ tezmer ad tili temeqqer aṭas.",
|
||||
"canvasTooBigTip": "Tixidest: eɛreḍ ad tesqerbeḍ ciṭ iferdisen yembaɛaden."
|
||||
},
|
||||
"errorSplash": {
|
||||
"headingMain_pre": "Teḍra-d tuccḍa. Eɛreḍ ",
|
||||
"headingMain_button": "asali n usebter tikkelt-nniḍen.",
|
||||
"clearCanvasMessage": "Ma yella tulsa n usali ur tefri ara ugur, eɛreḍ ",
|
||||
"clearCanvasMessage_button": "asfaḍ n teɣzut n usuneɣ.",
|
||||
"clearCanvasCaveat": " Ayagi ad d-iglu s usṛuḥu n umahil ",
|
||||
"trackedToSentry_pre": "Tuccḍa akked umesmagi ",
|
||||
"trackedToSentry_post": " tettwasekles deg unagraw-nneɣ.",
|
||||
"openIssueMessage_pre": "",
|
||||
"openIssueMessage_button": "afecku n weḍfar n yibugen.",
|
||||
"openIssueMessage_post": " Ma ulac uɣilif seddu talɣut ukessar-agi s wenɣal akked usenṭeḍ di GitHub issue.",
|
||||
"sceneContent": "Agbur n usayes:"
|
||||
},
|
||||
"roomDialog": {
|
||||
"desc_intro": "Tzemreḍ ad d-teɛerḍeḍ medden ɣer usayes-inek (m) amiran akken ad ttekkin yid-k.",
|
||||
"desc_privacy": "Ur tqelliq ara, tiɣimit tsseqdac awgelhen ixef s ixef, dɣa ayen ara tsunɣeḍ ad iqqim d amaẓlay. Ula d aqeddac-nneɣ ur yezmir ara ad iwali acu txeddemeḍ.",
|
||||
"button_startSession": "Bdu tiɣimit",
|
||||
"button_stopSession": "Ḥbes tiɣimit",
|
||||
"desc_inProgressIntro": "Tiɣimit n umɛawen s srid tetteddu akka tura.",
|
||||
"desc_shareLink": "Bḍu aseɣwen-agi akked medden ukud tebɣiḍ ad temɛawaneḍ:",
|
||||
"desc_exitSession": "Aḥbas n tɣimit ad k (m) yesenser si texxamt, maca ad tizmireḍ ad tkemmeleḍ amahil s usayes, s wudem adigan. Ẓer belli ayagi ur yettḥaz ara imdanen-nniḍen, yerna ad izmiren ad kemmelen ad mɛawanen di tsuffeɣt-nnsen."
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Tuccḍa"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "Ɣeṛ ablug-nneɣ",
|
||||
"click": "ssit",
|
||||
"curvedArrow": "Taneccabt izelgen",
|
||||
"curvedLine": "Izirig izelgen",
|
||||
"documentation": "Tasemlit",
|
||||
"drag": "zuɣer",
|
||||
"editor": "Amaẓrag",
|
||||
"github": "Tufiḍ-d ugur? Azen-aɣ-d",
|
||||
"howto": "Ḍfer imniren-nneɣ",
|
||||
"or": "neɣ",
|
||||
"preventBinding": "",
|
||||
"shapes": "Talɣiwin",
|
||||
"shortcuts": "Inegzumen n unasiw",
|
||||
"textFinish": "Fak asiẓreg (aḍris)",
|
||||
"textNewLine": "Rnu ajerriḍ amaynut (aḍris)",
|
||||
"title": "Tallelt",
|
||||
"view": "Tamuɣli",
|
||||
"zoomToFit": "Simɣur akken ad twliḍ akk iferdisen",
|
||||
"zoomToSelection": "Simɣur ɣer tefrayt"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. "
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Tiɣmeṛt",
|
||||
"element": "Aferdis",
|
||||
"elements": "Iferdisen",
|
||||
"height": "Tattayt",
|
||||
"scene": "Asayes",
|
||||
"selected": "Yettwafren",
|
||||
"storage": "Aḥraz",
|
||||
"title": "",
|
||||
"total": "Aɣrud",
|
||||
"width": "Tehri"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Iɣunab yettwaneɣlen.",
|
||||
"copyToClipboardAsPng": "Yettwanɣel ɣer tecfawit am PNG."
|
||||
}
|
||||
}
|
@ -91,7 +91,8 @@
|
||||
"centerVertically": "수직으로 중앙 정렬",
|
||||
"centerHorizontally": "수평으로 중앙 정렬",
|
||||
"distributeHorizontally": "수평으로 분배",
|
||||
"distributeVertically": "수직으로 분배"
|
||||
"distributeVertically": "수직으로 분배",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "캔버스 초기화",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "오류"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "키보드 단축키",
|
||||
"shapes": "모양",
|
||||
"or": "또는",
|
||||
"click": "클릭",
|
||||
"drag": "드래그",
|
||||
"curvedArrow": "곡선 화살표",
|
||||
"curvedLine": "곡선",
|
||||
"editor": "편집",
|
||||
"view": "보기",
|
||||
"blog": "블로그 읽어보기",
|
||||
"howto": "가이드 참고하기",
|
||||
"github": "이슈 제보하기",
|
||||
"textNewLine": "줄바꿈 (텍스트)",
|
||||
"textFinish": "편집 완료 (텍스트)",
|
||||
"zoomToFit": "모든 요소가 보이도록 확대/축소",
|
||||
"zoomToSelection": "선택 영역으로 확대/축소",
|
||||
"preventBinding": "화살표가 붙지 않게 하기"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다."
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "덕후들을 위한 통계",
|
||||
"total": "합계",
|
||||
"width": "너비"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,9 @@
|
||||
"gridMode": "",
|
||||
"addToLibrary": "မှတ်တမ်းတင်",
|
||||
"removeFromLibrary": "မှတ်တမ်းမှထုတ်",
|
||||
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်...",
|
||||
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်…",
|
||||
"libraries": "စာကြည့်တိုက်တွင်ရှာဖွေပါ",
|
||||
"loadingScene": "မြင်ကွင်းဖော်နေသည်...",
|
||||
"loadingScene": "မြင်ကွင်းဖော်နေသည်…",
|
||||
"align": "ချိန်ညှိ",
|
||||
"alignTop": "ထိပ်ညှိ",
|
||||
"alignBottom": "အခြေညှိ",
|
||||
@ -91,7 +91,8 @@
|
||||
"centerVertically": "ဒေါင်လိုက်အလယ်ညှိ",
|
||||
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
|
||||
"distributeHorizontally": "အလျားလိုက်",
|
||||
"distributeVertically": "ထောင်လိုက်"
|
||||
"distributeVertically": "ထောင်လိုက်",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "ကားချပ်ရှင်းလင်း",
|
||||
@ -199,24 +200,26 @@
|
||||
"errorDialog": {
|
||||
"title": "ချို့ယွင်းချက်"
|
||||
},
|
||||
"shortcutsDialog": {
|
||||
"title": "ကီးဘုတ်ရှော့ကတ်များ",
|
||||
"shapes": "ပုံသဏ္ဌာန်",
|
||||
"or": "(သို့)",
|
||||
"click": "ကလစ်နှိပ်",
|
||||
"drag": "တရွတ်ဆွဲ",
|
||||
"curvedArrow": "မြှားကွေး",
|
||||
"curvedLine": "မျဉ်းကွေး",
|
||||
"editor": "တည်းဖြတ်",
|
||||
"view": "မြင်ကွင်း",
|
||||
"blog": "ဘလော့ဂ်တွင်လေ့လာပါ",
|
||||
"howto": "အညွှန်း",
|
||||
"github": "ချို့ယွင်းမှုအတွက်အသိပေးရန်",
|
||||
"textNewLine": "စာသားဖြည့်သွင်း",
|
||||
"textFinish": "စာသားဖြည့်သွင်းပြီး",
|
||||
"zoomToFit": "ကားချပ်အပြည့်ဖေါ်",
|
||||
"zoomToSelection": "",
|
||||
"preventBinding": "မြှားများမပေါင်းစေရန်"
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။"
|
||||
@ -232,5 +235,9 @@
|
||||
"title": "အက္ခရာများအတွက်အချက်အလက်များ",
|
||||
"total": "စုစုပေါင်း",
|
||||
"width": "အကျယ်"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user