fix: merge branch 'master' into alex-kim-dev/flip-multiple-elements

This commit is contained in:
Alex Kim 2023-05-04 20:16:33 +05:00
commit 0ca1140e13
No known key found for this signature in database
GPG key ID: CEE74CFA44D238D7
100 changed files with 985 additions and 453 deletions

View file

@ -31,10 +31,29 @@ You can pass `null` / `undefined` if not applicable.
restoreElements( restoreElements(
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp; elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp; localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
refreshDimensions?: boolean<br/> opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
) )
</pre> </pre>
| Prop | Type | Description |
| ---- | ---- | ---- |
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> &#124; null &#124; undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
#### localElements
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
#### opts
The extra optional parameter to configure restored elements. It has the following attributes
| Prop | Type | Description|
| --- | --- | ------|
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
**_How to use_** **_How to use_**
```js ```js
@ -43,9 +62,6 @@ import { restoreElements } from "@excalidraw/excalidraw";
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value. This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates.
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
### restore ### restore
@ -56,7 +72,9 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
restore( restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp; data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a> localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
) )
</pre> </pre>

View file

@ -339,3 +339,47 @@ The `device` has the following `attributes`
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` | | `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices | | `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` | | `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
### i18n
To help with localization, we export the following.
| name | type |
| --- | --- |
| `defaultLang` | `string` |
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
```js
import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
```
#### defaultLang
Default language code, `en`.
#### languages
List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode).
#### useI18n
A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of `<Excalidraw>`.
```jsx live
function App() {
const { t } = useI18n();
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<button
style={{ position: "absolute", zIndex: 10, height: "2rem" }}
onClick={() => window.alert(t("labels.madeWithExcalidraw"))}
>
{t("buttons.confirm")}
</button>
</Excalidraw>
</div>
);
}
```

View file

@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0", "@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0", "@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.14.2", "@excalidraw/excalidraw": "0.15.2",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3", "docusaurus-plugin-sass": "0.2.3",

View file

@ -24,6 +24,7 @@ const ExcalidrawScope = {
Sidebar: ExcalidrawComp.Sidebar, Sidebar: ExcalidrawComp.Sidebar,
exportToCanvas: ExcalidrawComp.exportToCanvas, exportToCanvas: ExcalidrawComp.exportToCanvas,
initialData, initialData,
useI18n: ExcalidrawComp.useI18n,
}; };
export default ExcalidrawScope; export default ExcalidrawScope;

View file

@ -1631,10 +1631,10 @@
url-loader "^4.1.1" url-loader "^4.1.1"
webpack "^5.73.0" webpack "^5.73.0"
"@excalidraw/excalidraw@0.14.2": "@excalidraw/excalidraw@0.15.2":
version "0.14.2" version "0.15.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375" resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg== integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
"@hapi/hoek@^9.0.0": "@hapi/hoek@^9.0.0":
version "9.3.0" version "9.3.0"
@ -7159,9 +7159,9 @@ typescript@^4.7.4:
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
ua-parser-js@^0.7.30: ua-parser-js@^0.7.30:
version "0.7.31" version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
unescape@^1.0.1: unescape@^1.0.1:
version "1.0.1" version "1.0.1"

View file

@ -150,6 +150,14 @@
</script> </script>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %> <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
<!-- Fathom - privacy-friendly analytics -->
<script
src="https://cdn.usefathom.com/script.js"
data-site="VMSBUEYA"
defer
></script>
<!-- / Fathom -->
<!-- LEGACY GOOGLE ANALYTICS --> <!-- LEGACY GOOGLE ANALYTICS -->
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> <% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<script <script
@ -166,31 +174,6 @@
</script> </script>
<% } %> <% } %>
<!-- end LEGACY GOOGLE ANALYTICS --> <!-- end LEGACY GOOGLE ANALYTICS -->
<!-- Matomo -->
<% if (process.env.REACT_APP_MATOMO_URL &&
process.env.REACT_APP_MATOMO_SITE_ID &&
process.env.REACT_APP_CDN_MATOMO_TRACKER_URL) { %>
<script>
var _paq = (window._paq = window._paq || []);
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
var u = "%REACT_APP_MATOMO_URL%";
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", "%REACT_APP_MATOMO_SITE_ID%"]);
var d = document,
g = d.createElement("script"),
s = d.getElementsByTagName("script")[0];
g.async = true;
g.src = "%REACT_APP_CDN_MATOMO_TRACKER_URL%";
s.parentNode.insertBefore(g, s);
})();
</script>
<% } %>
<!-- end Matomo analytics -->
<% } %> <% } %>
<!-- FIXME: remove this when we update CRA (fix SW caching) --> <!-- FIXME: remove this when we update CRA (fix SW caching) -->
@ -244,5 +227,17 @@
<h1 class="visually-hidden">Excalidraw</h1> <h1 class="visually-hidden">Excalidraw</h1>
</header> </header>
<div id="root"></div> <div id="root"></div>
<!-- 100% privacy friendly analytics -->
<script
async
defer
src="https://scripts.simpleanalyticscdn.com/latest.js"
></script>
<noscript
><img
src="https://queue.simpleanalyticscdn.com/noscript.gif"
alt=""
referrerpolicy="no-referrer-when-downgrade"
/></noscript>
</body> </body>
</html> </html>

View file

@ -1,22 +1,9 @@
const fs = require("fs");
const { execSync } = require("child_process"); const { execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`; const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage); const pkg = require(excalidrawPackage);
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
const updateReadme = () => {
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
// remove note for stable readme
const data = originalReadMe.slice(excalidrawIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
const publish = () => { const publish = () => {
try { try {
execSync(`yarn --frozen-lockfile`); execSync(`yarn --frozen-lockfile`);
@ -30,15 +17,8 @@ const publish = () => {
}; };
const release = () => { const release = () => {
updateReadme();
console.info("Note for stable readme removed");
publish(); publish();
console.info(`Published ${pkg.version}!`); console.info(`Published ${pkg.version}!`);
// revert readme after release
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
console.info("Readme reverted");
}; };
release(); release();

View file

@ -16,6 +16,7 @@ import {
import { import {
getOriginalContainerHeightFromCache, getOriginalContainerHeightFromCache,
resetOriginalContainerCache, resetOriginalContainerCache,
updateOriginalContainerCache,
} from "../element/textWysiwyg"; } from "../element/textWysiwyg";
import { import {
hasBoundTextElement, hasBoundTextElement,
@ -145,7 +146,11 @@ export const actionBindText = register({
id: textElement.id, id: textElement.id,
}), }),
}); });
const originalContainerHeight = container.height;
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(textElement, container);
// overwritting the cache with original container height so
// it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight);
return { return {
elements: pushTextAboveContainer(elements, container, textElement), elements: pushTextAboveContainer(elements, container, textElement),
@ -191,8 +196,8 @@ const pushContainerBelowText = (
return updatedElements; return updatedElements;
}; };
export const actionCreateContainerFromText = register({ export const actionWrapTextInContainer = register({
name: "createContainerFromText", name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText", contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState) => {
@ -281,6 +286,7 @@ export const actionCreateContainerFromText = register({
containerId: container.id, containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null, boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
}, },
false, false,
); );

View file

@ -18,7 +18,7 @@ export const actionCopy = register({
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, true); const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(selectedElements, appState, app.files); copyToClipboard(selectedElements, app.files);
return { return {
commitToHistory: false, commitToHistory: false,

View file

@ -84,7 +84,7 @@ import {
isSomeElementSelected, isSomeElementSelected,
} from "../scene"; } from "../scene";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { arrayToMap } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@ -314,9 +314,9 @@ export const actionChangeFillStyle = register({
}, },
PanelComponent: ({ elements, appState, updateData }) => { PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
const allElementsZigZag = selectedElements.every( const allElementsZigZag =
(el) => el.fillStyle === "zigzag", selectedElements.length > 0 &&
); selectedElements.every((el) => el.fillStyle === "zigzag");
return ( return (
<fieldset> <fieldset>
@ -326,7 +326,9 @@ export const actionChangeFillStyle = register({
options={[ options={[
{ {
value: "hachure", value: "hachure",
text: t("labels.hachure"), text: `${
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
} (${getShortcutKey("Alt-Click")})`,
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon, icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
active: allElementsZigZag ? true : undefined, active: allElementsZigZag ? true : undefined,
}, },

View file

@ -115,7 +115,7 @@ export type ActionName =
| "toggleLinearEditor" | "toggleLinearEditor"
| "toggleEraserTool" | "toggleEraserTool"
| "toggleHandTool" | "toggleHandTool"
| "createContainerFromText"; | "wrapTextInContainer";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];

View file

@ -20,9 +20,20 @@ export const trackEvent = (
}); });
} }
// MATOMO event tracking _paq must be same as the one in index.html if (window.sa_event) {
if (window._paq) { window.sa_event(action, {
window._paq.push(["trackEvent", category, action, label, value]); category,
label,
value,
});
}
if (window.fathom) {
window.fathom.trackEvent(action, {
category,
label,
value,
});
} }
} catch (error) { } catch (error) {
console.error("error during analytics", error); console.error("error during analytics", error);

View file

@ -1,5 +1,6 @@
import oc from "open-color"; import oc from "open-color";
import { import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
@ -23,18 +24,18 @@ export const getDefaultAppState = (): Omit<
theme: THEME.LIGHT, theme: THEME.LIGHT,
collaborators: new Map(), collaborators: new Map(),
currentChartType: "bar", currentChartType: "bar",
currentItemBackgroundColor: "transparent", currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
currentItemEndArrowhead: "arrow", currentItemEndArrowhead: "arrow",
currentItemFillStyle: "hachure", currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
currentItemFontFamily: DEFAULT_FONT_FAMILY, currentItemFontFamily: DEFAULT_FONT_FAMILY,
currentItemFontSize: DEFAULT_FONT_SIZE, currentItemFontSize: DEFAULT_FONT_SIZE,
currentItemOpacity: 100, currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
currentItemRoughness: 1, currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
currentItemStartArrowhead: null, currentItemStartArrowhead: null,
currentItemStrokeColor: oc.black, currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
currentItemRoundness: "round", currentItemRoundness: "round",
currentItemStrokeStyle: "solid", currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
currentItemStrokeWidth: 1, currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN, currentItemTextAlign: DEFAULT_TEXT_ALIGN,
cursorButton: "up", cursorButton: "up",
draggingElement: null, draggingElement: null,
@ -44,7 +45,7 @@ export const getDefaultAppState = (): Omit<
activeTool: { activeTool: {
type: "selection", type: "selection",
customType: null, customType: null,
locked: false, locked: DEFAULT_ELEMENT_PROPS.locked,
lastActiveTool: null, lastActiveTool: null,
}, },
penMode: false, penMode: false,

View file

@ -1,10 +1,5 @@
import colors from "./colors"; import colors from "./colors";
import { import { DEFAULT_FONT_SIZE, ENV } from "./constants";
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
ENV,
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element"; import { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types"; import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random"; import { randomId } from "./random";
@ -166,17 +161,7 @@ const bgColors = colors.elementBackground.slice(
// Put all the common properties here so when the whole chart is selected // Put all the common properties here so when the whole chart is selected
// the properties dialog shows the correct selected values // the properties dialog shows the correct selected values
const commonProps = { const commonProps = {
fillStyle: "hachure",
fontFamily: DEFAULT_FONT_FAMILY,
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: colors.elementStroke[0], strokeColor: colors.elementStroke[0],
roundness: null,
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
locked: false,
} as const; } as const;
const getChartDimentions = (spreadsheet: Spreadsheet) => { const getChartDimentions = (spreadsheet: Spreadsheet) => {
@ -323,7 +308,6 @@ const chartBaseElements = (
x: x + chartWidth / 2, x: x + chartWidth / 2,
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE, y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
roundness: null, roundness: null,
strokeStyle: "solid",
textAlign: "center", textAlign: "center",
}) })
: null; : null;

View file

@ -2,12 +2,12 @@ import {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import { AppState, BinaryFiles } from "./types"; import { BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export"; import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks"; import { isInitializedImageElement } from "./element/typeChecks";
import { isPromiseLike } from "./utils"; import { isPromiseLike, isTestEnv } from "./utils";
type ElementsClipboard = { type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@ -55,24 +55,40 @@ const clipboardContainsElements = (
export const copyToClipboard = async ( export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles | null, files: BinaryFiles | null,
) => { ) => {
let foundFile = false;
const _files = elements.reduce((acc, element) => {
if (isInitializedImageElement(element)) {
foundFile = true;
if (files && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
}
return acc;
}, {} as BinaryFiles);
if (foundFile && !files) {
console.warn(
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
);
}
// select binded text elements when copying // select binded text elements when copying
const contents: ElementsClipboard = { const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard, type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements, elements,
files: files files: files ? _files : undefined,
? elements.reduce((acc, element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
return acc;
}, {} as BinaryFiles)
: undefined,
}; };
const json = JSON.stringify(contents); const json = JSON.stringify(contents);
if (isTestEnv()) {
return json;
}
CLIPBOARD = json; CLIPBOARD = json;
try { try {
PREFER_APP_CLIPBOARD = false; PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json); await copyTextToSystemClipboard(json);

View file

@ -60,6 +60,7 @@ import {
ENV, ENV,
EVENT, EVENT,
GRID_SIZE, GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
isAndroid, isAndroid,
isBrave, isBrave,
@ -295,7 +296,7 @@ import {
} from "../actions/actionCanvas"; } 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 { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
const deviceContextInitialValue = { const deviceContextInitialValue = {
@ -1589,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
elements: data.elements, elements: data.elements,
files: data.files || null, files: data.files || null,
position: "cursor", position: "cursor",
retainSeed: isPlainPaste,
}); });
} else if (data.text) { } else if (data.text) {
this.addTextFromPaste(data.text, isPlainPaste); this.addTextFromPaste(data.text, isPlainPaste);
@ -1602,6 +1604,7 @@ class App extends React.Component<AppProps, AppState> {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
files: BinaryFiles | null; files: BinaryFiles | null;
position: { clientX: number; clientY: number } | "cursor" | "center"; position: { clientX: number; clientY: number } | "cursor" | "center";
retainSeed?: boolean;
}) => { }) => {
const elements = restoreElements(opts.elements, null); const elements = restoreElements(opts.elements, null);
const [minX, minY, maxX, maxY] = getCommonBounds(elements); const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@ -1639,6 +1642,9 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + gridY - minY, y: element.y + gridY - minY,
}); });
}), }),
{
randomizeSeed: !opts.retainSeed,
},
); );
const nextElements = [ const nextElements = [
@ -2732,7 +2738,6 @@ class App extends React.Component<AppProps, AppState> {
strokeStyle: this.state.currentItemStrokeStyle, strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness, roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity, opacity: this.state.currentItemOpacity,
roundness: null,
text: "", text: "",
fontSize, fontSize,
fontFamily, fontFamily,
@ -2744,8 +2749,8 @@ class App extends React.Component<AppProps, AppState> {
: DEFAULT_VERTICAL_ALIGN, : DEFAULT_VERTICAL_ALIGN,
containerId: shouldBindToContainer ? container?.id : undefined, containerId: shouldBindToContainer ? container?.id : undefined,
groupIds: container?.groupIds ?? [], groupIds: container?.groupIds ?? [],
locked: false,
lineHeight, lineHeight,
angle: container?.angle ?? 0,
}); });
if (!existingTextElement && shouldBindToContainer && container) { if (!existingTextElement && shouldBindToContainer && container) {
@ -4721,7 +4726,12 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.drag.hasOccurred = true; pointerDownState.drag.hasOccurred = true;
// prevent dragging even if we're no longer holding cmd/ctrl otherwise // prevent dragging even if we're no longer holding cmd/ctrl otherwise
// it would have weird results (stuff jumping all over the screen) // it would have weird results (stuff jumping all over the screen)
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) { // Checking for editingElement to avoid jump while editing on mobile #6503
if (
selectedElements.length > 0 &&
!pointerDownState.withCmdOrCtrl &&
!this.state.editingElement
) {
const [dragX, dragY] = getGridPoint( const [dragX, dragY] = getGridPoint(
pointerCoords.x - pointerDownState.drag.offset.x, pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y, pointerCoords.y - pointerDownState.drag.offset.y,
@ -5744,7 +5754,9 @@ class App extends React.Component<AppProps, AppState> {
const imageFile = await fileOpen({ const imageFile = await fileOpen({
description: "Image", description: "Image",
extensions: ["jpg", "png", "svg", "gif"], extensions: Object.keys(
IMAGE_MIME_TYPES,
) as (keyof typeof IMAGE_MIME_TYPES)[],
}); });
const imageElement = this.createImageElement({ const imageElement = this.createImageElement({
@ -6366,7 +6378,7 @@ class App extends React.Component<AppProps, AppState> {
actionGroup, actionGroup,
actionUnbindText, actionUnbindText,
actionBindText, actionBindText,
actionCreateContainerFromText, actionWrapTextInContainer,
actionUngroup, actionUngroup,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionAddToLibrary, actionAddToLibrary,

View file

@ -183,6 +183,7 @@
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
font-family: inherit;
background-color: transparent; background-color: transparent;
color: var(--text-primary-color); color: var(--text-primary-color);
border: 0; border: 0;

View file

@ -30,6 +30,7 @@
background-color: transparent; background-color: transparent;
border: none; border: none;
white-space: nowrap; white-space: nowrap;
font-family: inherit;
display: grid; display: grid;
grid-template-columns: 1fr 0.2fr; grid-template-columns: 1fr 0.2fr;

View file

@ -4,7 +4,6 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types"; import { AppState, BinaryFiles } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { clipboard } from "./icons"; import { clipboard } from "./icons";
@ -15,6 +14,7 @@ import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { exportToCanvas } from "../packages/utils";
const supportsContextFilters = const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!; "filter" in document.createElement("canvas").getContext("2d")!;
@ -83,7 +83,6 @@ const ImageExportModal = ({
const someElementIsSelected = isSomeElementSelected(elements, appState); const someElementIsSelected = isSomeElementSelected(elements, appState);
const [exportSelected, setExportSelected] = useState(someElementIsSelected); const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
const { exportBackground, viewBackgroundColor } = appState;
const [renderError, setRenderError] = useState<Error | null>(null); const [renderError, setRenderError] = useState<Error | null>(null);
const exportedElements = exportSelected const exportedElements = exportSelected
@ -99,10 +98,16 @@ const ImageExportModal = ({
if (!previewNode) { if (!previewNode) {
return; return;
} }
exportToCanvas(exportedElements, appState, files, { const maxWidth = previewNode.offsetWidth;
exportBackground, if (!maxWidth) {
viewBackgroundColor, return;
}
exportToCanvas({
elements: exportedElements,
appState,
files,
exportPadding, exportPadding,
maxWidthOrHeight: maxWidth,
}) })
.then((canvas) => { .then((canvas) => {
setRenderError(null); setRenderError(null);
@ -116,14 +121,7 @@ const ImageExportModal = ({
console.error(error); console.error(error);
setRenderError(error); setRenderError(error);
}); });
}, [ }, [appState, files, exportedElements, exportPadding]);
appState,
files,
exportedElements,
exportBackground,
exportPadding,
viewBackgroundColor,
]);
return ( return (
<div className="ExportDialog"> <div className="ExportDialog">

View file

@ -102,7 +102,7 @@ const LibraryMenuItems = ({
...item, ...item,
// duplicate each library item before inserting on canvas to confine // duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465 // ids and bindings to each library item. See #6465
elements: duplicateElements(item.elements), elements: duplicateElements(item.elements, { randomizeSeed: true }),
}; };
}); });
}; };

View file

@ -2,6 +2,9 @@
// container in body where the actual tooltip is appended to // container in body where the actual tooltip is appended to
.excalidraw-tooltip { .excalidraw-tooltip {
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;

View file

@ -1,6 +1,7 @@
import cssVariables from "./css/variables.module.scss"; import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types"; import { AppProps } from "./types";
import { FontFamilyValues } from "./element/types"; import { ExcalidrawElement, FontFamilyValues } from "./element/types";
import oc from "open-color";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const isWindows = /^Win/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform);
@ -104,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20; // TODO make it configurable? export const GRID_SIZE = 20; // TODO make it configurable?
export const MIME_TYPES = { export const IMAGE_MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
json: "application/json",
svg: "image/svg+xml", svg: "image/svg+xml",
"excalidraw.svg": "image/svg+xml",
png: "image/png", png: "image/png",
"excalidraw.png": "image/png",
jpg: "image/jpeg", jpg: "image/jpeg",
gif: "image/gif", gif: "image/gif",
webp: "image/webp", webp: "image/webp",
bmp: "image/bmp", bmp: "image/bmp",
ico: "image/x-icon", ico: "image/x-icon",
avif: "image/avif",
jfif: "image/jfif",
} as const;
export const MIME_TYPES = {
json: "application/json",
// excalidraw data
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
// image-encoded excalidraw data
"excalidraw.svg": "image/svg+xml",
"excalidraw.png": "image/png",
// binary
binary: "application/octet-stream", binary: "application/octet-stream",
// image
...IMAGE_MIME_TYPES,
} as const; } as const;
export const EXPORT_DATA_TYPES = { export const EXPORT_DATA_TYPES = {
@ -188,16 +199,6 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440; export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
export const ALLOWED_IMAGE_MIME_TYPES = [
MIME_TYPES.png,
MIME_TYPES.jpg,
MIME_TYPES.svg,
MIME_TYPES.gif,
MIME_TYPES.webp,
MIME_TYPES.bmp,
MIME_TYPES.ico,
] as const;
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024; export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg"; export const SVG_NS = "http://www.w3.org/2000/svg";
@ -254,3 +255,23 @@ export const ROUNDNESS = {
/** key containt id of precedeing elemnt id we use in reconciliation during /** key containt id of precedeing elemnt id we use in reconciliation during
* collaboration */ * collaboration */
export const PRECEDING_ELEMENT_KEY = "__precedingElement__"; export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
export const DEFAULT_ELEMENT_PROPS: {
strokeColor: ExcalidrawElement["strokeColor"];
backgroundColor: ExcalidrawElement["backgroundColor"];
fillStyle: ExcalidrawElement["fillStyle"];
strokeWidth: ExcalidrawElement["strokeWidth"];
strokeStyle: ExcalidrawElement["strokeStyle"];
roughness: ExcalidrawElement["roughness"];
opacity: ExcalidrawElement["opacity"];
locked: ExcalidrawElement["locked"];
} = {
strokeColor: oc.black,
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
locked: false,
};

View file

@ -354,6 +354,7 @@
border-radius: var(--space-factor); border-radius: var(--space-factor);
border: 1px solid var(--button-gray-2); border: 1px solid var(--button-gray-2);
font-size: 0.8rem; font-size: 0.8rem;
font-family: inherit;
outline: none; outline: none;
appearance: none; appearance: none;
background-image: var(--dropdown-icon); background-image: var(--dropdown-icon);
@ -413,6 +414,7 @@
bottom: 30px; bottom: 30px;
transform: translateX(-50%); transform: translateX(-50%);
pointer-events: all; pointer-events: all;
font-family: inherit;
&:hover { &:hover {
background-color: var(--button-hover-bg); background-color: var(--button-hover-bg);

View file

@ -1,6 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState"; import { cleanAppStateForExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants"; import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types"; import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors"; import { CanvasError } from "../errors";
@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
export const isSupportedImageFile = ( export const isSupportedImageFile = (
blob: Blob | null | undefined, blob: Blob | null | undefined,
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => { ): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
const { type } = blob || {}; const { type } = blob || {};
return ( return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
);
}; };
export const loadSceneOrLibraryFromBlob = async ( export const loadSceneOrLibraryFromBlob = async (
@ -157,7 +155,7 @@ export const loadSceneOrLibraryFromBlob = async (
}, },
localAppState, localAppState,
localElements, localElements,
{ repairBindings: true, refreshDimensions: true }, { repairBindings: true, refreshDimensions: false },
), ),
}; };
} else if (isValidLibrary(data)) { } else if (isValidLibrary(data)) {

View file

@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants";
import { AbortError } from "../errors"; import { AbortError } from "../errors";
import { debounce } from "../utils"; import { debounce } from "../utils";
type FILE_EXTENSION = type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
| "gif"
| "jpg"
| "png"
| "excalidraw.png"
| "svg"
| "excalidraw.svg"
| "json"
| "excalidraw"
| "excalidrawlib";
const INPUT_CHANGE_INTERVAL_MS = 500; const INPUT_CHANGE_INTERVAL_MS = 500;

View file

@ -20,7 +20,7 @@ import {
isTestEnv, isTestEnv,
} from "../utils"; } from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement"; import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups"; import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types"; import { AppState } from "../types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
@ -33,10 +33,17 @@ import {
measureText, measureText,
normalizeText, normalizeText,
wrapText, wrapText,
getMaxContainerWidth, getBoundTextMaxWidth,
getDefaultLineHeight, getDefaultLineHeight,
} from "./textElement"; } from "./textElement";
import { VERTICAL_ALIGN } from "../constants"; import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { isArrowElement } from "./typeChecks"; import { isArrowElement } from "./typeChecks";
import { MarkOptional, Merge, Mutable } from "../utility-types"; import { MarkOptional, Merge, Mutable } from "../utility-types";
@ -51,6 +58,15 @@ type ElementConstructorOpts = MarkOptional<
| "version" | "version"
| "versionNonce" | "versionNonce"
| "link" | "link"
| "strokeStyle"
| "fillStyle"
| "strokeColor"
| "backgroundColor"
| "roughness"
| "strokeWidth"
| "roundness"
| "locked"
| "opacity"
>; >;
const _newElementBase = <T extends ExcalidrawElement>( const _newElementBase = <T extends ExcalidrawElement>(
@ -58,13 +74,13 @@ const _newElementBase = <T extends ExcalidrawElement>(
{ {
x, x,
y, y,
strokeColor, strokeColor = DEFAULT_ELEMENT_PROPS.strokeColor,
backgroundColor, backgroundColor = DEFAULT_ELEMENT_PROPS.backgroundColor,
fillStyle, fillStyle = DEFAULT_ELEMENT_PROPS.fillStyle,
strokeWidth, strokeWidth = DEFAULT_ELEMENT_PROPS.strokeWidth,
strokeStyle, strokeStyle = DEFAULT_ELEMENT_PROPS.strokeStyle,
roughness, roughness = DEFAULT_ELEMENT_PROPS.roughness,
opacity, opacity = DEFAULT_ELEMENT_PROPS.opacity,
width = 0, width = 0,
height = 0, height = 0,
angle = 0, angle = 0,
@ -72,7 +88,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
roundness = null, roundness = null,
boundElements = null, boundElements = null,
link = null, link = null,
locked, locked = DEFAULT_ELEMENT_PROPS.locked,
...rest ...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">, }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => { ) => {
@ -138,27 +154,39 @@ const getTextElementPositionOffsets = (
export const newTextElement = ( export const newTextElement = (
opts: { opts: {
text: string; text: string;
fontSize: number; fontSize?: number;
fontFamily: FontFamilyValues; fontFamily?: FontFamilyValues;
textAlign: TextAlign; textAlign?: TextAlign;
verticalAlign: VerticalAlign; verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"]; containerId?: ExcalidrawTextContainer["id"];
lineHeight?: ExcalidrawTextElement["lineHeight"]; lineHeight?: ExcalidrawTextElement["lineHeight"];
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => { ): NonDeleted<ExcalidrawTextElement> => {
const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily); const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
const text = normalizeText(opts.text); const text = normalizeText(opts.text);
const metrics = measureText(text, getFontString(opts), lineHeight); const metrics = measureText(
const offsets = getTextElementPositionOffsets(opts, metrics); text,
getFontString({ fontFamily, fontSize }),
lineHeight,
);
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
const offsets = getTextElementPositionOffsets(
{ textAlign, verticalAlign },
metrics,
);
const textElement = newElementWith( const textElement = newElementWith(
{ {
..._newElementBase<ExcalidrawTextElement>("text", opts), ..._newElementBase<ExcalidrawTextElement>("text", opts),
text, text,
fontSize: opts.fontSize, fontSize,
fontFamily: opts.fontFamily, fontFamily,
textAlign: opts.textAlign, textAlign,
verticalAlign: opts.verticalAlign, verticalAlign,
x: opts.x - offsets.x, x: opts.x - offsets.x,
y: opts.y - offsets.y, y: opts.y - offsets.y,
width: metrics.width, width: metrics.width,
@ -282,7 +310,7 @@ export const refreshTextDimensions = (
text = wrapText( text = wrapText(
text, text,
getFontString(textElement), getFontString(textElement),
getMaxContainerWidth(container), getBoundTextMaxWidth(container),
); );
} }
const dimensions = getAdjustedDimensions(textElement, text); const dimensions = getAdjustedDimensions(textElement, text);
@ -511,8 +539,16 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
* it's advised to supply the whole elements array, or sets of elements that * it's advised to supply the whole elements array, or sets of elements that
* are encapsulated (such as library items), if the purpose is to retain * are encapsulated (such as library items), if the purpose is to retain
* bindings to the cloned elements intact. * bindings to the cloned elements intact.
*
* NOTE by default does not randomize or regenerate anything except the id.
*/ */
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { export const duplicateElements = (
elements: readonly ExcalidrawElement[],
opts?: {
/** NOTE also updates version flags and `updated` */
randomizeSeed: boolean;
},
) => {
const clonedElements: ExcalidrawElement[] = []; const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements); const origElementsMap = arrayToMap(elements);
@ -546,6 +582,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
clonedElement.id = maybeGetNewId(element.id)!; clonedElement.id = maybeGetNewId(element.id)!;
if (opts?.randomizeSeed) {
clonedElement.seed = randomInteger();
bumpVersion(clonedElement);
}
if (clonedElement.groupIds) { if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => { clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) { if (!groupNewIdsMap.has(groupId)) {

View file

@ -46,10 +46,10 @@ import {
getBoundTextElementId, getBoundTextElementId,
getContainerElement, getContainerElement,
handleBindTextResize, handleBindTextResize,
getMaxContainerWidth, getBoundTextMaxWidth,
getApproxMinLineHeight, getApproxMinLineHeight,
measureText, measureText,
getMaxContainerHeight, getBoundTextMaxHeight,
} from "./textElement"; } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
@ -210,7 +210,7 @@ const measureFontSizeFromWidth = (
if (hasContainer) { if (hasContainer) {
const container = getContainerElement(element); const container = getContainerElement(element);
if (container) { if (container) {
width = getMaxContainerWidth(container); width = getBoundTextMaxWidth(container);
} }
} }
const nextFontSize = element.fontSize * (nextWidth / width); const nextFontSize = element.fontSize * (nextWidth / width);
@ -441,8 +441,8 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth( const nextFont = measureFontSizeFromWidth(
boundTextElement, boundTextElement,
getMaxContainerWidth(updatedElement), getBoundTextMaxWidth(updatedElement),
getMaxContainerHeight(updatedElement), getBoundTextMaxHeight(updatedElement, boundTextElement),
); );
if (nextFont === null) { if (nextFont === null) {
return; return;

View file

@ -3,14 +3,15 @@ import { API } from "../tests/helpers/api";
import { import {
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
getContainerCoords, getContainerCoords,
getMaxContainerWidth, getBoundTextMaxWidth,
getMaxContainerHeight, getBoundTextMaxHeight,
wrapText, wrapText,
detectLineHeight, detectLineHeight,
getLineHeightInPx, getLineHeightInPx,
getDefaultLineHeight, getDefaultLineHeight,
parseTokens,
} from "./textElement"; } from "./textElement";
import { FontString } from "./types"; import { ExcalidrawTextElementWithContainer, FontString } from "./types";
describe("Test wrapText", () => { describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString; const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@ -183,6 +184,56 @@ now`,
expect(wrapText(text, font, -1)).toEqual(text); expect(wrapText(text, font, -1)).toEqual(text);
expect(wrapText(text, font, Infinity)).toEqual(text); expect(wrapText(text, font, Infinity)).toEqual(text);
}); });
it("should wrap the text correctly when text contains hyphen", () => {
let text =
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
const res = wrapText(text, font, 110);
expect(res).toBe(
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`,
);
text = "Hello thereusing-now";
expect(wrapText(text, font, 100)).toEqual("Hello \nthereusin\ng-now");
});
});
describe("Test parseTokens", () => {
it("should split into tokens correctly", () => {
let text = "Excalidraw is a virtual collaborative whiteboard";
expect(parseTokens(text)).toEqual([
"Excalidraw",
"is",
"a",
"virtual",
"collaborative",
"whiteboard",
]);
text =
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
expect(parseTokens(text)).toEqual([
"Wikipedia",
"is",
"hosted",
"by",
"Wikimedia-",
"",
"Foundation,",
"a",
"non-",
"profit",
"organization",
"that",
"also",
"hosts",
"a",
"range-",
"of",
"other",
"projects",
]);
});
}); });
describe("Test measureText", () => { describe("Test measureText", () => {
@ -260,7 +311,7 @@ describe("Test measureText", () => {
}); });
}); });
describe("Test getMaxContainerWidth", () => { describe("Test getBoundTextMaxWidth", () => {
const params = { const params = {
width: 178, width: 178,
height: 194, height: 194,
@ -268,39 +319,76 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => { it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params }); const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerWidth(container)).toBe(168); expect(getBoundTextMaxWidth(container)).toBe(168);
}); });
it("should return max width when container is ellipse", () => { it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params }); const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerWidth(container)).toBe(116); expect(getBoundTextMaxWidth(container)).toBe(116);
}); });
it("should return max width when container is diamond", () => { it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params }); const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerWidth(container)).toBe(79); expect(getBoundTextMaxWidth(container)).toBe(79);
}); });
}); });
describe("Test getMaxContainerHeight", () => { describe("Test getBoundTextMaxHeight", () => {
const params = { const params = {
width: 178, width: 178,
height: 194, height: 194,
id: '"container-id',
}; };
const boundTextElement = API.createElement({
type: "text",
id: "text-id",
x: 560.51171875,
y: 202.033203125,
width: 154,
height: 175,
fontSize: 20,
fontFamily: 1,
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
textAlign: "center",
verticalAlign: "middle",
containerId: params.id,
}) as ExcalidrawTextElementWithContainer;
it("should return max height when container is rectangle", () => { it("should return max height when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params }); const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerHeight(container)).toBe(184); expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
}); });
it("should return max height when container is ellipse", () => { it("should return max height when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params }); const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerHeight(container)).toBe(127); expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
}); });
it("should return max height when container is diamond", () => { it("should return max height when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params }); const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerHeight(container)).toBe(87); expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
});
it("should return max height when container is arrow", () => {
const container = API.createElement({
type: "arrow",
...params,
});
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
});
it("should return max height when container is arrow and height is less than threshold", () => {
const container = API.createElement({
type: "arrow",
...params,
height: 70,
boundElements: [{ type: "text", id: "text-id" }],
});
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
boundTextElement.height,
);
}); });
}); });
}); });

View file

@ -65,7 +65,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text; boundTextUpdates.text = textElement.text;
if (container) { if (container) {
maxWidth = getMaxContainerWidth(container); maxWidth = getBoundTextMaxWidth(container);
boundTextUpdates.text = wrapText( boundTextUpdates.text = wrapText(
textElement.originalText, textElement.originalText,
getFontString(textElement), getFontString(textElement),
@ -83,35 +83,28 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline; boundTextUpdates.baseline = metrics.baseline;
if (container) { if (container) {
if (isArrowElement(container)) { const containerDims = getContainerDims(container);
const centerX = textElement.x + textElement.width / 2; const maxContainerHeight = getBoundTextMaxHeight(
const centerY = textElement.y + textElement.height / 2; container,
const diffWidth = metrics.width - textElement.width; textElement as ExcalidrawTextElementWithContainer,
const diffHeight = metrics.height - textElement.height; );
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
} else {
const containerDims = getContainerDims(container);
let maxContainerHeight = getMaxContainerHeight(container);
let nextHeight = containerDims.height; let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) { if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText( nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
container.type, container.type,
); );
mutateElement(container, { height: nextHeight }); mutateElement(container, { height: nextHeight });
maxContainerHeight = getMaxContainerHeight(container); updateOriginalContainerCache(container.id, nextHeight);
updateOriginalContainerCache(container.id, nextHeight);
}
const updatedTextElement = {
...textElement,
...boundTextUpdates,
} as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
boundTextUpdates.x = x;
boundTextUpdates.y = y;
} }
const updatedTextElement = {
...textElement,
...boundTextUpdates,
} as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
boundTextUpdates.x = x;
boundTextUpdates.y = y;
} }
mutateElement(textElement, boundTextUpdates); mutateElement(textElement, boundTextUpdates);
@ -183,8 +176,11 @@ export const handleBindTextResize = (
let nextHeight = textElement.height; let nextHeight = textElement.height;
let nextWidth = textElement.width; let nextWidth = textElement.width;
const containerDims = getContainerDims(container); const containerDims = getContainerDims(container);
const maxWidth = getMaxContainerWidth(container); const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getMaxContainerHeight(container); const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let containerHeight = containerDims.height; let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline; let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") { if (transformHandleType !== "n" && transformHandleType !== "s") {
@ -256,8 +252,8 @@ export const computeBoundTextPosition = (
); );
} }
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container); const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getMaxContainerWidth(container); const maxContainerWidth = getBoundTextMaxWidth(container);
let x; let x;
let y; let y;
@ -419,6 +415,24 @@ export const getTextHeight = (
return getLineHeightInPx(fontSize, lineHeight) * lineCount; return getLineHeightInPx(fontSize, lineHeight) * lineCount;
}; };
export const parseTokens = (text: string) => {
// Splitting words containing "-" as those are treated as separate words
// by css wrapping algorithm eg non-profit => non-, profit
const words = text.split("-");
if (words.length > 1) {
// non-proft org => ['non-', 'profit org']
words.forEach((word, index) => {
if (index !== words.length - 1) {
words[index] = word += "-";
}
});
}
// Joining the words with space and splitting them again with space to get the
// final list of tokens
// ['non-', 'profit org'] =>,'non- proft org' => ['non-','profit','org']
return words.join(" ").split(" ");
};
export const wrapText = (text: string, font: FontString, maxWidth: number) => { export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// if maxWidth is not finite or NaN which can happen in case of bugs in // if maxWidth is not finite or NaN which can happen in case of bugs in
// computation, we need to make sure we don't continue as we'll end up // computation, we need to make sure we don't continue as we'll end up
@ -444,17 +458,16 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine = ""; currentLine = "";
currentLineWidthTillNow = 0; currentLineWidthTillNow = 0;
}; };
originalLines.forEach((originalLine) => { originalLines.forEach((originalLine) => {
const currentLineWidth = getTextWidth(originalLine, font); const currentLineWidth = getTextWidth(originalLine, font);
//Push the line if its <= maxWidth // Push the line if its <= maxWidth
if (currentLineWidth <= maxWidth) { if (currentLineWidth <= maxWidth) {
lines.push(originalLine); lines.push(originalLine);
return; // continue return; // continue
} }
const words = originalLine.split(" ");
const words = parseTokens(originalLine);
resetParams(); resetParams();
let index = 0; let index = 0;
@ -472,6 +485,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
else 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);
resetParams(); resetParams();
@ -492,15 +506,15 @@ 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);
resetParams(); resetParams();
} 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
// to previous line // to previous line unless the line ends with hyphen to sync
// with css word-wrap
} else if (!currentLine.endsWith("-")) {
currentLine += " "; currentLine += " ";
currentLineWidthTillNow += spaceWidth; currentLineWidthTillNow += spaceWidth;
} }
@ -518,12 +532,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
break; break;
} }
index++; index++;
currentLine += `${word} `;
// if word ends with "-" then we don't need to add space
// to sync with css word-wrap
const shouldAppendSpace = !word.endsWith("-");
currentLine += word;
if (shouldAppendSpace) {
currentLine += " ";
}
// Push the word if appending space exceeds max width // Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) { if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1); if (shouldAppendSpace) {
push(word); lines.push(currentLine.slice(0, -1));
} else {
lines.push(currentLine);
}
resetParams(); resetParams();
break; break;
} }
@ -861,18 +886,10 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding; return dimension + padding;
}; };
export const getMaxContainerWidth = (container: ExcalidrawElement) => { export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width; const width = getContainerDims(container).width;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2; return 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") { if (container.type === "ellipse") {
@ -889,16 +906,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => {
return width - BOUND_TEXT_PADDING * 2; return width - BOUND_TEXT_PADDING * 2;
}; };
export const getMaxContainerHeight = (container: ExcalidrawElement) => { export const getBoundTextMaxHeight = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const height = getContainerDims(container).height; const height = getContainerDims(container).height;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) { if (containerHeight <= 0) {
const boundText = getBoundTextElement(container); return boundTextElement.height;
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
} }
return height; return height;
} }

