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

This commit is contained in:
Alex Kim
2023-05-04 20:16:33 +05:00
100 changed files with 985 additions and 453 deletions
+8 -2
View File
@@ -16,6 +16,7 @@ import {
import {
getOriginalContainerHeightFromCache,
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "../element/textWysiwyg";
import {
hasBoundTextElement,
@@ -145,7 +146,11 @@ export const actionBindText = register({
id: textElement.id,
}),
});
const originalContainerHeight = container.height;
redrawTextBoundingBox(textElement, container);
// overwritting the cache with original container height so
// it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight);
return {
elements: pushTextAboveContainer(elements, container, textElement),
@@ -191,8 +196,8 @@ const pushContainerBelowText = (
return updatedElements;
};
export const actionCreateContainerFromText = register({
name: "createContainerFromText",
export const actionWrapTextInContainer = register({
name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" },
predicate: (elements, appState) => {
@@ -281,6 +286,7 @@ export const actionCreateContainerFromText = register({
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
},
false,
);
+1 -1
View File
@@ -18,7 +18,7 @@ export const actionCopy = register({
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(selectedElements, appState, app.files);
copyToClipboard(selectedElements, app.files);
return {
commitToHistory: false,
+7 -5
View File
@@ -84,7 +84,7 @@ import {
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import { arrayToMap } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@@ -314,9 +314,9 @@ export const actionChangeFillStyle = register({
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
const allElementsZigZag = selectedElements.every(
(el) => el.fillStyle === "zigzag",
);
const allElementsZigZag =
selectedElements.length > 0 &&
selectedElements.every((el) => el.fillStyle === "zigzag");
return (
<fieldset>
@@ -326,7 +326,9 @@ export const actionChangeFillStyle = register({
options={[
{
value: "hachure",
text: t("labels.hachure"),
text: `${
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
} (${getShortcutKey("Alt-Click")})`,
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
active: allElementsZigZag ? true : undefined,
},
+1 -1
View File
@@ -115,7 +115,7 @@ export type ActionName =
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool"
| "createContainerFromText";
| "wrapTextInContainer";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
+14 -3
View File
@@ -20,9 +20,20 @@ export const trackEvent = (
});
}
// MATOMO event tracking _paq must be same as the one in index.html
if (window._paq) {
window._paq.push(["trackEvent", category, action, label, value]);
if (window.sa_event) {
window.sa_event(action, {
category,
label,
value,
});
}
if (window.fathom) {
window.fathom.trackEvent(action, {
category,
label,
value,
});
}
} catch (error) {
console.error("error during analytics", error);
+9 -8
View File
@@ -1,5 +1,6 @@
import oc from "open-color";
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
@@ -23,18 +24,18 @@ export const getDefaultAppState = (): Omit<
theme: THEME.LIGHT,
collaborators: new Map(),
currentChartType: "bar",
currentItemBackgroundColor: "transparent",
currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
currentItemEndArrowhead: "arrow",
currentItemFillStyle: "hachure",
currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
currentItemFontFamily: DEFAULT_FONT_FAMILY,
currentItemFontSize: DEFAULT_FONT_SIZE,
currentItemOpacity: 100,
currentItemRoughness: 1,
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
currentItemStartArrowhead: null,
currentItemStrokeColor: oc.black,
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
currentItemRoundness: "round",
currentItemStrokeStyle: "solid",
currentItemStrokeWidth: 1,
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
cursorButton: "up",
draggingElement: null,
@@ -44,7 +45,7 @@ export const getDefaultAppState = (): Omit<
activeTool: {
type: "selection",
customType: null,
locked: false,
locked: DEFAULT_ELEMENT_PROPS.locked,
lastActiveTool: null,
},
penMode: false,
+1 -17
View File
@@ -1,10 +1,5 @@
import colors from "./colors";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
ENV,
VERTICAL_ALIGN,
} from "./constants";
import { DEFAULT_FONT_SIZE, ENV } from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random";
@@ -166,17 +161,7 @@ const bgColors = colors.elementBackground.slice(
// Put all the common properties here so when the whole chart is selected
// the properties dialog shows the correct selected values
const commonProps = {
fillStyle: "hachure",
fontFamily: DEFAULT_FONT_FAMILY,
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: colors.elementStroke[0],
roundness: null,
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
locked: false,
} as const;
const getChartDimentions = (spreadsheet: Spreadsheet) => {
@@ -323,7 +308,6 @@ const chartBaseElements = (
x: x + chartWidth / 2,
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
roundness: null,
strokeStyle: "solid",
textAlign: "center",
})
: null;
+27 -11
View File
@@ -2,12 +2,12 @@ import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { AppState, BinaryFiles } from "./types";
import { BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks";
import { isPromiseLike } from "./utils";
import { isPromiseLike, isTestEnv } from "./utils";
type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@@ -55,24 +55,40 @@ const clipboardContainsElements = (
export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles | null,
) => {
let foundFile = false;
const _files = elements.reduce((acc, element) => {
if (isInitializedImageElement(element)) {
foundFile = true;
if (files && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
}
return acc;
}, {} as BinaryFiles);
if (foundFile && !files) {
console.warn(
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
);
}
// select binded text elements when copying
const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements,
files: files
? elements.reduce((acc, element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
return acc;
}, {} as BinaryFiles)
: undefined,
files: files ? _files : undefined,
};
const json = JSON.stringify(contents);
if (isTestEnv()) {
return json;
}
CLIPBOARD = json;
try {
PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json);
+18 -6
View File
@@ -60,6 +60,7 @@ import {
ENV,
EVENT,
GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
isAndroid,
isBrave,
@@ -295,7 +296,7 @@ import {
} from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionCreateContainerFromText } from "../actions/actionBoundText";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
const deviceContextInitialValue = {
@@ -1589,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
elements: data.elements,
files: data.files || null,
position: "cursor",
retainSeed: isPlainPaste,
});
} else if (data.text) {
this.addTextFromPaste(data.text, isPlainPaste);
@@ -1602,6 +1604,7 @@ class App extends React.Component<AppProps, AppState> {
elements: readonly ExcalidrawElement[];
files: BinaryFiles | null;
position: { clientX: number; clientY: number } | "cursor" | "center";
retainSeed?: boolean;
}) => {
const elements = restoreElements(opts.elements, null);
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@@ -1639,6 +1642,9 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + gridY - minY,
});
}),
{
randomizeSeed: !opts.retainSeed,
},
);
const nextElements = [
@@ -2732,7 +2738,6 @@ class App extends React.Component<AppProps, AppState> {
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness: null,
text: "",
fontSize,
fontFamily,
@@ -2744,8 +2749,8 @@ class App extends React.Component<AppProps, AppState> {
: DEFAULT_VERTICAL_ALIGN,
containerId: shouldBindToContainer ? container?.id : undefined,
groupIds: container?.groupIds ?? [],
locked: false,
lineHeight,
angle: container?.angle ?? 0,
});
if (!existingTextElement && shouldBindToContainer && container) {
@@ -4721,7 +4726,12 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.drag.hasOccurred = true;
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
// it would have weird results (stuff jumping all over the screen)
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
// Checking for editingElement to avoid jump while editing on mobile #6503
if (
selectedElements.length > 0 &&
!pointerDownState.withCmdOrCtrl &&
!this.state.editingElement
) {
const [dragX, dragY] = getGridPoint(
pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y,
@@ -5744,7 +5754,9 @@ class App extends React.Component<AppProps, AppState> {
const imageFile = await fileOpen({
description: "Image",
extensions: ["jpg", "png", "svg", "gif"],
extensions: Object.keys(
IMAGE_MIME_TYPES,
) as (keyof typeof IMAGE_MIME_TYPES)[],
});
const imageElement = this.createImageElement({
@@ -6366,7 +6378,7 @@ class App extends React.Component<AppProps, AppState> {
actionGroup,
actionUnbindText,
actionBindText,
actionCreateContainerFromText,
actionWrapTextInContainer,
actionUngroup,
CONTEXT_MENU_SEPARATOR,
actionAddToLibrary,
+1
View File
@@ -183,6 +183,7 @@
width: 100%;
margin: 0;
font-size: 0.875rem;
font-family: inherit;
background-color: transparent;
color: var(--text-primary-color);
border: 0;
+1
View File
@@ -30,6 +30,7 @@
background-color: transparent;
border: none;
white-space: nowrap;
font-family: inherit;
display: grid;
grid-template-columns: 1fr 0.2fr;
+11 -13
View File
@@ -4,7 +4,6 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types";
import { Dialog } from "./Dialog";
import { clipboard } from "./icons";
@@ -15,6 +14,7 @@ import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager";
import { exportToCanvas } from "../packages/utils";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
@@ -83,7 +83,6 @@ const ImageExportModal = ({
const someElementIsSelected = isSomeElementSelected(elements, appState);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null);
const { exportBackground, viewBackgroundColor } = appState;
const [renderError, setRenderError] = useState<Error | null>(null);
const exportedElements = exportSelected
@@ -99,10 +98,16 @@ const ImageExportModal = ({
if (!previewNode) {
return;
}
exportToCanvas(exportedElements, appState, files, {
exportBackground,
viewBackgroundColor,
const maxWidth = previewNode.offsetWidth;
if (!maxWidth) {
return;
}
exportToCanvas({
elements: exportedElements,
appState,
files,
exportPadding,
maxWidthOrHeight: maxWidth,
})
.then((canvas) => {
setRenderError(null);
@@ -116,14 +121,7 @@ const ImageExportModal = ({
console.error(error);
setRenderError(error);
});
}, [
appState,
files,
exportedElements,
exportBackground,
exportPadding,
viewBackgroundColor,
]);
}, [appState, files, exportedElements, exportPadding]);
return (
<div className="ExportDialog">
+1 -1
View File
@@ -102,7 +102,7 @@ const LibraryMenuItems = ({
...item,
// duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465
elements: duplicateElements(item.elements),
elements: duplicateElements(item.elements, { randomizeSeed: true }),
};
});
};
+3
View File
@@ -2,6 +2,9 @@
// container in body where the actual tooltip is appended to
.excalidraw-tooltip {
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
position: fixed;
z-index: 1000;
+38 -17
View File
@@ -1,6 +1,7 @@
import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types";
import { FontFamilyValues } from "./element/types";
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
import oc from "open-color";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const isWindows = /^Win/.test(navigator.platform);
@@ -104,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20; // TODO make it configurable?
export const MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
json: "application/json",
export const IMAGE_MIME_TYPES = {
svg: "image/svg+xml",
"excalidraw.svg": "image/svg+xml",
png: "image/png",
"excalidraw.png": "image/png",
jpg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
ico: "image/x-icon",
avif: "image/avif",
jfif: "image/jfif",
} as const;
export const MIME_TYPES = {
json: "application/json",
// excalidraw data
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
// image-encoded excalidraw data
"excalidraw.svg": "image/svg+xml",
"excalidraw.png": "image/png",
// binary
binary: "application/octet-stream",
// image
...IMAGE_MIME_TYPES,
} as const;
export const EXPORT_DATA_TYPES = {
@@ -188,16 +199,6 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
export const ALLOWED_IMAGE_MIME_TYPES = [
MIME_TYPES.png,
MIME_TYPES.jpg,
MIME_TYPES.svg,
MIME_TYPES.gif,
MIME_TYPES.webp,
MIME_TYPES.bmp,
MIME_TYPES.ico,
] as const;
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg";
@@ -254,3 +255,23 @@ export const ROUNDNESS = {
/** key containt id of precedeing elemnt id we use in reconciliation during
* collaboration */
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
export const DEFAULT_ELEMENT_PROPS: {
strokeColor: ExcalidrawElement["strokeColor"];
backgroundColor: ExcalidrawElement["backgroundColor"];
fillStyle: ExcalidrawElement["fillStyle"];
strokeWidth: ExcalidrawElement["strokeWidth"];
strokeStyle: ExcalidrawElement["strokeStyle"];
roughness: ExcalidrawElement["roughness"];
opacity: ExcalidrawElement["opacity"];
locked: ExcalidrawElement["locked"];
} = {
strokeColor: oc.black,
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
locked: false,
};
+2
View File
@@ -354,6 +354,7 @@
border-radius: var(--space-factor);
border: 1px solid var(--button-gray-2);
font-size: 0.8rem;
font-family: inherit;
outline: none;
appearance: none;
background-image: var(--dropdown-icon);
@@ -413,6 +414,7 @@
bottom: 30px;
transform: translateX(-50%);
pointer-events: all;
font-family: inherit;
&:hover {
background-color: var(--button-hover-bg);
+4 -6
View File
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors";
@@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
export const isSupportedImageFile = (
blob: Blob | null | undefined,
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => {
): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
const { type } = blob || {};
return (
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
);
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
};
export const loadSceneOrLibraryFromBlob = async (
@@ -157,7 +155,7 @@ export const loadSceneOrLibraryFromBlob = async (
},
localAppState,
localElements,
{ repairBindings: true, refreshDimensions: true },
{ repairBindings: true, refreshDimensions: false },
),
};
} else if (isValidLibrary(data)) {
+1 -10
View File
@@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants";
import { AbortError } from "../errors";
import { debounce } from "../utils";
type FILE_EXTENSION =
| "gif"
| "jpg"
| "png"
| "excalidraw.png"
| "svg"
| "excalidraw.svg"
| "json"
| "excalidraw"
| "excalidrawlib";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 500;
+65 -24
View File
@@ -20,7 +20,7 @@ import {
isTestEnv,
} from "../utils";
import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement";
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types";
import { getElementAbsoluteCoords } from ".";
@@ -33,10 +33,17 @@ import {
measureText,
normalizeText,
wrapText,
getMaxContainerWidth,
getBoundTextMaxWidth,
getDefaultLineHeight,
} from "./textElement";
import { VERTICAL_ALIGN } from "../constants";
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { isArrowElement } from "./typeChecks";
import { MarkOptional, Merge, Mutable } from "../utility-types";
@@ -51,6 +58,15 @@ type ElementConstructorOpts = MarkOptional<
| "version"
| "versionNonce"
| "link"
| "strokeStyle"
| "fillStyle"
| "strokeColor"
| "backgroundColor"
| "roughness"
| "strokeWidth"
| "roundness"
| "locked"
| "opacity"
>;
const _newElementBase = <T extends ExcalidrawElement>(
@@ -58,13 +74,13 @@ const _newElementBase = <T extends ExcalidrawElement>(
{
x,
y,
strokeColor,
backgroundColor,
fillStyle,
strokeWidth,
strokeStyle,
roughness,
opacity,
strokeColor = DEFAULT_ELEMENT_PROPS.strokeColor,
backgroundColor = DEFAULT_ELEMENT_PROPS.backgroundColor,
fillStyle = DEFAULT_ELEMENT_PROPS.fillStyle,
strokeWidth = DEFAULT_ELEMENT_PROPS.strokeWidth,
strokeStyle = DEFAULT_ELEMENT_PROPS.strokeStyle,
roughness = DEFAULT_ELEMENT_PROPS.roughness,
opacity = DEFAULT_ELEMENT_PROPS.opacity,
width = 0,
height = 0,
angle = 0,
@@ -72,7 +88,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
roundness = null,
boundElements = null,
link = null,
locked,
locked = DEFAULT_ELEMENT_PROPS.locked,
...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => {
@@ -138,27 +154,39 @@ const getTextElementPositionOffsets = (
export const newTextElement = (
opts: {
text: string;
fontSize: number;
fontFamily: FontFamilyValues;
textAlign: TextAlign;
verticalAlign: VerticalAlign;
fontSize?: number;
fontFamily?: FontFamilyValues;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"];
lineHeight?: ExcalidrawTextElement["lineHeight"];
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => {
const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily);
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
const text = normalizeText(opts.text);
const metrics = measureText(text, getFontString(opts), lineHeight);
const offsets = getTextElementPositionOffsets(opts, metrics);
const metrics = measureText(
text,
getFontString({ fontFamily, fontSize }),
lineHeight,
);
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
const offsets = getTextElementPositionOffsets(
{ textAlign, verticalAlign },
metrics,
);
const textElement = newElementWith(
{
..._newElementBase<ExcalidrawTextElement>("text", opts),
text,
fontSize: opts.fontSize,
fontFamily: opts.fontFamily,
textAlign: opts.textAlign,
verticalAlign: opts.verticalAlign,
fontSize,
fontFamily,
textAlign,
verticalAlign,
x: opts.x - offsets.x,
y: opts.y - offsets.y,
width: metrics.width,
@@ -282,7 +310,7 @@ export const refreshTextDimensions = (
text = wrapText(
text,
getFontString(textElement),
getMaxContainerWidth(container),
getBoundTextMaxWidth(container),
);
}
const dimensions = getAdjustedDimensions(textElement, text);
@@ -511,8 +539,16 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
* it's advised to supply the whole elements array, or sets of elements that
* are encapsulated (such as library items), if the purpose is to retain
* bindings to the cloned elements intact.
*
* NOTE by default does not randomize or regenerate anything except the id.
*/
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
export const duplicateElements = (
elements: readonly ExcalidrawElement[],
opts?: {
/** NOTE also updates version flags and `updated` */
randomizeSeed: boolean;
},
) => {
const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements);
@@ -546,6 +582,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
clonedElement.id = maybeGetNewId(element.id)!;
if (opts?.randomizeSeed) {
clonedElement.seed = randomInteger();
bumpVersion(clonedElement);
}
if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) {
+5 -5
View File
@@ -46,10 +46,10 @@ import {
getBoundTextElementId,
getContainerElement,
handleBindTextResize,
getMaxContainerWidth,
getBoundTextMaxWidth,
getApproxMinLineHeight,
measureText,
getMaxContainerHeight,
getBoundTextMaxHeight,
} from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
@@ -210,7 +210,7 @@ const measureFontSizeFromWidth = (
if (hasContainer) {
const container = getContainerElement(element);
if (container) {
width = getMaxContainerWidth(container);
width = getBoundTextMaxWidth(container);
}
}
const nextFontSize = element.fontSize * (nextWidth / width);
@@ -441,8 +441,8 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
getMaxContainerWidth(updatedElement),
getMaxContainerHeight(updatedElement),
getBoundTextMaxWidth(updatedElement),
getBoundTextMaxHeight(updatedElement, boundTextElement),
);
if (nextFont === null) {
return;
+99 -11
View File
@@ -3,14 +3,15 @@ import { API } from "../tests/helpers/api";
import {
computeContainerDimensionForBoundText,
getContainerCoords,
getMaxContainerWidth,
getMaxContainerHeight,
getBoundTextMaxWidth,
getBoundTextMaxHeight,
wrapText,
detectLineHeight,
getLineHeightInPx,
getDefaultLineHeight,
parseTokens,
} from "./textElement";
import { FontString } from "./types";
import { ExcalidrawTextElementWithContainer, FontString } from "./types";
describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@@ -183,6 +184,56 @@ now`,
expect(wrapText(text, font, -1)).toEqual(text);
expect(wrapText(text, font, Infinity)).toEqual(text);
});
it("should wrap the text correctly when text contains hyphen", () => {
let text =
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
const res = wrapText(text, font, 110);
expect(res).toBe(
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`,
);
text = "Hello thereusing-now";
expect(wrapText(text, font, 100)).toEqual("Hello \nthereusin\ng-now");
});
});
describe("Test parseTokens", () => {
it("should split into tokens correctly", () => {
let text = "Excalidraw is a virtual collaborative whiteboard";
expect(parseTokens(text)).toEqual([
"Excalidraw",
"is",
"a",
"virtual",
"collaborative",
"whiteboard",
]);
text =
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
expect(parseTokens(text)).toEqual([
"Wikipedia",
"is",
"hosted",
"by",
"Wikimedia-",
"",
"Foundation,",
"a",
"non-",
"profit",
"organization",
"that",
"also",
"hosts",
"a",
"range-",
"of",
"other",
"projects",
]);
});
});
describe("Test measureText", () => {
@@ -260,7 +311,7 @@ describe("Test measureText", () => {
});
});
describe("Test getMaxContainerWidth", () => {
describe("Test getBoundTextMaxWidth", () => {
const params = {
width: 178,
height: 194,
@@ -268,39 +319,76 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerWidth(container)).toBe(168);
expect(getBoundTextMaxWidth(container)).toBe(168);
});
it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerWidth(container)).toBe(116);
expect(getBoundTextMaxWidth(container)).toBe(116);
});
it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerWidth(container)).toBe(79);
expect(getBoundTextMaxWidth(container)).toBe(79);
});
});
describe("Test getMaxContainerHeight", () => {
describe("Test getBoundTextMaxHeight", () => {
const params = {
width: 178,
height: 194,
id: '"container-id',
};
const boundTextElement = API.createElement({
type: "text",
id: "text-id",
x: 560.51171875,
y: 202.033203125,
width: 154,
height: 175,
fontSize: 20,
fontFamily: 1,
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
textAlign: "center",
verticalAlign: "middle",
containerId: params.id,
}) as ExcalidrawTextElementWithContainer;
it("should return max height when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerHeight(container)).toBe(184);
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
});
it("should return max height when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerHeight(container)).toBe(127);
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
});
it("should return max height when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerHeight(container)).toBe(87);
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
});
it("should return max height when container is arrow", () => {
const container = API.createElement({
type: "arrow",
...params,
});
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
});
it("should return max height when container is arrow and height is less than threshold", () => {
const container = API.createElement({
type: "arrow",
...params,
height: 70,
boundElements: [{ type: "text", id: "text-id" }],
});
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
boundTextElement.height,
);
});
});
});
+73 -57
View File
@@ -65,7 +65,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text;
if (container) {
maxWidth = getMaxContainerWidth(container);
maxWidth = getBoundTextMaxWidth(container);
boundTextUpdates.text = wrapText(
textElement.originalText,
getFontString(textElement),
@@ -83,35 +83,28 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline;
if (container) {
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;
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
} else {
const containerDims = getContainerDims(container);
let maxContainerHeight = getMaxContainerHeight(container);
const containerDims = getContainerDims(container);
const maxContainerHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
metrics.height,
container.type,
);
mutateElement(container, { height: nextHeight });
maxContainerHeight = getMaxContainerHeight(container);
updateOriginalContainerCache(container.id, nextHeight);
}
const updatedTextElement = {
...textElement,
...boundTextUpdates,
} as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
boundTextUpdates.x = x;
boundTextUpdates.y = y;
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
metrics.height,
container.type,
);
mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight);
}
const updatedTextElement = {
...textElement,
...boundTextUpdates,
} as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
boundTextUpdates.x = x;
boundTextUpdates.y = y;
}
mutateElement(textElement, boundTextUpdates);
@@ -183,8 +176,11 @@ export const handleBindTextResize = (
let nextHeight = textElement.height;
let nextWidth = textElement.width;
const containerDims = getContainerDims(container);
const maxWidth = getMaxContainerWidth(container);
const maxHeight = getMaxContainerHeight(container);
const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
@@ -256,8 +252,8 @@ export const computeBoundTextPosition = (
);
}
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container);
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getBoundTextMaxWidth(container);
let x;
let y;
@@ -419,6 +415,24 @@ export const getTextHeight = (
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
};
export const parseTokens = (text: string) => {
// Splitting words containing "-" as those are treated as separate words
// by css wrapping algorithm eg non-profit => non-, profit
const words = text.split("-");
if (words.length > 1) {
// non-proft org => ['non-', 'profit org']
words.forEach((word, index) => {
if (index !== words.length - 1) {
words[index] = word += "-";
}
});
}
// Joining the words with space and splitting them again with space to get the
// final list of tokens
// ['non-', 'profit org'] =>,'non- proft org' => ['non-','profit','org']
return words.join(" ").split(" ");
};
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// if maxWidth is not finite or NaN which can happen in case of bugs in
// computation, we need to make sure we don't continue as we'll end up
@@ -444,17 +458,16 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine = "";
currentLineWidthTillNow = 0;
};
originalLines.forEach((originalLine) => {
const currentLineWidth = getTextWidth(originalLine, font);
//Push the line if its <= maxWidth
// Push the line if its <= maxWidth
if (currentLineWidth <= maxWidth) {
lines.push(originalLine);
return; // continue
}
const words = originalLine.split(" ");
const words = parseTokens(originalLine);
resetParams();
let index = 0;
@@ -472,6 +485,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
else if (currentWordWidth > maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
push(currentLine);
resetParams();
@@ -492,15 +506,15 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
push(currentLine);
resetParams();
} else {
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
// to previous line
// to previous line unless the line ends with hyphen to sync
// with css word-wrap
} else if (!currentLine.endsWith("-")) {
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
@@ -518,12 +532,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
break;
}
index++;
currentLine += `${word} `;
// if word ends with "-" then we don't need to add space
// to sync with css word-wrap
const shouldAppendSpace = !word.endsWith("-");
currentLine += word;
if (shouldAppendSpace) {
currentLine += " ";
}
// Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1);
push(word);
if (shouldAppendSpace) {
lines.push(currentLine.slice(0, -1));
} else {
lines.push(currentLine);
}
resetParams();
break;
}
@@ -861,18 +886,10 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding;
};
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
export const getBoundTextMaxWidth = (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 * 8 * 2;
}
if (container.type === "ellipse") {
@@ -889,16 +906,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => {
return width - BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
export const getBoundTextMaxHeight = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
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 boundTextElement.height;
}
return height;
}
+31 -1
View File
@@ -526,6 +526,36 @@ describe("textWysiwyg", () => {
]);
});
it("should set the text element angle to same as container angle when binding to rotated container", async () => {
const rectangle = API.createElement({
type: "rectangle",
width: 90,
height: 75,
angle: 45,
});
h.elements = [rectangle];
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
expect(text.angle).toBe(rectangle.angle);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
const diamond = API.createElement({
type: "diamond",
@@ -1506,7 +1536,7 @@ describe("textWysiwyg", () => {
expect.objectContaining({
text: "Excalidraw is an opensource virtual collaborative whiteboard",
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.LEFT,
textAlign: TEXT_ALIGN.CENTER,
boundElements: null,
}),
);
+9 -6
View File
@@ -32,8 +32,8 @@ import {
normalizeText,
redrawTextBoundingBox,
wrapText,
getMaxContainerHeight,
getMaxContainerWidth,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
computeContainerDimensionForBoundText,
detectLineHeight,
} from "./textElement";
@@ -205,8 +205,11 @@ export const textWysiwyg = ({
}
}
maxWidth = getMaxContainerWidth(container);
maxHeight = getMaxContainerHeight(container);
maxWidth = getBoundTextMaxWidth(container);
maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
);
// autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) {
@@ -377,7 +380,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getMaxContainerWidth(container),
getBoundTextMaxWidth(container),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
@@ -394,7 +397,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getMaxContainerWidth(container!),
getBoundTextMaxWidth(container!),
);
const { width, height } = measureText(
wrappedText,
+1 -1
View File
@@ -65,7 +65,7 @@ export const reconcileElements = (
// Mark duplicate for removal as it'll be replaced with the remote element
if (local) {
// Unless the ramote and local elements are the same element in which case
// Unless the remote and local elements are the same element in which case
// we need to keep it as we'd otherwise discard it from the resulting
// array.
if (local[0] === remoteElement) {
+1 -1
View File
@@ -263,7 +263,7 @@ export const loadScene = async (
await importFromBackend(id, privateKey),
localDataState?.appState,
localDataState?.elements,
{ repairBindings: true, refreshDimensions: true },
{ repairBindings: true, refreshDimensions: false },
);
} else {
data = restore(localDataState || null, null, null, {
+2 -2
View File
@@ -18,8 +18,8 @@ interface Window {
EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function;
_paq: any[];
_mtm: any[];
sa_event: Function;
fathom: { trackEvent: Function };
}
interface CanvasRenderingContext2D {
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "كبير جدا",
"solid": "كامل",
"hachure": "خطوط",
"zigzag": "",
"crossHatch": "خطوط متقطعة",
"thin": "نحيف",
"bold": "داكن",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Много голям",
"solid": "Солиден",
"hachure": "Хералдика",
"zigzag": "",
"crossHatch": "Двойно-пресечено",
"thin": "Тънък",
"bold": "Ясно очертан",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "অনেক বড়",
"solid": "দৃঢ়",
"hachure": "ভ্রুলেখা",
"zigzag": "",
"crossHatch": "ক্রস হ্যাচ",
"thin": "পাতলা",
"bold": "পুরু",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Molt gran",
"solid": "Sòlid",
"hachure": "Ratlletes",
"zigzag": "",
"crossHatch": "Ratlletes creuades",
"thin": "Fi",
"bold": "Negreta",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Velmi velké",
"solid": "Plný",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "Tenký",
"bold": "Tlustý",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Meget stor",
"solid": "Solid",
"hachure": "Skravering",
"zigzag": "",
"crossHatch": "Krydsskravering",
"thin": "Tynd",
"bold": "Fed",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Sehr groß",
"solid": "Deckend",
"hachure": "Schraffiert",
"zigzag": "Zickzack",
"crossHatch": "Kreuzschraffiert",
"thin": "Dünn",
"bold": "Fett",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Πολύ μεγάλο",
"solid": "Συμπαγής",
"hachure": "Εκκόλαψη",
"zigzag": "",
"crossHatch": "Διασταυρούμενη εκκόλαψη",
"thin": "Λεπτή",
"bold": "Έντονη",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Very large",
"solid": "Solid",
"hachure": "Hachure",
"zigzag": "Zigzag",
"crossHatch": "Cross-hatch",
"thin": "Thin",
"bold": "Bold",
+5 -4
View File
@@ -54,6 +54,7 @@
"veryLarge": "Muy grande",
"solid": "Sólido",
"hachure": "Folleto",
"zigzag": "Zigzag",
"crossHatch": "Rayado transversal",
"thin": "Fino",
"bold": "Grueso",
@@ -207,8 +208,8 @@
"collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.",
"collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"start": "Parece que estás usando el navegador Brave",
"aggressive_block_fingerprint": "Bloquear huellas dactilares agresivamente",
"setting_enabled": "ajuste activado",
"break": "Esto podría resultar en romper los",
"text_elements": "Elementos de texto",
@@ -319,8 +320,8 @@
"doubleClick": "doble clic",
"drag": "arrastrar",
"editor": "Editor",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Editar puntos de línea/flecha",
"editText": "Editar texto / añadir etiqueta",
"github": "¿Ha encontrado un problema? Envíelo",
"howto": "Siga nuestras guías",
"or": "o",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Oso handia",
"solid": "Solidoa",
"hachure": "Itzalduna",
"zigzag": "",
"crossHatch": "Marraduna",
"thin": "Mehea",
"bold": "Lodia",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "بسیار بزرگ",
"solid": "توپر",
"hachure": "هاشور",
"zigzag": "",
"crossHatch": "هاشور متقاطع",
"thin": "نازک",
"bold": "ضخیم",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Erittäin suuri",
"solid": "Yhtenäinen",
"hachure": "Vinoviivoitus",
"zigzag": "",
"crossHatch": "Ristiviivoitus",
"thin": "Ohut",
"bold": "Lihavoitu",
+3 -2
View File
@@ -54,6 +54,7 @@
"veryLarge": "Très grande",
"solid": "Solide",
"hachure": "Hachures",
"zigzag": "",
"crossHatch": "Hachures croisées",
"thin": "Fine",
"bold": "Épaisse",
@@ -319,8 +320,8 @@
"doubleClick": "double-clic",
"drag": "glisser",
"editor": "Éditeur",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Modifier les points de ligne/flèche",
"editText": "Modifier le texte / ajouter un libellé",
"github": "Problème trouvé ? Soumettre",
"howto": "Suivez nos guides",
"or": "ou",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Moi grande",
"solid": "Sólido",
"hachure": "Folleto",
"zigzag": "",
"crossHatch": "Raiado transversal",
"thin": "Estreito",
"bold": "Groso",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "גדול מאוד",
"solid": "מוצק",
"hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה",
"zigzag": "",
"crossHatch": "קווים מוצלבים שתי וערב",
"thin": "דק",
"bold": "מודגש",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "बहुत बड़ा",
"solid": "दृढ़",
"hachure": "हैशूर",
"zigzag": "तेढ़ी मेढ़ी",
"crossHatch": "क्रॉस हैच",
"thin": "पतला",
"bold": "मोटा",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Nagyon nagy",
"solid": "Kitöltött",
"hachure": "Vonalkázott",
"zigzag": "",
"crossHatch": "Keresztcsíkozott",
"thin": "Vékony",
"bold": "Félkövér",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Sangat besar",
"solid": "Padat",
"hachure": "Garis-garis",
"zigzag": "",
"crossHatch": "Asiran silang",
"thin": "Lembut",
"bold": "Tebal",
+2 -1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Molto grande",
"solid": "Pieno",
"hachure": "Tratteggio obliquo",
"zigzag": "Zig zag",
"crossHatch": "Tratteggio incrociato",
"thin": "Sottile",
"bold": "Grassetto",
@@ -319,7 +320,7 @@
"doubleClick": "doppio-click",
"drag": "trascina",
"editor": "Editor",
"editLineArrowPoints": "",
"editLineArrowPoints": "Modifica punti linea/freccia",
"editText": "Modifica testo / aggiungi etichetta",
"github": "Trovato un problema? Segnalalo",
"howto": "Segui le nostre guide",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "特大",
"solid": "ベタ塗り",
"hachure": "斜線",
"zigzag": "",
"crossHatch": "網掛け",
"thin": "細",
"bold": "太字",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Meqqer aṭas",
"solid": "Aččuran",
"hachure": "Azerreg",
"zigzag": "",
"crossHatch": "Azerreg anmidag",
"thin": "Arqaq",
"bold": "Azuran",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Өте үлкен",
"solid": "",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "",
"bold": "",
+6 -5
View File
@@ -54,6 +54,7 @@
"veryLarge": "매우 크게",
"solid": "단색",
"hachure": "평행선",
"zigzag": "지그재그",
"crossHatch": "교차선",
"thin": "얇게",
"bold": "굵게",
@@ -256,7 +257,7 @@
"resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.",
"resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기",
"rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.",
"lineEditor_info": "포인트를 편집하려면 Ctrl/Cmd 누르고 더블 클릭을 하거나 Ctrl/Cmd + Enter를 누르세요",
"lineEditor_info": "꼭짓점을 수정하려면 CtrlOrCmd 키를 누르고 더블 클릭을 하거나 CtrlOrCmd + Enter를 누르세요.",
"lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기",
"lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기",
"placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기",
@@ -319,8 +320,8 @@
"doubleClick": "더블 클릭",
"drag": "드래그",
"editor": "에디터",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "직선 / 화살표 꼭짓점 수정",
"editText": "텍스트 수정 / 라벨 추가",
"github": "문제 제보하기",
"howto": "가이드 참고하기",
"or": "또는",
@@ -382,8 +383,8 @@
},
"publishSuccessDialog": {
"title": "라이브러리 제출됨",
"content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을 다음의 링크에서 확인할 수 있습니다.",
"link": "여기"
"content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을",
"link": "여기에서 확인하실 수 있습니다."
},
"confirmDialog": {
"resetLibrary": "라이브러리 리셋",
+35 -34
View File
@@ -1,7 +1,7 @@
{
"labels": {
"paste": "دانانەوە",
"pasteAsPlaintext": "",
"pasteAsPlaintext": "دایبنێ وەک دەقی سادە",
"pasteCharts": "دانانەوەی خشتەکان",
"selectAll": "دیاریکردنی هەموو",
"multiSelect": "زیادکردنی بۆ دیاریکراوەکان",
@@ -54,6 +54,7 @@
"veryLarge": "زۆر گه‌وره‌",
"solid": "سادە",
"hachure": "هاچور",
"zigzag": "زیگزاگ",
"crossHatch": "کرۆس هاتچ",
"thin": "تەنک",
"bold": "تۆخ",
@@ -110,7 +111,7 @@
"increaseFontSize": "زایدکردنی قەبارەی فۆنت",
"unbindText": "دەقەکە جیابکەرەوە",
"bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە",
"createContainerFromText": "",
"createContainerFromText": "دەق لە چوارچێوەیەکدا بپێچە",
"link": {
"edit": "دەستکاریکردنی بەستەر",
"create": "دروستکردنی بەستەر",
@@ -194,7 +195,7 @@
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.",
"collabOfflineWarning": ""
"collabOfflineWarning": "هێڵی ئینتەرنێت بەردەست نییە.\n گۆڕانکارییەکانت سەیڤ ناکرێن!"
},
"errors": {
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
@@ -204,22 +205,22 @@
"invalidSVGString": "ئێس ڤی جی نادروستە.",
"cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.",
"importLibraryError": "نەیتوانی کتێبخانە بار بکات",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"collabSaveFailed": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت. ئەگەر کێشەکان بەردەوام بوون، پێویستە فایلەکەت لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.",
"collabSaveFailed_sizeExceeded": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت، پێدەچێت تابلۆکە زۆر گەورە بێت. پێویستە فایلەکە لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
"start": "پێدەچێت وێبگەڕی Brave بەکاربهێنیت لەگەڵ",
"aggressive_block_fingerprint": "بلۆککردنی Fingerprinting بەشێوەیەکی توندوتیژانە",
"setting_enabled": "ڕێکخستن چالاک کراوە",
"break": "ئەمە دەکرێت ببێتە هۆی تێکدانی",
"text_elements": "دانە دەقییەکان",
"in_your_drawings": "لە وێنەکێشانەکانتدا",
"strongly_recommend": "بە توندی پێشنیار دەکەین ئەم ڕێکخستنە لەکاربخەیت. دەتوانیت بڕۆیت بە دوای",
"steps": "ئەم هەنگاوانەدا",
"how": "بۆ ئەوەی ئەنجامی بدەیت",
"disable_setting": " ئەگەر لەکارخستنی ئەم ڕێکخستنە پیشاندانی توخمەکانی دەق چاک نەکاتەوە، تکایە هەڵبستە بە کردنەوەی",
"issue": "کێشەیەک",
"write": "لەسەر گیتهەبەکەمان، یان بۆمان بنوسە لە",
"discord": "دیسکۆرد"
}
},
"toolBar": {
@@ -237,7 +238,7 @@
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
"eraser": "سڕەر",
"hand": ""
"hand": "دەست (ئامرازی پانکردن)"
},
"headings": {
"canvasActions": "کردارەکانی تابلۆ",
@@ -245,7 +246,7 @@
"shapes": "شێوەکان"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "بۆ جوڵاندنی تابلۆ، ویلی ماوسەکەت یان دوگمەی سپەیس بگرە لەکاتی ڕاکێشاندە، یانیش ئامرازی دەستەکە بەکاربهێنە",
"linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ",
"freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە",
"text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن",
@@ -256,7 +257,7 @@
"resize": "دەتوانیت ڕێژەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی گۆڕینی قەبارەدا،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە",
"resizeImage": "دەتوانیت بە ئازادی قەبارە بگۆڕیت بە ڕاگرتنی SHIFT،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە",
"rotate": "دەتوانیت گۆشەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی سوڕانەوەدا",
"lineEditor_info": "",
"lineEditor_info": "یان Ctrl یان Cmd بگرە و دوانە کلیک بکە یانیش پەنجە بنێ بە Ctrl یان Cmd + ئینتەر بۆ دەستکاریکردنی خاڵەکان",
"lineEditor_pointSelected": "بۆ لابردنی خاڵەکان Delete دابگرە، Ctrl Cmd+D بکە بۆ لەبەرگرتنەوە، یان بۆ جووڵە ڕاکێشان بکە",
"lineEditor_nothingSelected": "خاڵێک هەڵبژێرە بۆ دەستکاریکردن (SHIFT ڕابگرە بۆ هەڵبژاردنی چەندین)،\nیان Alt ڕابگرە و کلیک بکە بۆ زیادکردنی خاڵە نوێیەکان",
"placeImage": "کلیک بکە بۆ دانانی وێنەکە، یان کلیک بکە و ڕایبکێشە بۆ ئەوەی قەبارەکەی بە دەستی دابنێیت",
@@ -264,7 +265,7 @@
"bindTextToElement": "بۆ زیادکردنی دەق enter بکە",
"deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان",
"eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە",
"firefox_clipboard_write": ""
"firefox_clipboard_write": "ئەم تایبەتمەندییە بە ئەگەرێکی زۆرەوە دەتوانرێت چالاک بکرێت بە ڕێکخستنی ئاڵای \"dom.events.asyncClipboard.clipboardItem\" بۆ \"true\". بۆ گۆڕینی ئاڵاکانی وێبگەڕ لە فایەرفۆکسدا، سەردانی لاپەڕەی \"about:config\" بکە."
},
"canvasError": {
"cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت",
@@ -319,8 +320,8 @@
"doubleClick": "دوو گرتە",
"drag": "راکێشان",
"editor": "دەستکاریکەر",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "دەستکاری خاڵەکانی هێڵ/تیر بکە",
"editText": "دەستکاری دەق بکە / لەیبڵێک زیاد بکە",
"github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن",
"howto": "شوێن ڕینماییەکانمان بکەوە",
"or": "یان",
@@ -334,8 +335,8 @@
"zoomToFit": "زووم بکە بۆ ئەوەی لەگەڵ هەموو توخمەکاندا بگونجێت",
"zoomToSelection": "زووم بکە بۆ دەستنیشانکراوەکان",
"toggleElementLock": "قفڵ/کردنەوەی دەستنیشانکراوەکان",
"movePageUpDown": "",
"movePageLeftRight": ""
"movePageUpDown": "لاپەڕەکە بجوڵێنە بۆ سەرەوە/خوارەوە",
"movePageLeftRight": "لاپەڕەکە بجوڵێنە بۆ چەپ/ڕاست"
},
"clearCanvasDialog": {
"title": "تابلۆکە خاوێن بکەرەوە"
@@ -417,7 +418,7 @@
"fileSavedToFilename": "هەڵگیراوە بۆ {filename}",
"canvas": "تابلۆ",
"selection": "دەستنیشانکراوەکان",
"pasteAsSingleElement": ""
"pasteAsSingleElement": "بۆ دانانەوە وەکو یەک توخم یان دانانەوە بۆ نێو دەسکاریکەرێکی دەق کە بوونی هەیە {{shortcut}} بەکاربهێنە"
},
"colors": {
"ffffff": "سپی",
@@ -468,15 +469,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
"center_heading": "هەموو داتاکانت لە ناوخۆی وێنگەڕەکەتدا پاشەکەوت کراوە.",
"center_heading_plus": "ویستت بڕۆیت بۆ Excalidraw+?",
"menuHint": "هەناردەکردن، ڕێکخستنەکان، زمانەکان، ..."
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
"menuHint": "هەناردەکردن، ڕێکخستنەکان، و زیاتر...",
"center_heading": "دایاگرامەکان. ئاسان. کراون.",
"toolbarHint": "ئامرازێک هەڵبگرە و دەستبکە بە کێشان!",
"helpHint": "قەدبڕەکان و یارمەتی"
}
}
}
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Labai didelis",
"solid": "",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "Plonas",
"bold": "Pastorintas",
+28 -27
View File
@@ -54,6 +54,7 @@
"veryLarge": "Ļoti liels",
"solid": "Pilns",
"hachure": "Svītrots",
"zigzag": "Zigzaglīnija",
"crossHatch": "Šķērssvītrots",
"thin": "Šaurs",
"bold": "Trekns",
@@ -110,7 +111,7 @@
"increaseFontSize": "Palielināt fonta izmēru",
"unbindText": "Atdalīt tekstu",
"bindText": "Piesaistīt tekstu figūrai",
"createContainerFromText": "",
"createContainerFromText": "Ietilpināt tekstu figurā",
"link": {
"edit": "Rediģēt saiti",
"create": "Izveidot saiti",
@@ -194,7 +195,7 @@
"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.",
"collabOfflineWarning": ""
"collabOfflineWarning": "Nav pieejams interneta pieslēgums.\nJūsu izmaiņas netiks saglabātas!"
},
"errors": {
"unsupportedFileType": "Neatbalstīts datnes veids.",
@@ -207,19 +208,19 @@
"collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
"start": "Izskatās, ka izmanto Brave interneta plārlūku ar ieslēgtu",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
"setting_enabled": "ieslēgtu iestatījumu",
"break": "Tas var salauzt",
"text_elements": "Teksta elementus",
"in_your_drawings": "tavos zīmējumos",
"strongly_recommend": "Mēs iesakām izslēgt šo iestatījumu. Tu vari sekot",
"steps": "šiem soļiem",
"how": "kā to izdarīt",
"disable_setting": " Ja šī iestatījuma izslēgšana neatrisina teksta elementu attēlošanu, tad, lūdzu, atver",
"issue": "problēmu",
"write": "mūsu GitHub vai raksti mums",
"discord": "Discord"
}
},
"toolBar": {
@@ -237,7 +238,7 @@
"penMode": "Pildspalvas režīms novērst pieskaršanos",
"link": "Pievienot/rediģēt atlasītās figūras saiti",
"eraser": "Dzēšgumija",
"hand": ""
"hand": "Roka (panoramēšanas rīks)"
},
"headings": {
"canvasActions": "Tāfeles darbības",
@@ -245,7 +246,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "Lai bīdītu tāfeli, turiet nospiestu ritināšanas vai atstarpes taustiņu, vai izmanto rokas rīku",
"linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju",
"freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat",
"text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku",
@@ -264,7 +265,7 @@
"bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu",
"deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu",
"eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases",
"firefox_clipboard_write": ""
"firefox_clipboard_write": "Šis iestatījums var tikt ieslēgts ar \"dom.events.asyncClipboard.clipboardItem\" marķieri pārslēgtu uz \"true\". Lai mainītu pārlūka marķierus Firefox, apmeklē \"about:config\" lapu."
},
"canvasError": {
"cannotShowPreview": "Nevar rādīt priekšskatījumu",
@@ -319,8 +320,8 @@
"doubleClick": "dubultklikšķis",
"drag": "vilkt",
"editor": "Redaktors",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Rediģēt līniju/bultu punktus",
"editText": "Rediģēt tekstu/pievienot birku",
"github": "Sastapāt kļūdu? Ziņot",
"howto": "Sekojiet mūsu instrukcijām",
"or": "vai",
@@ -468,15 +469,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
"center_heading": "Visi jūsu dati tiek glabāti uz vietas jūsu pārlūkā.",
"center_heading_plus": "Vai tā vietā vēlies doties uz Excalidraw+?",
"menuHint": "Eksportēšana, iestatījumi, valodas..."
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
"menuHint": "Eksportēšana, iestatījumi un vēl...",
"center_heading": "Diagrammas. Izveidotas. Vienkārši.",
"toolbarHint": "Izvēlies rīku un sāc zīmēt!",
"helpHint": "Īsceļi un palīdzība"
}
}
}
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "फार मोठं",
"solid": "भरीव",
"hachure": "हैशूर रेखांकन",
"zigzag": "वाकडी तिकड़ी",
"crossHatch": "आडव्या रेघा",
"thin": "पातळ",
"bold": "जाड",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "ပိုကြီး",
"solid": "အပြည့်",
"hachure": "မျဉ်းစောင်း",
"zigzag": "",
"crossHatch": "ဇကာကွက်",
"thin": "ပါး",
"bold": "ထူ",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Svært stor",
"solid": "Helfarge",
"hachure": "Skravert",
"zigzag": "Sikk-sakk",
"crossHatch": "Krysskravert",
"thin": "Tynn",
"bold": "Tykk",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Zeer groot",
"solid": "Ingekleurd",
"hachure": "Arcering",
"zigzag": "",
"crossHatch": "Tweemaal gearceerd",
"thin": "Dun",
"bold": "Vet",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Svært stor",
"solid": "Solid",
"hachure": "Skravert",
"zigzag": "",
"crossHatch": "Krysskravert",
"thin": "Tynn",
"bold": "Tjukk",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Gradassa",
"solid": "Solide",
"hachure": "Raia",
"zigzag": "",
"crossHatch": "Raia crosada",
"thin": "Fin",
"bold": "Espés",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "ਬਹੁਤ ਵੱਡਾ",
"solid": "ਠੋਸ",
"hachure": "ਤਿਰਛੀਆਂ ਗਰਿੱਲਾਂ",
"zigzag": "",
"crossHatch": "ਜਾਲੀ",
"thin": "ਪਤਲੀ",
"bold": "ਮੋਟੀ",
+18 -18
View File
@@ -1,17 +1,17 @@
{
"ar-SA": 89,
"ar-SA": 88,
"bg-BG": 52,
"bn-BD": 57,
"ca-ES": 96,
"cs-CZ": 72,
"ca-ES": 95,
"cs-CZ": 71,
"da-DK": 31,
"de-DE": 100,
"el-GR": 98,
"en": 100,
"es-ES": 99,
"es-ES": 100,
"eu-ES": 99,
"fa-IR": 91,
"fi-FI": 96,
"fi-FI": 95,
"fr-FR": 99,
"gl-ES": 99,
"he-IL": 99,
@@ -19,35 +19,35 @@
"hu-HU": 85,
"id-ID": 98,
"it-IT": 99,
"ja-JP": 97,
"ja-JP": 96,
"kab-KAB": 93,
"kk-KZ": 19,
"ko-KR": 99,
"ku-TR": 91,
"ko-KR": 100,
"ku-TR": 100,
"lt-LT": 61,
"lv-LV": 93,
"lv-LV": 100,
"mr-IN": 100,
"my-MM": 40,
"my-MM": 39,
"nb-NO": 100,
"nl-NL": 92,
"nn-NO": 86,
"nn-NO": 85,
"oc-FR": 94,
"pa-IN": 79,
"pl-PL": 87,
"pt-BR": 96,
"pt-PT": 99,
"pt-BR": 95,
"pt-PT": 100,
"ro-RO": 100,
"ru-RU": 96,
"ru-RU": 100,
"si-LK": 8,
"sk-SK": 99,
"sl-SI": 100,
"sv-SE": 99,
"sv-SE": 100,
"ta-IN": 90,
"th-TH": 39,
"tr-TR": 98,
"uk-UA": 93,
"tr-TR": 97,
"uk-UA": 92,
"vi-VN": 52,
"zh-CN": 99,
"zh-HK": 25,
"zh-HK": 24,
"zh-TW": 100
}
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Bardzo duży",
"solid": "Pełne",
"hachure": "Linie",
"zigzag": "",
"crossHatch": "Zakreślone",
"thin": "Cienkie",
"bold": "Pogrubione",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Muito grande",
"solid": "Sólido",
"hachure": "Hachura",
"zigzag": "",
"crossHatch": "Hachura cruzada",
"thin": "Fino",
"bold": "Espesso",
+3 -2
View File
@@ -54,6 +54,7 @@
"veryLarge": "Muito grande",
"solid": "Sólido",
"hachure": "Eclosão",
"zigzag": "ziguezague",
"crossHatch": "Sombreado",
"thin": "Fino",
"bold": "Espesso",
@@ -319,8 +320,8 @@
"doubleClick": "clique duplo",
"drag": "arrastar",
"editor": "Editor",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Editar pontos de linha/seta",
"editText": "Editar texto / adicionar etiqueta",
"github": "Encontrou algum problema? Informe-nos",
"howto": "Siga os nossos guias",
"or": "ou",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Foarte mare",
"solid": "Plină",
"hachure": "Hașură",
"zigzag": "Zigzag",
"crossHatch": "Hașură transversală",
"thin": "Subțire",
"bold": "Îngroșată",
+17 -16
View File
@@ -54,6 +54,7 @@
"veryLarge": "Очень большой",
"solid": "Однотонная",
"hachure": "Штрихованная",
"zigzag": "Зигзаг",
"crossHatch": "Перекрестная",
"thin": "Тонкая",
"bold": "Жирная",
@@ -110,7 +111,7 @@
"increaseFontSize": "Увеличить шрифт",
"unbindText": "Отвязать текст",
"bindText": "Привязать текст к контейнеру",
"createContainerFromText": "",
"createContainerFromText": "Поместить текст в контейнер",
"link": {
"edit": "Редактировать ссылку",
"create": "Создать ссылку",
@@ -207,19 +208,19 @@
"collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.",
"collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
"start": "Похоже, вы используете браузер Brave с",
"aggressive_block_fingerprint": "Агрессивно блокировать фингерпринтинг",
"setting_enabled": "параметр включен",
"break": "Это может привести к поломке",
"text_elements": "Текстовых элементов",
"in_your_drawings": "в ваших рисунках",
"strongly_recommend": "Мы настоятельно рекомендуем отключить эту настройку. Вы можете выполнить",
"steps": "эти действия",
"how": "для отключения",
"disable_setting": " Если отключение этого параметра не исправит отображение текстовых элементов, пожалуйста, откройте",
"issue": "issue",
"write": "на нашем GitHub, или напишите нам в",
"discord": "Discord"
}
},
"toolBar": {
@@ -319,8 +320,8 @@
"doubleClick": "двойной клик",
"drag": "перетащить",
"editor": "Редактор",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Редактировать концы линий/стрелок",
"editText": "Редактировать текст / добавить метку",
"github": "Нашли проблему? Отправьте",
"howto": "Следуйте нашим инструкциям",
"or": "или",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "ඉතා විශාල",
"solid": "විශාල",
"hachure": "මධ්‍යම",
"zigzag": "",
"crossHatch": "",
"thin": "කෙට්ටු",
"bold": "තද",
+3 -2
View File
@@ -54,6 +54,7 @@
"veryLarge": "Veľmi veľké",
"solid": "Plná",
"hachure": "Šrafovaná",
"zigzag": "",
"crossHatch": "Mriežkovaná",
"thin": "Tenká",
"bold": "Hrubá",
@@ -319,8 +320,8 @@
"doubleClick": "dvojklik",
"drag": "potiahnutie",
"editor": "Editovanie",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Editácia bodov čiary/šípky",
"editText": "Editácia textu / pridanie štítku",
"github": "Objavili ste problém? Nahláste ho",
"howto": "Postupujte podľa naších návodov",
"or": "alebo",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Zelo velika",
"solid": "Polno",
"hachure": "Šrafura",
"zigzag": "Cikcak",
"crossHatch": "Križno",
"thin": "Tanko",
"bold": "Krepko",
+3 -2
View File
@@ -54,6 +54,7 @@
"veryLarge": "Mycket stor",
"solid": "Solid",
"hachure": "Skraffering",
"zigzag": "Sicksack",
"crossHatch": "Skraffera med kors",
"thin": "Tunn",
"bold": "Fet",
@@ -319,8 +320,8 @@
"doubleClick": "dubbelklicka",
"drag": "dra",
"editor": "Redigerare",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "Redigera linje-/pilpunkter",
"editText": "Redigera text / lägg till etikett",
"github": "Hittat ett problem? Rapportera",
"howto": "Följ våra guider",
"or": "eller",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "மிகப் பெரிய",
"solid": "திடமான",
"hachure": "மலைக்குறிக்கோடு",
"zigzag": "",
"crossHatch": "குறுக்குகதவு",
"thin": "மெல்லிய",
"bold": "பட்டை",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "ใหญ่มาก",
"solid": "",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "บาง",
"bold": "หนา",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Çok geniş",
"solid": "Dolu",
"hachure": "Taralı",
"zigzag": "",
"crossHatch": "Çapraz-taralı",
"thin": "İnce",
"bold": "Kalın",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Дуже великий",
"solid": "Суцільна",
"hachure": "Штриховка",
"zigzag": "",
"crossHatch": "Перехресна штриховка",
"thin": "Тонкий",
"bold": "Жирний",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Rất lớn",
"solid": "Đặc",
"hachure": "Nét gạch gạch",
"zigzag": "",
"crossHatch": "Nét gạch chéo",
"thin": "Mỏng",
"bold": "In đậm",
+3 -2
View File
@@ -54,6 +54,7 @@
"veryLarge": "加大",
"solid": "实心",
"hachure": "线条",
"zigzag": "",
"crossHatch": "交叉线条",
"thin": "细",
"bold": "粗",
@@ -319,8 +320,8 @@
"doubleClick": "双击",
"drag": "拖动",
"editor": "编辑器",
"editLineArrowPoints": "",
"editText": "",
"editLineArrowPoints": "编辑线条或箭头的点",
"editText": "添加或编辑文本",
"github": "提交问题",
"howto": "帮助文档",
"or": "或",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "勁大",
"solid": "實心",
"hachure": "斜線",
"zigzag": "",
"crossHatch": "交叉格仔",
"thin": "幼",
"bold": "粗",
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "特大",
"solid": "實心",
"hachure": "斜線筆觸",
"zigzag": "Z字形",
"crossHatch": "交叉筆觸",
"thin": "細",
"bold": "粗",
+181 -1
View File
@@ -11,7 +11,39 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section.
-->
## Unreleased
## 0.15.2 (2023-04-20)
### Docs
- Fix docs link in readme [#6486](https://github.com/excalidraw/excalidraw/pull/6486)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Fixes
- Rotate the text element when binding to a rotated container [#6477](https://github.com/excalidraw/excalidraw/pull/6477)
- Support breaking words containing hyphen - [#6014](https://github.com/excalidraw/excalidraw/pull/6014)
- Incorrect background fill button active state [#6491](https://github.com/excalidraw/excalidraw/pull/6491)
---
## 0.15.1 (2023-04-18)
### Docs
- Add the readme back to the package which was mistakenly removed [#6484](https://github.com/excalidraw/excalidraw/pull/6484)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
---
## 0.15.0 (2023-04-18)
### Features
@@ -37,6 +69,154 @@ For more details refer to the [docs](https://docs.excalidraw.com)
- Exporting labelled arrows via export utils [#6443](https://github.com/excalidraw/excalidraw/pull/6443)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Features
- Constrain export dialog preview size [#6475](https://github.com/excalidraw/excalidraw/pull/6475)
- Zigzag fill easter egg [#6439](https://github.com/excalidraw/excalidraw/pull/6439)
- Add container to multiple text elements [#6428](https://github.com/excalidraw/excalidraw/pull/6428)
- Starting migration from GA to Matomo for better privacy [#6398](https://github.com/excalidraw/excalidraw/pull/6398)
- Add line height attribute to text element [#6360](https://github.com/excalidraw/excalidraw/pull/6360)
- Add thai lang support [#6314](https://github.com/excalidraw/excalidraw/pull/6314)
- Create bound container from text [#6301](https://github.com/excalidraw/excalidraw/pull/6301)
- Improve text measurements in bound containers [#6187](https://github.com/excalidraw/excalidraw/pull/6187)
- Bind text to container if double clicked on filled shape or stroke [#6250](https://github.com/excalidraw/excalidraw/pull/6250)
- Make repair and refreshDimensions configurable in restoreElements [#6238](https://github.com/excalidraw/excalidraw/pull/6238)
- Show error message when not connected to internet while collabo… [#6165](https://github.com/excalidraw/excalidraw/pull/6165)
- Shortcut for clearCanvas confirmDialog [#6114](https://github.com/excalidraw/excalidraw/pull/6114)
- Disable canvas smoothing (antialiasing) for right-angled elements [#6186](https://github.com/excalidraw/excalidraw/pull/6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
### Fixes
- Center align text when wrapped in container via context menu [#6480](https://github.com/excalidraw/excalidraw/pull/6480)
- Restore original container height when unbinding text which was binded via context menu [#6444](https://github.com/excalidraw/excalidraw/pull/6444)
- Mark more props as optional for element [#6448](https://github.com/excalidraw/excalidraw/pull/6448)
- Improperly cache-busting on canvas scale instead of zoom [#6473](https://github.com/excalidraw/excalidraw/pull/6473)
- Incorrectly duplicating items on paste/library insert [#6467](https://github.com/excalidraw/excalidraw/pull/6467)
- Library ids cross-contamination on multiple insert [#6466](https://github.com/excalidraw/excalidraw/pull/6466)
- Color picker keyboard handling not working [#6464](https://github.com/excalidraw/excalidraw/pull/6464)
- Abort freedraw line if second touch is detected [#6440](https://github.com/excalidraw/excalidraw/pull/6440)
- Utils leaking Scene state [#6461](https://github.com/excalidraw/excalidraw/pull/6461)
- Split "Edit selected shape" shortcut [#6457](https://github.com/excalidraw/excalidraw/pull/6457)
- Center align text when bind to container via context menu [#6451](https://github.com/excalidraw/excalidraw/pull/6451)
- Update coords when text unbinded from its container [#6445](https://github.com/excalidraw/excalidraw/pull/6445)
- Autoredirect to plus in prod only [#6446](https://github.com/excalidraw/excalidraw/pull/6446)
- Fixing popover overflow on small screen [#6433](https://github.com/excalidraw/excalidraw/pull/6433)
- Introduce baseline to fix the layout shift when switching to text editor [#6397](https://github.com/excalidraw/excalidraw/pull/6397)
- Don't refresh dimensions for deleted text elements [#6438](https://github.com/excalidraw/excalidraw/pull/6438)
- Element vanishes when zoomed in [#6417](https://github.com/excalidraw/excalidraw/pull/6417)
- Don't jump text to end when out of viewport in safari [#6416](https://github.com/excalidraw/excalidraw/pull/6416)
- GetDefaultLineHeight should return default font family line height for unknown font [#6399](https://github.com/excalidraw/excalidraw/pull/6399)
- Revert use `ideographic` textBaseline to improve layout shift when editing text" [#6400](https://github.com/excalidraw/excalidraw/pull/6400)
- Call stack size exceeded when paste large text [#6373](https://github.com/excalidraw/excalidraw/pull/6373) (#6396)
- Use `ideographic` textBaseline to improve layout shift when editing text [#6384](https://github.com/excalidraw/excalidraw/pull/6384)
- Chrome crashing when embedding scene on chrome arm [#6383](https://github.com/excalidraw/excalidraw/pull/6383)
- Division by zero in findFocusPointForEllipse leads to infinite loop in wrapText freezing Excalidraw [#6377](https://github.com/excalidraw/excalidraw/pull/6377)
- Containerizing text incorrectly updates arrow bindings [#6369](https://github.com/excalidraw/excalidraw/pull/6369)
- Ensure export preview is centered [#6337](https://github.com/excalidraw/excalidraw/pull/6337)
- Hide text align for labelled arrows [#6339](https://github.com/excalidraw/excalidraw/pull/6339)
- Refresh dimensions when elements loaded from shareable link and blob [#6333](https://github.com/excalidraw/excalidraw/pull/6333)
- Show error message when measureText API breaks in brave [#6336](https://github.com/excalidraw/excalidraw/pull/6336)
- Add an offset of 0.5px for text editor in containers [#6328](https://github.com/excalidraw/excalidraw/pull/6328)
- Move utility types out of `.d.ts` file to fix exported declaration files [#6315](https://github.com/excalidraw/excalidraw/pull/6315)
- More jotai scopes missing [#6313](https://github.com/excalidraw/excalidraw/pull/6313)
- Provide HelpButton title prop [#6209](https://github.com/excalidraw/excalidraw/pull/6209)
- Respect text align when wrapping in a container [#6310](https://github.com/excalidraw/excalidraw/pull/6310)
- Compute bounding box correctly for text element when multiple element resizing [#6307](https://github.com/excalidraw/excalidraw/pull/6307)
- Use jotai scope for editor-specific atoms [#6308](https://github.com/excalidraw/excalidraw/pull/6308)
- Consider arrow for bound text element [#6297](https://github.com/excalidraw/excalidraw/pull/6297)
- Text never goes beyond max width for unbound text elements [#6288](https://github.com/excalidraw/excalidraw/pull/6288)
- Svg text baseline [#6285](https://github.com/excalidraw/excalidraw/pull/6273)
- Compute container height from bound text correctly [#6273](https://github.com/excalidraw/excalidraw/pull/6273)
- Fit mobile toolbar and make scrollable [#6270](https://github.com/excalidraw/excalidraw/pull/6270)
- Indenting via `tab` clashing with IME compositor [#6258](https://github.com/excalidraw/excalidraw/pull/6258)
- Improve text wrapping inside rhombus and more fixes [#6265](https://github.com/excalidraw/excalidraw/pull/6265)
- Improve text wrapping in ellipse and alignment [#6172](https://github.com/excalidraw/excalidraw/pull/6172)
- Don't allow blank space in collab name [#6211](https://github.com/excalidraw/excalidraw/pull/6211)
- Docker build architecture:linux/amd64 error occur on linux/arm64 instance [#6197](https://github.com/excalidraw/excalidraw/pull/6197)
- Sort bound text elements to fix text duplication z-index error [#5130](https://github.com/excalidraw/excalidraw/pull/5130)
- Hide welcome screen on mobile once user interacts [#6185](https://github.com/excalidraw/excalidraw/pull/6185)
- Edit link in docs [#6182](https://github.com/excalidraw/excalidraw/pull/6182)
### Refactor
- Inline `SingleLibraryItem` into `PublishLibrary` [#6462](https://github.com/excalidraw/excalidraw/pull/6462)
- Make the example React app reusable without duplication [#6188](https://github.com/excalidraw/excalidraw/pull/6188)
### Performance
- Break early if the line width <= max width of the container [#6347](https://github.com/excalidraw/excalidraw/pull/6347)
### Build
- Move TS and types to devDependencies [#6346](https://github.com/excalidraw/excalidraw/pull/6346)
---
## 0.14.2 (2023-02-01)
### Features
+2 -2
View File
@@ -38,8 +38,8 @@ Excalidraw takes _100%_ of `width` and `height` of the containing block so make
## Integration
Head over to the [docs](https://docs.excalidraw.com/docs/package/integration)
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration)
## API
Head over to the [docs](https://docs.excalidraw.com/docs/package/api)
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@excalidraw/excalidraw",
"version": "0.14.2",
"version": "0.15.2",
"main": "main.js",
"types": "types/packages/excalidraw/index.d.ts",
"files": [
+6 -10
View File
@@ -79,7 +79,11 @@ export const exportToCanvas = ({
const max = Math.max(width, height);
const scale = maxWidthOrHeight / max;
// if content is less then maxWidthOrHeight, fallback on supplied scale
const scale =
maxWidthOrHeight < max
? maxWidthOrHeight / max
: appState?.exportScale ?? 1;
canvas.width = width * scale;
canvas.height = height * scale;
@@ -216,15 +220,7 @@ export const exportToClipboard = async (
} else if (opts.type === "png") {
await copyBlobToClipboardAsPng(exportToBlob(opts));
} else if (opts.type === "json") {
const appState = {
offsetTop: 0,
offsetLeft: 0,
width: 0,
height: 0,
...getDefaultAppState(),
...opts.appState,
};
await copyToClipboard(opts.elements, appState, opts.files);
await copyToClipboard(opts.elements, opts.files);
} else {
throw new Error("Invalid export type");
}
+9 -5
View File
@@ -44,8 +44,8 @@ import {
getContainerCoords,
getContainerElement,
getLineHeightInPx,
getMaxContainerHeight,
getMaxContainerWidth,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -864,17 +864,21 @@ const drawElementFromCanvas = (
);
if (
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
"true" &&
hasBoundTextElement(element)
) {
const textElement = getBoundTextElement(
element,
) as ExcalidrawTextElementWithContainer;
const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a";
context.lineWidth = 3;
context.strokeRect(
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
getMaxContainerWidth(element) * window.devicePixelRatio,
getMaxContainerHeight(element) * window.devicePixelRatio,
getBoundTextMaxWidth(element) * window.devicePixelRatio,
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
);
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { isTextElement, refreshTextDimensions } from "../element";
import { newElementWith } from "../element/mutateElement";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { getFontString } from "../utils";
@@ -52,7 +53,7 @@ export class Fonts {
let didUpdate = false;
this.scene.mapElements((element) => {
if (isTextElement(element)) {
if (isTextElement(element) && !isBoundToContainer(element)) {
invalidateShapeForElement(element);
didUpdate = true;
return newElementWith(element, {
@@ -121,7 +121,7 @@ Object {
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"name": "wrapTextInContainer",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
@@ -4518,7 +4518,7 @@ Object {
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"name": "wrapTextInContainer",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
@@ -5068,7 +5068,7 @@ Object {
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"name": "wrapTextInContainer",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
@@ -5917,7 +5917,7 @@ Object {
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"name": "wrapTextInContainer",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
@@ -6263,7 +6263,7 @@ Object {
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"name": "wrapTextInContainer",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
+2 -2
View File
@@ -4,7 +4,7 @@ import { UI, Pointer, Keyboard } from "./helpers/ui";
import { getTransformHandles } from "../element/transformHandles";
import { API } from "./helpers/api";
import { KEYS } from "../keys";
import { actionCreateContainerFromText } from "../actions/actionBoundText";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
const { h } = window;
@@ -277,7 +277,7 @@ describe("element binding", () => {
expect(h.state.selectedElementIds[text1.id]).toBe(true);
h.app.actionManager.executeAction(actionCreateContainerFromText);
h.app.actionManager.executeAction(actionWrapTextInContainer);
// new text container will be placed before the text element
const container = h.elements.at(-2)!;
+42 -19
View File
@@ -1,5 +1,10 @@
import ReactDOM from "react-dom";
import { render, waitFor, GlobalTestState } from "./test-utils";
import {
render,
waitFor,
GlobalTestState,
createPasteEvent,
} from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys";
@@ -9,6 +14,8 @@ import {
} from "../element/textElement";
import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api";
import { copyToClipboard } from "../clipboard";
const { h } = window;
@@ -35,38 +42,28 @@ const setClipboardText = (text: string) => {
});
};
const sendPasteEvent = () => {
const clipboardEvent = new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [],
};
const sendPasteEvent = (text?: string) => {
const clipboardEvent = createPasteEvent(
text || (() => window.navigator.clipboard.readText()),
);
document.dispatchEvent(clipboardEvent);
};
const pasteWithCtrlCmdShiftV = () => {
const pasteWithCtrlCmdShiftV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent();
sendPasteEvent(text);
});
};
const pasteWithCtrlCmdV = () => {
const pasteWithCtrlCmdV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent();
sendPasteEvent(text);
});
};
@@ -89,6 +86,32 @@ beforeEach(async () => {
});
});
describe("general paste behavior", () => {
it("should randomize seed on paste", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].seed).not.toBe(rectangle.seed);
});
});
it("should retain seed on shift-paste", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
// assert we don't randomize seed on shift-paste
pasteWithCtrlCmdShiftV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].seed).toBe(rectangle.seed);
});
});
});
describe("paste text as single lines", () => {
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
+7 -14
View File
@@ -1,5 +1,10 @@
import ReactDOM from "react-dom";
import { GlobalTestState, render, waitFor } from "./test-utils";
import {
createPasteEvent,
GlobalTestState,
render,
waitFor,
} from "./test-utils";
import { UI, Pointer } from "./helpers/ui";
import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
@@ -693,19 +698,7 @@ describe("freedraw", () => {
describe("image", () => {
const createImage = async () => {
const sendPasteEvent = (file?: File) => {
const clipboardEvent = new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [file],
};
const clipboardEvent = createPasteEvent("", file ? [file] : []);
document.dispatchEvent(clipboardEvent);
};
+4 -4
View File
@@ -20,7 +20,7 @@ import { resize, rotate } from "./utils";
import {
getBoundTextElementPosition,
wrapText,
getMaxContainerWidth,
getBoundTextMaxWidth,
} from "../element/textElement";
import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
@@ -729,7 +729,7 @@ describe("Test Linear Elements", () => {
type: "text",
x: 0,
y: 0,
text: wrapText(text, font, getMaxContainerWidth(container)),
text: wrapText(text, font, getBoundTextMaxWidth(container)),
containerId: container.id,
width: 30,
height: 20,
@@ -1149,7 +1149,7 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(400);
expect(rect.y).toBe(0);
expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard collaboration
made easy"
@@ -1172,7 +1172,7 @@ describe("Test Linear Elements", () => {
false,
);
expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
+21
View File
@@ -190,3 +190,24 @@ export const toggleMenu = (container: HTMLElement) => {
// open menu
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
};
export const createPasteEvent = (
text:
| string
| /* getData function */ ((type: string) => string | Promise<string>),
files?: File[],
) => {
return Object.assign(
new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
}),
{
clipboardData: {
getData: typeof text === "string" ? () => text : text,
files: files || [],
},
},
);
};
+4 -4
View File
@@ -29,9 +29,9 @@ import { isOverScrollBars } from "./scene";
import { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef } from "./utility-types";
import { Merge, ForwardRef, ValueOf } from "./utility-types";
import React from "react";
export type Point = Readonly<RoughPoint>;
@@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" };
export type BinaryFileData = {
mimeType:
| typeof ALLOWED_IMAGE_MIME_TYPES[number]
| ValueOf<typeof IMAGE_MIME_TYPES>
// future user or unknown file type
| typeof MIME_TYPES.binary;
id: FileId;
@@ -419,7 +419,7 @@ export type AppClassProperties = {
FileId,
{
image: HTMLImageElement | Promise<HTMLImageElement>;
mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number];
mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
}
>;
files: BinaryFiles;