Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c447c4e245 | |||
| 7e330c8ee1 | |||
| 7d21747644 | |||
| e718136aea | |||
| fe83e2922d | |||
| 20edddcd4e | |||
| f6e8be399e | |||
| ab49cad6b1 | |||
| 6aeb18b784 | |||
| 023313e92f | |||
| 1eee488dab | |||
| dd4c333925 | |||
| 8542c95a7a | |||
| cef6094d4c | |||
| 3322f0fa6f | |||
| 34a7d48b95 | |||
| 5c0b15ce2b | |||
| 9f9666110e | |||
| 05ffce62ef | |||
| 0f06fa3851 | |||
| 1ce933d2f5 | |||
| 15655acb5a | |||
| d5b264c2d2 | |||
| bd4424bbe3 | |||
| 38fc51b4e3 | |||
| e1dc748aef | |||
| 0e95e2b386 | |||
| 9659254fd6 | |||
| 39b96cb011 | |||
| 04a8c22f39 | |||
| e4506be3e8 | |||
| 1e816e87bf | |||
| 5368ddef74 | |||
| 88ff32e9b3 | |||
| 0fcbddda8e |
@@ -22,3 +22,8 @@ REACT_APP_DEV_ENABLE_SW=
|
|||||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
#Debug flags
|
||||||
|
|
||||||
|
# To enable bounding box for text containers
|
||||||
|
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ src/packages/excalidraw/types
|
|||||||
src/packages/excalidraw/example/public/bundle.js
|
src/packages/excalidraw/example/public/bundle.js
|
||||||
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
||||||
src/packages/excalidraw/example/public/excalidraw.development.js
|
src/packages/excalidraw/example/public/excalidraw.development.js
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports:
|
|||||||
|
|
||||||
## Excalidraw.com
|
## Excalidraw.com
|
||||||
|
|
||||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/maielo/new-readme/src/excalidraw-app) is part of this repository as well, and the app features:
|
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||||
|
|
||||||
- 📡 PWA support (works offline).
|
- 📡 PWA support (works offline).
|
||||||
- 🤼 Real-time collaboration.
|
- 🤼 Real-time collaboration.
|
||||||
|
|||||||
@@ -4,6 +4,34 @@
|
|||||||
|
|
||||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
||||||
|
|
||||||
|
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
||||||
|
|
||||||
|
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
|
||||||
|
|
||||||
|
We strongly recommend turning it off. You can follow the steps below on how to do so.
|
||||||
|
|
||||||
|
|
||||||
|
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
|
||||||
|

