Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8802ea62ca | |||
| 3c86b014de | |||
| 02598c6163 | |||
| 3e2890bd21 | |||
| 210649f383 | |||
| f8087e01c8 | |||
| e2522645f7 | |||
| d46a9166be | |||
| 675da16ca4 | |||
| 2b1b62d8f2 | |||
| 627c56ef1c | |||
| 0d0fe32485 | |||
| 6576b9442e | |||
| ee4cb2d4a9 | |||
| cdcc91faa5 | |||
| 9093341dc1 | |||
| 1973ae9444 | |||
| 10cd6a24b0 | |||
| 6abf4f52ff | |||
| 4624ec2bd6 | |||
| e8685c5236 | |||
| 6e9df2bae7 | |||
| ed0bec41dc | |||
| d4e12a2962 | |||
| 978e85a33b | |||
| b5e26ba81f | |||
| 4392a4644a | |||
| b1c8c538ee | |||
| f6492895df | |||
| e75f5f20e7 | |||
| 0a0be839b9 | |||
| 03f6d9c783 | |||
| df745c1098 | |||
| b888f7e7ba | |||
| 3010253f72 | |||
| 0815f3282e | |||
| 9dd58288de | |||
| c2e2bb495c | |||
| a31cfe1f07 | |||
| d3367bfe12 | |||
| 70791dfa7b | |||
| d63ec678db | |||
| 26acebcdb6 | |||
| 9dc930b447 | |||
| 6e767fc949 | |||
| c6f06dd1fc | |||
| 922e28a198 | |||
| 49363afac1 | |||
| 28a1b9a787 | |||
| 9cfab40343 | |||
| 1d7c5705b2 | |||
| 40cd4caeec |
@@ -6,7 +6,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Setup Node.js 12.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
name: Cancel
|
name: Cancel previous runs
|
||||||
on: [push]
|
|
||||||
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cancel:
|
cancel:
|
||||||
name: "Cancel Previous Runs"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
steps:
|
steps:
|
||||||
- uses: styfle/cancel-workflow-action@0.6.0
|
- uses: styfle/cancel-workflow-action@0.6.0
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -13,10 +9,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Setup Node.js 12.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and lint
|
- name: Install and lint
|
||||||
run: |
|
run: |
|
||||||
@@ -24,5 +20,3 @@ jobs:
|
|||||||
npm run test:other
|
npm run test:other
|
||||||
npm run test:code
|
npm run test:code
|
||||||
npm run test:typecheck
|
npm run test:typecheck
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js 12.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
npm run locales-coverage
|
npm run locales-coverage
|
||||||
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
||||||
if [ ! -z "${FILE_CHANGED}" ]; then
|
if [ ! -z "${FILE_CHANGED}" ]; then
|
||||||
git config --global user.name 'Kostas Bariotis'
|
git config --global user.name 'Excalidraw Bot'
|
||||||
git config --global user.email 'konmpar@gmail.com'
|
git config --global user.email 'bot@excalidraw.com'
|
||||||
git add src/locales/percentages.json
|
git add src/locales/percentages.json
|
||||||
git commit -am "Auto commit: Calculate translation coverage"
|
git commit -am "Auto commit: Calculate translation coverage"
|
||||||
git push
|
git push
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
- uses: amannn/action-semantic-pull-request@v3.0.0
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1.0.0
|
- uses: actions/checkout@v1.0.0
|
||||||
|
|
||||||
- name: Setup Node.js 12.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -13,14 +9,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Setup Node.js 12.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm run test:app
|
npm run test:app
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
|
|||||||
Generated
+86
-125
@@ -4,9 +4,9 @@
|
|||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/json-schema-ref-parser": {
|
"@apidevtools/json-schema-ref-parser": {
|
||||||
"version": "9.0.6",
|
"version": "9.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz",
|
||||||
"integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==",
|
"integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jsdevtools/ono": "^7.1.3",
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
@@ -1308,9 +1308,9 @@
|
|||||||
"integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA=="
|
"integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA=="
|
||||||
},
|
},
|
||||||
"@firebase/app": {
|
"@firebase/app": {
|
||||||
"version": "0.6.13",
|
"version": "0.6.14",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.14.tgz",
|
||||||
"integrity": "sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ==",
|
"integrity": "sha512-ZQKuiJ+fzr4tULgWoXbW+AZVTGsejOkSrlQ+zx78WiGKIubpFJLklnP3S0oYr/1nHzr4vaKuM4G8IL1Wv/+MpQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@firebase/app-types": "0.6.1",
|
"@firebase/app-types": "0.6.1",
|
||||||
"@firebase/component": "0.1.21",
|
"@firebase/component": "0.1.21",
|
||||||
@@ -1334,9 +1334,9 @@
|
|||||||
"integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg=="
|
"integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg=="
|
||||||
},
|
},
|
||||||
"@firebase/auth": {
|
"@firebase/auth": {
|
||||||
"version": "0.16.1",
|
"version": "0.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.2.tgz",
|
||||||
"integrity": "sha512-7juD7D/kaxNti/xa5G+ZGJJs+bdJUWOW0MlNBtXwiG+TjMh69EDmwJnQmmc9h/32QVvXt1qo1OGWOoMMpF/2Gg==",
|
"integrity": "sha512-68TlDL0yh3kF8PiCzI8m8RWd/bf/xCLUsdz1NZ2Dwea0sp6e2WAhu0sem1GfhwuEwL+Ns4jCdX7qbe/OQlkVEA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@firebase/auth-types": "0.10.1"
|
"@firebase/auth-types": "0.10.1"
|
||||||
}
|
}
|
||||||
@@ -1368,9 +1368,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@firebase/database": {
|
"@firebase/database": {
|
||||||
"version": "0.8.3",
|
"version": "0.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.9.1.tgz",
|
||||||
"integrity": "sha512-i29rr3kcPltIkA8La9M1lgsSxx9bfu5lCQ0T+tbJptZ3UpqpcL1NzCcZa24cJjiLgq3HQNPyLvUvCtcPSFDlRg==",
|
"integrity": "sha512-JdxgNvniSZiAx+lrdAQxkCZOTv+UfdmhRm9JA4RTs4XOpvwzmRtJTAIGBn+9CWXUAkWkjt5CYHLmYysD7NGj6g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@firebase/auth-interop-types": "0.1.5",
|
"@firebase/auth-interop-types": "0.1.5",
|
||||||
"@firebase/component": "0.1.21",
|
"@firebase/component": "0.1.21",
|
||||||
@@ -1405,9 +1405,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@firebase/firestore": {
|
"@firebase/firestore": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.4.tgz",
|
||||||
"integrity": "sha512-8yUdBLLr6UhE+IjPR+fxLBD0bDnEqF9GalohfURZeLQPaL3b+LtqqGCLvvXC4MKT0lJAHOV8J9LA6rHj8vI0/Q==",
|
"integrity": "sha512-chSOvJyVoS7HmH7YOyqQP66wMwmsYNo2nPbFkrmQM/fRGXntNxXD1Greu1uts2hNyNeDLNrFHW5y7PlE3LAbwQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@firebase/component": "0.1.21",
|
"@firebase/component": "0.1.21",
|
||||||
"@firebase/firestore-types": "2.1.0",
|
"@firebase/firestore-types": "2.1.0",
|
||||||
@@ -2663,125 +2663,86 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sentry/browser": {
|
"@sentry/browser": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.3.tgz",
|
||||||
"integrity": "sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw==",
|
"integrity": "sha512-Ukxh83Twql4UmUgds9wPWllE62NG71cYvm5AM6daTojvM8wFR2jh7G6GiA0WYfgMb2fw6SlbevB2xb6RDG5DzQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sentry/core": "5.30.0",
|
"@sentry/core": "6.0.3",
|
||||||
"@sentry/types": "5.30.0",
|
"@sentry/types": "6.0.3",
|
||||||
"@sentry/utils": "5.30.0",
|
"@sentry/utils": "6.0.3",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/types": {
|
|
||||||
"version": "5.30.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
|
|
||||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
|
|
||||||
},
|
|
||||||
"@sentry/utils": {
|
|
||||||
"version": "5.30.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
|
|
||||||
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/types": "5.30.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sentry/core": {
|
"@sentry/core": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.3.tgz",
|
||||||
"integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==",
|
"integrity": "sha512-UykB/4/98y2DkNvwTiL2ofFPuK3KDHc7rIRNsdj6dg6D+Cf7FRexgmWUUkZrpC/y+QBj0TPqkcFDcZAuQDa3Ag==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sentry/hub": "5.30.0",
|
"@sentry/hub": "6.0.3",
|
||||||
"@sentry/minimal": "5.30.0",
|
"@sentry/minimal": "6.0.3",
|
||||||
"@sentry/types": "5.30.0",
|
"@sentry/types": "6.0.3",
|
||||||
"@sentry/utils": "5.30.0",
|
"@sentry/utils": "6.0.3",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/types": {
|
|
||||||
"version": "5.30.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
|
|
||||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
|
|
||||||
},
|
|
||||||
"@sentry/utils": {
|
|
||||||
"version": "5.30.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
|
|
||||||
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/types": "5.30.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sentry/hub": {
|
"@sentry/hub": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.3.tgz",
|
||||||
"integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==",
|
"integrity": "sha512-BfV32tE09rjTWM9W0kk8gzxUC2k1h57Z5dNWJ35na79+LguNNtCcI6fHlFQ3PkJca6ITYof9FI8iQHUfsHFZnw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sentry/types": "5.30.0",
|
"@sentry/types": "6.0.3",
|
||||||
"@sentry/utils": "5.30.0",
|
"@sentry/utils": "6.0.3",
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@sentry/integrations": {
|
||||||
|
"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": "6.0.3",
|
||||||
|
"@sentry/utils": "6.0.3",
|
||||||
|
"localforage": "^1.8.1",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/types": {
|
"@sentry/types": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.3.tgz",
|
||||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
|
"integrity": "sha512-266aBQbk9AGedhG2dzXshWbn23LYLElXqlI74DLku48UrU2v7TGKdyik/8/nfOfquCoRSp0GFGYHbItwU124XQ=="
|
||||||
},
|
},
|
||||||
"@sentry/utils": {
|
"@sentry/utils": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.3.tgz",
|
||||||
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
|
"integrity": "sha512-lvuBFvZHYs1zYwI8dkC8Z8ryb0aYnwPFUl1rbZiMwJpYI2Dgl1jpqqZWv9luux2rSRYOMid74uGedV708rvEgA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sentry/types": "5.30.0",
|
"@sentry/types": "6.0.3",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sentry/integrations": {
|
|
||||||
"version": "5.30.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.30.0.tgz",
|
|
||||||
"integrity": "sha512-Fqh4ALLoQWdd+1ih0iBduANWFyNmFWMxwvBu3V/wLDRi8OcquI0lEzWai1InzTJTiNhRHPnhuU++l/vkO0OCww==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/types": "5.30.0",
|
|
||||||
"@sentry/utils": "5.30.0",
|
|
||||||
"localforage": "1.8.1",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/minimal": {
|
"@sentry/minimal": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.3.tgz",
|
||||||
"integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==",
|
"integrity": "sha512-YsW+nw0SMyyb7UQdjZeKlZjxbGsJFpXNLh9iIp6fHKnoLTTv17YPm2ej9sOikDsQuVotaPg/xn/Qt5wySGHIxw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sentry/hub": "5.30.0",
|
"@sentry/hub": "6.0.3",
|
||||||
"@sentry/types": "5.30.0",
|
"@sentry/types": "6.0.3",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/types": {
|
|
||||||
"version": "5.30.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
|
|
||||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sentry/types": {
|
"@sentry/types": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.3.tgz",
|
||||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
|
"integrity": "sha512-266aBQbk9AGedhG2dzXshWbn23LYLElXqlI74DLku48UrU2v7TGKdyik/8/nfOfquCoRSp0GFGYHbItwU124XQ=="
|
||||||
},
|
},
|
||||||
"@sentry/utils": {
|
"@sentry/utils": {
|
||||||
"version": "5.30.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.3.tgz",
|
||||||
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
|
"integrity": "sha512-lvuBFvZHYs1zYwI8dkC8Z8ryb0aYnwPFUl1rbZiMwJpYI2Dgl1jpqqZWv9luux2rSRYOMid74uGedV708rvEgA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sentry/types": "5.30.0",
|
"@sentry/types": "6.0.3",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5155,10 +5116,10 @@
|
|||||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||||
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
|
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
|
||||||
},
|
},
|
||||||
"browser-nativefs": {
|
"browser-fs-access": {
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.13.0.tgz",
|
||||||
"integrity": "sha512-ZCHJcQI6bBm9YjB+6wMT1nWg+/mnWnz7r3gJ8sx7RjgLtWROFq+BuD12cAncD6y45MIbUqFM8eMKXoHXOxSFxA=="
|
"integrity": "sha512-qP8zFVhRQThxYgBXdlFHbzIrWb1us0G5kL2ZL0vW4BO5llKE4qBAcQsQrw4KN+6vjw8sKeWaGWJtzijfRT4N0Q=="
|
||||||
},
|
},
|
||||||
"browser-process-hrtime": {
|
"browser-process-hrtime": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -7987,9 +7948,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-config-prettier": {
|
"eslint-config-prettier": {
|
||||||
"version": "7.1.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz",
|
||||||
"integrity": "sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==",
|
"integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"eslint-config-react-app": {
|
"eslint-config-react-app": {
|
||||||
@@ -9067,16 +9028,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firebase": {
|
"firebase": {
|
||||||
"version": "8.2.3",
|
"version": "8.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.5.tgz",
|
||||||
"integrity": "sha512-WdbcGSiLxiW/kGZT+EyqD9z3Md7kR35+k9qMjDn/twiIrm6Hh7Qi/Z69cqxhKW6+4uK5ghXIF28CjK67OyD9Qw==",
|
"integrity": "sha512-x9KUJR8PvqLUNzNKWHjAnO7rJVgK546G0F+vjlJTNl+J/8oFTdWh8X4PvYda0z0XM68A2Y9xPGf3blz5qHCn0A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@firebase/analytics": "0.6.2",
|
"@firebase/analytics": "0.6.2",
|
||||||
"@firebase/app": "0.6.13",
|
"@firebase/app": "0.6.14",
|
||||||
"@firebase/app-types": "0.6.1",
|
"@firebase/app-types": "0.6.1",
|
||||||
"@firebase/auth": "0.16.1",
|
"@firebase/auth": "0.16.2",
|
||||||
"@firebase/database": "0.8.3",
|
"@firebase/database": "0.9.1",
|
||||||
"@firebase/firestore": "2.1.2",
|
"@firebase/firestore": "2.1.4",
|
||||||
"@firebase/functions": "0.6.1",
|
"@firebase/functions": "0.6.1",
|
||||||
"@firebase/installations": "0.4.19",
|
"@firebase/installations": "0.4.19",
|
||||||
"@firebase/messaging": "0.7.3",
|
"@firebase/messaging": "0.7.3",
|
||||||
@@ -9088,9 +9049,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firebase-tools": {
|
"firebase-tools": {
|
||||||
"version": "9.2.1",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.2.tgz",
|
||||||
"integrity": "sha512-sD4wfB5hs/8IKXV6AJOmkpvXf/St7gVc9QeW4Qz21PG7CkirgRf6FqcYkPKtBcro4wfj48dihnYx/IO1+XPTGg==",
|
"integrity": "sha512-AFjf7S9NjEM+u8ZByJEKASxRG1g+LLg/A0CrzA3V91P92MN+8cyrCigEs7mCdtFknLaShrCgzROyo/OEwd4xdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@google-cloud/pubsub": "^2.7.0",
|
"@google-cloud/pubsub": "^2.7.0",
|
||||||
@@ -11562,9 +11523,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-regex": {
|
"ip-regex": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
|
||||||
"integrity": "sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==",
|
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14301,9 +14262,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"localforage": {
|
"localforage": {
|
||||||
"version": "1.8.1",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
|
||||||
"integrity": "sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ==",
|
"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"lie": "3.1.1"
|
"lie": "3.1.1"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -19,17 +19,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "5.30.0",
|
"@sentry/browser": "6.0.3",
|
||||||
"@sentry/integrations": "5.30.0",
|
"@sentry/integrations": "6.0.3",
|
||||||
"@testing-library/jest-dom": "5.11.9",
|
"@testing-library/jest-dom": "5.11.9",
|
||||||
"@testing-library/react": "11.2.3",
|
"@testing-library/react": "11.2.3",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@types/react": "17.0.0",
|
"@types/react": "17.0.0",
|
||||||
"@types/react-dom": "17.0.0",
|
"@types/react-dom": "17.0.0",
|
||||||
"@types/socket.io-client": "1.4.35",
|
"@types/socket.io-client": "1.4.35",
|
||||||
"browser-nativefs": "0.12.0",
|
"browser-fs-access": "0.13.0",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.2.3",
|
"firebase": "8.2.5",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.0.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.1.20",
|
"nanoid": "3.1.20",
|
||||||
@@ -51,9 +51,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.6",
|
||||||
"@types/pako": "1.0.1",
|
"@types/pako": "1.0.1",
|
||||||
"eslint-config-prettier": "7.1.0",
|
"eslint-config-prettier": "7.2.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"firebase-tools": "9.2.1",
|
"firebase-tools": "9.2.2",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"jest-canvas-mock": "2.3.0",
|
"jest-canvas-mock": "2.3.0",
|
||||||
"lint-staged": "10.5.3",
|
"lint-staged": "10.5.3",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transformIgnorePatterns": [
|
"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
|
"resetMocks": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const crowdinMap = {
|
|||||||
"id-ID": "en-id",
|
"id-ID": "en-id",
|
||||||
"it-IT": "en-it",
|
"it-IT": "en-it",
|
||||||
"ja-JP": "en-ja",
|
"ja-JP": "en-ja",
|
||||||
|
"kab-KAB": "en-kab",
|
||||||
"ko-KR": "en-ko",
|
"ko-KR": "en-ko",
|
||||||
"my-MM": "en-my",
|
"my-MM": "en-my",
|
||||||
"nb-NO": "en-nb",
|
"nb-NO": "en-nb",
|
||||||
@@ -40,7 +41,7 @@ const crowdinMap = {
|
|||||||
const flags = {
|
const flags = {
|
||||||
"ar-SA": "🇸🇦",
|
"ar-SA": "🇸🇦",
|
||||||
"bg-BG": "🇧🇬",
|
"bg-BG": "🇧🇬",
|
||||||
"ca-ES": "🇪🇸",
|
"ca-ES": "🏳",
|
||||||
"de-DE": "🇩🇪",
|
"de-DE": "🇩🇪",
|
||||||
"el-GR": "🇬🇷",
|
"el-GR": "🇬🇷",
|
||||||
"es-ES": "🇪🇸",
|
"es-ES": "🇪🇸",
|
||||||
@@ -53,6 +54,7 @@ const flags = {
|
|||||||
"id-ID": "🇮🇩",
|
"id-ID": "🇮🇩",
|
||||||
"it-IT": "🇮🇹",
|
"it-IT": "🇮🇹",
|
||||||
"ja-JP": "🇯🇵",
|
"ja-JP": "🇯🇵",
|
||||||
|
"kab-KAB": "🏳",
|
||||||
"ko-KR": "🇰🇷",
|
"ko-KR": "🇰🇷",
|
||||||
"my-MM": "🇲🇲",
|
"my-MM": "🇲🇲",
|
||||||
"nb-NO": "🇳🇴",
|
"nb-NO": "🇳🇴",
|
||||||
@@ -88,6 +90,7 @@ const languages = {
|
|||||||
"id-ID": "Bahasa Indonesia",
|
"id-ID": "Bahasa Indonesia",
|
||||||
"it-IT": "Italiano",
|
"it-IT": "Italiano",
|
||||||
"ja-JP": "日本語",
|
"ja-JP": "日本語",
|
||||||
|
"kab-KAB": "Taqbaylit",
|
||||||
"ko-KR": "한국어",
|
"ko-KR": "한국어",
|
||||||
"my-MM": "Burmese",
|
"my-MM": "Burmese",
|
||||||
"nb-NO": "Norsk bokmål",
|
"nb-NO": "Norsk bokmål",
|
||||||
|
|||||||
@@ -17,6 +17,5 @@ export const actionAddToLibrary = register({
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
contextMenuOrder: 6,
|
|
||||||
contextItemLabel: "labels.addToLibrary",
|
contextItemLabel: "labels.addToLibrary",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getDefaultAppState } from "../appState";
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { GRID_SIZE, ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
@@ -51,7 +52,7 @@ export const actionClearCanvas = register({
|
|||||||
elementLocked: appState.elementLocked,
|
elementLocked: appState.elementLocked,
|
||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
exportEmbedScene: appState.exportEmbedScene,
|
exportEmbedScene: appState.exportEmbedScene,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize || GRID_SIZE,
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
shouldAddWatermark: appState.shouldAddWatermark,
|
||||||
showStats: appState.showStats,
|
showStats: appState.showStats,
|
||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
@@ -75,8 +76,6 @@ export const actionClearCanvas = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ZOOM_STEP = 0.1;
|
|
||||||
|
|
||||||
export const actionZoomIn = register({
|
export const actionZoomIn = register({
|
||||||
name: "zoomIn",
|
name: "zoomIn",
|
||||||
perform: (_elements, appState) => {
|
perform: (_elements, appState) => {
|
||||||
|
|||||||
@@ -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",
|
contextItemLabel: "labels.delete",
|
||||||
contextMenuOrder: 999999,
|
|
||||||
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ export const actionGroup = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextMenuOrder: 4,
|
|
||||||
contextItemLabel: "labels.group",
|
contextItemLabel: "labels.group",
|
||||||
contextItemPredicate: (elements, appState) =>
|
contextItemPredicate: (elements, appState) =>
|
||||||
enableActionGroup(elements, appState),
|
enableActionGroup(elements, appState),
|
||||||
@@ -174,7 +173,6 @@ export const actionUngroup = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
||||||
contextMenuOrder: 5,
|
|
||||||
contextItemLabel: "labels.ungroup",
|
contextItemLabel: "labels.ungroup",
|
||||||
contextItemPredicate: (elements, appState) =>
|
contextItemPredicate: (elements, appState) =>
|
||||||
getSelectedGroupIds(appState).length > 0,
|
getSelectedGroupIds(appState).length > 0,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { t } from "../i18n";
|
|||||||
import { SceneHistory, HistoryEntry } from "../history";
|
import { SceneHistory, HistoryEntry } from "../history";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { KEYS } from "../keys";
|
import { isWindows, KEYS } from "../keys";
|
||||||
import { getElementMap } from "../element";
|
import { getElementMap } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||||
@@ -59,16 +59,16 @@ const writeData = (
|
|||||||
return { commitToHistory };
|
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;
|
type ActionCreator = (history: SceneHistory) => Action;
|
||||||
|
|
||||||
export const createUndoAction: ActionCreator = (history) => ({
|
export const createUndoAction: ActionCreator = (history) => ({
|
||||||
name: "undo",
|
name: "undo",
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState) =>
|
||||||
writeData(elements, appState, () => history.undoOnce()),
|
writeData(elements, appState, () => history.undoOnce()),
|
||||||
keyTest: testUndo(false),
|
keyTest: (event) =>
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.key.toLowerCase() === KEYS.Z &&
|
||||||
|
!event.shiftKey,
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -84,7 +84,11 @@ export const createRedoAction: ActionCreator = (history) => ({
|
|||||||
name: "redo",
|
name: "redo",
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState) =>
|
||||||
writeData(elements, appState, () => history.redoOnce()),
|
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 }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const actionShortcuts = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
showHelpDialog: true,
|
showHelpDialog: !appState.showHelpDialog,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export const actionCopyStyles = register({
|
|||||||
contextItemLabel: "labels.copyStyles",
|
contextItemLabel: "labels.copyStyles",
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C,
|
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C,
|
||||||
contextMenuOrder: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionPasteStyles = register({
|
export const actionPasteStyles = register({
|
||||||
@@ -74,5 +73,4 @@ export const actionPasteStyles = register({
|
|||||||
contextItemLabel: "labels.pasteStyles",
|
contextItemLabel: "labels.pasteStyles",
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||||
contextMenuOrder: 1,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
import { CODES, KEYS } from "../keys";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionToggleGridMode = register({
|
||||||
|
name: "gridMode",
|
||||||
|
perform(elements, appState) {
|
||||||
|
trackEvent("view", "mode", "grid");
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
showGrid: !appState.showGrid,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState: AppState) => appState.showGrid,
|
||||||
|
contextItemLabel: "labels.gridMode",
|
||||||
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
distributeHorizontally,
|
||||||
distributeVertically,
|
distributeVertically,
|
||||||
} from "./actionDistribute";
|
} from "./actionDistribute";
|
||||||
|
|
||||||
|
export {
|
||||||
|
actionCopy,
|
||||||
|
actionCut,
|
||||||
|
actionCopyAsPng,
|
||||||
|
actionCopyAsSvg,
|
||||||
|
} from "./actionClipboard";
|
||||||
|
|
||||||
|
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||||
|
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||||
|
|
||||||
|
export { actionToggleStats } from "./actionToggleStats";
|
||||||
|
|||||||
+17
-38
@@ -3,14 +3,15 @@ import {
|
|||||||
Action,
|
Action,
|
||||||
ActionsManagerInterface,
|
ActionsManagerInterface,
|
||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionFilterFn,
|
|
||||||
ActionName,
|
ActionName,
|
||||||
ActionResult,
|
ActionResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { t } from "../i18n";
|
|
||||||
import { ShortcutName } from "./shortcuts";
|
// 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 {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@@ -18,13 +19,14 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||||
|
|
||||||
getAppState: () => Readonly<AppState>;
|
getAppState: () => Readonly<AppState>;
|
||||||
|
|
||||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
||||||
|
app: App;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
updater: UpdaterFn,
|
updater: UpdaterFn,
|
||||||
getAppState: () => AppState,
|
getAppState: () => AppState,
|
||||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
|
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
|
||||||
|
app: App,
|
||||||
) {
|
) {
|
||||||
this.updater = (actionResult) => {
|
this.updater = (actionResult) => {
|
||||||
if (actionResult && "then" in actionResult) {
|
if (actionResult && "then" in actionResult) {
|
||||||
@@ -37,6 +39,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
};
|
};
|
||||||
this.getAppState = getAppState;
|
this.getAppState = getAppState;
|
||||||
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
|
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
|
||||||
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerAction(action: Action) {
|
registerAction(action: Action) {
|
||||||
@@ -63,6 +66,12 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const { viewModeEnabled } = this.getAppState();
|
||||||
|
if (viewModeEnabled) {
|
||||||
|
if (data[0].name !== "viewMode") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.updater(
|
this.updater(
|
||||||
@@ -70,6 +79,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
null,
|
null,
|
||||||
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -81,43 +91,11 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
null,
|
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.
|
// Id is an attribute that we can use to pass in data like keys.
|
||||||
// This is needed for dynamically generated action components
|
// This is needed for dynamically generated action components
|
||||||
// like the user list. We can use this key to extract more
|
// like the user list. We can use this key to extract more
|
||||||
@@ -132,6 +110,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
formState,
|
formState,
|
||||||
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type ShortcutName =
|
|||||||
| "copyStyles"
|
| "copyStyles"
|
||||||
| "pasteStyles"
|
| "pasteStyles"
|
||||||
| "selectAll"
|
| "selectAll"
|
||||||
| "delete"
|
| "deleteSelectedElements"
|
||||||
| "duplicateSelection"
|
| "duplicateSelection"
|
||||||
| "sendBackward"
|
| "sendBackward"
|
||||||
| "bringForward"
|
| "bringForward"
|
||||||
@@ -22,7 +22,8 @@ export type ShortcutName =
|
|||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary";
|
| "addToLibrary"
|
||||||
|
| "viewMode";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
@@ -31,7 +32,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
|
copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
|
||||||
pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
|
pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
|
||||||
selectAll: [getShortcutKey("CtrlOrCmd+A")],
|
selectAll: [getShortcutKey("CtrlOrCmd+A")],
|
||||||
delete: [getShortcutKey("Del")],
|
deleteSelectedElements: [getShortcutKey("Del")],
|
||||||
duplicateSelection: [
|
duplicateSelection: [
|
||||||
getShortcutKey("CtrlOrCmd+D"),
|
getShortcutKey("CtrlOrCmd+D"),
|
||||||
getShortcutKey(`Alt+${t("helpDialog.drag")}`),
|
getShortcutKey(`Alt+${t("helpDialog.drag")}`),
|
||||||
@@ -56,6 +57,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
stats: [],
|
stats: [],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
|
viewMode: [getShortcutKey("Alt+R")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
|
|||||||
+12
-5
@@ -16,12 +16,18 @@ type ActionFn = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
formData: any,
|
formData: any,
|
||||||
|
app: { canvas: HTMLCanvasElement | null },
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
export type ActionFilterFn = (action: Action) => void;
|
export type ActionFilterFn = (action: Action) => void;
|
||||||
|
|
||||||
export type ActionName =
|
export type ActionName =
|
||||||
|
| "copy"
|
||||||
|
| "cut"
|
||||||
|
| "paste"
|
||||||
|
| "copyAsPng"
|
||||||
|
| "copyAsSvg"
|
||||||
| "sendBackward"
|
| "sendBackward"
|
||||||
| "bringForward"
|
| "bringForward"
|
||||||
| "sendToBack"
|
| "sendToBack"
|
||||||
@@ -29,6 +35,9 @@ export type ActionName =
|
|||||||
| "copyStyles"
|
| "copyStyles"
|
||||||
| "selectAll"
|
| "selectAll"
|
||||||
| "pasteStyles"
|
| "pasteStyles"
|
||||||
|
| "gridMode"
|
||||||
|
| "zenMode"
|
||||||
|
| "stats"
|
||||||
| "changeStrokeColor"
|
| "changeStrokeColor"
|
||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
| "changeFillStyle"
|
| "changeFillStyle"
|
||||||
@@ -75,7 +84,8 @@ export type ActionName =
|
|||||||
| "alignVerticallyCentered"
|
| "alignVerticallyCentered"
|
||||||
| "alignHorizontallyCentered"
|
| "alignHorizontallyCentered"
|
||||||
| "distributeHorizontally"
|
| "distributeHorizontally"
|
||||||
| "distributeVertically";
|
| "distributeVertically"
|
||||||
|
| "viewMode";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
@@ -93,19 +103,16 @@ export interface Action {
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => boolean;
|
) => boolean;
|
||||||
contextItemLabel?: string;
|
contextItemLabel?: string;
|
||||||
contextMenuOrder?: number;
|
|
||||||
contextItemPredicate?: (
|
contextItemPredicate?: (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionsManagerInterface {
|
export interface ActionsManagerInterface {
|
||||||
actions: Record<ActionName, Action>;
|
actions: Record<ActionName, Action>;
|
||||||
registerAction: (action: Action) => void;
|
registerAction: (action: Action) => void;
|
||||||
handleKeyDown: (event: KeyboardEvent) => boolean;
|
handleKeyDown: (event: KeyboardEvent) => boolean;
|
||||||
getContextMenuItems: (
|
|
||||||
actionFilter: ActionFilterFn,
|
|
||||||
) => { label: string; action: () => void }[];
|
|
||||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -3,9 +3,10 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
|
GRID_SIZE,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { t } from "./i18n";
|
import { t } from "./i18n";
|
||||||
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
import { AppState, NormalizedZoomValue } from "./types";
|
||||||
import { getDateTime } from "./utils";
|
import { getDateTime } from "./utils";
|
||||||
|
|
||||||
export const getDefaultAppState = (): Omit<
|
export const getDefaultAppState = (): Omit<
|
||||||
@@ -41,7 +42,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
exportEmbedScene: false,
|
exportEmbedScene: false,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
gridSize: null,
|
gridSize: GRID_SIZE,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
isLibraryOpen: false,
|
isLibraryOpen: false,
|
||||||
@@ -56,13 +57,14 @@ export const getDefaultAppState = (): Omit<
|
|||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
scrolledOutside: false,
|
scrolledOutside: false,
|
||||||
scrollX: 0 as FlooredNumber,
|
scrollX: 0,
|
||||||
scrollY: 0 as FlooredNumber,
|
scrollY: 0,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldAddWatermark: false,
|
shouldAddWatermark: false,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
|
showGrid: false,
|
||||||
showHelpDialog: false,
|
showHelpDialog: false,
|
||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
@@ -72,6 +74,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||||
|
viewModeEnabled: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,6 +122,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
exportEmbedScene: { browser: true, export: false },
|
exportEmbedScene: { browser: true, export: false },
|
||||||
fileHandle: { browser: false, export: false },
|
fileHandle: { browser: false, export: false },
|
||||||
gridSize: { browser: true, export: true },
|
gridSize: { browser: true, export: true },
|
||||||
|
showGrid: { browser: true, export: false },
|
||||||
height: { browser: false, export: false },
|
height: { browser: false, export: false },
|
||||||
isBindingEnabled: { browser: false, export: false },
|
isBindingEnabled: { browser: false, export: false },
|
||||||
isLibraryOpen: { browser: false, export: false },
|
isLibraryOpen: { browser: false, export: false },
|
||||||
@@ -151,6 +155,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
width: { browser: false, export: false },
|
width: { browser: false, export: false },
|
||||||
zenModeEnabled: { browser: true, export: false },
|
zenModeEnabled: { browser: true, export: false },
|
||||||
zoom: { browser: true, export: false },
|
zoom: { browser: true, export: false },
|
||||||
|
viewModeEnabled: { browser: false, export: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
||||||
|
|||||||
+305
-203
@@ -2,8 +2,31 @@ import { Point, simplify } from "points-on-curve";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
import "../actions";
|
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 { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { actions } from "../actions/register";
|
import { actions } from "../actions/register";
|
||||||
@@ -18,7 +41,6 @@ import {
|
|||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
CANVAS_ONLY_ACTIONS,
|
|
||||||
CURSOR_TYPE,
|
CURSOR_TYPE,
|
||||||
DEFAULT_VERTICAL_ALIGN,
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
DRAGGING_THRESHOLD,
|
DRAGGING_THRESHOLD,
|
||||||
@@ -26,15 +48,15 @@ import {
|
|||||||
ELEMENT_TRANSLATE_AMOUNT,
|
ELEMENT_TRANSLATE_AMOUNT,
|
||||||
ENV,
|
ENV,
|
||||||
EVENT,
|
EVENT,
|
||||||
GRID_SIZE,
|
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
POINTER_BUTTON,
|
POINTER_BUTTON,
|
||||||
TAP_TWICE_TIMEOUT,
|
TAP_TWICE_TIMEOUT,
|
||||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||||
TOUCH_CTX_MENU_TIMEOUT,
|
TOUCH_CTX_MENU_TIMEOUT,
|
||||||
|
ZOOM_STEP,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { exportCanvas, loadFromBlob } from "../data";
|
import { loadFromBlob } from "../data";
|
||||||
import { isValidLibrary } from "../data/json";
|
import { isValidLibrary } from "../data/json";
|
||||||
import { Library } from "../data/library";
|
import { Library } from "../data/library";
|
||||||
import { restore } from "../data/restore";
|
import { restore } from "../data/restore";
|
||||||
@@ -127,7 +149,6 @@ import {
|
|||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
isOverScrollBars,
|
isOverScrollBars,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
normalizeScroll,
|
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { SceneState, ScrollBars } from "../scene/types";
|
import { SceneState, ScrollBars } from "../scene/types";
|
||||||
@@ -155,10 +176,12 @@ import {
|
|||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import ContextMenu from "./ContextMenu";
|
import { isMobile } from "../is-mobile";
|
||||||
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
|
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
|
|
||||||
@@ -248,6 +271,7 @@ export type ExcalidrawImperativeAPI = {
|
|||||||
};
|
};
|
||||||
setScrollToCenter: InstanceType<typeof App>["setScrollToCenter"];
|
setScrollToCenter: InstanceType<typeof App>["setScrollToCenter"];
|
||||||
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
||||||
|
getAppState: () => InstanceType<typeof App>["state"];
|
||||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
||||||
ready: true;
|
ready: true;
|
||||||
};
|
};
|
||||||
@@ -274,6 +298,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
offsetLeft,
|
offsetLeft,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
excalidrawRef,
|
excalidrawRef,
|
||||||
|
viewModeEnabled = false,
|
||||||
} = props;
|
} = props;
|
||||||
this.state = {
|
this.state = {
|
||||||
...defaultAppState,
|
...defaultAppState,
|
||||||
@@ -281,6 +306,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
|
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
|
||||||
|
viewModeEnabled,
|
||||||
};
|
};
|
||||||
if (excalidrawRef) {
|
if (excalidrawRef) {
|
||||||
const readyPromise =
|
const readyPromise =
|
||||||
@@ -298,6 +324,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
},
|
},
|
||||||
setScrollToCenter: this.setScrollToCenter,
|
setScrollToCenter: this.setScrollToCenter,
|
||||||
getSceneElements: this.getSceneElements,
|
getSceneElements: this.getSceneElements,
|
||||||
|
getAppState: () => this.state,
|
||||||
} as const;
|
} as const;
|
||||||
if (typeof excalidrawRef === "function") {
|
if (typeof excalidrawRef === "function") {
|
||||||
excalidrawRef(api);
|
excalidrawRef(api);
|
||||||
@@ -312,6 +339,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.syncActionResult,
|
this.syncActionResult,
|
||||||
() => this.state,
|
() => this.state,
|
||||||
() => this.scene.getElementsIncludingDeleted(),
|
() => this.scene.getElementsIncludingDeleted(),
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
this.actionManager.registerAll(actions);
|
this.actionManager.registerAll(actions);
|
||||||
|
|
||||||
@@ -319,6 +347,62 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.actionManager.registerAction(createRedoAction(history));
|
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() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
@@ -326,20 +410,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
height: canvasDOMHeight,
|
height: canvasDOMHeight,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
offsetLeft,
|
offsetLeft,
|
||||||
|
viewModeEnabled,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
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_X = canvasDOMWidth / 2;
|
||||||
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
|
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="excalidraw"
|
className={clsx("excalidraw", {
|
||||||
|
"excalidraw--view-mode": viewModeEnabled,
|
||||||
|
})}
|
||||||
ref={this.excalidrawContainerRef}
|
ref={this.excalidrawContainerRef}
|
||||||
style={{
|
style={{
|
||||||
width: canvasDOMWidth,
|
width: canvasDOMWidth,
|
||||||
@@ -369,9 +452,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
isCollaborating={this.props.isCollaborating || false}
|
isCollaborating={this.props.isCollaborating || false}
|
||||||
onExportToBackend={onExportToBackend}
|
onExportToBackend={onExportToBackend}
|
||||||
renderCustomFooter={renderFooter}
|
renderCustomFooter={renderFooter}
|
||||||
|
viewModeEnabled={viewModeEnabled}
|
||||||
/>
|
/>
|
||||||
{this.state.showStats && (
|
{this.state.showStats && (
|
||||||
<Stats
|
<Stats
|
||||||
|
setAppState={this.setAppState}
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
elements={this.scene.getElements()}
|
elements={this.scene.getElements()}
|
||||||
onClose={this.toggleStats}
|
onClose={this.toggleStats}
|
||||||
@@ -383,28 +468,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
clearToast={this.clearToast}
|
clearToast={this.clearToast}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<main>
|
<main>{this.renderCanvas()}</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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -444,6 +508,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (actionResult.commitToHistory) {
|
if (actionResult.commitToHistory) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
||||||
|
|
||||||
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||||
|
viewModeEnabled = this.props.viewModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
...actionResult.appState,
|
...actionResult.appState,
|
||||||
@@ -453,6 +524,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
height: state.height,
|
height: state.height,
|
||||||
offsetTop: state.offsetTop,
|
offsetTop: state.offsetTop,
|
||||||
offsetLeft: state.offsetLeft,
|
offsetLeft: state.offsetLeft,
|
||||||
|
viewModeEnabled,
|
||||||
}),
|
}),
|
||||||
() => {
|
() => {
|
||||||
if (actionResult.syncHistory) {
|
if (actionResult.syncHistory) {
|
||||||
@@ -635,7 +707,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scene.addCallback(this.onSceneUpdated);
|
this.scene.addCallback(this.onSceneUpdated);
|
||||||
|
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
|
||||||
// optim to avoid extra render on init
|
// optim to avoid extra render on init
|
||||||
@@ -702,25 +773,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addEventListeners() {
|
private addEventListeners() {
|
||||||
|
this.removeEventListeners();
|
||||||
document.addEventListener(EVENT.COPY, this.onCopy);
|
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.KEYDOWN, this.onKeyDown, false);
|
||||||
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
EVENT.MOUSE_MOVE,
|
EVENT.MOUSE_MOVE,
|
||||||
this.updateCurrentCursorPosition,
|
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
|
// rerender text elements on font load to fix #637 && #1553
|
||||||
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
||||||
|
|
||||||
// Safari-only desktop pinch zoom
|
// Safari-only desktop pinch zoom
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
EVENT.GESTURE_START,
|
EVENT.GESTURE_START,
|
||||||
@@ -737,6 +799,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.onGestureEnd as any,
|
this.onGestureEnd as any,
|
||||||
false,
|
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) {
|
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
||||||
@@ -759,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
|
document
|
||||||
.querySelector(".excalidraw")
|
.querySelector(".excalidraw")
|
||||||
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
|
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
|
||||||
@@ -906,44 +991,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
copyToClipboard(this.scene.getElements(), this.state);
|
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,
|
|
||||||
);
|
|
||||||
this.setState({ toastMessage: t("toast.copyToClipboardAsPng") });
|
|
||||||
} 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() {
|
private static resetTapTwice() {
|
||||||
didTapTwice = false;
|
didTapTwice = false;
|
||||||
}
|
}
|
||||||
@@ -1041,7 +1088,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const dy = y - elementsCenterY;
|
const dy = y - elementsCenterY;
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize);
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
this.state.showGrid,
|
||||||
|
this.state.gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
const oldIdToDuplicatedId = new Map();
|
const oldIdToDuplicatedId = new Map();
|
||||||
const newElements = clipboardElements.map((element) => {
|
const newElements = clipboardElements.map((element) => {
|
||||||
@@ -1146,24 +1198,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
toggleZenMode = () => {
|
toggleZenMode = () => {
|
||||||
this.setState({
|
this.actionManager.executeAction(actionToggleZenMode);
|
||||||
zenModeEnabled: !this.state.zenModeEnabled,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleGridMode = () => {
|
toggleGridMode = () => {
|
||||||
this.setState({
|
this.actionManager.executeAction(actionToggleGridMode);
|
||||||
gridSize: this.state.gridSize ? null : GRID_SIZE,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleStats = () => {
|
toggleStats = () => {
|
||||||
if (!this.state.showStats) {
|
if (!this.state.showStats) {
|
||||||
trackEvent("dialog", "stats");
|
trackEvent("dialog", "stats");
|
||||||
}
|
}
|
||||||
this.setState({
|
this.actionManager.executeAction(actionToggleStats);
|
||||||
showStats: !this.state.showStats,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
|
setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
|
||||||
@@ -1253,34 +1299,25 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
if (this.actionManager.handleKeyDown(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
|
this.setState({ isBindingEnabled: false });
|
||||||
|
}
|
||||||
|
|
||||||
if (event.code === CODES.NINE) {
|
if (event.code === CODES.NINE) {
|
||||||
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
const step =
|
const step =
|
||||||
(this.state.gridSize &&
|
(this.state.showGrid &&
|
||||||
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
|
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
|
||||||
(event.shiftKey
|
(event.shiftKey
|
||||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||||
@@ -1778,8 +1815,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const scaleFactor = distance / gesture.initialDistance;
|
const scaleFactor = distance / gesture.initialDistance;
|
||||||
|
|
||||||
this.setState(({ zoom, scrollX, scrollY, offsetLeft, offsetTop }) => ({
|
this.setState(({ zoom, scrollX, scrollY, offsetLeft, offsetTop }) => ({
|
||||||
scrollX: normalizeScroll(scrollX + deltaX / zoom.value),
|
scrollX: scrollX + deltaX / zoom.value,
|
||||||
scrollY: normalizeScroll(scrollY + deltaY / zoom.value),
|
scrollY: scrollY + deltaY / zoom.value,
|
||||||
zoom: getNewZoom(
|
zoom: getNewZoom(
|
||||||
getNormalizedZoom(initialScale * scaleFactor),
|
getNormalizedZoom(initialScale * scaleFactor),
|
||||||
zoom,
|
zoom,
|
||||||
@@ -1822,6 +1859,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
scenePointerX,
|
scenePointerX,
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
this.state.editingLinearElement,
|
this.state.editingLinearElement,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
if (editingLinearElement !== this.state.editingLinearElement) {
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
||||||
@@ -2080,14 +2118,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
lastPointerUp = onPointerUp;
|
||||||
|
|
||||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
if (!this.state.viewModeEnabled) {
|
||||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
window.addEventListener(EVENT.KEYUP, onKeyUp);
|
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
||||||
pointerDownState.eventListeners.onMove = onPointerMove;
|
window.addEventListener(EVENT.KEYUP, onKeyUp);
|
||||||
pointerDownState.eventListeners.onUp = onPointerUp;
|
pointerDownState.eventListeners.onMove = onPointerMove;
|
||||||
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
pointerDownState.eventListeners.onUp = onPointerUp;
|
||||||
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
||||||
|
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
||||||
@@ -2137,7 +2177,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
!(
|
!(
|
||||||
gesture.pointers.size === 0 &&
|
gesture.pointers.size === 0 &&
|
||||||
(event.button === POINTER_BUTTON.WHEEL ||
|
(event.button === POINTER_BUTTON.WHEEL ||
|
||||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||||
|
this.state.viewModeEnabled)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2190,12 +2231,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollX: normalizeScroll(
|
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
|
||||||
this.state.scrollX - deltaX / this.state.zoom.value,
|
scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
|
||||||
),
|
|
||||||
scrollY: normalizeScroll(
|
|
||||||
this.state.scrollY - deltaY / this.state.zoom.value,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const teardown = withBatchedUpdates(
|
const teardown = withBatchedUpdates(
|
||||||
@@ -2252,7 +2289,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
return {
|
return {
|
||||||
origin,
|
origin,
|
||||||
originInGrid: tupleToCoors(
|
originInGrid: tupleToCoors(
|
||||||
getGridPoint(origin.x, origin.y, this.state.gridSize),
|
getGridPoint(
|
||||||
|
origin.x,
|
||||||
|
origin.y,
|
||||||
|
this.state.showGrid,
|
||||||
|
this.state.gridSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
scrollbars: isOverScrollBars(
|
scrollbars: isOverScrollBars(
|
||||||
currentScrollBars,
|
currentScrollBars,
|
||||||
@@ -2608,7 +2650,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
elementType === "draw" ? null : this.state.gridSize,
|
elementType === "draw" ? false : this.state.showGrid,
|
||||||
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
|
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
|
||||||
@@ -2670,6 +2713,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
const element = newElement({
|
const element = newElement({
|
||||||
@@ -2759,6 +2803,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2831,6 +2876,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [dragX, dragY] = getGridPoint(
|
const [dragX, dragY] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
pointerCoords.x - pointerDownState.drag.offset.x,
|
||||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
pointerCoords.y - pointerDownState.drag.offset.y,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2883,6 +2929,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [originDragX, originDragY] = getGridPoint(
|
const [originDragX, originDragY] = getGridPoint(
|
||||||
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
||||||
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
mutateElement(duplicatedElement, {
|
mutateElement(duplicatedElement, {
|
||||||
@@ -3009,9 +3056,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const dx = x - pointerDownState.lastCoords.x;
|
const dx = x - pointerDownState.lastCoords.x;
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollX: normalizeScroll(
|
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
||||||
this.state.scrollX - dx / this.state.zoom.value,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.x = x;
|
pointerDownState.lastCoords.x = x;
|
||||||
return true;
|
return true;
|
||||||
@@ -3021,9 +3066,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
const dy = y - pointerDownState.lastCoords.y;
|
const dy = y - pointerDownState.lastCoords.y;
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollY: normalizeScroll(
|
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
||||||
this.state.scrollY - dy / this.state.zoom.value,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.y = y;
|
pointerDownState.lastCoords.y = y;
|
||||||
return true;
|
return true;
|
||||||
@@ -3543,6 +3586,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
dragNewElement(
|
dragNewElement(
|
||||||
@@ -3581,6 +3625,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const [resizeX, resizeY] = getGridPoint(
|
const [resizeX, resizeY] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||||
|
this.state.showGrid,
|
||||||
this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
@@ -3616,52 +3661,87 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.state,
|
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 elements = this.scene.getElements();
|
||||||
const element = this.getElementAtPosition(x, y);
|
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) {
|
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({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
navigator.clipboard && {
|
_isMobile &&
|
||||||
shortcutName: "paste",
|
navigator.clipboard && {
|
||||||
label: t("labels.paste"),
|
name: "paste",
|
||||||
action: () => this.pasteFromClipboard(null),
|
perform: (elements, appStates) => {
|
||||||
},
|
this.pasteFromClipboard(null);
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: "labels.paste",
|
||||||
|
},
|
||||||
|
_isMobile && navigator.clipboard && separator,
|
||||||
probablySupportsClipboardBlob &&
|
probablySupportsClipboardBlob &&
|
||||||
elements.length > 0 && {
|
elements.length > 0 &&
|
||||||
shortcutName: "copyAsPng",
|
actionCopyAsPng,
|
||||||
label: t("labels.copyAsPng"),
|
|
||||||
action: this.copyToClipboardAsPng,
|
|
||||||
},
|
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
elements.length > 0 && {
|
elements.length > 0 &&
|
||||||
shortcutName: "copyAsSvg",
|
actionCopyAsSvg,
|
||||||
label: t("labels.copyAsSvg"),
|
((probablySupportsClipboardBlob && elements.length > 0) ||
|
||||||
action: this.copyToClipboardAsSvg,
|
(probablySupportsClipboardWriteText && elements.length > 0)) &&
|
||||||
},
|
separator,
|
||||||
...this.actionManager.getContextMenuItems((action) =>
|
actionSelectAll,
|
||||||
CANVAS_ONLY_ACTIONS.includes(action.name),
|
separator,
|
||||||
),
|
actionToggleGridMode,
|
||||||
{
|
actionToggleZenMode,
|
||||||
checked: this.state.gridSize !== null,
|
typeof this.props.viewModeEnabled === "undefined" &&
|
||||||
shortcutName: "gridMode",
|
actionToggleViewMode,
|
||||||
label: t("labels.gridMode"),
|
actionToggleStats,
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
top: clientY,
|
top: clientY,
|
||||||
left: clientX,
|
left: clientX,
|
||||||
|
actionManager: this.actionManager,
|
||||||
|
appState: this.state,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3670,39 +3750,55 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
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({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
{
|
_isMobile && actionCut,
|
||||||
shortcutName: "cut",
|
_isMobile && navigator.clipboard && actionCopy,
|
||||||
label: t("labels.cut"),
|
_isMobile &&
|
||||||
action: this.cutAll,
|
navigator.clipboard && {
|
||||||
},
|
name: "paste",
|
||||||
navigator.clipboard && {
|
perform: (elements, appStates) => {
|
||||||
shortcutName: "copy",
|
this.pasteFromClipboard(null);
|
||||||
label: t("labels.copy"),
|
return {
|
||||||
action: this.copyAll,
|
commitToHistory: false,
|
||||||
},
|
};
|
||||||
navigator.clipboard && {
|
},
|
||||||
shortcutName: "paste",
|
contextItemLabel: "labels.paste",
|
||||||
label: t("labels.paste"),
|
},
|
||||||
action: () => this.pasteFromClipboard(null),
|
_isMobile && separator,
|
||||||
},
|
...options,
|
||||||
probablySupportsClipboardBlob && {
|
separator,
|
||||||
shortcutName: "copyAsPng",
|
actionCopyStyles,
|
||||||
label: t("labels.copyAsPng"),
|
actionPasteStyles,
|
||||||
action: this.copyToClipboardAsPng,
|
separator,
|
||||||
},
|
maybeGroupAction && actionGroup,
|
||||||
probablySupportsClipboardWriteText && {
|
maybeUngroupAction && actionUngroup,
|
||||||
shortcutName: "copyAsSvg",
|
(maybeGroupAction || maybeUngroupAction) && separator,
|
||||||
label: t("labels.copyAsSvg"),
|
actionAddToLibrary,
|
||||||
action: this.copyToClipboardAsSvg,
|
separator,
|
||||||
},
|
actionSendBackward,
|
||||||
...this.actionManager.getContextMenuItems(
|
actionBringForward,
|
||||||
(action) => !CANVAS_ONLY_ACTIONS.includes(action.name),
|
actionSendToBack,
|
||||||
),
|
actionBringToFront,
|
||||||
|
separator,
|
||||||
|
actionDuplicateSelection,
|
||||||
|
actionDeleteSelected,
|
||||||
],
|
],
|
||||||
top: clientY,
|
top: clientY,
|
||||||
left: clientX,
|
left: clientX,
|
||||||
|
actionManager: this.actionManager,
|
||||||
|
appState: this.state,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3733,9 +3829,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}, 1000);
|
}, 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 }) => ({
|
this.setState(({ zoom, offsetLeft, offsetTop }) => ({
|
||||||
zoom: getNewZoom(
|
zoom: getNewZoom(
|
||||||
getNormalizedZoom(zoom.value - delta / 100),
|
getNormalizedZoom(newZoom),
|
||||||
zoom,
|
zoom,
|
||||||
{ left: offsetLeft, top: offsetTop },
|
{ left: offsetLeft, top: offsetTop },
|
||||||
{
|
{
|
||||||
@@ -3758,14 +3860,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.setState(({ zoom, scrollX }) => ({
|
this.setState(({ zoom, scrollX }) => ({
|
||||||
// on Mac, shift+wheel tends to result in deltaX
|
// on Mac, shift+wheel tends to result in deltaX
|
||||||
scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom.value),
|
scrollX: scrollX - (deltaY || deltaX) / zoom.value,
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(({ zoom, scrollX, scrollY }) => ({
|
this.setState(({ zoom, scrollX, scrollY }) => ({
|
||||||
scrollX: normalizeScroll(scrollX - deltaX / zoom.value),
|
scrollX: scrollX - deltaX / zoom.value,
|
||||||
scrollY: normalizeScroll(scrollY - deltaY / zoom.value),
|
scrollY: scrollY - deltaY / zoom.value,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Avatar {
|
.Avatar {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.CollabButton.is-collaborating {
|
.CollabButton.is-collaborating {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.color-picker {
|
.color-picker {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.context-menu {
|
.context-menu {
|
||||||
@@ -9,9 +9,10 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin: -0.25rem 0 0 0.125rem;
|
margin: -0.25rem 0 0 0.125rem;
|
||||||
padding: 0.25rem 0;
|
padding: 0.5rem 0;
|
||||||
background-color: var(--popup-secondary-background-color);
|
background-color: var(--popup-secondary-background-color);
|
||||||
border: 1px solid var(--button-gray-3);
|
border: 1px solid var(--button-gray-3);
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu button {
|
.context-menu button {
|
||||||
@@ -88,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 { render, unmountComponentAtNode } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import "./ContextMenu.scss";
|
import "./ContextMenu.scss";
|
||||||
import {
|
import {
|
||||||
getShortcutFromShortcutName,
|
getShortcutFromShortcutName,
|
||||||
ShortcutName,
|
ShortcutName,
|
||||||
} from "../actions/shortcuts";
|
} from "../actions/shortcuts";
|
||||||
|
import { Action } from "../actions/types";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
type ContextMenuOption = {
|
export type ContextMenuOption = "separator" | Action;
|
||||||
checked?: boolean;
|
|
||||||
shortcutName: ShortcutName;
|
|
||||||
label: string;
|
|
||||||
action(): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type ContextMenuProps = {
|
||||||
options: ContextMenuOption[];
|
options: ContextMenuOption[];
|
||||||
onCloseRequest?(): void;
|
onCloseRequest?(): void;
|
||||||
top: number;
|
top: number;
|
||||||
left: 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
|
const isDarkTheme = !!document
|
||||||
.querySelector(".excalidraw")
|
.querySelector(".excalidraw")
|
||||||
?.classList.contains("Appearance_dark");
|
?.classList.contains("Appearance_dark");
|
||||||
@@ -43,23 +51,34 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
|||||||
className="context-menu"
|
className="context-menu"
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
{options.map(({ action, checked, shortcutName, label }, idx) => (
|
{options.map((option, idx) => {
|
||||||
<li data-testid={shortcutName} key={idx} onClick={onCloseRequest}>
|
if (option === "separator") {
|
||||||
<button
|
return <hr key={idx} className="context-menu-option-separator" />;
|
||||||
className={`context-menu-option
|
}
|
||||||
${shortcutName === "delete" ? "dangerous" : ""}
|
|
||||||
${checked ? "checkmark" : ""}`}
|
const actionName = option.name;
|
||||||
onClick={action}
|
const label = option.contextItemLabel
|
||||||
>
|
? t(option.contextItemLabel)
|
||||||
<div className="context-menu-option__label">{label}</div>
|
: "";
|
||||||
<kbd className="context-menu-option__shortcut">
|
return (
|
||||||
{shortcutName
|
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||||
? getShortcutFromShortcutName(shortcutName)
|
<button
|
||||||
: ""}
|
className={clsx("context-menu-option", {
|
||||||
</kbd>
|
dangerous: actionName === "deleteSelectedElements",
|
||||||
</button>
|
checkmark: option.checked?.(appState),
|
||||||
</li>
|
})}
|
||||||
))}
|
onClick={() => actionManager.executeAction(option)}
|
||||||
|
>
|
||||||
|
<div className="context-menu-option__label">{label}</div>
|
||||||
|
<kbd className="context-menu-option__shortcut">
|
||||||
|
{actionName
|
||||||
|
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||||
|
: ""}
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,8 +97,10 @@ const getContextMenuNode = (): HTMLDivElement => {
|
|||||||
|
|
||||||
type ContextMenuParams = {
|
type ContextMenuParams = {
|
||||||
options: (ContextMenuOption | false | null | undefined)[];
|
options: (ContextMenuOption | false | null | undefined)[];
|
||||||
top: number;
|
top: ContextMenuProps["top"];
|
||||||
left: number;
|
left: ContextMenuProps["left"];
|
||||||
|
actionManager: ContextMenuProps["actionManager"];
|
||||||
|
appState: Readonly<AppState>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -101,6 +122,8 @@ export default {
|
|||||||
left={params.left}
|
left={params.left}
|
||||||
options={options}
|
options={options}
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
|
actionManager={params.actionManager}
|
||||||
|
appState={params.appState}
|
||||||
/>,
|
/>,
|
||||||
getContextMenuNode(),
|
getContextMenuNode(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Dialog {
|
.Dialog {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import clsx from "clsx";
|
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 { t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -8,14 +9,6 @@ import { back, close } from "./icons";
|
|||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { Modal } from "./Modal";
|
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: {
|
export const Dialog = (props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -24,7 +17,7 @@ export const Dialog = (props: {
|
|||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ExportDialog__preview {
|
.ExportDialog__preview {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.HelpDialog h3 {
|
.HelpDialog h3 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin } from "../keys";
|
import { isDarwin, isWindows } from "../keys";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import "./HelpDialog.scss";
|
import "./HelpDialog.scss";
|
||||||
@@ -227,6 +227,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.gridMode")}
|
label={t("labels.gridMode")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.viewMode")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+R")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
<Column>
|
||||||
@@ -328,7 +332,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("buttons.redo")}
|
label={t("buttons.redo")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]}
|
shortcuts={
|
||||||
|
isWindows
|
||||||
|
? [
|
||||||
|
getShortcutKey("CtrlOrCmd+Y"),
|
||||||
|
getShortcutKey("CtrlOrCmd+Shift+Z"),
|
||||||
|
]
|
||||||
|
: [getShortcutKey("CtrlOrCmd+Shift+Z")]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.group")}
|
label={t("labels.group")}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
// this is loosely based on the longest hint text
|
// this is loosely based on the longest hint text
|
||||||
$wide-viewport-width: 1000px;
|
$wide-viewport-width: 1000px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.picker-container {
|
.picker-container {
|
||||||
|
|||||||
+79
-43
@@ -61,6 +61,7 @@ interface LayerUIProps {
|
|||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement | null,
|
||||||
) => void;
|
) => void;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||||
|
viewModeEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@@ -299,6 +300,7 @@ const LayerUI = ({
|
|||||||
isCollaborating,
|
isCollaborating,
|
||||||
onExportToBackend,
|
onExportToBackend,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
|
viewModeEnabled,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
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 = () => (
|
const renderCanvasActions = () => (
|
||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
@@ -448,38 +472,42 @@ const LayerUI = ({
|
|||||||
gap={4}
|
gap={4}
|
||||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
||||||
>
|
>
|
||||||
{renderCanvasActions()}
|
{viewModeEnabled
|
||||||
|
? renderViewModeCanvasActions()
|
||||||
|
: renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
<Section heading="shapes">
|
{!viewModeEnabled && (
|
||||||
{(heading) => (
|
<Section heading="shapes">
|
||||||
<Stack.Col gap={4} align="start">
|
{(heading) => (
|
||||||
<Stack.Row gap={1}>
|
<Stack.Col gap={4} align="start">
|
||||||
<Island
|
<Stack.Row gap={1}>
|
||||||
padding={1}
|
<Island
|
||||||
className={clsx({ "zen-mode": zenModeEnabled })}
|
padding={1}
|
||||||
>
|
className={clsx({ "zen-mode": zenModeEnabled })}
|
||||||
<HintViewer appState={appState} elements={elements} />
|
>
|
||||||
{heading}
|
<HintViewer appState={appState} elements={elements} />
|
||||||
<Stack.Row gap={1}>
|
{heading}
|
||||||
<ShapesSwitcher
|
<Stack.Row gap={1}>
|
||||||
elementType={appState.elementType}
|
<ShapesSwitcher
|
||||||
setAppState={setAppState}
|
elementType={appState.elementType}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
setAppState={setAppState}
|
||||||
/>
|
isLibraryOpen={appState.isLibraryOpen}
|
||||||
</Stack.Row>
|
/>
|
||||||
</Island>
|
</Stack.Row>
|
||||||
<LockIcon
|
</Island>
|
||||||
zenModeEnabled={zenModeEnabled}
|
<LockIcon
|
||||||
checked={appState.elementLocked}
|
zenModeEnabled={zenModeEnabled}
|
||||||
onChange={onLockToggle}
|
checked={appState.elementLocked}
|
||||||
title={t("toolBar.lock")}
|
onChange={onLockToggle}
|
||||||
/>
|
title={t("toolBar.lock")}
|
||||||
</Stack.Row>
|
/>
|
||||||
{libraryMenu}
|
</Stack.Row>
|
||||||
</Stack.Col>
|
{libraryMenu}
|
||||||
)}
|
</Stack.Col>
|
||||||
</Section>
|
)}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
<UserList
|
<UserList
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-right": zenModeEnabled,
|
"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 = () => (
|
const renderFooter = () => (
|
||||||
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
||||||
<div
|
<div
|
||||||
@@ -599,25 +641,19 @@ const LayerUI = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
renderCustomFooter={renderCustomFooter}
|
renderCustomFooter={renderCustomFooter}
|
||||||
|
viewModeEnabled={viewModeEnabled}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="layer-ui__wrapper">
|
<div
|
||||||
|
className={clsx("layer-ui__wrapper", {
|
||||||
|
"disable-pointerEvents": appState.cursorButton === "down",
|
||||||
|
})}
|
||||||
|
>
|
||||||
{dialogs}
|
{dialogs}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
{renderBottomAppMenu()}
|
{renderBottomAppMenu()}
|
||||||
{
|
{renderGitHubCorner()}
|
||||||
<aside
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
|
||||||
{
|
|
||||||
"transition-right": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<GitHubCorner appearance={appState.appearance} />
|
|
||||||
</aside>
|
|
||||||
}
|
|
||||||
{renderFooter()}
|
{renderFooter()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+160
-116
@@ -29,6 +29,7 @@ type MobileMenuProps = {
|
|||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||||
|
viewModeEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -43,121 +44,164 @@ export const MobileMenu = ({
|
|||||||
canvas,
|
canvas,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
}: MobileMenuProps) => (
|
viewModeEnabled,
|
||||||
<>
|
}: MobileMenuProps) => {
|
||||||
<FixedSideContainer side="top">
|
const renderFixedSideContainer = () => {
|
||||||
<Section heading="shapes">
|
return (
|
||||||
{(heading) => (
|
<FixedSideContainer side="top">
|
||||||
<Stack.Col gap={4} align="center">
|
<Section heading="shapes">
|
||||||
<Stack.Row gap={1}>
|
{(heading) => (
|
||||||
<Island padding={1}>
|
<Stack.Col gap={4} align="center">
|
||||||
{heading}
|
<Stack.Row gap={1}>
|
||||||
<Stack.Row gap={1}>
|
<Island padding={1}>
|
||||||
<ShapesSwitcher
|
{heading}
|
||||||
elementType={appState.elementType}
|
<Stack.Row gap={1}>
|
||||||
setAppState={setAppState}
|
<ShapesSwitcher
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
elementType={appState.elementType}
|
||||||
/>
|
setAppState={setAppState}
|
||||||
</Stack.Row>
|
isLibraryOpen={appState.isLibraryOpen}
|
||||||
</Island>
|
/>
|
||||||
<LockIcon
|
</Stack.Row>
|
||||||
checked={appState.elementLocked}
|
</Island>
|
||||||
onChange={onLockToggle}
|
<LockIcon
|
||||||
title={t("toolBar.lock")}
|
checked={appState.elementLocked}
|
||||||
/>
|
onChange={onLockToggle}
|
||||||
</Stack.Row>
|
title={t("toolBar.lock")}
|
||||||
{libraryMenu}
|
|
||||||
</Stack.Col>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
<HintViewer appState={appState} elements={elements} />
|
|
||||||
</FixedSideContainer>
|
|
||||||
<div
|
|
||||||
className="App-bottom-bar"
|
|
||||||
style={{
|
|
||||||
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Island padding={0}>
|
|
||||||
{appState.openMenu === "canvas" ? (
|
|
||||||
<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}
|
|
||||||
/>
|
/>
|
||||||
{renderCustomFooter?.(true)}
|
</Stack.Row>
|
||||||
<fieldset>
|
{libraryMenu}
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
</Stack.Col>
|
||||||
<UserList mobile>
|
|
||||||
{Array.from(appState.collaborators)
|
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
|
||||||
.map(([clientId, client]) => (
|
|
||||||
<React.Fragment key={clientId}>
|
|
||||||
{actionManager.renderAction(
|
|
||||||
"goToCollaborator",
|
|
||||||
clientId,
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</UserList>
|
|
||||||
</fieldset>
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
) : appState.openMenu === "shape" &&
|
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
|
||||||
<SelectedShapeActions
|
|
||||||
appState={appState}
|
|
||||||
elements={elements}
|
|
||||||
renderAction={actionManager.renderAction}
|
|
||||||
elementType={appState.elementType}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
{appState.scrolledOutside && !appState.openMenu && (
|
|
||||||
<button
|
|
||||||
className="scroll-back-to-content"
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({
|
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("buttons.scrollBackToContent")}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</footer>
|
</Section>
|
||||||
</Island>
|
<HintViewer appState={appState} elements={elements} />
|
||||||
</div>
|
</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={{
|
||||||
|
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Island padding={0}>
|
||||||
|
{appState.openMenu === "canvas" ? (
|
||||||
|
<Section className="App-mobile-menu" heading="canvasActions">
|
||||||
|
<div className="panelColumn">
|
||||||
|
<Stack.Col gap={4}>
|
||||||
|
{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,
|
||||||
|
)
|
||||||
|
.map(([clientId, client]) => (
|
||||||
|
<React.Fragment key={clientId}>
|
||||||
|
{actionManager.renderAction(
|
||||||
|
"goToCollaborator",
|
||||||
|
clientId,
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</UserList>
|
||||||
|
</fieldset>
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
) : appState.openMenu === "shape" &&
|
||||||
|
!viewModeEnabled &&
|
||||||
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
|
<SelectedShapeActions
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
elementType={appState.elementType}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
) : null}
|
||||||
|
<footer className="App-toolbar">
|
||||||
|
{renderAppToolbar()}
|
||||||
|
{appState.scrolledOutside && !appState.openMenu && (
|
||||||
|
<button
|
||||||
|
className="scroll-back-to-content"
|
||||||
|
onClick={() => {
|
||||||
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</Island>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Modal {
|
.Modal {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.PasteChartDialog {
|
.PasteChartDialog {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.Stats {
|
.Stats {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
export const Stats = (props: {
|
export const Stats = (props: {
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -46,6 +47,12 @@ export const Stats = (props: {
|
|||||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||||
|
|
||||||
|
const onGridSizeChange = () => {
|
||||||
|
props.setAppState({
|
||||||
|
gridSize: ((props.appState.gridSize - 5) % 50) + 10,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (isMobile && props.appState.openMenu) {
|
if (isMobile && props.appState.openMenu) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -156,6 +163,17 @@ export const Stats = (props: {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
{props.appState.showGrid && (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{"Misc"}</th>
|
||||||
|
</tr>
|
||||||
|
<tr onClick={onGridSizeChange} style={{ cursor: "pointer" }}>
|
||||||
|
<td>{"Grid size"}</td>
|
||||||
|
<td>{props.appState.gridSize}</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables.scss";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.TextInput {
|
.TextInput {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Toast {
|
.Toast {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "open-color/open-color.scss";
|
@import "open-color/open-color.scss";
|
||||||
@import "../css/variables";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ToolIcon {
|
.ToolIcon {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../css/_variables";
|
@import "../css/variables.module";
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Tooltip {
|
.Tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -48,15 +48,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the following 3 rules ensure that the tooltip doesn't show (nor affect
|
.Tooltip:hover .Tooltip__label {
|
||||||
// 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 {
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -90,4 +90,6 @@ export const TAP_TWICE_TIMEOUT = 300;
|
|||||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||||
export const TITLE_TIMEOUT = 10000;
|
export const TITLE_TIMEOUT = 10000;
|
||||||
export const TOAST_TIMEOUT = 5000;
|
export const TOAST_TIMEOUT = 5000;
|
||||||
export const VERSION_TIMEOUT = 15000;
|
export const VERSION_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
export const ZOOM_STEP = 0.1;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
+10
-3
@@ -1,4 +1,4 @@
|
|||||||
@import "./_variables";
|
@import "./variables.module";
|
||||||
@import "./theme";
|
@import "./theme";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-menu_top > * {
|
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-menu_bottom > * {
|
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +492,13 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.excalidraw--view-mode {
|
||||||
|
.App-menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.App-bottom-bar,
|
.App-bottom-bar,
|
||||||
.FixedSideContainer,
|
.FixedSideContainer,
|
||||||
|
|||||||
@@ -2,3 +2,7 @@
|
|||||||
|
|
||||||
// keep up to date with is-mobile.tsx
|
// keep up to date with is-mobile.tsx
|
||||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||||
|
|
||||||
|
:export {
|
||||||
|
isMobileQuery: unquote($is-mobile-query);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { fileSave } from "browser-nativefs";
|
import { fileSave } from "browser-fs-access";
|
||||||
import {
|
import {
|
||||||
copyCanvasToClipboardAsPng,
|
copyCanvasToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { fileOpen, fileSave } from "browser-nativefs";
|
import { fileOpen, fileSave } from "browser-fs-access";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||||
|
appState.showGrid,
|
||||||
appState.gridSize,
|
appState.gridSize,
|
||||||
);
|
);
|
||||||
LinearElementEditor.movePoint(element, activePointIndex, newPoint);
|
LinearElementEditor.movePoint(element, activePointIndex, newPoint);
|
||||||
@@ -198,6 +199,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
|
appState.showGrid,
|
||||||
appState.gridSize,
|
appState.gridSize,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -282,7 +284,8 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
editingLinearElement: LinearElementEditor,
|
editingLinearElement: LinearElementEditor,
|
||||||
gridSize: number | null,
|
isGridOn: boolean,
|
||||||
|
gridSize: number,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
const { elementId, lastUncommittedPoint } = editingLinearElement;
|
const { elementId, lastUncommittedPoint } = editingLinearElement;
|
||||||
const element = LinearElementEditor.getElement(elementId);
|
const element = LinearElementEditor.getElement(elementId);
|
||||||
@@ -304,6 +307,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||||
|
isGridOn,
|
||||||
gridSize,
|
gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -398,9 +402,15 @@ export class LinearElementEditor {
|
|||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
gridSize: number | null,
|
isGridOn: boolean,
|
||||||
|
gridSize: number,
|
||||||
): Point {
|
): Point {
|
||||||
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
|
const pointerOnGrid = getGridPoint(
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
isGridOn,
|
||||||
|
gridSize,
|
||||||
|
);
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export const showSelectedShapeActions = (
|
|||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
) =>
|
) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
appState.editingElement ||
|
!appState.viewModeEnabled &&
|
||||||
getSelectedElements(elements, appState).length ||
|
(appState.editingElement ||
|
||||||
appState.elementType !== "selection",
|
getSelectedElements(elements, appState).length ||
|
||||||
|
appState.elementType !== "selection"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { APP_NAME, ENV, EVENT } from "../../constants";
|
|||||||
import { ImportedDataState } from "../../data/types";
|
import { ImportedDataState } from "../../data/types";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import {
|
import {
|
||||||
|
getElementMap,
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
} from "../../packages/excalidraw/index";
|
} from "../../packages/excalidraw/index";
|
||||||
import { AppState, Collaborator, Gesture } from "../../types";
|
import { Collaborator, Gesture } from "../../types";
|
||||||
import { resolvablePromise, withBatchedUpdates } from "../../utils";
|
import { resolvablePromise, withBatchedUpdates } from "../../utils";
|
||||||
import {
|
import {
|
||||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
import Portal from "./Portal";
|
import Portal from "./Portal";
|
||||||
import RoomDialog from "./RoomDialog";
|
import RoomDialog from "./RoomDialog";
|
||||||
|
import { createInverseContext } from "../../createInverseContext";
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
@@ -56,17 +58,21 @@ type ReconciledElements = readonly ExcalidrawElement[] & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: (collab: CollabAPI) => React.ReactNode;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
// NOTE not type-safe because the refObject may in fact not be initialized
|
|
||||||
// with ExcalidrawImperativeAPI yet
|
|
||||||
excalidrawRef: React.MutableRefObject<ExcalidrawImperativeAPI>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
Context: CollabContext,
|
||||||
|
Consumer: CollabContextConsumer,
|
||||||
|
Provider: CollabContextProvider,
|
||||||
|
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
|
||||||
|
|
||||||
|
export { CollabContext, CollabContextConsumer };
|
||||||
|
|
||||||
class CollabWrapper extends PureComponent<Props, CollabState> {
|
class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
portal: Portal;
|
portal: Portal;
|
||||||
|
excalidrawAPI: Props["excalidrawAPI"];
|
||||||
private socketInitializationTimer?: NodeJS.Timeout;
|
private socketInitializationTimer?: NodeJS.Timeout;
|
||||||
private excalidrawRef: Props["excalidrawRef"];
|
|
||||||
excalidrawAppState?: AppState;
|
|
||||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||||
private collaborators = new Map<string, Collaborator>();
|
private collaborators = new Map<string, Collaborator>();
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
activeRoomLink: "",
|
activeRoomLink: "",
|
||||||
};
|
};
|
||||||
this.portal = new Portal(this);
|
this.portal = new Portal(this);
|
||||||
this.excalidrawRef = props.excalidrawRef;
|
this.excalidrawAPI = props.excalidrawAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -142,7 +148,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
saveCollabRoomToFirebase = async (
|
saveCollabRoomToFirebase = async (
|
||||||
syncableElements: ExcalidrawElement[] = getSyncableElements(
|
syncableElements: ExcalidrawElement[] = getSyncableElements(
|
||||||
this.excalidrawRef.current!.getSceneElementsIncludingDeleted(),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
),
|
),
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -154,13 +160,13 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
openPortal = async () => {
|
openPortal = async () => {
|
||||||
window.history.pushState({}, APP_NAME, await generateCollaborationLink());
|
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
|
// remove deleted elements from elements array & history to ensure we don't
|
||||||
// expose potentially sensitive user data in case user manually deletes
|
// expose potentially sensitive user data in case user manually deletes
|
||||||
// existing elements (or clears scene), which would otherwise be persisted
|
// existing elements (or clears scene), which would otherwise be persisted
|
||||||
// to database even if deleted before creating the room.
|
// to database even if deleted before creating the room.
|
||||||
this.excalidrawRef.current!.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
this.excalidrawRef.current!.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
@@ -175,7 +181,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
private destroySocketClient = () => {
|
private destroySocketClient = () => {
|
||||||
this.collaborators = new Map();
|
this.collaborators = new Map();
|
||||||
this.excalidrawRef.current!.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
collaborators: this.collaborators,
|
collaborators: this.collaborators,
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -265,7 +271,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
user.selectedElementIds = selectedElementIds;
|
user.selectedElementIds = selectedElementIds;
|
||||||
user.username = username;
|
user.username = username;
|
||||||
collaborators.set(socketId, user);
|
collaborators.set(socketId, user);
|
||||||
this.excalidrawRef.current!.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
collaborators,
|
collaborators,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -300,7 +306,55 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
private reconcileElements = (
|
private reconcileElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): ReconciledElements => {
|
): 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
|
// Avoid broadcasting to the rest of the collaborators the scene
|
||||||
// we just received!
|
// we just received!
|
||||||
@@ -319,10 +373,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
}: { init?: boolean; initFromSnapshot?: boolean } = {},
|
}: { init?: boolean; initFromSnapshot?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
if (init || initFromSnapshot) {
|
if (init || initFromSnapshot) {
|
||||||
this.excalidrawRef.current!.setScrollToCenter(elements);
|
this.excalidrawAPI.setScrollToCenter(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.excalidrawRef.current!.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
commitToHistory: !!init,
|
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
|
// 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,
|
// 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.
|
// right now we think this is the right tradeoff.
|
||||||
this.excalidrawRef.current!.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
setCollaborators(sockets: string[]) {
|
setCollaborators(sockets: string[]) {
|
||||||
@@ -347,7 +401,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.collaborators = collaborators;
|
this.collaborators = collaborators;
|
||||||
this.excalidrawRef.current!.updateScene({ collaborators });
|
this.excalidrawAPI.updateScene({ collaborators });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +414,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public getSceneElementsIncludingDeleted = () => {
|
public getSceneElementsIncludingDeleted = () => {
|
||||||
return this.excalidrawRef.current!.getSceneElementsIncludingDeleted();
|
return this.excalidrawAPI.getSceneElementsIncludingDeleted();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPointerUpdate = (payload: {
|
onPointerUpdate = (payload: {
|
||||||
@@ -373,11 +427,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.portal.broadcastMouseLocation(payload);
|
this.portal.broadcastMouseLocation(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
broadcastElements = (
|
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
state: AppState,
|
|
||||||
) => {
|
|
||||||
this.excalidrawAppState = state;
|
|
||||||
if (
|
if (
|
||||||
getSceneVersion(elements) >
|
getSceneVersion(elements) >
|
||||||
this.getLastBroadcastedOrReceivedSceneVersion()
|
this.getLastBroadcastedOrReceivedSceneVersion()
|
||||||
@@ -396,7 +446,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.portal.broadcastScene(
|
this.portal.broadcastScene(
|
||||||
SCENE.UPDATE,
|
SCENE.UPDATE,
|
||||||
getSyncableElements(
|
getSyncableElements(
|
||||||
this.excalidrawRef.current!.getSceneElementsIncludingDeleted(),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
),
|
),
|
||||||
true,
|
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() {
|
render() {
|
||||||
const { children } = this.props;
|
|
||||||
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
|
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -450,14 +515,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
onClose={() => this.setState({ errorMessage: "" })}
|
onClose={() => this.setState({ errorMessage: "" })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{children({
|
<CollabContextProvider
|
||||||
isCollaborating: this.state.isCollaborating,
|
value={{
|
||||||
username: this.state.username,
|
api: this.getContextValue(),
|
||||||
onPointerUpdate: this.onPointerUpdate,
|
}}
|
||||||
initializeSocketClient: this.initializeSocketClient,
|
/>
|
||||||
onCollabButtonClick: this.onCollabButtonClick,
|
|
||||||
broadcastElements: this.broadcastElements,
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,20 @@ import {
|
|||||||
|
|
||||||
import CollabWrapper from "./CollabWrapper";
|
import CollabWrapper from "./CollabWrapper";
|
||||||
|
|
||||||
import {
|
import { getSyncableElements } from "../../packages/excalidraw/index";
|
||||||
getElementMap,
|
|
||||||
getSyncableElements,
|
|
||||||
} from "../../packages/excalidraw/index";
|
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { BROADCAST, SCENE } from "../app_constants";
|
import { BROADCAST, SCENE } from "../app_constants";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
app: CollabWrapper;
|
collab: CollabWrapper;
|
||||||
socket: SocketIOClient.Socket | null = null;
|
socket: SocketIOClient.Socket | null = null;
|
||||||
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
||||||
roomId: string | null = null;
|
roomId: string | null = null;
|
||||||
roomKey: string | null = null;
|
roomKey: string | null = null;
|
||||||
broadcastedElementVersions: Map<string, number> = new Map();
|
broadcastedElementVersions: Map<string, number> = new Map();
|
||||||
|
|
||||||
constructor(app: CollabWrapper) {
|
constructor(collab: CollabWrapper) {
|
||||||
this.app = app;
|
this.collab = collab;
|
||||||
}
|
}
|
||||||
|
|
||||||
open(socket: SocketIOClient.Socket, id: string, key: string) {
|
open(socket: SocketIOClient.Socket, id: string, key: string) {
|
||||||
@@ -30,7 +27,7 @@ class Portal {
|
|||||||
this.roomId = id;
|
this.roomId = id;
|
||||||
this.roomKey = key;
|
this.roomKey = key;
|
||||||
|
|
||||||
// Initialize socket listeners (moving from App)
|
// Initialize socket listeners
|
||||||
this.socket.on("init-room", () => {
|
this.socket.on("init-room", () => {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit("join-room", this.roomId);
|
this.socket.emit("join-room", this.roomId);
|
||||||
@@ -39,12 +36,12 @@ class Portal {
|
|||||||
this.socket.on("new-user", async (_socketId: string) => {
|
this.socket.on("new-user", async (_socketId: string) => {
|
||||||
this.broadcastScene(
|
this.broadcastScene(
|
||||||
SCENE.INIT,
|
SCENE.INIT,
|
||||||
getSyncableElements(this.app.getSceneElementsIncludingDeleted()),
|
getSyncableElements(this.collab.getSceneElementsIncludingDeleted()),
|
||||||
/* syncAll */ true,
|
/* syncAll */ true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.socket.on("room-user-change", (clients: string[]) => {
|
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,
|
data as SocketUpdateData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (syncAll && this.app.state.isCollaborating) {
|
if (syncAll && this.collab.state.isCollaborating) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
broadcastPromise,
|
broadcastPromise,
|
||||||
this.app.saveCollabRoomToFirebase(syncableElements),
|
this.collab.saveCollabRoomToFirebase(syncableElements),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
await broadcastPromise;
|
await broadcastPromise;
|
||||||
@@ -146,9 +143,9 @@ class Portal {
|
|||||||
socketId: this.socket.id,
|
socketId: this.socket.id,
|
||||||
pointer: payload.pointer,
|
pointer: payload.pointer,
|
||||||
button: payload.button || "up",
|
button: payload.button || "up",
|
||||||
selectedElementIds:
|
selectedElementIds: this.collab.excalidrawAPI.getAppState()
|
||||||
this.app.excalidrawAppState?.selectedElementIds || {},
|
.selectedElementIds,
|
||||||
username: this.app.state.username,
|
username: this.collab.state.username,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return this._broadcastSocketData(
|
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;
|
export default Portal;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../../css/_variables";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.RoomDialog-linkContainer {
|
.RoomDialog-linkContainer {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -17,12 +18,13 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import Excalidraw, {
|
import Excalidraw, {
|
||||||
defaultLang,
|
defaultLang,
|
||||||
languages,
|
languages,
|
||||||
} from "../packages/excalidraw/index";
|
} from "../packages/excalidraw/index";
|
||||||
import { AppState, ExcalidrawAPIRefValue } from "../types";
|
import { AppState } from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
@@ -30,7 +32,11 @@ import {
|
|||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants";
|
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 { LanguageList } from "./components/LanguageList";
|
||||||
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
||||||
import { loadFromFirebase } from "./data/firebase";
|
import { loadFromFirebase } from "./data/firebase";
|
||||||
@@ -49,15 +55,6 @@ languageDetector.init({
|
|||||||
checkWhitelist: false,
|
checkWhitelist: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const excalidrawRef: React.MutableRefObject<
|
|
||||||
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
|
|
||||||
> = {
|
|
||||||
current: {
|
|
||||||
readyPromise: resolvablePromise(),
|
|
||||||
ready: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveDebounced = debounce(
|
const saveDebounced = debounce(
|
||||||
(elements: readonly ExcalidrawElement[], state: AppState) => {
|
(elements: readonly ExcalidrawElement[], state: AppState) => {
|
||||||
saveToLocalStorage(elements, state);
|
saveToLocalStorage(elements, state);
|
||||||
@@ -191,7 +188,7 @@ const initializeScene = async (opts: {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
function ExcalidrawWrapper() {
|
||||||
// dimensions
|
// dimensions
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -226,35 +223,40 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
initialStatePromiseRef.current.promise = resolvablePromise<ImportedDataState | null>();
|
initialStatePromiseRef.current.promise = resolvablePromise<ImportedDataState | null>();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { collab } = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Delayed so that the app has a time to load the latest SW
|
// Delayed so that the app has a time to load the latest SW
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
trackEvent("load", "version", getVersion());
|
trackEvent("load", "version", getVersion());
|
||||||
}, VERSION_TIMEOUT);
|
}, VERSION_TIMEOUT);
|
||||||
|
}, []);
|
||||||
|
|
||||||
excalidrawRef.current!.readyPromise.then((excalidrawApi) => {
|
const [
|
||||||
initializeScene({
|
excalidrawAPI,
|
||||||
resetScene: excalidrawApi.resetScene,
|
excalidrawRefCallback,
|
||||||
initializeSocketClient: collab.initializeSocketClient,
|
] = useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||||
}).then((scene) => {
|
|
||||||
initialStatePromiseRef.current.promise.resolve(scene);
|
const collabAPI = useContext(CollabContext)?.api;
|
||||||
});
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!collabAPI || !excalidrawAPI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeScene({
|
||||||
|
resetScene: excalidrawAPI.resetScene,
|
||||||
|
initializeSocketClient: collabAPI.initializeSocketClient,
|
||||||
|
}).then((scene) => {
|
||||||
|
initialStatePromiseRef.current.promise.resolve(scene);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHashChange = (_: HashChangeEvent) => {
|
const onHashChange = (_: HashChangeEvent) => {
|
||||||
const api = excalidrawRef.current!;
|
|
||||||
if (!api.ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.location.hash.length > 1) {
|
if (window.location.hash.length > 1) {
|
||||||
initializeScene({
|
initializeScene({
|
||||||
resetScene: api.resetScene,
|
resetScene: excalidrawAPI.resetScene,
|
||||||
initializeSocketClient: collab.initializeSocketClient,
|
initializeSocketClient: collabAPI.initializeSocketClient,
|
||||||
}).then((scene) => {
|
}).then((scene) => {
|
||||||
if (scene) {
|
if (scene) {
|
||||||
api.updateScene(scene);
|
excalidrawAPI.updateScene(scene);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -273,7 +275,7 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
||||||
clearTimeout(titleTimeout);
|
clearTimeout(titleTimeout);
|
||||||
};
|
};
|
||||||
}, [collab.initializeSocketClient]);
|
}, [collabAPI, excalidrawAPI]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
languageDetector.cacheUserLanguage(langCode);
|
languageDetector.cacheUserLanguage(langCode);
|
||||||
@@ -284,8 +286,8 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
saveDebounced(elements, appState);
|
saveDebounced(elements, appState);
|
||||||
if (collab.isCollaborating) {
|
if (collabAPI?.isCollaborating) {
|
||||||
collab.broadcastElements(elements, appState);
|
collabAPI.broadcastElements(elements);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,19 +345,20 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={excalidrawRef}
|
ref={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
width={dimensions.width}
|
width={dimensions.width}
|
||||||
height={dimensions.height}
|
height={dimensions.height}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
user={{ name: collab.username }}
|
user={{ name: collabAPI?.username }}
|
||||||
onCollabButtonClick={collab.onCollabButtonClick}
|
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||||
isCollaborating={collab.isCollaborating}
|
isCollaborating={collabAPI?.isCollaborating}
|
||||||
onPointerUpdate={collab.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
onExportToBackend={onExportToBackend}
|
onExportToBackend={onExportToBackend}
|
||||||
renderFooter={renderFooter}
|
renderFooter={renderFooter}
|
||||||
langCode={langCode}
|
langCode={langCode}
|
||||||
/>
|
/>
|
||||||
|
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
message={errorMessage}
|
message={errorMessage}
|
||||||
@@ -369,13 +372,9 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
export default function ExcalidrawApp() {
|
export default function ExcalidrawApp() {
|
||||||
return (
|
return (
|
||||||
<TopErrorBoundary>
|
<TopErrorBoundary>
|
||||||
<CollabWrapper
|
<CollabContextConsumer>
|
||||||
excalidrawRef={
|
<ExcalidrawWrapper />
|
||||||
excalidrawRef as React.MutableRefObject<ExcalidrawImperativeAPI>
|
</CollabContextConsumer>
|
||||||
}
|
|
||||||
>
|
|
||||||
{(collab) => <ExcalidrawWrapper collab={collab} />}
|
|
||||||
</CollabWrapper>
|
|
||||||
</TopErrorBoundary>
|
</TopErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,11 +1,10 @@
|
|||||||
import { PointerCoords } from "./types";
|
import { PointerCoords } from "./types";
|
||||||
import { normalizeScroll } from "./scene";
|
|
||||||
|
|
||||||
export const getCenter = (pointers: Map<number, PointerCoords>) => {
|
export const getCenter = (pointers: Map<number, PointerCoords>) => {
|
||||||
const allCoords = Array.from(pointers.values());
|
const allCoords = Array.from(pointers.values());
|
||||||
return {
|
return {
|
||||||
x: normalizeScroll(sum(allCoords, (coords) => coords.x) / allCoords.length),
|
x: sum(allCoords, (coords) => coords.x) / allCoords.length,
|
||||||
y: normalizeScroll(sum(allCoords, (coords) => coords.y) / allCoords.length),
|
y: sum(allCoords, (coords) => coords.y) / allCoords.length,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -85,6 +85,6 @@ type ForwardRef<T, P = any> = Parameters<
|
|||||||
// --------------------------------------------------------------------------—
|
// --------------------------------------------------------------------------—
|
||||||
|
|
||||||
interface Blob {
|
interface Blob {
|
||||||
handle?: import("browser-nativefs").FileSystemHandle;
|
handle?: import("browser-fs-acces").FileSystemHandle;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "id-ID", label: "Bahasa Indonesia" },
|
||||||
{ code: "it-IT", label: "Italiano" },
|
{ code: "it-IT", label: "Italiano" },
|
||||||
{ code: "ja-JP", label: "日本語" },
|
{ code: "ja-JP", label: "日本語" },
|
||||||
|
{ code: "kab-KAB", label: "Taqbaylit" },
|
||||||
{ code: "ko-KR", label: "한국어" },
|
{ code: "ko-KR", label: "한국어" },
|
||||||
{ code: "my-MM", label: "Burmese" },
|
{ code: "my-MM", label: "Burmese" },
|
||||||
{ code: "nb-NO", label: "Norsk bokmål" },
|
{ code: "nb-NO", label: "Norsk bokmål" },
|
||||||
|
|||||||
+14
-10
@@ -1,7 +1,18 @@
|
|||||||
import React, { useState, useEffect, useRef, useContext } from "react";
|
import React, { useState, useEffect, useRef, useContext } from "react";
|
||||||
|
import variables from "./css/variables.module.scss";
|
||||||
|
|
||||||
const context = React.createContext(false);
|
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 = ({
|
export const IsMobileProvider = ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -9,16 +20,7 @@ export const IsMobileProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const query = useRef<MediaQueryList>();
|
const query = useRef<MediaQueryList>();
|
||||||
if (!query.current) {
|
if (!query.current) {
|
||||||
query.current = window.matchMedia
|
query.current = getIsMobileMatcher();
|
||||||
? 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);
|
|
||||||
}
|
}
|
||||||
const [isMobile, setMobile] = useState(query.current.matches);
|
const [isMobile, setMobile] = useState(query.current.matches);
|
||||||
|
|
||||||
@@ -31,6 +33,8 @@ export const IsMobileProvider = ({
|
|||||||
return <context.Provider value={isMobile}>{children}</context.Provider>;
|
return <context.Provider value={isMobile}>{children}</context.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isMobile = () => getIsMobileMatcher().matches;
|
||||||
|
|
||||||
export default function useIsMobile() {
|
export default function useIsMobile() {
|
||||||
return useContext(context);
|
return useContext(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
||||||
|
export const isWindows = /^Win/.test(window.navigator.platform);
|
||||||
|
|
||||||
export const CODES = {
|
export const CODES = {
|
||||||
EQUAL: "Equal",
|
EQUAL: "Equal",
|
||||||
@@ -18,7 +19,9 @@ export const CODES = {
|
|||||||
F: "KeyF",
|
F: "KeyF",
|
||||||
H: "KeyH",
|
H: "KeyH",
|
||||||
V: "KeyV",
|
V: "KeyV",
|
||||||
|
X: "KeyX",
|
||||||
Z: "KeyZ",
|
Z: "KeyZ",
|
||||||
|
R: "KeyR",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
@@ -48,6 +51,7 @@ export const KEYS = {
|
|||||||
T: "t",
|
T: "t",
|
||||||
V: "v",
|
V: "v",
|
||||||
X: "x",
|
X: "x",
|
||||||
|
Y: "y",
|
||||||
Z: "z",
|
Z: "z",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "وضع الشبكة",
|
"gridMode": "وضع الشبكة",
|
||||||
"addToLibrary": "أضف إلى المكتبة",
|
"addToLibrary": "أضف إلى المكتبة",
|
||||||
"removeFromLibrary": "حذف من المكتبة",
|
"removeFromLibrary": "حذف من المكتبة",
|
||||||
"libraryLoadingMessage": "جارٍ تحميل المكتبة...",
|
"libraryLoadingMessage": "جارٍ تحميل المكتبة…",
|
||||||
"libraries": "تصفح المكتبات",
|
"libraries": "تصفح المكتبات",
|
||||||
"loadingScene": "جاري تحميل المشهد...",
|
"loadingScene": "جاري تحميل المشهد…",
|
||||||
"align": "محاذاة",
|
"align": "محاذاة",
|
||||||
"alignTop": "محاذاة إلى اﻷعلى",
|
"alignTop": "محاذاة إلى اﻷعلى",
|
||||||
"alignBottom": "محاذاة إلى اﻷسفل",
|
"alignBottom": "محاذاة إلى اﻷسفل",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "توسيط عمودي",
|
"centerVertically": "توسيط عمودي",
|
||||||
"centerHorizontally": "توسيط أفقي",
|
"centerHorizontally": "توسيط أفقي",
|
||||||
"distributeHorizontally": "التوزيع الأفقي",
|
"distributeHorizontally": "التوزيع الأفقي",
|
||||||
"distributeVertically": "التوزيع عمودياً"
|
"distributeVertically": "التوزيع عمودياً",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "إعادة تعيين اللوحة",
|
"clearReset": "إعادة تعيين اللوحة",
|
||||||
|
|||||||
+12
-11
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Решетъчен режим",
|
"gridMode": "Решетъчен режим",
|
||||||
"addToLibrary": "Добавяне към библиотеката",
|
"addToLibrary": "Добавяне към библиотеката",
|
||||||
"removeFromLibrary": "Премахване от библиотеката",
|
"removeFromLibrary": "Премахване от библиотеката",
|
||||||
"libraryLoadingMessage": "Зареждане на библиотеката...",
|
"libraryLoadingMessage": "Зареждане на библиотеката…",
|
||||||
"libraries": "Разглеждане на библиотеките",
|
"libraries": "Разглеждане на библиотеките",
|
||||||
"loadingScene": "Зареждане на сцена...",
|
"loadingScene": "Зареждане на сцена…",
|
||||||
"align": "Подравняване",
|
"align": "Подравняване",
|
||||||
"alignTop": "Подравняване отгоре",
|
"alignTop": "Подравняване отгоре",
|
||||||
"alignBottom": "Подравняване отдолу",
|
"alignBottom": "Подравняване отдолу",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Центрирай вертикално",
|
"centerVertically": "Центрирай вертикално",
|
||||||
"centerHorizontally": "Центрирай хоризонтално",
|
"centerHorizontally": "Центрирай хоризонтално",
|
||||||
"distributeHorizontally": "Разпредели хоризонтално",
|
"distributeHorizontally": "Разпредели хоризонтално",
|
||||||
"distributeVertically": "Разпредели вертикално"
|
"distributeVertically": "Разпредели вертикално",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Нулиране на платно",
|
"clearReset": "Нулиране на платно",
|
||||||
@@ -201,24 +202,24 @@
|
|||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "",
|
||||||
"click": "",
|
"click": "клик",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "",
|
||||||
"curvedLine": "",
|
"curvedLine": "",
|
||||||
"documentation": "",
|
"documentation": "",
|
||||||
"drag": "",
|
"drag": "плъзнете",
|
||||||
"editor": "",
|
"editor": "Редактор",
|
||||||
"github": "",
|
"github": "",
|
||||||
"howto": "",
|
"howto": "",
|
||||||
"or": "",
|
"or": "или",
|
||||||
"preventBinding": "",
|
"preventBinding": "",
|
||||||
"shapes": "",
|
"shapes": "Фигури",
|
||||||
"shortcuts": "",
|
"shortcuts": "Клавиши за бърз достъп",
|
||||||
"textFinish": "",
|
"textFinish": "",
|
||||||
"textNewLine": "",
|
"textNewLine": "",
|
||||||
"title": "",
|
"title": "",
|
||||||
"view": "",
|
"view": "Преглед",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "Приближи селекцията"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат."
|
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат."
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
"removeFromLibrary": "Eliminar de la biblioteca",
|
"removeFromLibrary": "Eliminar de la biblioteca",
|
||||||
"libraryLoadingMessage": "Carregant la biblioteca...",
|
"libraryLoadingMessage": "Carregant la biblioteca...",
|
||||||
"libraries": "Explorar biblioteques",
|
"libraries": "Explorar biblioteques",
|
||||||
"loadingScene": "Carregant escena...",
|
"loadingScene": "Carregant escena…",
|
||||||
"align": "Alinear",
|
"align": "Alinear",
|
||||||
"alignTop": "Alinear a dalt",
|
"alignTop": "Alinear a dalt",
|
||||||
"alignBottom": "Alinear a baix",
|
"alignBottom": "Alinear a baix",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrar verticalment",
|
"centerVertically": "Centrar verticalment",
|
||||||
"centerHorizontally": "Centrar horitzontalment",
|
"centerHorizontally": "Centrar horitzontalment",
|
||||||
"distributeHorizontally": "Distribuir horitzontalment",
|
"distributeHorizontally": "Distribuir horitzontalment",
|
||||||
"distributeVertically": "Distribuir verticalment"
|
"distributeVertically": "Distribuir verticalment",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Netejar el llenç",
|
"clearReset": "Netejar el llenç",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Rastermodus",
|
"gridMode": "Rastermodus",
|
||||||
"addToLibrary": "Zur Bibliothek hinzufügen",
|
"addToLibrary": "Zur Bibliothek hinzufügen",
|
||||||
"removeFromLibrary": "Aus Bibliothek entfernen",
|
"removeFromLibrary": "Aus Bibliothek entfernen",
|
||||||
"libraryLoadingMessage": "Lade Bibliothek...",
|
"libraryLoadingMessage": "Lade Bibliothek…",
|
||||||
"libraries": "Bibliotheken durchsuchen",
|
"libraries": "Bibliotheken durchsuchen",
|
||||||
"loadingScene": "Lade Zeichnung...",
|
"loadingScene": "Lade Zeichnung…",
|
||||||
"align": "Ausrichten",
|
"align": "Ausrichten",
|
||||||
"alignTop": "Obere Kanten",
|
"alignTop": "Obere Kanten",
|
||||||
"alignBottom": "Untere Kanten",
|
"alignBottom": "Untere Kanten",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Vertikal zentrieren",
|
"centerVertically": "Vertikal zentrieren",
|
||||||
"centerHorizontally": "Horizontal zentrieren",
|
"centerHorizontally": "Horizontal zentrieren",
|
||||||
"distributeHorizontally": "Horizontal verteilen",
|
"distributeHorizontally": "Horizontal verteilen",
|
||||||
"distributeVertically": "Vertikal verteilen"
|
"distributeVertically": "Vertikal verteilen",
|
||||||
|
"viewMode": "Ansichtsmodus"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Εμφάνιση σε πλέγμα",
|
"gridMode": "Εμφάνιση σε πλέγμα",
|
||||||
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
|
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
|
||||||
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
|
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
|
||||||
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης...",
|
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης…",
|
||||||
"libraries": "Άλλες βιβλιοθήκες",
|
"libraries": "Άλλες βιβλιοθήκες",
|
||||||
"loadingScene": "Φόρτωση σκηνής...",
|
"loadingScene": "Φόρτωση σκηνής…",
|
||||||
"align": "Στοίχιση",
|
"align": "Στοίχιση",
|
||||||
"alignTop": "Στοίχιση πάνω",
|
"alignTop": "Στοίχιση πάνω",
|
||||||
"alignBottom": "Στοίχιση κάτω",
|
"alignBottom": "Στοίχιση κάτω",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Κέντρο κάθετα",
|
"centerVertically": "Κέντρο κάθετα",
|
||||||
"centerHorizontally": "Κέντρο οριζόντια",
|
"centerHorizontally": "Κέντρο οριζόντια",
|
||||||
"distributeHorizontally": "Οριζόντια κατανομή",
|
"distributeHorizontally": "Οριζόντια κατανομή",
|
||||||
"distributeVertically": "Κατακόρυφη κατανομή"
|
"distributeVertically": "Κατακόρυφη κατανομή",
|
||||||
|
"viewMode": "Λειτουργία προβολής"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Επαναφορά του καμβά",
|
"clearReset": "Επαναφορά του καμβά",
|
||||||
|
|||||||
+4
-3
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Grid mode",
|
"gridMode": "Grid mode",
|
||||||
"addToLibrary": "Add to library",
|
"addToLibrary": "Add to library",
|
||||||
"removeFromLibrary": "Remove from library",
|
"removeFromLibrary": "Remove from library",
|
||||||
"libraryLoadingMessage": "Loading library...",
|
"libraryLoadingMessage": "Loading library…",
|
||||||
"libraries": "Browse libraries",
|
"libraries": "Browse libraries",
|
||||||
"loadingScene": "Loading scene...",
|
"loadingScene": "Loading scene…",
|
||||||
"align": "Align",
|
"align": "Align",
|
||||||
"alignTop": "Align top",
|
"alignTop": "Align top",
|
||||||
"alignBottom": "Align bottom",
|
"alignBottom": "Align bottom",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Center vertically",
|
"centerVertically": "Center vertically",
|
||||||
"centerHorizontally": "Center horizontally",
|
"centerHorizontally": "Center horizontally",
|
||||||
"distributeHorizontally": "Distribute horizontally",
|
"distributeHorizontally": "Distribute horizontally",
|
||||||
"distributeVertically": "Distribute vertically"
|
"distributeVertically": "Distribute vertically",
|
||||||
|
"viewMode": "View mode"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
|
|||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrar verticalmente",
|
"centerVertically": "Centrar verticalmente",
|
||||||
"centerHorizontally": "Centrar horizontalmente",
|
"centerHorizontally": "Centrar horizontalmente",
|
||||||
"distributeHorizontally": "Distribuir horizontalmente",
|
"distributeHorizontally": "Distribuir horizontalmente",
|
||||||
"distributeVertically": "Distribuir verticalmente"
|
"distributeVertically": "Distribuir verticalmente",
|
||||||
|
"viewMode": "Modo presentación"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
||||||
|
|||||||
+22
-21
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "حالت شبکه ای",
|
"gridMode": "حالت شبکه ای",
|
||||||
"addToLibrary": "افزودن به کتابخانه",
|
"addToLibrary": "افزودن به کتابخانه",
|
||||||
"removeFromLibrary": "حذف از کتابخانه",
|
"removeFromLibrary": "حذف از کتابخانه",
|
||||||
"libraryLoadingMessage": "بارگذاری کتابخانه...",
|
"libraryLoadingMessage": "بارگذاری کتابخانه…",
|
||||||
"libraries": "مرور کردن کتابخانه ها",
|
"libraries": "مرور کردن کتابخانه ها",
|
||||||
"loadingScene": "باگذاری صحنه...",
|
"loadingScene": "باگذاری صحنه…",
|
||||||
"align": "تراز",
|
"align": "تراز",
|
||||||
"alignTop": "تراز به بالا",
|
"alignTop": "تراز به بالا",
|
||||||
"alignBottom": "تراز به پایین",
|
"alignBottom": "تراز به پایین",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "وسط قرار دادن به صورت عمودی",
|
"centerVertically": "وسط قرار دادن به صورت عمودی",
|
||||||
"centerHorizontally": "وسط قرار دادن به صورت افقی",
|
"centerHorizontally": "وسط قرار دادن به صورت افقی",
|
||||||
"distributeHorizontally": "توزیع کردن به صورت افقی",
|
"distributeHorizontally": "توزیع کردن به صورت افقی",
|
||||||
"distributeVertically": "توزیع کردن به صورت عمودی"
|
"distributeVertically": "توزیع کردن به صورت عمودی",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "پاکسازی بوم نقاشی",
|
"clearReset": "پاکسازی بوم نقاشی",
|
||||||
@@ -200,25 +201,25 @@
|
|||||||
"title": "خطا"
|
"title": "خطا"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "بلاگ ما را بخوانید",
|
||||||
"click": "",
|
"click": "",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "فلش خمیده",
|
||||||
"curvedLine": "",
|
"curvedLine": "منحنی",
|
||||||
"documentation": "",
|
"documentation": "مستندات",
|
||||||
"drag": "",
|
"drag": "",
|
||||||
"editor": "",
|
"editor": "ویرایشگر",
|
||||||
"github": "",
|
"github": "اشکالی می بینید؟ گزارش دهید",
|
||||||
"howto": "",
|
"howto": "راهنمای ما را دنبال کنید",
|
||||||
"or": "",
|
"or": "یا",
|
||||||
"preventBinding": "",
|
"preventBinding": "مانع شدن از چسبیدن فلش ها",
|
||||||
"shapes": "",
|
"shapes": "شکلها",
|
||||||
"shortcuts": "",
|
"shortcuts": "میانبرهای صفحه کلید",
|
||||||
"textFinish": "",
|
"textFinish": "",
|
||||||
"textNewLine": "",
|
"textNewLine": "یک خط جدید اضافه کنید (متن)",
|
||||||
"title": "",
|
"title": "راهنما",
|
||||||
"view": "",
|
"view": "مشاهده",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
|
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
|
||||||
@@ -236,7 +237,7 @@
|
|||||||
"width": "عرض"
|
"width": "عرض"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "کپی سبک.",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Ruudukkotila",
|
"gridMode": "Ruudukkotila",
|
||||||
"addToLibrary": "Lisää kirjastoon",
|
"addToLibrary": "Lisää kirjastoon",
|
||||||
"removeFromLibrary": "Poista kirjastosta",
|
"removeFromLibrary": "Poista kirjastosta",
|
||||||
"libraryLoadingMessage": "Ladataan kirjastoa...",
|
"libraryLoadingMessage": "Ladataan kirjastoa…",
|
||||||
"libraries": "Selaa kirjastoja",
|
"libraries": "Selaa kirjastoja",
|
||||||
"loadingScene": "Ladataan työtä...",
|
"loadingScene": "Ladataan työtä…",
|
||||||
"align": "Tasaa",
|
"align": "Tasaa",
|
||||||
"alignTop": "Tasaa ylös",
|
"alignTop": "Tasaa ylös",
|
||||||
"alignBottom": "Tasaa alas",
|
"alignBottom": "Tasaa alas",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Keskitä pystysuunnassa",
|
"centerVertically": "Keskitä pystysuunnassa",
|
||||||
"centerHorizontally": "Keskitä vaakasuunnassa",
|
"centerHorizontally": "Keskitä vaakasuunnassa",
|
||||||
"distributeHorizontally": "Jaa vaakasuunnassa",
|
"distributeHorizontally": "Jaa vaakasuunnassa",
|
||||||
"distributeVertically": "Jaa pystysuunnassa"
|
"distributeVertically": "Jaa pystysuunnassa",
|
||||||
|
"viewMode": "Katselutila"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Tyhjennä piirtoalue",
|
"clearReset": "Tyhjennä piirtoalue",
|
||||||
|
|||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrer verticalement",
|
"centerVertically": "Centrer verticalement",
|
||||||
"centerHorizontally": "Centrer horizontalement",
|
"centerHorizontally": "Centrer horizontalement",
|
||||||
"distributeHorizontally": "Distribuer horizontalement",
|
"distributeHorizontally": "Distribuer horizontalement",
|
||||||
"distributeVertically": "Distribuer verticalement"
|
"distributeVertically": "Distribuer verticalement",
|
||||||
|
"viewMode": "Mode présentation"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Réinitialiser le canevas",
|
"clearReset": "Réinitialiser le canevas",
|
||||||
@@ -229,7 +230,7 @@
|
|||||||
"elements": "Éléments",
|
"elements": "Éléments",
|
||||||
"height": "Hauteur",
|
"height": "Hauteur",
|
||||||
"scene": "Scène",
|
"scene": "Scène",
|
||||||
"selected": "Sélectionné",
|
"selected": "Sélection",
|
||||||
"storage": "Stockage",
|
"storage": "Stockage",
|
||||||
"title": "Stats pour les nerds",
|
"title": "Stats pour les nerds",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "מצב רשת",
|
"gridMode": "מצב רשת",
|
||||||
"addToLibrary": "הוסף לספריה",
|
"addToLibrary": "הוסף לספריה",
|
||||||
"removeFromLibrary": "הסר מספריה",
|
"removeFromLibrary": "הסר מספריה",
|
||||||
"libraryLoadingMessage": "טוען ספריה...",
|
"libraryLoadingMessage": "טוען ספריה…",
|
||||||
"libraries": "דפדף בספריות",
|
"libraries": "דפדף בספריות",
|
||||||
"loadingScene": "טוען תצוגה...",
|
"loadingScene": "טוען תצוגה…",
|
||||||
"align": "יישר",
|
"align": "יישר",
|
||||||
"alignTop": "יישר למעלה",
|
"alignTop": "יישר למעלה",
|
||||||
"alignBottom": "יישר למטה",
|
"alignBottom": "יישר למטה",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "מרכז אנכית",
|
"centerVertically": "מרכז אנכית",
|
||||||
"centerHorizontally": "מרכז אופקית",
|
"centerHorizontally": "מרכז אופקית",
|
||||||
"distributeHorizontally": "חלוקה אופקית",
|
"distributeHorizontally": "חלוקה אופקית",
|
||||||
"distributeVertically": "חלוקה אנכית"
|
"distributeVertically": "חלוקה אנכית",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "אפס את הלוח",
|
"clearReset": "אפס את הלוח",
|
||||||
|
|||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "लंबवत केन्द्रित",
|
"centerVertically": "लंबवत केन्द्रित",
|
||||||
"centerHorizontally": "क्षैतिज केन्द्रित",
|
"centerHorizontally": "क्षैतिज केन्द्रित",
|
||||||
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
||||||
"distributeVertically": "खड़ी रूप से वितरित करें"
|
"distributeVertically": "खड़ी रूप से वितरित करें",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "कैनवास रीसेट करें",
|
"clearReset": "कैनवास रीसेट करें",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Hálómód",
|
"gridMode": "Hálómód",
|
||||||
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
||||||
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
"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",
|
"libraries": "Könyvtárak böngészése",
|
||||||
"loadingScene": "Jelenet betöltése...",
|
"loadingScene": "Jelenet betöltése…",
|
||||||
"align": "Igazítás",
|
"align": "Igazítás",
|
||||||
"alignTop": "Felülre igazítás",
|
"alignTop": "Felülre igazítás",
|
||||||
"alignBottom": "Alulra igazítás",
|
"alignBottom": "Alulra igazítás",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Függőlegesen középre igazított",
|
"centerVertically": "Függőlegesen középre igazított",
|
||||||
"centerHorizontally": "Vízszintesen középre igazított",
|
"centerHorizontally": "Vízszintesen középre igazított",
|
||||||
"distributeHorizontally": "Vízszintes elosztás",
|
"distributeHorizontally": "Vízszintes elosztás",
|
||||||
"distributeVertically": "Függőleges elosztás"
|
"distributeVertically": "Függőleges elosztás",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Vászon törlése",
|
"clearReset": "Vászon törlése",
|
||||||
|
|||||||
+23
-22
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Mode grid",
|
"gridMode": "Mode grid",
|
||||||
"addToLibrary": "Tambahkan ke pustaka",
|
"addToLibrary": "Tambahkan ke pustaka",
|
||||||
"removeFromLibrary": "Hapus dari pustaka",
|
"removeFromLibrary": "Hapus dari pustaka",
|
||||||
"libraryLoadingMessage": "Memuat pustaka...",
|
"libraryLoadingMessage": "Memuat pustaka…",
|
||||||
"libraries": "Telusur pustaka",
|
"libraries": "Telusur pustaka",
|
||||||
"loadingScene": "Memuat pemandangan...",
|
"loadingScene": "Memuat pemandangan…",
|
||||||
"align": "Perataan",
|
"align": "Perataan",
|
||||||
"alignTop": "Rata atas",
|
"alignTop": "Rata atas",
|
||||||
"alignBottom": "Rata bawah",
|
"alignBottom": "Rata bawah",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Pusatkan secara vertikal",
|
"centerVertically": "Pusatkan secara vertikal",
|
||||||
"centerHorizontally": "Pusatkan secara horizontal",
|
"centerHorizontally": "Pusatkan secara horizontal",
|
||||||
"distributeHorizontally": "Distribusikan horizontal",
|
"distributeHorizontally": "Distribusikan horizontal",
|
||||||
"distributeVertically": "Distribusikan vertikal"
|
"distributeVertically": "Distribusikan vertikal",
|
||||||
|
"viewMode": "Mode tampilan"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Setel Ulang Kanvas",
|
"clearReset": "Setel Ulang Kanvas",
|
||||||
@@ -200,25 +201,25 @@
|
|||||||
"title": "Kesalahan"
|
"title": "Kesalahan"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "Baca blog kami",
|
||||||
"click": "",
|
"click": "klik",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "Panah lengkung",
|
||||||
"curvedLine": "",
|
"curvedLine": "Garis lengkung",
|
||||||
"documentation": "",
|
"documentation": "Dokumentasi",
|
||||||
"drag": "",
|
"drag": "seret",
|
||||||
"editor": "",
|
"editor": "Editor",
|
||||||
"github": "",
|
"github": "Menemukan masalah? Kirimkan",
|
||||||
"howto": "",
|
"howto": "Ikuti panduan kami",
|
||||||
"or": "",
|
"or": "atau",
|
||||||
"preventBinding": "",
|
"preventBinding": "Cegah pengikatan panah",
|
||||||
"shapes": "",
|
"shapes": "Bentuk",
|
||||||
"shortcuts": "",
|
"shortcuts": "Pintasan keyboard",
|
||||||
"textFinish": "",
|
"textFinish": "Selesai mengedit (teks)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Tambahkan baris baru (teks)",
|
||||||
"title": "",
|
"title": "Bantuan",
|
||||||
"view": "",
|
"view": "Tampilan",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "Perbesar agar sesuai dengan semua elemen",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "Perbesar ke seleksi"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
|
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
|
||||||
|
|||||||
+23
-22
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Modalità griglia",
|
"gridMode": "Modalità griglia",
|
||||||
"addToLibrary": "Aggiungi alla libreria",
|
"addToLibrary": "Aggiungi alla libreria",
|
||||||
"removeFromLibrary": "Rimuovi dalla libreria",
|
"removeFromLibrary": "Rimuovi dalla libreria",
|
||||||
"libraryLoadingMessage": "Caricamento della biblioteca...",
|
"libraryLoadingMessage": "Caricamento della biblioteca…",
|
||||||
"libraries": "Sfoglia librerie",
|
"libraries": "Sfoglia librerie",
|
||||||
"loadingScene": "Caricamento della scena...",
|
"loadingScene": "Caricamento della scena…",
|
||||||
"align": "Allinea",
|
"align": "Allinea",
|
||||||
"alignTop": "Allinea in alto",
|
"alignTop": "Allinea in alto",
|
||||||
"alignBottom": "Allinea in basso",
|
"alignBottom": "Allinea in basso",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centra Verticalmente",
|
"centerVertically": "Centra Verticalmente",
|
||||||
"centerHorizontally": "Centra orizzontalmente",
|
"centerHorizontally": "Centra orizzontalmente",
|
||||||
"distributeHorizontally": "Distribuisci orizzontalmente",
|
"distributeHorizontally": "Distribuisci orizzontalmente",
|
||||||
"distributeVertically": "Distribuisci verticalmente"
|
"distributeVertically": "Distribuisci verticalmente",
|
||||||
|
"viewMode": "Modalità visualizzazione"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Svuota la tela",
|
"clearReset": "Svuota la tela",
|
||||||
@@ -200,25 +201,25 @@
|
|||||||
"title": "Errore"
|
"title": "Errore"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "Leggi il nostro blog",
|
||||||
"click": "",
|
"click": "click",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "Freccia curva",
|
||||||
"curvedLine": "",
|
"curvedLine": "Linea curva",
|
||||||
"documentation": "",
|
"documentation": "Documentazione",
|
||||||
"drag": "",
|
"drag": "trascina",
|
||||||
"editor": "",
|
"editor": "Editor",
|
||||||
"github": "",
|
"github": "Trovato un problema? Segnalalo",
|
||||||
"howto": "",
|
"howto": "Segui le nostre guide",
|
||||||
"or": "",
|
"or": "oppure",
|
||||||
"preventBinding": "",
|
"preventBinding": "Impedisci legame della freccia",
|
||||||
"shapes": "",
|
"shapes": "Forme",
|
||||||
"shortcuts": "",
|
"shortcuts": "Scorciatoie da tastiera",
|
||||||
"textFinish": "",
|
"textFinish": "Termina la modifica (testo)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Aggiungi nuova riga (testo)",
|
||||||
"title": "",
|
"title": "Guida",
|
||||||
"view": "",
|
"view": "Vista",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "Adatta zoom per mostrare tutti gli elementi",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "Zoom alla selezione"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
|
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "",
|
"gridMode": "",
|
||||||
"addToLibrary": "ライブラリに追加",
|
"addToLibrary": "ライブラリに追加",
|
||||||
"removeFromLibrary": "ライブラリから削除",
|
"removeFromLibrary": "ライブラリから削除",
|
||||||
"libraryLoadingMessage": "ライブラリを読み込み中...",
|
"libraryLoadingMessage": "ライブラリを読み込み中…",
|
||||||
"libraries": "",
|
"libraries": "",
|
||||||
"loadingScene": "シーンを読み込み中...",
|
"loadingScene": "シーンを読み込み中…",
|
||||||
"align": "整列",
|
"align": "整列",
|
||||||
"alignTop": "上揃え",
|
"alignTop": "上揃え",
|
||||||
"alignBottom": "下揃え",
|
"alignBottom": "下揃え",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "縦方向に中央揃え",
|
"centerVertically": "縦方向に中央揃え",
|
||||||
"centerHorizontally": "横方向に中央揃え",
|
"centerHorizontally": "横方向に中央揃え",
|
||||||
"distributeHorizontally": "",
|
"distributeHorizontally": "",
|
||||||
"distributeVertically": ""
|
"distributeVertically": "",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "キャンバスのリセット",
|
"clearReset": "キャンバスのリセット",
|
||||||
|
|||||||
@@ -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": "수직으로 중앙 정렬",
|
"centerVertically": "수직으로 중앙 정렬",
|
||||||
"centerHorizontally": "수평으로 중앙 정렬",
|
"centerHorizontally": "수평으로 중앙 정렬",
|
||||||
"distributeHorizontally": "수평으로 분배",
|
"distributeHorizontally": "수평으로 분배",
|
||||||
"distributeVertically": "수직으로 분배"
|
"distributeVertically": "수직으로 분배",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "캔버스 초기화",
|
"clearReset": "캔버스 초기화",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "",
|
"gridMode": "",
|
||||||
"addToLibrary": "မှတ်တမ်းတင်",
|
"addToLibrary": "မှတ်တမ်းတင်",
|
||||||
"removeFromLibrary": "မှတ်တမ်းမှထုတ်",
|
"removeFromLibrary": "မှတ်တမ်းမှထုတ်",
|
||||||
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်...",
|
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်…",
|
||||||
"libraries": "စာကြည့်တိုက်တွင်ရှာဖွေပါ",
|
"libraries": "စာကြည့်တိုက်တွင်ရှာဖွေပါ",
|
||||||
"loadingScene": "မြင်ကွင်းဖော်နေသည်...",
|
"loadingScene": "မြင်ကွင်းဖော်နေသည်…",
|
||||||
"align": "ချိန်ညှိ",
|
"align": "ချိန်ညှိ",
|
||||||
"alignTop": "ထိပ်ညှိ",
|
"alignTop": "ထိပ်ညှိ",
|
||||||
"alignBottom": "အခြေညှိ",
|
"alignBottom": "အခြေညှိ",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "ဒေါင်လိုက်အလယ်ညှိ",
|
"centerVertically": "ဒေါင်လိုက်အလယ်ညှိ",
|
||||||
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
|
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
|
||||||
"distributeHorizontally": "အလျားလိုက်",
|
"distributeHorizontally": "အလျားလိုက်",
|
||||||
"distributeVertically": "ထောင်လိုက်"
|
"distributeVertically": "ထောင်လိုက်",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "ကားချပ်ရှင်းလင်း",
|
"clearReset": "ကားချပ်ရှင်းလင်း",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Rutevisning",
|
"gridMode": "Rutevisning",
|
||||||
"addToLibrary": "Legg til i bibliotek",
|
"addToLibrary": "Legg til i bibliotek",
|
||||||
"removeFromLibrary": "Fjern fra bibliotek",
|
"removeFromLibrary": "Fjern fra bibliotek",
|
||||||
"libraryLoadingMessage": "Laster bibliotek...",
|
"libraryLoadingMessage": "Laster bibliotek…",
|
||||||
"libraries": "Bla gjennom biblioteker",
|
"libraries": "Bla gjennom biblioteker",
|
||||||
"loadingScene": "Laster inn scene...",
|
"loadingScene": "Laster inn scene…",
|
||||||
"align": "Juster",
|
"align": "Juster",
|
||||||
"alignTop": "Juster øverst",
|
"alignTop": "Juster øverst",
|
||||||
"alignBottom": "Juster nederst",
|
"alignBottom": "Juster nederst",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Midtstill vertikalt",
|
"centerVertically": "Midtstill vertikalt",
|
||||||
"centerHorizontally": "Midtstill horisontalt",
|
"centerHorizontally": "Midtstill horisontalt",
|
||||||
"distributeHorizontally": "Distribuer horisontalt",
|
"distributeHorizontally": "Distribuer horisontalt",
|
||||||
"distributeVertically": "Distribuer vertikalt"
|
"distributeVertically": "Distribuer vertikalt",
|
||||||
|
"viewMode": "Visningsmodus"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen",
|
"clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen",
|
||||||
|
|||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Verticaal Centreren",
|
"centerVertically": "Verticaal Centreren",
|
||||||
"centerHorizontally": "Horizontaal Centreren",
|
"centerHorizontally": "Horizontaal Centreren",
|
||||||
"distributeHorizontally": "Horizontaal verspreiden",
|
"distributeHorizontally": "Horizontaal verspreiden",
|
||||||
"distributeVertically": "Verticaal distribueren"
|
"distributeVertically": "Verticaal distribueren",
|
||||||
|
"viewMode": "Weergavemodus"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Canvas opnieuw instellen",
|
"clearReset": "Canvas opnieuw instellen",
|
||||||
|
|||||||
+10
-9
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Rutevisning",
|
"gridMode": "Rutevisning",
|
||||||
"addToLibrary": "Legg til i bibliotek",
|
"addToLibrary": "Legg til i bibliotek",
|
||||||
"removeFromLibrary": "Fjern frå bibliotek",
|
"removeFromLibrary": "Fjern frå bibliotek",
|
||||||
"libraryLoadingMessage": "Laster bibliotek...",
|
"libraryLoadingMessage": "Laster bibliotek…",
|
||||||
"libraries": "Blad gjennom bibliotek",
|
"libraries": "Blad gjennom bibliotek",
|
||||||
"loadingScene": "Laster scene...",
|
"loadingScene": "Laster scene…",
|
||||||
"align": "Juster",
|
"align": "Juster",
|
||||||
"alignTop": "Juster til topp",
|
"alignTop": "Juster til topp",
|
||||||
"alignBottom": "Juster til botn",
|
"alignBottom": "Juster til botn",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Midtstill vertikalt",
|
"centerVertically": "Midtstill vertikalt",
|
||||||
"centerHorizontally": "Midtstill horisontalt",
|
"centerHorizontally": "Midtstill horisontalt",
|
||||||
"distributeHorizontally": "Sprei horisontalt",
|
"distributeHorizontally": "Sprei horisontalt",
|
||||||
"distributeVertically": "Sprei vertikalt"
|
"distributeVertically": "Sprei vertikalt",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Tilbakestill lerretet",
|
"clearReset": "Tilbakestill lerretet",
|
||||||
@@ -201,22 +202,22 @@
|
|||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "",
|
||||||
"click": "",
|
"click": "klikk",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "",
|
||||||
"curvedLine": "",
|
"curvedLine": "",
|
||||||
"documentation": "",
|
"documentation": "",
|
||||||
"drag": "",
|
"drag": "",
|
||||||
"editor": "",
|
"editor": "Redigering",
|
||||||
"github": "",
|
"github": "",
|
||||||
"howto": "",
|
"howto": "",
|
||||||
"or": "",
|
"or": "eller",
|
||||||
"preventBinding": "",
|
"preventBinding": "",
|
||||||
"shapes": "",
|
"shapes": "Formar",
|
||||||
"shortcuts": "",
|
"shortcuts": "",
|
||||||
"textFinish": "",
|
"textFinish": "",
|
||||||
"textNewLine": "",
|
"textNewLine": "",
|
||||||
"title": "",
|
"title": "Hjelp",
|
||||||
"view": "",
|
"view": "Vising",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": ""
|
||||||
},
|
},
|
||||||
|
|||||||
+21
-20
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "ਲੇਟਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
|
"centerVertically": "ਲੇਟਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
|
||||||
"centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
|
"centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
|
||||||
"distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
|
"distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
|
||||||
"distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ"
|
"distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ",
|
"clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ",
|
||||||
@@ -200,25 +201,25 @@
|
|||||||
"title": "ਗਲਤੀ"
|
"title": "ਗਲਤੀ"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "ਸਾਡਾ ਬਲੌਗ ਪੜ੍ਹੋ",
|
||||||
"click": "",
|
"click": "ਕਲਿੱਕ",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "ਵਿੰਗਾ ਤੀਰ",
|
||||||
"curvedLine": "",
|
"curvedLine": "ਵਿੰਗੀ ਲਕੀਰ",
|
||||||
"documentation": "",
|
"documentation": "ਕਾਗਜ਼ਾਤ",
|
||||||
"drag": "",
|
"drag": "ਘਸੀਟੋ",
|
||||||
"editor": "",
|
"editor": "ਸੋਧਕ",
|
||||||
"github": "",
|
"github": "ਕੋਈ ਸਮੱਸਿਆ ਲੱਭੀ? ਜਮ੍ਹਾਂ ਕਰਵਾਓ",
|
||||||
"howto": "",
|
"howto": "ਸਾਡੀਆਂ ਗਾਈਡਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ",
|
||||||
"or": "",
|
"or": "ਜਾਂ",
|
||||||
"preventBinding": "",
|
"preventBinding": "ਤੀਰ ਬੱਝਣਾ ਰੋਕੋ",
|
||||||
"shapes": "",
|
"shapes": "ਆਕ੍ਰਿਤੀਆਂ",
|
||||||
"shortcuts": "",
|
"shortcuts": "ਕੀਬੋਰਡ ਸ਼ਾਰਟਕੱਟ",
|
||||||
"textFinish": "",
|
"textFinish": "ਸੋਧ ਮੁਕੰਮਲ ਕਰੋ (ਪਾਠ)",
|
||||||
"textNewLine": "",
|
"textNewLine": "ਨਵੀਂ ਪੰਕਤੀ ਜੋੜੋ (ਪਾਠ)",
|
||||||
"title": "",
|
"title": "ਮਦਦ",
|
||||||
"view": "",
|
"view": "ਦਿੱਖ",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "ਸਾਰੇ ਐਲੀਮੈਂਟਾਂ ਨੂੰ ਫਿੱਟ ਕਰਨ ਲਈ ਜ਼ੂਮ ਕਰੋ",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।"
|
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।"
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
{
|
{
|
||||||
"ar-SA": 90,
|
"ar-SA": 89,
|
||||||
"bg-BG": 90,
|
"bg-BG": 93,
|
||||||
"ca-ES": 90,
|
"ca-ES": 89,
|
||||||
"de-DE": 100,
|
"de-DE": 100,
|
||||||
"el-GR": 100,
|
"el-GR": 100,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-ES": 100,
|
"es-ES": 100,
|
||||||
"fa-IR": 90,
|
"fa-IR": 98,
|
||||||
"fi-FI": 100,
|
"fi-FI": 100,
|
||||||
"fr-FR": 100,
|
"fr-FR": 100,
|
||||||
"he-IL": 90,
|
"he-IL": 89,
|
||||||
"hi-IN": 90,
|
"hi-IN": 89,
|
||||||
"hu-HU": 90,
|
"hu-HU": 89,
|
||||||
"id-ID": 91,
|
"id-ID": 100,
|
||||||
"it-IT": 91,
|
"it-IT": 100,
|
||||||
"ja-JP": 81,
|
"ja-JP": 81,
|
||||||
"ko-KR": 90,
|
"kab-KAB": 97,
|
||||||
|
"ko-KR": 89,
|
||||||
"my-MM": 83,
|
"my-MM": 83,
|
||||||
"nb-NO": 100,
|
"nb-NO": 100,
|
||||||
"nl-NL": 100,
|
"nl-NL": 100,
|
||||||
"nn-NO": 90,
|
"nn-NO": 92,
|
||||||
"pa-IN": 91,
|
"pa-IN": 99,
|
||||||
"pl-PL": 90,
|
"pl-PL": 90,
|
||||||
"pt-BR": 100,
|
"pt-BR": 100,
|
||||||
"pt-PT": 100,
|
"pt-PT": 99,
|
||||||
"ro-RO": 100,
|
"ro-RO": 100,
|
||||||
"ru-RU": 91,
|
"ru-RU": 99,
|
||||||
"sk-SK": 91,
|
"sk-SK": 100,
|
||||||
"sv-SE": 100,
|
"sv-SE": 100,
|
||||||
"tr-TR": 90,
|
"tr-TR": 89,
|
||||||
"uk-UA": 100,
|
"uk-UA": 99,
|
||||||
"zh-CN": 90,
|
"zh-CN": 99,
|
||||||
"zh-TW": 100
|
"zh-TW": 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Wyśrodkuj w pionie",
|
"centerVertically": "Wyśrodkuj w pionie",
|
||||||
"centerHorizontally": "Wyśrodkuj w poziomie",
|
"centerHorizontally": "Wyśrodkuj w poziomie",
|
||||||
"distributeHorizontally": "Rozłóż poziomo",
|
"distributeHorizontally": "Rozłóż poziomo",
|
||||||
"distributeVertically": "Rozłóż pionowo"
|
"distributeVertically": "Rozłóż pionowo",
|
||||||
|
"viewMode": "Tryb widoku"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Wyczyść dokument i zresetuj kolor dokumentu",
|
"clearReset": "Wyczyść dokument i zresetuj kolor dokumentu",
|
||||||
|
|||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centralizar verticalmente",
|
"centerVertically": "Centralizar verticalmente",
|
||||||
"centerHorizontally": "Centralizar horizontalmente",
|
"centerHorizontally": "Centralizar horizontalmente",
|
||||||
"distributeHorizontally": "Distribuir horizontalmente",
|
"distributeHorizontally": "Distribuir horizontalmente",
|
||||||
"distributeVertically": "Distribuir verticalmente"
|
"distributeVertically": "Distribuir verticalmente",
|
||||||
|
"viewMode": "Modo de visualização"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
|
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Modo grade",
|
"gridMode": "Modo grade",
|
||||||
"addToLibrary": "Adicionar à biblioteca",
|
"addToLibrary": "Adicionar à biblioteca",
|
||||||
"removeFromLibrary": "Remover da biblioteca",
|
"removeFromLibrary": "Remover da biblioteca",
|
||||||
"libraryLoadingMessage": "Carregando biblioteca...",
|
"libraryLoadingMessage": "Carregando biblioteca…",
|
||||||
"libraries": "Procurar bibliotecas",
|
"libraries": "Procurar bibliotecas",
|
||||||
"loadingScene": "Carregando cena...",
|
"loadingScene": "Carregando cena…",
|
||||||
"align": "Alinhamento",
|
"align": "Alinhamento",
|
||||||
"alignTop": "Alinhar ao topo",
|
"alignTop": "Alinhar ao topo",
|
||||||
"alignBottom": "Alinhar ao fundo",
|
"alignBottom": "Alinhar ao fundo",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centralizar verticalmente",
|
"centerVertically": "Centralizar verticalmente",
|
||||||
"centerHorizontally": "Centralizar horizontalmente",
|
"centerHorizontally": "Centralizar horizontalmente",
|
||||||
"distributeHorizontally": "Distribuir horizontalmente",
|
"distributeHorizontally": "Distribuir horizontalmente",
|
||||||
"distributeVertically": "Distribuir verticalmente"
|
"distributeVertically": "Distribuir verticalmente",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
|
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Mod grilă",
|
"gridMode": "Mod grilă",
|
||||||
"addToLibrary": "Adăugare la bibliotecă",
|
"addToLibrary": "Adăugare la bibliotecă",
|
||||||
"removeFromLibrary": "Eliminare din bibliotecă",
|
"removeFromLibrary": "Eliminare din bibliotecă",
|
||||||
"libraryLoadingMessage": "Se încarcă biblioteca...",
|
"libraryLoadingMessage": "Se încarcă biblioteca…",
|
||||||
"libraries": "Răsfoiește bibliotecile",
|
"libraries": "Răsfoiește bibliotecile",
|
||||||
"loadingScene": "Se încarcă scena...",
|
"loadingScene": "Se încarcă scena…",
|
||||||
"align": "Aliniere",
|
"align": "Aliniere",
|
||||||
"alignTop": "Aliniere sus",
|
"alignTop": "Aliniere sus",
|
||||||
"alignBottom": "Aliniere jos",
|
"alignBottom": "Aliniere jos",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrare verticală",
|
"centerVertically": "Centrare verticală",
|
||||||
"centerHorizontally": "Centrare orizontală",
|
"centerHorizontally": "Centrare orizontală",
|
||||||
"distributeHorizontally": "Distribuie orizontal",
|
"distributeHorizontally": "Distribuie orizontal",
|
||||||
"distributeVertically": "Distribuie vertical"
|
"distributeVertically": "Distribuie vertical",
|
||||||
|
"viewMode": "Mod de vizualizare"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Resetare pânză",
|
"clearReset": "Resetare pânză",
|
||||||
|
|||||||
+23
-22
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Сетка",
|
"gridMode": "Сетка",
|
||||||
"addToLibrary": "Добавить в библиотеку",
|
"addToLibrary": "Добавить в библиотеку",
|
||||||
"removeFromLibrary": "Удалить из библиотеки",
|
"removeFromLibrary": "Удалить из библиотеки",
|
||||||
"libraryLoadingMessage": "Загрузка библиотеки...",
|
"libraryLoadingMessage": "Загрузка библиотеки…",
|
||||||
"libraries": "Просмотреть библиотеки",
|
"libraries": "Просмотреть библиотеки",
|
||||||
"loadingScene": "Загрузка сцены...",
|
"loadingScene": "Загрузка сцены…",
|
||||||
"align": "Выровнять",
|
"align": "Выровнять",
|
||||||
"alignTop": "Выровнять по верхнему краю",
|
"alignTop": "Выровнять по верхнему краю",
|
||||||
"alignBottom": "Выровнять по нижнему краю",
|
"alignBottom": "Выровнять по нижнему краю",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Центрировать по вертикали",
|
"centerVertically": "Центрировать по вертикали",
|
||||||
"centerHorizontally": "Центрировать по горизонтали",
|
"centerHorizontally": "Центрировать по горизонтали",
|
||||||
"distributeHorizontally": "Распределить по горизонтали",
|
"distributeHorizontally": "Распределить по горизонтали",
|
||||||
"distributeVertically": "Распределить по вертикали"
|
"distributeVertically": "Распределить по вертикали",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Очистить холст и сбросить цвет фона",
|
"clearReset": "Очистить холст и сбросить цвет фона",
|
||||||
@@ -200,25 +201,25 @@
|
|||||||
"title": "Ошибка"
|
"title": "Ошибка"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "Прочитайте наш блог",
|
||||||
"click": "",
|
"click": "нажать",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "Изогнутая стрелка",
|
||||||
"curvedLine": "",
|
"curvedLine": "Изогнутая линия",
|
||||||
"documentation": "",
|
"documentation": "Документация",
|
||||||
"drag": "",
|
"drag": "перетащить",
|
||||||
"editor": "",
|
"editor": "Редактор",
|
||||||
"github": "",
|
"github": "Нашли проблему? Отправьте",
|
||||||
"howto": "",
|
"howto": "Следуйте нашим инструкциям",
|
||||||
"or": "",
|
"or": "или",
|
||||||
"preventBinding": "",
|
"preventBinding": "Предотвращать привязку стрелок",
|
||||||
"shapes": "",
|
"shapes": "Фигуры",
|
||||||
"shortcuts": "",
|
"shortcuts": "Горячие клавиши",
|
||||||
"textFinish": "",
|
"textFinish": "Закончить редактирование (текст)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Добавить новую строку (текст)",
|
||||||
"title": "",
|
"title": "Помощь",
|
||||||
"view": "",
|
"view": "Просмотр",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "Отмастштабировать, чтобы поместились все элементы",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "Увеличить до выделенного"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним."
|
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним."
|
||||||
|
|||||||
+23
-22
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Režim mriežky",
|
"gridMode": "Režim mriežky",
|
||||||
"addToLibrary": "Pridať do knižnice",
|
"addToLibrary": "Pridať do knižnice",
|
||||||
"removeFromLibrary": "Odstrániť z knižnice",
|
"removeFromLibrary": "Odstrániť z knižnice",
|
||||||
"libraryLoadingMessage": "Načítavanie knižnice...",
|
"libraryLoadingMessage": "Načítavanie knižnice…",
|
||||||
"libraries": "Prehliadať knižnice",
|
"libraries": "Prehliadať knižnice",
|
||||||
"loadingScene": "Načítavanie scény...",
|
"loadingScene": "Načítavanie scény…",
|
||||||
"align": "Zarovnanie",
|
"align": "Zarovnanie",
|
||||||
"alignTop": "Zarovnať nahor",
|
"alignTop": "Zarovnať nahor",
|
||||||
"alignBottom": "Zarovnať nadol",
|
"alignBottom": "Zarovnať nadol",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Zarovnať zvislo na stred",
|
"centerVertically": "Zarovnať zvislo na stred",
|
||||||
"centerHorizontally": "Zarovnať vodorovne na stred",
|
"centerHorizontally": "Zarovnať vodorovne na stred",
|
||||||
"distributeHorizontally": "Rozmiestniť vodorovne",
|
"distributeHorizontally": "Rozmiestniť vodorovne",
|
||||||
"distributeVertically": "Rozmiestniť zvisle"
|
"distributeVertically": "Rozmiestniť zvisle",
|
||||||
|
"viewMode": "Režim zobrazenia"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Obnoviť plátno",
|
"clearReset": "Obnoviť plátno",
|
||||||
@@ -200,25 +201,25 @@
|
|||||||
"title": "Chyba"
|
"title": "Chyba"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "Prečítajte si náš blog",
|
||||||
"click": "",
|
"click": "kliknutie",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "Zakrivená šípka",
|
||||||
"curvedLine": "",
|
"curvedLine": "Zakrivená čiara",
|
||||||
"documentation": "",
|
"documentation": "Dokumentácia",
|
||||||
"drag": "",
|
"drag": "potiahnutie",
|
||||||
"editor": "",
|
"editor": "Editovanie",
|
||||||
"github": "",
|
"github": "Objavili ste problém? Nahláste ho",
|
||||||
"howto": "",
|
"howto": "Postupujte podľa naších návodov",
|
||||||
"or": "",
|
"or": "alebo",
|
||||||
"preventBinding": "",
|
"preventBinding": "Zakázať pripájanie šípky",
|
||||||
"shapes": "",
|
"shapes": "Tvary",
|
||||||
"shortcuts": "",
|
"shortcuts": "Klávesové skratky",
|
||||||
"textFinish": "",
|
"textFinish": "Ukončenie editovania (text)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Vložiť nový riadok (text)",
|
||||||
"title": "",
|
"title": "Pomocník",
|
||||||
"view": "",
|
"view": "Zobrazenie",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "Priblížiť aby boli zahrnuté všetky prvky",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "Priblížiť na výber"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať."
|
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať."
|
||||||
|
|||||||
@@ -80,9 +80,9 @@
|
|||||||
"gridMode": "Rutnätsläge",
|
"gridMode": "Rutnätsläge",
|
||||||
"addToLibrary": "Lägg till i biblioteket",
|
"addToLibrary": "Lägg till i biblioteket",
|
||||||
"removeFromLibrary": "Ta bort från bibliotek",
|
"removeFromLibrary": "Ta bort från bibliotek",
|
||||||
"libraryLoadingMessage": "Laddar bibliotek...",
|
"libraryLoadingMessage": "Laddar bibliotek…",
|
||||||
"libraries": "Bläddra i bibliotek",
|
"libraries": "Bläddra i bibliotek",
|
||||||
"loadingScene": "Laddar scen...",
|
"loadingScene": "Laddar scen…",
|
||||||
"align": "Justera",
|
"align": "Justera",
|
||||||
"alignTop": "Justera överkant",
|
"alignTop": "Justera överkant",
|
||||||
"alignBottom": "Justera underkant",
|
"alignBottom": "Justera underkant",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrera vertikalt",
|
"centerVertically": "Centrera vertikalt",
|
||||||
"centerHorizontally": "Centrera horisontellt",
|
"centerHorizontally": "Centrera horisontellt",
|
||||||
"distributeHorizontally": "Fördela horisontellt",
|
"distributeHorizontally": "Fördela horisontellt",
|
||||||
"distributeVertically": "Fördela vertikalt"
|
"distributeVertically": "Fördela vertikalt",
|
||||||
|
"viewMode": "Visningsläge"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Återställ canvasen",
|
"clearReset": "Återställ canvasen",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user