diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx index 198626eeca..665a1ef9f2 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx @@ -31,10 +31,29 @@ You can pass `null` / `undefined` if not applicable. restoreElements( elements: ImportedDataState["elements"],
  localElements: ExcalidrawElement[] | null | undefined): ExcalidrawElement[],
  - refreshDimensions?: boolean
+ opts: { refreshDimensions?: boolean, repairBindings?: boolean }
) +| Prop | Type | Description | +| ---- | ---- | ---- | +| `elements` | ImportedDataState["elements"] | The `elements` to be restored | +| [`localElements`](#localelements) | ExcalidrawElement[] | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. | +| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements + +#### localElements + +When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. +Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update + +#### opts +The extra optional parameter to configure restored elements. It has the following attributes + +| Prop | Type | Description| +| --- | --- | ------| +| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. | +| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. | + **_How to use_** ```js @@ -43,9 +62,6 @@ import { restoreElements } from "@excalidraw/excalidraw"; This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value. -When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. -Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates. - Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. ### restore @@ -56,7 +72,9 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex restore( data: ImportedDataState,
  localAppState: Partial<AppState> | null | undefined,
  - localElements: ExcalidrawElement[] | null | undefined
): DataState + localElements: ExcalidrawElement[] | null | undefined
): DataState
+ opts: { refreshDimensions?: boolean, repairBindings?: boolean }
+ ) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md index c215923821..4d2745c090 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md @@ -339,3 +339,47 @@ The `device` has the following `attributes` | `isMobile` | `boolean` | Set to `true` when the device is `mobile` | | `isTouchScreen` | `boolean` | Set to `true` for `touch` devices | | `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` | + +### i18n + +To help with localization, we export the following. + +| name | type | +| --- | --- | +| `defaultLang` | `string` | +| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | +| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | + +```js +import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw"; +``` + +#### defaultLang + +Default language code, `en`. + +#### languages + +List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode). + +#### useI18n + +A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of ``. + +```jsx live +function App() { + const { t } = useI18n(); + return ( +
+ + + +
+ ); +} +``` diff --git a/dev-docs/package.json b/dev-docs/package.json index dd3c45872e..0aee8e01f4 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -18,7 +18,7 @@ "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0", - "@excalidraw/excalidraw": "0.14.2", + "@excalidraw/excalidraw": "0.15.2", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "docusaurus-plugin-sass": "0.2.3", diff --git a/dev-docs/src/theme/ReactLiveScope/index.js b/dev-docs/src/theme/ReactLiveScope/index.js index a282ad6f0b..e5263e1dbc 100644 --- a/dev-docs/src/theme/ReactLiveScope/index.js +++ b/dev-docs/src/theme/ReactLiveScope/index.js @@ -24,6 +24,7 @@ const ExcalidrawScope = { Sidebar: ExcalidrawComp.Sidebar, exportToCanvas: ExcalidrawComp.exportToCanvas, initialData, + useI18n: ExcalidrawComp.useI18n, }; export default ExcalidrawScope; diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 041f39b559..6206a60e9b 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1631,10 +1631,10 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@excalidraw/excalidraw@0.14.2": - version "0.14.2" - resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375" - integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg== +"@excalidraw/excalidraw@0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c" + integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw== "@hapi/hoek@^9.0.0": version "9.3.0" @@ -7159,9 +7159,9 @@ typescript@^4.7.4: integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== ua-parser-js@^0.7.30: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unescape@^1.0.1: version "1.0.1" diff --git a/public/index.html b/public/index.html index a8633fc4df..f65e481f33 100644 --- a/public/index.html +++ b/public/index.html @@ -150,6 +150,14 @@ <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %> + + + + <% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> - <% } %> - - <% } %> @@ -244,5 +227,17 @@

Excalidraw

+ + + diff --git a/scripts/release.js b/scripts/release.js index 986eadc2a3..24ac89c6be 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -1,22 +1,9 @@ -const fs = require("fs"); const { execSync } = require("child_process"); const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawPackage = `${excalidrawDir}/package.json`; const pkg = require(excalidrawPackage); -const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8"); - -const updateReadme = () => { - const excalidrawIndex = originalReadMe.indexOf("### Excalidraw"); - - // remove note for stable readme - const data = originalReadMe.slice(excalidrawIndex); - - // update readme - fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8"); -}; - const publish = () => { try { execSync(`yarn --frozen-lockfile`); @@ -30,15 +17,8 @@ const publish = () => { }; const release = () => { - updateReadme(); - console.info("Note for stable readme removed"); - publish(); console.info(`Published ${pkg.version}!`); - - // revert readme after release - fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8"); - console.info("Readme reverted"); }; release(); diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 3f240b5d82..658bdf8ce7 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -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, ); diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 661f65f38f..18fefafd22 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -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, diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index ed714816b6..382e964b9c 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -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 (
@@ -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, }, diff --git a/src/actions/types.ts b/src/actions/types.ts index e46cd2ab80..b03e1053b2 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -115,7 +115,7 @@ export type ActionName = | "toggleLinearEditor" | "toggleEraserTool" | "toggleHandTool" - | "createContainerFromText"; + | "wrapTextInContainer"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/analytics.ts b/src/analytics.ts index 1e9a429b62..e952bc6807 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -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); diff --git a/src/appState.ts b/src/appState.ts index f02d5943c7..6f4db75572 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -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, diff --git a/src/charts.ts b/src/charts.ts index e8980db6c8..c3b0950d1f 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -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; diff --git a/src/clipboard.ts b/src/clipboard.ts index 5f7950c53e..c0f5844dd2 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -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); diff --git a/src/components/App.tsx b/src/components/App.tsx index be5561995a..d22a0507cb 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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 { 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 { 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 { y: element.y + gridY - minY, }); }), + { + randomizeSeed: !opts.retainSeed, + }, ); const nextElements = [ @@ -2732,7 +2738,6 @@ class App extends React.Component { 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 { : 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 { 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 { 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 { actionGroup, actionUnbindText, actionBindText, - actionCreateContainerFromText, + actionWrapTextInContainer, actionUngroup, CONTEXT_MENU_SEPARATOR, actionAddToLibrary, diff --git a/src/components/ColorPicker.scss b/src/components/ColorPicker.scss index 52ea20a195..b816b25536 100644 --- a/src/components/ColorPicker.scss +++ b/src/components/ColorPicker.scss @@ -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; diff --git a/src/components/ContextMenu.scss b/src/components/ContextMenu.scss index 5797631196..81ced38807 100644 --- a/src/components/ContextMenu.scss +++ b/src/components/ContextMenu.scss @@ -30,6 +30,7 @@ background-color: transparent; border: none; white-space: nowrap; + font-family: inherit; display: grid; grid-template-columns: 1fr 0.2fr; diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index fb2c1ec810..0e4eff3657 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -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(null); - const { exportBackground, viewBackgroundColor } = appState; const [renderError, setRenderError] = useState(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 (
diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index 7ae6517a8f..19bb33308e 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -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 }), }; }); }; diff --git a/src/components/Tooltip.scss b/src/components/Tooltip.scss index bb2b2f72e6..490e255780 100644 --- a/src/components/Tooltip.scss +++ b/src/components/Tooltip.scss @@ -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; diff --git a/src/constants.ts b/src/constants.ts index ef563e4a41..19b41b6880 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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, +}; diff --git a/src/css/styles.scss b/src/css/styles.scss index 8dafbfbdfa..29e52011e7 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -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); diff --git a/src/data/blob.ts b/src/data/blob.ts index 47cff293fa..c0aa66ee73 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -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 } => { 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)) { diff --git a/src/data/filesystem.ts b/src/data/filesystem.ts index ffe088fafc..fa29604f45 100644 --- a/src/data/filesystem.ts +++ b/src/data/filesystem.ts @@ -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; const INPUT_CHANGE_INTERVAL_MS = 500; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index e3b25e848d..4922a5b4ee 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -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 = ( @@ -58,13 +74,13 @@ const _newElementBase = ( { 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 = ( roundness = null, boundElements = null, link = null, - locked, + locked = DEFAULT_ELEMENT_PROPS.locked, ...rest }: ElementConstructorOpts & Omit, "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 => { - 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("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 = ( * 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)) { diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index ee30eae5ba..f7cffdfd0a 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -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; diff --git a/src/element/textElement.test.ts b/src/element/textElement.test.ts index 106ed7beab..b6221336d1 100644 --- a/src/element/textElement.test.ts +++ b/src/element/textElement.test.ts @@ -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, + ); }); }); }); diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 38da5df5a4..a6d0c3271c 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -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; } diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 4ae3f26f97..f3c75db8bb 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -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, }), ); diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index ef4f7c926e..63bc9e4a44 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -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, diff --git a/src/excalidraw-app/collab/reconciliation.ts b/src/excalidraw-app/collab/reconciliation.ts index 76b6f052a3..3f50bc3587 100644 --- a/src/excalidraw-app/collab/reconciliation.ts +++ b/src/excalidraw-app/collab/reconciliation.ts @@ -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) { diff --git a/src/excalidraw-app/data/index.ts b/src/excalidraw-app/data/index.ts index 7f13bc6153..2e50abf1f9 100644 --- a/src/excalidraw-app/data/index.ts +++ b/src/excalidraw-app/data/index.ts @@ -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, { diff --git a/src/global.d.ts b/src/global.d.ts index 73c8fc8139..3a666e11ae 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -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 { diff --git a/src/locales/ar-SA.json b/src/locales/ar-SA.json index 25a32f2228..77387b11d6 100644 --- a/src/locales/ar-SA.json +++ b/src/locales/ar-SA.json @@ -54,6 +54,7 @@ "veryLarge": "كبير جدا", "solid": "كامل", "hachure": "خطوط", + "zigzag": "", "crossHatch": "خطوط متقطعة", "thin": "نحيف", "bold": "داكن", diff --git a/src/locales/bg-BG.json b/src/locales/bg-BG.json index 501ce7399d..d5421ccef9 100644 --- a/src/locales/bg-BG.json +++ b/src/locales/bg-BG.json @@ -54,6 +54,7 @@ "veryLarge": "Много голям", "solid": "Солиден", "hachure": "Хералдика", + "zigzag": "", "crossHatch": "Двойно-пресечено", "thin": "Тънък", "bold": "Ясно очертан", diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index a5d9dec0f9..ce17d6670f 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -54,6 +54,7 @@ "veryLarge": "অনেক বড়", "solid": "দৃঢ়", "hachure": "ভ্রুলেখা", + "zigzag": "", "crossHatch": "ক্রস হ্যাচ", "thin": "পাতলা", "bold": "পুরু", diff --git a/src/locales/ca-ES.json b/src/locales/ca-ES.json index ae45e764de..425070e49e 100644 --- a/src/locales/ca-ES.json +++ b/src/locales/ca-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Molt gran", "solid": "Sòlid", "hachure": "Ratlletes", + "zigzag": "", "crossHatch": "Ratlletes creuades", "thin": "Fi", "bold": "Negreta", diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index d57a8837d7..d039a78a23 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -54,6 +54,7 @@ "veryLarge": "Velmi velké", "solid": "Plný", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "Tenký", "bold": "Tlustý", diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json index c8b5ad6e32..4d74ab80f7 100644 --- a/src/locales/da-DK.json +++ b/src/locales/da-DK.json @@ -54,6 +54,7 @@ "veryLarge": "Meget stor", "solid": "Solid", "hachure": "Skravering", + "zigzag": "", "crossHatch": "Krydsskravering", "thin": "Tynd", "bold": "Fed", diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index bdf30a371d..86b168ae51 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -54,6 +54,7 @@ "veryLarge": "Sehr groß", "solid": "Deckend", "hachure": "Schraffiert", + "zigzag": "Zickzack", "crossHatch": "Kreuzschraffiert", "thin": "Dünn", "bold": "Fett", diff --git a/src/locales/el-GR.json b/src/locales/el-GR.json index 888c395683..f4e0cfcaa2 100644 --- a/src/locales/el-GR.json +++ b/src/locales/el-GR.json @@ -54,6 +54,7 @@ "veryLarge": "Πολύ μεγάλο", "solid": "Συμπαγής", "hachure": "Εκκόλαψη", + "zigzag": "", "crossHatch": "Διασταυρούμενη εκκόλαψη", "thin": "Λεπτή", "bold": "Έντονη", diff --git a/src/locales/en.json b/src/locales/en.json index 8752d415aa..7e250a800e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -54,6 +54,7 @@ "veryLarge": "Very large", "solid": "Solid", "hachure": "Hachure", + "zigzag": "Zigzag", "crossHatch": "Cross-hatch", "thin": "Thin", "bold": "Bold", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index 67a1102935..c345c5f831 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -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", diff --git a/src/locales/eu-ES.json b/src/locales/eu-ES.json index 1aec330cb3..9c5f14facf 100644 --- a/src/locales/eu-ES.json +++ b/src/locales/eu-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Oso handia", "solid": "Solidoa", "hachure": "Itzalduna", + "zigzag": "", "crossHatch": "Marraduna", "thin": "Mehea", "bold": "Lodia", diff --git a/src/locales/fa-IR.json b/src/locales/fa-IR.json index 44cf7ae00a..a22ad86a95 100644 --- a/src/locales/fa-IR.json +++ b/src/locales/fa-IR.json @@ -54,6 +54,7 @@ "veryLarge": "بسیار بزرگ", "solid": "توپر", "hachure": "هاشور", + "zigzag": "", "crossHatch": "هاشور متقاطع", "thin": "نازک", "bold": "ضخیم", diff --git a/src/locales/fi-FI.json b/src/locales/fi-FI.json index e0701f2d23..403bf00733 100644 --- a/src/locales/fi-FI.json +++ b/src/locales/fi-FI.json @@ -54,6 +54,7 @@ "veryLarge": "Erittäin suuri", "solid": "Yhtenäinen", "hachure": "Vinoviivoitus", + "zigzag": "", "crossHatch": "Ristiviivoitus", "thin": "Ohut", "bold": "Lihavoitu", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 49135c3b62..406a11a166 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -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", diff --git a/src/locales/gl-ES.json b/src/locales/gl-ES.json index 5571f3f15a..53ae05d6b7 100644 --- a/src/locales/gl-ES.json +++ b/src/locales/gl-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Moi grande", "solid": "Sólido", "hachure": "Folleto", + "zigzag": "", "crossHatch": "Raiado transversal", "thin": "Estreito", "bold": "Groso", diff --git a/src/locales/he-IL.json b/src/locales/he-IL.json index 810fc1776c..4cd8c11404 100644 --- a/src/locales/he-IL.json +++ b/src/locales/he-IL.json @@ -54,6 +54,7 @@ "veryLarge": "גדול מאוד", "solid": "מוצק", "hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה", + "zigzag": "", "crossHatch": "קווים מוצלבים שתי וערב", "thin": "דק", "bold": "מודגש", diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index 77d6dae2dc..d9462e78b4 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -54,6 +54,7 @@ "veryLarge": "बहुत बड़ा", "solid": "दृढ़", "hachure": "हैशूर", + "zigzag": "तेढ़ी मेढ़ी", "crossHatch": "क्रॉस हैच", "thin": "पतला", "bold": "मोटा", diff --git a/src/locales/hu-HU.json b/src/locales/hu-HU.json index d514520ed1..5dc19945b4 100644 --- a/src/locales/hu-HU.json +++ b/src/locales/hu-HU.json @@ -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", diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json index 01b510fcd6..eb5d8df71e 100644 --- a/src/locales/id-ID.json +++ b/src/locales/id-ID.json @@ -54,6 +54,7 @@ "veryLarge": "Sangat besar", "solid": "Padat", "hachure": "Garis-garis", + "zigzag": "", "crossHatch": "Asiran silang", "thin": "Lembut", "bold": "Tebal", diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json index 8380fd8e5d..c31462ce2a 100644 --- a/src/locales/it-IT.json +++ b/src/locales/it-IT.json @@ -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", diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index 53333aea30..a457b1dfec 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -54,6 +54,7 @@ "veryLarge": "特大", "solid": "ベタ塗り", "hachure": "斜線", + "zigzag": "", "crossHatch": "網掛け", "thin": "細", "bold": "太字", diff --git a/src/locales/kab-KAB.json b/src/locales/kab-KAB.json index ba6a3de7e9..62c6071c45 100644 --- a/src/locales/kab-KAB.json +++ b/src/locales/kab-KAB.json @@ -54,6 +54,7 @@ "veryLarge": "Meqqer aṭas", "solid": "Aččuran", "hachure": "Azerreg", + "zigzag": "", "crossHatch": "Azerreg anmidag", "thin": "Arqaq", "bold": "Azuran", diff --git a/src/locales/kk-KZ.json b/src/locales/kk-KZ.json index 97a9063fa4..38acace0e2 100644 --- a/src/locales/kk-KZ.json +++ b/src/locales/kk-KZ.json @@ -54,6 +54,7 @@ "veryLarge": "Өте үлкен", "solid": "", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "", "bold": "", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index f170e4cbf8..aa4647f287 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -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": "라이브러리 리셋", diff --git a/src/locales/ku-TR.json b/src/locales/ku-TR.json index 76b5086e8f..4fbf60492f 100644 --- a/src/locales/ku-TR.json +++ b/src/locales/ku-TR.json @@ -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": "قەدبڕەکان و یارمەتی" } } } diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index d80739f093..1c18c2729e 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -54,6 +54,7 @@ "veryLarge": "Labai didelis", "solid": "", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "Plonas", "bold": "Pastorintas", diff --git a/src/locales/lv-LV.json b/src/locales/lv-LV.json index 4a311f7cd5..bc8a3a6781 100644 --- a/src/locales/lv-LV.json +++ b/src/locales/lv-LV.json @@ -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" } } } diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index 4f58009c95..346ae5603f 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -54,6 +54,7 @@ "veryLarge": "फार मोठं", "solid": "भरीव", "hachure": "हैशूर रेखांकन", + "zigzag": "वाकडी तिकड़ी", "crossHatch": "आडव्या रेघा", "thin": "पातळ", "bold": "जाड", diff --git a/src/locales/my-MM.json b/src/locales/my-MM.json index efc874218c..437e9d13b0 100644 --- a/src/locales/my-MM.json +++ b/src/locales/my-MM.json @@ -54,6 +54,7 @@ "veryLarge": "ပိုကြီး", "solid": "အပြည့်", "hachure": "မျဉ်းစောင်း", + "zigzag": "", "crossHatch": "ဇကာကွက်", "thin": "ပါး", "bold": "ထူ", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index 27e717d2af..7a2e72c55d 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -54,6 +54,7 @@ "veryLarge": "Svært stor", "solid": "Helfarge", "hachure": "Skravert", + "zigzag": "Sikk-sakk", "crossHatch": "Krysskravert", "thin": "Tynn", "bold": "Tykk", diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json index 7c2bb105b1..62baedf839 100644 --- a/src/locales/nl-NL.json +++ b/src/locales/nl-NL.json @@ -54,6 +54,7 @@ "veryLarge": "Zeer groot", "solid": "Ingekleurd", "hachure": "Arcering", + "zigzag": "", "crossHatch": "Tweemaal gearceerd", "thin": "Dun", "bold": "Vet", diff --git a/src/locales/nn-NO.json b/src/locales/nn-NO.json index 1733b84c48..49333b910e 100644 --- a/src/locales/nn-NO.json +++ b/src/locales/nn-NO.json @@ -54,6 +54,7 @@ "veryLarge": "Svært stor", "solid": "Solid", "hachure": "Skravert", + "zigzag": "", "crossHatch": "Krysskravert", "thin": "Tynn", "bold": "Tjukk", diff --git a/src/locales/oc-FR.json b/src/locales/oc-FR.json index a9ca70a845..cae6682dff 100644 --- a/src/locales/oc-FR.json +++ b/src/locales/oc-FR.json @@ -54,6 +54,7 @@ "veryLarge": "Gradassa", "solid": "Solide", "hachure": "Raia", + "zigzag": "", "crossHatch": "Raia crosada", "thin": "Fin", "bold": "Espés", diff --git a/src/locales/pa-IN.json b/src/locales/pa-IN.json index 31c11ceb80..2b7c66a33e 100644 --- a/src/locales/pa-IN.json +++ b/src/locales/pa-IN.json @@ -54,6 +54,7 @@ "veryLarge": "ਬਹੁਤ ਵੱਡਾ", "solid": "ਠੋਸ", "hachure": "ਤਿਰਛੀਆਂ ਗਰਿੱਲਾਂ", + "zigzag": "", "crossHatch": "ਜਾਲੀ", "thin": "ਪਤਲੀ", "bold": "ਮੋਟੀ", diff --git a/src/locales/percentages.json b/src/locales/percentages.json index b28b6c259d..b129286980 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -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 } diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index 1abaa038f0..797d88fa15 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -54,6 +54,7 @@ "veryLarge": "Bardzo duży", "solid": "Pełne", "hachure": "Linie", + "zigzag": "", "crossHatch": "Zakreślone", "thin": "Cienkie", "bold": "Pogrubione", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index d790c5db53..12a1a5d46b 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -54,6 +54,7 @@ "veryLarge": "Muito grande", "solid": "Sólido", "hachure": "Hachura", + "zigzag": "", "crossHatch": "Hachura cruzada", "thin": "Fino", "bold": "Espesso", diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index 7ce28f00f0..947682311a 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -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", diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index a0e3b5883a..611ed56769 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -54,6 +54,7 @@ "veryLarge": "Foarte mare", "solid": "Plină", "hachure": "Hașură", + "zigzag": "Zigzag", "crossHatch": "Hașură transversală", "thin": "Subțire", "bold": "Îngroșată", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index d4030897ce..cda20347a9 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -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": "или", diff --git a/src/locales/si-LK.json b/src/locales/si-LK.json index b84ad567fe..01f5bcac20 100644 --- a/src/locales/si-LK.json +++ b/src/locales/si-LK.json @@ -54,6 +54,7 @@ "veryLarge": "ඉතා විශාල", "solid": "විශාල", "hachure": "මධ්‍යම", + "zigzag": "", "crossHatch": "", "thin": "කෙට්ටු", "bold": "තද", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index d6322ac1c8..4bb6f0e76c 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -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", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index 57199c8138..dc073c0cb0 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -54,6 +54,7 @@ "veryLarge": "Zelo velika", "solid": "Polno", "hachure": "Šrafura", + "zigzag": "Cikcak", "crossHatch": "Križno", "thin": "Tanko", "bold": "Krepko", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index d0392b119e..502c1bb1ec 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -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", diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index 6dc865766f..47c6e644b2 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -54,6 +54,7 @@ "veryLarge": "மிகப் பெரிய", "solid": "திடமான", "hachure": "மலைக்குறிக்கோடு", + "zigzag": "", "crossHatch": "குறுக்குகதவு", "thin": "மெல்லிய", "bold": "பட்டை", diff --git a/src/locales/th-TH.json b/src/locales/th-TH.json index 2dcb9cb433..021c0ce977 100644 --- a/src/locales/th-TH.json +++ b/src/locales/th-TH.json @@ -54,6 +54,7 @@ "veryLarge": "ใหญ่มาก", "solid": "", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "บาง", "bold": "หนา", diff --git a/src/locales/tr-TR.json b/src/locales/tr-TR.json index 78dd9329a7..8e1f2b4a07 100644 --- a/src/locales/tr-TR.json +++ b/src/locales/tr-TR.json @@ -54,6 +54,7 @@ "veryLarge": "Çok geniş", "solid": "Dolu", "hachure": "Taralı", + "zigzag": "", "crossHatch": "Çapraz-taralı", "thin": "İnce", "bold": "Kalın", diff --git a/src/locales/uk-UA.json b/src/locales/uk-UA.json index ed1a01e684..417e0a16ab 100644 --- a/src/locales/uk-UA.json +++ b/src/locales/uk-UA.json @@ -54,6 +54,7 @@ "veryLarge": "Дуже великий", "solid": "Суцільна", "hachure": "Штриховка", + "zigzag": "", "crossHatch": "Перехресна штриховка", "thin": "Тонкий", "bold": "Жирний", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index 887c32e3e7..27fccfb531 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -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", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 5863f5ceb1..18584cda76 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -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": "或", diff --git a/src/locales/zh-HK.json b/src/locales/zh-HK.json index bbf23f7f74..5cff35a5c6 100644 --- a/src/locales/zh-HK.json +++ b/src/locales/zh-HK.json @@ -54,6 +54,7 @@ "veryLarge": "勁大", "solid": "實心", "hachure": "斜線", + "zigzag": "", "crossHatch": "交叉格仔", "thin": "幼", "bold": "粗", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index f4462842cd..25c9b0f57c 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -54,6 +54,7 @@ "veryLarge": "特大", "solid": "實心", "hachure": "斜線筆觸", + "zigzag": "Z字形", "crossHatch": "交叉筆觸", "thin": "細", "bold": "粗", diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index c36883b8e8..a2f7466b8d 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -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 diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index eaeef4b0c6..d650885df0 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -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) diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index be4e61d27c..57f6ce3951 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -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": [ diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 1fb6cd3d95..d9365895e4 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -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"); } diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 21de70cb91..77ea14587d 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -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, ); } } diff --git a/src/scene/Fonts.ts b/src/scene/Fonts.ts index cc206c776b..e245eb16e2 100644 --- a/src/scene/Fonts.ts +++ b/src/scene/Fonts.ts @@ -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, { diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 18656edd19..c5b61e42fb 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -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 { diff --git a/src/tests/binding.test.tsx b/src/tests/binding.test.tsx index c615eb9259..07af36569d 100644 --- a/src/tests/binding.test.tsx +++ b/src/tests/binding.test.tsx @@ -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)!; diff --git a/src/tests/clipboard.test.tsx b/src/tests/clipboard.test.tsx index 1fdc0f4521..bbaa4d1799 100644 --- a/src/tests/clipboard.test.tsx +++ b/src/tests/clipboard.test.tsx @@ -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"; diff --git a/src/tests/flip.test.tsx b/src/tests/flip.test.tsx index 3be0824f34..091d1c73b4 100644 --- a/src/tests/flip.test.tsx +++ b/src/tests/flip.test.tsx @@ -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); }; diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index 15fd105ec4..c71283a4c0 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -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 diff --git a/src/tests/test-utils.ts b/src/tests/test-utils.ts index c33e80c7d8..9560f681fd 100644 --- a/src/tests/test-utils.ts +++ b/src/tests/test-utils.ts @@ -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), + files?: File[], +) => { + return Object.assign( + new Event("paste", { + bubbles: true, + cancelable: true, + composed: true, + }), + { + clipboardData: { + getData: typeof text === "string" ? () => text : text, + files: files || [], + }, + }, + ); +}; diff --git a/src/types.ts b/src/types.ts index 09848df1dc..e5ad01b595 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; @@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" }; export type BinaryFileData = { mimeType: - | typeof ALLOWED_IMAGE_MIME_TYPES[number] + | ValueOf // future user or unknown file type | typeof MIME_TYPES.binary; id: FileId; @@ -419,7 +419,7 @@ export type AppClassProperties = { FileId, { image: HTMLImageElement | Promise; - mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number]; + mimeType: ValueOf; } >; files: BinaryFiles; diff --git a/yarn.lock b/yarn.lock index 31624f92bc..89d1539098 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10601,9 +10601,9 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.75.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" - integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== + version "5.76.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c" + integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^0.0.51"