View file

@ -526,6 +526,36 @@ describe("textWysiwyg", () => {
]); ]);
}); });
it("should set the text element angle to same as container angle when binding to rotated container", async () => {
const rectangle = API.createElement({
type: "rectangle",
width: 90,
height: 75,
angle: 45,
});
h.elements = [rectangle];
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
expect(text.angle).toBe(rectangle.angle);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => { it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
const diamond = API.createElement({ const diamond = API.createElement({
type: "diamond", type: "diamond",
@ -1506,7 +1536,7 @@ describe("textWysiwyg", () => {
expect.objectContaining({ expect.objectContaining({
text: "Excalidraw is an opensource virtual collaborative whiteboard", text: "Excalidraw is an opensource virtual collaborative whiteboard",
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.LEFT, textAlign: TEXT_ALIGN.CENTER,
boundElements: null, boundElements: null,
}), }),
); );

View file

@ -32,8 +32,8 @@ import {
normalizeText, normalizeText,
redrawTextBoundingBox, redrawTextBoundingBox,
wrapText, wrapText,
getMaxContainerHeight, getBoundTextMaxHeight,
getMaxContainerWidth, getBoundTextMaxWidth,
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
detectLineHeight, detectLineHeight,
} from "./textElement"; } from "./textElement";
@ -205,8 +205,11 @@ export const textWysiwyg = ({
} }
} }
maxWidth = getMaxContainerWidth(container); maxWidth = getBoundTextMaxWidth(container);
maxHeight = getMaxContainerHeight(container); maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
);
// autogrow container height if text exceeds // autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) { if (!isArrowElement(container) && textElementHeight > maxHeight) {
@ -377,7 +380,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText( const wrappedText = wrapText(
`${editable.value}${data}`, `${editable.value}${data}`,
font, font,
getMaxContainerWidth(container), getBoundTextMaxWidth(container),
); );
const width = getTextWidth(wrappedText, font); const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`; editable.style.width = `${width}px`;
@ -394,7 +397,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText( const wrappedText = wrapText(
normalizeText(editable.value), normalizeText(editable.value),
font, font,
getMaxContainerWidth(container!), getBoundTextMaxWidth(container!),
); );
const { width, height } = measureText( const { width, height } = measureText(
wrappedText, wrappedText,

View file

@ -65,7 +65,7 @@ export const reconcileElements = (
// Mark duplicate for removal as it'll be replaced with the remote element // Mark duplicate for removal as it'll be replaced with the remote element
if (local) { if (local) {
// Unless the ramote and local elements are the same element in which case // Unless the remote and local elements are the same element in which case
// we need to keep it as we'd otherwise discard it from the resulting // we need to keep it as we'd otherwise discard it from the resulting
// array. // array.
if (local[0] === remoteElement) { if (local[0] === remoteElement) {

View file

@ -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, refreshDimensions: true }, { repairBindings: true, refreshDimensions: false },
); );
} else { } else {
data = restore(localDataState || null, null, null, { data = restore(localDataState || null, null, null, {

4
src/global.d.ts vendored
View file

@ -18,8 +18,8 @@ interface Window {
EXCALIDRAW_EXPORT_SOURCE: string; EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined; EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function; gtag: Function;
_paq: any[]; sa_event: Function;
_mtm: any[]; fathom: { trackEvent: Function };
} }
interface CanvasRenderingContext2D { interface CanvasRenderingContext2D {

View file

@ -54,6 +54,7 @@
"veryLarge": "كبير جدا", "veryLarge": "كبير جدا",
"solid": "كامل", "solid": "كامل",
"hachure": "خطوط", "hachure": "خطوط",
"zigzag": "",
"crossHatch": "خطوط متقطعة", "crossHatch": "خطوط متقطعة",
"thin": "نحيف", "thin": "نحيف",
"bold": "داكن", "bold": "داكن",

View file

@ -54,6 +54,7 @@
"veryLarge": "Много голям", "veryLarge": "Много голям",
"solid": "Солиден", "solid": "Солиден",
"hachure": "Хералдика", "hachure": "Хералдика",
"zigzag": "",
"crossHatch": "Двойно-пресечено", "crossHatch": "Двойно-пресечено",
"thin": "Тънък", "thin": "Тънък",
"bold": "Ясно очертан", "bold": "Ясно очертан",

View file

@ -54,6 +54,7 @@
"veryLarge": "অনেক বড়", "veryLarge": "অনেক বড়",
"solid": "দৃঢ়", "solid": "দৃঢ়",
"hachure": "ভ্রুলেখা", "hachure": "ভ্রুলেখা",
"zigzag": "",
"crossHatch": "ক্রস হ্যাচ", "crossHatch": "ক্রস হ্যাচ",
"thin": "পাতলা", "thin": "পাতলা",
"bold": "পুরু", "bold": "পুরু",

View file

@ -54,6 +54,7 @@
"veryLarge": "Molt gran", "veryLarge": "Molt gran",
"solid": "Sòlid", "solid": "Sòlid",
"hachure": "Ratlletes", "hachure": "Ratlletes",
"zigzag": "",
"crossHatch": "Ratlletes creuades", "crossHatch": "Ratlletes creuades",
"thin": "Fi", "thin": "Fi",
"bold": "Negreta", "bold": "Negreta",

View file

@ -54,6 +54,7 @@
"veryLarge": "Velmi velké", "veryLarge": "Velmi velké",
"solid": "Plný", "solid": "Plný",
"hachure": "", "hachure": "",
"zigzag": "",
"crossHatch": "", "crossHatch": "",
"thin": "Tenký", "thin": "Tenký",
"bold": "Tlustý", "bold": "Tlustý",

View file

@ -54,6 +54,7 @@
"veryLarge": "Meget stor", "veryLarge": "Meget stor",
"solid": "Solid", "solid": "Solid",
"hachure": "Skravering", "hachure": "Skravering",
"zigzag": "",
"crossHatch": "Krydsskravering", "crossHatch": "Krydsskravering",
"thin": "Tynd", "thin": "Tynd",
"bold": "Fed", "bold": "Fed",

View file

@ -54,6 +54,7 @@
"veryLarge": "Sehr groß", "veryLarge": "Sehr groß",
"solid": "Deckend", "solid": "Deckend",
"hachure": "Schraffiert", "hachure": "Schraffiert",
"zigzag": "Zickzack",
"crossHatch": "Kreuzschraffiert", "crossHatch": "Kreuzschraffiert",
"thin": "Dünn", "thin": "Dünn",
"bold": "Fett", "bold": "Fett",

View file

@ -54,6 +54,7 @@
"veryLarge": "Πολύ μεγάλο", "veryLarge": "Πολύ μεγάλο",
"solid": "Συμπαγής", "solid": "Συμπαγής",
"hachure": "Εκκόλαψη", "hachure": "Εκκόλαψη",
"zigzag": "",
"crossHatch": "Διασταυρούμενη εκκόλαψη", "crossHatch": "Διασταυρούμενη εκκόλαψη",
"thin": "Λεπτή", "thin": "Λεπτή",
"bold": "Έντονη", "bold": "Έντονη",

View file

@ -54,6 +54,7 @@
"veryLarge": "Very large", "veryLarge": "Very large",
"solid": "Solid", "solid": "Solid",
"hachure": "Hachure", "hachure": "Hachure",
"zigzag": "Zigzag",
"crossHatch": "Cross-hatch", "crossHatch": "Cross-hatch",
"thin": "Thin", "thin": "Thin",
"bold": "Bold", "bold": "Bold",

View file

@ -54,6 +54,7 @@
"veryLarge": "Muy grande", "veryLarge": "Muy grande",
"solid": "Sólido", "solid": "Sólido",
"hachure": "Folleto", "hachure": "Folleto",
"zigzag": "Zigzag",
"crossHatch": "Rayado transversal", "crossHatch": "Rayado transversal",
"thin": "Fino", "thin": "Fino",
"bold": "Grueso", "bold": "Grueso",
@ -207,8 +208,8 @@
"collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.",
"collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "start": "Parece que estás usando el navegador Brave",
"aggressive_block_fingerprint": "", "aggressive_block_fingerprint": "Bloquear huellas dactilares agresivamente",
"setting_enabled": "ajuste activado", "setting_enabled": "ajuste activado",
"break": "Esto podría resultar en romper los", "break": "Esto podría resultar en romper los",
"text_elements": "Elementos de texto", "text_elements": "Elementos de texto",
@ -319,8 +320,8 @@
"doubleClick": "doble clic", "doubleClick": "doble clic",
"drag": "arrastrar", "drag": "arrastrar",
"editor": "Editor", "editor": "Editor",
"editLineArrowPoints": "", "editLineArrowPoints": "Editar puntos de línea/flecha",
"editText": "", "editText": "Editar texto / añadir etiqueta",
"github": "¿Ha encontrado un problema? Envíelo", "github": "¿Ha encontrado un problema? Envíelo",
"howto": "Siga nuestras guías", "howto": "Siga nuestras guías",
"or": "o", "or": "o",

View file

@ -54,6 +54,7 @@
"veryLarge": "Oso handia", "veryLarge": "Oso handia",
"solid": "Solidoa", "solid": "Solidoa",
"hachure": "Itzalduna", "hachure": "Itzalduna",
"zigzag": "",
"crossHatch": "Marraduna", "crossHatch": "Marraduna",
"thin": "Mehea", "thin": "Mehea",
"bold": "Lodia", "bold": "Lodia",

View file

@ -54,6 +54,7 @@
"veryLarge": "بسیار بزرگ", "veryLarge": "بسیار بزرگ",
"solid": "توپر", "solid": "توپر",
"hachure": "هاشور", "hachure": "هاشور",
"zigzag": "",
"crossHatch": "هاشور متقاطع", "crossHatch": "هاشور متقاطع",
"thin": "نازک", "thin": "نازک",
"bold": "ضخیم", "bold": "ضخیم",

View file

@ -54,6 +54,7 @@
"veryLarge": "Erittäin suuri", "veryLarge": "Erittäin suuri",
"solid": "Yhtenäinen", "solid": "Yhtenäinen",
"hachure": "Vinoviivoitus", "hachure": "Vinoviivoitus",
"zigzag": "",
"crossHatch": "Ristiviivoitus", "crossHatch": "Ristiviivoitus",
"thin": "Ohut", "thin": "Ohut",
"bold": "Lihavoitu", "bold": "Lihavoitu",

View file

@ -54,6 +54,7 @@
"veryLarge": "Très grande", "veryLarge": "Très grande",
"solid": "Solide", "solid": "Solide",
"hachure": "Hachures", "hachure": "Hachures",
"zigzag": "",
"crossHatch": "Hachures croisées", "crossHatch": "Hachures croisées",
"thin": "Fine", "thin": "Fine",
"bold": "Épaisse", "bold": "Épaisse",
@ -319,8 +320,8 @@
"doubleClick": "double-clic", "doubleClick": "double-clic",
"drag": "glisser", "drag": "glisser",
"editor": "Éditeur", "editor": "Éditeur",
"editLineArrowPoints": "", "editLineArrowPoints": "Modifier les points de ligne/flèche",
"editText": "", "editText": "Modifier le texte / ajouter un libellé",
"github": "Problème trouvé ? Soumettre", "github": "Problème trouvé ? Soumettre",
"howto": "Suivez nos guides", "howto": "Suivez nos guides",
"or": "ou", "or": "ou",

View file

@ -54,6 +54,7 @@
"veryLarge": "Moi grande", "veryLarge": "Moi grande",
"solid": "Sólido", "solid": "Sólido",
"hachure": "Folleto", "hachure": "Folleto",
"zigzag": "",
"crossHatch": "Raiado transversal", "crossHatch": "Raiado transversal",
"thin": "Estreito", "thin": "Estreito",
"bold": "Groso", "bold": "Groso",

View file

@ -54,6 +54,7 @@
"veryLarge": "גדול מאוד", "veryLarge": "גדול מאוד",
"solid": "מוצק", "solid": "מוצק",
"hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה", "hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה",
"zigzag": "",
"crossHatch": "קווים מוצלבים שתי וערב", "crossHatch": "קווים מוצלבים שתי וערב",
"thin": "דק", "thin": "דק",
"bold": "מודגש", "bold": "מודגש",

View file

@ -54,6 +54,7 @@
"veryLarge": "बहुत बड़ा", "veryLarge": "बहुत बड़ा",
"solid": "दृढ़", "solid": "दृढ़",
"hachure": "हैशूर", "hachure": "हैशूर",
"zigzag": "तेढ़ी मेढ़ी",
"crossHatch": "क्रॉस हैच", "crossHatch": "क्रॉस हैच",
"thin": "पतला", "thin": "पतला",
"bold": "मोटा", "bold": "मोटा",

View file

@ -54,6 +54,7 @@
"veryLarge": "Nagyon nagy", "veryLarge": "Nagyon nagy",
"solid": "Kitöltött", "solid": "Kitöltött",
"hachure": "Vonalkázott", "hachure": "Vonalkázott",
"zigzag": "",
"crossHatch": "Keresztcsíkozott", "crossHatch": "Keresztcsíkozott",
"thin": "Vékony", "thin": "Vékony",
"bold": "Félkövér", "bold": "Félkövér",

View file

@ -54,6 +54,7 @@
"veryLarge": "Sangat besar", "veryLarge": "Sangat besar",
"solid": "Padat", "solid": "Padat",
"hachure": "Garis-garis", "hachure": "Garis-garis",
"zigzag": "",
"crossHatch": "Asiran silang", "crossHatch": "Asiran silang",
"thin": "Lembut", "thin": "Lembut",
"bold": "Tebal", "bold": "Tebal",

View file

@ -54,6 +54,7 @@
"veryLarge": "Molto grande", "veryLarge": "Molto grande",
"solid": "Pieno", "solid": "Pieno",
"hachure": "Tratteggio obliquo", "hachure": "Tratteggio obliquo",
"zigzag": "Zig zag",
"crossHatch": "Tratteggio incrociato", "crossHatch": "Tratteggio incrociato",
"thin": "Sottile", "thin": "Sottile",
"bold": "Grassetto", "bold": "Grassetto",
@ -319,7 +320,7 @@
"doubleClick": "doppio-click", "doubleClick": "doppio-click",
"drag": "trascina", "drag": "trascina",
"editor": "Editor", "editor": "Editor",
"editLineArrowPoints": "", "editLineArrowPoints": "Modifica punti linea/freccia",
"editText": "Modifica testo / aggiungi etichetta", "editText": "Modifica testo / aggiungi etichetta",
"github": "Trovato un problema? Segnalalo", "github": "Trovato un problema? Segnalalo",
"howto": "Segui le nostre guide", "howto": "Segui le nostre guide",

View file

@ -54,6 +54,7 @@
"veryLarge": "特大", "veryLarge": "特大",
"solid": "ベタ塗り", "solid": "ベタ塗り",
"hachure": "斜線", "hachure": "斜線",
"zigzag": "",
"crossHatch": "網掛け", "crossHatch": "網掛け",
"thin": "細", "thin": "細",
"bold": "太字", "bold": "太字",

View file

@ -54,6 +54,7 @@
"veryLarge": "Meqqer aṭas", "veryLarge": "Meqqer aṭas",
"solid": "Aččuran", "solid": "Aččuran",
"hachure": "Azerreg", "hachure": "Azerreg",
"zigzag": "",
"crossHatch": "Azerreg anmidag", "crossHatch": "Azerreg anmidag",
"thin": "Arqaq", "thin": "Arqaq",
"bold": "Azuran", "bold": "Azuran",

View file

@ -54,6 +54,7 @@
"veryLarge": "Өте үлкен", "veryLarge": "Өте үлкен",
"solid": "", "solid": "",
"hachure": "", "hachure": "",
"zigzag": "",
"crossHatch": "", "crossHatch": "",
"thin": "", "thin": "",
"bold": "", "bold": "",

View file

@ -54,6 +54,7 @@
"veryLarge": "매우 크게", "veryLarge": "매우 크게",
"solid": "단색", "solid": "단색",
"hachure": "평행선", "hachure": "평행선",
"zigzag": "지그재그",
"crossHatch": "교차선", "crossHatch": "교차선",
"thin": "얇게", "thin": "얇게",
"bold": "굵게", "bold": "굵게",
@ -256,7 +257,7 @@
"resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.", "resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.",
"resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기", "resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기",
"rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.", "rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.",
"lineEditor_info": "포인트를 편집하려면 Ctrl/Cmd을 누르고 더블 클릭을 하거나 Ctrl/Cmd + Enter를 누르세요", "lineEditor_info": "꼭짓점을 수정하려면 CtrlOrCmd 키를 누르고 더블 클릭을 하거나 CtrlOrCmd + Enter를 누르세요.",
"lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기", "lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기",
"lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기", "lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기",
"placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기", "placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기",
@ -319,8 +320,8 @@
"doubleClick": "더블 클릭", "doubleClick": "더블 클릭",
"drag": "드래그", "drag": "드래그",
"editor": "에디터", "editor": "에디터",
"editLineArrowPoints": "", "editLineArrowPoints": "직선 / 화살표 꼭짓점 수정",
"editText": "", "editText": "텍스트 수정 / 라벨 추가",
"github": "문제 제보하기", "github": "문제 제보하기",
"howto": "가이드 참고하기", "howto": "가이드 참고하기",
"or": "또는", "or": "또는",
@ -382,8 +383,8 @@
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "라이브러리 제출됨", "title": "라이브러리 제출됨",
"content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을 다음의 링크에서 확인할 수 있습니다.", "content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을",
"link": "여기" "link": "여기에서 확인하실 수 있습니다."
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "라이브러리 리셋", "resetLibrary": "라이브러리 리셋",

View file

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "دانانەوە", "paste": "دانانەوە",
"pasteAsPlaintext": "", "pasteAsPlaintext": "دایبنێ وەک دەقی سادە",
"pasteCharts": "دانانەوەی خشتەکان", "pasteCharts": "دانانەوەی خشتەکان",
"selectAll": "دیاریکردنی هەموو", "selectAll": "دیاریکردنی هەموو",
"multiSelect": "زیادکردنی بۆ دیاریکراوەکان", "multiSelect": "زیادکردنی بۆ دیاریکراوەکان",
@ -54,6 +54,7 @@
"veryLarge": "زۆر گه‌وره‌", "veryLarge": "زۆر گه‌وره‌",
"solid": "سادە", "solid": "سادە",
"hachure": "هاچور", "hachure": "هاچور",
"zigzag": "زیگزاگ",
"crossHatch": "کرۆس هاتچ", "crossHatch": "کرۆس هاتچ",
"thin": "تەنک", "thin": "تەنک",
"bold": "تۆخ", "bold": "تۆخ",
@ -110,7 +111,7 @@
"increaseFontSize": "زایدکردنی قەبارەی فۆنت", "increaseFontSize": "زایدکردنی قەبارەی فۆنت",
"unbindText": "دەقەکە جیابکەرەوە", "unbindText": "دەقەکە جیابکەرەوە",
"bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە", "bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە",
"createContainerFromText": "", "createContainerFromText": "دەق لە چوارچێوەیەکدا بپێچە",
"link": { "link": {
"edit": "دەستکاریکردنی بەستەر", "edit": "دەستکاریکردنی بەستەر",
"create": "دروستکردنی بەستەر", "create": "دروستکردنی بەستەر",
@ -194,7 +195,7 @@
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?", "resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟", "removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.", "invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.",
"collabOfflineWarning": "" "collabOfflineWarning": "هێڵی ئینتەرنێت بەردەست نییە.\n گۆڕانکارییەکانت سەیڤ ناکرێن!"
}, },
"errors": { "errors": {
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.", "unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
@ -204,22 +205,22 @@
"invalidSVGString": "ئێس ڤی جی نادروستە.", "invalidSVGString": "ئێس ڤی جی نادروستە.",
"cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.", "cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.",
"importLibraryError": "نەیتوانی کتێبخانە بار بکات", "importLibraryError": "نەیتوانی کتێبخانە بار بکات",
"collabSaveFailed": "", "collabSaveFailed": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت. ئەگەر کێشەکان بەردەوام بوون، پێویستە فایلەکەت لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت، پێدەچێت تابلۆکە زۆر گەورە بێت. پێویستە فایلەکە لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "start": "پێدەچێت وێبگەڕی Brave بەکاربهێنیت لەگەڵ",
"aggressive_block_fingerprint": "", "aggressive_block_fingerprint": "بلۆککردنی Fingerprinting بەشێوەیەکی توندوتیژانە",
"setting_enabled": "", "setting_enabled": "ڕێکخستن چالاک کراوە",
"break": "", "break": "ئەمە دەکرێت ببێتە هۆی تێکدانی",
"text_elements": "", "text_elements": "دانە دەقییەکان",
"in_your_drawings": "", "in_your_drawings": "لە وێنەکێشانەکانتدا",
"strongly_recommend": "", "strongly_recommend": "بە توندی پێشنیار دەکەین ئەم ڕێکخستنە لەکاربخەیت. دەتوانیت بڕۆیت بە دوای",
"steps": "", "steps": "ئەم هەنگاوانەدا",
"how": "", "how": "بۆ ئەوەی ئەنجامی بدەیت",
"disable_setting": "", "disable_setting": " ئەگەر لەکارخستنی ئەم ڕێکخستنە پیشاندانی توخمەکانی دەق چاک نەکاتەوە، تکایە هەڵبستە بە کردنەوەی",
"issue": "", "issue": "کێشەیەک",
"write": "", "write": "لەسەر گیتهەبەکەمان، یان بۆمان بنوسە لە",
"discord": "" "discord": "دیسکۆرد"
} }
}, },
"toolBar": { "toolBar": {
@ -237,7 +238,7 @@
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە", "penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو", "link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
"eraser": "سڕەر", "eraser": "سڕەر",
"hand": "" "hand": "دەست (ئامرازی پانکردن)"
}, },
"headings": { "headings": {
"canvasActions": "کردارەکانی تابلۆ", "canvasActions": "کردارەکانی تابلۆ",
@ -245,7 +246,7 @@
"shapes": "شێوەکان" "shapes": "شێوەکان"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "بۆ جوڵاندنی تابلۆ، ویلی ماوسەکەت یان دوگمەی سپەیس بگرە لەکاتی ڕاکێشاندە، یانیش ئامرازی دەستەکە بەکاربهێنە",
"linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ", "linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ",
"freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە", "freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە",
"text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن", "text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن",
@ -256,7 +257,7 @@
"resize": "دەتوانیت ڕێژەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی گۆڕینی قەبارەدا،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە", "resize": "دەتوانیت ڕێژەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی گۆڕینی قەبارەدا،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە",
"resizeImage": "دەتوانیت بە ئازادی قەبارە بگۆڕیت بە ڕاگرتنی SHIFT،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە", "resizeImage": "دەتوانیت بە ئازادی قەبارە بگۆڕیت بە ڕاگرتنی SHIFT،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە",
"rotate": "دەتوانیت گۆشەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی سوڕانەوەدا", "rotate": "دەتوانیت گۆشەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی سوڕانەوەدا",
"lineEditor_info": "", "lineEditor_info": "یان Ctrl یان Cmd بگرە و دوانە کلیک بکە یانیش پەنجە بنێ بە Ctrl یان Cmd + ئینتەر بۆ دەستکاریکردنی خاڵەکان",
"lineEditor_pointSelected": "بۆ لابردنی خاڵەکان Delete دابگرە، Ctrl Cmd+D بکە بۆ لەبەرگرتنەوە، یان بۆ جووڵە ڕاکێشان بکە", "lineEditor_pointSelected": "بۆ لابردنی خاڵەکان Delete دابگرە، Ctrl Cmd+D بکە بۆ لەبەرگرتنەوە، یان بۆ جووڵە ڕاکێشان بکە",
"lineEditor_nothingSelected": "خاڵێک هەڵبژێرە بۆ دەستکاریکردن (SHIFT ڕابگرە بۆ هەڵبژاردنی چەندین)،\nیان Alt ڕابگرە و کلیک بکە بۆ زیادکردنی خاڵە نوێیەکان", "lineEditor_nothingSelected": "خاڵێک هەڵبژێرە بۆ دەستکاریکردن (SHIFT ڕابگرە بۆ هەڵبژاردنی چەندین)،\nیان Alt ڕابگرە و کلیک بکە بۆ زیادکردنی خاڵە نوێیەکان",
"placeImage": "کلیک بکە بۆ دانانی وێنەکە، یان کلیک بکە و ڕایبکێشە بۆ ئەوەی قەبارەکەی بە دەستی دابنێیت", "placeImage": "کلیک بکە بۆ دانانی وێنەکە، یان کلیک بکە و ڕایبکێشە بۆ ئەوەی قەبارەکەی بە دەستی دابنێیت",
@ -264,7 +265,7 @@
"bindTextToElement": "بۆ زیادکردنی دەق enter بکە", "bindTextToElement": "بۆ زیادکردنی دەق enter بکە",
"deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان", "deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان",
"eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە", "eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە",
"firefox_clipboard_write": "" "firefox_clipboard_write": "ئەم تایبەتمەندییە بە ئەگەرێکی زۆرەوە دەتوانرێت چالاک بکرێت بە ڕێکخستنی ئاڵای \"dom.events.asyncClipboard.clipboardItem\" بۆ \"true\". بۆ گۆڕینی ئاڵاکانی وێبگەڕ لە فایەرفۆکسدا، سەردانی لاپەڕەی \"about:config\" بکە."
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت", "cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت",
@ -319,8 +320,8 @@
"doubleClick": "دوو گرتە", "doubleClick": "دوو گرتە",
"drag": "راکێشان", "drag": "راکێشان",
"editor": "دەستکاریکەر", "editor": "دەستکاریکەر",
"editLineArrowPoints": "", "editLineArrowPoints": "دەستکاری خاڵەکانی هێڵ/تیر بکە",
"editText": "", "editText": "دەستکاری دەق بکە / لەیبڵێک زیاد بکە",
"github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن", "github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن",
"howto": "شوێن ڕینماییەکانمان بکەوە", "howto": "شوێن ڕینماییەکانمان بکەوە",
"or": "یان", "or": "یان",
@ -334,8 +335,8 @@
"zoomToFit": "زووم بکە بۆ ئەوەی لەگەڵ هەموو توخمەکاندا بگونجێت", "zoomToFit": "زووم بکە بۆ ئەوەی لەگەڵ هەموو توخمەکاندا بگونجێت",
"zoomToSelection": "زووم بکە بۆ دەستنیشانکراوەکان", "zoomToSelection": "زووم بکە بۆ دەستنیشانکراوەکان",
"toggleElementLock": "قفڵ/کردنەوەی دەستنیشانکراوەکان", "toggleElementLock": "قفڵ/کردنەوەی دەستنیشانکراوەکان",
"movePageUpDown": "", "movePageUpDown": "لاپەڕەکە بجوڵێنە بۆ سەرەوە/خوارەوە",
"movePageLeftRight": "" "movePageLeftRight": "لاپەڕەکە بجوڵێنە بۆ چەپ/ڕاست"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "تابلۆکە خاوێن بکەرەوە" "title": "تابلۆکە خاوێن بکەرەوە"
@ -417,7 +418,7 @@
"fileSavedToFilename": "هەڵگیراوە بۆ {filename}", "fileSavedToFilename": "هەڵگیراوە بۆ {filename}",
"canvas": "تابلۆ", "canvas": "تابلۆ",
"selection": "دەستنیشانکراوەکان", "selection": "دەستنیشانکراوەکان",
"pasteAsSingleElement": "" "pasteAsSingleElement": "بۆ دانانەوە وەکو یەک توخم یان دانانەوە بۆ نێو دەسکاریکەرێکی دەق کە بوونی هەیە {{shortcut}} بەکاربهێنە"
}, },
"colors": { "colors": {
"ffffff": "سپی", "ffffff": "سپی",
@ -468,15 +469,15 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "", "center_heading": "هەموو داتاکانت لە ناوخۆی وێنگەڕەکەتدا پاشەکەوت کراوە.",
"center_heading_plus": "", "center_heading_plus": "ویستت بڕۆیت بۆ Excalidraw+?",
"menuHint": "" "menuHint": "هەناردەکردن، ڕێکخستنەکان، زمانەکان، ..."
}, },
"defaults": { "defaults": {
"menuHint": "", "menuHint": "هەناردەکردن، ڕێکخستنەکان، و زیاتر...",
"center_heading": "", "center_heading": "دایاگرامەکان. ئاسان. کراون.",
"toolbarHint": "", "toolbarHint": "ئامرازێک هەڵبگرە و دەستبکە بە کێشان!",
"helpHint": "" "helpHint": "قەدبڕەکان و یارمەتی"
} }
} }
} }

View file

@ -54,6 +54,7 @@
"veryLarge": "Labai didelis", "veryLarge": "Labai didelis",
"solid": "", "solid": "",
"hachure": "", "hachure": "",
"zigzag": "",
"crossHatch": "", "crossHatch": "",
"thin": "Plonas", "thin": "Plonas",
"bold": "Pastorintas", "bold": "Pastorintas",

View file

@ -54,6 +54,7 @@
"veryLarge": "Ļoti liels", "veryLarge": "Ļoti liels",
"solid": "Pilns", "solid": "Pilns",
"hachure": "Svītrots", "hachure": "Svītrots",
"zigzag": "Zigzaglīnija",
"crossHatch": "Šķērssvītrots", "crossHatch": "Šķērssvītrots",
"thin": "Šaurs", "thin": "Šaurs",
"bold": "Trekns", "bold": "Trekns",
@ -110,7 +111,7 @@
"increaseFontSize": "Palielināt fonta izmēru", "increaseFontSize": "Palielināt fonta izmēru",
"unbindText": "Atdalīt tekstu", "unbindText": "Atdalīt tekstu",
"bindText": "Piesaistīt tekstu figūrai", "bindText": "Piesaistīt tekstu figūrai",
"createContainerFromText": "", "createContainerFromText": "Ietilpināt tekstu figurā",
"link": { "link": {
"edit": "Rediģēt saiti", "edit": "Rediģēt saiti",
"create": "Izveidot saiti", "create": "Izveidot saiti",
@ -194,7 +195,7 @@
"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": "" "collabOfflineWarning": "Nav pieejams interneta pieslēgums.\nJūsu izmaiņas netiks saglabātas!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Neatbalstīts datnes veids.", "unsupportedFileType": "Neatbalstīts datnes veids.",
@ -207,19 +208,19 @@
"collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "start": "Izskatās, ka izmanto Brave interneta plārlūku ar ieslēgtu",
"aggressive_block_fingerprint": "", "aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
"setting_enabled": "", "setting_enabled": "ieslēgtu iestatījumu",
"break": "", "break": "Tas var salauzt",
"text_elements": "", "text_elements": "Teksta elementus",
"in_your_drawings": "", "in_your_drawings": "tavos zīmējumos",
"strongly_recommend": "", "strongly_recommend": "Mēs iesakām izslēgt šo iestatījumu. Tu vari sekot",
"steps": "", "steps": "šiem soļiem",
"how": "", "how": "kā to izdarīt",
"disable_setting": "", "disable_setting": " Ja šī iestatījuma izslēgšana neatrisina teksta elementu attēlošanu, tad, lūdzu, atver",
"issue": "", "issue": "problēmu",
"write": "", "write": "mūsu GitHub vai raksti mums",
"discord": "" "discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -237,7 +238,7 @@
"penMode": "Pildspalvas režīms novērst pieskaršanos", "penMode": "Pildspalvas režīms novērst pieskaršanos",
"link": "Pievienot/rediģēt atlasītās figūras saiti", "link": "Pievienot/rediģēt atlasītās figūras saiti",
"eraser": "Dzēšgumija", "eraser": "Dzēšgumija",
"hand": "" "hand": "Roka (panoramēšanas rīks)"
}, },
"headings": { "headings": {
"canvasActions": "Tāfeles darbības", "canvasActions": "Tāfeles darbības",
@ -245,7 +246,7 @@
"shapes": "Formas" "shapes": "Formas"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "Lai bīdītu tāfeli, turiet nospiestu ritināšanas vai atstarpes taustiņu, vai izmanto rokas rīku",
"linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju", "linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju",
"freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat", "freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat",
"text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku", "text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku",
@ -264,7 +265,7 @@
"bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu", "bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu",
"deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu", "deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu",
"eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases", "eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases",
"firefox_clipboard_write": "" "firefox_clipboard_write": "Šis iestatījums var tikt ieslēgts ar \"dom.events.asyncClipboard.clipboardItem\" marķieri pārslēgtu uz \"true\". Lai mainītu pārlūka marķierus Firefox, apmeklē \"about:config\" lapu."
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Nevar rādīt priekšskatījumu", "cannotShowPreview": "Nevar rādīt priekšskatījumu",
@ -319,8 +320,8 @@
"doubleClick": "dubultklikšķis", "doubleClick": "dubultklikšķis",
"drag": "vilkt", "drag": "vilkt",
"editor": "Redaktors", "editor": "Redaktors",
"editLineArrowPoints": "", "editLineArrowPoints": "Rediģēt līniju/bultu punktus",
"editText": "", "editText": "Rediģēt tekstu/pievienot birku",
"github": "Sastapāt kļūdu? Ziņot", "github": "Sastapāt kļūdu? Ziņot",
"howto": "Sekojiet mūsu instrukcijām", "howto": "Sekojiet mūsu instrukcijām",
"or": "vai", "or": "vai",
@ -468,15 +469,15 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "", "center_heading": "Visi jūsu dati tiek glabāti uz vietas jūsu pārlūkā.",
"center_heading_plus": "", "center_heading_plus": "Vai tā vietā vēlies doties uz Excalidraw+?",
"menuHint": "" "menuHint": "Eksportēšana, iestatījumi, valodas..."
}, },
"defaults": { "defaults": {
"menuHint": "", "menuHint": "Eksportēšana, iestatījumi un vēl...",
"center_heading": "", "center_heading": "Diagrammas. Izveidotas. Vienkārši.",
"toolbarHint": "", "toolbarHint": "Izvēlies rīku un sāc zīmēt!",
"helpHint": "" "helpHint": "Īsceļi un palīdzība"
} }
} }
} }

View file

@ -54,6 +54,7 @@
"veryLarge": "फार मोठं", "veryLarge": "फार मोठं",
"solid": "भरीव", "solid": "भरीव",
"hachure": "हैशूर रेखांकन", "hachure": "हैशूर रेखांकन",
"zigzag": "वाकडी तिकड़ी",
"crossHatch": "आडव्या रेघा", "crossHatch": "आडव्या रेघा",
"thin": "पातळ", "thin": "पातळ",
"bold": "जाड", "bold": "जाड",

View file

@ -54,6 +54,7 @@
"veryLarge": "ပိုကြီး", "veryLarge": "ပိုကြီး",
"solid": "အပြည့်", "solid": "အပြည့်",
"hachure": "မျဉ်းစောင်း", "hachure": "မျဉ်းစောင်း",
"zigzag": "",
"crossHatch": "ဇကာကွက်", "crossHatch": "ဇကာကွက်",
"thin": "ပါး", "thin": "ပါး",
"bold": "ထူ", "bold": "ထူ",

View file

@ -54,6 +54,7 @@
"veryLarge": "Svært stor", "veryLarge": "Svært stor",
"solid": "Helfarge", "solid": "Helfarge",
"hachure": "Skravert", "hachure": "Skravert",
"zigzag": "Sikk-sakk",
"crossHatch": "Krysskravert", "crossHatch": "Krysskravert",
"thin": "Tynn", "thin": "Tynn",
"bold": "Tykk", "bold": "Tykk",

View file

@ -54,6 +54,7 @@
"veryLarge": "Zeer groot", "veryLarge": "Zeer groot",
"solid": "Ingekleurd", "solid": "Ingekleurd",
"hachure": "Arcering", "hachure": "Arcering",
"zigzag": "",
"crossHatch": "Tweemaal gearceerd", "crossHatch": "Tweemaal gearceerd",
"thin": "Dun", "thin": "Dun",
"bold": "Vet", "bold": "Vet",

View file

@ -54,6 +54,7 @@
"veryLarge": "Svært stor", "veryLarge": "Svært stor",
"solid": "Solid", "solid": "Solid",
"hachure": "Skravert", "hachure": "Skravert",
"zigzag": "",
"crossHatch": "Krysskravert", "crossHatch": "Krysskravert",
"thin": "Tynn", "thin": "Tynn",
"bold": "Tjukk", "bold": "Tjukk",

View file

@ -54,6 +54,7 @@
"veryLarge": "Gradassa", "veryLarge": "Gradassa",
"solid": "Solide", "solid": "Solide",
"hachure": "Raia", "hachure": "Raia",
"zigzag": "",
"crossHatch": "Raia crosada", "crossHatch": "Raia crosada",
"thin": "Fin", "thin": "Fin",
"bold": "Espés", "bold": "Espés",

View file

@ -54,6 +54,7 @@
"veryLarge": "ਬਹੁਤ ਵੱਡਾ", "veryLarge": "ਬਹੁਤ ਵੱਡਾ",
"solid": "ਠੋਸ", "solid": "ਠੋਸ",
"hachure": "ਤਿਰਛੀਆਂ ਗਰਿੱਲਾਂ", "hachure": "ਤਿਰਛੀਆਂ ਗਰਿੱਲਾਂ",
"zigzag": "",
"crossHatch": "ਜਾਲੀ", "crossHatch": "ਜਾਲੀ",
"thin": "ਪਤਲੀ", "thin": "ਪਤਲੀ",
"bold": "ਮੋਟੀ", "bold": "ਮੋਟੀ",

View file

@ -1,17 +1,17 @@
{ {
"ar-SA": 89, "ar-SA": 88,
"bg-BG": 52, "bg-BG": 52,
"bn-BD": 57, "bn-BD": 57,
"ca-ES": 96, "ca-ES": 95,
"cs-CZ": 72, "cs-CZ": 71,
"da-DK": 31, "da-DK": 31,
"de-DE": 100, "de-DE": 100,
"el-GR": 98, "el-GR": 98,
"en": 100, "en": 100,
"es-ES": 99, "es-ES": 100,
"eu-ES": 99, "eu-ES": 99,
"fa-IR": 91, "fa-IR": 91,
"fi-FI": 96, "fi-FI": 95,
"fr-FR": 99, "fr-FR": 99,
"gl-ES": 99, "gl-ES": 99,
"he-IL": 99, "he-IL": 99,
@ -19,35 +19,35 @@
"hu-HU": 85, "hu-HU": 85,
"id-ID": 98, "id-ID": 98,
"it-IT": 99, "it-IT": 99,
"ja-JP": 97, "ja-JP": 96,
"kab-KAB": 93, "kab-KAB": 93,
"kk-KZ": 19, "kk-KZ": 19,
"ko-KR": 99, "ko-KR": 100,
"ku-TR": 91, "ku-TR": 100,
"lt-LT": 61, "lt-LT": 61,
"lv-LV": 93, "lv-LV": 100,
"mr-IN": 100, "mr-IN": 100,
"my-MM": 40, "my-MM": 39,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 92, "nl-NL": 92,
"nn-NO": 86, "nn-NO": 85,
"oc-FR": 94, "oc-FR": 94,
"pa-IN": 79, "pa-IN": 79,
"pl-PL": 87, "pl-PL": 87,
"pt-BR": 96, "pt-BR": 95,
"pt-PT": 99, "pt-PT": 100,
"ro-RO": 100, "ro-RO": 100,
"ru-RU": 96, "ru-RU": 100,
"si-LK": 8, "si-LK": 8,
"sk-SK": 99, "sk-SK": 99,
"sl-SI": 100, "sl-SI": 100,
"sv-SE": 99, "sv-SE": 100,
"ta-IN": 90, "ta-IN": 90,
"th-TH": 39, "th-TH": 39,
"tr-TR": 98, "tr-TR": 97,
"uk-UA": 93, "uk-UA": 92,
"vi-VN": 52, "vi-VN": 52,
"zh-CN": 99, "zh-CN": 99,
"zh-HK": 25, "zh-HK": 24,
"zh-TW": 100 "zh-TW": 100
} }

View file

@ -54,6 +54,7 @@
"veryLarge": "Bardzo duży", "veryLarge": "Bardzo duży",
"solid": "Pełne", "solid": "Pełne",
"hachure": "Linie", "hachure": "Linie",
"zigzag": "",
"crossHatch": "Zakreślone", "crossHatch": "Zakreślone",
"thin": "Cienkie", "thin": "Cienkie",
"bold": "Pogrubione", "bold": "Pogrubione",

View file

@ -54,6 +54,7 @@
"veryLarge": "Muito grande", "veryLarge": "Muito grande",
"solid": "Sólido", "solid": "Sólido",
"hachure": "Hachura", "hachure": "Hachura",
"zigzag": "",
"crossHatch": "Hachura cruzada", "crossHatch": "Hachura cruzada",
"thin": "Fino", "thin": "Fino",
"bold": "Espesso", "bold": "Espesso",

View file

@ -54,6 +54,7 @@
"veryLarge": "Muito grande", "veryLarge": "Muito grande",
"solid": "Sólido", "solid": "Sólido",
"hachure": "Eclosão", "hachure": "Eclosão",
"zigzag": "ziguezague",
"crossHatch": "Sombreado", "crossHatch": "Sombreado",
"thin": "Fino", "thin": "Fino",
"bold": "Espesso", "bold": "Espesso",
@ -319,8 +320,8 @@
"doubleClick": "clique duplo", "doubleClick": "clique duplo",
"drag": "arrastar", "drag": "arrastar",
"editor": "Editor", "editor": "Editor",
"editLineArrowPoints": "", "editLineArrowPoints": "Editar pontos de linha/seta",
"editText": "", "editText": "Editar texto / adicionar etiqueta",
"github": "Encontrou algum problema? Informe-nos", "github": "Encontrou algum problema? Informe-nos",
"howto": "Siga os nossos guias", "howto": "Siga os nossos guias",
"or": "ou", "or": "ou",

View file

@ -54,6 +54,7 @@
"veryLarge": "Foarte mare", "veryLarge": "Foarte mare",
"solid": "Plină", "solid": "Plină",
"hachure": "Hașură", "hachure": "Hașură",
"zigzag": "Zigzag",
"crossHatch": "Hașură transversală", "crossHatch": "Hașură transversală",
"thin": "Subțire", "thin": "Subțire",
"bold": "Îngroșată", "bold": "Îngroșată",

View file

@ -54,6 +54,7 @@
"veryLarge": "Очень большой", "veryLarge": "Очень большой",
"solid": "Однотонная", "solid": "Однотонная",
"hachure": "Штрихованная", "hachure": "Штрихованная",
"zigzag": "Зигзаг",
"crossHatch": "Перекрестная", "crossHatch": "Перекрестная",
"thin": "Тонкая", "thin": "Тонкая",
"bold": "Жирная", "bold": "Жирная",
@ -110,7 +111,7 @@
"increaseFontSize": "Увеличить шрифт", "increaseFontSize": "Увеличить шрифт",
"unbindText": "Отвязать текст", "unbindText": "Отвязать текст",
"bindText": "Привязать текст к контейнеру", "bindText": "Привязать текст к контейнеру",
"createContainerFromText": "", "createContainerFromText": "Поместить текст в контейнер",
"link": { "link": {
"edit": "Редактировать ссылку", "edit": "Редактировать ссылку",
"create": "Создать ссылку", "create": "Создать ссылку",
@ -207,19 +208,19 @@
"collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", "collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.",
"collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", "collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "start": "Похоже, вы используете браузер Brave с",
"aggressive_block_fingerprint": "", "aggressive_block_fingerprint": "Агрессивно блокировать фингерпринтинг",
"setting_enabled": "", "setting_enabled": "параметр включен",
"break": "", "break": "Это может привести к поломке",
"text_elements": "", "text_elements": "Текстовых элементов",
"in_your_drawings": "", "in_your_drawings": "в ваших рисунках",
"strongly_recommend": "", "strongly_recommend": "Мы настоятельно рекомендуем отключить эту настройку. Вы можете выполнить",
"steps": "", "steps": "эти действия",
"how": "", "how": "для отключения",
"disable_setting": "", "disable_setting": " Если отключение этого параметра не исправит отображение текстовых элементов, пожалуйста, откройте",
"issue": "", "issue": "issue",
"write": "", "write": "на нашем GitHub, или напишите нам в",
"discord": "" "discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -319,8 +320,8 @@
"doubleClick": "двойной клик", "doubleClick": "двойной клик",
"drag": "перетащить", "drag": "перетащить",
"editor": "Редактор", "editor": "Редактор",
"editLineArrowPoints": "", "editLineArrowPoints": "Редактировать концы линий/стрелок",
"editText": "", "editText": "Редактировать текст / добавить метку",
"github": "Нашли проблему? Отправьте", "github": "Нашли проблему? Отправьте",
"howto": "Следуйте нашим инструкциям", "howto": "Следуйте нашим инструкциям",
"or": "или", "or": "или",

View file

@ -54,6 +54,7 @@
"veryLarge": "ඉතා විශාල", "veryLarge": "ඉතා විශාල",
"solid": "විශාල", "solid": "විශාල",
"hachure": "මධ්‍යම", "hachure": "මධ්‍යම",
"zigzag": "",
"crossHatch": "", "crossHatch": "",
"thin": "කෙට්ටු", "thin": "කෙට්ටු",
"bold": "තද", "bold": "තද",

View file

@ -54,6 +54,7 @@
"veryLarge": "Veľmi veľké", "veryLarge": "Veľmi veľké",
"solid": "Plná", "solid": "Plná",
"hachure": "Šrafovaná", "hachure": "Šrafovaná",
"zigzag": "",
"crossHatch": "Mriežkovaná", "crossHatch": "Mriežkovaná",
"thin": "Tenká", "thin": "Tenká",
"bold": "Hrubá", "bold": "Hrubá",
@ -319,8 +320,8 @@
"doubleClick": "dvojklik", "doubleClick": "dvojklik",
"drag": "potiahnutie", "drag": "potiahnutie",
"editor": "Editovanie", "editor": "Editovanie",
"editLineArrowPoints": "", "editLineArrowPoints": "Editácia bodov čiary/šípky",
"editText": "", "editText": "Editácia textu / pridanie štítku",
"github": "Objavili ste problém? Nahláste ho", "github": "Objavili ste problém? Nahláste ho",
"howto": "Postupujte podľa naších návodov", "howto": "Postupujte podľa naších návodov",
"or": "alebo", "or": "alebo",

View file

@ -54,6 +54,7 @@
"veryLarge": "Zelo velika", "veryLarge": "Zelo velika",
"solid": "Polno", "solid": "Polno",
"hachure": "Šrafura", "hachure": "Šrafura",
"zigzag": "Cikcak",
"crossHatch": "Križno", "crossHatch": "Križno",
"thin": "Tanko", "thin": "Tanko",
"bold": "Krepko", "bold": "Krepko",

View file

@ -54,6 +54,7 @@
"veryLarge": "Mycket stor", "veryLarge": "Mycket stor",
"solid": "Solid", "solid": "Solid",
"hachure": "Skraffering", "hachure": "Skraffering",
"zigzag": "Sicksack",
"crossHatch": "Skraffera med kors", "crossHatch": "Skraffera med kors",
"thin": "Tunn", "thin": "Tunn",
"bold": "Fet", "bold": "Fet",
@ -319,8 +320,8 @@
"doubleClick": "dubbelklicka", "doubleClick": "dubbelklicka",
"drag": "dra", "drag": "dra",
"editor": "Redigerare", "editor": "Redigerare",
"editLineArrowPoints": "", "editLineArrowPoints": "Redigera linje-/pilpunkter",
"editText": "", "editText": "Redigera text / lägg till etikett",
"github": "Hittat ett problem? Rapportera", "github": "Hittat ett problem? Rapportera",
"howto": "Följ våra guider", "howto": "Följ våra guider",
"or": "eller", "or": "eller",

View file

@ -54,6 +54,7 @@
"veryLarge": "மிகப் பெரிய", "veryLarge": "மிகப் பெரிய",
"solid": "திடமான", "solid": "திடமான",
"hachure": "மலைக்குறிக்கோடு", "hachure": "மலைக்குறிக்கோடு",
"zigzag": "",
"crossHatch": "குறுக்குகதவு", "crossHatch": "குறுக்குகதவு",
"thin": "மெல்லிய", "thin": "மெல்லிய",
"bold": "பட்டை", "bold": "பட்டை",

View file

@ -54,6 +54,7 @@
"veryLarge": "ใหญ่มาก", "veryLarge": "ใหญ่มาก",
"solid": "", "solid": "",
"hachure": "", "hachure": "",
"zigzag": "",
"crossHatch": "", "crossHatch": "",
"thin": "บาง", "thin": "บาง",
"bold": "หนา", "bold": "หนา",

View file

@ -54,6 +54,7 @@
"veryLarge": "Çok geniş", "veryLarge": "Çok geniş",
"solid": "Dolu", "solid": "Dolu",
"hachure": "Taralı", "hachure": "Taralı",
"zigzag": "",
"crossHatch": "Çapraz-taralı", "crossHatch": "Çapraz-taralı",
"thin": "İnce", "thin": "İnce",
"bold": "Kalın", "bold": "Kalın",

View file

@ -54,6 +54,7 @@
"veryLarge": "Дуже великий", "veryLarge": "Дуже великий",
"solid": "Суцільна", "solid": "Суцільна",
"hachure": "Штриховка", "hachure": "Штриховка",
"zigzag": "",
"crossHatch": "Перехресна штриховка", "crossHatch": "Перехресна штриховка",
"thin": "Тонкий", "thin": "Тонкий",
"bold": "Жирний", "bold": "Жирний",

View file

@ -54,6 +54,7 @@
"veryLarge": "Rất lớn", "veryLarge": "Rất lớn",
"solid": "Đặc", "solid": "Đặc",
"hachure": "Nét gạch gạch", "hachure": "Nét gạch gạch",
"zigzag": "",
"crossHatch": "Nét gạch chéo", "crossHatch": "Nét gạch chéo",
"thin": "Mỏng", "thin": "Mỏng",
"bold": "In đậm", "bold": "In đậm",

View file

@ -54,6 +54,7 @@
"veryLarge": "加大", "veryLarge": "加大",
"solid": "实心", "solid": "实心",
"hachure": "线条", "hachure": "线条",
"zigzag": "",
"crossHatch": "交叉线条", "crossHatch": "交叉线条",
"thin": "细", "thin": "细",
"bold": "粗", "bold": "粗",
@ -319,8 +320,8 @@
"doubleClick": "双击", "doubleClick": "双击",
"drag": "拖动", "drag": "拖动",
"editor": "编辑器", "editor": "编辑器",
"editLineArrowPoints": "", "editLineArrowPoints": "编辑线条或箭头的点",
"editText": "", "editText": "添加或编辑文本",
"github": "提交问题", "github": "提交问题",
"howto": "帮助文档", "howto": "帮助文档",
"or": "或", "or": "或",

View file

@ -54,6 +54,7 @@
"veryLarge": "勁大", "veryLarge": "勁大",
"solid": "實心", "solid": "實心",
"hachure": "斜線", "hachure": "斜線",
"zigzag": "",
"crossHatch": "交叉格仔", "crossHatch": "交叉格仔",
"thin": "幼", "thin": "幼",
"bold": "粗", "bold": "粗",

View file

@ -54,6 +54,7 @@
"veryLarge": "特大", "veryLarge": "特大",
"solid": "實心", "solid": "實心",
"hachure": "斜線筆觸", "hachure": "斜線筆觸",
"zigzag": "Z字形",
"crossHatch": "交叉筆觸", "crossHatch": "交叉筆觸",
"thin": "細", "thin": "細",
"bold": "粗", "bold": "粗",

View file

@ -11,7 +11,39 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## Unreleased ## 0.15.2 (2023-04-20)
### Docs
- Fix docs link in readme [#6486](https://github.com/excalidraw/excalidraw/pull/6486)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Fixes
- Rotate the text element when binding to a rotated container [#6477](https://github.com/excalidraw/excalidraw/pull/6477)
- Support breaking words containing hyphen - [#6014](https://github.com/excalidraw/excalidraw/pull/6014)
- Incorrect background fill button active state [#6491](https://github.com/excalidraw/excalidraw/pull/6491)
---
## 0.15.1 (2023-04-18)
### Docs
- Add the readme back to the package which was mistakenly removed [#6484](https://github.com/excalidraw/excalidraw/pull/6484)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
---
## 0.15.0 (2023-04-18)
### Features ### Features
@ -37,6 +69,154 @@ For more details refer to the [docs](https://docs.excalidraw.com)
- Exporting labelled arrows via export utils [#6443](https://github.com/excalidraw/excalidraw/pull/6443) - Exporting labelled arrows via export utils [#6443](https://github.com/excalidraw/excalidraw/pull/6443)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Features
- Constrain export dialog preview size [#6475](https://github.com/excalidraw/excalidraw/pull/6475)
- Zigzag fill easter egg [#6439](https://github.com/excalidraw/excalidraw/pull/6439)
- Add container to multiple text elements [#6428](https://github.com/excalidraw/excalidraw/pull/6428)
- Starting migration from GA to Matomo for better privacy [#6398](https://github.com/excalidraw/excalidraw/pull/6398)
- Add line height attribute to text element [#6360](https://github.com/excalidraw/excalidraw/pull/6360)
- Add thai lang support [#6314](https://github.com/excalidraw/excalidraw/pull/6314)
- Create bound container from text [#6301](https://github.com/excalidraw/excalidraw/pull/6301)
- Improve text measurements in bound containers [#6187](https://github.com/excalidraw/excalidraw/pull/6187)
- Bind text to container if double clicked on filled shape or stroke [#6250](https://github.com/excalidraw/excalidraw/pull/6250)
- Make repair and refreshDimensions configurable in restoreElements [#6238](https://github.com/excalidraw/excalidraw/pull/6238)
- Show error message when not connected to internet while collabo… [#6165](https://github.com/excalidraw/excalidraw/pull/6165)
- Shortcut for clearCanvas confirmDialog [#6114](https://github.com/excalidraw/excalidraw/pull/6114)
- Disable canvas smoothing (antialiasing) for right-angled elements [#6186](https://github.com/excalidraw/excalidraw/pull/6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
### Fixes
- Center align text when wrapped in container via context menu [#6480](https://github.com/excalidraw/excalidraw/pull/6480)
- Restore original container height when unbinding text which was binded via context menu [#6444](https://github.com/excalidraw/excalidraw/pull/6444)
- Mark more props as optional for element [#6448](https://github.com/excalidraw/excalidraw/pull/6448)
- Improperly cache-busting on canvas scale instead of zoom [#6473](https://github.com/excalidraw/excalidraw/pull/6473)
- Incorrectly duplicating items on paste/library insert [#6467](https://github.com/excalidraw/excalidraw/pull/6467)
- Library ids cross-contamination on multiple insert [#6466](https://github.com/excalidraw/excalidraw/pull/6466)
- Color picker keyboard handling not working [#6464](https://github.com/excalidraw/excalidraw/pull/6464)
- Abort freedraw line if second touch is detected [#6440](https://github.com/excalidraw/excalidraw/pull/6440)
- Utils leaking Scene state [#6461](https://github.com/excalidraw/excalidraw/pull/6461)
- Split "Edit selected shape" shortcut [#6457](https://github.com/excalidraw/excalidraw/pull/6457)
- Center align text when bind to container via context menu [#6451](https://github.com/excalidraw/excalidraw/pull/6451)
- Update coords when text unbinded from its container [#6445](https://github.com/excalidraw/excalidraw/pull/6445)
- Autoredirect to plus in prod only [#6446](https://github.com/excalidraw/excalidraw/pull/6446)
- Fixing popover overflow on small screen [#6433](https://github.com/excalidraw/excalidraw/pull/6433)
- Introduce baseline to fix the layout shift when switching to text editor [#6397](https://github.com/excalidraw/excalidraw/pull/6397)
- Don't refresh dimensions for deleted text elements [#6438](https://github.com/excalidraw/excalidraw/pull/6438)
- Element vanishes when zoomed in [#6417](https://github.com/excalidraw/excalidraw/pull/6417)
- Don't jump text to end when out of viewport in safari [#6416](https://github.com/excalidraw/excalidraw/pull/6416)
- GetDefaultLineHeight should return default font family line height for unknown font [#6399](https://github.com/excalidraw/excalidraw/pull/6399)
- Revert use `ideographic` textBaseline to improve layout shift when editing text" [#6400](https://github.com/excalidraw/excalidraw/pull/6400)
- Call stack size exceeded when paste large text [#6373](https://github.com/excalidraw/excalidraw/pull/6373) (#6396)
- Use `ideographic` textBaseline to improve layout shift when editing text [#6384](https://github.com/excalidraw/excalidraw/pull/6384)
- Chrome crashing when embedding scene on chrome arm [#6383](https://github.com/excalidraw/excalidraw/pull/6383)
- Division by zero in findFocusPointForEllipse leads to infinite loop in wrapText freezing Excalidraw [#6377](https://github.com/excalidraw/excalidraw/pull/6377)
- Containerizing text incorrectly updates arrow bindings [#6369](https://github.com/excalidraw/excalidraw/pull/6369)
- Ensure export preview is centered [#6337](https://github.com/excalidraw/excalidraw/pull/6337)
- Hide text align for labelled arrows [#6339](https://github.com/excalidraw/excalidraw/pull/6339)
- Refresh dimensions when elements loaded from shareable link and blob [#6333](https://github.com/excalidraw/excalidraw/pull/6333)
- Show error message when measureText API breaks in brave [#6336](https://github.com/excalidraw/excalidraw/pull/6336)
- Add an offset of 0.5px for text editor in containers [#6328](https://github.com/excalidraw/excalidraw/pull/6328)
- Move utility types out of `.d.ts` file to fix exported declaration files [#6315](https://github.com/excalidraw/excalidraw/pull/6315)
- More jotai scopes missing [#6313](https://github.com/excalidraw/excalidraw/pull/6313)
- Provide HelpButton title prop [#6209](https://github.com/excalidraw/excalidraw/pull/6209)
- Respect text align when wrapping in a container [#6310](https://github.com/excalidraw/excalidraw/pull/6310)
- Compute bounding box correctly for text element when multiple element resizing [#6307](https://github.com/excalidraw/excalidraw/pull/6307)
- Use jotai scope for editor-specific atoms [#6308](https://github.com/excalidraw/excalidraw/pull/6308)
- Consider arrow for bound text element [#6297](https://github.com/excalidraw/excalidraw/pull/6297)
- Text never goes beyond max width for unbound text elements [#6288](https://github.com/excalidraw/excalidraw/pull/6288)
- Svg text baseline [#6285](https://github.com/excalidraw/excalidraw/pull/6273)
- Compute container height from bound text correctly [#6273](https://github.com/excalidraw/excalidraw/pull/6273)
- Fit mobile toolbar and make scrollable [#6270](https://github.com/excalidraw/excalidraw/pull/6270)
- Indenting via `tab` clashing with IME compositor [#6258](https://github.com/excalidraw/excalidraw/pull/6258)
- Improve text wrapping inside rhombus and more fixes [#6265](https://github.com/excalidraw/excalidraw/pull/6265)
- Improve text wrapping in ellipse and alignment [#6172](https://github.com/excalidraw/excalidraw/pull/6172)
- Don't allow blank space in collab name [#6211](https://github.com/excalidraw/excalidraw/pull/6211)
- Docker build architecture:linux/amd64 error occur on linux/arm64 instance [#6197](https://github.com/excalidraw/excalidraw/pull/6197)
- Sort bound text elements to fix text duplication z-index error [#5130](https://github.com/excalidraw/excalidraw/pull/5130)
- Hide welcome screen on mobile once user interacts [#6185](https://github.com/excalidraw/excalidraw/pull/6185)
- Edit link in docs [#6182](https://github.com/excalidraw/excalidraw/pull/6182)
### Refactor
- Inline `SingleLibraryItem` into `PublishLibrary` [#6462](https://github.com/excalidraw/excalidraw/pull/6462)
- Make the example React app reusable without duplication [#6188](https://github.com/excalidraw/excalidraw/pull/6188)
### Performance
- Break early if the line width <= max width of the container [#6347](https://github.com/excalidraw/excalidraw/pull/6347)
### Build
- Move TS and types to devDependencies [#6346](https://github.com/excalidraw/excalidraw/pull/6346)
---
## 0.14.2 (2023-02-01) ## 0.14.2 (2023-02-01)
### Features ### Features

View file

@ -38,8 +38,8 @@ Excalidraw takes _100%_ of `width` and `height` of the containing block so make
## Integration ## Integration
Head over to the [docs](https://docs.excalidraw.com/docs/package/integration) Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration)
## API ## API
Head over to the [docs](https://docs.excalidraw.com/docs/package/api) Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api)

View file

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.14.2", "version": "0.15.2",
"main": "main.js", "main": "main.js",
"types": "types/packages/excalidraw/index.d.ts", "types": "types/packages/excalidraw/index.d.ts",
"files": [ "files": [

View file

@ -79,7 +79,11 @@ export const exportToCanvas = ({
const max = Math.max(width, height); const max = Math.max(width, height);
const scale = maxWidthOrHeight / max; // if content is less then maxWidthOrHeight, fallback on supplied scale
const scale =
maxWidthOrHeight < max
? maxWidthOrHeight / max
: appState?.exportScale ?? 1;
canvas.width = width * scale; canvas.width = width * scale;
canvas.height = height * scale; canvas.height = height * scale;
@ -216,15 +220,7 @@ export const exportToClipboard = async (
} else if (opts.type === "png") { } else if (opts.type === "png") {
await copyBlobToClipboardAsPng(exportToBlob(opts)); await copyBlobToClipboardAsPng(exportToBlob(opts));
} else if (opts.type === "json") { } else if (opts.type === "json") {
const appState = { await copyToClipboard(opts.elements, opts.files);
offsetTop: 0,
offsetLeft: 0,
width: 0,
height: 0,
...getDefaultAppState(),
...opts.appState,
};
await copyToClipboard(opts.elements, appState, opts.files);
} else { } else {
throw new Error("Invalid export type"); throw new Error("Invalid export type");
} }

View file

@ -44,8 +44,8 @@ import {
getContainerCoords, getContainerCoords,
getContainerElement, getContainerElement,
getLineHeightInPx, getLineHeightInPx,
getMaxContainerHeight, getBoundTextMaxHeight,
getMaxContainerWidth, getBoundTextMaxWidth,
} from "../element/textElement"; } from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@ -864,17 +864,21 @@ const drawElementFromCanvas = (
); );
if ( if (
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX && process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
"true" &&
hasBoundTextElement(element) hasBoundTextElement(element)
) { ) {
const textElement = getBoundTextElement(
element,
) as ExcalidrawTextElementWithContainer;
const coords = getContainerCoords(element); const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a"; context.strokeStyle = "#c92a2a";
context.lineWidth = 3; context.lineWidth = 3;
context.strokeRect( context.strokeRect(
(coords.x + renderConfig.scrollX) * window.devicePixelRatio, (coords.x + renderConfig.scrollX) * window.devicePixelRatio,
(coords.y + renderConfig.scrollY) * window.devicePixelRatio, (coords.y + renderConfig.scrollY) * window.devicePixelRatio,
getMaxContainerWidth(element) * window.devicePixelRatio, getBoundTextMaxWidth(element) * window.devicePixelRatio,
getMaxContainerHeight(element) * window.devicePixelRatio, getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
); );
} }
} }

View file

@ -1,5 +1,6 @@
import { isTextElement, refreshTextDimensions } from "../element"; import { isTextElement, refreshTextDimensions } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { invalidateShapeForElement } from "../renderer/renderElement"; import { invalidateShapeForElement } from "../renderer/renderElement";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
@ -52,7 +53,7 @@ export class Fonts {
let didUpdate = false; let didUpdate = false;
this.scene.mapElements((element) => { this.scene.mapElements((element) => {
if (isTextElement(element)) { if (isTextElement(element) && !isBoundToContainer(element)) {
invalidateShapeForElement(element); invalidateShapeForElement(element);
didUpdate = true; didUpdate = true;
return newElementWith(element, { return newElementWith(element, {

View file

@ -121,7 +121,7 @@ Object {
}, },
Object { Object {
"contextItemLabel": "labels.createContainerFromText", "contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText", "name": "wrapTextInContainer",
"perform": [Function], "perform": [Function],
"predicate": [Function], "predicate": [Function],
"trackEvent": Object { "trackEvent": Object {
@ -4518,7 +4518,7 @@ Object {
}, },
Object { Object {
"contextItemLabel": "labels.createContainerFromText", "contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText", "name": "wrapTextInContainer",
"perform": [Function], "perform": [Function],
"predicate": [Function], "predicate": [Function],
"trackEvent": Object { "trackEvent": Object {
@ -5068,7 +5068,7 @@ Object {
}, },
Object { Object {
"contextItemLabel": "labels.createContainerFromText", "contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText", "name": "wrapTextInContainer",
"perform": [Function], "perform": [Function],
"predicate": [Function], "predicate": [Function],
"trackEvent": Object { "trackEvent": Object {
@ -5917,7 +5917,7 @@ Object {
}, },
Object { Object {
"contextItemLabel": "labels.createContainerFromText", "contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText", "name": "wrapTextInContainer",
"perform": [Function], "perform": [Function],
"predicate": [Function], "predicate": [Function],
"trackEvent": Object { "trackEvent": Object {
@ -6263,7 +6263,7 @@ Object {
}, },
Object { Object {
"contextItemLabel": "labels.createContainerFromText", "contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText", "name": "wrapTextInContainer",
"perform": [Function], "perform": [Function],
"predicate": [Function], "predicate": [Function],
"trackEvent": Object { "trackEvent": Object {

View file

@ -4,7 +4,7 @@ import { UI, Pointer, Keyboard } from "./helpers/ui";
import { getTransformHandles } from "../element/transformHandles"; import { getTransformHandles } from "../element/transformHandles";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { actionCreateContainerFromText } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
const { h } = window; const { h } = window;
@ -277,7 +277,7 @@ describe("element binding", () => {
expect(h.state.selectedElementIds[text1.id]).toBe(true); expect(h.state.selectedElementIds[text1.id]).toBe(true);
h.app.actionManager.executeAction(actionCreateContainerFromText); h.app.actionManager.executeAction(actionWrapTextInContainer);
// new text container will be placed before the text element // new text container will be placed before the text element
const container = h.elements.at(-2)!; const container = h.elements.at(-2)!;

View file

@ -1,5 +1,10 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render, waitFor, GlobalTestState } from "./test-utils"; import {
render,
waitFor,
GlobalTestState,
createPasteEvent,
} from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui"; import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@ -9,6 +14,8 @@ import {
} from "../element/textElement"; } from "../element/textElement";
import { getElementBounds } from "../element"; import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types"; import { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api";
import { copyToClipboard } from "../clipboard";
const { h } = window; const { h } = window;
@ -35,38 +42,28 @@ const setClipboardText = (text: string) => {
}); });
}; };
const sendPasteEvent = () => { const sendPasteEvent = (text?: string) => {
const clipboardEvent = new Event("paste", { const clipboardEvent = createPasteEvent(
bubbles: true, text || (() => window.navigator.clipboard.readText()),
cancelable: true, );
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [],
};
document.dispatchEvent(clipboardEvent); document.dispatchEvent(clipboardEvent);
}; };
const pasteWithCtrlCmdShiftV = () => { const pasteWithCtrlCmdShiftV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
//triggering keydown with an empty clipboard //triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V); Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard //triggering paste event with faked clipboard
sendPasteEvent(); sendPasteEvent(text);
}); });
}; };
const pasteWithCtrlCmdV = () => { const pasteWithCtrlCmdV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
//triggering keydown with an empty clipboard //triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V); Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard //triggering paste event with faked clipboard
sendPasteEvent(); sendPasteEvent(text);
}); });
}; };
@ -89,6 +86,32 @@ beforeEach(async () => {
}); });
}); });
describe("general paste behavior", () => {
it("should randomize seed on paste", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].seed).not.toBe(rectangle.seed);
});
});
it("should retain seed on shift-paste", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
// assert we don't randomize seed on shift-paste
pasteWithCtrlCmdShiftV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].seed).toBe(rectangle.seed);
});
});
});
describe("paste text as single lines", () => { describe("paste text as single lines", () => {
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => { it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf"; const text = "sajgfakfn\naaksfnknas\nakefnkasf";

View file

@ -1,5 +1,10 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { GlobalTestState, render, waitFor } from "./test-utils"; import {
createPasteEvent,
GlobalTestState,
render,
waitFor,
} from "./test-utils";
import { UI, Pointer } from "./helpers/ui"; import { UI, Pointer } from "./helpers/ui";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions"; import { actionFlipHorizontal, actionFlipVertical } from "../actions";
@ -693,19 +698,7 @@ describe("freedraw", () => {
describe("image", () => { describe("image", () => {
const createImage = async () => { const createImage = async () => {
const sendPasteEvent = (file?: File) => { const sendPasteEvent = (file?: File) => {
const clipboardEvent = new Event("paste", { const clipboardEvent = createPasteEvent("", file ? [file] : []);
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [file],
};
document.dispatchEvent(clipboardEvent); document.dispatchEvent(clipboardEvent);
}; };

View file

@ -20,7 +20,7 @@ import { resize, rotate } from "./utils";
import { import {
getBoundTextElementPosition, getBoundTextElementPosition,
wrapText, wrapText,
getMaxContainerWidth, getBoundTextMaxWidth,
} from "../element/textElement"; } from "../element/textElement";
import * as textElementUtils from "../element/textElement"; import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
@ -729,7 +729,7 @@ describe("Test Linear Elements", () => {
type: "text", type: "text",
x: 0, x: 0,
y: 0, y: 0,
text: wrapText(text, font, getMaxContainerWidth(container)), text: wrapText(text, font, getBoundTextMaxWidth(container)),
containerId: container.id, containerId: container.id,
width: 30, width: 30,
height: 20, height: 20,
@ -1149,7 +1149,7 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(400); expect(rect.x).toBe(400);
expect(rect.y).toBe(0); expect(rect.y).toBe(0);
expect( expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard collaboration "Online whiteboard collaboration
made easy" made easy"
@ -1172,7 +1172,7 @@ describe("Test Linear Elements", () => {
false, false,
); );
expect( expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard "Online whiteboard
collaboration made collaboration made

View file

@ -190,3 +190,24 @@ export const toggleMenu = (container: HTMLElement) => {
// open menu // open menu
fireEvent.click(container.querySelector(".dropdown-menu-button")!); fireEvent.click(container.querySelector(".dropdown-menu-button")!);
}; };
export const createPasteEvent = (
text:
| string
| /* getData function */ ((type: string) => string | Promise<string>),
files?: File[],
) => {
return Object.assign(
new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
}),
{
clipboardData: {
getData: typeof text === "string" ? () => text : text,
files: files || [],
},
},
);
};

View file

@ -29,9 +29,9 @@ import { isOverScrollBars } from "./scene";
import { MaybeTransformHandleType } from "./element/transformHandles"; import { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library"; import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem"; import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu"; import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef } from "./utility-types"; import { Merge, ForwardRef, ValueOf } from "./utility-types";
import React from "react"; import React from "react";
export type Point = Readonly<RoughPoint>; export type Point = Readonly<RoughPoint>;
@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" };
export type BinaryFileData = { export type BinaryFileData = {
mimeType: mimeType:
| typeof ALLOWED_IMAGE_MIME_TYPES[number] | ValueOf<typeof IMAGE_MIME_TYPES>
// future user or unknown file type // future user or unknown file type
| typeof MIME_TYPES.binary; | typeof MIME_TYPES.binary;
id: FileId; id: FileId;
@ -419,7 +419,7 @@ export type AppClassProperties = {
FileId, FileId,
{ {
image: HTMLImageElement | Promise<HTMLImageElement>; image: HTMLImageElement | Promise<HTMLImageElement>;
mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number]; mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
} }
>; >;
files: BinaryFiles; files: BinaryFiles;

View file

@ -10601,9 +10601,9 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.64.4: webpack@^5.64.4:
version "5.75.0" version "5.76.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== 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"