Compare commits

..

4 Commits

Author SHA1 Message Date
Aakansha Doshi 09daff487a Merge remote-tracking branch 'origin/master' into aakansha-sm 2021-01-21 13:15:32 +05:30
Aakansha Doshi 17330c4c03 fix 2021-01-20 21:21:50 +05:30
Aakansha Doshi 2a4ad6fc41 upload sourcemaps to cdn 2021-01-20 21:20:28 +05:30
Aakansha Doshi ec6999554a build(webpack): attach sourcemaps so it helps in debugging 2021-01-20 01:58:45 +05:30
130 changed files with 1422 additions and 2418 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ on:
- master - master
jobs: jobs:
build: build-docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+2 -2
View File
@@ -13,10 +13,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Setup Node.js 14.x - name: Setup Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 12.x
- name: Install dependencies - name: Install dependencies
run: | run: |
+3 -5
View File
@@ -1,11 +1,9 @@
name: Cancel previous runs name: Cancel
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
+9 -3
View File
@@ -1,6 +1,10 @@
name: Lint name: Lint
on: push on:
push:
branches:
- master
pull_request:
jobs: jobs:
lint: lint:
@@ -9,10 +13,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Setup Node.js 14.x - name: Setup Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 12.x
- name: Install and lint - name: Install and lint
run: | run: |
@@ -20,3 +24,5 @@ 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
+4 -4
View File
@@ -14,18 +14,18 @@ jobs:
with: with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }} token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
- name: Setup Node.js 14.x - name: Setup Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 12.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 'Excalidraw Bot' git config --global user.name 'Kostas Bariotis'
git config --global user.email 'bot@excalidraw.com' git config --global user.email 'konmpar@gmail.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
-1
View File
@@ -10,7 +10,6 @@ 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:
+2 -3
View File
@@ -8,14 +8,13 @@ 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 14.x - name: Setup Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 12.x
- name: Install and build - name: Install and build
run: | run: |
+9 -3
View File
@@ -1,6 +1,10 @@
name: Tests name: Tests
on: push on:
push:
branches:
- master
pull_request:
jobs: jobs:
test: test:
@@ -9,12 +13,14 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Setup Node.js 14.x - name: Setup Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 12.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
+7
View File
@@ -21,5 +21,12 @@
} }
] ]
} }
],
"redirects": [
{
"source": "/([^.]+)",
"destination": "/",
"statusCode": 301
}
] ]
} }
+136 -97
View File
@@ -4,9 +4,9 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": { "@apidevtools/json-schema-ref-parser": {
"version": "9.0.7", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz",
"integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", "integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==",
"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.14", "version": "0.6.13",
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.14.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.13.tgz",
"integrity": "sha512-ZQKuiJ+fzr4tULgWoXbW+AZVTGsejOkSrlQ+zx78WiGKIubpFJLklnP3S0oYr/1nHzr4vaKuM4G8IL1Wv/+MpQ==", "integrity": "sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ==",
"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.2", "version": "0.16.1",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.1.tgz",
"integrity": "sha512-68TlDL0yh3kF8PiCzI8m8RWd/bf/xCLUsdz1NZ2Dwea0sp6e2WAhu0sem1GfhwuEwL+Ns4jCdX7qbe/OQlkVEA==", "integrity": "sha512-7juD7D/kaxNti/xa5G+ZGJJs+bdJUWOW0MlNBtXwiG+TjMh69EDmwJnQmmc9h/32QVvXt1qo1OGWOoMMpF/2Gg==",
"requires": { "requires": {
"@firebase/auth-types": "0.10.1" "@firebase/auth-types": "0.10.1"
} }
@@ -1368,9 +1368,9 @@
} }
}, },
"@firebase/database": { "@firebase/database": {
"version": "0.9.1", "version": "0.8.3",
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.9.1.tgz", "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.3.tgz",
"integrity": "sha512-JdxgNvniSZiAx+lrdAQxkCZOTv+UfdmhRm9JA4RTs4XOpvwzmRtJTAIGBn+9CWXUAkWkjt5CYHLmYysD7NGj6g==", "integrity": "sha512-i29rr3kcPltIkA8La9M1lgsSxx9bfu5lCQ0T+tbJptZ3UpqpcL1NzCcZa24cJjiLgq3HQNPyLvUvCtcPSFDlRg==",
"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.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.2.tgz",
"integrity": "sha512-chSOvJyVoS7HmH7YOyqQP66wMwmsYNo2nPbFkrmQM/fRGXntNxXD1Greu1uts2hNyNeDLNrFHW5y7PlE3LAbwQ==", "integrity": "sha512-8yUdBLLr6UhE+IjPR+fxLBD0bDnEqF9GalohfURZeLQPaL3b+LtqqGCLvvXC4MKT0lJAHOV8J9LA6rHj8vI0/Q==",
"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,86 +2663,125 @@
} }
}, },
"@sentry/browser": { "@sentry/browser": {
"version": "6.0.3", "version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.30.0.tgz",
"integrity": "sha512-Ukxh83Twql4UmUgds9wPWllE62NG71cYvm5AM6daTojvM8wFR2jh7G6GiA0WYfgMb2fw6SlbevB2xb6RDG5DzQ==", "integrity": "sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw==",
"requires": { "requires": {
"@sentry/core": "6.0.3", "@sentry/core": "5.30.0",
"@sentry/types": "6.0.3", "@sentry/types": "5.30.0",
"@sentry/utils": "6.0.3", "@sentry/utils": "5.30.0",
"tslib": "^1.9.3"
}
},
"@sentry/core": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.3.tgz",
"integrity": "sha512-UykB/4/98y2DkNvwTiL2ofFPuK3KDHc7rIRNsdj6dg6D+Cf7FRexgmWUUkZrpC/y+QBj0TPqkcFDcZAuQDa3Ag==",
"requires": {
"@sentry/hub": "6.0.3",
"@sentry/minimal": "6.0.3",
"@sentry/types": "6.0.3",
"@sentry/utils": "6.0.3",
"tslib": "^1.9.3"
}
},
"@sentry/hub": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.3.tgz",
"integrity": "sha512-BfV32tE09rjTWM9W0kk8gzxUC2k1h57Z5dNWJ35na79+LguNNtCcI6fHlFQ3PkJca6ITYof9FI8iQHUfsHFZnw==",
"requires": {
"@sentry/types": "6.0.3",
"@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": "6.0.3", "version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-266aBQbk9AGedhG2dzXshWbn23LYLElXqlI74DLku48UrU2v7TGKdyik/8/nfOfquCoRSp0GFGYHbItwU124XQ==" "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
}, },
"@sentry/utils": { "@sentry/utils": {
"version": "6.0.3", "version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
"integrity": "sha512-lvuBFvZHYs1zYwI8dkC8Z8ryb0aYnwPFUl1rbZiMwJpYI2Dgl1jpqqZWv9luux2rSRYOMid74uGedV708rvEgA==", "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
"requires": { "requires": {
"@sentry/types": "6.0.3", "@sentry/types": "5.30.0",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
} }
} }
}, },
"@sentry/minimal": { "@sentry/core": {
"version": "6.0.3", "version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz",
"integrity": "sha512-YsW+nw0SMyyb7UQdjZeKlZjxbGsJFpXNLh9iIp6fHKnoLTTv17YPm2ej9sOikDsQuVotaPg/xn/Qt5wySGHIxw==", "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==",
"requires": { "requires": {
"@sentry/hub": "6.0.3", "@sentry/hub": "5.30.0",
"@sentry/types": "6.0.3", "@sentry/minimal": "5.30.0",
"@sentry/types": "5.30.0",
"@sentry/utils": "5.30.0",
"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": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz",
"integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==",
"requires": {
"@sentry/types": "5.30.0",
"@sentry/utils": "5.30.0",
"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/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" "tslib": "^1.9.3"
} }
}, },
"@sentry/minimal": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz",
"integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==",
"requires": {
"@sentry/hub": "5.30.0",
"@sentry/types": "5.30.0",
"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": "6.0.3", "version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-266aBQbk9AGedhG2dzXshWbn23LYLElXqlI74DLku48UrU2v7TGKdyik/8/nfOfquCoRSp0GFGYHbItwU124XQ==" "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
}, },
"@sentry/utils": { "@sentry/utils": {
"version": "6.0.3", "version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
"integrity": "sha512-lvuBFvZHYs1zYwI8dkC8Z8ryb0aYnwPFUl1rbZiMwJpYI2Dgl1jpqqZWv9luux2rSRYOMid74uGedV708rvEgA==", "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
"requires": { "requires": {
"@sentry/types": "6.0.3", "@sentry/types": "5.30.0",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
@@ -5116,10 +5155,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-fs-access": { "browser-nativefs": {
"version": "0.13.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.13.0.tgz", "resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.12.0.tgz",
"integrity": "sha512-qP8zFVhRQThxYgBXdlFHbzIrWb1us0G5kL2ZL0vW4BO5llKE4qBAcQsQrw4KN+6vjw8sKeWaGWJtzijfRT4N0Q==" "integrity": "sha512-ZCHJcQI6bBm9YjB+6wMT1nWg+/mnWnz7r3gJ8sx7RjgLtWROFq+BuD12cAncD6y45MIbUqFM8eMKXoHXOxSFxA=="
}, },
"browser-process-hrtime": { "browser-process-hrtime": {
"version": "1.0.0", "version": "1.0.0",
@@ -7948,9 +7987,9 @@
} }
}, },
"eslint-config-prettier": { "eslint-config-prettier": {
"version": "7.2.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz",
"integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==", "integrity": "sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==",
"dev": true "dev": true
}, },
"eslint-config-react-app": { "eslint-config-react-app": {
@@ -9028,16 +9067,16 @@
} }
}, },
"firebase": { "firebase": {
"version": "8.2.5", "version": "8.2.3",
"resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.5.tgz", "resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.3.tgz",
"integrity": "sha512-x9KUJR8PvqLUNzNKWHjAnO7rJVgK546G0F+vjlJTNl+J/8oFTdWh8X4PvYda0z0XM68A2Y9xPGf3blz5qHCn0A==", "integrity": "sha512-WdbcGSiLxiW/kGZT+EyqD9z3Md7kR35+k9qMjDn/twiIrm6Hh7Qi/Z69cqxhKW6+4uK5ghXIF28CjK67OyD9Qw==",
"requires": { "requires": {
"@firebase/analytics": "0.6.2", "@firebase/analytics": "0.6.2",
"@firebase/app": "0.6.14", "@firebase/app": "0.6.13",
"@firebase/app-types": "0.6.1", "@firebase/app-types": "0.6.1",
"@firebase/auth": "0.16.2", "@firebase/auth": "0.16.1",
"@firebase/database": "0.9.1", "@firebase/database": "0.8.3",
"@firebase/firestore": "2.1.4", "@firebase/firestore": "2.1.2",
"@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",
@@ -9049,9 +9088,9 @@
} }
}, },
"firebase-tools": { "firebase-tools": {
"version": "9.2.2", "version": "9.2.1",
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.2.tgz", "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.1.tgz",
"integrity": "sha512-AFjf7S9NjEM+u8ZByJEKASxRG1g+LLg/A0CrzA3V91P92MN+8cyrCigEs7mCdtFknLaShrCgzROyo/OEwd4xdA==", "integrity": "sha512-sD4wfB5hs/8IKXV6AJOmkpvXf/St7gVc9QeW4Qz21PG7CkirgRf6FqcYkPKtBcro4wfj48dihnYx/IO1+XPTGg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@google-cloud/pubsub": "^2.7.0", "@google-cloud/pubsub": "^2.7.0",
@@ -11523,9 +11562,9 @@
}, },
"dependencies": { "dependencies": {
"ip-regex": { "ip-regex": {
"version": "4.3.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz",
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", "integrity": "sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==",
"dev": true "dev": true
} }
} }
@@ -14262,9 +14301,9 @@
} }
}, },
"localforage": { "localforage": {
"version": "1.9.0", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.8.1.tgz",
"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==", "integrity": "sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ==",
"requires": { "requires": {
"lie": "3.1.1" "lie": "3.1.1"
} }
+7 -7
View File
@@ -19,17 +19,17 @@
] ]
}, },
"dependencies": { "dependencies": {
"@sentry/browser": "6.0.3", "@sentry/browser": "5.30.0",
"@sentry/integrations": "6.0.3", "@sentry/integrations": "5.30.0",
"@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-fs-access": "0.13.0", "browser-nativefs": "0.12.0",
"clsx": "1.1.1", "clsx": "1.1.1",
"firebase": "8.2.5", "firebase": "8.2.3",
"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.2.0", "eslint-config-prettier": "7.1.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.2.2", "firebase-tools": "9.2.1",
"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-fs-access)/)" "node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-nativefs)/)"
], ],
"resetMocks": false "resetMocks": false
}, },
+1 -4
View File
@@ -18,7 +18,6 @@ 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",
@@ -41,7 +40,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": "🇪🇸",
@@ -54,7 +53,6 @@ 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": "🇳🇴",
@@ -90,7 +88,6 @@ 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",
+1
View File
@@ -17,5 +17,6 @@ export const actionAddToLibrary = register({
}); });
return false; return false;
}, },
contextMenuOrder: 6,
contextItemLabel: "labels.addToLibrary", contextItemLabel: "labels.addToLibrary",
}); });
+3 -2
View File
@@ -3,7 +3,6 @@ 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";
@@ -52,7 +51,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 || GRID_SIZE, gridSize: appState.gridSize,
shouldAddWatermark: appState.shouldAddWatermark, shouldAddWatermark: appState.shouldAddWatermark,
showStats: appState.showStats, showStats: appState.showStats,
pasteDialog: appState.pasteDialog, pasteDialog: appState.pasteDialog,
@@ -76,6 +75,8 @@ 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) => {
-114
View File
@@ -1,114 +0,0 @@
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,
});
+1
View File
@@ -136,6 +136,7 @@ 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
+2
View File
@@ -125,6 +125,7 @@ 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),
@@ -173,6 +174,7 @@ 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 -10
View File
@@ -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 { isWindows, KEYS } from "../keys"; import { 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: (event) => keyTest: testUndo(false),
event[KEYS.CTRL_OR_CMD] &&
event.key.toLowerCase() === KEYS.Z &&
!event.shiftKey,
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
@@ -84,11 +84,7 @@ 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: (event) => keyTest: testUndo(true),
(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"
+1 -1
View File
@@ -74,7 +74,7 @@ export const actionShortcuts = register({
return { return {
appState: { appState: {
...appState, ...appState,
showHelpDialog: !appState.showHelpDialog, showHelpDialog: true,
}, },
commitToHistory: false, commitToHistory: false,
}; };
+2
View File
@@ -34,6 +34,7 @@ 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({
@@ -73,4 +74,5 @@ 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,
}); });
-21
View File
@@ -1,21 +0,0 @@
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,
});
-16
View File
@@ -1,16 +0,0 @@
import { register } from "./register";
export const actionToggleStats = register({
name: "stats",
perform(elements, appState) {
return {
appState: {
...appState,
showStats: !this.checked!(appState),
},
commitToHistory: false,
};
},
checked: (appState) => appState.showStats,
contextItemLabel: "stats.title",
});
-22
View File
@@ -1,22 +0,0 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
import { trackEvent } from "../analytics";
export const actionToggleViewMode = register({
name: "viewMode",
perform(elements, appState) {
trackEvent("view", "mode", "view");
return {
appState: {
...appState,
viewModeEnabled: !this.checked!(appState),
selectedElementIds: {},
},
commitToHistory: false,
};
},
checked: (appState) => appState.viewModeEnabled,
contextItemLabel: "labels.viewMode",
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R,
});
-22
View File
@@ -1,22 +0,0 @@
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,
});
-12
View File
@@ -65,15 +65,3 @@ 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";
+38 -17
View File
@@ -3,15 +3,14 @@ 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, ExcalidrawProps } from "../types"; import { AppState } from "../types";
import { t } from "../i18n";
// This is the <App> component, but for now we don't care about anything but its import { ShortcutName } from "./shortcuts";
// `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"];
@@ -19,14 +18,13 @@ 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) {
@@ -39,7 +37,6 @@ 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) {
@@ -66,12 +63,6 @@ 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(
@@ -79,7 +70,6 @@ export class ActionManager implements ActionsManagerInterface {
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.getAppState(), this.getAppState(),
null, null,
this.app,
), ),
); );
return true; return true;
@@ -91,11 +81,43 @@ 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
@@ -110,7 +132,6 @@ export class ActionManager implements ActionsManagerInterface {
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.getAppState(), this.getAppState(),
formState, formState,
this.app,
), ),
); );
}; };
+3 -5
View File
@@ -9,7 +9,7 @@ export type ShortcutName =
| "copyStyles" | "copyStyles"
| "pasteStyles" | "pasteStyles"
| "selectAll" | "selectAll"
| "deleteSelectedElements" | "delete"
| "duplicateSelection" | "duplicateSelection"
| "sendBackward" | "sendBackward"
| "bringForward" | "bringForward"
@@ -22,8 +22,7 @@ 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")],
@@ -32,7 +31,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")],
deleteSelectedElements: [getShortcutKey("Del")], delete: [getShortcutKey("Del")],
duplicateSelection: [ duplicateSelection: [
getShortcutKey("CtrlOrCmd+D"), getShortcutKey("CtrlOrCmd+D"),
getShortcutKey(`Alt+${t("helpDialog.drag")}`), getShortcutKey(`Alt+${t("helpDialog.drag")}`),
@@ -57,7 +56,6 @@ 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) => {
+5 -12
View File
@@ -16,18 +16,12 @@ 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"
@@ -35,9 +29,6 @@ export type ActionName =
| "copyStyles" | "copyStyles"
| "selectAll" | "selectAll"
| "pasteStyles" | "pasteStyles"
| "gridMode"
| "zenMode"
| "stats"
| "changeStrokeColor" | "changeStrokeColor"
| "changeBackgroundColor" | "changeBackgroundColor"
| "changeFillStyle" | "changeFillStyle"
@@ -84,8 +75,7 @@ export type ActionName =
| "alignVerticallyCentered" | "alignVerticallyCentered"
| "alignHorizontallyCentered" | "alignHorizontallyCentered"
| "distributeHorizontally" | "distributeHorizontally"
| "distributeVertically" | "distributeVertically";
| "viewMode";
export interface Action { export interface Action {
name: ActionName; name: ActionName;
@@ -103,16 +93,19 @@ 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;
} }
+4 -9
View File
@@ -3,10 +3,9 @@ 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, NormalizedZoomValue } from "./types"; import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
import { getDateTime } from "./utils"; import { getDateTime } from "./utils";
export const getDefaultAppState = (): Omit< export const getDefaultAppState = (): Omit<
@@ -42,7 +41,7 @@ export const getDefaultAppState = (): Omit<
exportBackground: true, exportBackground: true,
exportEmbedScene: false, exportEmbedScene: false,
fileHandle: null, fileHandle: null,
gridSize: GRID_SIZE, gridSize: null,
height: window.innerHeight, height: window.innerHeight,
isBindingEnabled: true, isBindingEnabled: true,
isLibraryOpen: false, isLibraryOpen: false,
@@ -57,14 +56,13 @@ export const getDefaultAppState = (): Omit<
previousSelectedElementIds: {}, previousSelectedElementIds: {},
resizingElement: null, resizingElement: null,
scrolledOutside: false, scrolledOutside: false,
scrollX: 0, scrollX: 0 as FlooredNumber,
scrollY: 0, scrollY: 0 as FlooredNumber,
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,
@@ -74,7 +72,6 @@ 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,
}; };
}; };
@@ -122,7 +119,6 @@ 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 },
@@ -155,7 +151,6 @@ 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">(
+200 -302
View File
@@ -2,31 +2,8 @@ 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 { import { actionDeleteSelected, actionFinalize } from "../actions";
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";
@@ -41,6 +18,7 @@ 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,
@@ -48,15 +26,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 { loadFromBlob } from "../data"; import { exportCanvas, 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";
@@ -149,6 +127,7 @@ 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";
@@ -176,12 +155,10 @@ import {
viewportCoordsToSceneCoords, viewportCoordsToSceneCoords,
withBatchedUpdates, withBatchedUpdates,
} from "../utils"; } from "../utils";
import { isMobile } from "../is-mobile"; import ContextMenu from "./ContextMenu";
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();
@@ -271,7 +248,6 @@ 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;
}; };
@@ -298,7 +274,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetLeft, offsetLeft,
offsetTop, offsetTop,
excalidrawRef, excalidrawRef,
viewModeEnabled = false,
} = props; } = props;
this.state = { this.state = {
...defaultAppState, ...defaultAppState,
@@ -306,7 +281,6 @@ 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 =
@@ -324,7 +298,6 @@ 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);
@@ -339,7 +312,6 @@ 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);
@@ -347,62 +319,6 @@ 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,
@@ -410,19 +326,20 @@ 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={clsx("excalidraw", { className="excalidraw"
"excalidraw--view-mode": viewModeEnabled,
})}
ref={this.excalidrawContainerRef} ref={this.excalidrawContainerRef}
style={{ style={{
width: canvasDOMWidth, width: canvasDOMWidth,
@@ -452,11 +369,9 @@ 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}
@@ -468,7 +383,28 @@ class App extends React.Component<ExcalidrawProps, AppState> {
clearToast={this.clearToast} clearToast={this.clearToast}
/> />
)} )}
<main>{this.renderCanvas()}</main> <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>
); );
} }
@@ -508,13 +444,6 @@ 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,
@@ -524,7 +453,6 @@ 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) {
@@ -707,6 +635,7 @@ 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
@@ -773,16 +702,25 @@ 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,
@@ -799,18 +737,6 @@ 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) {
@@ -833,17 +759,6 @@ 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");
@@ -991,6 +906,44 @@ 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;
} }
@@ -1088,12 +1041,7 @@ 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( const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize);
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) => {
@@ -1198,18 +1146,24 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}; };
toggleZenMode = () => { toggleZenMode = () => {
this.actionManager.executeAction(actionToggleZenMode); this.setState({
zenModeEnabled: !this.state.zenModeEnabled,
});
}; };
toggleGridMode = () => { toggleGridMode = () => {
this.actionManager.executeAction(actionToggleGridMode); this.setState({
gridSize: this.state.gridSize ? null : GRID_SIZE,
});
}; };
toggleStats = () => { toggleStats = () => {
if (!this.state.showStats) { if (!this.state.showStats) {
trackEvent("dialog", "stats"); trackEvent("dialog", "stats");
} }
this.actionManager.executeAction(actionToggleStats); this.setState({
showStats: !this.state.showStats,
});
}; };
setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => { setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
@@ -1299,25 +1253,34 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
} }
if (this.actionManager.handleKeyDown(event)) { if (!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z) {
return; this.toggleZenMode();
} }
if (this.state.viewModeEnabled) { if (event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE) {
return; this.toggleGridMode();
} }
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
this.setState({ isBindingEnabled: false }); this.setState({ isBindingEnabled: false });
} }
if (event.code === CODES.C && event.altKey && event.shiftKey) {
this.copyToClipboardAsPng();
event.preventDefault();
return;
}
if (this.actionManager.handleKeyDown(event)) {
return;
}
if (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.showGrid && (this.state.gridSize &&
(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
@@ -1815,8 +1778,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: scrollX + deltaX / zoom.value, scrollX: normalizeScroll(scrollX + deltaX / zoom.value),
scrollY: scrollY + deltaY / zoom.value, scrollY: normalizeScroll(scrollY + deltaY / zoom.value),
zoom: getNewZoom( zoom: getNewZoom(
getNormalizedZoom(initialScale * scaleFactor), getNormalizedZoom(initialScale * scaleFactor),
zoom, zoom,
@@ -1859,7 +1822,6 @@ 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) {
@@ -2118,16 +2080,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
lastPointerUp = onPointerUp; lastPointerUp = onPointerUp;
if (!this.state.viewModeEnabled) { window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); window.addEventListener(EVENT.POINTER_UP, onPointerUp);
window.addEventListener(EVENT.POINTER_UP, onPointerUp); window.addEventListener(EVENT.KEYDOWN, onKeyDown);
window.addEventListener(EVENT.KEYDOWN, onKeyDown); window.addEventListener(EVENT.KEYUP, onKeyUp);
window.addEventListener(EVENT.KEYUP, onKeyUp); pointerDownState.eventListeners.onMove = onPointerMove;
pointerDownState.eventListeners.onMove = onPointerMove; pointerDownState.eventListeners.onUp = onPointerUp;
pointerDownState.eventListeners.onUp = onPointerUp; pointerDownState.eventListeners.onKeyUp = onKeyUp;
pointerDownState.eventListeners.onKeyUp = onKeyUp; pointerDownState.eventListeners.onKeyDown = onKeyDown;
pointerDownState.eventListeners.onKeyDown = onKeyDown;
}
}; };
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = ( private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
@@ -2177,8 +2137,7 @@ 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;
@@ -2231,8 +2190,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
this.setState({ this.setState({
scrollX: this.state.scrollX - deltaX / this.state.zoom.value, scrollX: normalizeScroll(
scrollY: this.state.scrollY - deltaY / this.state.zoom.value, this.state.scrollX - deltaX / this.state.zoom.value,
),
scrollY: normalizeScroll(
this.state.scrollY - deltaY / this.state.zoom.value,
),
}); });
}); });
const teardown = withBatchedUpdates( const teardown = withBatchedUpdates(
@@ -2289,12 +2252,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return { return {
origin, origin,
originInGrid: tupleToCoors( originInGrid: tupleToCoors(
getGridPoint( getGridPoint(origin.x, origin.y, this.state.gridSize),
origin.x,
origin.y,
this.state.showGrid,
this.state.gridSize,
),
), ),
scrollbars: isOverScrollBars( scrollbars: isOverScrollBars(
currentScrollBars, currentScrollBars,
@@ -2650,8 +2608,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,
elementType === "draw" ? false : this.state.showGrid, elementType === "draw" ? null : this.state.gridSize,
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.
@@ -2713,7 +2670,6 @@ 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({
@@ -2803,7 +2759,6 @@ 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,
); );
@@ -2876,7 +2831,6 @@ 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,
); );
@@ -2929,7 +2883,6 @@ 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, {
@@ -3056,7 +3009,9 @@ 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: this.state.scrollX - dx / this.state.zoom.value, scrollX: normalizeScroll(
this.state.scrollX - dx / this.state.zoom.value,
),
}); });
pointerDownState.lastCoords.x = x; pointerDownState.lastCoords.x = x;
return true; return true;
@@ -3066,7 +3021,9 @@ 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: this.state.scrollY - dy / this.state.zoom.value, scrollY: normalizeScroll(
this.state.scrollY - dy / this.state.zoom.value,
),
}); });
pointerDownState.lastCoords.y = y; pointerDownState.lastCoords.y = y;
return true; return true;
@@ -3586,7 +3543,6 @@ 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(
@@ -3625,7 +3581,6 @@ 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 (
@@ -3661,87 +3616,52 @@ 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: [
_isMobile && navigator.clipboard && {
navigator.clipboard && { shortcutName: "paste",
name: "paste", label: t("labels.paste"),
perform: (elements, appStates) => { action: () => this.pasteFromClipboard(null),
this.pasteFromClipboard(null); },
return {
commitToHistory: false,
};
},
contextItemLabel: "labels.paste",
},
_isMobile && navigator.clipboard && separator,
probablySupportsClipboardBlob && probablySupportsClipboardBlob &&
elements.length > 0 && elements.length > 0 && {
actionCopyAsPng, shortcutName: "copyAsPng",
label: t("labels.copyAsPng"),
action: this.copyToClipboardAsPng,
},
probablySupportsClipboardWriteText && probablySupportsClipboardWriteText &&
elements.length > 0 && elements.length > 0 && {
actionCopyAsSvg, shortcutName: "copyAsSvg",
((probablySupportsClipboardBlob && elements.length > 0) || label: t("labels.copyAsSvg"),
(probablySupportsClipboardWriteText && elements.length > 0)) && action: this.copyToClipboardAsSvg,
separator, },
actionSelectAll, ...this.actionManager.getContextMenuItems((action) =>
separator, CANVAS_ONLY_ACTIONS.includes(action.name),
actionToggleGridMode, ),
actionToggleZenMode, {
typeof this.props.viewModeEnabled === "undefined" && checked: this.state.gridSize !== null,
actionToggleViewMode, shortcutName: "gridMode",
actionToggleStats, label: t("labels.gridMode"),
action: this.toggleGridMode,
},
{
checked: this.state.zenModeEnabled,
shortcutName: "zenMode",
label: t("buttons.zenMode"),
action: this.toggleZenMode,
},
{
checked: this.state.showStats,
shortcutName: "stats",
label: t("stats.title"),
action: this.toggleStats,
},
], ],
top: clientY, top: clientY,
left: clientX, left: clientX,
actionManager: this.actionManager,
appState: this.state,
}); });
return; return;
} }
@@ -3750,55 +3670,39 @@ 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, {
_isMobile && navigator.clipboard && actionCopy, shortcutName: "cut",
_isMobile && label: t("labels.cut"),
navigator.clipboard && { action: this.cutAll,
name: "paste", },
perform: (elements, appStates) => { navigator.clipboard && {
this.pasteFromClipboard(null); shortcutName: "copy",
return { label: t("labels.copy"),
commitToHistory: false, action: this.copyAll,
}; },
}, navigator.clipboard && {
contextItemLabel: "labels.paste", shortcutName: "paste",
}, label: t("labels.paste"),
_isMobile && separator, action: () => this.pasteFromClipboard(null),
...options, },
separator, probablySupportsClipboardBlob && {
actionCopyStyles, shortcutName: "copyAsPng",
actionPasteStyles, label: t("labels.copyAsPng"),
separator, action: this.copyToClipboardAsPng,
maybeGroupAction && actionGroup, },
maybeUngroupAction && actionUngroup, probablySupportsClipboardWriteText && {
(maybeGroupAction || maybeUngroupAction) && separator, shortcutName: "copyAsSvg",
actionAddToLibrary, label: t("labels.copyAsSvg"),
separator, action: this.copyToClipboardAsSvg,
actionSendBackward, },
actionBringForward, ...this.actionManager.getContextMenuItems(
actionSendToBack, (action) => !CANVAS_ONLY_ACTIONS.includes(action.name),
actionBringToFront, ),
separator,
actionDuplicateSelection,
actionDeleteSelected,
], ],
top: clientY, top: clientY,
left: clientX, left: clientX,
actionManager: this.actionManager,
appState: this.state,
}); });
}; };
@@ -3829,15 +3733,9 @@ 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(newZoom), getNormalizedZoom(zoom.value - delta / 100),
zoom, zoom,
{ left: offsetLeft, top: offsetTop }, { left: offsetLeft, top: offsetTop },
{ {
@@ -3860,14 +3758,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: scrollX - (deltaY || deltaX) / zoom.value, scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom.value),
})); }));
return; return;
} }
this.setState(({ zoom, scrollX, scrollY }) => ({ this.setState(({ zoom, scrollX, scrollY }) => ({
scrollX: scrollX - deltaX / zoom.value, scrollX: normalizeScroll(scrollX - deltaX / zoom.value),
scrollY: scrollY - deltaY / zoom.value, scrollY: normalizeScroll(scrollY - deltaY / zoom.value),
})); }));
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.Avatar { .Avatar {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.CollabButton.is-collaborating { .CollabButton.is-collaborating {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.color-picker { .color-picker {
+2 -8
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.context-menu { .context-menu {
@@ -9,10 +9,9 @@
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.5rem 0; padding: 0.25rem 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 {
@@ -89,9 +88,4 @@
} }
} }
} }
.context-menu-option-separator {
border: none;
border-top: 1px solid $oc-gray-5;
}
} }
+27 -50
View File
@@ -2,36 +2,28 @@ 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";
export type ContextMenuOption = "separator" | Action; type ContextMenuOption = {
checked?: boolean;
shortcutName: ShortcutName;
label: string;
action(): void;
};
type ContextMenuProps = { type Props = {
options: ContextMenuOption[]; options: ContextMenuOption[];
onCloseRequest?(): void; onCloseRequest?(): void;
top: number; top: number;
left: number; left: number;
actionManager: ActionManager;
appState: Readonly<AppState>;
}; };
const ContextMenu = ({ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
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");
@@ -51,34 +43,23 @@ const ContextMenu = ({
className="context-menu" className="context-menu"
onContextMenu={(event) => event.preventDefault()} onContextMenu={(event) => event.preventDefault()}
> >
{options.map((option, idx) => { {options.map(({ action, checked, shortcutName, label }, idx) => (
if (option === "separator") { <li data-testid={shortcutName} key={idx} onClick={onCloseRequest}>
return <hr key={idx} className="context-menu-option-separator" />; <button
} className={`context-menu-option
${shortcutName === "delete" ? "dangerous" : ""}
const actionName = option.name; ${checked ? "checkmark" : ""}`}
const label = option.contextItemLabel onClick={action}
? t(option.contextItemLabel) >
: ""; <div className="context-menu-option__label">{label}</div>
return ( <kbd className="context-menu-option__shortcut">
<li key={idx} data-testid={actionName} onClick={onCloseRequest}> {shortcutName
<button ? getShortcutFromShortcutName(shortcutName)
className={clsx("context-menu-option", { : ""}
dangerous: actionName === "deleteSelectedElements", </kbd>
checkmark: option.checked?.(appState), </button>
})} </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>
@@ -97,10 +78,8 @@ const getContextMenuNode = (): HTMLDivElement => {
type ContextMenuParams = { type ContextMenuParams = {
options: (ContextMenuOption | false | null | undefined)[]; options: (ContextMenuOption | false | null | undefined)[];
top: ContextMenuProps["top"]; top: number;
left: ContextMenuProps["left"]; left: number;
actionManager: ContextMenuProps["actionManager"];
appState: Readonly<AppState>;
}; };
const handleClose = () => { const handleClose = () => {
@@ -122,8 +101,6 @@ 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 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.Dialog { .Dialog {
+10 -3
View File
@@ -1,6 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { useEffect } from "react"; import React, { useCallback, useEffect, useState } 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";
@@ -9,6 +8,14 @@ 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;
@@ -17,7 +24,7 @@ export const Dialog = (props: {
title: React.ReactNode; title: React.ReactNode;
autofocus?: boolean; autofocus?: boolean;
}) => { }) => {
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>(); const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
useEffect(() => { useEffect(() => {
if (!islandNode) { if (!islandNode) {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.ExportDialog__preview { .ExportDialog__preview {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.HelpDialog h3 { .HelpDialog h3 {
+2 -13
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { isDarwin, isWindows } from "../keys"; import { isDarwin } 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,10 +227,6 @@ 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>
@@ -332,14 +328,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
/> />
<Shortcut <Shortcut
label={t("buttons.redo")} label={t("buttons.redo")}
shortcuts={ shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]}
isWindows
? [
getShortcutKey("CtrlOrCmd+Y"),
getShortcutKey("CtrlOrCmd+Shift+Z"),
]
: [getShortcutKey("CtrlOrCmd+Shift+Z")]
}
/> />
<Shortcut <Shortcut
label={t("labels.group")} label={t("labels.group")}
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
// 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 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.picker-container { .picker-container {
+43 -79
View File
@@ -61,7 +61,6 @@ 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 = (
@@ -300,7 +299,6 @@ const LayerUI = ({
isCollaborating, isCollaborating,
onExportToBackend, onExportToBackend,
renderCustomFooter, renderCustomFooter,
viewModeEnabled,
}: LayerUIProps) => { }: LayerUIProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -360,28 +358,6 @@ 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"
@@ -472,42 +448,38 @@ const LayerUI = ({
gap={4} gap={4}
className={clsx({ "disable-pointerEvents": zenModeEnabled })} className={clsx({ "disable-pointerEvents": zenModeEnabled })}
> >
{viewModeEnabled {renderCanvasActions()}
? renderViewModeCanvasActions()
: renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col> </Stack.Col>
{!viewModeEnabled && ( <Section heading="shapes">
<Section heading="shapes"> {(heading) => (
{(heading) => ( <Stack.Col gap={4} align="start">
<Stack.Col gap={4} align="start"> <Stack.Row gap={1}>
<Stack.Row gap={1}> <Island
<Island padding={1}
padding={1} className={clsx({ "zen-mode": zenModeEnabled })}
className={clsx({ "zen-mode": zenModeEnabled })} >
> <HintViewer appState={appState} elements={elements} />
<HintViewer appState={appState} elements={elements} /> {heading}
{heading} <Stack.Row gap={1}>
<Stack.Row gap={1}> <ShapesSwitcher
<ShapesSwitcher elementType={appState.elementType}
elementType={appState.elementType} setAppState={setAppState}
setAppState={setAppState} isLibraryOpen={appState.isLibraryOpen}
isLibraryOpen={appState.isLibraryOpen} />
/> </Stack.Row>
</Stack.Row> </Island>
</Island> <LockIcon
<LockIcon zenModeEnabled={zenModeEnabled}
zenModeEnabled={zenModeEnabled} checked={appState.elementLocked}
checked={appState.elementLocked} onChange={onLockToggle}
onChange={onLockToggle} title={t("toolBar.lock")}
title={t("toolBar.lock")} />
/> </Stack.Row>
</Stack.Row> {libraryMenu}
{libraryMenu} </Stack.Col>
</Stack.Col> )}
)} </Section>
</Section>
)}
<UserList <UserList
className={clsx("zen-mode-transition", { className={clsx("zen-mode-transition", {
"transition-right": zenModeEnabled, "transition-right": zenModeEnabled,
@@ -552,20 +524,6 @@ 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
@@ -641,19 +599,25 @@ const LayerUI = ({
canvas={canvas} canvas={canvas}
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter} renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
/> />
</> </>
) : ( ) : (
<div <div className="layer-ui__wrapper">
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>
); );
+116 -160
View File
@@ -29,7 +29,6 @@ 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 = ({
@@ -44,164 +43,121 @@ export const MobileMenu = ({
canvas, canvas,
isCollaborating, isCollaborating,
renderCustomFooter, renderCustomFooter,
viewModeEnabled, }: MobileMenuProps) => (
}: MobileMenuProps) => { <>
const renderFixedSideContainer = () => { <FixedSideContainer side="top">
return ( <Section heading="shapes">
<FixedSideContainer side="top"> {(heading) => (
<Section heading="shapes"> <Stack.Col gap={4} align="center">
{(heading) => ( <Stack.Row gap={1}>
<Stack.Col gap={4} align="center"> <Island padding={1}>
<Stack.Row gap={1}> {heading}
<Island padding={1}> <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> checked={appState.elementLocked}
<LockIcon onChange={onLockToggle}
checked={appState.elementLocked} title={t("toolBar.lock")}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</Section>
<HintViewer appState={appState} elements={elements} />
</FixedSideContainer>
);
};
const renderAppToolbar = () => {
if (viewModeEnabled) {
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
</div>
);
}
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
{actionManager.renderAction("deleteSelectedElements")}
</div>
);
};
const renderCanvasActions = () => {
if (viewModeEnabled) {
return (
<>
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{exportButton}
</>
);
}
return (
<>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{exportButton}
{actionManager.renderAction("clearCanvas")}
{onCollabButtonClick && (
<CollabButton
isCollaborating={isCollaborating}
collaboratorCount={appState.collaborators.size}
onClick={onCollabButtonClick}
/>
)}
{
<BackgroundPickerAndDarkModeToggle
actionManager={actionManager}
appState={appState}
setAppState={setAppState}
/>
}
</>
);
};
return (
<>
{!viewModeEnabled && renderFixedSideContainer()}
<div
className="App-bottom-bar"
style={{
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> </Stack.Row>
) : null} {libraryMenu}
<footer className="App-toolbar"> </Stack.Col>
{renderAppToolbar()} )}
{appState.scrolledOutside && !appState.openMenu && ( </Section>
<button <HintViewer appState={appState} elements={elements} />
className="scroll-back-to-content" </FixedSideContainer>
onClick={() => { <div
setAppState({ className="App-bottom-bar"
...calculateScrollCenter(elements, appState, canvas), style={{
}); marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}} marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
> marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
{t("buttons.scrollBackToContent")} }}
</button> >
<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)}
<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" &&
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",
)} )}
</footer> {actionManager.renderAction("deleteSelectedElements")}
</Island> </div>
</div> {appState.scrolledOutside && !appState.openMenu && (
</> <button
); className="scroll-back-to-content"
}; onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>
</>
);
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.Modal { .Modal {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.PasteChartDialog { .PasteChartDialog {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.Stats { .Stats {
position: fixed; position: fixed;
-18
View File
@@ -24,7 +24,6 @@ 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;
@@ -47,12 +46,6 @@ 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;
} }
@@ -163,17 +156,6 @@ 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 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables.scss";
.excalidraw { .excalidraw {
.TextInput { .TextInput {
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.Toast { .Toast {
+1 -1
View File
@@ -1,5 +1,5 @@
@import "open-color/open-color.scss"; @import "open-color/open-color.scss";
@import "../css/variables.module"; @import "../css/variables";
.excalidraw { .excalidraw {
.ToolIcon { .ToolIcon {
+10 -2
View File
@@ -1,4 +1,4 @@
@import "../css/variables.module"; @import "../css/_variables";
.excalidraw { .excalidraw {
.Tooltip { .Tooltip {
position: relative; position: relative;
@@ -48,7 +48,15 @@
} }
} }
.Tooltip:hover .Tooltip__label { // the following 3 rules ensure that the tooltip doesn't show (nor affect
// the cursor) when you drag over when you draw on canvas, but at the same
// time it still works when clicking on the link/shield
body:active & .Tooltip:not(:hover) {
pointer-events: none;
}
body:not(:active) & .Tooltip:hover .Tooltip__label {
visibility: visible; visibility: visible;
} }
+1 -3
View File
@@ -90,6 +90,4 @@ export const TAP_TWICE_TIMEOUT = 300;
export const TOUCH_CTX_MENU_TIMEOUT = 500; export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const TITLE_TIMEOUT = 10000; export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000; export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000; export const VERSION_TIMEOUT = 15000;
export const ZOOM_STEP = 0.1;
-42
View File
@@ -1,42 +0,0 @@
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,
};
};
@@ -2,7 +2,3 @@
// 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);
}
+3 -10
View File
@@ -1,4 +1,4 @@
@import "./variables.module"; @import "./_variables";
@import "./theme"; @import "./theme";
.excalidraw { .excalidraw {
@@ -282,7 +282,7 @@
pointer-events: none !important; pointer-events: none !important;
} }
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * { .App-menu_top > * {
pointer-events: all; pointer-events: all;
} }
@@ -323,7 +323,7 @@
} }
} }
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * { .App-menu_bottom > * {
pointer-events: all; pointer-events: all;
} }
@@ -492,13 +492,6 @@
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,
+1 -1
View File
@@ -1,4 +1,4 @@
import { fileSave } from "browser-fs-access"; import { fileSave } from "browser-nativefs";
import { import {
copyCanvasToClipboardAsPng, copyCanvasToClipboardAsPng,
copyTextToSystemClipboard, copyTextToSystemClipboard,
+1 -1
View File
@@ -1,4 +1,4 @@
import { fileOpen, fileSave } from "browser-fs-access"; import { fileOpen, fileSave } from "browser-nativefs";
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";
+3 -13
View File
@@ -102,7 +102,6 @@ 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);
@@ -199,7 +198,6 @@ export class LinearElementEditor {
element, element,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
appState.showGrid,
appState.gridSize, appState.gridSize,
), ),
], ],
@@ -284,8 +282,7 @@ export class LinearElementEditor {
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
editingLinearElement: LinearElementEditor, editingLinearElement: LinearElementEditor,
isGridOn: boolean, gridSize: number | null,
gridSize: number,
): LinearElementEditor { ): LinearElementEditor {
const { elementId, lastUncommittedPoint } = editingLinearElement; const { elementId, lastUncommittedPoint } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId);
@@ -307,7 +304,6 @@ export class LinearElementEditor {
element, element,
scenePointerX - editingLinearElement.pointerOffset.x, scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y, scenePointerY - editingLinearElement.pointerOffset.y,
isGridOn,
gridSize, gridSize,
); );
@@ -402,15 +398,9 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
isGridOn: boolean, gridSize: number | null,
gridSize: number,
): Point { ): Point {
const pointerOnGrid = getGridPoint( const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
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;
+3 -4
View File
@@ -7,8 +7,7 @@ export const showSelectedShapeActions = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
) => ) =>
Boolean( Boolean(
!appState.viewModeEnabled && appState.editingElement ||
(appState.editingElement || getSelectedElements(elements, appState).length ||
getSelectedElements(elements, appState).length || appState.elementType !== "selection",
appState.elementType !== "selection"),
); );
+35 -97
View File
@@ -6,11 +6,10 @@ 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 { Collaborator, Gesture } from "../../types"; import { AppState, Collaborator, Gesture } from "../../types";
import { resolvablePromise, withBatchedUpdates } from "../../utils"; import { resolvablePromise, withBatchedUpdates } from "../../utils";
import { import {
INITIAL_SCENE_UPDATE_TIMEOUT, INITIAL_SCENE_UPDATE_TIMEOUT,
@@ -32,7 +31,6 @@ 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;
@@ -58,21 +56,17 @@ type ReconciledElements = readonly ExcalidrawElement[] & {
}; };
interface Props { interface Props {
excalidrawAPI: ExcalidrawImperativeAPI; children: (collab: CollabAPI) => React.ReactNode;
// NOTE not type-safe because the refObject may in fact not be initialized
// with ExcalidrawImperativeAPI yet
excalidrawRef: React.MutableRefObject<ExcalidrawImperativeAPI>;
} }
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>();
@@ -86,7 +80,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
activeRoomLink: "", activeRoomLink: "",
}; };
this.portal = new Portal(this); this.portal = new Portal(this);
this.excalidrawAPI = props.excalidrawAPI; this.excalidrawRef = props.excalidrawRef;
} }
componentDidMount() { componentDidMount() {
@@ -148,7 +142,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
saveCollabRoomToFirebase = async ( saveCollabRoomToFirebase = async (
syncableElements: ExcalidrawElement[] = getSyncableElements( syncableElements: ExcalidrawElement[] = getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(), this.excalidrawRef.current!.getSceneElementsIncludingDeleted(),
), ),
) => { ) => {
try { try {
@@ -160,13 +154,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.excalidrawAPI.getSceneElements(); const elements = this.excalidrawRef.current!.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.excalidrawAPI.history.clear(); this.excalidrawRef.current!.history.clear();
this.excalidrawAPI.updateScene({ this.excalidrawRef.current!.updateScene({
elements, elements,
commitToHistory: true, commitToHistory: true,
}); });
@@ -181,7 +175,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private destroySocketClient = () => { private destroySocketClient = () => {
this.collaborators = new Map(); this.collaborators = new Map();
this.excalidrawAPI.updateScene({ this.excalidrawRef.current!.updateScene({
collaborators: this.collaborators, collaborators: this.collaborators,
}); });
this.setState({ this.setState({
@@ -271,7 +265,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.excalidrawAPI.updateScene({ this.excalidrawRef.current!.updateScene({
collaborators, collaborators,
}); });
break; break;
@@ -306,55 +300,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private reconcileElements = ( private reconcileElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
): ReconciledElements => { ): ReconciledElements => {
const currentElements = this.getSceneElementsIncludingDeleted(); const newElements = this.portal.reconcileElements(elements);
// 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!
@@ -373,10 +319,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}: { init?: boolean; initFromSnapshot?: boolean } = {}, }: { init?: boolean; initFromSnapshot?: boolean } = {},
) => { ) => {
if (init || initFromSnapshot) { if (init || initFromSnapshot) {
this.excalidrawAPI.setScrollToCenter(elements); this.excalidrawRef.current!.setScrollToCenter(elements);
} }
this.excalidrawAPI.updateScene({ this.excalidrawRef.current!.updateScene({
elements, elements,
commitToHistory: !!init, commitToHistory: !!init,
}); });
@@ -385,7 +331,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.excalidrawAPI.history.clear(); this.excalidrawRef.current!.history.clear();
}; };
setCollaborators(sockets: string[]) { setCollaborators(sockets: string[]) {
@@ -401,7 +347,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
} }
} }
this.collaborators = collaborators; this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators }); this.excalidrawRef.current!.updateScene({ collaborators });
}); });
} }
@@ -414,7 +360,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}; };
public getSceneElementsIncludingDeleted = () => { public getSceneElementsIncludingDeleted = () => {
return this.excalidrawAPI.getSceneElementsIncludingDeleted(); return this.excalidrawRef.current!.getSceneElementsIncludingDeleted();
}; };
onPointerUpdate = (payload: { onPointerUpdate = (payload: {
@@ -427,7 +373,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.portal.broadcastMouseLocation(payload); this.portal.broadcastMouseLocation(payload);
}; };
broadcastElements = (elements: readonly ExcalidrawElement[]) => { broadcastElements = (
elements: readonly ExcalidrawElement[],
state: AppState,
) => {
this.excalidrawAppState = state;
if ( if (
getSceneVersion(elements) > getSceneVersion(elements) >
this.getLastBroadcastedOrReceivedSceneVersion() this.getLastBroadcastedOrReceivedSceneVersion()
@@ -446,7 +396,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.portal.broadcastScene( this.portal.broadcastScene(
SCENE.UPDATE, SCENE.UPDATE,
getSyncableElements( getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(), this.excalidrawRef.current!.getSceneElementsIncludingDeleted(),
), ),
true, true,
); );
@@ -475,23 +425,8 @@ 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 (
@@ -515,11 +450,14 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
onClose={() => this.setState({ errorMessage: "" })} onClose={() => this.setState({ errorMessage: "" })}
/> />
)} )}
<CollabContextProvider {children({
value={{ isCollaborating: this.state.isCollaborating,
api: this.getContextValue(), username: this.state.username,
}} onPointerUpdate: this.onPointerUpdate,
/> initializeSocketClient: this.initializeSocketClient,
onCollabButtonClick: this.onCollabButtonClick,
broadcastElements: this.broadcastElements,
})}
</> </>
); );
} }
+71 -12
View File
@@ -6,20 +6,23 @@ import {
import CollabWrapper from "./CollabWrapper"; import CollabWrapper from "./CollabWrapper";
import { getSyncableElements } from "../../packages/excalidraw/index"; import {
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 {
collab: CollabWrapper; app: 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(collab: CollabWrapper) { constructor(app: CollabWrapper) {
this.collab = collab; this.app = app;
} }
open(socket: SocketIOClient.Socket, id: string, key: string) { open(socket: SocketIOClient.Socket, id: string, key: string) {
@@ -27,7 +30,7 @@ class Portal {
this.roomId = id; this.roomId = id;
this.roomKey = key; this.roomKey = key;
// Initialize socket listeners // Initialize socket listeners (moving from App)
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);
@@ -36,12 +39,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.collab.getSceneElementsIncludingDeleted()), getSyncableElements(this.app.getSceneElementsIncludingDeleted()),
/* syncAll */ true, /* syncAll */ true,
); );
}); });
this.socket.on("room-user-change", (clients: string[]) => { this.socket.on("room-user-change", (clients: string[]) => {
this.collab.setCollaborators(clients); this.app.setCollaborators(clients);
}); });
} }
@@ -122,10 +125,10 @@ class Portal {
data as SocketUpdateData, data as SocketUpdateData,
); );
if (syncAll && this.collab.state.isCollaborating) { if (syncAll && this.app.state.isCollaborating) {
await Promise.all([ await Promise.all([
broadcastPromise, broadcastPromise,
this.collab.saveCollabRoomToFirebase(syncableElements), this.app.saveCollabRoomToFirebase(syncableElements),
]); ]);
} else { } else {
await broadcastPromise; await broadcastPromise;
@@ -143,9 +146,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: this.collab.excalidrawAPI.getAppState() selectedElementIds:
.selectedElementIds, this.app.excalidrawAppState?.selectedElementIds || {},
username: this.collab.state.username, username: this.app.state.username,
}, },
}; };
return this._broadcastSocketData( return this._broadcastSocketData(
@@ -154,6 +157,62 @@ 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 -1
View File
@@ -1,4 +1,4 @@
@import "../../css/variables.module"; @import "../../css/_variables";
.excalidraw { .excalidraw {
.RoomDialog-linkContainer { .RoomDialog-linkContainer {
+43 -42
View File
@@ -1,7 +1,6 @@
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,
@@ -18,13 +17,12 @@ 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 } from "../types"; import { AppState, ExcalidrawAPIRefValue } from "../types";
import { import {
debounce, debounce,
getVersion, getVersion,
@@ -32,11 +30,7 @@ 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, { import CollabWrapper, { CollabAPI } from "./collab/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";
@@ -55,6 +49,15 @@ 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);
@@ -188,7 +191,7 @@ const initializeScene = async (opts: {
return null; return null;
}; };
function ExcalidrawWrapper() { function ExcalidrawWrapper(props: { collab: CollabAPI }) {
// dimensions // dimensions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -223,40 +226,35 @@ function ExcalidrawWrapper() {
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);
}, []);
const [ excalidrawRef.current!.readyPromise.then((excalidrawApi) => {
excalidrawAPI, initializeScene({
excalidrawRefCallback, resetScene: excalidrawApi.resetScene,
] = useCallbackRefState<ExcalidrawImperativeAPI>(); initializeSocketClient: collab.initializeSocketClient,
}).then((scene) => {
const collabAPI = useContext(CollabContext)?.api; initialStatePromiseRef.current.promise.resolve(scene);
});
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: excalidrawAPI.resetScene, resetScene: api.resetScene,
initializeSocketClient: collabAPI.initializeSocketClient, initializeSocketClient: collab.initializeSocketClient,
}).then((scene) => { }).then((scene) => {
if (scene) { if (scene) {
excalidrawAPI.updateScene(scene); api.updateScene(scene);
} }
}); });
} }
@@ -275,7 +273,7 @@ function ExcalidrawWrapper() {
window.removeEventListener(EVENT.BLUR, onBlur, false); window.removeEventListener(EVENT.BLUR, onBlur, false);
clearTimeout(titleTimeout); clearTimeout(titleTimeout);
}; };
}, [collabAPI, excalidrawAPI]); }, [collab.initializeSocketClient]);
useEffect(() => { useEffect(() => {
languageDetector.cacheUserLanguage(langCode); languageDetector.cacheUserLanguage(langCode);
@@ -286,8 +284,8 @@ function ExcalidrawWrapper() {
appState: AppState, appState: AppState,
) => { ) => {
saveDebounced(elements, appState); saveDebounced(elements, appState);
if (collabAPI?.isCollaborating) { if (collab.isCollaborating) {
collabAPI.broadcastElements(elements); collab.broadcastElements(elements, appState);
} }
}; };
@@ -345,20 +343,19 @@ function ExcalidrawWrapper() {
return ( return (
<> <>
<Excalidraw <Excalidraw
ref={excalidrawRefCallback} ref={excalidrawRef}
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: collabAPI?.username }} user={{ name: collab.username }}
onCollabButtonClick={collabAPI?.onCollabButtonClick} onCollabButtonClick={collab.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating} isCollaborating={collab.isCollaborating}
onPointerUpdate={collabAPI?.onPointerUpdate} onPointerUpdate={collab.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}
@@ -372,9 +369,13 @@ function ExcalidrawWrapper() {
export default function ExcalidrawApp() { export default function ExcalidrawApp() {
return ( return (
<TopErrorBoundary> <TopErrorBoundary>
<CollabContextConsumer> <CollabWrapper
<ExcalidrawWrapper /> excalidrawRef={
</CollabContextConsumer> excalidrawRef as React.MutableRefObject<ExcalidrawImperativeAPI>
}
>
{(collab) => <ExcalidrawWrapper collab={collab} />}
</CollabWrapper>
</TopErrorBoundary> </TopErrorBoundary>
); );
} }
+3 -2
View File
@@ -1,10 +1,11 @@
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: sum(allCoords, (coords) => coords.x) / allCoords.length, x: normalizeScroll(sum(allCoords, (coords) => coords.x) / allCoords.length),
y: sum(allCoords, (coords) => coords.y) / allCoords.length, y: normalizeScroll(sum(allCoords, (coords) => coords.y) / allCoords.length),
}; };
}; };
+1 -1
View File
@@ -85,6 +85,6 @@ type ForwardRef<T, P = any> = Parameters<
// --------------------------------------------------------------------------— // --------------------------------------------------------------------------—
interface Blob { interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle; handle?: import("browser-nativefs").FileSystemHandle;
name?: string; name?: string;
} }
-7
View File
@@ -1,7 +0,0 @@
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;
};
-1
View File
@@ -27,7 +27,6 @@ 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" },
+10 -14
View File
@@ -1,18 +1,7 @@
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,
}: { }: {
@@ -20,7 +9,16 @@ export const IsMobileProvider = ({
}) => { }) => {
const query = useRef<MediaQueryList>(); const query = useRef<MediaQueryList>();
if (!query.current) { if (!query.current) {
query.current = getIsMobileMatcher(); query.current = window.matchMedia
? window.matchMedia(
// keep up to date with _variables.scss
"(max-width: 640px), (max-height: 500px) and (max-width: 1000px)",
)
: (({
matches: false,
addListener: () => {},
removeListener: () => {},
} as any) as MediaQueryList);
} }
const [isMobile, setMobile] = useState(query.current.matches); const [isMobile, setMobile] = useState(query.current.matches);
@@ -33,8 +31,6 @@ 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);
} }
-4
View File
@@ -1,5 +1,4 @@
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",
@@ -19,9 +18,7 @@ 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 = {
@@ -51,7 +48,6 @@ 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;
+3 -4
View File
@@ -80,9 +80,9 @@
"gridMode": "وضع الشبكة", "gridMode": "وضع الشبكة",
"addToLibrary": "أضف إلى المكتبة", "addToLibrary": "أضف إلى المكتبة",
"removeFromLibrary": "حذف من المكتبة", "removeFromLibrary": "حذف من المكتبة",
"libraryLoadingMessage": "جارٍ تحميل المكتبة", "libraryLoadingMessage": "جارٍ تحميل المكتبة...",
"libraries": "تصفح المكتبات", "libraries": "تصفح المكتبات",
"loadingScene": "جاري تحميل المشهد", "loadingScene": "جاري تحميل المشهد...",
"align": "محاذاة", "align": "محاذاة",
"alignTop": "محاذاة إلى اﻷعلى", "alignTop": "محاذاة إلى اﻷعلى",
"alignBottom": "محاذاة إلى اﻷسفل", "alignBottom": "محاذاة إلى اﻷسفل",
@@ -91,8 +91,7 @@
"centerVertically": "توسيط عمودي", "centerVertically": "توسيط عمودي",
"centerHorizontally": "توسيط أفقي", "centerHorizontally": "توسيط أفقي",
"distributeHorizontally": "التوزيع الأفقي", "distributeHorizontally": "التوزيع الأفقي",
"distributeVertically": "التوزيع عمودياً", "distributeVertically": "التوزيع عمودياً"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "إعادة تعيين اللوحة", "clearReset": "إعادة تعيين اللوحة",
+11 -12
View File
@@ -80,9 +80,9 @@
"gridMode": "Решетъчен режим", "gridMode": "Решетъчен режим",
"addToLibrary": "Добавяне към библиотеката", "addToLibrary": "Добавяне към библиотеката",
"removeFromLibrary": "Премахване от библиотеката", "removeFromLibrary": "Премахване от библиотеката",
"libraryLoadingMessage": "Зареждане на библиотеката", "libraryLoadingMessage": "Зареждане на библиотеката...",
"libraries": "Разглеждане на библиотеките", "libraries": "Разглеждане на библиотеките",
"loadingScene": "Зареждане на сцена", "loadingScene": "Зареждане на сцена...",
"align": "Подравняване", "align": "Подравняване",
"alignTop": "Подравняване отгоре", "alignTop": "Подравняване отгоре",
"alignBottom": "Подравняване отдолу", "alignBottom": "Подравняване отдолу",
@@ -91,8 +91,7 @@
"centerVertically": "Центрирай вертикално", "centerVertically": "Центрирай вертикално",
"centerHorizontally": "Центрирай хоризонтално", "centerHorizontally": "Центрирай хоризонтално",
"distributeHorizontally": "Разпредели хоризонтално", "distributeHorizontally": "Разпредели хоризонтално",
"distributeVertically": "Разпредели вертикално", "distributeVertically": "Разпредели вертикално"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "Нулиране на платно", "clearReset": "Нулиране на платно",
@@ -202,24 +201,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 няма да могат да ги виждат."
+2 -3
View File
@@ -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,8 +91,7 @@
"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ç",
+3 -4
View File
@@ -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,8 +91,7 @@
"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",
+3 -4
View File
@@ -80,9 +80,9 @@
"gridMode": "Εμφάνιση σε πλέγμα", "gridMode": "Εμφάνιση σε πλέγμα",
"addToLibrary": "Προσθήκη στη βιβλιοθήκη", "addToLibrary": "Προσθήκη στη βιβλιοθήκη",
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη", "removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης", "libraryLoadingMessage": "Φόρτωση βιβλιοθήκης...",
"libraries": "Άλλες βιβλιοθήκες", "libraries": "Άλλες βιβλιοθήκες",
"loadingScene": "Φόρτωση σκηνής", "loadingScene": "Φόρτωση σκηνής...",
"align": "Στοίχιση", "align": "Στοίχιση",
"alignTop": "Στοίχιση πάνω", "alignTop": "Στοίχιση πάνω",
"alignBottom": "Στοίχιση κάτω", "alignBottom": "Στοίχιση κάτω",
@@ -91,8 +91,7 @@
"centerVertically": "Κέντρο κάθετα", "centerVertically": "Κέντρο κάθετα",
"centerHorizontally": "Κέντρο οριζόντια", "centerHorizontally": "Κέντρο οριζόντια",
"distributeHorizontally": "Οριζόντια κατανομή", "distributeHorizontally": "Οριζόντια κατανομή",
"distributeVertically": "Κατακόρυφη κατανομή", "distributeVertically": "Κατακόρυφη κατανομή"
"viewMode": "Λειτουργία προβολής"
}, },
"buttons": { "buttons": {
"clearReset": "Επαναφορά του καμβά", "clearReset": "Επαναφορά του καμβά",
+3 -4
View File
@@ -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,8 +91,7 @@
"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",
+1 -2
View File
@@ -91,8 +91,7 @@
"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",
+21 -22
View File
@@ -80,9 +80,9 @@
"gridMode": "حالت شبکه ای", "gridMode": "حالت شبکه ای",
"addToLibrary": "افزودن به کتابخانه", "addToLibrary": "افزودن به کتابخانه",
"removeFromLibrary": "حذف از کتابخانه", "removeFromLibrary": "حذف از کتابخانه",
"libraryLoadingMessage": "بارگذاری کتابخانه", "libraryLoadingMessage": "بارگذاری کتابخانه...",
"libraries": "مرور کردن کتابخانه ها", "libraries": "مرور کردن کتابخانه ها",
"loadingScene": "باگذاری صحنه", "loadingScene": "باگذاری صحنه...",
"align": "تراز", "align": "تراز",
"alignTop": "تراز به بالا", "alignTop": "تراز به بالا",
"alignBottom": "تراز به پایین", "alignBottom": "تراز به پایین",
@@ -91,8 +91,7 @@
"centerVertically": "وسط قرار دادن به صورت عمودی", "centerVertically": "وسط قرار دادن به صورت عمودی",
"centerHorizontally": "وسط قرار دادن به صورت افقی", "centerHorizontally": "وسط قرار دادن به صورت افقی",
"distributeHorizontally": "توزیع کردن به صورت افقی", "distributeHorizontally": "توزیع کردن به صورت افقی",
"distributeVertically": "توزیع کردن به صورت عمودی", "distributeVertically": "توزیع کردن به صورت عمودی"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "پاکسازی بوم نقاشی", "clearReset": "پاکسازی بوم نقاشی",
@@ -201,25 +200,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 هرگز طرح های شما را نمیبند."
@@ -237,7 +236,7 @@
"width": "عرض" "width": "عرض"
}, },
"toast": { "toast": {
"copyStyles": "کپی سبک.", "copyStyles": "",
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG." "copyToClipboardAsPng": ""
} }
} }
+3 -4
View File
@@ -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,8 +91,7 @@
"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",
+2 -3
View File
@@ -91,8 +91,7 @@
"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",
@@ -230,7 +229,7 @@
"elements": "Éléments", "elements": "Éléments",
"height": "Hauteur", "height": "Hauteur",
"scene": "Scène", "scene": "Scène",
"selected": "Sélection", "selected": "Sélection",
"storage": "Stockage", "storage": "Stockage",
"title": "Stats pour les nerds", "title": "Stats pour les nerds",
"total": "Total", "total": "Total",
+3 -4
View File
@@ -80,9 +80,9 @@
"gridMode": "מצב רשת", "gridMode": "מצב רשת",
"addToLibrary": "הוסף לספריה", "addToLibrary": "הוסף לספריה",
"removeFromLibrary": "הסר מספריה", "removeFromLibrary": "הסר מספריה",
"libraryLoadingMessage": "טוען ספריה", "libraryLoadingMessage": "טוען ספריה...",
"libraries": "דפדף בספריות", "libraries": "דפדף בספריות",
"loadingScene": "טוען תצוגה", "loadingScene": "טוען תצוגה...",
"align": "יישר", "align": "יישר",
"alignTop": "יישר למעלה", "alignTop": "יישר למעלה",
"alignBottom": "יישר למטה", "alignBottom": "יישר למטה",
@@ -91,8 +91,7 @@
"centerVertically": "מרכז אנכית", "centerVertically": "מרכז אנכית",
"centerHorizontally": "מרכז אופקית", "centerHorizontally": "מרכז אופקית",
"distributeHorizontally": "חלוקה אופקית", "distributeHorizontally": "חלוקה אופקית",
"distributeVertically": "חלוקה אנכית", "distributeVertically": "חלוקה אנכית"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "אפס את הלוח", "clearReset": "אפס את הלוח",
+1 -2
View File
@@ -91,8 +91,7 @@
"centerVertically": "लंबवत केन्द्रित", "centerVertically": "लंबवत केन्द्रित",
"centerHorizontally": "क्षैतिज केन्द्रित", "centerHorizontally": "क्षैतिज केन्द्रित",
"distributeHorizontally": "क्षैतिज रूप से वितरित करें", "distributeHorizontally": "क्षैतिज रूप से वितरित करें",
"distributeVertically": "खड़ी रूप से वितरित करें", "distributeVertically": "खड़ी रूप से वितरित करें"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "कैनवास रीसेट करें", "clearReset": "कैनवास रीसेट करें",
+3 -4
View File
@@ -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,8 +91,7 @@
"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",
+22 -23
View File
@@ -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,8 +91,7 @@
"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",
@@ -201,25 +200,25 @@
"title": "Kesalahan" "title": "Kesalahan"
}, },
"helpDialog": { "helpDialog": {
"blog": "Baca blog kami", "blog": "",
"click": "klik", "click": "",
"curvedArrow": "Panah lengkung", "curvedArrow": "",
"curvedLine": "Garis lengkung", "curvedLine": "",
"documentation": "Dokumentasi", "documentation": "",
"drag": "seret", "drag": "",
"editor": "Editor", "editor": "",
"github": "Menemukan masalah? Kirimkan", "github": "",
"howto": "Ikuti panduan kami", "howto": "",
"or": "atau", "or": "",
"preventBinding": "Cegah pengikatan panah", "preventBinding": "",
"shapes": "Bentuk", "shapes": "",
"shortcuts": "Pintasan keyboard", "shortcuts": "",
"textFinish": "Selesai mengedit (teks)", "textFinish": "",
"textNewLine": "Tambahkan baris baru (teks)", "textNewLine": "",
"title": "Bantuan", "title": "",
"view": "Tampilan", "view": "",
"zoomToFit": "Perbesar agar sesuai dengan semua elemen", "zoomToFit": "",
"zoomToSelection": "Perbesar ke seleksi" "zoomToSelection": ""
}, },
"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."
+22 -23
View File
@@ -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,8 +91,7 @@
"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",
@@ -201,25 +200,25 @@
"title": "Errore" "title": "Errore"
}, },
"helpDialog": { "helpDialog": {
"blog": "Leggi il nostro blog", "blog": "",
"click": "click", "click": "",
"curvedArrow": "Freccia curva", "curvedArrow": "",
"curvedLine": "Linea curva", "curvedLine": "",
"documentation": "Documentazione", "documentation": "",
"drag": "trascina", "drag": "",
"editor": "Editor", "editor": "",
"github": "Trovato un problema? Segnalalo", "github": "",
"howto": "Segui le nostre guide", "howto": "",
"or": "oppure", "or": "",
"preventBinding": "Impedisci legame della freccia", "preventBinding": "",
"shapes": "Forme", "shapes": "",
"shortcuts": "Scorciatoie da tastiera", "shortcuts": "",
"textFinish": "Termina la modifica (testo)", "textFinish": "",
"textNewLine": "Aggiungi nuova riga (testo)", "textNewLine": "",
"title": "Guida", "title": "",
"view": "Vista", "view": "",
"zoomToFit": "Adatta zoom per mostrare tutti gli elementi", "zoomToFit": "",
"zoomToSelection": "Zoom alla selezione" "zoomToSelection": ""
}, },
"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."
+3 -4
View File
@@ -80,9 +80,9 @@
"gridMode": "", "gridMode": "",
"addToLibrary": "ライブラリに追加", "addToLibrary": "ライブラリに追加",
"removeFromLibrary": "ライブラリから削除", "removeFromLibrary": "ライブラリから削除",
"libraryLoadingMessage": "ライブラリを読み込み中", "libraryLoadingMessage": "ライブラリを読み込み中...",
"libraries": "", "libraries": "",
"loadingScene": "シーンを読み込み中", "loadingScene": "シーンを読み込み中...",
"align": "整列", "align": "整列",
"alignTop": "上揃え", "alignTop": "上揃え",
"alignBottom": "下揃え", "alignBottom": "下揃え",
@@ -91,8 +91,7 @@
"centerVertically": "縦方向に中央揃え", "centerVertically": "縦方向に中央揃え",
"centerHorizontally": "横方向に中央揃え", "centerHorizontally": "横方向に中央揃え",
"distributeHorizontally": "", "distributeHorizontally": "",
"distributeVertically": "", "distributeVertically": ""
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "キャンバスのリセット", "clearReset": "キャンバスのリセット",
-243
View File
@@ -1,243 +0,0 @@
{
"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."
}
}
+1 -2
View File
@@ -91,8 +91,7 @@
"centerVertically": "수직으로 중앙 정렬", "centerVertically": "수직으로 중앙 정렬",
"centerHorizontally": "수평으로 중앙 정렬", "centerHorizontally": "수평으로 중앙 정렬",
"distributeHorizontally": "수평으로 분배", "distributeHorizontally": "수평으로 분배",
"distributeVertically": "수직으로 분배", "distributeVertically": "수직으로 분배"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "캔버스 초기화", "clearReset": "캔버스 초기화",
+3 -4
View File
@@ -80,9 +80,9 @@
"gridMode": "", "gridMode": "",
"addToLibrary": "မှတ်တမ်းတင်", "addToLibrary": "မှတ်တမ်းတင်",
"removeFromLibrary": "မှတ်တမ်းမှထုတ်", "removeFromLibrary": "မှတ်တမ်းမှထုတ်",
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်", "libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်...",
"libraries": "စာကြည့်တိုက်တွင်ရှာဖွေပါ", "libraries": "စာကြည့်တိုက်တွင်ရှာဖွေပါ",
"loadingScene": "မြင်ကွင်းဖော်နေသည်", "loadingScene": "မြင်ကွင်းဖော်နေသည်...",
"align": "ချိန်ညှိ", "align": "ချိန်ညှိ",
"alignTop": "ထိပ်ညှိ", "alignTop": "ထိပ်ညှိ",
"alignBottom": "အခြေညှိ", "alignBottom": "အခြေညှိ",
@@ -91,8 +91,7 @@
"centerVertically": "ဒေါင်လိုက်အလယ်ညှိ", "centerVertically": "ဒေါင်လိုက်အလယ်ညှိ",
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ", "centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
"distributeHorizontally": "အလျားလိုက်", "distributeHorizontally": "အလျားလိုက်",
"distributeVertically": "ထောင်လိုက်", "distributeVertically": "ထောင်လိုက်"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "ကားချပ်ရှင်းလင်း", "clearReset": "ကားချပ်ရှင်းလင်း",
+3 -4
View File
@@ -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,8 +91,7 @@
"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",
+1 -2
View File
@@ -91,8 +91,7 @@
"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",
+9 -10
View File
@@ -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,8 +91,7 @@
"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",
@@ -202,22 +201,22 @@
}, },
"helpDialog": { "helpDialog": {
"blog": "", "blog": "",
"click": "klikk", "click": "",
"curvedArrow": "", "curvedArrow": "",
"curvedLine": "", "curvedLine": "",
"documentation": "", "documentation": "",
"drag": "", "drag": "",
"editor": "Redigering", "editor": "",
"github": "", "github": "",
"howto": "", "howto": "",
"or": "eller", "or": "",
"preventBinding": "", "preventBinding": "",
"shapes": "Formar", "shapes": "",
"shortcuts": "", "shortcuts": "",
"textFinish": "", "textFinish": "",
"textNewLine": "", "textNewLine": "",
"title": "Hjelp", "title": "",
"view": "Vising", "view": "",
"zoomToFit": "", "zoomToFit": "",
"zoomToSelection": "" "zoomToSelection": ""
}, },
+20 -21
View File
@@ -91,8 +91,7 @@
"centerVertically": "ਲੇਟਵੇਂ ਵਿਚਕਾਰ ਕਰੋ", "centerVertically": "ਲੇਟਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
"centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ", "centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
"distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ", "distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
"distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ", "distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ", "clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ",
@@ -201,25 +200,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 ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।"
+18 -19
View File
@@ -1,36 +1,35 @@
{ {
"ar-SA": 89, "ar-SA": 90,
"bg-BG": 93, "bg-BG": 90,
"ca-ES": 89, "ca-ES": 90,
"de-DE": 100, "de-DE": 100,
"el-GR": 100, "el-GR": 100,
"en": 100, "en": 100,
"es-ES": 100, "es-ES": 100,
"fa-IR": 98, "fa-IR": 90,
"fi-FI": 100, "fi-FI": 100,
"fr-FR": 100, "fr-FR": 100,
"he-IL": 89, "he-IL": 90,
"hi-IN": 89, "hi-IN": 90,
"hu-HU": 89, "hu-HU": 90,
"id-ID": 100, "id-ID": 91,
"it-IT": 100, "it-IT": 91,
"ja-JP": 81, "ja-JP": 81,
"kab-KAB": 97, "ko-KR": 90,
"ko-KR": 89,
"my-MM": 83, "my-MM": 83,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 100, "nl-NL": 100,
"nn-NO": 92, "nn-NO": 90,
"pa-IN": 99, "pa-IN": 91,
"pl-PL": 90, "pl-PL": 90,
"pt-BR": 100, "pt-BR": 100,
"pt-PT": 99, "pt-PT": 100,
"ro-RO": 100, "ro-RO": 100,
"ru-RU": 99, "ru-RU": 91,
"sk-SK": 100, "sk-SK": 91,
"sv-SE": 100, "sv-SE": 100,
"tr-TR": 89, "tr-TR": 90,
"uk-UA": 99, "uk-UA": 100,
"zh-CN": 99, "zh-CN": 90,
"zh-TW": 100 "zh-TW": 100
} }
+1 -2
View File
@@ -91,8 +91,7 @@
"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",
+1 -2
View File
@@ -91,8 +91,7 @@
"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",
+3 -4
View File
@@ -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,8 +91,7 @@
"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",
+3 -4
View File
@@ -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,8 +91,7 @@
"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ă",
+22 -23
View File
@@ -80,9 +80,9 @@
"gridMode": "Сетка", "gridMode": "Сетка",
"addToLibrary": "Добавить в библиотеку", "addToLibrary": "Добавить в библиотеку",
"removeFromLibrary": "Удалить из библиотеки", "removeFromLibrary": "Удалить из библиотеки",
"libraryLoadingMessage": "Загрузка библиотеки", "libraryLoadingMessage": "Загрузка библиотеки...",
"libraries": "Просмотреть библиотеки", "libraries": "Просмотреть библиотеки",
"loadingScene": "Загрузка сцены", "loadingScene": "Загрузка сцены...",
"align": "Выровнять", "align": "Выровнять",
"alignTop": "Выровнять по верхнему краю", "alignTop": "Выровнять по верхнему краю",
"alignBottom": "Выровнять по нижнему краю", "alignBottom": "Выровнять по нижнему краю",
@@ -91,8 +91,7 @@
"centerVertically": "Центрировать по вертикали", "centerVertically": "Центрировать по вертикали",
"centerHorizontally": "Центрировать по горизонтали", "centerHorizontally": "Центрировать по горизонтали",
"distributeHorizontally": "Распределить по горизонтали", "distributeHorizontally": "Распределить по горизонтали",
"distributeVertically": "Распределить по вертикали", "distributeVertically": "Распределить по вертикали"
"viewMode": ""
}, },
"buttons": { "buttons": {
"clearReset": "Очистить холст и сбросить цвет фона", "clearReset": "Очистить холст и сбросить цвет фона",
@@ -201,25 +200,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 никогда не получат доступ к ним."
+22 -23
View File
@@ -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,8 +91,7 @@
"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",
@@ -201,25 +200,25 @@
"title": "Chyba" "title": "Chyba"
}, },
"helpDialog": { "helpDialog": {
"blog": "Prečítajte si náš blog", "blog": "",
"click": "kliknutie", "click": "",
"curvedArrow": "Zakrivená šípka", "curvedArrow": "",
"curvedLine": "Zakrivená čiara", "curvedLine": "",
"documentation": "Dokumentácia", "documentation": "",
"drag": "potiahnutie", "drag": "",
"editor": "Editovanie", "editor": "",
"github": "Objavili ste problém? Nahláste ho", "github": "",
"howto": "Postupujte podľa naších návodov", "howto": "",
"or": "alebo", "or": "",
"preventBinding": "Zakázať pripájanie šípky", "preventBinding": "",
"shapes": "Tvary", "shapes": "",
"shortcuts": "Klávesové skratky", "shortcuts": "",
"textFinish": "Ukončenie editovania (text)", "textFinish": "",
"textNewLine": "Vložiť nový riadok (text)", "textNewLine": "",
"title": "Pomocník", "title": "",
"view": "Zobrazenie", "view": "",
"zoomToFit": "Priblížiť aby boli zahrnuté všetky prvky", "zoomToFit": "",
"zoomToSelection": "Priblížiť na výber" "zoomToSelection": ""
}, },
"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ť."

Some files were not shown because too many files have changed in this diff Show More