Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
commit
4c939cefad
@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports:
|
||||
|
||||
## Excalidraw.com
|
||||
|
||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/maielo/new-readme/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||
|
||||
- 📡 PWA support (works offline).
|
||||
- 🤼 Real-time collaboration.
|
||||
|
@ -37,10 +37,9 @@ export const actionUnbindText = register({
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const { width, height, baseline } = measureTextElement(
|
||||
boundTextElement,
|
||||
{ text: boundTextElement.originalText },
|
||||
);
|
||||
const { width, height } = measureTextElement(boundTextElement, {
|
||||
text: boundTextElement.originalText,
|
||||
});
|
||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||
element.id,
|
||||
);
|
||||
@ -50,7 +49,6 @@ export const actionUnbindText = register({
|
||||
containerId: null,
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
text: boundTextElement.originalText,
|
||||
});
|
||||
mutateElement(element, {
|
||||
|
@ -109,6 +109,7 @@ import {
|
||||
textWysiwyg,
|
||||
transformElements,
|
||||
updateTextElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element";
|
||||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
@ -276,7 +277,6 @@ import {
|
||||
getContainerElement,
|
||||
getTextBindableContainerAtPosition,
|
||||
isValidTextContainer,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element/textElement";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||
import {
|
||||
@ -1688,6 +1688,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||
return newElement;
|
||||
});
|
||||
|
||||
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
|
||||
const nextElements = [
|
||||
...this.scene.getElementsIncludingDeleted(),
|
||||
@ -1700,6 +1701,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
this.scene.replaceAllElements(nextElements);
|
||||
|
||||
newElements.forEach((newElement) => {
|
||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||
const container = getContainerElement(newElement);
|
||||
redrawTextBoundingBox(newElement, container);
|
||||
}
|
||||
});
|
||||
|
||||
this.history.resumeRecording();
|
||||
|
||||
this.setState(
|
||||
@ -2728,14 +2737,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element,
|
||||
]);
|
||||
}
|
||||
|
||||
// case: creating new text not centered to parent element → offset Y
|
||||
// so that the text is centered to cursor position
|
||||
if (!parentCenterPosition) {
|
||||
mutateElement(element, {
|
||||
y: element.y - element.baseline / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
import {
|
||||
useExcalidrawAppState,
|
||||
useExcalidrawSetAppState,
|
||||
@ -33,9 +33,7 @@ import { useSetAtom } from "jotai";
|
||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||
|
||||
export const LoadScene = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||
@ -57,9 +55,7 @@ export const LoadScene = () => {
|
||||
LoadScene.displayName = "LoadScene";
|
||||
|
||||
export const SaveToActiveFile = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||
@ -80,9 +76,7 @@ SaveToActiveFile.displayName = "SaveToActiveFile";
|
||||
|
||||
export const SaveAsImage = () => {
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={ExportImageIcon}
|
||||
@ -98,9 +92,7 @@ export const SaveAsImage = () => {
|
||||
SaveAsImage.displayName = "SaveAsImage";
|
||||
|
||||
export const Help = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -119,9 +111,8 @@ export const Help = () => {
|
||||
Help.displayName = "Help";
|
||||
|
||||
export const ClearCanvas = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
|
||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -143,6 +134,7 @@ export const ClearCanvas = () => {
|
||||
ClearCanvas.displayName = "ClearCanvas";
|
||||
|
||||
export const ToggleTheme = () => {
|
||||
const { t } = useI18n();
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -175,6 +167,7 @@ export const ToggleTheme = () => {
|
||||
ToggleTheme.displayName = "ToggleTheme";
|
||||
|
||||
export const ChangeCanvasBackground = () => {
|
||||
const { t } = useI18n();
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -195,9 +188,7 @@ export const ChangeCanvasBackground = () => {
|
||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||
|
||||
export const Export = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
@ -248,9 +239,7 @@ export const LiveCollaborationTrigger = ({
|
||||
onSelect: () => void;
|
||||
isCollaborating: boolean;
|
||||
}) => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
data-testid="collab-button"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import { t, useI18n } from "../../i18n";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawActionManager,
|
||||
@ -172,10 +172,7 @@ const MenuItemLiveCollaborationTrigger = ({
|
||||
}: {
|
||||
onSelect: () => any;
|
||||
}) => {
|
||||
// FIXME when we tie t() to lang state
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const appState = useExcalidrawAppState();
|
||||
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||
{t("labels.liveCollaboration")}
|
||||
|
@ -9,6 +9,9 @@ export const isFirefox =
|
||||
"netscape" in window &&
|
||||
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||
navigator.userAgent.indexOf("Gecko") > 1;
|
||||
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
|
||||
export const isSafari =
|
||||
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
||||
|
||||
export const APP_NAME = "Excalidraw";
|
||||
|
||||
|
@ -530,6 +530,7 @@
|
||||
// (doesn't work in Firefox)
|
||||
::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@ -567,8 +568,8 @@
|
||||
}
|
||||
|
||||
.App-toolbar--mobile {
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
overflow-x: auto;
|
||||
max-width: 90vw;
|
||||
|
||||
.ToolIcon__keybinding {
|
||||
display: none;
|
||||
|
@ -176,7 +176,6 @@ const restoreElement = (
|
||||
fontSize,
|
||||
fontFamily,
|
||||
text: element.text ?? "",
|
||||
baseline: element.baseline,
|
||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: element.containerId ?? null,
|
||||
|
@ -22,15 +22,15 @@ import { getElementAbsoluteCoords } from ".";
|
||||
import { adjustXYWithRotation } from "../math";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getBoundTextElementOffset,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureTextElement,
|
||||
normalizeText,
|
||||
wrapTextElement,
|
||||
getMaxContainerWidth,
|
||||
} from "./textElement";
|
||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||
import { VERTICAL_ALIGN } from "../constants";
|
||||
import { isArrowElement } from "./typeChecks";
|
||||
import { getSubtypeMethods, isValidSubtype } from "../subtypes";
|
||||
|
||||
@ -189,7 +189,6 @@ export const newTextElement = (
|
||||
y: opts.y - offsets.y,
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
containerId: opts.containerId || null,
|
||||
originalText: text,
|
||||
},
|
||||
@ -206,18 +205,12 @@ const getAdjustedDimensions = (
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
baseline: number;
|
||||
} => {
|
||||
let maxWidth = null;
|
||||
const container = getContainerElement(element);
|
||||
if (container) {
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
}
|
||||
const {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseline,
|
||||
} = measureTextElement(element, { text: nextText }, maxWidth);
|
||||
|
||||
const { width: nextWidth, height: nextHeight } = measureTextElement(element, {
|
||||
text: nextText,
|
||||
});
|
||||
const { textAlign, verticalAlign } = element;
|
||||
let x: number;
|
||||
let y: number;
|
||||
@ -226,11 +219,9 @@ const getAdjustedDimensions = (
|
||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||
!element.containerId
|
||||
) {
|
||||
const prevMetrics = measureTextElement(
|
||||
element,
|
||||
{ fontSize: element.fontSize },
|
||||
maxWidth,
|
||||
);
|
||||
const prevMetrics = measureTextElement(element, {
|
||||
fontSize: element.fontSize,
|
||||
});
|
||||
const offsets = getTextElementPositionOffsets(element, {
|
||||
width: nextWidth - prevMetrics.width,
|
||||
height: nextHeight - prevMetrics.height,
|
||||
@ -294,7 +285,6 @@ const getAdjustedDimensions = (
|
||||
height: nextHeight,
|
||||
x: Number.isFinite(x) ? x : element.x,
|
||||
y: Number.isFinite(y) ? y : element.y,
|
||||
baseline: nextBaseline,
|
||||
};
|
||||
};
|
||||
|
||||
@ -312,38 +302,6 @@ export const refreshTextDimensions = (
|
||||
return { text, ...dimensions };
|
||||
};
|
||||
|
||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||
const width = getContainerDims(container).width;
|
||||
if (isArrowElement(container)) {
|
||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerWidth <= 0) {
|
||||
const boundText = getBoundTextElement(container);
|
||||
if (boundText) {
|
||||
return boundText.width;
|
||||
}
|
||||
return BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
return containerWidth;
|
||||
}
|
||||
return width - BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
||||
const height = getContainerDims(container).height;
|
||||
if (isArrowElement(container)) {
|
||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerHeight <= 0) {
|
||||
const boundText = getBoundTextElement(container);
|
||||
if (boundText) {
|
||||
return boundText.height;
|
||||
}
|
||||
return BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
return height - BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
||||
export const updateTextElement = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
{
|
||||
|
@ -43,12 +43,10 @@ import {
|
||||
getApproxMinLineWidth,
|
||||
getBoundTextElement,
|
||||
getBoundTextElementId,
|
||||
getBoundTextElementOffset,
|
||||
getContainerElement,
|
||||
handleBindTextResize,
|
||||
measureTextElement,
|
||||
getMaxContainerWidth,
|
||||
} from "./textElement";
|
||||
import { getMaxContainerWidth } from "./newElement";
|
||||
|
||||
export const normalizeAngle = (angle: number): number => {
|
||||
if (angle >= 2 * Math.PI) {
|
||||
@ -192,11 +190,10 @@ const rescalePointsInElement = (
|
||||
|
||||
const MIN_FONT_SIZE = 1;
|
||||
|
||||
const measureFontSizeFromWH = (
|
||||
const measureFontSizeFromWidth = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
nextWidth: number,
|
||||
nextHeight: number,
|
||||
): { size: number; baseline: number } | null => {
|
||||
): number | null => {
|
||||
// We only use width to scale font on resize
|
||||
let width = element.width;
|
||||
|
||||
@ -211,15 +208,8 @@ const measureFontSizeFromWH = (
|
||||
if (nextFontSize < MIN_FONT_SIZE) {
|
||||
return null;
|
||||
}
|
||||
const metrics = measureTextElement(
|
||||
element,
|
||||
{ fontSize: nextFontSize },
|
||||
element.containerId ? width : null,
|
||||
);
|
||||
return {
|
||||
size: nextFontSize,
|
||||
baseline: metrics.baseline + (nextHeight - metrics.height),
|
||||
};
|
||||
|
||||
return nextFontSize;
|
||||
};
|
||||
|
||||
const getSidesForTransformHandle = (
|
||||
@ -290,8 +280,8 @@ const resizeSingleTextElement = (
|
||||
if (scale > 0) {
|
||||
const nextWidth = element.width * scale;
|
||||
const nextHeight = element.height * scale;
|
||||
const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
|
||||
if (nextFont === null) {
|
||||
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
|
||||
if (nextFontSize === null) {
|
||||
return;
|
||||
}
|
||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||
@ -315,10 +305,9 @@ const resizeSingleTextElement = (
|
||||
deltaY2,
|
||||
);
|
||||
mutateElement(element, {
|
||||
fontSize: nextFont.size,
|
||||
fontSize: nextFontSize,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextFont.baseline,
|
||||
x: nextElementX,
|
||||
y: nextElementY,
|
||||
});
|
||||
@ -371,7 +360,7 @@ export const resizeSingleElement = (
|
||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||
|
||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||
let boundTextFont: { fontSize?: number } = {};
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
@ -423,23 +412,24 @@ export const resizeSingleElement = (
|
||||
if (stateOfBoundTextElementAtResize) {
|
||||
boundTextFont = {
|
||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||
baseline: stateOfBoundTextElementAtResize.baseline,
|
||||
};
|
||||
}
|
||||
if (shouldMaintainAspectRatio) {
|
||||
const boundTextElementPadding =
|
||||
getBoundTextElementOffset(boundTextElement);
|
||||
const nextFont = measureFontSizeFromWH(
|
||||
const updatedElement = {
|
||||
...element,
|
||||
width: eleNewWidth,
|
||||
height: eleNewHeight,
|
||||
};
|
||||
|
||||
const nextFontSize = measureFontSizeFromWidth(
|
||||
boundTextElement,
|
||||
eleNewWidth - boundTextElementPadding * 2,
|
||||
eleNewHeight - boundTextElementPadding * 2,
|
||||
getMaxContainerWidth(updatedElement),
|
||||
);
|
||||
if (nextFont === null) {
|
||||
if (nextFontSize === null) {
|
||||
return;
|
||||
}
|
||||
boundTextFont = {
|
||||
fontSize: nextFont.size,
|
||||
baseline: nextFont.baseline,
|
||||
fontSize: nextFontSize,
|
||||
};
|
||||
} else {
|
||||
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
||||
@ -683,7 +673,6 @@ const resizeMultipleElements = (
|
||||
y: number;
|
||||
points?: Point[];
|
||||
fontSize?: number;
|
||||
baseline?: number;
|
||||
} = {
|
||||
width,
|
||||
height,
|
||||
@ -692,31 +681,32 @@ const resizeMultipleElements = (
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
||||
let boundTextUpdates: { fontSize: number } | null = null;
|
||||
|
||||
const boundTextElement = getBoundTextElement(element.latest);
|
||||
|
||||
if (boundTextElement || isTextElement(element.orig)) {
|
||||
const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2;
|
||||
const textMeasurements = measureFontSizeFromWH(
|
||||
const updatedElement = {
|
||||
...element.latest,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
const fontSize = measureFontSizeFromWidth(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
width - optionalPadding,
|
||||
height - optionalPadding,
|
||||
getMaxContainerWidth(updatedElement),
|
||||
);
|
||||
|
||||
if (!textMeasurements) {
|
||||
if (!fontSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextElement(element.orig)) {
|
||||
update.fontSize = textMeasurements.size;
|
||||
update.baseline = textMeasurements.baseline;
|
||||
update.fontSize = fontSize;
|
||||
}
|
||||
|
||||
if (boundTextElement) {
|
||||
boundTextUpdates = {
|
||||
fontSize: textMeasurements.size,
|
||||
baseline: textMeasurements.baseline,
|
||||
fontSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { BOUND_TEXT_PADDING } from "../constants";
|
||||
import { measureText, wrapText } from "./textElement";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import {
|
||||
computeContainerHeightForBoundText,
|
||||
getContainerCoords,
|
||||
getMaxContainerWidth,
|
||||
getMaxContainerHeight,
|
||||
wrapText,
|
||||
} from "./textElement";
|
||||
import { FontString } from "./types";
|
||||
|
||||
describe("Test wrapText", () => {
|
||||
@ -65,6 +72,13 @@ up`,
|
||||
width: 250,
|
||||
res: "Hello whats up",
|
||||
},
|
||||
{
|
||||
desc: "should push the word if its equal to max width",
|
||||
width: 60,
|
||||
res: `Hello
|
||||
whats
|
||||
up`,
|
||||
},
|
||||
].forEach((data) => {
|
||||
it(`should ${data.desc}`, () => {
|
||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||
@ -72,6 +86,7 @@ up`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("When text contain new lines", () => {
|
||||
const text = `Hello
|
||||
whats up`;
|
||||
@ -162,35 +177,115 @@ break it now`,
|
||||
});
|
||||
|
||||
describe("Test measureText", () => {
|
||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||
const text = "Hello World";
|
||||
describe("Test getContainerCoords", () => {
|
||||
const params = { width: 200, height: 100, x: 10, y: 20 };
|
||||
|
||||
it("should add correct attributes when maxWidth is passed", () => {
|
||||
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
||||
const res = measureText(text, font, maxWidth);
|
||||
it("should compute coords correctly when ellipse", () => {
|
||||
const element = API.createElement({
|
||||
type: "ellipse",
|
||||
...params,
|
||||
});
|
||||
expect(getContainerCoords(element)).toEqual({
|
||||
x: 44.2893218813452455,
|
||||
y: 39.64466094067262,
|
||||
});
|
||||
});
|
||||
|
||||
expect(res.container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; max-width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
||||
>
|
||||
<span
|
||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
it("should compute coords correctly when rectangle", () => {
|
||||
const element = API.createElement({
|
||||
type: "rectangle",
|
||||
...params,
|
||||
});
|
||||
expect(getContainerCoords(element)).toEqual({
|
||||
x: 15,
|
||||
y: 25,
|
||||
});
|
||||
});
|
||||
|
||||
it("should compute coords correctly when diamond", () => {
|
||||
const element = API.createElement({
|
||||
type: "diamond",
|
||||
...params,
|
||||
});
|
||||
expect(getContainerCoords(element)).toEqual({
|
||||
x: 65,
|
||||
y: 50,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should add correct attributes when maxWidth is not passed", () => {
|
||||
const res = measureText(text, font);
|
||||
describe("Test computeContainerHeightForBoundText", () => {
|
||||
const params = {
|
||||
width: 178,
|
||||
height: 194,
|
||||
};
|
||||
|
||||
expect(res.container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
style="position: absolute; white-space: pre; font: Emoji 20px 20px; min-height: 1em;"
|
||||
>
|
||||
<span
|
||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
it("should compute container height correctly for rectangle", () => {
|
||||
const element = API.createElement({
|
||||
type: "rectangle",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerHeightForBoundText(element, 150)).toEqual(160);
|
||||
});
|
||||
|
||||
it("should compute container height correctly for ellipse", () => {
|
||||
const element = API.createElement({
|
||||
type: "ellipse",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerHeightForBoundText(element, 150)).toEqual(226);
|
||||
});
|
||||
|
||||
it("should compute container height correctly for diamond", () => {
|
||||
const element = API.createElement({
|
||||
type: "diamond",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerHeightForBoundText(element, 150)).toEqual(320);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getMaxContainerWidth", () => {
|
||||
const params = {
|
||||
width: 178,
|
||||
height: 194,
|
||||
};
|
||||
|
||||
it("should return max width when container is rectangle", () => {
|
||||
const container = API.createElement({ type: "rectangle", ...params });
|
||||
expect(getMaxContainerWidth(container)).toBe(168);
|
||||
});
|
||||
|
||||
it("should return max width when container is ellipse", () => {
|
||||
const container = API.createElement({ type: "ellipse", ...params });
|
||||
expect(getMaxContainerWidth(container)).toBe(116);
|
||||
});
|
||||
|
||||
it("should return max width when container is diamond", () => {
|
||||
const container = API.createElement({ type: "diamond", ...params });
|
||||
expect(getMaxContainerWidth(container)).toBe(79);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getMaxContainerHeight", () => {
|
||||
const params = {
|
||||
width: 178,
|
||||
height: 194,
|
||||
};
|
||||
|
||||
it("should return max height when container is rectangle", () => {
|
||||
const container = API.createElement({ type: "rectangle", ...params });
|
||||
expect(getMaxContainerHeight(container)).toBe(184);
|
||||
});
|
||||
|
||||
it("should return max height when container is ellipse", () => {
|
||||
const container = API.createElement({ type: "ellipse", ...params });
|
||||
expect(getMaxContainerHeight(container)).toBe(127);
|
||||
});
|
||||
|
||||
it("should return max height when container is diamond", () => {
|
||||
const container = API.createElement({ type: "diamond", ...params });
|
||||
expect(getMaxContainerHeight(container)).toBe(87);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -13,7 +13,6 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||
import { MaybeTransformHandleType } from "./transformHandles";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isTextElement } from ".";
|
||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isImageElement,
|
||||
@ -30,16 +29,16 @@ import {
|
||||
updateOriginalContainerCache,
|
||||
} from "./textWysiwyg";
|
||||
|
||||
export const measureTextElement = function (element, next, maxWidth) {
|
||||
export const measureTextElement = function (element, next) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.measureText) {
|
||||
return map.measureText(element, next, maxWidth);
|
||||
return map.measureText(element, next);
|
||||
}
|
||||
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||
const text = next?.text ?? element.text;
|
||||
return measureText(text, font, maxWidth);
|
||||
return measureText(text, font);
|
||||
} as SubtypeMethods["measureText"];
|
||||
|
||||
export const wrapTextElement = function (element, containerWidth, next) {
|
||||
@ -69,78 +68,69 @@ export const redrawTextBoundingBox = (
|
||||
container: ExcalidrawElement | null,
|
||||
) => {
|
||||
let maxWidth = undefined;
|
||||
let text = textElement.text;
|
||||
|
||||
const boundTextUpdates = {
|
||||
x: textElement.x,
|
||||
y: textElement.y,
|
||||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
|
||||
if (container) {
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
text = wrapTextElement(textElement, maxWidth);
|
||||
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const width = measureTextElement(
|
||||
textElement,
|
||||
{ text: textElement.originalText },
|
||||
maxWidth,
|
||||
).width;
|
||||
const { height, baseline } = measureTextElement(textElement, { text });
|
||||
const metrics = { width, height, baseline };
|
||||
let coordY = textElement.y;
|
||||
let coordX = textElement.x;
|
||||
const metrics = measureTextElement(textElement, {
|
||||
text: boundTextUpdates.text,
|
||||
});
|
||||
|
||||
boundTextUpdates.width = metrics.width;
|
||||
boundTextUpdates.height = metrics.height;
|
||||
|
||||
// Maintain coordX for non left-aligned text in case the width has changed
|
||||
if (!container) {
|
||||
if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||
coordX += textElement.width - metrics.width;
|
||||
boundTextUpdates.x += textElement.width - metrics.width;
|
||||
} else if (textElement.textAlign === TEXT_ALIGN.CENTER) {
|
||||
coordX += textElement.width / 2 - metrics.width / 2;
|
||||
boundTextUpdates.x += textElement.width / 2 - metrics.width / 2;
|
||||
}
|
||||
}
|
||||
// Resize container and vertically center align the text
|
||||
if (container) {
|
||||
if (!isArrowElement(container)) {
|
||||
const containerDims = getContainerDims(container);
|
||||
let nextHeight = containerDims.height;
|
||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||
coordY = container.y;
|
||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
coordY =
|
||||
container.y +
|
||||
containerDims.height -
|
||||
metrics.height -
|
||||
BOUND_TEXT_PADDING;
|
||||
} else {
|
||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
||||
if (metrics.height > getMaxContainerHeight(container)) {
|
||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
||||
}
|
||||
}
|
||||
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||
coordX = container.x + BOUND_TEXT_PADDING;
|
||||
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||
coordX =
|
||||
container.x +
|
||||
containerDims.width -
|
||||
metrics.width -
|
||||
BOUND_TEXT_PADDING;
|
||||
} else {
|
||||
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
|
||||
}
|
||||
updateOriginalContainerCache(container.id, nextHeight);
|
||||
mutateElement(container, { height: nextHeight });
|
||||
} else {
|
||||
if (isArrowElement(container)) {
|
||||
const centerX = textElement.x + textElement.width / 2;
|
||||
const centerY = textElement.y + textElement.height / 2;
|
||||
const diffWidth = metrics.width - textElement.width;
|
||||
const diffHeight = metrics.height - textElement.height;
|
||||
coordY = centerY - (textElement.height + diffHeight) / 2;
|
||||
coordX = centerX - (textElement.width + diffWidth) / 2;
|
||||
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;
|
||||
if (metrics.height > maxContainerHeight) {
|
||||
nextHeight = computeContainerHeightForBoundText(
|
||||
container,
|
||||
metrics.height,
|
||||
);
|
||||
mutateElement(container, { height: nextHeight });
|
||||
maxContainerHeight = getMaxContainerHeight(container);
|
||||
updateOriginalContainerCache(container.id, nextHeight);
|
||||
}
|
||||
const updatedTextElement = {
|
||||
...textElement,
|
||||
...boundTextUpdates,
|
||||
} as ExcalidrawTextElementWithContainer;
|
||||
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
|
||||
boundTextUpdates.x = x;
|
||||
boundTextUpdates.y = y;
|
||||
}
|
||||
}
|
||||
mutateElement(textElement, {
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
y: coordY,
|
||||
x: coordX,
|
||||
text,
|
||||
});
|
||||
|
||||
mutateElement(textElement, boundTextUpdates);
|
||||
};
|
||||
|
||||
export const bindTextToShapeAfterDuplication = (
|
||||
@ -212,23 +202,21 @@ export const handleBindTextResize = (
|
||||
const maxWidth = getMaxContainerWidth(container);
|
||||
const maxHeight = getMaxContainerHeight(container);
|
||||
let containerHeight = containerDims.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||
if (text) {
|
||||
text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const dimensions = measureTextElement(
|
||||
textElement,
|
||||
{ text },
|
||||
container.width,
|
||||
);
|
||||
const dimensions = measureTextElement(textElement, { text });
|
||||
nextHeight = dimensions.height;
|
||||
nextWidth = dimensions.width;
|
||||
nextBaseLine = dimensions.baseline;
|
||||
}
|
||||
// increase height in case text element height exceeds
|
||||
if (nextHeight > maxHeight) {
|
||||
containerHeight = nextHeight + getBoundTextElementOffset(textElement) * 2;
|
||||
containerHeight = computeContainerHeightForBoundText(
|
||||
container,
|
||||
nextHeight,
|
||||
);
|
||||
|
||||
const diff = containerHeight - containerDims.height;
|
||||
// fix the y coord when resizing from ne/nw/n
|
||||
const updatedY =
|
||||
@ -248,94 +236,64 @@ export const handleBindTextResize = (
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
|
||||
baseline: nextBaseLine,
|
||||
});
|
||||
|
||||
if (!isArrowElement(container)) {
|
||||
updateBoundTextPosition(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
mutateElement(
|
||||
textElement,
|
||||
computeBoundTextPosition(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateBoundTextPosition = (
|
||||
const computeBoundTextPosition = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
) => {
|
||||
const containerDims = getContainerDims(container);
|
||||
const boundTextElementPadding = getBoundTextElementOffset(boundTextElement);
|
||||
const containerCoords = getContainerCoords(container);
|
||||
const maxContainerHeight = getMaxContainerHeight(container);
|
||||
const maxContainerWidth = getMaxContainerWidth(container);
|
||||
|
||||
let x;
|
||||
let y;
|
||||
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||
y = container.y + boundTextElementPadding;
|
||||
y = containerCoords.y;
|
||||
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
y =
|
||||
container.y +
|
||||
containerDims.height -
|
||||
boundTextElement.height -
|
||||
boundTextElementPadding;
|
||||
y = containerCoords.y + (maxContainerHeight - boundTextElement.height);
|
||||
} else {
|
||||
y = container.y + containerDims.height / 2 - boundTextElement.height / 2;
|
||||
y =
|
||||
containerCoords.y +
|
||||
(maxContainerHeight / 2 - boundTextElement.height / 2);
|
||||
}
|
||||
const x =
|
||||
boundTextElement.textAlign === TEXT_ALIGN.LEFT
|
||||
? container.x + boundTextElementPadding
|
||||
: boundTextElement.textAlign === TEXT_ALIGN.RIGHT
|
||||
? container.x +
|
||||
containerDims.width -
|
||||
boundTextElement.width -
|
||||
boundTextElementPadding
|
||||
: container.x + containerDims.width / 2 - boundTextElement.width / 2;
|
||||
|
||||
mutateElement(boundTextElement, { x, y });
|
||||
if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||
x = containerCoords.x;
|
||||
} else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||
x = containerCoords.x + (maxContainerWidth - boundTextElement.width);
|
||||
} else {
|
||||
x =
|
||||
containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2);
|
||||
}
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||
export const measureText = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
maxWidth?: number | null,
|
||||
) => {
|
||||
|
||||
export const measureText = (text: string, font: FontString) => {
|
||||
text = text
|
||||
.split("\n")
|
||||
// replace empty lines with single space because leading/trailing empty
|
||||
// lines would be stripped from computation
|
||||
.map((x) => x || " ")
|
||||
.join("\n");
|
||||
const container = document.createElement("div");
|
||||
container.style.position = "absolute";
|
||||
container.style.whiteSpace = "pre";
|
||||
container.style.font = font;
|
||||
container.style.minHeight = "1em";
|
||||
|
||||
if (maxWidth) {
|
||||
const lineHeight = getApproxLineHeight(font);
|
||||
// since we are adding a span of width 1px later
|
||||
container.style.maxWidth = `${maxWidth + 1}px`;
|
||||
container.style.overflow = "hidden";
|
||||
container.style.wordBreak = "break-word";
|
||||
container.style.lineHeight = `${String(lineHeight)}px`;
|
||||
container.style.whiteSpace = "pre-wrap";
|
||||
}
|
||||
document.body.appendChild(container);
|
||||
container.innerText = text;
|
||||
const height = getTextHeight(text, font);
|
||||
const width = getTextWidth(text, font);
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.overflow = "hidden";
|
||||
span.style.width = "1px";
|
||||
span.style.height = "1px";
|
||||
container.appendChild(span);
|
||||
// Baseline is important for positioning text on canvas
|
||||
const baseline = span.offsetTop + span.offsetHeight;
|
||||
// since we are adding a span of width 1px
|
||||
const width = container.offsetWidth + 1;
|
||||
const height = container.offsetHeight;
|
||||
document.body.removeChild(container);
|
||||
if (isTestEnv()) {
|
||||
return { width, height, baseline, container };
|
||||
}
|
||||
return { width, height, baseline };
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||
@ -345,40 +303,47 @@ export const getApproxLineHeight = (font: FontString) => {
|
||||
if (cacheApproxLineHeight[font]) {
|
||||
return cacheApproxLineHeight[font];
|
||||
}
|
||||
cacheApproxLineHeight[font] = measureText(DUMMY_TEXT, font, null).height;
|
||||
const fontSize = parseInt(font);
|
||||
|
||||
// Calculate line height relative to font size
|
||||
cacheApproxLineHeight[font] = fontSize * 1.2;
|
||||
return cacheApproxLineHeight[font];
|
||||
};
|
||||
|
||||
let canvas: HTMLCanvasElement | undefined;
|
||||
|
||||
const getLineWidth = (text: string, font: FontString) => {
|
||||
if (!canvas) {
|
||||
canvas = document.createElement("canvas");
|
||||
}
|
||||
const canvas2dContext = canvas.getContext("2d")!;
|
||||
canvas2dContext.font = font;
|
||||
const width = canvas2dContext.measureText(text).width;
|
||||
|
||||
const metrics = canvas2dContext.measureText(text);
|
||||
// since in test env the canvas measureText algo
|
||||
// doesn't measure text and instead just returns number of
|
||||
// characters hence we assume that each letteris 10px
|
||||
if (isTestEnv()) {
|
||||
return metrics.width * 10;
|
||||
return width * 10;
|
||||
}
|
||||
// Since measureText behaves differently in different browsers
|
||||
// OS so considering a adjustment factor of 0.2
|
||||
const adjustmentFactor = 0.2;
|
||||
|
||||
return metrics.width + adjustmentFactor;
|
||||
return width;
|
||||
};
|
||||
|
||||
export const getTextWidth = (text: string, font: FontString) => {
|
||||
const lines = text.split("\n");
|
||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||
let width = 0;
|
||||
lines.forEach((line) => {
|
||||
width = Math.max(width, getLineWidth(line, font));
|
||||
});
|
||||
return width;
|
||||
};
|
||||
|
||||
export const getTextHeight = (text: string, font: FontString) => {
|
||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||
const lineHeight = getApproxLineHeight(font);
|
||||
return lineHeight * lines.length;
|
||||
};
|
||||
|
||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||
const lines: Array<string> = [];
|
||||
const originalLines = text.split("\n");
|
||||
@ -400,16 +365,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||
let currentLineWidthTillNow = 0;
|
||||
|
||||
let index = 0;
|
||||
|
||||
while (index < words.length) {
|
||||
const currentWordWidth = getLineWidth(words[index], font);
|
||||
// This will only happen when single word takes entire width
|
||||
if (currentWordWidth === maxWidth) {
|
||||
push(words[index]);
|
||||
index++;
|
||||
}
|
||||
|
||||
// Start breaking longer words exceeding max width
|
||||
if (currentWordWidth >= maxWidth) {
|
||||
else if (currentWordWidth > maxWidth) {
|
||||
// push current line since the current word exceeds the max width
|
||||
// so will be appended in next line
|
||||
push(currentLine);
|
||||
currentLine = "";
|
||||
currentLineWidthTillNow = 0;
|
||||
|
||||
while (words[index].length > 0) {
|
||||
const currentChar = String.fromCodePoint(
|
||||
words[index].codePointAt(0)!,
|
||||
@ -510,9 +482,9 @@ export const charWidth = (() => {
|
||||
getCache,
|
||||
};
|
||||
})();
|
||||
|
||||
export const getApproxMinLineWidth = (font: FontString) => {
|
||||
const maxCharWidth = getMaxCharWidth(font);
|
||||
|
||||
if (maxCharWidth === 0) {
|
||||
return (
|
||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
||||
@ -652,6 +624,26 @@ export const getContainerCenter = (
|
||||
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
||||
};
|
||||
|
||||
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||
let offsetX = BOUND_TEXT_PADDING;
|
||||
let offsetY = BOUND_TEXT_PADDING;
|
||||
|
||||
if (container.type === "ellipse") {
|
||||
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
|
||||
offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2);
|
||||
offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2);
|
||||
}
|
||||
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265
|
||||
if (container.type === "diamond") {
|
||||
offsetX += container.width / 4;
|
||||
offsetY += container.height / 4;
|
||||
}
|
||||
return {
|
||||
x: container.x + offsetX,
|
||||
y: container.y + offsetY,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||
const container = getContainerElement(textElement);
|
||||
if (!container || isArrowElement(container)) {
|
||||
@ -664,12 +656,13 @@ export const getBoundTextElementOffset = (
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const container = getContainerElement(boundTextElement);
|
||||
if (!container) {
|
||||
if (!container || !boundTextElement) {
|
||||
return 0;
|
||||
}
|
||||
if (isArrowElement(container)) {
|
||||
return BOUND_TEXT_PADDING * 8;
|
||||
}
|
||||
|
||||
return BOUND_TEXT_PADDING;
|
||||
};
|
||||
|
||||
@ -754,3 +747,76 @@ export const isValidTextContainer = (element: ExcalidrawElement) => {
|
||||
isArrowElement(element)
|
||||
);
|
||||
};
|
||||
|
||||
export const computeContainerHeightForBoundText = (
|
||||
container: NonDeletedExcalidrawElement,
|
||||
boundTextElementHeight: number,
|
||||
) => {
|
||||
if (container.type === "ellipse") {
|
||||
return Math.round(
|
||||
((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2,
|
||||
);
|
||||
}
|
||||
if (isArrowElement(container)) {
|
||||
return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
if (container.type === "diamond") {
|
||||
return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2);
|
||||
}
|
||||
return boundTextElementHeight + BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||
const width = getContainerDims(container).width;
|
||||
if (isArrowElement(container)) {
|
||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerWidth <= 0) {
|
||||
const boundText = getBoundTextElement(container);
|
||||
if (boundText) {
|
||||
return boundText.width;
|
||||
}
|
||||
return BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
return containerWidth;
|
||||
}
|
||||
|
||||
if (container.type === "ellipse") {
|
||||
// The width of the largest rectangle inscribed inside an ellipse is
|
||||
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
||||
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
|
||||
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||
}
|
||||
if (container.type === "diamond") {
|
||||
// The width of the largest rectangle inscribed inside a rhombus is
|
||||
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
|
||||
}
|
||||
return width - BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
||||
const height = getContainerDims(container).height;
|
||||
if (isArrowElement(container)) {
|
||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerHeight <= 0) {
|
||||
const boundText = getBoundTextElement(container);
|
||||
if (boundText) {
|
||||
return boundText.height;
|
||||
}
|
||||
return BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
if (container.type === "ellipse") {
|
||||
// The height of the largest rectangle inscribed inside an ellipse is
|
||||
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
|
||||
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
|
||||
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||
}
|
||||
if (container.type === "diamond") {
|
||||
// The height of the largest rectangle inscribed inside a rhombus is
|
||||
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
|
||||
}
|
||||
return height - BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
@ -6,14 +6,11 @@ import { CODES, KEYS } from "../keys";
|
||||
import { fireEvent } from "../tests/test-utils";
|
||||
import { queryByText } from "@testing-library/react";
|
||||
|
||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||
import { FONT_FAMILY } from "../constants";
|
||||
import {
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
FontString,
|
||||
} from "./types";
|
||||
import * as textElementUtils from "./textElement";
|
||||
import { getFontString } from "../utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { resize } from "../tests/utils";
|
||||
@ -442,17 +439,6 @@ describe("textWysiwyg", () => {
|
||||
let rectangle: any;
|
||||
const { h } = window;
|
||||
|
||||
const DUMMY_HEIGHT = 240;
|
||||
const DUMMY_WIDTH = 160;
|
||||
const APPROX_LINE_HEIGHT = 25;
|
||||
const INITIAL_WIDTH = 10;
|
||||
|
||||
beforeAll(() => {
|
||||
jest
|
||||
.spyOn(textElementUtils, "getApproxLineHeight")
|
||||
.mockReturnValue(APPROX_LINE_HEIGHT);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
h.elements = [];
|
||||
@ -734,53 +720,6 @@ describe("textWysiwyg", () => {
|
||||
});
|
||||
|
||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||
const mockMeasureText = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
maxWidth?: number | null,
|
||||
) => {
|
||||
let width = INITIAL_WIDTH;
|
||||
let height = APPROX_LINE_HEIGHT;
|
||||
let baseline = 10;
|
||||
if (!text) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
}
|
||||
baseline = 30;
|
||||
width = DUMMY_WIDTH;
|
||||
if (text === "Hello \nWorld!") {
|
||||
height = APPROX_LINE_HEIGHT * 2;
|
||||
}
|
||||
if (maxWidth) {
|
||||
width = maxWidth;
|
||||
// To capture cases where maxWidth passed is initial width
|
||||
// due to which the text is not wrapped correctly
|
||||
if (maxWidth === INITIAL_WIDTH) {
|
||||
height = DUMMY_HEIGHT;
|
||||
}
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation(mockMeasureText);
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureTextElement")
|
||||
.mockImplementation((element, next, maxWidth) => {
|
||||
return mockMeasureText(
|
||||
next?.text ?? element.text,
|
||||
getFontString(element),
|
||||
maxWidth,
|
||||
);
|
||||
});
|
||||
expect(h.elements.length).toBe(1);
|
||||
|
||||
Keyboard.keyDown(KEYS.ENTER);
|
||||
@ -789,11 +728,6 @@ describe("textWysiwyg", () => {
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
|
||||
// mock scroll height
|
||||
jest
|
||||
.spyOn(editor, "scrollHeight", "get")
|
||||
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
|
||||
|
||||
fireEvent.change(editor, {
|
||||
target: {
|
||||
value: "Hello World!",
|
||||
@ -808,11 +742,11 @@ describe("textWysiwyg", () => {
|
||||
expect(text.text).toBe("Hello \nWorld!");
|
||||
expect(text.originalText).toBe("Hello World!");
|
||||
expect(text.y).toBe(
|
||||
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
|
||||
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||
);
|
||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
||||
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
|
||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
||||
expect(text.x).toBe(25);
|
||||
expect(text.height).toBe(48);
|
||||
expect(text.width).toBe(60);
|
||||
|
||||
// Edit and text by removing second line and it should
|
||||
// still vertically align correctly
|
||||
@ -829,11 +763,6 @@ describe("textWysiwyg", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// mock scroll height
|
||||
jest
|
||||
.spyOn(editor, "scrollHeight", "get")
|
||||
.mockImplementation(() => APPROX_LINE_HEIGHT);
|
||||
editor.style.height = "25px";
|
||||
editor.dispatchEvent(new Event("input"));
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@ -843,12 +772,12 @@ describe("textWysiwyg", () => {
|
||||
|
||||
expect(text.text).toBe("Hello");
|
||||
expect(text.originalText).toBe("Hello");
|
||||
expect(text.height).toBe(24);
|
||||
expect(text.width).toBe(50);
|
||||
expect(text.y).toBe(
|
||||
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
|
||||
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||
);
|
||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
||||
expect(text.height).toBe(APPROX_LINE_HEIGHT);
|
||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
||||
expect(text.x).toBe(30);
|
||||
});
|
||||
|
||||
it("should unbind bound text when unbind action from context menu is triggered", async () => {
|
||||
@ -935,8 +864,8 @@ describe("textWysiwyg", () => {
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
109.5,
|
||||
17,
|
||||
85,
|
||||
5,
|
||||
]
|
||||
`);
|
||||
|
||||
@ -950,6 +879,8 @@ describe("textWysiwyg", () => {
|
||||
editor.select();
|
||||
|
||||
fireEvent.click(screen.getByTitle("Left"));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
@ -960,7 +891,7 @@ describe("textWysiwyg", () => {
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
15,
|
||||
90,
|
||||
66,
|
||||
]
|
||||
`);
|
||||
|
||||
@ -983,7 +914,7 @@ describe("textWysiwyg", () => {
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
424,
|
||||
375,
|
||||
-539,
|
||||
]
|
||||
`);
|
||||
@ -1098,9 +1029,9 @@ describe("textWysiwyg", () => {
|
||||
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||
expect(rectangle.x).toBe(80);
|
||||
expect(rectangle.y).toBe(85);
|
||||
expect(text.x).toBe(89.5);
|
||||
expect(text.y).toBe(90);
|
||||
expect(rectangle.y).toBe(-35);
|
||||
expect(text.x).toBe(85);
|
||||
expect(text.y).toBe(-30);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
@ -1130,43 +1061,6 @@ describe("textWysiwyg", () => {
|
||||
});
|
||||
|
||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||
const mockMeasureText = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
maxWidth?: number | null,
|
||||
) => {
|
||||
let width = INITIAL_WIDTH;
|
||||
let height = APPROX_LINE_HEIGHT;
|
||||
let baseline = 10;
|
||||
if (!text) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
}
|
||||
baseline = 30;
|
||||
width = DUMMY_WIDTH;
|
||||
height = APPROX_LINE_HEIGHT * 5;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
};
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation(mockMeasureText);
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureTextElement")
|
||||
.mockImplementation((element, next, maxWidth) => {
|
||||
return mockMeasureText(
|
||||
next?.text ?? element.text,
|
||||
getFontString(element),
|
||||
maxWidth,
|
||||
);
|
||||
});
|
||||
const originalRectHeight = rectangle.height;
|
||||
expect(rectangle.height).toBe(originalRectHeight);
|
||||
|
||||
@ -1180,7 +1074,7 @@ describe("textWysiwyg", () => {
|
||||
target: { value: "Online whiteboard collaboration made easy" },
|
||||
});
|
||||
editor.blur();
|
||||
expect(rectangle.height).toBe(135);
|
||||
expect(rectangle.height).toBe(178);
|
||||
mouse.select(rectangle);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
button: 2,
|
||||
@ -1206,7 +1100,7 @@ describe("textWysiwyg", () => {
|
||||
editor.blur();
|
||||
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect(rectangle.height).toBe(215);
|
||||
expect(rectangle.height).toBe(156);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||
|
||||
mouse.select(rectangle);
|
||||
@ -1218,13 +1112,12 @@ describe("textWysiwyg", () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.height).toBe(215);
|
||||
expect(rectangle.height).toBe(156);
|
||||
// cache updated again
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(215);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
|
||||
});
|
||||
|
||||
//@todo fix this test later once measureText is mocked correctly
|
||||
it.skip("should reset the container height cache when font properties updated", async () => {
|
||||
it("should reset the container height cache when font properties updated", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
|
||||
@ -1250,7 +1143,9 @@ describe("textWysiwyg", () => {
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
||||
).toEqual(36);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(
|
||||
96.39999999999999,
|
||||
);
|
||||
});
|
||||
|
||||
describe("should align correctly", () => {
|
||||
@ -1278,7 +1173,7 @@ describe("textWysiwyg", () => {
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
15,
|
||||
20,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
});
|
||||
@ -1288,8 +1183,8 @@ describe("textWysiwyg", () => {
|
||||
fireEvent.click(screen.getByTitle("Align top"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
94.5,
|
||||
20,
|
||||
30,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
});
|
||||
@ -1299,22 +1194,22 @@ describe("textWysiwyg", () => {
|
||||
fireEvent.click(screen.getByTitle("Align top"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
174,
|
||||
20,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
45,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("when center left", async () => {
|
||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||
fireEvent.click(screen.getByTitle("Left"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
15,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
15,
|
||||
45.5,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("when center center", async () => {
|
||||
@ -1322,11 +1217,11 @@ describe("textWysiwyg", () => {
|
||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
-25,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
30,
|
||||
45.5,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("when center right", async () => {
|
||||
@ -1334,11 +1229,11 @@ describe("textWysiwyg", () => {
|
||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
174,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
45,
|
||||
45.5,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("when bottom left", async () => {
|
||||
@ -1346,33 +1241,33 @@ describe("textWysiwyg", () => {
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
15,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
15,
|
||||
66,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("when bottom center", async () => {
|
||||
fireEvent.click(screen.getByTitle("Center"));
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
94.5,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
30,
|
||||
66,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("when bottom right", async () => {
|
||||
fireEvent.click(screen.getByTitle("Right"));
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
174,
|
||||
25,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
45,
|
||||
66,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
isBoundToContainer,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { CLASSES, VERTICAL_ALIGN } from "../constants";
|
||||
import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
@ -24,14 +24,17 @@ import { mutateElement } from "./mutateElement";
|
||||
import {
|
||||
getApproxLineHeight,
|
||||
getBoundTextElementId,
|
||||
getBoundTextElementOffset,
|
||||
getContainerCoords,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
getTextElementAngle,
|
||||
measureText,
|
||||
getTextWidth,
|
||||
measureText,
|
||||
normalizeText,
|
||||
redrawTextBoundingBox,
|
||||
wrapText,
|
||||
getMaxContainerHeight,
|
||||
getMaxContainerWidth,
|
||||
} from "./textElement";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
@ -39,7 +42,6 @@ import {
|
||||
} from "../actions/actionProperties";
|
||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||
import App from "../components/App";
|
||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
|
||||
@ -155,19 +157,23 @@ export const textWysiwyg = ({
|
||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||
let coordX = updatedTextElement.x;
|
||||
let coordY = updatedTextElement.y;
|
||||
let eCoordY = coordY;
|
||||
const container = getContainerElement(updatedTextElement);
|
||||
let maxWidth = updatedTextElement.width;
|
||||
|
||||
// Editing metrics
|
||||
const eMetrics = measureText(
|
||||
updatedTextElement.originalText,
|
||||
container && updatedTextElement.containerId
|
||||
? wrapText(
|
||||
updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
getMaxContainerWidth(container),
|
||||
)
|
||||
: updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
container ? getContainerDims(container).width : null,
|
||||
);
|
||||
|
||||
let maxWidth = eMetrics.width;
|
||||
let maxHeight = eMetrics.height;
|
||||
const width = eMetrics.width;
|
||||
let textElementWidth = Math.max(updatedTextElement.width, eMetrics.width);
|
||||
// Set to element height by default since that's
|
||||
// what is going to be used for unbounded text
|
||||
let textElementHeight = Math.max(updatedTextElement.height, maxHeight);
|
||||
@ -181,7 +187,6 @@ export const textWysiwyg = ({
|
||||
);
|
||||
coordX = boundTextCoords.x;
|
||||
coordY = boundTextCoords.y;
|
||||
eCoordY = coordY;
|
||||
}
|
||||
const propertiesUpdated = textPropertiesUpdated(
|
||||
updatedTextElement,
|
||||
@ -198,7 +203,11 @@ export const textWysiwyg = ({
|
||||
const font = getFontString(updatedTextElement);
|
||||
textElementHeight =
|
||||
getApproxLineHeight(font) *
|
||||
updatedTextElement.text.split("\n").length;
|
||||
wrapText(
|
||||
updatedTextElement.originalText,
|
||||
font,
|
||||
getMaxContainerWidth(container),
|
||||
).split("\n").length;
|
||||
textElementHeight = Math.max(
|
||||
textElementHeight,
|
||||
updatedTextElement.height,
|
||||
@ -248,25 +257,21 @@ export const textWysiwyg = ({
|
||||
// Start pushing text upward until a diff of 30px (padding)
|
||||
// is reached
|
||||
else {
|
||||
const containerCoords = getContainerCoords(container);
|
||||
|
||||
// vertically center align the text
|
||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||
if (!isArrowElement(container)) {
|
||||
coordY =
|
||||
container.y + containerDims.height / 2 - textElementHeight / 2;
|
||||
eCoordY = coordY + textElementHeight / 2 - eMetrics.height / 2;
|
||||
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
|
||||
}
|
||||
}
|
||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
coordY =
|
||||
container.y +
|
||||
containerDims.height -
|
||||
textElementHeight -
|
||||
getBoundTextElementOffset(updatedTextElement);
|
||||
eCoordY = coordY + textElementHeight - eMetrics.height;
|
||||
coordY = containerCoords.y + (maxHeight - textElementHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
const [viewportX, viewportY] = getViewportCoords(coordX, eCoordY);
|
||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||
const initialSelectionStart = editable.selectionStart;
|
||||
const initialSelectionEnd = editable.selectionEnd;
|
||||
const initialLength = editable.value.length;
|
||||
@ -308,6 +313,12 @@ export const textWysiwyg = ({
|
||||
: 0;
|
||||
const { width: w, height: h } = updatedTextElement;
|
||||
|
||||
let transformWidth = updatedTextElement.width;
|
||||
// As firefox, Safari needs little higher dimensions on DOM
|
||||
if (isFirefox || isSafari) {
|
||||
textElementWidth += 0.5;
|
||||
transformWidth += 0.5;
|
||||
}
|
||||
// Make sure text editor height doesn't go beyond viewport
|
||||
const editorMaxHeight =
|
||||
(appState.height - viewportY) / appState.zoom.value;
|
||||
@ -315,14 +326,14 @@ export const textWysiwyg = ({
|
||||
font: getFontString(updatedTextElement),
|
||||
// must be defined *after* font ¯\_(ツ)_/¯
|
||||
lineHeight: `${lineHeight}px`,
|
||||
width: `${Math.min(width, maxWidth)}px`,
|
||||
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
||||
height: `${textElementHeight}px`,
|
||||
left: `${viewportX}px`,
|
||||
top: `${viewportY}px`,
|
||||
transformOrigin: `${w / 2}px ${h / 2}px`,
|
||||
transform: getTransform(
|
||||
offsetX,
|
||||
updatedTextElement.width,
|
||||
transformWidth,
|
||||
updatedTextElement.height,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
appState,
|
||||
@ -415,55 +426,16 @@ export const textWysiwyg = ({
|
||||
id,
|
||||
) as ExcalidrawTextElement;
|
||||
const font = getFontString(updatedTextElement);
|
||||
// using scrollHeight here since we need to calculate
|
||||
// number of lines so cannot use editable.style.height
|
||||
// as that gets updated below
|
||||
// Rounding here so that the lines calculated is more accurate in all browsers.
|
||||
// The scrollHeight and approxLineHeight differs in diff browsers
|
||||
// eg it gives 1.05 in firefox for handewritten small font due to which
|
||||
// height gets updated as lines > 1 and leads to jumping text for first line in bound container
|
||||
// hence rounding here to avoid that
|
||||
const lines = Math.round(
|
||||
editable.scrollHeight / getApproxLineHeight(font),
|
||||
);
|
||||
// auto increase height only when lines > 1 so its
|
||||
// measured correctly and vertically aligns for
|
||||
// first line as well as setting height to "auto"
|
||||
// doubles the height as soon as user starts typing
|
||||
if (isBoundToContainer(element) && lines > 1) {
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element);
|
||||
|
||||
let height = "auto";
|
||||
editable.style.height = "0px";
|
||||
let heightSet = false;
|
||||
if (lines === 2) {
|
||||
const actualLineCount = wrapText(
|
||||
editable.value,
|
||||
font,
|
||||
getMaxContainerWidth(container!),
|
||||
).split("\n").length;
|
||||
// This is browser behaviour when setting height to "auto"
|
||||
// It sets the height needed for 2 lines even if actual
|
||||
// line count is 1 as mentioned above as well
|
||||
// hence reducing the height by half if actual line count is 1
|
||||
// so single line aligns vertically when deleting
|
||||
if (actualLineCount === 1) {
|
||||
height = `${editable.scrollHeight / 2}px`;
|
||||
editable.style.height = height;
|
||||
heightSet = true;
|
||||
}
|
||||
}
|
||||
const wrappedText = wrapText(
|
||||
normalizeText(editable.value),
|
||||
font,
|
||||
getMaxContainerWidth(container!),
|
||||
);
|
||||
const width = getTextWidth(wrappedText, font);
|
||||
const { width, height } = measureText(wrappedText, font);
|
||||
editable.style.width = `${width}px`;
|
||||
|
||||
if (!heightSet) {
|
||||
editable.style.height = `${editable.scrollHeight}px`;
|
||||
}
|
||||
editable.style.height = `${height}px`;
|
||||
}
|
||||
onChange(normalizeText(editable.value));
|
||||
};
|
||||
@ -500,7 +472,9 @@ export const textWysiwyg = ({
|
||||
event.code === CODES.BRACKET_RIGHT))
|
||||
) {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||
if (event.isComposing) {
|
||||
return;
|
||||
} else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||
outdent();
|
||||
} else {
|
||||
indent();
|
||||
@ -649,6 +623,7 @@ export const textWysiwyg = ({
|
||||
),
|
||||
});
|
||||
}
|
||||
redrawTextBoundingBox(updateElement, container);
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
|
@ -132,7 +132,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||
fontSize: number;
|
||||
fontFamily: FontFamilyValues;
|
||||
text: string;
|
||||
baseline: number;
|
||||
textAlign: TextAlign;
|
||||
verticalAlign: VerticalAlign;
|
||||
containerId: ExcalidrawGenericElement["id"] | null;
|
||||
|
3
src/excalidraw-app/app-jotai.ts
Normal file
3
src/excalidraw-app/app-jotai.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { unstable_createStore } from "jotai";
|
||||
|
||||
export const appJotaiStore = unstable_createStore();
|
@ -70,7 +70,7 @@ import { decryptData } from "../../data/encryption";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiStore } from "../../jotai";
|
||||
import { appJotaiStore } from "../app-jotai";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const collabDialogShownAtom = atom(false);
|
||||
@ -167,7 +167,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
setUsername: this.setUsername,
|
||||
};
|
||||
|
||||
jotaiStore.set(collabAPIAtom, collabAPI);
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
this.onOfflineStatusToggle();
|
||||
|
||||
if (
|
||||
@ -185,7 +185,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
|
||||
onOfflineStatusToggle = () => {
|
||||
jotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
||||
appJotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -208,10 +208,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
}
|
||||
|
||||
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
|
||||
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||
|
||||
private setIsCollaborating = (isCollaborating: boolean) => {
|
||||
jotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||
appJotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||
};
|
||||
|
||||
private onUnload = () => {
|
||||
@ -804,7 +804,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
|
||||
handleClose = () => {
|
||||
jotaiStore.set(collabDialogShownAtom, false);
|
||||
appJotaiStore.set(collabDialogShownAtom, false);
|
||||
};
|
||||
|
||||
setUsername = (username: string) => {
|
||||
|
@ -10,13 +10,13 @@ import {
|
||||
shareWindows,
|
||||
} from "../../components/icons";
|
||||
import { ToolButton } from "../../components/ToolButton";
|
||||
import { t } from "../../i18n";
|
||||
import "./RoomDialog.scss";
|
||||
import Stack from "../../components/Stack";
|
||||
import { AppState } from "../../types";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { getFrame } from "../../utils";
|
||||
import DialogActionButton from "../../components/DialogActionButton";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
const getShareIcon = () => {
|
||||
const navigator = window.navigator as any;
|
||||
@ -51,6 +51,7 @@ const RoomDialog = ({
|
||||
setErrorMessage: (message: string) => void;
|
||||
theme: AppState["theme"];
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const copyRoomLink = async () => {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
import { PlusPromoIcon } from "../../components/icons";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
export const AppWelcomeScreen: React.FC<{
|
||||
setCollabDialogShown: (toggle: boolean) => any;
|
||||
}> = React.memo((props) => {
|
||||
const { t } = useI18n();
|
||||
let headingContent;
|
||||
|
||||
if (isExcalidrawPlusSignedUser) {
|
||||
|
@ -1,17 +1,21 @@
|
||||
import { shield } from "../../components/icons";
|
||||
import { Tooltip } from "../../components/Tooltip";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
export const EncryptedIcon = () => (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
{shield}
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
export const EncryptedIcon = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
{shield}
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
||||
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { excalidrawPlusIcon } from "./icons";
|
||||
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
||||
@ -79,6 +79,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
||||
files: BinaryFiles;
|
||||
onError: (error: Error) => void;
|
||||
}> = ({ elements, appState, files, onError }) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Card color="primary">
|
||||
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useSetAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { langCodeAtom } from "..";
|
||||
import * as i18n from "../../i18n";
|
||||
import { appLangCodeAtom } from "..";
|
||||
import { defaultLang, useI18n } from "../../i18n";
|
||||
import { languages } from "../../i18n";
|
||||
|
||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
||||
const { t, langCode } = useI18n();
|
||||
const setLangCode = useSetAtom(appLangCodeAtom);
|
||||
|
||||
return (
|
||||
<select
|
||||
className="dropdown-select dropdown-select__language"
|
||||
onChange={({ target }) => setLangCode(target.value)}
|
||||
value={langCode}
|
||||
aria-label={i18n.t("buttons.selectLanguage")}
|
||||
aria-label={t("buttons.selectLanguage")}
|
||||
style={style}
|
||||
>
|
||||
<option key={i18n.defaultLang.code} value={i18n.defaultLang.code}>
|
||||
{i18n.defaultLang.label}
|
||||
<option key={defaultLang.code} value={defaultLang.code}>
|
||||
{defaultLang.label}
|
||||
</option>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
|
@ -76,13 +76,14 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import clsx from "clsx";
|
||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||
import { reconcileElements } from "./collab/reconciliation";
|
||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
import { AppMainMenu } from "./components/AppMainMenu";
|
||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||
import { AppFooter } from "./components/AppFooter";
|
||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||
import { useAtomWithInitialValue } from "../jotai";
|
||||
import { appJotaiStore } from "./app-jotai";
|
||||
|
||||
import "./index.scss";
|
||||
|
||||
@ -227,15 +228,15 @@ const initializeScene = async (opts: {
|
||||
return { scene: null, isExternalScene: false };
|
||||
};
|
||||
|
||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
||||
|
||||
export const langCodeAtom = atom(
|
||||
Array.isArray(currentLangCode) ? currentLangCode[0] : currentLangCode,
|
||||
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
||||
export const appLangCodeAtom = atom(
|
||||
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
||||
);
|
||||
|
||||
const ExcalidrawWrapper = () => {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||
|
||||
// initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -688,7 +689,7 @@ const ExcalidrawWrapper = () => {
|
||||
const ExcalidrawApp = () => {
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<Provider unstable_createStore={() => jotaiStore}>
|
||||
<Provider unstable_createStore={() => appJotaiStore}>
|
||||
<ExcalidrawWrapper />
|
||||
</Provider>
|
||||
</TopErrorBoundary>
|
||||
|
16
src/i18n.ts
16
src/i18n.ts
@ -1,6 +1,8 @@
|
||||
import fallbackLangData from "./locales/en.json";
|
||||
import percentages from "./locales/percentages.json";
|
||||
import { ENV } from "./constants";
|
||||
import { jotaiScope, jotaiStore } from "./jotai";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
@ -126,6 +128,8 @@ export const setLanguage = async (lang: Language) => {
|
||||
currentLangData = fallbackLangData;
|
||||
}
|
||||
}
|
||||
|
||||
jotaiStore.set(editorLangCodeAtom, lang.code);
|
||||
};
|
||||
|
||||
export const getLanguage = () => currentLang;
|
||||
@ -177,3 +181,15 @@ export const t = (
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
/** @private atom used solely to rerender components using `useI18n` hook */
|
||||
const editorLangCodeAtom = atom(defaultLang.code);
|
||||
|
||||
// Should be used in components that fall under these cases:
|
||||
// - component is rendered as an <Excalidraw> child
|
||||
// - component is rendered internally by <Excalidraw>, but the component
|
||||
// is memoized w/o being updated on `langCode`, `AppState`, or `UIAppState`
|
||||
export const useI18n = () => {
|
||||
const langCode = useAtomValue(editorLangCodeAtom, jotaiScope);
|
||||
return { t, langCode };
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
|
||||
import { PrimitiveAtom, unstable_createStore, useAtom } from "jotai";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export const jotaiScope = Symbol();
|
||||
@ -6,7 +6,7 @@ export const jotaiStore = unstable_createStore();
|
||||
|
||||
export const useAtomWithInitialValue = <
|
||||
T extends unknown,
|
||||
A extends WritableAtom<T, T>,
|
||||
A extends PrimitiveAtom<T>,
|
||||
>(
|
||||
atom: A,
|
||||
initialValue: T | (() => T),
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
|
||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷",
|
||||
"resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?",
|
||||
"removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?",
|
||||
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।"
|
||||
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "অসমর্থিত ফাইল।",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Enganxa",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Enganxar com a text pla",
|
||||
"pasteCharts": "Enganxa els diagrames",
|
||||
"selectAll": "Selecciona-ho tot",
|
||||
"multiSelect": "Afegeix un element a la selecció",
|
||||
@ -72,7 +72,7 @@
|
||||
"layers": "Capes",
|
||||
"actions": "Accions",
|
||||
"language": "Llengua",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "Col·laboració en directe...",
|
||||
"duplicateSelection": "Duplica",
|
||||
"untitled": "Sense títol",
|
||||
"name": "Nom",
|
||||
@ -116,8 +116,8 @@
|
||||
"label": "Enllaç"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
"exit": ""
|
||||
"edit": "Editar línia",
|
||||
"exit": "Sortir de l'editor de línia"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Bloca",
|
||||
@ -136,8 +136,8 @@
|
||||
"buttons": {
|
||||
"clearReset": "Neteja el llenç",
|
||||
"exportJSON": "Exporta a un fitxer",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportImage": "Exporta la imatge...",
|
||||
"export": "Guardar a...",
|
||||
"exportToPng": "Exporta a PNG",
|
||||
"exportToSvg": "Exporta a SNG",
|
||||
"copyToClipboard": "Copia al porta-retalls",
|
||||
@ -145,7 +145,7 @@
|
||||
"scale": "Escala",
|
||||
"save": "Desa al fitxer actual",
|
||||
"saveAs": "Anomena i desa",
|
||||
"load": "",
|
||||
"load": "Obrir",
|
||||
"getShareableLink": "Obté l'enllaç per a compartir",
|
||||
"close": "Tanca",
|
||||
"selectLanguage": "Trieu la llengua",
|
||||
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
||||
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
|
||||
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
|
||||
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
|
||||
"collabOfflineWarning": "Sense connexió a internet disponible.\nEls vostres canvis no seran guardats!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipus de fitxer no suportat.",
|
||||
@ -202,8 +203,8 @@
|
||||
"invalidSVGString": "SVG no vàlid.",
|
||||
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
||||
"importLibraryError": "No s'ha pogut carregar la biblioteca",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
"collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.",
|
||||
"collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selecció",
|
||||
@ -217,10 +218,10 @@
|
||||
"text": "Text",
|
||||
"library": "Biblioteca",
|
||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||
"penMode": "",
|
||||
"penMode": "Mode de llapis - evita tocar",
|
||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||
"eraser": "Esborrador",
|
||||
"hand": ""
|
||||
"hand": "Mà (eina de desplaçament)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accions del llenç",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "Formes"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Per moure el llenç, manteniu premuda la roda del ratolí o la barra espaiadora mentre arrossegueu o utilitzeu l'eina manual",
|
||||
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
|
||||
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
|
||||
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
|
||||
@ -239,7 +240,7 @@
|
||||
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
|
||||
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
||||
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_info": "Mantingueu premut Ctrl o Cmd i feu doble clic o premeu Ctrl o Cmd + Retorn per editar els punts",
|
||||
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
|
||||
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
|
||||
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
||||
@ -247,7 +248,7 @@
|
||||
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
||||
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "És probable que aquesta funció es pugui activar posant la marca \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Per canviar les marques del navegador al Firefox, visiteu la pàgina \"about:config\"."
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||
@ -295,7 +296,7 @@
|
||||
"blog": "Llegiu el nostre blog",
|
||||
"click": "clic",
|
||||
"deepSelect": "Selecció profunda",
|
||||
"deepBoxSelect": "",
|
||||
"deepBoxSelect": "Seleccioneu profundament dins del quadre i eviteu arrossegar",
|
||||
"curvedArrow": "Fletxa corba",
|
||||
"curvedLine": "Línia corba",
|
||||
"documentation": "Documentació",
|
||||
@ -316,8 +317,8 @@
|
||||
"zoomToFit": "Zoom per veure tots els elements",
|
||||
"zoomToSelection": "Zoom per veure la selecció",
|
||||
"toggleElementLock": "Blocar/desblocar la selecció",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Mou la pàgina cap amunt/a baix",
|
||||
"movePageLeftRight": "Mou la pàgina cap a l'esquerra/dreta"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Neteja el llenç"
|
||||
@ -399,7 +400,7 @@
|
||||
"fileSavedToFilename": "S'ha desat a {filename}",
|
||||
"canvas": "el llenç",
|
||||
"selection": "la selecció",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "Blanc",
|
||||
@ -450,15 +451,15 @@
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
"center_heading": "Totes les vostres dades es guarden localment al vostre navegador.",
|
||||
"center_heading_plus": "Vols anar a Excalidraw+ en comptes?",
|
||||
"menuHint": "Exportar, preferències, llenguatges..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
"menuHint": "Exportar, preferències i més...",
|
||||
"center_heading": "Diagrames. Fer. Simple.",
|
||||
"toolbarHint": "Selecciona una eina i comença a dibuixar!",
|
||||
"helpHint": "Dreceres i ajuda"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
||||
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
|
||||
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
|
||||
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
|
||||
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
|
||||
"collabOfflineWarning": "Keine Internetverbindung verfügbar.\nDeine Änderungen werden nicht gespeichert!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nicht unterstützter Dateityp.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
|
||||
"collabOfflineWarning": "Δεν υπάρχει διαθέσιμη σύνδεση στο internet.\nΟι αλλαγές σας δεν θα αποθηκευτούν!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
||||
|
@ -103,7 +103,7 @@
|
||||
"share": "Compartir",
|
||||
"showStroke": "Mostrar selector de color de trazo",
|
||||
"showBackground": "Mostrar el selector de color de fondo",
|
||||
"toggleTheme": "Alternar tema",
|
||||
"toggleTheme": "Cambiar tema",
|
||||
"personalLib": "Biblioteca personal",
|
||||
"excalidrawLib": "Biblioteca Excalidraw",
|
||||
"decreaseFontSize": "Disminuir tamaño de letra",
|
||||
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
||||
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
||||
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
|
||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
|
||||
"collabOfflineWarning": "No hay conexión a internet disponible.\n¡No se guardarán los cambios!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipo de archivo no admitido.",
|
||||
@ -233,7 +234,7 @@
|
||||
"freeDraw": "Haz clic y arrastra, suelta al terminar",
|
||||
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
|
||||
"text_selected": "Doble clic o pulse ENTER para editar el texto",
|
||||
"text_editing": "Pulse Escape o CtrlOrCmd+ENTER para terminar de editar",
|
||||
"text_editing": "Pulse Escape o Ctrl/Cmd + ENTER para terminar de editar",
|
||||
"linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar",
|
||||
"lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT",
|
||||
"resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro",
|
||||
@ -314,7 +315,7 @@
|
||||
"title": "Ayuda",
|
||||
"view": "Vista",
|
||||
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
|
||||
"zoomToSelection": "Zoom a la selección",
|
||||
"zoomToSelection": "Ampliar selección",
|
||||
"toggleElementLock": "Bloquear/desbloquear selección",
|
||||
"movePageUpDown": "Mover página hacia arriba/abajo",
|
||||
"movePageLeftRight": "Mover página hacia la izquierda/derecha"
|
||||
@ -326,9 +327,9 @@
|
||||
"title": "Publicar biblioteca",
|
||||
"itemName": "Nombre del artículo",
|
||||
"authorName": "Nombre del autor",
|
||||
"githubUsername": "Nombre de usuario de Github",
|
||||
"githubUsername": "Nombre de usuario de GitHub",
|
||||
"twitterUsername": "Nombre de usuario de Twitter",
|
||||
"libraryName": "Nombre de la librería",
|
||||
"libraryName": "Nombre de la biblioteca",
|
||||
"libraryDesc": "Descripción de la biblioteca",
|
||||
"website": "Sitio Web",
|
||||
"placeholder": {
|
||||
@ -336,7 +337,7 @@
|
||||
"libraryName": "Nombre de tu biblioteca",
|
||||
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
|
||||
"githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión",
|
||||
"twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
|
||||
"twitterHandle": "Nombre de usuario de Twitter (opcional), así sabemos a quién acreditar cuando se promociona en Twitter",
|
||||
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
|
||||
},
|
||||
"errors": {
|
||||
@ -458,7 +459,7 @@
|
||||
"menuHint": "Exportar, preferencias y más...",
|
||||
"center_heading": "Diagramas. Hecho. Simplemente.",
|
||||
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
||||
"helpHint": "Atajos & ayuda"
|
||||
"helpHint": "Atajos y ayuda"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
|
||||
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
|
||||
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
|
||||
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
|
||||
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
|
||||
"collabOfflineWarning": "Ez dago Interneteko konexiorik.\nZure aldaketak ez dira gordeko!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
|
||||
@ -220,7 +221,7 @@
|
||||
"penMode": "Luma modua - ukipena saihestu",
|
||||
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
||||
"eraser": "Borragoma",
|
||||
"hand": ""
|
||||
"hand": "Eskua (panoratze tresna)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Canvas ekintzak",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "Formak"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Oihala mugitzeko, eutsi saguaren gurpila edo zuriune-barra arrastatzean, edo erabili esku tresna",
|
||||
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
|
||||
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
|
||||
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
|
||||
@ -247,7 +248,7 @@
|
||||
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
||||
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
|
||||
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "Ezaugarri hau \"dom.events.asyncClipboard.clipboardItem\" marka \"true\" gisa ezarrita gaitu daiteke. Firefox-en arakatzailearen banderak aldatzeko, bisitatu \"about:config\" orrialdera."
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
||||
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
||||
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
||||
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
|
||||
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Liitä",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Liitä pelkkänä tekstinä",
|
||||
"pasteCharts": "Liitä kaaviot",
|
||||
"selectAll": "Valitse kaikki",
|
||||
"multiSelect": "Lisää kohde valintaan",
|
||||
@ -72,7 +72,7 @@
|
||||
"layers": "Tasot",
|
||||
"actions": "Toiminnot",
|
||||
"language": "Kieli",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "Live Yhteistyö...",
|
||||
"duplicateSelection": "Monista",
|
||||
"untitled": "Nimetön",
|
||||
"name": "Nimi",
|
||||
@ -116,14 +116,14 @@
|
||||
"label": "Linkki"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
"exit": ""
|
||||
"edit": "Muokkaa riviä",
|
||||
"exit": "Poistu rivieditorista"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Lukitse",
|
||||
"unlock": "Poista lukitus",
|
||||
"lockAll": "Lukitse kaikki",
|
||||
"unlockAll": "Poista lukitus kaikista"
|
||||
},
|
||||
"statusPublished": "Julkaistu",
|
||||
"sidebarLock": "Pidä sivupalkki avoinna"
|
||||
@ -136,8 +136,8 @@
|
||||
"buttons": {
|
||||
"clearReset": "Tyhjennä piirtoalue",
|
||||
"exportJSON": "Vie tiedostoon",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportImage": "Vie kuva...",
|
||||
"export": "Tallenna nimellä...",
|
||||
"exportToPng": "Vie PNG-tiedostona",
|
||||
"exportToSvg": "Vie SVG-tiedostona",
|
||||
"copyToClipboard": "Kopioi leikepöydälle",
|
||||
@ -145,7 +145,7 @@
|
||||
"scale": "Koko",
|
||||
"save": "Tallenna nykyiseen tiedostoon",
|
||||
"saveAs": "Tallenna nimellä",
|
||||
"load": "",
|
||||
"load": "Avaa",
|
||||
"getShareableLink": "Hae jaettava linkki",
|
||||
"close": "Sulje",
|
||||
"selectLanguage": "Valitse kieli",
|
||||
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
|
||||
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
|
||||
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
|
||||
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
|
||||
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
|
||||
"collabOfflineWarning": "Internet-yhteyttä ei ole saatavilla.\nMuutoksiasi ei tallenneta!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
|
||||
@ -201,9 +202,9 @@
|
||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||
"invalidSVGString": "Virheellinen SVG.",
|
||||
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
"importLibraryError": "Kokoelman lataaminen epäonnistui",
|
||||
"collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
|
||||
"collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Valinta",
|
||||
@ -217,10 +218,10 @@
|
||||
"text": "Teksti",
|
||||
"library": "Kirjasto",
|
||||
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
|
||||
"penMode": "",
|
||||
"penMode": "Kynätila - estä kosketus",
|
||||
"link": "Lisää/päivitä linkki valitulle muodolle",
|
||||
"eraser": "Poistotyökalu",
|
||||
"hand": ""
|
||||
"hand": "Käsi (panning-työkalu)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Piirtoalueen toiminnot",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "Muodot"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Piirtoalueen liikuttamiseksi pidä hiiren pyörää tai välilyöntiä pohjassa tai käytä käsityökalua",
|
||||
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
|
||||
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
|
||||
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
|
||||
@ -239,7 +240,7 @@
|
||||
"resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen",
|
||||
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
|
||||
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_info": "Pidä CtrlOrCmd pohjassa ja kaksoisnapsauta tai paina CtrlOrCmd + Enter muokataksesi pisteitä",
|
||||
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
|
||||
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
|
||||
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
|
||||
@ -247,7 +248,7 @@
|
||||
"bindTextToElement": "Lisää tekstiä painamalla enter",
|
||||
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
|
||||
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "Tämä ominaisuus voidaan todennäköisesti ottaa käyttöön asettamalla \"dom.events.asyncClipboard.clipboardItem\" kohta \"true\":ksi. Vaihtaaksesi selaimen kohdan Firefoxissa, käy \"about:config\" sivulla."
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
||||
@ -315,9 +316,9 @@
|
||||
"view": "Näkymä",
|
||||
"zoomToFit": "Näytä kaikki elementit",
|
||||
"zoomToSelection": "Näytä valinta",
|
||||
"toggleElementLock": "",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"toggleElementLock": "Lukitse / poista lukitus valinta",
|
||||
"movePageUpDown": "Siirrä sivua ylös/alas",
|
||||
"movePageLeftRight": "Siirrä sivua vasemmalle/oikealle"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Pyyhi piirtoalue"
|
||||
@ -399,7 +400,7 @@
|
||||
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
||||
"canvas": "piirtoalue",
|
||||
"selection": "valinta",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "Valkoinen",
|
||||
@ -450,15 +451,15 @@
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
"center_heading": "Kaikki tietosi on tallennettu paikallisesti selaimellesi.",
|
||||
"center_heading_plus": "Haluatko sen sijaan mennä Excalidraw+:aan?",
|
||||
"menuHint": "Vie, asetukset, kielet, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
"menuHint": "Vie, asetukset ja lisää...",
|
||||
"center_heading": "Kaaviot. Tehty. Yksinkertaiseksi.",
|
||||
"toolbarHint": "Valitse työkalu ja aloita piirtäminen!",
|
||||
"helpHint": "Pikanäppäimet & ohje"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
|
||||
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
|
||||
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
|
||||
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
|
||||
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
|
||||
"collabOfflineWarning": "Aucune connexion internet disponible.\nVos modifications ne seront pas enregistrées !"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Type de fichier non supporté.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.",
|
||||
"resetLibrary": "Isto limpará a súa biblioteca. Está seguro?",
|
||||
"removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?",
|
||||
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada."
|
||||
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipo de ficheiro non soportado.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
|
||||
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
|
||||
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
|
||||
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
|
||||
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।",
|
||||
"resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?",
|
||||
"removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?",
|
||||
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं"
|
||||
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं",
|
||||
"collabOfflineWarning": "कोई इंटरनेट कनेक्शन उपलब्ध नहीं है।\nआपके बदलाव सहेजे नहीं जाएंगे!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "असमर्थित फाइल प्रकार",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
|
||||
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
|
||||
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
|
||||
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
|
||||
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nem támogatott fájltípus.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
|
||||
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
|
||||
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
|
||||
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
|
||||
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipe file tidak didukung.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
|
||||
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
|
||||
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
|
||||
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
|
||||
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata.",
|
||||
"collabOfflineWarning": "Nessuna connessione internet disponibile.\nLe tue modifiche non verranno salvate!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipo di file non supportato.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
|
||||
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
|
||||
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
|
||||
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
|
||||
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。",
|
||||
"collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "サポートされていないファイル形式です。",
|
||||
@ -220,7 +221,7 @@
|
||||
"penMode": "ペンモード - タッチ防止",
|
||||
"link": "選択した図形のリンクを追加/更新",
|
||||
"eraser": "消しゴム",
|
||||
"hand": ""
|
||||
"hand": "手 (パンニングツール)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "キャンバス操作",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "図形"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグするか、手ツールを使用します",
|
||||
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
|
||||
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
||||
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
|
||||
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
|
||||
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
|
||||
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
|
||||
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
|
||||
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
|
||||
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
|
||||
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다."
|
||||
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "ناتوانێت دیمەنەکە هاوردە بکات لە URL ی دابینکراو. یان نادروستە، یان داتای \"ئێکسکالیدراو\" JSON ی دروستی تێدا نییە.",
|
||||
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
|
||||
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
|
||||
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە."
|
||||
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Nepavyko suimportuoti scenos iš pateiktos nuorodos (URL). Ji arba blogai suformatuota, arba savyje neturi teisingų Excalidraw JSON duomenų.",
|
||||
"resetLibrary": "Tai išvalys tavo biblioteką. Ar tikrai to nori?",
|
||||
"removeItemsFromsLibrary": "Ištrinti {{count}} elementą/-us iš bibliotekos?",
|
||||
"invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas."
|
||||
"invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nepalaikomas failo tipas.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
|
||||
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
|
||||
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
|
||||
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
|
||||
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Neatbalstīts datnes veids.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
|
||||
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
|
||||
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
|
||||
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे."
|
||||
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे.",
|
||||
"collabOfflineWarning": "इंटरनेट कनेक्शन उपलब्ध नाही.\nतुमचे बदल जतन केले जाणार नाहीत!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "असमर्थित फाइल प्रकार.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
|
||||
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
|
||||
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
|
||||
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
|
||||
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert.",
|
||||
"collabOfflineWarning": "Ingen Internett-tilkobling tilgjengelig.\nEndringer dine vil ikke bli lagret!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Filtypen støttes ikke.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
|
||||
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
|
||||
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
|
||||
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
|
||||
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Niet-ondersteund bestandstype.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
|
||||
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
|
||||
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
|
||||
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
|
||||
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Filtypen er ikkje støtta.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de l’URL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
|
||||
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament ?",
|
||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca ?",
|
||||
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada."
|
||||
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada.",
|
||||
"collabOfflineWarning": "Cap de connexion pas disponibla.\nVòstras modificacions seràn pas salvadas !"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",
|
||||
@ -220,7 +221,7 @@
|
||||
"penMode": "Mòde estilo - empachar lo contact",
|
||||
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
|
||||
"eraser": "Goma",
|
||||
"hand": ""
|
||||
"hand": "Man (aisina de desplaçament de la vista)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accions del canabàs",
|
||||
@ -239,7 +240,7 @@
|
||||
"resize": "Podètz servar las proporcions en mantenent la tòca MAJ pendent lo redimensionament,\nmantenètz la tòca ALT per redimensionar a partir del centre",
|
||||
"resizeImage": "Podètz retalhar liurament en quichant CTRL,\nquichatz ALT per retalhar a partir del centre",
|
||||
"rotate": "Podètz restrénger los angles en mantenent MAJ pendent la rotacion",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_info": "Tenètz quichat Ctrl o Cmd e doble clic o quichatz Ctrl o Cmd + Entrada per modificar los ponches",
|
||||
"lineEditor_pointSelected": "Quichar Suprimir per tirar lo(s) punt(s),\nCtrlOCmd+D per duplicar, o lisatz per desplaçar",
|
||||
"lineEditor_nothingSelected": "Seleccionar un punt d’editar (manténer Maj. per ne seleccionar mantun),\no manténer Alt e clicar per n’apondre de novèls",
|
||||
"placeImage": "Clicatz per plaçar l’imatge, o clicatz e lisatz per definir sa talha manualament",
|
||||
@ -316,8 +317,8 @@
|
||||
"zoomToFit": "Zoomar per veire totes los elements",
|
||||
"zoomToSelection": "Zoomar la seleccion",
|
||||
"toggleElementLock": "Verrolhar/Desverrolhar la seleccion",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Desplaçar la pagina ennaut/enbàs",
|
||||
"movePageLeftRight": "Desplaçar la pagina a esquèrra/drecha"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Escafar canabàs"
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
|
||||
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -1,26 +1,26 @@
|
||||
{
|
||||
"ar-SA": 92,
|
||||
"bg-BG": 54,
|
||||
"bn-BD": 60,
|
||||
"ca-ES": 93,
|
||||
"cs-CZ": 75,
|
||||
"da-DK": 33,
|
||||
"bn-BD": 59,
|
||||
"ca-ES": 100,
|
||||
"cs-CZ": 74,
|
||||
"da-DK": 32,
|
||||
"de-DE": 100,
|
||||
"el-GR": 99,
|
||||
"en": 100,
|
||||
"es-ES": 100,
|
||||
"eu-ES": 99,
|
||||
"eu-ES": 100,
|
||||
"fa-IR": 95,
|
||||
"fi-FI": 92,
|
||||
"fi-FI": 100,
|
||||
"fr-FR": 100,
|
||||
"gl-ES": 100,
|
||||
"gl-ES": 99,
|
||||
"he-IL": 89,
|
||||
"hi-IN": 71,
|
||||
"hu-HU": 89,
|
||||
"hu-HU": 88,
|
||||
"id-ID": 99,
|
||||
"it-IT": 100,
|
||||
"ja-JP": 99,
|
||||
"kab-KAB": 94,
|
||||
"ja-JP": 100,
|
||||
"kab-KAB": 93,
|
||||
"kk-KZ": 20,
|
||||
"ko-KR": 98,
|
||||
"ku-TR": 95,
|
||||
@ -31,22 +31,22 @@
|
||||
"nb-NO": 100,
|
||||
"nl-NL": 90,
|
||||
"nn-NO": 89,
|
||||
"oc-FR": 97,
|
||||
"pa-IN": 83,
|
||||
"oc-FR": 98,
|
||||
"pa-IN": 82,
|
||||
"pl-PL": 84,
|
||||
"pt-BR": 97,
|
||||
"pt-PT": 99,
|
||||
"ro-RO": 99,
|
||||
"pt-BR": 100,
|
||||
"pt-PT": 100,
|
||||
"ro-RO": 100,
|
||||
"ru-RU": 100,
|
||||
"si-LK": 8,
|
||||
"sk-SK": 100,
|
||||
"sl-SI": 100,
|
||||
"sv-SE": 100,
|
||||
"ta-IN": 92,
|
||||
"ta-IN": 94,
|
||||
"tr-TR": 97,
|
||||
"uk-UA": 96,
|
||||
"vi-VN": 20,
|
||||
"zh-CN": 100,
|
||||
"zh-HK": 26,
|
||||
"zh-HK": 25,
|
||||
"zh-TW": 100
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
|
||||
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
|
||||
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
|
||||
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona."
|
||||
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nieobsługiwany typ pliku.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
|
||||
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
|
||||
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
|
||||
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada."
|
||||
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.",
|
||||
"collabOfflineWarning": "Sem conexão com a internet disponível.\nSuas alterações não serão salvas!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipo de arquivo não suportado.",
|
||||
@ -220,7 +221,7 @@
|
||||
"penMode": "Modo caneta — impede o toque",
|
||||
"link": "Adicionar/Atualizar link para uma forma selecionada",
|
||||
"eraser": "Borracha",
|
||||
"hand": ""
|
||||
"hand": "Mão (ferramenta de rolagem)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Ações da tela",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "Formas"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta ou use a ferramenta de mão",
|
||||
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
||||
"freeDraw": "Toque e arraste, solte quando terminar",
|
||||
"text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
||||
@ -247,7 +248,7 @@
|
||||
"bindTextToElement": "Pressione Enter para adicionar o texto",
|
||||
"deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar",
|
||||
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "Esse recurso pode ser ativado configurando a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"."
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Não é possível mostrar pré-visualização",
|
||||
@ -450,15 +451,15 @@
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
"center_heading": "Todos os dados são salvos localmente no seu navegador.",
|
||||
"center_heading_plus": "Você queria ir para o Excalidraw+ em vez disso?",
|
||||
"menuHint": "Exportar, preferências, idiomas..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
"menuHint": "Exportar, preferências e mais...",
|
||||
"center_heading": "Diagramas, Feito. Simples.",
|
||||
"toolbarHint": "Escolha uma ferramenta e comece a desenhar!",
|
||||
"helpHint": "Atalhos e ajuda"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
|
||||
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
|
||||
"removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?",
|
||||
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada."
|
||||
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada.",
|
||||
"collabOfflineWarning": "Sem ligação à internet disponível.\nAs suas alterações não serão salvas!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipo de ficheiro não suportado.",
|
||||
@ -220,7 +221,7 @@
|
||||
"penMode": "Modo caneta - impedir toque",
|
||||
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
|
||||
"eraser": "Borracha",
|
||||
"hand": ""
|
||||
"hand": "Mão (ferramenta de movimento da tela)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Ações da área de desenho",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "Formas"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta, ou use a ferramenta da mão",
|
||||
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
||||
"freeDraw": "Clique e arraste, large quando terminar",
|
||||
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
||||
@ -247,7 +248,7 @@
|
||||
"bindTextToElement": "Carregue Enter para acrescentar texto",
|
||||
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
|
||||
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "Esta função pode provavelmente ser ativada definindo a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"."
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
|
||||
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
|
||||
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
|
||||
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată."
|
||||
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată.",
|
||||
"collabOfflineWarning": "Nu este disponibilă nicio conexiune la internet.\nModificările nu vor fi salvate!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tip de fișier neacceptat.",
|
||||
@ -220,7 +221,7 @@
|
||||
"penMode": "Mod stilou – împiedică atingerea",
|
||||
"link": "Adăugare/actualizare URL pentru forma selectată",
|
||||
"eraser": "Radieră",
|
||||
"hand": ""
|
||||
"hand": "Mână (instrument de panoramare)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Acțiuni pentru pânză",
|
||||
@ -228,7 +229,7 @@
|
||||
"shapes": "Forme"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu sau folosește instrumentul în formă de mână",
|
||||
"linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie",
|
||||
"freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat",
|
||||
"text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție",
|
||||
@ -247,7 +248,7 @@
|
||||
"bindTextToElement": "Apasă tasta Enter pentru a adăuga text",
|
||||
"deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea",
|
||||
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "Această caracteristică poate fi probabil activată prin setarea preferinței „dom.events.asyncClipboard.clipboardItem” ca „true”. Pentru a schimba preferințele navigatorului în Firefox, accesează pagina „about:config”."
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Nu se poate afișa previzualizarea",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Невозможно импортировать сцену с предоставленного URL. Неверный формат, или не содержит верных Excalidraw JSON данных.",
|
||||
"resetLibrary": "Это очистит вашу библиотеку. Вы уверены?",
|
||||
"removeItemsFromsLibrary": "Удалить {{count}} объект(ов) из библиотеки?",
|
||||
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено."
|
||||
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено.",
|
||||
"collabOfflineWarning": "Отсутствует интернет-соединение.\nВаши изменения не будут сохранены!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Неподдерживаемый тип файла.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Nepodarilo sa načítať scénu z poskytnutej URL. Je nevalidná alebo neobsahuje žiadne validné Excalidraw JSON dáta.",
|
||||
"resetLibrary": "Týmto vyprázdnite vašu knižnicu. Ste si istý?",
|
||||
"removeItemsFromsLibrary": "Odstrániť {{count}} položiek z knižnice?",
|
||||
"invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá."
|
||||
"invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá.",
|
||||
"collabOfflineWarning": "Internetové pripojenie nie je dostupné.\nVaše zmeny nebudú uložené!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nepodporovaný typ súboru.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "S priloženega URL-ja ni bilo mogoče uvoziti scene. Je napačno oblikovana ali pa ne vsebuje veljavnih podatkov Excalidraw JSON.",
|
||||
"resetLibrary": "To bo počistilo vašo knjižnico. Ali ste prepričani?",
|
||||
"removeItemsFromsLibrary": "Izbriši elemente ({{count}}) iz knjižnice?",
|
||||
"invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno."
|
||||
"invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno.",
|
||||
"collabOfflineWarning": "Internetna povezava ni na voljo.\nVaše spremembe ne bodo shranjene!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nepodprt tip datoteke.",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Det gick inte att importera skiss från den angivna webbadressen. Antingen har den fel format, eller så innehåller den ingen giltig Excalidraw JSON data.",
|
||||
"resetLibrary": "Detta kommer att rensa ditt bibliotek. Är du säker?",
|
||||
"removeItemsFromsLibrary": "Ta bort {{count}} objekt från biblioteket?",
|
||||
"invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat."
|
||||
"invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat.",
|
||||
"collabOfflineWarning": "Ingen internetanslutning tillgänglig.\nDina ändringar kommer inte att sparas!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Filtypen stöds inte.",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "ஒட்டு",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "அலங்காரமின்றி ஒட்டு",
|
||||
"pasteCharts": "விளக்கப்படங்களை ஒட்டு",
|
||||
"selectAll": "எல்லாம் தேர்ந்தெடு",
|
||||
"multiSelect": "உறுப்பைத் தெரிவில் சேர்",
|
||||
@ -54,7 +54,7 @@
|
||||
"veryLarge": "மிகப் பெரிய",
|
||||
"solid": "திடமான",
|
||||
"hachure": "மலைக்குறிக்கோடு",
|
||||
"crossHatch": "",
|
||||
"crossHatch": "குறுக்குகதவு",
|
||||
"thin": "மெல்லிய",
|
||||
"bold": "பட்டை",
|
||||
"left": "இடது",
|
||||
@ -72,7 +72,7 @@
|
||||
"layers": "அடுக்குகள்",
|
||||
"actions": "செயல்கள்",
|
||||
"language": "மொழி",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "நேரடி கூட்டுப்பணி...",
|
||||
"duplicateSelection": "நகலாக்கு",
|
||||
"untitled": "தலைப்பற்றது",
|
||||
"name": "பெயர்",
|
||||
@ -116,7 +116,7 @@
|
||||
"label": "தொடுப்பு"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
"edit": "தொடுப்பைத் திருத்து",
|
||||
"exit": ""
|
||||
},
|
||||
"elementLock": {
|
||||
@ -137,7 +137,7 @@
|
||||
"clearReset": "கித்தானை அகரமாக்கு",
|
||||
"exportJSON": "கோப்புக்கு ஏற்றுமதிசெய்",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"export": "இதில் சேமி...",
|
||||
"exportToPng": "PNGக்கு ஏற்றுமதிசெய்",
|
||||
"exportToSvg": "SVGக்கு ஏற்றுமதிசெய்",
|
||||
"copyToClipboard": "நகலகத்திற்கு நகலெடு",
|
||||
@ -145,7 +145,7 @@
|
||||
"scale": "அளவு",
|
||||
"save": "தற்போதைய கோப்புக்குச் சேமி",
|
||||
"saveAs": "இப்படி சேமி",
|
||||
"load": "",
|
||||
"load": "திற",
|
||||
"getShareableLink": "பகிரக்கூடிய தொடுப்பைப் பெறு",
|
||||
"close": "மூடு",
|
||||
"selectLanguage": "மொழியைத் தேர்ந்தெடு",
|
||||
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "வழங்கப்பட்ட உரலியிலிருந்து காட்சியை இறக்கவியலா. இது தவறான வடிவத்தில் உள்ளது, அ செல்லத்தக்க எக்ஸ்கேலிட்ரா JSON தரவைக் கொண்டில்லை.",
|
||||
"resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?",
|
||||
"removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?",
|
||||
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது."
|
||||
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.",
|
||||
@ -456,9 +457,9 @@
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
"center_heading": "எளிமையாக வரைபடங்கள் உருவாக்க!",
|
||||
"toolbarHint": "கருவியைத் தேர்ந்தெடு & வரை!",
|
||||
"helpHint": "குறுக்குவழிகள் & உதவி"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Verilen bağlantıdan çalışma alanı yüklenemedi. Dosya bozuk olabilir veya geçerli bir Excalidraw JSON verisi bulundurmuyor olabilir.",
|
||||
"resetLibrary": "Bu işlem kütüphanenizi sıfırlayacak. Emin misiniz?",
|
||||
"removeItemsFromsLibrary": "{{count}} öğe(ler) kitaplıktan kaldırılsın mı?",
|
||||
"invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı."
|
||||
"invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı.",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Desteklenmeyen dosya türü.",
|
||||
|
@ -72,7 +72,7 @@
|
||||
"layers": "Шари",
|
||||
"actions": "Дії",
|
||||
"language": "Мова",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "Спільна робота у режимі реального часу...",
|
||||
"duplicateSelection": "Дублювати",
|
||||
"untitled": "Без назви",
|
||||
"name": "Ім’я",
|
||||
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "Не вдалося імпортувати сцену з наданого URL. Він або недоформований, або не містить дійсних даних Excalidraw JSON.",
|
||||
"resetLibrary": "Це призведе до очищення бібліотеки. Ви впевнені?",
|
||||
"removeItemsFromsLibrary": "Видалити {{count}} елемент(ів) з бібліотеки?",
|
||||
"invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено."
|
||||
"invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено.",
|
||||
"collabOfflineWarning": "Немає підключення до Інтернету.\nВаші зміни не будуть збережені!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Непідтримуваний тип файлу.",
|
||||
@ -457,8 +458,8 @@
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
"toolbarHint": "Оберіть інструмент і почніть малювати!",
|
||||
"helpHint": "Гарячі клавіші і допомога"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "无法从提供的 URL 导入场景。它或者格式不正确,或者不包含有效的 Excalidraw JSON 数据。",
|
||||
"resetLibrary": "这将会清除你的素材库。你确定要这么做吗?",
|
||||
"removeItemsFromsLibrary": "确定要从素材库中删除 {{count}} 个项目吗?",
|
||||
"invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。"
|
||||
"invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。",
|
||||
"collabOfflineWarning": "无网络连接。\n您的改动将不会被保存!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "不支持的文件格式。",
|
||||
@ -239,7 +240,7 @@
|
||||
"resize": "您可以按住SHIFT来限制比例大小,\n按住ALT来调整中心大小",
|
||||
"resizeImage": "按住SHIFT可以自由缩放,\n按住ALT可以从中间缩放",
|
||||
"rotate": "旋转时可以按住 Shift 来约束角度",
|
||||
"lineEditor_info": "按住 CtrlOrCmd 并双击或按 CtrlOrmd + Enter 来编辑点",
|
||||
"lineEditor_info": "按住 CtrlOrCmd 并双击或按 CtrlOrCmd + Enter 来编辑点",
|
||||
"lineEditor_pointSelected": "按下 Delete 移除点,Ctrl 或 Cmd+D 以复制,拖动以移动",
|
||||
"lineEditor_nothingSelected": "选择要编辑的点 (按住 SHIFT 选择多个),\n或按住 Alt 并点击以添加新点",
|
||||
"placeImage": "点击放置图像,或者点击并拖动以手动设置图像大小",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@ -192,7 +192,8 @@
|
||||
"invalidSceneUrl": "無法由提供的 URL 匯入場景。可能是發生異常,或未包含有效的 Excalidraw JSON 資料。",
|
||||
"resetLibrary": "這會清除您的資料庫,是否確定?",
|
||||
"removeItemsFromsLibrary": "從資料庫刪除 {{count}} 項?",
|
||||
"invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。"
|
||||
"invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。",
|
||||
"collabOfflineWarning": "沒有可用的網路連線。\n變更無法儲存!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "不支援的檔案類型。",
|
||||
|
@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)
|
||||
|
||||
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
|
||||
|
||||
```js
|
||||
|
@ -87,8 +87,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InitializeApp langCode={langCode} theme={theme}>
|
||||
<Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
|
||||
<Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
|
||||
<InitializeApp langCode={langCode} theme={theme}>
|
||||
<App
|
||||
onChange={onChange}
|
||||
initialData={initialData}
|
||||
@ -118,8 +118,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
</Provider>
|
||||
</InitializeApp>
|
||||
</InitializeApp>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -198,7 +198,7 @@ export {
|
||||
isInvisiblySmallElement,
|
||||
getNonDeletedElements,
|
||||
} from "../../element";
|
||||
export { defaultLang, languages } from "../../i18n";
|
||||
export { defaultLang, useI18n, languages } from "../../i18n";
|
||||
export {
|
||||
restore,
|
||||
restoreAppState,
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
getApproxLineHeight,
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
getMaxContainerWidth,
|
||||
getTextWidth,
|
||||
measureText,
|
||||
wrapText,
|
||||
@ -46,7 +47,6 @@ import {
|
||||
import { mathSubtypeIcon } from "./icon";
|
||||
import { getMathSubtypeRecord } from "./types";
|
||||
import { SubtypeButton } from "../../../../components/Subtypes";
|
||||
import { getMaxContainerWidth } from "../../../../element/newElement";
|
||||
|
||||
const mathSubtype = getMathSubtypeRecord().subtype;
|
||||
const FONT_FAMILY_MATH = FONT_FAMILY.Helvetica;
|
||||
@ -607,7 +607,10 @@ const measureMarkup = (
|
||||
const grandchild = child as Text;
|
||||
const text = grandchild.textContent ?? "";
|
||||
if (text !== "") {
|
||||
const textMetrics = measureText(text, font, maxWidth);
|
||||
const constrainedText = maxWidth
|
||||
? wrapText(text, font, maxWidth)
|
||||
: text;
|
||||
const textMetrics = measureText(constrainedText, font);
|
||||
childMetrics.push({
|
||||
x: nextX,
|
||||
y: baseline,
|
||||
@ -834,25 +837,16 @@ const cleanMathElementUpdate = function (updates) {
|
||||
return oldUpdates;
|
||||
} as SubtypeMethods["clean"];
|
||||
|
||||
const measureMathElement = function (element, next, maxWidth) {
|
||||
const measureMathElement = function (element, next) {
|
||||
ensureMathElement(element);
|
||||
const isMathJaxLoaded = mathJaxLoaded;
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const text = next?.text ?? element.text;
|
||||
const customData = next?.customData ?? element.customData;
|
||||
const mathProps = getMathProps.ensureMathProps(customData);
|
||||
const noMaxWidth = mathProps.mathOnly;
|
||||
const cWidth = noMaxWidth ? undefined : maxWidth;
|
||||
const metrics = getImageMetrics(
|
||||
text,
|
||||
fontSize,
|
||||
mathProps,
|
||||
isMathJaxLoaded,
|
||||
cWidth,
|
||||
);
|
||||
const { height, baseline } = metrics;
|
||||
const width = noMaxWidth ? maxWidth ?? metrics.width : metrics.width;
|
||||
return { width, height, baseline };
|
||||
const metrics = getImageMetrics(text, fontSize, mathProps, isMathJaxLoaded);
|
||||
const { width, height } = metrics;
|
||||
return { width, height };
|
||||
} as SubtypeMethods["measureText"];
|
||||
|
||||
const renderMathElement = function (element, context, renderCb) {
|
||||
|
@ -37,13 +37,11 @@ import {
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
MIME_TYPES,
|
||||
SVG_NS,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||
import {
|
||||
getApproxLineHeight,
|
||||
getBoundTextElement,
|
||||
getBoundTextElementOffset,
|
||||
getContainerElement,
|
||||
} from "../element/textElement";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
@ -287,22 +285,19 @@ const drawElementOnCanvas = (
|
||||
const lineHeight = element.containerId
|
||||
? getApproxLineHeight(getFontString(element))
|
||||
: element.height / lines.length;
|
||||
let verticalOffset = element.height - element.baseline;
|
||||
if (element.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
verticalOffset = getBoundTextElementOffset(element);
|
||||
}
|
||||
|
||||
const horizontalOffset =
|
||||
element.textAlign === "center"
|
||||
? element.width / 2
|
||||
: element.textAlign === "right"
|
||||
? element.width
|
||||
: 0;
|
||||
context.textBaseline = "bottom";
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
context.fillText(
|
||||
lines[index],
|
||||
horizontalOffset,
|
||||
(index + 1) * lineHeight - verticalOffset,
|
||||
(index + 1) * lineHeight,
|
||||
);
|
||||
}
|
||||
context.restore();
|
||||
@ -1312,7 +1307,7 @@ export const renderElementToSvg = (
|
||||
);
|
||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||
const lineHeight = element.height / lines.length;
|
||||
const verticalOffset = element.height - element.baseline;
|
||||
const verticalOffset = element.height;
|
||||
const horizontalOffset =
|
||||
element.textAlign === "center"
|
||||
? element.width / 2
|
||||
|
@ -231,8 +231,7 @@ export type SubtypeMethods = {
|
||||
text?: string;
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
},
|
||||
maxWidth?: number | null,
|
||||
) => { width: number; height: number; baseline: number };
|
||||
) => { width: number; height: number };
|
||||
render: (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
context: CanvasRenderingContext2D,
|
||||
|
@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
|
||||
class="excalidraw-wysiwyg"
|
||||
data-type="wysiwyg"
|
||||
dir="auto"
|
||||
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 1px; height: 0px; left: 39.5px; top: 20px; transform-origin: 0.5px 0px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -20px; font: Emoji 20px 20px; line-height: 0px; font-family: Virgil, Segoe UI Emoji;"
|
||||
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10px; height: 24px; left: 35px; top: 8px; transform-origin: 5px 12px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
|
||||
tabindex="0"
|
||||
wrap="off"
|
||||
/>
|
||||
|
@ -182,3 +182,73 @@ describe("paste text as a single element", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Paste bound text container", () => {
|
||||
const container = {
|
||||
type: "ellipse",
|
||||
id: "container-id",
|
||||
x: 554.984375,
|
||||
y: 196.0234375,
|
||||
width: 166,
|
||||
height: 187.01953125,
|
||||
roundness: { type: 2 },
|
||||
boundElements: [{ type: "text", id: "text-id" }],
|
||||
};
|
||||
const textElement = {
|
||||
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",
|
||||
baseline: 168,
|
||||
textAlign: "center",
|
||||
verticalAlign: "middle",
|
||||
containerId: container.id,
|
||||
originalText:
|
||||
"Excalidraw is a virtual opensource whiteboard for sketching hand-drawn like diagrams",
|
||||
};
|
||||
|
||||
it("should fix ellipse bounding box", async () => {
|
||||
const data = JSON.stringify({
|
||||
type: "excalidraw/clipboard",
|
||||
elements: [container, textElement],
|
||||
});
|
||||
setClipboardText(data);
|
||||
pasteWithCtrlCmdShiftV();
|
||||
|
||||
await waitFor(async () => {
|
||||
await sleep(1);
|
||||
expect(h.elements.length).toEqual(2);
|
||||
const container = h.elements[0];
|
||||
expect(container.height).toBe(354);
|
||||
expect(container.width).toBe(166);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fix diamond bounding box", async () => {
|
||||
const data = JSON.stringify({
|
||||
type: "excalidraw/clipboard",
|
||||
elements: [
|
||||
{
|
||||
...container,
|
||||
type: "diamond",
|
||||
},
|
||||
textElement,
|
||||
],
|
||||
});
|
||||
setClipboardText(data);
|
||||
pasteWithCtrlCmdShiftV();
|
||||
|
||||
await waitFor(async () => {
|
||||
await sleep(1);
|
||||
expect(h.elements.length).toEqual(2);
|
||||
const container = h.elements[0];
|
||||
expect(container.height).toBe(740);
|
||||
expect(container.width).toBe(166);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -282,7 +282,6 @@ exports[`restoreElements should restore text element correctly passing value for
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"baseline": 0,
|
||||
"boundElements": Array [],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
@ -312,8 +311,8 @@ Object {
|
||||
"versionNonce": 0,
|
||||
"verticalAlign": "middle",
|
||||
"width": 100,
|
||||
"x": -0.5,
|
||||
"y": 0,
|
||||
"x": -20,
|
||||
"y": -8.4,
|
||||
}
|
||||
`;
|
||||
|
||||
@ -321,7 +320,6 @@ exports[`restoreElements should restore text element correctly with unknown font
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"baseline": 0,
|
||||
"boundElements": Array [],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
|
@ -17,8 +17,11 @@ import { KEYS } from "../keys";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { queryByTestId, queryByText } from "@testing-library/react";
|
||||
import { resize, rotate } from "./utils";
|
||||
import { getBoundTextElementPosition, wrapText } from "../element/textElement";
|
||||
import { getMaxContainerWidth } from "../element/newElement";
|
||||
import {
|
||||
getBoundTextElementPosition,
|
||||
wrapText,
|
||||
getMaxContainerWidth,
|
||||
} from "../element/textElement";
|
||||
import * as textElementUtils from "../element/textElement";
|
||||
import { ROUNDNESS } from "../constants";
|
||||
|
||||
@ -1028,7 +1031,7 @@ describe("Test Linear Elements", () => {
|
||||
expect({ width: container.width, height: container.height })
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"height": 10,
|
||||
"height": 128,
|
||||
"width": 367,
|
||||
}
|
||||
`);
|
||||
@ -1036,8 +1039,8 @@ describe("Test Linear Elements", () => {
|
||||
expect(getBoundTextElementPosition(container, textElement))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"x": 386.5,
|
||||
"y": 70,
|
||||
"x": 272,
|
||||
"y": 46,
|
||||
}
|
||||
`);
|
||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||
@ -1049,11 +1052,11 @@ describe("Test Linear Elements", () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
20,
|
||||
60,
|
||||
391.8122896842806,
|
||||
70,
|
||||
36,
|
||||
502,
|
||||
94,
|
||||
205.9061448421403,
|
||||
65,
|
||||
53,
|
||||
]
|
||||
`);
|
||||
});
|
||||
@ -1087,7 +1090,7 @@ describe("Test Linear Elements", () => {
|
||||
expect({ width: container.width, height: container.height })
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"height": 0,
|
||||
"height": 128,
|
||||
"width": 340,
|
||||
}
|
||||
`);
|
||||
@ -1095,8 +1098,8 @@ describe("Test Linear Elements", () => {
|
||||
expect(getBoundTextElementPosition(container, textElement))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"x": 189.5,
|
||||
"y": 20,
|
||||
"x": 75,
|
||||
"y": -4,
|
||||
}
|
||||
`);
|
||||
expect(textElement.text).toMatchInlineSnapshot(`
|
||||
|
@ -33,7 +33,7 @@ import { actionChangeRoundness } from "../actions/actionProperties";
|
||||
const MW = 200;
|
||||
const TWIDTH = 200;
|
||||
const THEIGHT = 20;
|
||||
const TBASELINE = 15;
|
||||
const TBASELINE = 0;
|
||||
const FONTSIZE = 20;
|
||||
const DBFONTSIZE = 40;
|
||||
const TRFONTSIZE = 60;
|
||||
@ -155,11 +155,7 @@ const prepareTest1Subtype = function (
|
||||
return { actions, methods };
|
||||
} as SubtypePrepFn;
|
||||
|
||||
const measureTest2: SubtypeMethods["measureText"] = function (
|
||||
element,
|
||||
next,
|
||||
maxWidth,
|
||||
) {
|
||||
const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
||||
const text = next?.text ?? element.text;
|
||||
const customData = next?.customData ?? {};
|
||||
const fontSize = customData.triple
|
||||
@ -167,10 +163,10 @@ const measureTest2: SubtypeMethods["measureText"] = function (
|
||||
: next?.fontSize ?? element.fontSize;
|
||||
const fontFamily = element.fontFamily;
|
||||
const fontString = getFontString({ fontSize, fontFamily });
|
||||
const metrics = textElementUtils.measureText(text, fontString, maxWidth);
|
||||
const metrics = textElementUtils.measureText(text, fontString);
|
||||
const width = Math.max(metrics.width - 10, 0);
|
||||
const height = Math.max(metrics.height - 5, 0);
|
||||
return { width, height, baseline: metrics.baseline + 1 };
|
||||
return { width, height, baseline: 1 };
|
||||
};
|
||||
|
||||
const wrapTest2: SubtypeMethods["wrapText"] = function (
|
||||
@ -450,12 +446,6 @@ describe("subtypes", () => {
|
||||
height: THEIGHT - 5,
|
||||
baseline: TBASELINE + 1,
|
||||
});
|
||||
const mMetrics = textElementUtils.measureTextElement(el, {}, MW);
|
||||
expect(mMetrics).toStrictEqual({
|
||||
width: Math.min(TWIDTH, MW) - 10,
|
||||
height: THEIGHT - 5,
|
||||
baseline: TBASELINE + 1,
|
||||
});
|
||||
const wrappedText = textElementUtils.wrapTextElement(el, MW);
|
||||
expect(wrappedText).toEqual(
|
||||
`${testString.split(" ").join("\n")}\nHello world.`,
|
||||
@ -482,12 +472,6 @@ describe("subtypes", () => {
|
||||
height: 2 * THEIGHT - 5,
|
||||
baseline: 2 * TBASELINE + 1,
|
||||
});
|
||||
const nextFMW = textElementUtils.measureTextElement(el, next, MW);
|
||||
expect(nextFMW).toStrictEqual({
|
||||
width: Math.min(2 * TWIDTH, MW) - 10,
|
||||
height: 2 * THEIGHT - 5,
|
||||
baseline: 2 * TBASELINE + 1,
|
||||
});
|
||||
const nextFWrText = textElementUtils.wrapTextElement(el, MW, next);
|
||||
expect(nextFWrText).toEqual(
|
||||
`${testString.split(" ").join("\n")}\nHELLO World.`,
|
||||
@ -501,12 +485,6 @@ describe("subtypes", () => {
|
||||
height: 3 * THEIGHT - 5,
|
||||
baseline: 3 * TBASELINE + 1,
|
||||
});
|
||||
const nextCDMW = textElementUtils.measureTextElement(el, next, MW);
|
||||
expect(nextCDMW).toStrictEqual({
|
||||
width: Math.min(3 * TWIDTH, MW) - 10,
|
||||
height: 3 * THEIGHT - 5,
|
||||
baseline: 3 * TBASELINE + 1,
|
||||
});
|
||||
const nextCDWrText = textElementUtils.wrapTextElement(el, MW, next);
|
||||
expect(nextCDWrText).toEqual(
|
||||
`${testString.split(" ").join("\n")}\nHELLO WORLD.`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user