|
||||||
|
|
||||||
|
<div style={{width:'30rem'}}>
|
||||||
|
|
||||||
|
2. Once opened, look for **Aggressively Block Fingerprinting**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Switch to **Block Fingerprinting**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Thats all. All text elements should be fixed now 🎉
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Need help?
|
## Need help?
|
||||||
|
|
||||||
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
+9
-9
@@ -1785,9 +1785,9 @@
|
|||||||
"@hapi/hoek" "^9.0.0"
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@sideway/formula@^3.0.0":
|
"@sideway/formula@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||||
|
|
||||||
"@sideway/pinpoint@^2.0.0":
|
"@sideway/pinpoint@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1:
|
|||||||
entities "^4.3.0"
|
entities "^4.3.0"
|
||||||
|
|
||||||
http-cache-semantics@^4.0.0:
|
http-cache-semantics@^4.0.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||||
|
|
||||||
http-deceiver@^1.2.7:
|
http-deceiver@^1.2.7:
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
@@ -7542,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
|
|||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@^5.73.0:
|
webpack@^5.73.0:
|
||||||
version "5.74.0"
|
version "5.76.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
|
||||||
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
|
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.3"
|
"@types/eslint-scope" "^3.7.3"
|
||||||
"@types/estree" "^0.0.51"
|
"@types/estree" "^0.0.51"
|
||||||
|
|||||||
+7
-7
@@ -25,11 +25,6 @@
|
|||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@tldraw/vec": "1.7.1",
|
"@tldraw/vec": "1.7.1",
|
||||||
"@types/jest": "27.4.0",
|
|
||||||
"@types/pica": "5.1.3",
|
|
||||||
"@types/react": "18.0.15",
|
|
||||||
"@types/react-dom": "18.0.6",
|
|
||||||
"@types/socket.io-client": "1.4.36",
|
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
@@ -57,7 +52,6 @@
|
|||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"tunnel-rat": "0.1.0",
|
"tunnel-rat": "0.1.0",
|
||||||
"typescript": "4.9.4",
|
|
||||||
"workbox-background-sync": "^6.5.4",
|
"workbox-background-sync": "^6.5.4",
|
||||||
"workbox-broadcast-update": "^6.5.4",
|
"workbox-broadcast-update": "^6.5.4",
|
||||||
"workbox-cacheable-response": "^6.5.4",
|
"workbox-cacheable-response": "^6.5.4",
|
||||||
@@ -75,9 +69,14 @@
|
|||||||
"@excalidraw/eslint-config": "1.0.0",
|
"@excalidraw/eslint-config": "1.0.0",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
|
"@types/pica": "5.1.3",
|
||||||
|
"@types/react": "18.0.15",
|
||||||
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/resize-observer-browser": "0.1.7",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
|
"@types/socket.io-client": "1.4.36",
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
@@ -88,7 +87,8 @@
|
|||||||
"lint-staged": "12.3.7",
|
"lint-staged": "12.3.7",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0"
|
"rewire": "6.0.0",
|
||||||
|
"typescript": "4.9.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const fs = require("fs");
|
|||||||
|
|
||||||
const THRESSHOLD = 85;
|
const THRESSHOLD = 85;
|
||||||
|
|
||||||
|
// we're using BCP 47 language tags as keys
|
||||||
|
// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
|
||||||
|
|
||||||
const crowdinMap = {
|
const crowdinMap = {
|
||||||
"ar-SA": "en-ar",
|
"ar-SA": "en-ar",
|
||||||
"bg-BG": "en-bg",
|
"bg-BG": "en-bg",
|
||||||
@@ -52,6 +55,7 @@ const crowdinMap = {
|
|||||||
"kk-KZ": "en-kk",
|
"kk-KZ": "en-kk",
|
||||||
"vi-VN": "en-vi",
|
"vi-VN": "en-vi",
|
||||||
"mr-IN": "en-mr",
|
"mr-IN": "en-mr",
|
||||||
|
"th-TH": "en-th",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -104,6 +108,7 @@ const flags = {
|
|||||||
"eu-ES": "🇪🇦",
|
"eu-ES": "🇪🇦",
|
||||||
"vi-VN": "🇻🇳",
|
"vi-VN": "🇻🇳",
|
||||||
"mr-IN": "🇮🇳",
|
"mr-IN": "🇮🇳",
|
||||||
|
"th-TH": "🇹🇭",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@@ -156,6 +161,7 @@ const languages = {
|
|||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
"vi-VN": "Tiếng Việt",
|
"vi-VN": "Tiếng Việt",
|
||||||
"mr-IN": "मराठी",
|
"mr-IN": "मराठी",
|
||||||
|
"th-TH": "ภาษาไทย",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
|||||||
+149
-14
@@ -1,7 +1,8 @@
|
|||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -13,8 +14,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
|
isUsingAdaptiveRadius,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -38,7 +42,7 @@ export const actionUnbindText = register({
|
|||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureText(
|
const { width, height } = measureText(
|
||||||
boundTextElement.originalText,
|
boundTextElement.originalText,
|
||||||
getFontString(boundTextElement),
|
getFontString(boundTextElement),
|
||||||
);
|
);
|
||||||
@@ -51,7 +55,6 @@ export const actionUnbindText = register({
|
|||||||
containerId: null,
|
containerId: null,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
baseline,
|
|
||||||
text: boundTextElement.originalText,
|
text: boundTextElement.originalText,
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
@@ -130,19 +133,151 @@ export const actionBindText = register({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(textElement, container);
|
redrawTextBoundingBox(textElement, container);
|
||||||
const updatedElements = elements.slice();
|
|
||||||
const textElementIndex = updatedElements.findIndex(
|
|
||||||
(ele) => ele.id === textElement.id,
|
|
||||||
);
|
|
||||||
updatedElements.splice(textElementIndex, 1);
|
|
||||||
const containerIndex = updatedElements.findIndex(
|
|
||||||
(ele) => ele.id === container.id,
|
|
||||||
);
|
|
||||||
updatedElements.splice(containerIndex + 1, 0, textElement);
|
|
||||||
return {
|
return {
|
||||||
elements: updatedElements,
|
elements: pushTextAboveContainer(elements, container, textElement),
|
||||||
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pushTextAboveContainer = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
) => {
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
const textElementIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === textElement.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(textElementIndex, 1);
|
||||||
|
|
||||||
|
const containerIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === container.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(containerIndex + 1, 0, textElement);
|
||||||
|
return updatedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushContainerBelowText = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
) => {
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
const containerIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === container.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(containerIndex, 1);
|
||||||
|
|
||||||
|
const textElementIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === textElement.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(textElementIndex, 0, container);
|
||||||
|
return updatedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionCreateContainerFromText = register({
|
||||||
|
name: "createContainerFromText",
|
||||||
|
contextItemLabel: "labels.createContainerFromText",
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
|
||||||
|
},
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||||
|
const textElement = selectedElements[0];
|
||||||
|
const container = newElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: appState.currentItemBackgroundColor,
|
||||||
|
boundElements: [
|
||||||
|
...(textElement.boundElements || []),
|
||||||
|
{ id: textElement.id, type: "text" },
|
||||||
|
],
|
||||||
|
angle: textElement.angle,
|
||||||
|
fillStyle: appState.currentItemFillStyle,
|
||||||
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
|
roughness: appState.currentItemRoughness,
|
||||||
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
|
roundness:
|
||||||
|
appState.currentItemRoundness === "round"
|
||||||
|
? {
|
||||||
|
type: isUsingAdaptiveRadius("rectangle")
|
||||||
|
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||||
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
opacity: 100,
|
||||||
|
locked: false,
|
||||||
|
x: textElement.x - BOUND_TEXT_PADDING,
|
||||||
|
y: textElement.y - BOUND_TEXT_PADDING,
|
||||||
|
width: computeContainerDimensionForBoundText(
|
||||||
|
textElement.width,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
height: computeContainerDimensionForBoundText(
|
||||||
|
textElement.height,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
groupIds: textElement.groupIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update bindings
|
||||||
|
if (textElement.boundElements?.length) {
|
||||||
|
const linearElementIds = textElement.boundElements
|
||||||
|
.filter((ele) => ele.type === "arrow")
|
||||||
|
.map((el) => el.id);
|
||||||
|
const linearElements = updatedElements.filter((ele) =>
|
||||||
|
linearElementIds.includes(ele.id),
|
||||||
|
) as ExcalidrawLinearElement[];
|
||||||
|
linearElements.forEach((ele) => {
|
||||||
|
let startBinding = null;
|
||||||
|
let endBinding = null;
|
||||||
|
if (ele.startBinding) {
|
||||||
|
startBinding = { ...ele.startBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
if (ele.endBinding) {
|
||||||
|
endBinding = { ...ele.endBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
mutateElement(ele, { startBinding, endBinding });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(textElement, {
|
||||||
|
containerId: container.id,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
boundElements: null,
|
||||||
|
});
|
||||||
|
redrawTextBoundingBox(textElement, container);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: pushContainerBelowText(
|
||||||
|
[...elements, container],
|
||||||
|
container,
|
||||||
|
textElement,
|
||||||
|
),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: {
|
||||||
|
[container.id]: true,
|
||||||
|
[textElement.id]: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
elements: updatedElements,
|
||||||
|
appState,
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -745,16 +745,19 @@ export const actionChangeTextAlign = register({
|
|||||||
value: "left",
|
value: "left",
|
||||||
text: t("labels.left"),
|
text: t("labels.left"),
|
||||||
icon: TextAlignLeftIcon,
|
icon: TextAlignLeftIcon,
|
||||||
|
testId: "align-left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "center",
|
value: "center",
|
||||||
text: t("labels.center"),
|
text: t("labels.center"),
|
||||||
icon: TextAlignCenterIcon,
|
icon: TextAlignCenterIcon,
|
||||||
|
testId: "align-horizontal-center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "right",
|
value: "right",
|
||||||
text: t("labels.right"),
|
text: t("labels.right"),
|
||||||
icon: TextAlignRightIcon,
|
icon: TextAlignRightIcon,
|
||||||
|
testId: "align-right",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isDarwin } from "../constants";
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { SubtypeOf } from "../utility-types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { MarkOptional } from "../utility-types";
|
||||||
|
|
||||||
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
||||||
|
|
||||||
@@ -113,7 +114,8 @@ export type ActionName =
|
|||||||
| "toggleLock"
|
| "toggleLock"
|
||||||
| "toggleLinearEditor"
|
| "toggleLinearEditor"
|
||||||
| "toggleEraserTool"
|
| "toggleEraserTool"
|
||||||
| "toggleHandTool";
|
| "toggleHandTool"
|
||||||
|
| "createContainerFromText";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ import clsx from "clsx";
|
|||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { shouldAllowVerticalAlign } from "../element/textElement";
|
import {
|
||||||
|
shouldAllowVerticalAlign,
|
||||||
|
suppportsHorizontalAlign,
|
||||||
|
} from "../element/textElement";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -122,7 +125,8 @@ export const SelectedShapeActions = ({
|
|||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{renderAction("changeTextAlign")}
|
{suppportsHorizontalAlign(targetElements) &&
|
||||||
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { actionClearCanvas } from "../actions";
|
import { actionClearCanvas } from "../actions";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
import { useExcalidrawActionManager } from "./App";
|
import { useExcalidrawActionManager } from "./App";
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
|||||||
export const ActiveConfirmDialog = () => {
|
export const ActiveConfirmDialog = () => {
|
||||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||||
activeConfirmDialogAtom,
|
activeConfirmDialogAtom,
|
||||||
|
jotaiScope,
|
||||||
);
|
);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import * as Renderer from "../renderer/renderScene";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
import { render, queryByTestId } from "../tests/test-utils";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
|
||||||
|
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||||
|
|
||||||
|
describe("Test <App/>", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Unmount ReactDOM from root
|
||||||
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
localStorage.clear();
|
||||||
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error modal when using brave and measureText API is not working", async () => {
|
||||||
|
(global.navigator as any).brave = {
|
||||||
|
isBrave: {
|
||||||
|
name: "isBrave",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalContext = global.HTMLCanvasElement.prototype.getContext("2d");
|
||||||
|
//@ts-ignore
|
||||||
|
global.HTMLCanvasElement.prototype.getContext = (contextId) => {
|
||||||
|
return {
|
||||||
|
...originalContext,
|
||||||
|
measureText: () => ({
|
||||||
|
width: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
expect(
|
||||||
|
queryByTestId(
|
||||||
|
document.querySelector(".excalidraw-modal-container")!,
|
||||||
|
"brave-measure-text-error",
|
||||||
|
),
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
+26
-12
@@ -62,6 +62,7 @@ import {
|
|||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
IMAGE_RENDER_TIMEOUT,
|
IMAGE_RENDER_TIMEOUT,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
|
isBrave,
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MAX_ALLOWED_FILE_BYTES,
|
MAX_ALLOWED_FILE_BYTES,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
@@ -108,6 +109,7 @@ import {
|
|||||||
textWysiwyg,
|
textWysiwyg,
|
||||||
transformElements,
|
transformElements,
|
||||||
updateTextElement,
|
updateTextElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
@@ -264,7 +266,9 @@ import {
|
|||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
|
getContainerElement,
|
||||||
getTextBindableContainerAtPosition,
|
getTextBindableContainerAtPosition,
|
||||||
|
isMeasureTextSupported,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
@@ -282,6 +286,8 @@ import { actionPaste } from "../actions/actionClipboard";
|
|||||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
|
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
||||||
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
isSmScreen: false,
|
isSmScreen: false,
|
||||||
@@ -426,7 +432,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.id = nanoid();
|
this.id = nanoid();
|
||||||
|
|
||||||
this.library = new Library(this);
|
this.library = new Library(this);
|
||||||
if (excalidrawRef) {
|
if (excalidrawRef) {
|
||||||
const readyPromise =
|
const readyPromise =
|
||||||
@@ -708,6 +713,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const theme =
|
const theme =
|
||||||
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
|
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
|
||||||
let name = actionResult?.appState?.name ?? this.state.name;
|
let name = actionResult?.appState?.name ?? this.state.name;
|
||||||
|
const errorMessage =
|
||||||
|
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
|
||||||
if (typeof this.props.viewModeEnabled !== "undefined") {
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||||
viewModeEnabled = this.props.viewModeEnabled;
|
viewModeEnabled = this.props.viewModeEnabled;
|
||||||
}
|
}
|
||||||
@@ -723,7 +730,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (typeof this.props.name !== "undefined") {
|
if (typeof this.props.name !== "undefined") {
|
||||||
name = this.props.name;
|
name = this.props.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
(state) => {
|
(state) => {
|
||||||
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
||||||
@@ -741,6 +747,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
gridSize,
|
gridSize,
|
||||||
theme,
|
theme,
|
||||||
name,
|
name,
|
||||||
|
errorMessage,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
@@ -869,7 +876,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// FontFaceSet loadingdone event we listen on may not always fire
|
// FontFaceSet loadingdone event we listen on may not always fire
|
||||||
// (looking at you Safari), so on init we manually load fonts for current
|
// (looking at you Safari), so on init we manually load fonts for current
|
||||||
// text elements on canvas, and rerender them once done. This also
|
// text elements on canvas, and rerender them once done. This also
|
||||||
@@ -997,6 +1003,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} else {
|
} else {
|
||||||
this.updateDOMRect(this.initializeScene);
|
this.updateDOMRect(this.initializeScene);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// note that this check seems to always pass in localhost
|
||||||
|
if (isBrave() && !isMeasureTextSupported()) {
|
||||||
|
this.setState({
|
||||||
|
errorMessage: <BraveMeasureTextError />,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
@@ -1625,6 +1638,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||||
return newElement;
|
return newElement;
|
||||||
});
|
});
|
||||||
|
|
||||||
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
|
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
|
||||||
const nextElements = [
|
const nextElements = [
|
||||||
...this.scene.getElementsIncludingDeleted(),
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
@@ -1637,6 +1651,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
|
|
||||||
|
newElements.forEach((newElement) => {
|
||||||
|
if (isTextElement(newElement)) {
|
||||||
|
const container = getContainerElement(newElement);
|
||||||
|
redrawTextBoundingBox(newElement, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.history.resumeRecording();
|
this.history.resumeRecording();
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
@@ -2663,14 +2685,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// case: creating new text not centered to parent element → offset Y
|
|
||||||
// so that the text is centered to cursor position
|
|
||||||
if (!parentCenterPosition) {
|
|
||||||
mutateElement(element, {
|
|
||||||
y: element.y - element.baseline / 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -2764,7 +2778,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
if (container) {
|
if (container) {
|
||||||
if (
|
if (
|
||||||
isArrowElement(container) ||
|
|
||||||
hasBoundTextElement(container) ||
|
hasBoundTextElement(container) ||
|
||||||
!isTransparent(container.backgroundColor) ||
|
!isTransparent(container.backgroundColor) ||
|
||||||
isHittingElementNotConsideringBoundingBox(container, this.state, [
|
isHittingElementNotConsideringBoundingBox(container, this.state, [
|
||||||
@@ -6235,6 +6248,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionGroup,
|
actionGroup,
|
||||||
actionUnbindText,
|
actionUnbindText,
|
||||||
actionBindText,
|
actionBindText,
|
||||||
|
actionCreateContainerFromText,
|
||||||
actionUngroup,
|
actionUngroup,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { t } from "../i18n";
|
||||||
|
const BraveMeasureTextError = () => {
|
||||||
|
return (
|
||||||
|
<div data-testid="brave-measure-text-error">
|
||||||
|
<p>
|
||||||
|
{t("errors.brave_measure_text_error.start")}
|
||||||
|
<span style={{ fontWeight: 600 }}>
|
||||||
|
{t("errors.brave_measure_text_error.aggressive_block_fingerprint")}
|
||||||
|
</span>{" "}
|
||||||
|
{t("errors.brave_measure_text_error.setting_enabled")}.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{t("errors.brave_measure_text_error.break")}{" "}
|
||||||
|
<span style={{ fontWeight: 600 }}>
|
||||||
|
{t("errors.brave_measure_text_error.text_elements")}
|
||||||
|
</span>{" "}
|
||||||
|
{t("errors.brave_measure_text_error.in_your_drawings")}.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{t("errors.brave_measure_text_error.strongly_recommend")}{" "}
|
||||||
|
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
|
||||||
|
{" "}
|
||||||
|
{t("errors.brave_measure_text_error.steps")}
|
||||||
|
</a>{" "}
|
||||||
|
{t("errors.brave_measure_text_error.how")}.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{t("errors.brave_measure_text_error.disable_setting")}{" "}
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/issues/new">
|
||||||
|
{t("errors.brave_measure_text_error.issue")}
|
||||||
|
</a>{" "}
|
||||||
|
{t("errors.brave_measure_text_error.write")}{" "}
|
||||||
|
<a href="https://discord.gg/UexuTaE">
|
||||||
|
{t("errors.brave_measure_text_error.discord")}
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BraveMeasureTextError;
|
||||||
@@ -6,6 +6,7 @@ import DialogActionButton from "./DialogActionButton";
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||||
import { useExcalidrawSetAppState } from "./App";
|
import { useExcalidrawSetAppState } from "./App";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
|
||||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
@@ -24,7 +25,7 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { AppState } from "../types";
|
|||||||
import { queryFocusableElements } from "../utils";
|
import { queryFocusableElements } from "../utils";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -72,7 +73,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
}, [islandNode, props.autofocus]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { Dialog } from "./Dialog";
|
|||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
export const ErrorDialog = ({
|
export const ErrorDialog = ({
|
||||||
message,
|
children,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
children?: React.ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(!!message);
|
const [modalIsShown, setModalIsShown] = useState(!!children);
|
||||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
@@ -32,7 +32,7 @@ export const ErrorDialog = ({
|
|||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("errorDialog.title")}
|
title={t("errorDialog.title")}
|
||||||
>
|
>
|
||||||
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div>
|
<div style={{ whiteSpace: "pre-wrap" }}>{children}</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: var(--preview-padding);
|
padding: var(--preview-padding);
|
||||||
margin-bottom: calc(var(--space-factor) * 3);
|
margin-bottom: calc(var(--space-factor) * 3);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ExportDialog__preview canvas {
|
.ExportDialog__preview canvas {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { t } from "../i18n";
|
||||||
import { HelpIcon } from "./icons";
|
import { HelpIcon } from "./icons";
|
||||||
|
|
||||||
type HelpButtonProps = {
|
type HelpButtonProps = {
|
||||||
title?: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
@@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => (
|
|||||||
className="help-icon"
|
className="help-icon"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
type="button"
|
type="button"
|
||||||
title={`${props.title} — ?`}
|
title={`${t("helpDialog.title")} — ?`}
|
||||||
aria-label={props.title}
|
aria-label={t("helpDialog.title")}
|
||||||
>
|
>
|
||||||
{HelpIcon}
|
{HelpIcon}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -364,10 +364,9 @@ const LayerUI = ({
|
|||||||
|
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
{appState.errorMessage && (
|
{appState.errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog onClose={() => setAppState({ errorMessage: null })}>
|
||||||
message={appState.errorMessage}
|
{appState.errorMessage}
|
||||||
onClose={() => setAppState({ errorMessage: null })}
|
</ErrorDialog>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{appState.openDialog === "help" && (
|
{appState.openDialog === "help" && (
|
||||||
<HelpDialog
|
<HelpDialog
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
||||||
isLibraryMenuOpenAtom,
|
isLibraryMenuOpenAtom,
|
||||||
|
jotaiScope,
|
||||||
);
|
);
|
||||||
const renderRemoveLibAlert = useCallback(() => {
|
const renderRemoveLibAlert = useCallback(() => {
|
||||||
const content = selectedItems.length
|
const content = selectedItems.length
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
|
||||||
|
<div
|
||||||
|
data-testid="brave-measure-text-error"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Looks like you are using Brave browser with the
|
||||||
|
|
||||||
|
<span
|
||||||
|
style="font-weight: 600;"
|
||||||
|
>
|
||||||
|
Aggressively Block Fingerprinting
|
||||||
|
</span>
|
||||||
|
|
||||||
|
setting enabled
|
||||||
|
.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
This could result in breaking the
|
||||||
|
|
||||||
|
<span
|
||||||
|
style="font-weight: 600;"
|
||||||
|
>
|
||||||
|
Text Elements
|
||||||
|
</span>
|
||||||
|
|
||||||
|
in your drawings
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We strongly recommend disabling this setting. You can follow
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
|
||||||
|
>
|
||||||
|
|
||||||
|
these steps
|
||||||
|
</a>
|
||||||
|
|
||||||
|
on how to do so
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If disabling this setting doesn't fix the display of text elements, please open an
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://github.com/excalidraw/excalidraw/issues/new"
|
||||||
|
>
|
||||||
|
issue
|
||||||
|
</a>
|
||||||
|
|
||||||
|
on our GitHub, or write us on
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://discord.gg/UexuTaE"
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
useExcalidrawAppState,
|
useExcalidrawAppState,
|
||||||
useExcalidrawSetAppState,
|
useExcalidrawSetAppState,
|
||||||
@@ -31,11 +31,10 @@ import "./DefaultItems.scss";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
|
import { jotaiScope } from "../../jotai";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||||
@@ -57,9 +56,7 @@ export const LoadScene = () => {
|
|||||||
LoadScene.displayName = "LoadScene";
|
LoadScene.displayName = "LoadScene";
|
||||||
|
|
||||||
export const SaveToActiveFile = () => {
|
export const SaveToActiveFile = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||||
@@ -80,9 +77,7 @@ SaveToActiveFile.displayName = "SaveToActiveFile";
|
|||||||
|
|
||||||
export const SaveAsImage = () => {
|
export const SaveAsImage = () => {
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={ExportImageIcon}
|
icon={ExportImageIcon}
|
||||||
@@ -98,9 +93,7 @@ export const SaveAsImage = () => {
|
|||||||
SaveAsImage.displayName = "SaveAsImage";
|
SaveAsImage.displayName = "SaveAsImage";
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
@@ -119,10 +112,12 @@ export const Help = () => {
|
|||||||
Help.displayName = "Help";
|
Help.displayName = "Help";
|
||||||
|
|
||||||
export const ClearCanvas = () => {
|
export const ClearCanvas = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
const setActiveConfirmDialog = useSetAtom(
|
||||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
activeConfirmDialogAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||||
@@ -143,6 +138,7 @@ export const ClearCanvas = () => {
|
|||||||
ClearCanvas.displayName = "ClearCanvas";
|
ClearCanvas.displayName = "ClearCanvas";
|
||||||
|
|
||||||
export const ToggleTheme = () => {
|
export const ToggleTheme = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
@@ -175,6 +171,7 @@ export const ToggleTheme = () => {
|
|||||||
ToggleTheme.displayName = "ToggleTheme";
|
ToggleTheme.displayName = "ToggleTheme";
|
||||||
|
|
||||||
export const ChangeCanvasBackground = () => {
|
export const ChangeCanvasBackground = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
@@ -195,9 +192,7 @@ export const ChangeCanvasBackground = () => {
|
|||||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||||
|
|
||||||
export const Export = () => {
|
export const Export = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -248,9 +243,7 @@ export const LiveCollaborationTrigger = ({
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
data-testid="collab-button"
|
data-testid="collab-button"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { t } from "../../i18n";
|
import { t, useI18n } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
useDevice,
|
useDevice,
|
||||||
useExcalidrawActionManager,
|
useExcalidrawActionManager,
|
||||||
@@ -172,10 +172,7 @@ const MenuItemLiveCollaborationTrigger = ({
|
|||||||
}: {
|
}: {
|
||||||
onSelect: () => any;
|
onSelect: () => any;
|
||||||
}) => {
|
}) => {
|
||||||
// FIXME when we tie t() to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||||
{t("labels.liveCollaboration")}
|
{t("labels.liveCollaboration")}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ export const isFirefox =
|
|||||||
"netscape" in window &&
|
"netscape" in window &&
|
||||||
navigator.userAgent.indexOf("rv:") > 1 &&
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||||
navigator.userAgent.indexOf("Gecko") > 1;
|
navigator.userAgent.indexOf("Gecko") > 1;
|
||||||
|
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
|
||||||
|
export const isSafari =
|
||||||
|
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
||||||
|
// keeping function so it can be mocked in test
|
||||||
|
export const isBrave = () =>
|
||||||
|
(navigator as any).brave?.isBrave?.name === "isBrave";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -530,6 +530,7 @@
|
|||||||
// (doesn't work in Firefox)
|
// (doesn't work in Firefox)
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@@ -567,8 +568,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar--mobile {
|
.App-toolbar--mobile {
|
||||||
overflow-x: hidden;
|
overflow-x: auto;
|
||||||
max-width: 100vw;
|
max-width: 90vw;
|
||||||
|
|
||||||
.ToolIcon__keybinding {
|
.ToolIcon__keybinding {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
+2
-1
@@ -7,6 +7,7 @@ import { CanvasError } from "../errors";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState, DataURL, LibraryItem } from "../types";
|
import { AppState, DataURL, LibraryItem } from "../types";
|
||||||
|
import { ValueOf } from "../utility-types";
|
||||||
import { bytesToHexString } from "../utils";
|
import { bytesToHexString } from "../utils";
|
||||||
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
||||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||||
@@ -156,7 +157,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
{ repairBindings: true },
|
{ repairBindings: true, refreshDimensions: true },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
|||||||
+1
-1
@@ -34,6 +34,7 @@ import { bumpVersion } from "../element/mutateElement";
|
|||||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -171,7 +172,6 @@ const restoreElement = (
|
|||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
text: element.text ?? "",
|
text: element.text ?? "",
|
||||||
baseline: element.baseline,
|
|
||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: element.containerId ?? null,
|
containerId: element.containerId ?? null,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
export type Bounds = readonly [number, number, number, number];
|
export type Bounds = readonly [number, number, number, number];
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { isTextElement } from ".";
|
|||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
import { shouldShowBoundingBox } from "./transformHandles";
|
import { shouldShowBoundingBox } from "./transformHandles";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
const isElementDraggableFromInside = (
|
const isElementDraggableFromInside = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { shouldRotateWithDiscreteAngle } from "../keys";
|
|||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getSizeFromPoints } from "../points";
|
|||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
|
|||||||
@@ -22,16 +22,17 @@ import { getElementAbsoluteCoords } from ".";
|
|||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
|
||||||
getBoundTextElementOffset,
|
getBoundTextElementOffset,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureText,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
getMaxContainerWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@@ -153,7 +154,6 @@ export const newTextElement = (
|
|||||||
y: opts.y - offsets.y,
|
y: opts.y - offsets.y,
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
baseline: metrics.baseline,
|
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
},
|
},
|
||||||
@@ -170,18 +170,13 @@ const getAdjustedDimensions = (
|
|||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
baseline: number;
|
|
||||||
} => {
|
} => {
|
||||||
let maxWidth = null;
|
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
if (container) {
|
|
||||||
maxWidth = getMaxContainerWidth(container);
|
const { width: nextWidth, height: nextHeight } = measureText(
|
||||||
}
|
nextText,
|
||||||
const {
|
getFontString(element),
|
||||||
width: nextWidth,
|
);
|
||||||
height: nextHeight,
|
|
||||||
baseline: nextBaseline,
|
|
||||||
} = measureText(nextText, getFontString(element), maxWidth);
|
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
let x: number;
|
let x: number;
|
||||||
let y: number;
|
let y: number;
|
||||||
@@ -190,11 +185,7 @@ const getAdjustedDimensions = (
|
|||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(
|
const prevMetrics = measureText(element.text, getFontString(element));
|
||||||
element.text,
|
|
||||||
getFontString(element),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
@@ -258,7 +249,6 @@ const getAdjustedDimensions = (
|
|||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
x: Number.isFinite(x) ? x : element.x,
|
x: Number.isFinite(x) ? x : element.x,
|
||||||
y: Number.isFinite(y) ? y : element.y,
|
y: Number.isFinite(y) ? y : element.y,
|
||||||
baseline: nextBaseline,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,38 +268,6 @@ export const refreshTextDimensions = (
|
|||||||
return { text, ...dimensions };
|
return { text, ...dimensions };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
|
||||||
const width = getContainerDims(container).width;
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
if (containerWidth <= 0) {
|
|
||||||
const boundText = getBoundTextElement(container);
|
|
||||||
if (boundText) {
|
|
||||||
return boundText.width;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
|
||||||
return containerWidth;
|
|
||||||
}
|
|
||||||
return width - BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
|
||||||
const height = getContainerDims(container).height;
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
if (containerHeight <= 0) {
|
|
||||||
const boundText = getBoundTextElement(container);
|
|
||||||
if (boundText) {
|
|
||||||
return boundText.height;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
return height - BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTextElement = (
|
export const updateTextElement = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,12 +43,10 @@ import {
|
|||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getBoundTextElementOffset,
|
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
measureText,
|
getMaxContainerWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { getMaxContainerWidth } from "./newElement";
|
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle >= 2 * Math.PI) {
|
if (angle >= 2 * Math.PI) {
|
||||||
@@ -192,11 +190,10 @@ const rescalePointsInElement = (
|
|||||||
|
|
||||||
const MIN_FONT_SIZE = 1;
|
const MIN_FONT_SIZE = 1;
|
||||||
|
|
||||||
const measureFontSizeFromWH = (
|
const measureFontSizeFromWidth = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
): number | null => {
|
||||||
): { size: number; baseline: number } | null => {
|
|
||||||
// We only use width to scale font on resize
|
// We only use width to scale font on resize
|
||||||
let width = element.width;
|
let width = element.width;
|
||||||
|
|
||||||
@@ -211,15 +208,8 @@ const measureFontSizeFromWH = (
|
|||||||
if (nextFontSize < MIN_FONT_SIZE) {
|
if (nextFontSize < MIN_FONT_SIZE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
|
||||||
element.text,
|
return nextFontSize;
|
||||||
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
|
||||||
element.containerId ? width : null,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
size: nextFontSize,
|
|
||||||
baseline: metrics.baseline + (nextHeight - metrics.height),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSidesForTransformHandle = (
|
const getSidesForTransformHandle = (
|
||||||
@@ -290,8 +280,8 @@ const resizeSingleTextElement = (
|
|||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
const nextWidth = element.width * scale;
|
const nextWidth = element.width * scale;
|
||||||
const nextHeight = element.height * scale;
|
const nextHeight = element.height * scale;
|
||||||
const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
|
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
|
||||||
if (nextFont === null) {
|
if (nextFontSize === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||||
@@ -315,10 +305,9 @@ const resizeSingleTextElement = (
|
|||||||
deltaY2,
|
deltaY2,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
fontSize: nextFont.size,
|
fontSize: nextFontSize,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
baseline: nextFont.baseline,
|
|
||||||
x: nextElementX,
|
x: nextElementX,
|
||||||
y: nextElementY,
|
y: nextElementY,
|
||||||
});
|
});
|
||||||
@@ -371,7 +360,7 @@ export const resizeSingleElement = (
|
|||||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||||
|
|
||||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
let boundTextFont: { fontSize?: number } = {};
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
@@ -423,23 +412,24 @@ export const resizeSingleElement = (
|
|||||||
if (stateOfBoundTextElementAtResize) {
|
if (stateOfBoundTextElementAtResize) {
|
||||||
boundTextFont = {
|
boundTextFont = {
|
||||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||||
baseline: stateOfBoundTextElementAtResize.baseline,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (shouldMaintainAspectRatio) {
|
if (shouldMaintainAspectRatio) {
|
||||||
const boundTextElementPadding =
|
const updatedElement = {
|
||||||
getBoundTextElementOffset(boundTextElement);
|
...element,
|
||||||
const nextFont = measureFontSizeFromWH(
|
width: eleNewWidth,
|
||||||
|
height: eleNewHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextFontSize = measureFontSizeFromWidth(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
eleNewWidth - boundTextElementPadding * 2,
|
getMaxContainerWidth(updatedElement),
|
||||||
eleNewHeight - boundTextElementPadding * 2,
|
|
||||||
);
|
);
|
||||||
if (nextFont === null) {
|
if (nextFontSize === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boundTextFont = {
|
boundTextFont = {
|
||||||
fontSize: nextFont.size,
|
fontSize: nextFontSize,
|
||||||
baseline: nextFont.baseline,
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
||||||
@@ -683,7 +673,6 @@ const resizeMultipleElements = (
|
|||||||
y: number;
|
y: number;
|
||||||
points?: Point[];
|
points?: Point[];
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
baseline?: number;
|
|
||||||
} = {
|
} = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -692,31 +681,34 @@ const resizeMultipleElements = (
|
|||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
let boundTextUpdates: { fontSize: number } | null = null;
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element.latest);
|
const boundTextElement = getBoundTextElement(element.latest);
|
||||||
|
|
||||||
if (boundTextElement || isTextElement(element.orig)) {
|
if (boundTextElement || isTextElement(element.orig)) {
|
||||||
const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2;
|
const updatedElement = {
|
||||||
const textMeasurements = measureFontSizeFromWH(
|
...element.latest,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
const fontSize = measureFontSizeFromWidth(
|
||||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||||
width - optionalPadding,
|
boundTextElement
|
||||||
height - optionalPadding,
|
? getMaxContainerWidth(updatedElement)
|
||||||
|
: updatedElement.width,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!textMeasurements) {
|
if (!fontSize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element.orig)) {
|
if (isTextElement(element.orig)) {
|
||||||
update.fontSize = textMeasurements.size;
|
update.fontSize = fontSize;
|
||||||
update.baseline = textMeasurements.baseline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
boundTextUpdates = {
|
boundTextUpdates = {
|
||||||
fontSize: textMeasurements.size,
|
fontSize,
|
||||||
baseline: textMeasurements.baseline,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-31
@@ -1,5 +1,12 @@
|
|||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { BOUND_TEXT_PADDING } from "../constants";
|
||||||
import { measureText, wrapText } from "./textElement";
|
import { API } from "../tests/helpers/api";
|
||||||
|
import {
|
||||||
|
computeContainerDimensionForBoundText,
|
||||||
|
getContainerCoords,
|
||||||
|
getMaxContainerWidth,
|
||||||
|
getMaxContainerHeight,
|
||||||
|
wrapText,
|
||||||
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
describe("Test wrapText", () => {
|
describe("Test wrapText", () => {
|
||||||
@@ -9,7 +16,7 @@ describe("Test wrapText", () => {
|
|||||||
const text = "Hello whats up ";
|
const text = "Hello whats up ";
|
||||||
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
||||||
const res = wrapText(text, font, maxWidth);
|
const res = wrapText(text, font, maxWidth);
|
||||||
expect(res).toBe("Hello whats up ");
|
expect(res).toBe(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work with emojis", () => {
|
it("should work with emojis", () => {
|
||||||
@@ -19,7 +26,7 @@ describe("Test wrapText", () => {
|
|||||||
expect(res).toBe("😀");
|
expect(res).toBe("😀");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the text correctly when min width reached", () => {
|
it("should show the text correctly when max width reached", () => {
|
||||||
const text = "Hello😀";
|
const text = "Hello😀";
|
||||||
const maxWidth = 10;
|
const maxWidth = 10;
|
||||||
const res = wrapText(text, font, maxWidth);
|
const res = wrapText(text, font, maxWidth);
|
||||||
@@ -28,10 +35,11 @@ describe("Test wrapText", () => {
|
|||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
describe("When text doesn't contain new lines", () => {
|
||||||
const text = "Hello whats up";
|
const text = "Hello whats up";
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 90,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello
|
||||||
whats
|
whats
|
||||||
up`,
|
up`,
|
||||||
@@ -55,7 +63,7 @@ p`,
|
|||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
|
|
||||||
width: 150,
|
width: 140,
|
||||||
res: `Hello whats
|
res: `Hello whats
|
||||||
up`,
|
up`,
|
||||||
},
|
},
|
||||||
@@ -65,6 +73,13 @@ up`,
|
|||||||
width: 250,
|
width: 250,
|
||||||
res: "Hello whats up",
|
res: "Hello whats up",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "should push the word if its equal to max width",
|
||||||
|
width: 60,
|
||||||
|
res: `Hello
|
||||||
|
whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should ${data.desc}`, () => {
|
it(`should ${data.desc}`, () => {
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
@@ -72,13 +87,14 @@ up`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("When text contain new lines", () => {
|
describe("When text contain new lines", () => {
|
||||||
const text = `Hello
|
const text = `Hello
|
||||||
whats up`;
|
whats up`;
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 90,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello
|
||||||
whats
|
whats
|
||||||
up`,
|
up`,
|
||||||
@@ -120,6 +136,7 @@ whats up`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("When text is long", () => {
|
describe("When text is long", () => {
|
||||||
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
||||||
[
|
[
|
||||||
@@ -159,38 +176,132 @@ break it now`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should wrap the text correctly when word length is exactly equal to max width", () => {
|
||||||
|
const text = "Hello Excalidraw";
|
||||||
|
// Length of "Excalidraw" is 100 and exacty equal to max width
|
||||||
|
const res = wrapText(text, font, 100);
|
||||||
|
expect(res).toEqual(`Hello
|
||||||
|
Excalidraw`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test measureText", () => {
|
describe("Test measureText", () => {
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
describe("Test getContainerCoords", () => {
|
||||||
const text = "Hello World";
|
const params = { width: 200, height: 100, x: 10, y: 20 };
|
||||||
|
|
||||||
it("should add correct attributes when maxWidth is passed", () => {
|
it("should compute coords correctly when ellipse", () => {
|
||||||
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
const element = API.createElement({
|
||||||
const res = measureText(text, font, maxWidth);
|
type: "ellipse",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(getContainerCoords(element)).toEqual({
|
||||||
|
x: 44.2893218813452455,
|
||||||
|
y: 39.64466094067262,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(res.container).toMatchInlineSnapshot(`
|
it("should compute coords correctly when rectangle", () => {
|
||||||
<div
|
const element = API.createElement({
|
||||||
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; max-width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
type: "rectangle",
|
||||||
>
|
...params,
|
||||||
<span
|
});
|
||||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
expect(getContainerCoords(element)).toEqual({
|
||||||
/>
|
x: 15,
|
||||||
</div>
|
y: 25,
|
||||||
`);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute coords correctly when diamond", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "diamond",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(getContainerCoords(element)).toEqual({
|
||||||
|
x: 65,
|
||||||
|
y: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add correct attributes when maxWidth is not passed", () => {
|
describe("Test computeContainerDimensionForBoundText", () => {
|
||||||
const res = measureText(text, font);
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
expect(res.container).toMatchInlineSnapshot(`
|
it("should compute container height correctly for rectangle", () => {
|
||||||
<div
|
const element = API.createElement({
|
||||||
style="position: absolute; white-space: pre; font: Emoji 20px 20px; min-height: 1em;"
|
type: "rectangle",
|
||||||
>
|
...params,
|
||||||
<span
|
});
|
||||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
/>
|
160,
|
||||||
</div>
|
);
|
||||||
`);
|
});
|
||||||
|
|
||||||
|
it("should compute container height correctly for ellipse", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "ellipse",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
|
226,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute container height correctly for diamond", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "diamond",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
|
320,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getMaxContainerWidth", () => {
|
||||||
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return max width when container is rectangle", () => {
|
||||||
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(168);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max width when container is ellipse", () => {
|
||||||
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(116);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max width when container is diamond", () => {
|
||||||
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(79);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getMaxContainerHeight", () => {
|
||||||
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return max height when container is rectangle", () => {
|
||||||
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(184);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is ellipse", () => {
|
||||||
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(127);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is diamond", () => {
|
||||||
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(87);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+276
-178
@@ -8,16 +8,17 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
TEXT_ALIGN,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
|
} from "../constants";
|
||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||||
import {
|
|
||||||
isBoundToContainer,
|
|
||||||
isImageElement,
|
|
||||||
isArrowElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isTextBindableContainer } from "./typeChecks";
|
import { isTextBindableContainer } from "./typeChecks";
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
|
import { ExtractSetType } from "../utility-types";
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
@@ -44,68 +46,66 @@ export const redrawTextBoundingBox = (
|
|||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
let text = textElement.text;
|
|
||||||
|
const boundTextUpdates = {
|
||||||
|
x: textElement.x,
|
||||||
|
y: textElement.y,
|
||||||
|
text: textElement.text,
|
||||||
|
width: textElement.width,
|
||||||
|
height: textElement.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
boundTextUpdates.text = textElement.text;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getMaxContainerWidth(container);
|
||||||
text = wrapText(
|
boundTextUpdates.text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const metrics = measureText(text, getFontString(textElement), maxWidth);
|
const metrics = measureText(
|
||||||
let coordY = textElement.y;
|
boundTextUpdates.text,
|
||||||
let coordX = textElement.x;
|
getFontString(textElement),
|
||||||
// Resize container and vertically center align the text
|
);
|
||||||
|
|
||||||
|
boundTextUpdates.width = metrics.width;
|
||||||
|
boundTextUpdates.height = metrics.height;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
if (!isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerDims = getContainerDims(container);
|
|
||||||
let nextHeight = containerDims.height;
|
|
||||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
|
||||||
coordY = container.y;
|
|
||||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
|
||||||
coordY =
|
|
||||||
container.y +
|
|
||||||
containerDims.height -
|
|
||||||
metrics.height -
|
|
||||||
BOUND_TEXT_PADDING;
|
|
||||||
} else {
|
|
||||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
|
||||||
if (metrics.height > getMaxContainerHeight(container)) {
|
|
||||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
|
||||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
|
||||||
coordX = container.x + BOUND_TEXT_PADDING;
|
|
||||||
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
|
||||||
coordX =
|
|
||||||
container.x +
|
|
||||||
containerDims.width -
|
|
||||||
metrics.width -
|
|
||||||
BOUND_TEXT_PADDING;
|
|
||||||
} else {
|
|
||||||
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
|
|
||||||
}
|
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
|
||||||
mutateElement(container, { height: nextHeight });
|
|
||||||
} else {
|
|
||||||
const centerX = textElement.x + textElement.width / 2;
|
const centerX = textElement.x + textElement.width / 2;
|
||||||
const centerY = textElement.y + textElement.height / 2;
|
const centerY = textElement.y + textElement.height / 2;
|
||||||
const diffWidth = metrics.width - textElement.width;
|
const diffWidth = metrics.width - textElement.width;
|
||||||
const diffHeight = metrics.height - textElement.height;
|
const diffHeight = metrics.height - textElement.height;
|
||||||
coordY = centerY - (textElement.height + diffHeight) / 2;
|
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
|
||||||
coordX = centerX - (textElement.width + diffWidth) / 2;
|
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
|
||||||
|
} else {
|
||||||
|
const containerDims = getContainerDims(container);
|
||||||
|
let maxContainerHeight = getMaxContainerHeight(container);
|
||||||
|
|
||||||
|
let nextHeight = containerDims.height;
|
||||||
|
if (metrics.height > maxContainerHeight) {
|
||||||
|
nextHeight = computeContainerDimensionForBoundText(
|
||||||
|
metrics.height,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
mutateElement(container, { height: nextHeight });
|
||||||
|
maxContainerHeight = getMaxContainerHeight(container);
|
||||||
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
|
}
|
||||||
|
const updatedTextElement = {
|
||||||
|
...textElement,
|
||||||
|
...boundTextUpdates,
|
||||||
|
} as ExcalidrawTextElementWithContainer;
|
||||||
|
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
|
||||||
|
boundTextUpdates.x = x;
|
||||||
|
boundTextUpdates.y = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutateElement(textElement, {
|
|
||||||
width: metrics.width,
|
mutateElement(textElement, boundTextUpdates);
|
||||||
height: metrics.height,
|
|
||||||
baseline: metrics.baseline,
|
|
||||||
y: coordY,
|
|
||||||
x: coordX,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindTextToShapeAfterDuplication = (
|
export const bindTextToShapeAfterDuplication = (
|
||||||
@@ -177,7 +177,6 @@ export const handleBindTextResize = (
|
|||||||
const maxWidth = getMaxContainerWidth(container);
|
const maxWidth = getMaxContainerWidth(container);
|
||||||
const maxHeight = getMaxContainerHeight(container);
|
const maxHeight = getMaxContainerHeight(container);
|
||||||
let containerHeight = containerDims.height;
|
let containerHeight = containerDims.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
@@ -186,18 +185,17 @@ export const handleBindTextResize = (
|
|||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = measureText(
|
const dimensions = measureText(text, getFontString(textElement));
|
||||||
text,
|
|
||||||
getFontString(textElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
nextHeight = dimensions.height;
|
nextHeight = dimensions.height;
|
||||||
nextWidth = dimensions.width;
|
nextWidth = dimensions.width;
|
||||||
nextBaseLine = dimensions.baseline;
|
|
||||||
}
|
}
|
||||||
// increase height in case text element height exceeds
|
// increase height in case text element height exceeds
|
||||||
if (nextHeight > maxHeight) {
|
if (nextHeight > maxHeight) {
|
||||||
containerHeight = nextHeight + getBoundTextElementOffset(textElement) * 2;
|
containerHeight = computeContainerDimensionForBoundText(
|
||||||
|
nextHeight,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
|
||||||
const diff = containerHeight - containerDims.height;
|
const diff = containerHeight - containerDims.height;
|
||||||
// fix the y coord when resizing from ne/nw/n
|
// fix the y coord when resizing from ne/nw/n
|
||||||
const updatedY =
|
const updatedY =
|
||||||
@@ -217,94 +215,64 @@ export const handleBindTextResize = (
|
|||||||
text,
|
text,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
|
|
||||||
baseline: nextBaseLine,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
updateBoundTextPosition(
|
mutateElement(
|
||||||
container,
|
textElement,
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
computeBoundTextPosition(
|
||||||
|
container,
|
||||||
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBoundTextPosition = (
|
const computeBoundTextPosition = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
) => {
|
) => {
|
||||||
const containerDims = getContainerDims(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
const boundTextElementPadding = getBoundTextElementOffset(boundTextElement);
|
const maxContainerHeight = getMaxContainerHeight(container);
|
||||||
|
const maxContainerWidth = getMaxContainerWidth(container);
|
||||||
|
|
||||||
|
let x;
|
||||||
let y;
|
let y;
|
||||||
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
y = container.y + boundTextElementPadding;
|
y = containerCoords.y;
|
||||||
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
y =
|
y = containerCoords.y + (maxContainerHeight - boundTextElement.height);
|
||||||
container.y +
|
|
||||||
containerDims.height -
|
|
||||||
boundTextElement.height -
|
|
||||||
boundTextElementPadding;
|
|
||||||
} else {
|
} else {
|
||||||
y = container.y + containerDims.height / 2 - boundTextElement.height / 2;
|
y =
|
||||||
|
containerCoords.y +
|
||||||
|
(maxContainerHeight / 2 - boundTextElement.height / 2);
|
||||||
}
|
}
|
||||||
const x =
|
if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||||
boundTextElement.textAlign === TEXT_ALIGN.LEFT
|
x = containerCoords.x;
|
||||||
? container.x + boundTextElementPadding
|
} else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
: boundTextElement.textAlign === TEXT_ALIGN.RIGHT
|
x = containerCoords.x + (maxContainerWidth - boundTextElement.width);
|
||||||
? container.x +
|
} else {
|
||||||
containerDims.width -
|
x =
|
||||||
boundTextElement.width -
|
containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2);
|
||||||
boundTextElementPadding
|
}
|
||||||
: container.x + containerDims.width / 2 - boundTextElement.width / 2;
|
return { x, y };
|
||||||
|
|
||||||
mutateElement(boundTextElement, { x, y });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||||
export const measureText = (
|
|
||||||
text: string,
|
export const measureText = (text: string, font: FontString) => {
|
||||||
font: FontString,
|
|
||||||
maxWidth?: number | null,
|
|
||||||
) => {
|
|
||||||
text = text
|
text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const container = document.createElement("div");
|
|
||||||
container.style.position = "absolute";
|
|
||||||
container.style.whiteSpace = "pre";
|
|
||||||
container.style.font = font;
|
|
||||||
container.style.minHeight = "1em";
|
|
||||||
|
|
||||||
if (maxWidth) {
|
const height = getTextHeight(text, font);
|
||||||
const lineHeight = getApproxLineHeight(font);
|
const width = getTextWidth(text, font);
|
||||||
// since we are adding a span of width 1px later
|
|
||||||
container.style.maxWidth = `${maxWidth + 1}px`;
|
|
||||||
container.style.overflow = "hidden";
|
|
||||||
container.style.wordBreak = "break-word";
|
|
||||||
container.style.lineHeight = `${String(lineHeight)}px`;
|
|
||||||
container.style.whiteSpace = "pre-wrap";
|
|
||||||
}
|
|
||||||
document.body.appendChild(container);
|
|
||||||
container.innerText = text;
|
|
||||||
|
|
||||||
const span = document.createElement("span");
|
return { width, height };
|
||||||
span.style.display = "inline-block";
|
|
||||||
span.style.overflow = "hidden";
|
|
||||||
span.style.width = "1px";
|
|
||||||
span.style.height = "1px";
|
|
||||||
container.appendChild(span);
|
|
||||||
// Baseline is important for positioning text on canvas
|
|
||||||
const baseline = span.offsetTop + span.offsetHeight;
|
|
||||||
// since we are adding a span of width 1px
|
|
||||||
const width = container.offsetWidth + 1;
|
|
||||||
const height = container.offsetHeight;
|
|
||||||
document.body.removeChild(container);
|
|
||||||
if (isTestEnv()) {
|
|
||||||
return { width, height, baseline, container };
|
|
||||||
}
|
|
||||||
return { width, height, baseline };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
@@ -314,71 +282,97 @@ export const getApproxLineHeight = (font: FontString) => {
|
|||||||
if (cacheApproxLineHeight[font]) {
|
if (cacheApproxLineHeight[font]) {
|
||||||
return cacheApproxLineHeight[font];
|
return cacheApproxLineHeight[font];
|
||||||
}
|
}
|
||||||
cacheApproxLineHeight[font] = measureText(DUMMY_TEXT, font, null).height;
|
const fontSize = parseInt(font);
|
||||||
|
|
||||||
|
// Calculate line height relative to font size
|
||||||
|
cacheApproxLineHeight[font] = fontSize * 1.2;
|
||||||
return cacheApproxLineHeight[font];
|
return cacheApproxLineHeight[font];
|
||||||
};
|
};
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
const getLineWidth = (text: string, font: FontString) => {
|
const getLineWidth = (text: string, font: FontString) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement("canvas");
|
canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
const canvas2dContext = canvas.getContext("2d")!;
|
const canvas2dContext = canvas.getContext("2d")!;
|
||||||
canvas2dContext.font = font;
|
canvas2dContext.font = font;
|
||||||
|
const width = canvas2dContext.measureText(text).width;
|
||||||
|
|
||||||
const metrics = canvas2dContext.measureText(text);
|
|
||||||
// since in test env the canvas measureText algo
|
// since in test env the canvas measureText algo
|
||||||
// doesn't measure text and instead just returns number of
|
// doesn't measure text and instead just returns number of
|
||||||
// characters hence we assume that each letteris 10px
|
// characters hence we assume that each letteris 10px
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return metrics.width * 10;
|
return width * 10;
|
||||||
}
|
}
|
||||||
// Since measureText behaves differently in different browsers
|
return width;
|
||||||
// OS so considering a adjustment factor of 0.2
|
|
||||||
const adjustmentFactor = 0.2;
|
|
||||||
|
|
||||||
return metrics.width + adjustmentFactor;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
const lines = text.split("\n");
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
let width = 0;
|
let width = 0;
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
});
|
});
|
||||||
return width;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTextHeight = (text: string, font: FontString) => {
|
||||||
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
|
const lineHeight = getApproxLineHeight(font);
|
||||||
|
return lineHeight * lines.length;
|
||||||
|
};
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
|
let currentLine = "";
|
||||||
|
let currentLineWidthTillNow = 0;
|
||||||
|
|
||||||
const push = (str: string) => {
|
const push = (str: string) => {
|
||||||
if (str.trim()) {
|
if (str.trim()) {
|
||||||
lines.push(str);
|
lines.push(str);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetParams = () => {
|
||||||
|
currentLine = "";
|
||||||
|
currentLineWidthTillNow = 0;
|
||||||
|
};
|
||||||
|
|
||||||
originalLines.forEach((originalLine) => {
|
originalLines.forEach((originalLine) => {
|
||||||
const words = originalLine.split(" ");
|
const currentLineWidth = getTextWidth(originalLine, font);
|
||||||
// This means its newline so push it
|
|
||||||
if (words.length === 1 && words[0] === "") {
|
//Push the line if its <= maxWidth
|
||||||
lines.push(words[0]);
|
if (currentLineWidth <= maxWidth) {
|
||||||
|
lines.push(originalLine);
|
||||||
return; // continue
|
return; // continue
|
||||||
}
|
}
|
||||||
let currentLine = "";
|
const words = originalLine.split(" ");
|
||||||
let currentLineWidthTillNow = 0;
|
|
||||||
|
resetParams();
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
while (index < words.length) {
|
while (index < words.length) {
|
||||||
const currentWordWidth = getLineWidth(words[index], font);
|
const currentWordWidth = getLineWidth(words[index], font);
|
||||||
|
|
||||||
|
// This will only happen when single word takes entire width
|
||||||
|
if (currentWordWidth === maxWidth) {
|
||||||
|
push(words[index]);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
// Start breaking longer words exceeding max width
|
||||||
if (currentWordWidth >= maxWidth) {
|
else if (currentWordWidth > maxWidth) {
|
||||||
// push current line since the current word exceeds the max width
|
// push current line since the current word exceeds the max width
|
||||||
// so will be appended in next line
|
// so will be appended in next line
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
resetParams();
|
||||||
|
|
||||||
while (words[index].length > 0) {
|
while (words[index].length > 0) {
|
||||||
const currentChar = String.fromCodePoint(
|
const currentChar = String.fromCodePoint(
|
||||||
words[index].codePointAt(0)!,
|
words[index].codePointAt(0)!,
|
||||||
@@ -388,10 +382,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
words[index] = words[index].slice(currentChar.length);
|
words[index] = words[index].slice(currentChar.length);
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
// only remove last trailing space which we have added when joining words
|
|
||||||
if (currentLine.slice(-1) === " ") {
|
|
||||||
currentLine = currentLine.slice(0, -1);
|
|
||||||
}
|
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLine = currentChar;
|
currentLine = currentChar;
|
||||||
currentLineWidthTillNow = width;
|
currentLineWidthTillNow = width;
|
||||||
@@ -399,11 +389,11 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
currentLine += currentChar;
|
currentLine += currentChar;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// push current line if appending space exceeds max width
|
// push current line if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLine = "";
|
resetParams();
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
} else {
|
} else {
|
||||||
// space needs to be appended before next word
|
// space needs to be appended before next word
|
||||||
// as currentLine contains chars which couldn't be appended
|
// as currentLine contains chars which couldn't be appended
|
||||||
@@ -411,7 +401,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
currentLineWidthTillNow += spaceWidth;
|
currentLineWidthTillNow += spaceWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
} else {
|
} else {
|
||||||
// Start appending words in a line till max width reached
|
// Start appending words in a line till max width reached
|
||||||
@@ -419,10 +408,9 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
const word = words[index];
|
const word = words[index];
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLineWidthTillNow = 0;
|
resetParams();
|
||||||
currentLine = "";
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -433,22 +421,15 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
const word = currentLine.slice(0, -1);
|
const word = currentLine.slice(0, -1);
|
||||||
push(word);
|
push(word);
|
||||||
currentLine = "";
|
resetParams();
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentLineWidthTillNow === maxWidth) {
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentLine) {
|
if (currentLine.slice(-1) === " ") {
|
||||||
// only remove last trailing space which we have added when joining words
|
// only remove last trailing space which we have added when joining words
|
||||||
if (currentLine.slice(-1) === " ") {
|
currentLine = currentLine.slice(0, -1);
|
||||||
currentLine = currentLine.slice(0, -1);
|
|
||||||
}
|
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -479,9 +460,9 @@ export const charWidth = (() => {
|
|||||||
getCache,
|
getCache,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
export const getApproxMinLineWidth = (font: FontString) => {
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
|
|
||||||
if (maxCharWidth === 0) {
|
if (maxCharWidth === 0) {
|
||||||
return (
|
return (
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
||||||
@@ -621,6 +602,26 @@ export const getContainerCenter = (
|
|||||||
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||||
|
let offsetX = BOUND_TEXT_PADDING;
|
||||||
|
let offsetY = BOUND_TEXT_PADDING;
|
||||||
|
|
||||||
|
if (container.type === "ellipse") {
|
||||||
|
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
|
offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2);
|
||||||
|
offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2);
|
||||||
|
}
|
||||||
|
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
|
if (container.type === "diamond") {
|
||||||
|
offsetX += container.width / 4;
|
||||||
|
offsetY += container.height / 4;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: container.x + offsetX,
|
||||||
|
y: container.y + offsetY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||||
const container = getContainerElement(textElement);
|
const container = getContainerElement(textElement);
|
||||||
if (!container || isArrowElement(container)) {
|
if (!container || isArrowElement(container)) {
|
||||||
@@ -633,12 +634,13 @@ export const getBoundTextElementOffset = (
|
|||||||
boundTextElement: ExcalidrawTextElement | null,
|
boundTextElement: ExcalidrawTextElement | null,
|
||||||
) => {
|
) => {
|
||||||
const container = getContainerElement(boundTextElement);
|
const container = getContainerElement(boundTextElement);
|
||||||
if (!container) {
|
if (!container || !boundTextElement) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
return BOUND_TEXT_PADDING * 8;
|
return BOUND_TEXT_PADDING * 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
return BOUND_TEXT_PADDING;
|
return BOUND_TEXT_PADDING;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -666,14 +668,24 @@ export const shouldAllowVerticalAlign = (
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
return false;
|
||||||
if (boundTextElement) {
|
});
|
||||||
if (isArrowElement(element)) {
|
};
|
||||||
|
|
||||||
|
export const suppportsHorizontalAlign = (
|
||||||
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
return selectedElements.some((element) => {
|
||||||
|
const hasBoundContainer = isBoundToContainer(element);
|
||||||
|
if (hasBoundContainer) {
|
||||||
|
const container = getContainerElement(element);
|
||||||
|
if (isTextElement(element) && isArrowElement(container)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return isTextElement(element);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -714,12 +726,98 @@ export const getTextBindableContainerAtPosition = (
|
|||||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidTextContainer = (element: ExcalidrawElement) => {
|
const VALID_CONTAINER_TYPES = new Set([
|
||||||
return (
|
"rectangle",
|
||||||
element.type === "rectangle" ||
|
"ellipse",
|
||||||
element.type === "ellipse" ||
|
"diamond",
|
||||||
element.type === "diamond" ||
|
"image",
|
||||||
isImageElement(element) ||
|
"arrow",
|
||||||
isArrowElement(element)
|
]);
|
||||||
);
|
|
||||||
|
export const isValidTextContainer = (element: ExcalidrawElement) =>
|
||||||
|
VALID_CONTAINER_TYPES.has(element.type);
|
||||||
|
|
||||||
|
export const computeContainerDimensionForBoundText = (
|
||||||
|
dimension: number,
|
||||||
|
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
||||||
|
) => {
|
||||||
|
dimension = Math.ceil(dimension);
|
||||||
|
const padding = BOUND_TEXT_PADDING * 2;
|
||||||
|
|
||||||
|
if (containerType === "ellipse") {
|
||||||
|
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
||||||
|
}
|
||||||
|
if (containerType === "arrow") {
|
||||||
|
return dimension + padding * 8;
|
||||||
|
}
|
||||||
|
if (containerType === "diamond") {
|
||||||
|
return 2 * (dimension + padding);
|
||||||
|
}
|
||||||
|
return dimension + padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||||
|
const width = getContainerDims(container).width;
|
||||||
|
if (isArrowElement(container)) {
|
||||||
|
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
const boundText = getBoundTextElement(container);
|
||||||
|
if (boundText) {
|
||||||
|
return boundText.width;
|
||||||
|
}
|
||||||
|
return BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
}
|
||||||
|
return containerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container.type === "ellipse") {
|
||||||
|
// The width of the largest rectangle inscribed inside an ellipse is
|
||||||
|
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
||||||
|
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
|
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
if (container.type === "diamond") {
|
||||||
|
// The width of the largest rectangle inscribed inside a rhombus is
|
||||||
|
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
|
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
return width - BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
||||||
|
const height = getContainerDims(container).height;
|
||||||
|
if (isArrowElement(container)) {
|
||||||
|
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
if (containerHeight <= 0) {
|
||||||
|
const boundText = getBoundTextElement(container);
|
||||||
|
if (boundText) {
|
||||||
|
return boundText.height;
|
||||||
|
}
|
||||||
|
return BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
if (container.type === "ellipse") {
|
||||||
|
// The height of the largest rectangle inscribed inside an ellipse is
|
||||||
|
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
|
||||||
|
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
|
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
if (container.type === "diamond") {
|
||||||
|
// The height of the largest rectangle inscribed inside a rhombus is
|
||||||
|
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
|
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
return height - BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMeasureTextSupported = () => {
|
||||||
|
const width = getTextWidth(
|
||||||
|
DUMMY_TEXT,
|
||||||
|
getFontString({
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return width > 0;
|
||||||
};
|
};
|
||||||
|
|||||||
+191
-142
@@ -3,19 +3,23 @@ import ExcalidrawApp from "../excalidraw-app";
|
|||||||
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
||||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { fireEvent } from "../tests/test-utils";
|
import {
|
||||||
|
fireEvent,
|
||||||
|
mockBoundingClientRect,
|
||||||
|
restoreOriginalGetBoundingClientRect,
|
||||||
|
} from "../tests/test-utils";
|
||||||
import { queryByText } from "@testing-library/react";
|
import { queryByText } from "@testing-library/react";
|
||||||
|
|
||||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import * as textElementUtils from "./textElement";
|
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { resize } from "../tests/utils";
|
import { resize } from "../tests/utils";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -222,11 +226,19 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
describe("Test container-unbound text", () => {
|
describe("Test container-unbound text", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
const dimensions = { height: 400, width: 800 };
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockBoundingClientRect(dimensions);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
|
//@ts-ignore
|
||||||
|
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||||
|
|
||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
@@ -236,6 +248,10 @@ describe("textWysiwyg", () => {
|
|||||||
)!;
|
)!;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
restoreOriginalGetBoundingClientRect();
|
||||||
|
});
|
||||||
|
|
||||||
it("should add a tab at the start of the first line", () => {
|
it("should add a tab at the start of the first line", () => {
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
textarea.value = "Line#1\nLine#2";
|
textarea.value = "Line#1\nLine#2";
|
||||||
@@ -434,23 +450,33 @@ describe("textWysiwyg", () => {
|
|||||||
);
|
);
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("text should never go beyond max width", async () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
|
textarea = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
)!;
|
||||||
|
fireEvent.change(textarea, {
|
||||||
|
target: {
|
||||||
|
value:
|
||||||
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new Event("input"));
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
textarea.blur();
|
||||||
|
expect(textarea.style.width).toBe("792px");
|
||||||
|
expect(h.elements[0].width).toBe(1000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test container-bound text", () => {
|
describe("Test container-bound text", () => {
|
||||||
let rectangle: any;
|
let rectangle: any;
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const DUMMY_HEIGHT = 240;
|
|
||||||
const DUMMY_WIDTH = 160;
|
|
||||||
const APPROX_LINE_HEIGHT = 25;
|
|
||||||
const INITIAL_WIDTH = 10;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest
|
|
||||||
.spyOn(textElementUtils, "getApproxLineHeight")
|
|
||||||
.mockReturnValue(APPROX_LINE_HEIGHT);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
h.elements = [];
|
h.elements = [];
|
||||||
@@ -643,11 +669,11 @@ describe("textWysiwyg", () => {
|
|||||||
["freedraw", "line"].forEach((type: any) => {
|
["freedraw", "line"].forEach((type: any) => {
|
||||||
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
||||||
h.elements = [];
|
h.elements = [];
|
||||||
const elemnet = UI.createElement(type, {
|
const element = UI.createElement(type, {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 50,
|
height: 50,
|
||||||
});
|
});
|
||||||
API.setSelectedElements([elemnet]);
|
API.setSelectedElements([element]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -732,39 +758,6 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||||
jest
|
|
||||||
.spyOn(textElementUtils, "measureText")
|
|
||||||
.mockImplementation((text, font, maxWidth) => {
|
|
||||||
let width = INITIAL_WIDTH;
|
|
||||||
let height = APPROX_LINE_HEIGHT;
|
|
||||||
let baseline = 10;
|
|
||||||
if (!text) {
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
baseline = 30;
|
|
||||||
width = DUMMY_WIDTH;
|
|
||||||
if (text === "Hello \nWorld!") {
|
|
||||||
height = APPROX_LINE_HEIGHT * 2;
|
|
||||||
}
|
|
||||||
if (maxWidth) {
|
|
||||||
width = maxWidth;
|
|
||||||
// To capture cases where maxWidth passed is initial width
|
|
||||||
// due to which the text is not wrapped correctly
|
|
||||||
if (maxWidth === INITIAL_WIDTH) {
|
|
||||||
height = DUMMY_HEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
|
|
||||||
Keyboard.keyDown(KEYS.ENTER);
|
Keyboard.keyDown(KEYS.ENTER);
|
||||||
@@ -773,11 +766,6 @@ describe("textWysiwyg", () => {
|
|||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
// mock scroll height
|
|
||||||
jest
|
|
||||||
.spyOn(editor, "scrollHeight", "get")
|
|
||||||
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
fireEvent.change(editor, {
|
||||||
target: {
|
target: {
|
||||||
value: "Hello World!",
|
value: "Hello World!",
|
||||||
@@ -792,11 +780,11 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.text).toBe("Hello \nWorld!");
|
expect(text.text).toBe("Hello \nWorld!");
|
||||||
expect(text.originalText).toBe("Hello World!");
|
expect(text.originalText).toBe("Hello World!");
|
||||||
expect(text.y).toBe(
|
expect(text.y).toBe(
|
||||||
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
);
|
);
|
||||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
expect(text.x).toBe(25);
|
||||||
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
|
expect(text.height).toBe(48);
|
||||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
expect(text.width).toBe(60);
|
||||||
|
|
||||||
// Edit and text by removing second line and it should
|
// Edit and text by removing second line and it should
|
||||||
// still vertically align correctly
|
// still vertically align correctly
|
||||||
@@ -813,11 +801,6 @@ describe("textWysiwyg", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// mock scroll height
|
|
||||||
jest
|
|
||||||
.spyOn(editor, "scrollHeight", "get")
|
|
||||||
.mockImplementation(() => APPROX_LINE_HEIGHT);
|
|
||||||
editor.style.height = "25px";
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -827,12 +810,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
expect(text.text).toBe("Hello");
|
expect(text.text).toBe("Hello");
|
||||||
expect(text.originalText).toBe("Hello");
|
expect(text.originalText).toBe("Hello");
|
||||||
|
expect(text.height).toBe(24);
|
||||||
|
expect(text.width).toBe(50);
|
||||||
expect(text.y).toBe(
|
expect(text.y).toBe(
|
||||||
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
);
|
);
|
||||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
expect(text.x).toBe(30);
|
||||||
expect(text.height).toBe(APPROX_LINE_HEIGHT);
|
|
||||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should unbind bound text when unbind action from context menu is triggered", async () => {
|
it("should unbind bound text when unbind action from context menu is triggered", async () => {
|
||||||
@@ -919,8 +902,8 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
109.5,
|
85,
|
||||||
17,
|
5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -934,6 +917,8 @@ describe("textWysiwyg", () => {
|
|||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -944,7 +929,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
90,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -967,7 +952,7 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
424,
|
375,
|
||||||
-539,
|
-539,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -1082,9 +1067,9 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
||||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||||
expect(rectangle.x).toBe(80);
|
expect(rectangle.x).toBe(80);
|
||||||
expect(rectangle.y).toBe(85);
|
expect(rectangle.y).toBe(-35);
|
||||||
expect(text.x).toBe(89.5);
|
expect(text.x).toBe(85);
|
||||||
expect(text.y).toBe(90);
|
expect(text.y).toBe(-30);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
Keyboard.keyPress(KEYS.Z);
|
Keyboard.keyPress(KEYS.Z);
|
||||||
@@ -1114,29 +1099,6 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||||
jest
|
|
||||||
.spyOn(textElementUtils, "measureText")
|
|
||||||
.mockImplementation((text, font, maxWidth) => {
|
|
||||||
let width = INITIAL_WIDTH;
|
|
||||||
let height = APPROX_LINE_HEIGHT;
|
|
||||||
let baseline = 10;
|
|
||||||
if (!text) {
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
baseline = 30;
|
|
||||||
width = DUMMY_WIDTH;
|
|
||||||
height = APPROX_LINE_HEIGHT * 5;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const originalRectHeight = rectangle.height;
|
const originalRectHeight = rectangle.height;
|
||||||
expect(rectangle.height).toBe(originalRectHeight);
|
expect(rectangle.height).toBe(originalRectHeight);
|
||||||
|
|
||||||
@@ -1150,7 +1112,7 @@ describe("textWysiwyg", () => {
|
|||||||
target: { value: "Online whiteboard collaboration made easy" },
|
target: { value: "Online whiteboard collaboration made easy" },
|
||||||
});
|
});
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(135);
|
expect(rectangle.height).toBe(178);
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -1176,7 +1138,7 @@ describe("textWysiwyg", () => {
|
|||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect(rectangle.height).toBe(215);
|
expect(rectangle.height).toBe(156);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@@ -1188,13 +1150,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(215);
|
expect(rectangle.height).toBe(156);
|
||||||
// cache updated again
|
// cache updated again
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(215);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
|
||||||
});
|
});
|
||||||
|
|
||||||
//@todo fix this test later once measureText is mocked correctly
|
it("should reset the container height cache when font properties updated", async () => {
|
||||||
it.skip("should reset the container height cache when font properties updated", async () => {
|
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
@@ -1220,7 +1181,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
||||||
).toEqual(36);
|
).toEqual(36);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(
|
||||||
|
96.39999999999999,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should align correctly", () => {
|
describe("should align correctly", () => {
|
||||||
@@ -1248,7 +1211,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
20,
|
25,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -1258,8 +1221,8 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
94.5,
|
30,
|
||||||
20,
|
25,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -1269,22 +1232,22 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
174,
|
45,
|
||||||
20,
|
25,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when center left", async () => {
|
it("when center left", async () => {
|
||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
25,
|
45.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when center center", async () => {
|
it("when center center", async () => {
|
||||||
@@ -1292,11 +1255,11 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
-25,
|
30,
|
||||||
25,
|
45.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when center right", async () => {
|
it("when center right", async () => {
|
||||||
@@ -1304,11 +1267,11 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
174,
|
45,
|
||||||
25,
|
45.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when bottom left", async () => {
|
it("when bottom left", async () => {
|
||||||
@@ -1316,34 +1279,120 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
25,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when bottom center", async () => {
|
it("when bottom center", async () => {
|
||||||
fireEvent.click(screen.getByTitle("Center"));
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
94.5,
|
30,
|
||||||
25,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when bottom right", async () => {
|
it("when bottom right", async () => {
|
||||||
fireEvent.click(screen.getByTitle("Right"));
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
174,
|
45,
|
||||||
25,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
mouse.clickAt(20, 30);
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: {
|
||||||
|
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
|
||||||
|
editor.select();
|
||||||
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
|
expect(textElement.width).toBe(600);
|
||||||
|
expect(textElement.height).toBe(24);
|
||||||
|
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
||||||
|
expect((textElement as ExcalidrawTextElement).text).toBe(
|
||||||
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
|
);
|
||||||
|
|
||||||
|
API.setSelectedElements([textElement]);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 20,
|
||||||
|
clientY: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextMenu = document.querySelector(".context-menu");
|
||||||
|
fireEvent.click(
|
||||||
|
queryByText(contextMenu as HTMLElement, "Wrap text in a container")!,
|
||||||
|
);
|
||||||
|
expect(h.elements.length).toBe(3);
|
||||||
|
|
||||||
|
expect(h.elements[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
angle: 0,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: h.elements[2].id,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fillStyle: "hachure",
|
||||||
|
groupIds: [],
|
||||||
|
height: 34,
|
||||||
|
isDeleted: false,
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
opacity: 100,
|
||||||
|
roughness: 1,
|
||||||
|
roundness: {
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
strokeColor: "#000000",
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeWidth: 1,
|
||||||
|
type: "rectangle",
|
||||||
|
updated: 1,
|
||||||
|
version: 1,
|
||||||
|
width: 610,
|
||||||
|
x: 15,
|
||||||
|
y: 25,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(h.elements[2] as ExcalidrawTextElement).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: "Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
textAlign: TEXT_ALIGN.LEFT,
|
||||||
|
boundElements: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-55
@@ -24,13 +24,17 @@ import { mutateElement } from "./mutateElement";
|
|||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
getApproxLineHeight,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getBoundTextElementOffset,
|
getContainerCoords,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
getMaxContainerHeight,
|
||||||
|
getMaxContainerWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
@@ -38,7 +42,6 @@ import {
|
|||||||
} from "../actions/actionProperties";
|
} from "../actions/actionProperties";
|
||||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
|
||||||
@@ -157,7 +160,7 @@ export const textWysiwyg = ({
|
|||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
const width = updatedTextElement.width;
|
let textElementWidth = updatedTextElement.width;
|
||||||
// Set to element height by default since that's
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let textElementHeight = updatedTextElement.height;
|
let textElementHeight = updatedTextElement.height;
|
||||||
@@ -230,19 +233,17 @@ export const textWysiwyg = ({
|
|||||||
// Start pushing text upward until a diff of 30px (padding)
|
// Start pushing text upward until a diff of 30px (padding)
|
||||||
// is reached
|
// is reached
|
||||||
else {
|
else {
|
||||||
|
const containerCoords = getContainerCoords(container);
|
||||||
|
|
||||||
// vertically center align the text
|
// vertically center align the text
|
||||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
coordY =
|
coordY =
|
||||||
container.y + containerDims.height / 2 - textElementHeight / 2;
|
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
coordY =
|
coordY = containerCoords.y + (maxHeight - textElementHeight);
|
||||||
container.y +
|
|
||||||
containerDims.height -
|
|
||||||
textElementHeight -
|
|
||||||
getBoundTextElementOffset(updatedTextElement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,8 +272,10 @@ export const textWysiwyg = ({
|
|||||||
: updatedTextElement.height / lines.length;
|
: updatedTextElement.height / lines.length;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
|
} else {
|
||||||
|
textElementWidth += 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
const editorMaxHeight =
|
const editorMaxHeight =
|
||||||
(appState.height - viewportY) / appState.zoom.value;
|
(appState.height - viewportY) / appState.zoom.value;
|
||||||
@@ -280,12 +283,12 @@ export const textWysiwyg = ({
|
|||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: `${lineHeight}px`,
|
lineHeight: `${lineHeight}px`,
|
||||||
width: `${Math.min(width, maxWidth)}px`,
|
width: `${textElementWidth}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${textElementHeight}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
width,
|
textElementWidth,
|
||||||
textElementHeight,
|
textElementHeight,
|
||||||
getTextElementAngle(updatedTextElement),
|
getTextElementAngle(updatedTextElement),
|
||||||
appState,
|
appState,
|
||||||
@@ -378,55 +381,16 @@ export const textWysiwyg = ({
|
|||||||
id,
|
id,
|
||||||
) as ExcalidrawTextElement;
|
) as ExcalidrawTextElement;
|
||||||
const font = getFontString(updatedTextElement);
|
const font = getFontString(updatedTextElement);
|
||||||
// using scrollHeight here since we need to calculate
|
if (isBoundToContainer(element)) {
|
||||||
// number of lines so cannot use editable.style.height
|
|
||||||
// as that gets updated below
|
|
||||||
// Rounding here so that the lines calculated is more accurate in all browsers.
|
|
||||||
// The scrollHeight and approxLineHeight differs in diff browsers
|
|
||||||
// eg it gives 1.05 in firefox for handewritten small font due to which
|
|
||||||
// height gets updated as lines > 1 and leads to jumping text for first line in bound container
|
|
||||||
// hence rounding here to avoid that
|
|
||||||
const lines = Math.round(
|
|
||||||
editable.scrollHeight / getApproxLineHeight(font),
|
|
||||||
);
|
|
||||||
// auto increase height only when lines > 1 so its
|
|
||||||
// measured correctly and vertically aligns for
|
|
||||||
// first line as well as setting height to "auto"
|
|
||||||
// doubles the height as soon as user starts typing
|
|
||||||
if (isBoundToContainer(element) && lines > 1) {
|
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
|
|
||||||
let height = "auto";
|
|
||||||
editable.style.height = "0px";
|
|
||||||
let heightSet = false;
|
|
||||||
if (lines === 2) {
|
|
||||||
const actualLineCount = wrapText(
|
|
||||||
editable.value,
|
|
||||||
font,
|
|
||||||
getMaxContainerWidth(container!),
|
|
||||||
).split("\n").length;
|
|
||||||
// This is browser behaviour when setting height to "auto"
|
|
||||||
// It sets the height needed for 2 lines even if actual
|
|
||||||
// line count is 1 as mentioned above as well
|
|
||||||
// hence reducing the height by half if actual line count is 1
|
|
||||||
// so single line aligns vertically when deleting
|
|
||||||
if (actualLineCount === 1) {
|
|
||||||
height = `${editable.scrollHeight / 2}px`;
|
|
||||||
editable.style.height = height;
|
|
||||||
heightSet = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const wrappedText = wrapText(
|
const wrappedText = wrapText(
|
||||||
normalizeText(editable.value),
|
normalizeText(editable.value),
|
||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container!),
|
getMaxContainerWidth(container!),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font);
|
const { width, height } = measureText(wrappedText, font);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
|
editable.style.height = `${height}px`;
|
||||||
if (!heightSet) {
|
|
||||||
editable.style.height = `${editable.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onChange(normalizeText(editable.value));
|
onChange(normalizeText(editable.value));
|
||||||
};
|
};
|
||||||
@@ -463,7 +427,9 @@ export const textWysiwyg = ({
|
|||||||
event.code === CODES.BRACKET_RIGHT))
|
event.code === CODES.BRACKET_RIGHT))
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
if (event.isComposing) {
|
||||||
|
return;
|
||||||
|
} else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||||
outdent();
|
outdent();
|
||||||
} else {
|
} else {
|
||||||
indent();
|
indent();
|
||||||
@@ -612,6 +578,7 @@ export const textWysiwyg = ({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
redrawTextBoundingBox(updateElement, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
|
describe("Test TypeChecks", () => {
|
||||||
|
describe("Test hasBoundTextElement", () => {
|
||||||
|
it("should return true for text bindable containers with bound text", () => {
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "ellipse",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "image",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for text bindable containers without bound text", () => {
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "freedraw",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for non text bindable containers", () => {
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "freedraw",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { MarkNonNullable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -139,7 +140,7 @@ export const hasBoundTextElement = (
|
|||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
|
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
|
||||||
return (
|
return (
|
||||||
isBindableElement(element) &&
|
isTextBindableContainer(element) &&
|
||||||
!!element.boundElements?.some(({ type }) => type === "text")
|
!!element.boundElements?.some(({ type }) => type === "text")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
THEME,
|
THEME,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
|
import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
@@ -130,7 +131,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
fontFamily: FontFamilyValues;
|
fontFamily: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
baseline: number;
|
|
||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId: ExcalidrawGenericElement["id"] | null;
|
containerId: ExcalidrawGenericElement["id"] | null;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { unstable_createStore } from "jotai";
|
||||||
|
|
||||||
|
export const appJotaiStore = unstable_createStore();
|
||||||
@@ -70,7 +70,7 @@ import { decryptData } from "../../data/encryption";
|
|||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { jotaiStore } from "../../jotai";
|
import { appJotaiStore } from "../app-jotai";
|
||||||
|
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const collabDialogShownAtom = atom(false);
|
export const collabDialogShownAtom = atom(false);
|
||||||
@@ -167,7 +167,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
setUsername: this.setUsername,
|
setUsername: this.setUsername,
|
||||||
};
|
};
|
||||||
|
|
||||||
jotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
this.onOfflineStatusToggle();
|
this.onOfflineStatusToggle();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -185,7 +185,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onOfflineStatusToggle = () => {
|
onOfflineStatusToggle = () => {
|
||||||
jotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
appJotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -208,10 +208,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
|
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||||
|
|
||||||
private setIsCollaborating = (isCollaborating: boolean) => {
|
private setIsCollaborating = (isCollaborating: boolean) => {
|
||||||
jotaiStore.set(isCollaboratingAtom, isCollaborating);
|
appJotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUnload = () => {
|
private onUnload = () => {
|
||||||
@@ -804,7 +804,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
jotaiStore.set(collabDialogShownAtom, false);
|
appJotaiStore.set(collabDialogShownAtom, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
setUsername = (username: string) => {
|
setUsername = (username: string) => {
|
||||||
@@ -838,10 +838,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog onClose={() => this.setState({ errorMessage: "" })}>
|
||||||
message={errorMessage}
|
{errorMessage}
|
||||||
onClose={() => this.setState({ errorMessage: "" })}
|
</ErrorDialog>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
shareWindows,
|
shareWindows,
|
||||||
} from "../../components/icons";
|
} from "../../components/icons";
|
||||||
import { ToolButton } from "../../components/ToolButton";
|
import { ToolButton } from "../../components/ToolButton";
|
||||||
import { t } from "../../i18n";
|
|
||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
import Stack from "../../components/Stack";
|
import Stack from "../../components/Stack";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { getFrame } from "../../utils";
|
import { getFrame } from "../../utils";
|
||||||
import DialogActionButton from "../../components/DialogActionButton";
|
import DialogActionButton from "../../components/DialogActionButton";
|
||||||
|
import { useI18n } from "../../i18n";
|
||||||
|
|
||||||
const getShareIcon = () => {
|
const getShareIcon = () => {
|
||||||
const navigator = window.navigator as any;
|
const navigator = window.navigator as any;
|
||||||
@@ -51,6 +51,7 @@ const RoomDialog = ({
|
|||||||
setErrorMessage: (message: string) => void;
|
setErrorMessage: (message: string) => void;
|
||||||
theme: AppState["theme"];
|
theme: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const copyRoomLink = async () => {
|
const copyRoomLink = async () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../components/icons";
|
import { PlusPromoIcon } from "../../components/icons";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
|
const { t } = useI18n();
|
||||||
let headingContent;
|
let headingContent;
|
||||||
|
|
||||||
if (isExcalidrawPlusSignedUser) {
|
if (isExcalidrawPlusSignedUser) {
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { shield } from "../../components/icons";
|
import { shield } from "../../components/icons";
|
||||||
import { Tooltip } from "../../components/Tooltip";
|
import { Tooltip } from "../../components/Tooltip";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
|
|
||||||
export const EncryptedIcon = () => (
|
export const EncryptedIcon = () => {
|
||||||
<a
|
const { t } = useI18n();
|
||||||
className="encrypted-icon tooltip"
|
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
return (
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
className="encrypted-icon tooltip"
|
||||||
aria-label={t("encrypted.link")}
|
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||||
>
|
target="_blank"
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
rel="noopener noreferrer"
|
||||||
{shield}
|
aria-label={t("encrypted.link")}
|
||||||
</Tooltip>
|
>
|
||||||
</a>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
);
|
{shield}
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
|||||||
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { excalidrawPlusIcon } from "./icons";
|
import { excalidrawPlusIcon } from "./icons";
|
||||||
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
||||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
import { isInitializedImageElement } from "../../element/typeChecks";
|
||||||
@@ -79,6 +79,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
}> = ({ elements, appState, files, onError }) => {
|
}> = ({ elements, appState, files, onError }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Card color="primary">
|
<Card color="primary">
|
||||||
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { useAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { langCodeAtom } from "..";
|
import { appLangCodeAtom } from "..";
|
||||||
import * as i18n from "../../i18n";
|
import { defaultLang, useI18n } from "../../i18n";
|
||||||
import { languages } from "../../i18n";
|
import { languages } from "../../i18n";
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
const { t, langCode } = useI18n();
|
||||||
|
const setLangCode = useSetAtom(appLangCodeAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className="dropdown-select dropdown-select__language"
|
className="dropdown-select dropdown-select__language"
|
||||||
onChange={({ target }) => setLangCode(target.value)}
|
onChange={({ target }) => setLangCode(target.value)}
|
||||||
value={langCode}
|
value={langCode}
|
||||||
aria-label={i18n.t("buttons.selectLanguage")}
|
aria-label={t("buttons.selectLanguage")}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<option key={i18n.defaultLang.code} value={i18n.defaultLang.code}>
|
<option key={defaultLang.code} value={defaultLang.code}>
|
||||||
{i18n.defaultLang.label}
|
{defaultLang.label}
|
||||||
</option>
|
</option>
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
<option key={lang.code} value={lang.code}>
|
<option key={lang.code} value={lang.code}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { encryptData, decryptData } from "../../data/encryption";
|
|||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
import { reconcileElements } from "../collab/reconciliation";
|
import { reconcileElements } from "../collab/reconciliation";
|
||||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||||
|
import { ResolutionType } from "../../utility-types";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
{ repairBindings: true },
|
{ repairBindings: true, refreshDimensions: true },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null, {
|
data = restore(localDataState || null, null, null, {
|
||||||
|
|||||||
@@ -75,15 +75,17 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
|||||||
import { LocalData } from "./data/LocalData";
|
import { LocalData } from "./data/LocalData";
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
|
||||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
|
||||||
import { reconcileElements } from "./collab/reconciliation";
|
import { reconcileElements } from "./collab/reconciliation";
|
||||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
import { AppFooter } from "./components/AppFooter";
|
import { AppFooter } from "./components/AppFooter";
|
||||||
|
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||||
|
import { useAtomWithInitialValue } from "../jotai";
|
||||||
|
import { appJotaiStore } from "./app-jotai";
|
||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
|
import { ResolutionType } from "../utility-types";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -226,15 +228,15 @@ const initializeScene = async (opts: {
|
|||||||
return { scene: null, isExternalScene: false };
|
return { scene: null, isExternalScene: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
||||||
|
export const appLangCodeAtom = atom(
|
||||||
export const langCodeAtom = atom(
|
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
||||||
Array.isArray(currentLangCode) ? currentLangCode[0] : currentLangCode,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -671,10 +673,9 @@ const ExcalidrawWrapper = () => {
|
|||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||||
message={errorMessage}
|
{errorMessage}
|
||||||
onClose={() => setErrorMessage("")}
|
</ErrorDialog>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -683,7 +684,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
const ExcalidrawApp = () => {
|
const ExcalidrawApp = () => {
|
||||||
return (
|
return (
|
||||||
<TopErrorBoundary>
|
<TopErrorBoundary>
|
||||||
<Provider unstable_createStore={() => jotaiStore}>
|
<Provider unstable_createStore={() => appJotaiStore}>
|
||||||
<ExcalidrawWrapper />
|
<ExcalidrawWrapper />
|
||||||
</Provider>
|
</Provider>
|
||||||
</TopErrorBoundary>
|
</TopErrorBoundary>
|
||||||
|
|||||||
Vendored
-47
@@ -50,36 +50,6 @@ interface Clipboard extends EventTarget {
|
|||||||
write(data: any[]): Promise<void>;
|
write(data: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutable<T> = {
|
|
||||||
-readonly [P in keyof T]: T[P];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
|
||||||
|
|
||||||
type Merge<M, N> = Omit<M, keyof N> & N;
|
|
||||||
|
|
||||||
/** utility type to assert that the second type is a subtype of the first type.
|
|
||||||
* Returns the subtype. */
|
|
||||||
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
|
|
||||||
|
|
||||||
type ResolutionType<T extends (...args: any) => any> = T extends (
|
|
||||||
...args: any
|
|
||||||
) => Promise<infer R>
|
|
||||||
? R
|
|
||||||
: any;
|
|
||||||
|
|
||||||
// https://github.com/krzkaczor/ts-essentials
|
|
||||||
type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
||||||
|
|
||||||
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
|
|
||||||
Required<Pick<T, RK>>;
|
|
||||||
|
|
||||||
type MarkNonNullable<T, K extends keyof T> = {
|
|
||||||
[P in K]-?: P extends K ? NonNullable<T[P]> : T[P];
|
|
||||||
} & { [P in keyof T]: T[P] };
|
|
||||||
|
|
||||||
type NonOptional<T> = Exclude<T, undefined>;
|
|
||||||
|
|
||||||
// PNG encoding/decoding
|
// PNG encoding/decoding
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
||||||
@@ -101,23 +71,6 @@ declare module "png-chunks-extract" {
|
|||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// type getter for interface's callable type
|
|
||||||
// src: https://stackoverflow.com/a/58658851/927631
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
type SignatureType<T> = T extends (...args: infer R) => any ? R : never;
|
|
||||||
type CallableType<T extends (...args: any[]) => any> = (
|
|
||||||
...args: SignatureType<T>
|
|
||||||
) => ReturnType<T>;
|
|
||||||
// --------------------------------------------------------------------------—
|
|
||||||
|
|
||||||
// Type for React.forwardRef --- supply only the first generic argument T
|
|
||||||
type ForwardRef<T, P = any> = Parameters<
|
|
||||||
CallableType<React.ForwardRefRenderFunction<T, P>>
|
|
||||||
>[1];
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------—
|
|
||||||
|
|
||||||
interface Blob {
|
interface Blob {
|
||||||
handle?: import("browser-fs-acces").FileSystemHandle;
|
handle?: import("browser-fs-acces").FileSystemHandle;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AppState } from "./types";
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { isLinearElement } from "./element/typeChecks";
|
import { isLinearElement } from "./element/typeChecks";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
|
import { Mutable } from "./utility-types";
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
|
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
|
||||||
|
|||||||
+16
@@ -1,6 +1,8 @@
|
|||||||
import fallbackLangData from "./locales/en.json";
|
import fallbackLangData from "./locales/en.json";
|
||||||
import percentages from "./locales/percentages.json";
|
import percentages from "./locales/percentages.json";
|
||||||
import { ENV } from "./constants";
|
import { ENV } from "./constants";
|
||||||
|
import { jotaiScope, jotaiStore } from "./jotai";
|
||||||
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
|
||||||
const COMPLETION_THRESHOLD = 85;
|
const COMPLETION_THRESHOLD = 85;
|
||||||
|
|
||||||
@@ -99,6 +101,8 @@ export const setLanguage = async (lang: Language) => {
|
|||||||
currentLangData = fallbackLangData;
|
currentLangData = fallbackLangData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jotaiStore.set(editorLangCodeAtom, lang.code);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguage = () => currentLang;
|
export const getLanguage = () => currentLang;
|
||||||
@@ -143,3 +147,15 @@ export const t = (
|
|||||||
}
|
}
|
||||||
return translation;
|
return translation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @private atom used solely to rerender components using `useI18n` hook */
|
||||||
|
const editorLangCodeAtom = atom(defaultLang.code);
|
||||||
|
|
||||||
|
// Should be used in components that fall under these cases:
|
||||||
|
// - component is rendered as an <Excalidraw> child
|
||||||
|
// - component is rendered internally by <Excalidraw>, but the component
|
||||||
|
// is memoized w/o being updated on `langCode`, `AppState`, or `UIAppState`
|
||||||
|
export const useI18n = () => {
|
||||||
|
const langCode = useAtomValue(editorLangCodeAtom, jotaiScope);
|
||||||
|
return { t, langCode };
|
||||||
|
};
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
|
import { PrimitiveAtom, unstable_createStore, useAtom } from "jotai";
|
||||||
import { useLayoutEffect } from "react";
|
import { useLayoutEffect } from "react";
|
||||||
|
|
||||||
export const jotaiScope = Symbol();
|
export const jotaiScope = Symbol();
|
||||||
@@ -6,7 +6,7 @@ export const jotaiStore = unstable_createStore();
|
|||||||
|
|
||||||
export const useAtomWithInitialValue = <
|
export const useAtomWithInitialValue = <
|
||||||
T extends unknown,
|
T extends unknown,
|
||||||
A extends WritableAtom<T, T>,
|
A extends PrimitiveAtom<T>,
|
||||||
>(
|
>(
|
||||||
atom: A,
|
atom: A,
|
||||||
initialValue: T | (() => T),
|
initialValue: T | (() => T),
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
||||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||||
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
||||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
|
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷",
|
"invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷",
|
||||||
"resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?",
|
"resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?",
|
||||||
"removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?",
|
"removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?",
|
||||||
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।"
|
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "অসমর্থিত ফাইল।",
|
"unsupportedFileType": "অসমর্থিত ফাইল।",
|
||||||
|
|||||||
+27
-26
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Enganxa",
|
"paste": "Enganxa",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Enganxar com a text pla",
|
||||||
"pasteCharts": "Enganxa els diagrames",
|
"pasteCharts": "Enganxa els diagrames",
|
||||||
"selectAll": "Selecciona-ho tot",
|
"selectAll": "Selecciona-ho tot",
|
||||||
"multiSelect": "Afegeix un element a la selecció",
|
"multiSelect": "Afegeix un element a la selecció",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"layers": "Capes",
|
"layers": "Capes",
|
||||||
"actions": "Accions",
|
"actions": "Accions",
|
||||||
"language": "Llengua",
|
"language": "Llengua",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Col·laboració en directe...",
|
||||||
"duplicateSelection": "Duplica",
|
"duplicateSelection": "Duplica",
|
||||||
"untitled": "Sense títol",
|
"untitled": "Sense títol",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
@@ -116,8 +116,8 @@
|
|||||||
"label": "Enllaç"
|
"label": "Enllaç"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "Editar línia",
|
||||||
"exit": ""
|
"exit": "Sortir de l'editor de línia"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "Bloca",
|
"lock": "Bloca",
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Neteja el llenç",
|
"clearReset": "Neteja el llenç",
|
||||||
"exportJSON": "Exporta a un fitxer",
|
"exportJSON": "Exporta a un fitxer",
|
||||||
"exportImage": "",
|
"exportImage": "Exporta la imatge...",
|
||||||
"export": "",
|
"export": "Guardar a...",
|
||||||
"exportToPng": "Exporta a PNG",
|
"exportToPng": "Exporta a PNG",
|
||||||
"exportToSvg": "Exporta a SNG",
|
"exportToSvg": "Exporta a SNG",
|
||||||
"copyToClipboard": "Copia al porta-retalls",
|
"copyToClipboard": "Copia al porta-retalls",
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"scale": "Escala",
|
"scale": "Escala",
|
||||||
"save": "Desa al fitxer actual",
|
"save": "Desa al fitxer actual",
|
||||||
"saveAs": "Anomena i desa",
|
"saveAs": "Anomena i desa",
|
||||||
"load": "",
|
"load": "Obrir",
|
||||||
"getShareableLink": "Obté l'enllaç per a compartir",
|
"getShareableLink": "Obté l'enllaç per a compartir",
|
||||||
"close": "Tanca",
|
"close": "Tanca",
|
||||||
"selectLanguage": "Trieu la llengua",
|
"selectLanguage": "Trieu la llengua",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
||||||
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
||||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
|
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
|
||||||
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
|
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
|
||||||
|
"collabOfflineWarning": "Sense connexió a internet disponible.\nEls vostres canvis no seran guardats!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipus de fitxer no suportat.",
|
"unsupportedFileType": "Tipus de fitxer no suportat.",
|
||||||
@@ -202,8 +203,8 @@
|
|||||||
"invalidSVGString": "SVG no vàlid.",
|
"invalidSVGString": "SVG no vàlid.",
|
||||||
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
||||||
"importLibraryError": "No s'ha pogut carregar la biblioteca",
|
"importLibraryError": "No s'ha pogut carregar la biblioteca",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.",
|
||||||
"collabSaveFailed_sizeExceeded": ""
|
"collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
@@ -217,10 +218,10 @@
|
|||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||||
"penMode": "",
|
"penMode": "Mode de llapis - evita tocar",
|
||||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||||
"eraser": "Esborrador",
|
"eraser": "Esborrador",
|
||||||
"hand": ""
|
"hand": "Mà (eina de desplaçament)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del llenç",
|
"canvasActions": "Accions del llenç",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Formes"
|
"shapes": "Formes"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "Per moure el llenç, manteniu premuda la roda del ratolí o la barra espaiadora mentre arrossegueu o utilitzeu l'eina manual",
|
||||||
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
|
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
|
||||||
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
|
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
|
||||||
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
|
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
|
||||||
@@ -239,7 +240,7 @@
|
|||||||
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
|
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
|
||||||
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
||||||
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Mantingueu premut Ctrl o Cmd i feu doble clic o premeu Ctrl o Cmd + Retorn per editar els punts",
|
||||||
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
|
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
|
||||||
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
|
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
|
||||||
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
||||||
@@ -247,7 +248,7 @@
|
|||||||
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||||
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
||||||
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar",
|
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "És probable que aquesta funció es pugui activar posant la marca \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Per canviar les marques del navegador al Firefox, visiteu la pàgina \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||||
@@ -295,7 +296,7 @@
|
|||||||
"blog": "Llegiu el nostre blog",
|
"blog": "Llegiu el nostre blog",
|
||||||
"click": "clic",
|
"click": "clic",
|
||||||
"deepSelect": "Selecció profunda",
|
"deepSelect": "Selecció profunda",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Seleccioneu profundament dins del quadre i eviteu arrossegar",
|
||||||
"curvedArrow": "Fletxa corba",
|
"curvedArrow": "Fletxa corba",
|
||||||
"curvedLine": "Línia corba",
|
"curvedLine": "Línia corba",
|
||||||
"documentation": "Documentació",
|
"documentation": "Documentació",
|
||||||
@@ -316,8 +317,8 @@
|
|||||||
"zoomToFit": "Zoom per veure tots els elements",
|
"zoomToFit": "Zoom per veure tots els elements",
|
||||||
"zoomToSelection": "Zoom per veure la selecció",
|
"zoomToSelection": "Zoom per veure la selecció",
|
||||||
"toggleElementLock": "Blocar/desblocar la selecció",
|
"toggleElementLock": "Blocar/desblocar la selecció",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Mou la pàgina cap amunt/a baix",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Mou la pàgina cap a l'esquerra/dreta"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Neteja el llenç"
|
"title": "Neteja el llenç"
|
||||||
@@ -399,7 +400,7 @@
|
|||||||
"fileSavedToFilename": "S'ha desat a {filename}",
|
"fileSavedToFilename": "S'ha desat a {filename}",
|
||||||
"canvas": "el llenç",
|
"canvas": "el llenç",
|
||||||
"selection": "la selecció",
|
"selection": "la selecció",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Blanc",
|
"ffffff": "Blanc",
|
||||||
@@ -450,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Totes les vostres dades es guarden localment al vostre navegador.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Vols anar a Excalidraw+ en comptes?",
|
||||||
"menuHint": ""
|
"menuHint": "Exportar, preferències, llenguatges..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Exportar, preferències i més...",
|
||||||
"center_heading": "",
|
"center_heading": "Diagrames. Fer. Simple.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Selecciona una eina i comença a dibuixar!",
|
||||||
"helpHint": ""
|
"helpHint": "Dreceres i ajuda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
||||||
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
|
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
|
||||||
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
|
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
|
||||||
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
|
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
|
||||||
|
"collabOfflineWarning": "Keine Internetverbindung verfügbar.\nDeine Änderungen werden nicht gespeichert!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nicht unterstützter Dateityp.",
|
"unsupportedFileType": "Nicht unterstützter Dateityp.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
||||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||||
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
||||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
|
||||||
|
"collabOfflineWarning": "Δεν υπάρχει διαθέσιμη σύνδεση στο internet.\nΟι αλλαγές σας δεν θα αποθηκευτούν!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
||||||
|
|||||||
+17
-2
@@ -110,6 +110,7 @@
|
|||||||
"increaseFontSize": "Increase font size",
|
"increaseFontSize": "Increase font size",
|
||||||
"unbindText": "Unbind text",
|
"unbindText": "Unbind text",
|
||||||
"bindText": "Bind text to the container",
|
"bindText": "Bind text to the container",
|
||||||
|
"createContainerFromText": "Wrap text in a container",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edit link",
|
"edit": "Edit link",
|
||||||
"create": "Create link",
|
"create": "Create link",
|
||||||
@@ -119,7 +120,6 @@
|
|||||||
"edit": "Edit line",
|
"edit": "Edit line",
|
||||||
"exit": "Exit line editor"
|
"exit": "Exit line editor"
|
||||||
},
|
},
|
||||||
|
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "Lock",
|
"lock": "Lock",
|
||||||
"unlock": "Unlock",
|
"unlock": "Unlock",
|
||||||
@@ -205,7 +205,22 @@
|
|||||||
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
|
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
|
||||||
"importLibraryError": "Couldn't load library",
|
"importLibraryError": "Couldn't load library",
|
||||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work."
|
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
||||||
|
"brave_measure_text_error": {
|
||||||
|
"start": "Looks like you are using Brave browser with the",
|
||||||
|
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
|
||||||
|
"setting_enabled": "setting enabled",
|
||||||
|
"break": "This could result in breaking the",
|
||||||
|
"text_elements": "Text Elements",
|
||||||
|
"in_your_drawings": "in your drawings",
|
||||||
|
"strongly_recommend": "We strongly recommend disabling this setting. You can follow",
|
||||||
|
"steps": "these steps",
|
||||||
|
"how": "on how to do so",
|
||||||
|
"disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
|
||||||
|
"issue": "issue",
|
||||||
|
"write": "on our GitHub, or write us on",
|
||||||
|
"discord": "Discord"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
"share": "Compartir",
|
"share": "Compartir",
|
||||||
"showStroke": "Mostrar selector de color de trazo",
|
"showStroke": "Mostrar selector de color de trazo",
|
||||||
"showBackground": "Mostrar el selector de color de fondo",
|
"showBackground": "Mostrar el selector de color de fondo",
|
||||||
"toggleTheme": "Alternar tema",
|
"toggleTheme": "Cambiar tema",
|
||||||
"personalLib": "Biblioteca personal",
|
"personalLib": "Biblioteca personal",
|
||||||
"excalidrawLib": "Biblioteca Excalidraw",
|
"excalidrawLib": "Biblioteca Excalidraw",
|
||||||
"decreaseFontSize": "Disminuir tamaño de letra",
|
"decreaseFontSize": "Disminuir tamaño de letra",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
||||||
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
||||||
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
||||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
|
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
|
||||||
|
"collabOfflineWarning": "No hay conexión a internet disponible.\n¡No se guardarán los cambios!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de archivo no admitido.",
|
"unsupportedFileType": "Tipo de archivo no admitido.",
|
||||||
@@ -233,7 +234,7 @@
|
|||||||
"freeDraw": "Haz clic y arrastra, suelta al terminar",
|
"freeDraw": "Haz clic y arrastra, suelta al terminar",
|
||||||
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
|
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
|
||||||
"text_selected": "Doble clic o pulse ENTER para editar el texto",
|
"text_selected": "Doble clic o pulse ENTER para editar el texto",
|
||||||
"text_editing": "Pulse Escape o CtrlOrCmd+ENTER para terminar de editar",
|
"text_editing": "Pulse Escape o Ctrl/Cmd + ENTER para terminar de editar",
|
||||||
"linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar",
|
"linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar",
|
||||||
"lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT",
|
"lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT",
|
||||||
"resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro",
|
"resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro",
|
||||||
@@ -314,7 +315,7 @@
|
|||||||
"title": "Ayuda",
|
"title": "Ayuda",
|
||||||
"view": "Vista",
|
"view": "Vista",
|
||||||
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
|
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
|
||||||
"zoomToSelection": "Zoom a la selección",
|
"zoomToSelection": "Ampliar selección",
|
||||||
"toggleElementLock": "Bloquear/desbloquear selección",
|
"toggleElementLock": "Bloquear/desbloquear selección",
|
||||||
"movePageUpDown": "Mover página hacia arriba/abajo",
|
"movePageUpDown": "Mover página hacia arriba/abajo",
|
||||||
"movePageLeftRight": "Mover página hacia la izquierda/derecha"
|
"movePageLeftRight": "Mover página hacia la izquierda/derecha"
|
||||||
@@ -326,9 +327,9 @@
|
|||||||
"title": "Publicar biblioteca",
|
"title": "Publicar biblioteca",
|
||||||
"itemName": "Nombre del artículo",
|
"itemName": "Nombre del artículo",
|
||||||
"authorName": "Nombre del autor",
|
"authorName": "Nombre del autor",
|
||||||
"githubUsername": "Nombre de usuario de Github",
|
"githubUsername": "Nombre de usuario de GitHub",
|
||||||
"twitterUsername": "Nombre de usuario de Twitter",
|
"twitterUsername": "Nombre de usuario de Twitter",
|
||||||
"libraryName": "Nombre de la librería",
|
"libraryName": "Nombre de la biblioteca",
|
||||||
"libraryDesc": "Descripción de la biblioteca",
|
"libraryDesc": "Descripción de la biblioteca",
|
||||||
"website": "Sitio Web",
|
"website": "Sitio Web",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
@@ -336,7 +337,7 @@
|
|||||||
"libraryName": "Nombre de tu biblioteca",
|
"libraryName": "Nombre de tu biblioteca",
|
||||||
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
|
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
|
||||||
"githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión",
|
"githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión",
|
||||||
"twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
|
"twitterHandle": "Nombre de usuario de Twitter (opcional), así sabemos a quién acreditar cuando se promociona en Twitter",
|
||||||
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
|
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -458,7 +459,7 @@
|
|||||||
"menuHint": "Exportar, preferencias y más...",
|
"menuHint": "Exportar, preferencias y más...",
|
||||||
"center_heading": "Diagramas. Hecho. Simplemente.",
|
"center_heading": "Diagramas. Hecho. Simplemente.",
|
||||||
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
||||||
"helpHint": "Atajos & ayuda"
|
"helpHint": "Atajos y ayuda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
|
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
|
||||||
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
|
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
|
||||||
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
|
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
|
||||||
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
|
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
|
||||||
|
"collabOfflineWarning": "Ez dago Interneteko konexiorik.\nZure aldaketak ez dira gordeko!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
|
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"penMode": "Luma modua - ukipena saihestu",
|
"penMode": "Luma modua - ukipena saihestu",
|
||||||
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
||||||
"eraser": "Borragoma",
|
"eraser": "Borragoma",
|
||||||
"hand": ""
|
"hand": "Eskua (panoratze tresna)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas ekintzak",
|
"canvasActions": "Canvas ekintzak",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Formak"
|
"shapes": "Formak"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "Oihala mugitzeko, eutsi saguaren gurpila edo zuriune-barra arrastatzean, edo erabili esku tresna",
|
||||||
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
|
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
|
||||||
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
|
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
|
||||||
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
|
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
|
||||||
@@ -247,7 +248,7 @@
|
|||||||
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
||||||
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
|
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
|
||||||
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
|
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "Ezaugarri hau \"dom.events.asyncClipboard.clipboardItem\" marka \"true\" gisa ezarrita gaitu daiteke. Firefox-en arakatzailearen banderak aldatzeko, bisitatu \"about:config\" orrialdera."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
||||||
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
||||||
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
||||||
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
|
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
||||||
|
|||||||
+32
-31
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Liitä",
|
"paste": "Liitä",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Liitä pelkkänä tekstinä",
|
||||||
"pasteCharts": "Liitä kaaviot",
|
"pasteCharts": "Liitä kaaviot",
|
||||||
"selectAll": "Valitse kaikki",
|
"selectAll": "Valitse kaikki",
|
||||||
"multiSelect": "Lisää kohde valintaan",
|
"multiSelect": "Lisää kohde valintaan",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"layers": "Tasot",
|
"layers": "Tasot",
|
||||||
"actions": "Toiminnot",
|
"actions": "Toiminnot",
|
||||||
"language": "Kieli",
|
"language": "Kieli",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Live Yhteistyö...",
|
||||||
"duplicateSelection": "Monista",
|
"duplicateSelection": "Monista",
|
||||||
"untitled": "Nimetön",
|
"untitled": "Nimetön",
|
||||||
"name": "Nimi",
|
"name": "Nimi",
|
||||||
@@ -116,14 +116,14 @@
|
|||||||
"label": "Linkki"
|
"label": "Linkki"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "Muokkaa riviä",
|
||||||
"exit": ""
|
"exit": "Poistu rivieditorista"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "Lukitse",
|
||||||
"unlock": "",
|
"unlock": "Poista lukitus",
|
||||||
"lockAll": "",
|
"lockAll": "Lukitse kaikki",
|
||||||
"unlockAll": ""
|
"unlockAll": "Poista lukitus kaikista"
|
||||||
},
|
},
|
||||||
"statusPublished": "Julkaistu",
|
"statusPublished": "Julkaistu",
|
||||||
"sidebarLock": "Pidä sivupalkki avoinna"
|
"sidebarLock": "Pidä sivupalkki avoinna"
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Tyhjennä piirtoalue",
|
"clearReset": "Tyhjennä piirtoalue",
|
||||||
"exportJSON": "Vie tiedostoon",
|
"exportJSON": "Vie tiedostoon",
|
||||||
"exportImage": "",
|
"exportImage": "Vie kuva...",
|
||||||
"export": "",
|
"export": "Tallenna nimellä...",
|
||||||
"exportToPng": "Vie PNG-tiedostona",
|
"exportToPng": "Vie PNG-tiedostona",
|
||||||
"exportToSvg": "Vie SVG-tiedostona",
|
"exportToSvg": "Vie SVG-tiedostona",
|
||||||
"copyToClipboard": "Kopioi leikepöydälle",
|
"copyToClipboard": "Kopioi leikepöydälle",
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"scale": "Koko",
|
"scale": "Koko",
|
||||||
"save": "Tallenna nykyiseen tiedostoon",
|
"save": "Tallenna nykyiseen tiedostoon",
|
||||||
"saveAs": "Tallenna nimellä",
|
"saveAs": "Tallenna nimellä",
|
||||||
"load": "",
|
"load": "Avaa",
|
||||||
"getShareableLink": "Hae jaettava linkki",
|
"getShareableLink": "Hae jaettava linkki",
|
||||||
"close": "Sulje",
|
"close": "Sulje",
|
||||||
"selectLanguage": "Valitse kieli",
|
"selectLanguage": "Valitse kieli",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
|
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
|
||||||
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
|
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
|
||||||
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
|
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
|
||||||
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
|
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
|
||||||
|
"collabOfflineWarning": "Internet-yhteyttä ei ole saatavilla.\nMuutoksiasi ei tallenneta!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
|
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
|
||||||
@@ -201,9 +202,9 @@
|
|||||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||||
"invalidSVGString": "Virheellinen SVG.",
|
"invalidSVGString": "Virheellinen SVG.",
|
||||||
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
||||||
"importLibraryError": "",
|
"importLibraryError": "Kokoelman lataaminen epäonnistui",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
|
||||||
"collabSaveFailed_sizeExceeded": ""
|
"collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Valinta",
|
"selection": "Valinta",
|
||||||
@@ -217,10 +218,10 @@
|
|||||||
"text": "Teksti",
|
"text": "Teksti",
|
||||||
"library": "Kirjasto",
|
"library": "Kirjasto",
|
||||||
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
|
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
|
||||||
"penMode": "",
|
"penMode": "Kynätila - estä kosketus",
|
||||||
"link": "Lisää/päivitä linkki valitulle muodolle",
|
"link": "Lisää/päivitä linkki valitulle muodolle",
|
||||||
"eraser": "Poistotyökalu",
|
"eraser": "Poistotyökalu",
|
||||||
"hand": ""
|
"hand": "Käsi (panning-työkalu)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Piirtoalueen toiminnot",
|
"canvasActions": "Piirtoalueen toiminnot",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Muodot"
|
"shapes": "Muodot"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "Piirtoalueen liikuttamiseksi pidä hiiren pyörää tai välilyöntiä pohjassa tai käytä käsityökalua",
|
||||||
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
|
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
|
||||||
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
|
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
|
||||||
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
|
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
|
||||||
@@ -239,7 +240,7 @@
|
|||||||
"resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen",
|
"resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen",
|
||||||
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
|
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
|
||||||
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
|
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Pidä CtrlOrCmd pohjassa ja kaksoisnapsauta tai paina CtrlOrCmd + Enter muokataksesi pisteitä",
|
||||||
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
|
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
|
||||||
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
|
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
|
||||||
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
|
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
|
||||||
@@ -247,7 +248,7 @@
|
|||||||
"bindTextToElement": "Lisää tekstiä painamalla enter",
|
"bindTextToElement": "Lisää tekstiä painamalla enter",
|
||||||
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
|
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
|
||||||
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
|
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "Tämä ominaisuus voidaan todennäköisesti ottaa käyttöön asettamalla \"dom.events.asyncClipboard.clipboardItem\" kohta \"true\":ksi. Vaihtaaksesi selaimen kohdan Firefoxissa, käy \"about:config\" sivulla."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
||||||
@@ -315,9 +316,9 @@
|
|||||||
"view": "Näkymä",
|
"view": "Näkymä",
|
||||||
"zoomToFit": "Näytä kaikki elementit",
|
"zoomToFit": "Näytä kaikki elementit",
|
||||||
"zoomToSelection": "Näytä valinta",
|
"zoomToSelection": "Näytä valinta",
|
||||||
"toggleElementLock": "",
|
"toggleElementLock": "Lukitse / poista lukitus valinta",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Siirrä sivua ylös/alas",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Siirrä sivua vasemmalle/oikealle"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Pyyhi piirtoalue"
|
"title": "Pyyhi piirtoalue"
|
||||||
@@ -399,7 +400,7 @@
|
|||||||
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
||||||
"canvas": "piirtoalue",
|
"canvas": "piirtoalue",
|
||||||
"selection": "valinta",
|
"selection": "valinta",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Valkoinen",
|
"ffffff": "Valkoinen",
|
||||||
@@ -450,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Kaikki tietosi on tallennettu paikallisesti selaimellesi.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Haluatko sen sijaan mennä Excalidraw+:aan?",
|
||||||
"menuHint": ""
|
"menuHint": "Vie, asetukset, kielet, ..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Vie, asetukset ja lisää...",
|
||||||
"center_heading": "",
|
"center_heading": "Kaaviot. Tehty. Yksinkertaiseksi.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Valitse työkalu ja aloita piirtäminen!",
|
||||||
"helpHint": ""
|
"helpHint": "Pikanäppäimet & ohje"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
|
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
|
||||||
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
|
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
|
||||||
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
|
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
|
||||||
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
|
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
|
||||||
|
"collabOfflineWarning": "Aucune connexion internet disponible.\nVos modifications ne seront pas enregistrées !"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Type de fichier non supporté.",
|
"unsupportedFileType": "Type de fichier non supporté.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.",
|
"invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.",
|
||||||
"resetLibrary": "Isto limpará a súa biblioteca. Está seguro?",
|
"resetLibrary": "Isto limpará a súa biblioteca. Está seguro?",
|
||||||
"removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?",
|
"removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?",
|
||||||
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada."
|
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de ficheiro non soportado.",
|
"unsupportedFileType": "Tipo de ficheiro non soportado.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
|
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
|
||||||
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
|
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
|
||||||
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
|
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
|
||||||
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
|
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
|
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।",
|
"invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।",
|
||||||
"resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?",
|
"resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?",
|
||||||
"removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?",
|
"removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?",
|
||||||
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं"
|
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं",
|
||||||
|
"collabOfflineWarning": "कोई इंटरनेट कनेक्शन उपलब्ध नहीं है।\nआपके बदलाव सहेजे नहीं जाएंगे!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "असमर्थित फाइल प्रकार",
|
"unsupportedFileType": "असमर्थित फाइल प्रकार",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
|
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
|
||||||
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
|
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
|
||||||
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
|
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
|
||||||
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
|
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nem támogatott fájltípus.",
|
"unsupportedFileType": "Nem támogatott fájltípus.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
|
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
|
||||||
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
|
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
|
||||||
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
|
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
|
||||||
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
|
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipe file tidak didukung.",
|
"unsupportedFileType": "Tipe file tidak didukung.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
|
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
|
||||||
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
|
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
|
||||||
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
|
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
|
||||||
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
|
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata.",
|
||||||
|
"collabOfflineWarning": "Nessuna connessione internet disponibile.\nLe tue modifiche non verranno salvate!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo di file non supportato.",
|
"unsupportedFileType": "Tipo di file non supportato.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
|
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
|
||||||
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
|
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
|
||||||
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
|
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
|
||||||
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
|
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。",
|
||||||
|
"collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "サポートされていないファイル形式です。",
|
"unsupportedFileType": "サポートされていないファイル形式です。",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"penMode": "ペンモード - タッチ防止",
|
"penMode": "ペンモード - タッチ防止",
|
||||||
"link": "選択した図形のリンクを追加/更新",
|
"link": "選択した図形のリンクを追加/更新",
|
||||||
"eraser": "消しゴム",
|
"eraser": "消しゴム",
|
||||||
"hand": ""
|
"hand": "手 (パンニングツール)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "キャンバス操作",
|
"canvasActions": "キャンバス操作",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "図形"
|
"shapes": "図形"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグするか、手ツールを使用します",
|
||||||
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
|
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
|
||||||
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
||||||
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
|
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
|
||||||
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
|
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
|
||||||
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
|
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
|
||||||
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
|
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
|
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
|
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
|
||||||
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
|
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
|
||||||
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
|
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
|
||||||
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다."
|
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
|
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "ناتوانێت دیمەنەکە هاوردە بکات لە URL ی دابینکراو. یان نادروستە، یان داتای \"ئێکسکالیدراو\" JSON ی دروستی تێدا نییە.",
|
"invalidSceneUrl": "ناتوانێت دیمەنەکە هاوردە بکات لە URL ی دابینکراو. یان نادروستە، یان داتای \"ئێکسکالیدراو\" JSON ی دروستی تێدا نییە.",
|
||||||
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
|
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
|
||||||
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
|
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
|
||||||
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە."
|
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
|
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nepavyko suimportuoti scenos iš pateiktos nuorodos (URL). Ji arba blogai suformatuota, arba savyje neturi teisingų Excalidraw JSON duomenų.",
|
"invalidSceneUrl": "Nepavyko suimportuoti scenos iš pateiktos nuorodos (URL). Ji arba blogai suformatuota, arba savyje neturi teisingų Excalidraw JSON duomenų.",
|
||||||
"resetLibrary": "Tai išvalys tavo biblioteką. Ar tikrai to nori?",
|
"resetLibrary": "Tai išvalys tavo biblioteką. Ar tikrai to nori?",
|
||||||
"removeItemsFromsLibrary": "Ištrinti {{count}} elementą/-us iš bibliotekos?",
|
"removeItemsFromsLibrary": "Ištrinti {{count}} elementą/-us iš bibliotekos?",
|
||||||
"invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas."
|
"invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nepalaikomas failo tipas.",
|
"unsupportedFileType": "Nepalaikomas failo tipas.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
|
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
|
||||||
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
|
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
|
||||||
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
|
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
|
||||||
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
|
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Neatbalstīts datnes veids.",
|
"unsupportedFileType": "Neatbalstīts datnes veids.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
|
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
|
||||||
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
|
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
|
||||||
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
|
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
|
||||||
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे."
|
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे.",
|
||||||
|
"collabOfflineWarning": "इंटरनेट कनेक्शन उपलब्ध नाही.\nतुमचे बदल जतन केले जाणार नाहीत!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "असमर्थित फाइल प्रकार.",
|
"unsupportedFileType": "असमर्थित फाइल प्रकार.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
|
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
|
||||||
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
|
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
|
||||||
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
|
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
|
||||||
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
|
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert.",
|
||||||
|
"collabOfflineWarning": "Ingen Internett-tilkobling tilgjengelig.\nEndringer dine vil ikke bli lagret!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Filtypen støttes ikke.",
|
"unsupportedFileType": "Filtypen støttes ikke.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
|
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
|
||||||
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
|
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
|
||||||
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
|
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
|
||||||
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
|
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Niet-ondersteund bestandstype.",
|
"unsupportedFileType": "Niet-ondersteund bestandstype.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
|
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
|
||||||
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
|
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
|
||||||
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
|
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
|
||||||
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
|
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Filtypen er ikkje støtta.",
|
"unsupportedFileType": "Filtypen er ikkje støtta.",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de l’URL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
|
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de l’URL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
|
||||||
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament ?",
|
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament ?",
|
||||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca ?",
|
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca ?",
|
||||||
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada."
|
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada.",
|
||||||
|
"collabOfflineWarning": "Cap de connexion pas disponibla.\nVòstras modificacions seràn pas salvadas !"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",
|
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"penMode": "Mòde estilo - empachar lo contact",
|
"penMode": "Mòde estilo - empachar lo contact",
|
||||||
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
|
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
|
||||||
"eraser": "Goma",
|
"eraser": "Goma",
|
||||||
"hand": ""
|
"hand": "Man (aisina de desplaçament de la vista)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del canabàs",
|
"canvasActions": "Accions del canabàs",
|
||||||
@@ -239,7 +240,7 @@
|
|||||||
"resize": "Podètz servar las proporcions en mantenent la tòca MAJ pendent lo redimensionament,\nmantenètz la tòca ALT per redimensionar a partir del centre",
|
"resize": "Podètz servar las proporcions en mantenent la tòca MAJ pendent lo redimensionament,\nmantenètz la tòca ALT per redimensionar a partir del centre",
|
||||||
"resizeImage": "Podètz retalhar liurament en quichant CTRL,\nquichatz ALT per retalhar a partir del centre",
|
"resizeImage": "Podètz retalhar liurament en quichant CTRL,\nquichatz ALT per retalhar a partir del centre",
|
||||||
"rotate": "Podètz restrénger los angles en mantenent MAJ pendent la rotacion",
|
"rotate": "Podètz restrénger los angles en mantenent MAJ pendent la rotacion",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Tenètz quichat Ctrl o Cmd e doble clic o quichatz Ctrl o Cmd + Entrada per modificar los ponches",
|
||||||
"lineEditor_pointSelected": "Quichar Suprimir per tirar lo(s) punt(s),\nCtrlOCmd+D per duplicar, o lisatz per desplaçar",
|
"lineEditor_pointSelected": "Quichar Suprimir per tirar lo(s) punt(s),\nCtrlOCmd+D per duplicar, o lisatz per desplaçar",
|
||||||
"lineEditor_nothingSelected": "Seleccionar un punt d’editar (manténer Maj. per ne seleccionar mantun),\no manténer Alt e clicar per n’apondre de novèls",
|
"lineEditor_nothingSelected": "Seleccionar un punt d’editar (manténer Maj. per ne seleccionar mantun),\no manténer Alt e clicar per n’apondre de novèls",
|
||||||
"placeImage": "Clicatz per plaçar l’imatge, o clicatz e lisatz per definir sa talha manualament",
|
"placeImage": "Clicatz per plaçar l’imatge, o clicatz e lisatz per definir sa talha manualament",
|
||||||
@@ -316,8 +317,8 @@
|
|||||||
"zoomToFit": "Zoomar per veire totes los elements",
|
"zoomToFit": "Zoomar per veire totes los elements",
|
||||||
"zoomToSelection": "Zoomar la seleccion",
|
"zoomToSelection": "Zoomar la seleccion",
|
||||||
"toggleElementLock": "Verrolhar/Desverrolhar la seleccion",
|
"toggleElementLock": "Verrolhar/Desverrolhar la seleccion",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Desplaçar la pagina ennaut/enbàs",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Desplaçar la pagina a esquèrra/drecha"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Escafar canabàs"
|
"title": "Escafar canabàs"
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
|
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
|
||||||
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||||
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
|
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"ar-SA": 92,
|
"ar-SA": 92,
|
||||||
"bg-BG": 54,
|
"bg-BG": 54,
|
||||||
"bn-BD": 60,
|
"bn-BD": 59,
|
||||||
"ca-ES": 93,
|
"ca-ES": 100,
|
||||||
"cs-CZ": 75,
|
"cs-CZ": 74,
|
||||||
"da-DK": 33,
|
"da-DK": 32,
|
||||||
"de-DE": 100,
|
"de-DE": 100,
|
||||||
"el-GR": 99,
|
"el-GR": 99,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-ES": 100,
|
"es-ES": 100,
|
||||||
"eu-ES": 99,
|
"eu-ES": 100,
|
||||||
"fa-IR": 95,
|
"fa-IR": 95,
|
||||||
"fi-FI": 92,
|
"fi-FI": 100,
|
||||||
"fr-FR": 100,
|
"fr-FR": 100,
|
||||||
"gl-ES": 100,
|
"gl-ES": 99,
|
||||||
"he-IL": 89,
|
"he-IL": 89,
|
||||||
"hi-IN": 71,
|
"hi-IN": 71,
|
||||||
"hu-HU": 89,
|
"hu-HU": 88,
|
||||||
"id-ID": 99,
|
"id-ID": 99,
|
||||||
"it-IT": 100,
|
"it-IT": 100,
|
||||||
"ja-JP": 99,
|
"ja-JP": 100,
|
||||||
"kab-KAB": 94,
|
"kab-KAB": 93,
|
||||||
"kk-KZ": 20,
|
"kk-KZ": 20,
|
||||||
"ko-KR": 98,
|
"ko-KR": 98,
|
||||||
"ku-TR": 95,
|
"ku-TR": 95,
|
||||||
@@ -31,22 +31,22 @@
|
|||||||
"nb-NO": 100,
|
"nb-NO": 100,
|
||||||
"nl-NL": 90,
|
"nl-NL": 90,
|
||||||
"nn-NO": 89,
|
"nn-NO": 89,
|
||||||
"oc-FR": 97,
|
"oc-FR": 98,
|
||||||
"pa-IN": 83,
|
"pa-IN": 82,
|
||||||
"pl-PL": 84,
|
"pl-PL": 84,
|
||||||
"pt-BR": 97,
|
"pt-BR": 100,
|
||||||
"pt-PT": 99,
|
"pt-PT": 100,
|
||||||
"ro-RO": 99,
|
"ro-RO": 100,
|
||||||
"ru-RU": 100,
|
"ru-RU": 100,
|
||||||
"si-LK": 8,
|
"si-LK": 8,
|
||||||
"sk-SK": 100,
|
"sk-SK": 100,
|
||||||
"sl-SI": 100,
|
"sl-SI": 100,
|
||||||
"sv-SE": 100,
|
"sv-SE": 100,
|
||||||
"ta-IN": 92,
|
"ta-IN": 94,
|
||||||
"tr-TR": 97,
|
"tr-TR": 97,
|
||||||
"uk-UA": 96,
|
"uk-UA": 96,
|
||||||
"vi-VN": 20,
|
"vi-VN": 20,
|
||||||
"zh-CN": 100,
|
"zh-CN": 100,
|
||||||
"zh-HK": 26,
|
"zh-HK": 25,
|
||||||
"zh-TW": 100
|
"zh-TW": 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
|
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
|
||||||
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
|
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
|
||||||
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
|
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
|
||||||
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona."
|
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nieobsługiwany typ pliku.",
|
"unsupportedFileType": "Nieobsługiwany typ pliku.",
|
||||||
|
|||||||
+12
-11
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
|
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
|
||||||
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
|
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
|
||||||
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
|
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
|
||||||
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada."
|
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.",
|
||||||
|
"collabOfflineWarning": "Sem conexão com a internet disponível.\nSuas alterações não serão salvas!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de arquivo não suportado.",
|
"unsupportedFileType": "Tipo de arquivo não suportado.",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"penMode": "Modo caneta — impede o toque",
|
"penMode": "Modo caneta — impede o toque",
|
||||||
"link": "Adicionar/Atualizar link para uma forma selecionada",
|
"link": "Adicionar/Atualizar link para uma forma selecionada",
|
||||||
"eraser": "Borracha",
|
"eraser": "Borracha",
|
||||||
"hand": ""
|
"hand": "Mão (ferramenta de rolagem)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ações da tela",
|
"canvasActions": "Ações da tela",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta ou use a ferramenta de mão",
|
||||||
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
||||||
"freeDraw": "Toque e arraste, solte quando terminar",
|
"freeDraw": "Toque e arraste, solte quando terminar",
|
||||||
"text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
"text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
||||||
@@ -247,7 +248,7 @@
|
|||||||
"bindTextToElement": "Pressione Enter para adicionar o texto",
|
"bindTextToElement": "Pressione Enter para adicionar o texto",
|
||||||
"deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar",
|
"deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar",
|
||||||
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão",
|
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "Esse recurso pode ser ativado configurando a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Não é possível mostrar pré-visualização",
|
"cannotShowPreview": "Não é possível mostrar pré-visualização",
|
||||||
@@ -450,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Todos os dados são salvos localmente no seu navegador.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Você queria ir para o Excalidraw+ em vez disso?",
|
||||||
"menuHint": ""
|
"menuHint": "Exportar, preferências, idiomas..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Exportar, preferências e mais...",
|
||||||
"center_heading": "",
|
"center_heading": "Diagramas, Feito. Simples.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Escolha uma ferramenta e comece a desenhar!",
|
||||||
"helpHint": ""
|
"helpHint": "Atalhos e ajuda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
|
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
|
||||||
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
|
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
|
||||||
"removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?",
|
"removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?",
|
||||||
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada."
|
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada.",
|
||||||
|
"collabOfflineWarning": "Sem ligação à internet disponível.\nAs suas alterações não serão salvas!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de ficheiro não suportado.",
|
"unsupportedFileType": "Tipo de ficheiro não suportado.",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"penMode": "Modo caneta - impedir toque",
|
"penMode": "Modo caneta - impedir toque",
|
||||||
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
|
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
|
||||||
"eraser": "Borracha",
|
"eraser": "Borracha",
|
||||||
"hand": ""
|
"hand": "Mão (ferramenta de movimento da tela)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ações da área de desenho",
|
"canvasActions": "Ações da área de desenho",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta, ou use a ferramenta da mão",
|
||||||
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
||||||
"freeDraw": "Clique e arraste, large quando terminar",
|
"freeDraw": "Clique e arraste, large quando terminar",
|
||||||
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
||||||
@@ -247,7 +248,7 @@
|
|||||||
"bindTextToElement": "Carregue Enter para acrescentar texto",
|
"bindTextToElement": "Carregue Enter para acrescentar texto",
|
||||||
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
|
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
|
||||||
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados",
|
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "Esta função pode provavelmente ser ativada definindo a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",
|
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
|
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
|
||||||
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
|
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
|
||||||
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
|
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
|
||||||
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată."
|
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată.",
|
||||||
|
"collabOfflineWarning": "Nu este disponibilă nicio conexiune la internet.\nModificările nu vor fi salvate!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tip de fișier neacceptat.",
|
"unsupportedFileType": "Tip de fișier neacceptat.",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"penMode": "Mod stilou – împiedică atingerea",
|
"penMode": "Mod stilou – împiedică atingerea",
|
||||||
"link": "Adăugare/actualizare URL pentru forma selectată",
|
"link": "Adăugare/actualizare URL pentru forma selectată",
|
||||||
"eraser": "Radieră",
|
"eraser": "Radieră",
|
||||||
"hand": ""
|
"hand": "Mână (instrument de panoramare)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acțiuni pentru pânză",
|
"canvasActions": "Acțiuni pentru pânză",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Forme"
|
"shapes": "Forme"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu sau folosește instrumentul în formă de mână",
|
||||||
"linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie",
|
"linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie",
|
||||||
"freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat",
|
"freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat",
|
||||||
"text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție",
|
"text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție",
|
||||||
@@ -247,7 +248,7 @@
|
|||||||
"bindTextToElement": "Apasă tasta Enter pentru a adăuga text",
|
"bindTextToElement": "Apasă tasta Enter pentru a adăuga text",
|
||||||
"deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea",
|
"deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea",
|
||||||
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere",
|
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "Această caracteristică poate fi probabil activată prin setarea preferinței „dom.events.asyncClipboard.clipboardItem” ca „true”. Pentru a schimba preferințele navigatorului în Firefox, accesează pagina „about:config”."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Nu se poate afișa previzualizarea",
|
"cannotShowPreview": "Nu se poate afișa previzualizarea",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Невозможно импортировать сцену с предоставленного URL. Неверный формат, или не содержит верных Excalidraw JSON данных.",
|
"invalidSceneUrl": "Невозможно импортировать сцену с предоставленного URL. Неверный формат, или не содержит верных Excalidraw JSON данных.",
|
||||||
"resetLibrary": "Это очистит вашу библиотеку. Вы уверены?",
|
"resetLibrary": "Это очистит вашу библиотеку. Вы уверены?",
|
||||||
"removeItemsFromsLibrary": "Удалить {{count}} объект(ов) из библиотеки?",
|
"removeItemsFromsLibrary": "Удалить {{count}} объект(ов) из библиотеки?",
|
||||||
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено."
|
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено.",
|
||||||
|
"collabOfflineWarning": "Отсутствует интернет-соединение.\nВаши изменения не будут сохранены!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Неподдерживаемый тип файла.",
|
"unsupportedFileType": "Неподдерживаемый тип файла.",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user