Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e75f215838 | ||
|
|
2264a66241 | ||
|
|
57a700f82e | ||
|
|
45a57d70de | ||
|
|
66ccc254f0 | ||
|
|
da8dd389a9 | ||
|
|
dae81c0a2c | ||
|
|
1e9943323a | ||
|
|
1815cf3213 | ||
|
|
d35386755f | ||
|
|
9d5cfbbfb7 | ||
|
|
fee760d38c | ||
|
|
2a4799d8c8 | ||
|
|
c4445c181b | ||
|
|
d12a9fdd40 | ||
|
|
9368a9ce3e | ||
|
|
851b9b7aec | ||
|
|
5ddb28d378 | ||
|
|
404a79e241 | ||
|
|
eea30da05a | ||
|
|
98a77d7426 | ||
|
|
ff3c2e5a16 | ||
|
|
b64beaf5ba | ||
|
|
89304c9f66 | ||
|
|
1d0653ce50 |
@@ -12,14 +12,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
excalidraw/excalidraw
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: excalidraw/excalidraw:latest
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -31,10 +31,29 @@ You can pass `null` / `undefined` if not applicable.
|
|||||||
restoreElements(
|
restoreElements(
|
||||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
||||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||||
refreshDimensions?: boolean<br/>
|
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||||
)
|
)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
|
||||||
|
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | 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_**
|
**_How to use_**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -43,9 +62,6 @@ import { restoreElements } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
|
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
|
||||||
|
|
||||||
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
|
|
||||||
Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates.
|
|
||||||
|
|
||||||
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
|
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
|
||||||
|
|
||||||
### restore
|
### restore
|
||||||
@@ -56,7 +72,9 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
|
|||||||
restore(
|
restore(
|
||||||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
|
||||||
|
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||||
|
|
||||||
)
|
)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
|||||||
@@ -339,3 +339,47 @@ The `device` has the following `attributes`
|
|||||||
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
||||||
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
||||||
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
To help with localization, we export the following.
|
||||||
|
|
||||||
|
| name | type |
|
||||||
|
| --- | --- |
|
||||||
|
| `defaultLang` | `string` |
|
||||||
|
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||||
|
| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
#### defaultLang
|
||||||
|
|
||||||
|
Default language code, `en`.
|
||||||
|
|
||||||
|
#### languages
|
||||||
|
|
||||||
|
List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode).
|
||||||
|
|
||||||
|
#### useI18n
|
||||||
|
|
||||||
|
A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of `<Excalidraw>`.
|
||||||
|
|
||||||
|
```jsx live
|
||||||
|
function App() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
return (
|
||||||
|
<div style={{ height: "500px" }}>
|
||||||
|
<Excalidraw>
|
||||||
|
<button
|
||||||
|
style={{ position: "absolute", zIndex: 10, height: "2rem" }}
|
||||||
|
onClick={() => window.alert(t("labels.madeWithExcalidraw"))}
|
||||||
|
>
|
||||||
|
{t("buttons.confirm")}
|
||||||
|
</button>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@docusaurus/core": "2.2.0",
|
"@docusaurus/core": "2.2.0",
|
||||||
"@docusaurus/preset-classic": "2.2.0",
|
"@docusaurus/preset-classic": "2.2.0",
|
||||||
"@docusaurus/theme-live-codeblock": "2.2.0",
|
"@docusaurus/theme-live-codeblock": "2.2.0",
|
||||||
"@excalidraw/excalidraw": "0.14.2",
|
"@excalidraw/excalidraw": "0.15.2",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"docusaurus-plugin-sass": "0.2.3",
|
"docusaurus-plugin-sass": "0.2.3",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const ExcalidrawScope = {
|
|||||||
Sidebar: ExcalidrawComp.Sidebar,
|
Sidebar: ExcalidrawComp.Sidebar,
|
||||||
exportToCanvas: ExcalidrawComp.exportToCanvas,
|
exportToCanvas: ExcalidrawComp.exportToCanvas,
|
||||||
initialData,
|
initialData,
|
||||||
|
useI18n: ExcalidrawComp.useI18n,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExcalidrawScope;
|
export default ExcalidrawScope;
|
||||||
|
|||||||
+7
-7
@@ -1631,10 +1631,10 @@
|
|||||||
url-loader "^4.1.1"
|
url-loader "^4.1.1"
|
||||||
webpack "^5.73.0"
|
webpack "^5.73.0"
|
||||||
|
|
||||||
"@excalidraw/excalidraw@0.14.2":
|
"@excalidraw/excalidraw@0.15.2":
|
||||||
version "0.14.2"
|
version "0.15.2"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375"
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
|
||||||
integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==
|
integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
|
||||||
|
|
||||||
"@hapi/hoek@^9.0.0":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "9.3.0"
|
version "9.3.0"
|
||||||
@@ -7159,9 +7159,9 @@ typescript@^4.7.4:
|
|||||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||||
|
|
||||||
ua-parser-js@^0.7.30:
|
ua-parser-js@^0.7.30:
|
||||||
version "0.7.31"
|
version "0.7.33"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||||
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
|
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
|
||||||
|
|
||||||
unescape@^1.0.1:
|
unescape@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
|||||||
@@ -1,22 +1,9 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
const pkg = require(excalidrawPackage);
|
const pkg = require(excalidrawPackage);
|
||||||
|
|
||||||
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
|
|
||||||
|
|
||||||
const updateReadme = () => {
|
|
||||||
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
|
|
||||||
|
|
||||||
// remove note for stable readme
|
|
||||||
const data = originalReadMe.slice(excalidrawIndex);
|
|
||||||
|
|
||||||
// update readme
|
|
||||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
const publish = () => {
|
const publish = () => {
|
||||||
try {
|
try {
|
||||||
execSync(`yarn --frozen-lockfile`);
|
execSync(`yarn --frozen-lockfile`);
|
||||||
@@ -30,15 +17,8 @@ const publish = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const release = () => {
|
const release = () => {
|
||||||
updateReadme();
|
|
||||||
console.info("Note for stable readme removed");
|
|
||||||
|
|
||||||
publish();
|
publish();
|
||||||
console.info(`Published ${pkg.version}!`);
|
console.info(`Published ${pkg.version}!`);
|
||||||
|
|
||||||
// revert readme after release
|
|
||||||
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
|
|
||||||
console.info("Readme reverted");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
release();
|
release();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const actionCopy = register({
|
|||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const selectedElements = getSelectedElements(elements, appState, true);
|
||||||
|
|
||||||
copyToClipboard(selectedElements, appState, app.files);
|
copyToClipboard(selectedElements, app.files);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ import {
|
|||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
@@ -314,9 +314,9 @@ export const actionChangeFillStyle = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
const allElementsZigZag = selectedElements.every(
|
const allElementsZigZag =
|
||||||
(el) => el.fillStyle === "zigzag",
|
selectedElements.length > 0 &&
|
||||||
);
|
selectedElements.every((el) => el.fillStyle === "zigzag");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -326,7 +326,9 @@ export const actionChangeFillStyle = register({
|
|||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: "hachure",
|
value: "hachure",
|
||||||
text: t("labels.hachure"),
|
text: `${
|
||||||
|
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
|
||||||
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
active: allElementsZigZag ? true : undefined,
|
active: allElementsZigZag ? true : undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
+27
-11
@@ -2,12 +2,12 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { AppState, BinaryFiles } from "./types";
|
import { BinaryFiles } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { isInitializedImageElement } from "./element/typeChecks";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
import { isPromiseLike } from "./utils";
|
import { isPromiseLike, isTestEnv } from "./utils";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@@ -55,24 +55,40 @@ const clipboardContainsElements = (
|
|||||||
|
|
||||||
export const copyToClipboard = async (
|
export const copyToClipboard = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles | null,
|
files: BinaryFiles | null,
|
||||||
) => {
|
) => {
|
||||||
|
let foundFile = false;
|
||||||
|
|
||||||
|
const _files = elements.reduce((acc, element) => {
|
||||||
|
if (isInitializedImageElement(element)) {
|
||||||
|
foundFile = true;
|
||||||
|
if (files && files[element.fileId]) {
|
||||||
|
acc[element.fileId] = files[element.fileId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as BinaryFiles);
|
||||||
|
|
||||||
|
if (foundFile && !files) {
|
||||||
|
console.warn(
|
||||||
|
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// select binded text elements when copying
|
// select binded text elements when copying
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements,
|
elements,
|
||||||
files: files
|
files: files ? _files : undefined,
|
||||||
? elements.reduce((acc, element) => {
|
|
||||||
if (isInitializedImageElement(element) && files[element.fileId]) {
|
|
||||||
acc[element.fileId] = files[element.fileId];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as BinaryFiles)
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
|
|
||||||
|
if (isTestEnv()) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
CLIPBOARD = json;
|
CLIPBOARD = json;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PREFER_APP_CLIPBOARD = false;
|
PREFER_APP_CLIPBOARD = false;
|
||||||
await copyTextToSystemClipboard(json);
|
await copyTextToSystemClipboard(json);
|
||||||
|
|||||||
+16
-2
@@ -60,6 +60,7 @@ import {
|
|||||||
ENV,
|
ENV,
|
||||||
EVENT,
|
EVENT,
|
||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
|
IMAGE_MIME_TYPES,
|
||||||
IMAGE_RENDER_TIMEOUT,
|
IMAGE_RENDER_TIMEOUT,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
isBrave,
|
isBrave,
|
||||||
@@ -1589,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: data.elements,
|
elements: data.elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
position: "cursor",
|
position: "cursor",
|
||||||
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
this.addTextFromPaste(data.text, isPlainPaste);
|
this.addTextFromPaste(data.text, isPlainPaste);
|
||||||
@@ -1602,6 +1604,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
|
retainSeed?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null);
|
const elements = restoreElements(opts.elements, null);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
@@ -1639,6 +1642,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
y: element.y + gridY - minY,
|
y: element.y + gridY - minY,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
randomizeSeed: !opts.retainSeed,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextElements = [
|
const nextElements = [
|
||||||
@@ -2744,6 +2750,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
lineHeight,
|
lineHeight,
|
||||||
|
angle: container?.angle ?? 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingTextElement && shouldBindToContainer && container) {
|
if (!existingTextElement && shouldBindToContainer && container) {
|
||||||
@@ -4719,7 +4726,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
||||||
// it would have weird results (stuff jumping all over the screen)
|
// it would have weird results (stuff jumping all over the screen)
|
||||||
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
|
// Checking for editingElement to avoid jump while editing on mobile #6503
|
||||||
|
if (
|
||||||
|
selectedElements.length > 0 &&
|
||||||
|
!pointerDownState.withCmdOrCtrl &&
|
||||||
|
!this.state.editingElement
|
||||||
|
) {
|
||||||
const [dragX, dragY] = getGridPoint(
|
const [dragX, dragY] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
pointerCoords.x - pointerDownState.drag.offset.x,
|
||||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
pointerCoords.y - pointerDownState.drag.offset.y,
|
||||||
@@ -5742,7 +5754,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const imageFile = await fileOpen({
|
const imageFile = await fileOpen({
|
||||||
description: "Image",
|
description: "Image",
|
||||||
extensions: ["jpg", "png", "svg", "gif"],
|
extensions: Object.keys(
|
||||||
|
IMAGE_MIME_TYPES,
|
||||||
|
) as (keyof typeof IMAGE_MIME_TYPES)[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageElement = this.createImageElement({
|
const imageElement = this.createImageElement({
|
||||||
|
|||||||
@@ -183,6 +183,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
font-family: inherit;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 0.2fr;
|
grid-template-columns: 1fr 0.2fr;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const LibraryMenuItems = ({
|
|||||||
...item,
|
...item,
|
||||||
// duplicate each library item before inserting on canvas to confine
|
// duplicate each library item before inserting on canvas to confine
|
||||||
// ids and bindings to each library item. See #6465
|
// ids and bindings to each library item. See #6465
|
||||||
elements: duplicateElements(item.elements),
|
elements: duplicateElements(item.elements, { randomizeSeed: true }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
// container in body where the actual tooltip is appended to
|
// container in body where the actual tooltip is appended to
|
||||||
.excalidraw-tooltip {
|
.excalidraw-tooltip {
|
||||||
|
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
||||||
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-family: var(--ui-font);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
|
|||||||
+16
-16
@@ -105,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
|||||||
|
|
||||||
export const GRID_SIZE = 20; // TODO make it configurable?
|
export const GRID_SIZE = 20; // TODO make it configurable?
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const IMAGE_MIME_TYPES = {
|
||||||
excalidraw: "application/vnd.excalidraw+json",
|
|
||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
|
||||||
json: "application/json",
|
|
||||||
svg: "image/svg+xml",
|
svg: "image/svg+xml",
|
||||||
"excalidraw.svg": "image/svg+xml",
|
|
||||||
png: "image/png",
|
png: "image/png",
|
||||||
"excalidraw.png": "image/png",
|
|
||||||
jpg: "image/jpeg",
|
jpg: "image/jpeg",
|
||||||
gif: "image/gif",
|
gif: "image/gif",
|
||||||
webp: "image/webp",
|
webp: "image/webp",
|
||||||
bmp: "image/bmp",
|
bmp: "image/bmp",
|
||||||
ico: "image/x-icon",
|
ico: "image/x-icon",
|
||||||
|
avif: "image/avif",
|
||||||
|
jfif: "image/jfif",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MIME_TYPES = {
|
||||||
|
json: "application/json",
|
||||||
|
// excalidraw data
|
||||||
|
excalidraw: "application/vnd.excalidraw+json",
|
||||||
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
|
// image-encoded excalidraw data
|
||||||
|
"excalidraw.svg": "image/svg+xml",
|
||||||
|
"excalidraw.png": "image/png",
|
||||||
|
// binary
|
||||||
binary: "application/octet-stream",
|
binary: "application/octet-stream",
|
||||||
|
// image
|
||||||
|
...IMAGE_MIME_TYPES,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_DATA_TYPES = {
|
export const EXPORT_DATA_TYPES = {
|
||||||
@@ -189,16 +199,6 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
|
|||||||
|
|
||||||
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
|
|
||||||
export const ALLOWED_IMAGE_MIME_TYPES = [
|
|
||||||
MIME_TYPES.png,
|
|
||||||
MIME_TYPES.jpg,
|
|
||||||
MIME_TYPES.svg,
|
|
||||||
MIME_TYPES.gif,
|
|
||||||
MIME_TYPES.webp,
|
|
||||||
MIME_TYPES.bmp,
|
|
||||||
MIME_TYPES.ico,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|||||||
@@ -354,6 +354,7 @@
|
|||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
border: 1px solid var(--button-gray-2);
|
border: 1px solid var(--button-gray-2);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
font-family: inherit;
|
||||||
outline: none;
|
outline: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-image: var(--dropdown-icon);
|
background-image: var(--dropdown-icon);
|
||||||
@@ -413,6 +414,7 @@
|
|||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover-bg);
|
background-color: var(--button-hover-bg);
|
||||||
|
|||||||
+4
-6
@@ -1,6 +1,6 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement, FileId } from "../element/types";
|
import { ExcalidrawElement, FileId } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
@@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
|
|||||||
|
|
||||||
export const isSupportedImageFile = (
|
export const isSupportedImageFile = (
|
||||||
blob: Blob | null | undefined,
|
blob: Blob | null | undefined,
|
||||||
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => {
|
): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
|
||||||
const { type } = blob || {};
|
const { type } = blob || {};
|
||||||
return (
|
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
|
||||||
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadSceneOrLibraryFromBlob = async (
|
export const loadSceneOrLibraryFromBlob = async (
|
||||||
@@ -157,7 +155,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
{ repairBindings: true, refreshDimensions: true },
|
{ repairBindings: true, refreshDimensions: false },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
|||||||
+1
-10
@@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants";
|
|||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
import { debounce } from "../utils";
|
import { debounce } from "../utils";
|
||||||
|
|
||||||
type FILE_EXTENSION =
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
| "gif"
|
|
||||||
| "jpg"
|
|
||||||
| "png"
|
|
||||||
| "excalidraw.png"
|
|
||||||
| "svg"
|
|
||||||
| "excalidraw.svg"
|
|
||||||
| "json"
|
|
||||||
| "excalidraw"
|
|
||||||
| "excalidrawlib";
|
|
||||||
|
|
||||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ import { isBindingElement } from "./typeChecks";
|
|||||||
import { shouldRotateWithDiscreteAngle } from "../keys";
|
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
DRAGGING_THRESHOLD,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
|
} from "../constants";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
@@ -1304,6 +1308,16 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
x = midSegmentMidpoint[0] - boundTextElement.width / 2;
|
x = midSegmentMidpoint[0] - boundTextElement.width / 2;
|
||||||
y = midSegmentMidpoint[1] - boundTextElement.height / 2;
|
y = midSegmentMidpoint[1] - boundTextElement.height / 2;
|
||||||
|
if (element.points.length === 2) {
|
||||||
|
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
|
y =
|
||||||
|
midSegmentMidpoint[1] -
|
||||||
|
boundTextElement.height -
|
||||||
|
BOUND_TEXT_PADDING * 2;
|
||||||
|
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
|
y = midSegmentMidpoint[1] + BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
isTestEnv,
|
isTestEnv,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { mutateElement, newElementWith } from "./mutateElement";
|
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
measureText,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
@@ -310,7 +310,7 @@ export const refreshTextDimensions = (
|
|||||||
text = wrapText(
|
text = wrapText(
|
||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
getMaxContainerWidth(container),
|
getBoundTextMaxWidth(container),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, text);
|
const dimensions = getAdjustedDimensions(textElement, text);
|
||||||
@@ -539,8 +539,16 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
|||||||
* it's advised to supply the whole elements array, or sets of elements that
|
* it's advised to supply the whole elements array, or sets of elements that
|
||||||
* are encapsulated (such as library items), if the purpose is to retain
|
* are encapsulated (such as library items), if the purpose is to retain
|
||||||
* bindings to the cloned elements intact.
|
* bindings to the cloned elements intact.
|
||||||
|
*
|
||||||
|
* NOTE by default does not randomize or regenerate anything except the id.
|
||||||
*/
|
*/
|
||||||
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
|
export const duplicateElements = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
opts?: {
|
||||||
|
/** NOTE also updates version flags and `updated` */
|
||||||
|
randomizeSeed: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
const clonedElements: ExcalidrawElement[] = [];
|
const clonedElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
const origElementsMap = arrayToMap(elements);
|
const origElementsMap = arrayToMap(elements);
|
||||||
@@ -574,6 +582,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
|
|
||||||
clonedElement.id = maybeGetNewId(element.id)!;
|
clonedElement.id = maybeGetNewId(element.id)!;
|
||||||
|
|
||||||
|
if (opts?.randomizeSeed) {
|
||||||
|
clonedElement.seed = randomInteger();
|
||||||
|
bumpVersion(clonedElement);
|
||||||
|
}
|
||||||
|
|
||||||
if (clonedElement.groupIds) {
|
if (clonedElement.groupIds) {
|
||||||
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
|
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
|
||||||
if (!groupNewIdsMap.has(groupId)) {
|
if (!groupNewIdsMap.has(groupId)) {
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ import {
|
|||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
measureText,
|
measureText,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
@@ -204,7 +204,7 @@ const measureFontSizeFromWidth = (
|
|||||||
if (hasContainer) {
|
if (hasContainer) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
if (container) {
|
if (container) {
|
||||||
width = getMaxContainerWidth(container);
|
width = getBoundTextMaxWidth(container);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextFontSize = element.fontSize * (nextWidth / width);
|
const nextFontSize = element.fontSize * (nextWidth / width);
|
||||||
@@ -435,8 +435,8 @@ export const resizeSingleElement = (
|
|||||||
|
|
||||||
const nextFont = measureFontSizeFromWidth(
|
const nextFont = measureFontSizeFromWidth(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
getMaxContainerWidth(updatedElement),
|
getBoundTextMaxWidth(updatedElement),
|
||||||
getMaxContainerHeight(updatedElement),
|
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
||||||
);
|
);
|
||||||
if (nextFont === null) {
|
if (nextFont === null) {
|
||||||
return;
|
return;
|
||||||
@@ -718,10 +718,10 @@ const resizeMultipleElements = (
|
|||||||
const metrics = measureFontSizeFromWidth(
|
const metrics = measureFontSizeFromWidth(
|
||||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||||
boundTextElement
|
boundTextElement
|
||||||
? getMaxContainerWidth(updatedElement)
|
? getBoundTextMaxWidth(updatedElement)
|
||||||
: updatedElement.width,
|
: updatedElement.width,
|
||||||
boundTextElement
|
boundTextElement
|
||||||
? getMaxContainerHeight(updatedElement)
|
? getBoundTextMaxHeight(updatedElement, boundTextElement)
|
||||||
: updatedElement.height,
|
: updatedElement.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { API } from "../tests/helpers/api";
|
|||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxHeight,
|
||||||
wrapText,
|
wrapText,
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
|
parseTokens,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { ExcalidrawTextElementWithContainer, FontString } from "./types";
|
||||||
|
|
||||||
describe("Test wrapText", () => {
|
describe("Test wrapText", () => {
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||||
@@ -183,6 +184,56 @@ now`,
|
|||||||
expect(wrapText(text, font, -1)).toEqual(text);
|
expect(wrapText(text, font, -1)).toEqual(text);
|
||||||
expect(wrapText(text, font, Infinity)).toEqual(text);
|
expect(wrapText(text, font, Infinity)).toEqual(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should wrap the text correctly when text contains hyphen", () => {
|
||||||
|
let text =
|
||||||
|
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
||||||
|
const res = wrapText(text, font, 110);
|
||||||
|
expect(res).toBe(
|
||||||
|
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`,
|
||||||
|
);
|
||||||
|
|
||||||
|
text = "Hello thereusing-now";
|
||||||
|
expect(wrapText(text, font, 100)).toEqual("Hello \nthereusin\ng-now");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test parseTokens", () => {
|
||||||
|
it("should split into tokens correctly", () => {
|
||||||
|
let text = "Excalidraw is a virtual collaborative whiteboard";
|
||||||
|
expect(parseTokens(text)).toEqual([
|
||||||
|
"Excalidraw",
|
||||||
|
"is",
|
||||||
|
"a",
|
||||||
|
"virtual",
|
||||||
|
"collaborative",
|
||||||
|
"whiteboard",
|
||||||
|
]);
|
||||||
|
|
||||||
|
text =
|
||||||
|
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
||||||
|
expect(parseTokens(text)).toEqual([
|
||||||
|
"Wikipedia",
|
||||||
|
"is",
|
||||||
|
"hosted",
|
||||||
|
"by",
|
||||||
|
"Wikimedia-",
|
||||||
|
"",
|
||||||
|
"Foundation,",
|
||||||
|
"a",
|
||||||
|
"non-",
|
||||||
|
"profit",
|
||||||
|
"organization",
|
||||||
|
"that",
|
||||||
|
"also",
|
||||||
|
"hosts",
|
||||||
|
"a",
|
||||||
|
"range-",
|
||||||
|
"of",
|
||||||
|
"other",
|
||||||
|
"projects",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test measureText", () => {
|
describe("Test measureText", () => {
|
||||||
@@ -260,7 +311,7 @@ describe("Test measureText", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test getMaxContainerWidth", () => {
|
describe("Test getBoundTextMaxWidth", () => {
|
||||||
const params = {
|
const params = {
|
||||||
width: 178,
|
width: 178,
|
||||||
height: 194,
|
height: 194,
|
||||||
@@ -268,39 +319,76 @@ describe("Test measureText", () => {
|
|||||||
|
|
||||||
it("should return max width when container is rectangle", () => {
|
it("should return max width when container is rectangle", () => {
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
expect(getMaxContainerWidth(container)).toBe(168);
|
expect(getBoundTextMaxWidth(container)).toBe(168);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max width when container is ellipse", () => {
|
it("should return max width when container is ellipse", () => {
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
expect(getMaxContainerWidth(container)).toBe(116);
|
expect(getBoundTextMaxWidth(container)).toBe(116);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max width when container is diamond", () => {
|
it("should return max width when container is diamond", () => {
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
expect(getMaxContainerWidth(container)).toBe(79);
|
expect(getBoundTextMaxWidth(container)).toBe(79);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test getMaxContainerHeight", () => {
|
describe("Test getBoundTextMaxHeight", () => {
|
||||||
const params = {
|
const params = {
|
||||||
width: 178,
|
width: 178,
|
||||||
height: 194,
|
height: 194,
|
||||||
|
id: '"container-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const boundTextElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "text-id",
|
||||||
|
x: 560.51171875,
|
||||||
|
y: 202.033203125,
|
||||||
|
width: 154,
|
||||||
|
height: 175,
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 1,
|
||||||
|
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
||||||
|
textAlign: "center",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
containerId: params.id,
|
||||||
|
}) as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
it("should return max height when container is rectangle", () => {
|
it("should return max height when container is rectangle", () => {
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
expect(getMaxContainerHeight(container)).toBe(184);
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max height when container is ellipse", () => {
|
it("should return max height when container is ellipse", () => {
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
expect(getMaxContainerHeight(container)).toBe(127);
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max height when container is diamond", () => {
|
it("should return max height when container is diamond", () => {
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
expect(getMaxContainerHeight(container)).toBe(87);
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is arrow", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is arrow and height is less than threshold", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
...params,
|
||||||
|
height: 70,
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
|
||||||
|
boundTextElement.height,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-51
@@ -65,7 +65,7 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.text = textElement.text;
|
boundTextUpdates.text = textElement.text;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
boundTextUpdates.text = wrapText(
|
boundTextUpdates.text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
@@ -83,27 +83,22 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.baseline = metrics.baseline;
|
boundTextUpdates.baseline = metrics.baseline;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
if (isArrowElement(container)) {
|
const containerDims = getContainerDims(container);
|
||||||
const centerX = textElement.x + textElement.width / 2;
|
const maxContainerHeight = getBoundTextMaxHeight(
|
||||||
const centerY = textElement.y + textElement.height / 2;
|
container,
|
||||||
const diffWidth = metrics.width - textElement.width;
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
const diffHeight = metrics.height - textElement.height;
|
);
|
||||||
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
|
|
||||||
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
|
|
||||||
} else {
|
|
||||||
const containerDims = getContainerDims(container);
|
|
||||||
let maxContainerHeight = getMaxContainerHeight(container);
|
|
||||||
|
|
||||||
let nextHeight = containerDims.height;
|
let nextHeight = containerDims.height;
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (metrics.height > maxContainerHeight) {
|
||||||
nextHeight = computeContainerDimensionForBoundText(
|
nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
maxContainerHeight = getMaxContainerHeight(container);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
}
|
||||||
}
|
if (!isArrowElement(container)) {
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
@@ -183,8 +178,11 @@ export const handleBindTextResize = (
|
|||||||
let nextHeight = textElement.height;
|
let nextHeight = textElement.height;
|
||||||
let nextWidth = textElement.width;
|
let nextWidth = textElement.width;
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
const maxWidth = getMaxContainerWidth(container);
|
const maxWidth = getBoundTextMaxWidth(container);
|
||||||
const maxHeight = getMaxContainerHeight(container);
|
const maxHeight = getBoundTextMaxHeight(
|
||||||
|
container,
|
||||||
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
|
);
|
||||||
let containerHeight = containerDims.height;
|
let containerHeight = containerDims.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
let nextBaseLine = textElement.baseline;
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
@@ -256,8 +254,8 @@ export const computeBoundTextPosition = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const containerCoords = getContainerCoords(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
const maxContainerHeight = getMaxContainerHeight(container);
|
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
|
||||||
const maxContainerWidth = getMaxContainerWidth(container);
|
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||||
|
|
||||||
let x;
|
let x;
|
||||||
let y;
|
let y;
|
||||||
@@ -419,6 +417,24 @@ export const getTextHeight = (
|
|||||||
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseTokens = (text: string) => {
|
||||||
|
// Splitting words containing "-" as those are treated as separate words
|
||||||
|
// by css wrapping algorithm eg non-profit => non-, profit
|
||||||
|
const words = text.split("-");
|
||||||
|
if (words.length > 1) {
|
||||||
|
// non-proft org => ['non-', 'profit org']
|
||||||
|
words.forEach((word, index) => {
|
||||||
|
if (index !== words.length - 1) {
|
||||||
|
words[index] = word += "-";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Joining the words with space and splitting them again with space to get the
|
||||||
|
// final list of tokens
|
||||||
|
// ['non-', 'profit org'] =>,'non- proft org' => ['non-','profit','org']
|
||||||
|
return words.join(" ").split(" ");
|
||||||
|
};
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
// if maxWidth is not finite or NaN which can happen in case of bugs in
|
// if maxWidth is not finite or NaN which can happen in case of bugs in
|
||||||
// computation, we need to make sure we don't continue as we'll end up
|
// computation, we need to make sure we don't continue as we'll end up
|
||||||
@@ -444,17 +460,16 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
originalLines.forEach((originalLine) => {
|
originalLines.forEach((originalLine) => {
|
||||||
const currentLineWidth = getTextWidth(originalLine, font);
|
const currentLineWidth = getTextWidth(originalLine, font);
|
||||||
|
|
||||||
//Push the line if its <= maxWidth
|
// Push the line if its <= maxWidth
|
||||||
if (currentLineWidth <= maxWidth) {
|
if (currentLineWidth <= maxWidth) {
|
||||||
lines.push(originalLine);
|
lines.push(originalLine);
|
||||||
return; // continue
|
return; // continue
|
||||||
}
|
}
|
||||||
const words = originalLine.split(" ");
|
|
||||||
|
|
||||||
|
const words = parseTokens(originalLine);
|
||||||
resetParams();
|
resetParams();
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
@@ -472,6 +487,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
else if (currentWordWidth > maxWidth) {
|
else if (currentWordWidth > maxWidth) {
|
||||||
// push current line since the current word exceeds the max width
|
// push current line since the current word exceeds the max width
|
||||||
// so will be appended in next line
|
// so will be appended in next line
|
||||||
|
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
|
|
||||||
resetParams();
|
resetParams();
|
||||||
@@ -492,15 +508,15 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
currentLine += currentChar;
|
currentLine += currentChar;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// push current line if appending space exceeds max width
|
// push current line if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
resetParams();
|
resetParams();
|
||||||
} else {
|
|
||||||
// space needs to be appended before next word
|
// space needs to be appended before next word
|
||||||
// as currentLine contains chars which couldn't be appended
|
// as currentLine contains chars which couldn't be appended
|
||||||
// to previous line
|
// to previous line unless the line ends with hyphen to sync
|
||||||
|
// with css word-wrap
|
||||||
|
} else if (!currentLine.endsWith("-")) {
|
||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
currentLineWidthTillNow += spaceWidth;
|
currentLineWidthTillNow += spaceWidth;
|
||||||
}
|
}
|
||||||
@@ -518,12 +534,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
currentLine += `${word} `;
|
|
||||||
|
// if word ends with "-" then we don't need to add space
|
||||||
|
// to sync with css word-wrap
|
||||||
|
const shouldAppendSpace = !word.endsWith("-");
|
||||||
|
currentLine += word;
|
||||||
|
|
||||||
|
if (shouldAppendSpace) {
|
||||||
|
currentLine += " ";
|
||||||
|
}
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
// Push the word if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
const word = currentLine.slice(0, -1);
|
if (shouldAppendSpace) {
|
||||||
push(word);
|
lines.push(currentLine.slice(0, -1));
|
||||||
|
} else {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
resetParams();
|
resetParams();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -768,7 +795,11 @@ export const shouldAllowVerticalAlign = (
|
|||||||
const hasBoundContainer = isBoundToContainer(element);
|
const hasBoundContainer = isBoundToContainer(element);
|
||||||
if (hasBoundContainer) {
|
if (hasBoundContainer) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
if (isTextElement(element) && isArrowElement(container)) {
|
if (
|
||||||
|
isTextElement(element) &&
|
||||||
|
isArrowElement(container) &&
|
||||||
|
container.points.length > 2
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -861,18 +892,10 @@ export const computeContainerDimensionForBoundText = (
|
|||||||
return dimension + padding;
|
return dimension + padding;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
|
||||||
const width = getContainerDims(container).width;
|
const width = getContainerDims(container).width;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
return width - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
if (containerWidth <= 0) {
|
|
||||||
const boundText = getBoundTextElement(container);
|
|
||||||
if (boundText) {
|
|
||||||
return boundText.width;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
|
||||||
return containerWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.type === "ellipse") {
|
if (container.type === "ellipse") {
|
||||||
@@ -889,16 +912,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
|||||||
return width - BOUND_TEXT_PADDING * 2;
|
return width - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
export const getBoundTextMaxHeight = (
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
|
) => {
|
||||||
const height = getContainerDims(container).height;
|
const height = getContainerDims(container).height;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
if (containerHeight <= 0) {
|
if (containerHeight <= 0) {
|
||||||
const boundText = getBoundTextElement(container);
|
return boundTextElement.height;
|
||||||
if (boundText) {
|
|
||||||
return boundText.height;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -526,6 +526,36 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should set the text element angle to same as container angle when binding to rotated container", async () => {
|
||||||
|
const rectangle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
angle: 45,
|
||||||
|
});
|
||||||
|
h.elements = [rectangle];
|
||||||
|
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
expect(text.angle).toBe(rectangle.angle);
|
||||||
|
mouse.down();
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
editor.blur();
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
|
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
|
||||||
const diamond = API.createElement({
|
const diamond = API.createElement({
|
||||||
type: "diamond",
|
type: "diamond",
|
||||||
|
|||||||
+13
-13
@@ -32,8 +32,8 @@ import {
|
|||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxHeight,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
@@ -205,8 +205,11 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
maxHeight = getMaxContainerHeight(container);
|
maxHeight = getBoundTextMaxHeight(
|
||||||
|
container,
|
||||||
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
|
);
|
||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
||||||
@@ -230,17 +233,14 @@ export const textWysiwyg = ({
|
|||||||
);
|
);
|
||||||
mutateElement(container, { height: targetContainerHeight });
|
mutateElement(container, { height: targetContainerHeight });
|
||||||
}
|
}
|
||||||
// Start pushing text upward until a diff of 30px (padding)
|
// update y coord as you type, not needed for arrow as we calculate
|
||||||
// is reached
|
// position from the container element for editor and canvas when rendering labelled arrows
|
||||||
else {
|
else if (!isArrowElement(container)) {
|
||||||
const containerCoords = getContainerCoords(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
|
|
||||||
// vertically center align the text
|
// vertically center align the text
|
||||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||||
if (!isArrowElement(container)) {
|
coordY = containerCoords.y + maxHeight / 2 - textElementHeight / 2;
|
||||||
coordY =
|
|
||||||
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
coordY = containerCoords.y + (maxHeight - textElementHeight);
|
coordY = containerCoords.y + (maxHeight - textElementHeight);
|
||||||
@@ -377,7 +377,7 @@ export const textWysiwyg = ({
|
|||||||
const wrappedText = wrapText(
|
const wrappedText = wrapText(
|
||||||
`${editable.value}${data}`,
|
`${editable.value}${data}`,
|
||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container),
|
getBoundTextMaxWidth(container),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font);
|
const width = getTextWidth(wrappedText, font);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
@@ -394,7 +394,7 @@ export const textWysiwyg = ({
|
|||||||
const wrappedText = wrapText(
|
const wrappedText = wrapText(
|
||||||
normalizeText(editable.value),
|
normalizeText(editable.value),
|
||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container!),
|
getBoundTextMaxWidth(container!),
|
||||||
);
|
);
|
||||||
const { width, height } = measureText(
|
const { width, height } = measureText(
|
||||||
wrappedText,
|
wrappedText,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const reconcileElements = (
|
|||||||
|
|
||||||
// Mark duplicate for removal as it'll be replaced with the remote element
|
// Mark duplicate for removal as it'll be replaced with the remote element
|
||||||
if (local) {
|
if (local) {
|
||||||
// Unless the ramote and local elements are the same element in which case
|
// Unless the remote and local elements are the same element in which case
|
||||||
// we need to keep it as we'd otherwise discard it from the resulting
|
// we need to keep it as we'd otherwise discard it from the resulting
|
||||||
// array.
|
// array.
|
||||||
if (local[0] === remoteElement) {
|
if (local[0] === remoteElement) {
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
{ repairBindings: true, refreshDimensions: true },
|
{ repairBindings: true, refreshDimensions: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null, {
|
data = restore(localDataState || null, null, null, {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"veryLarge": "Very large",
|
"veryLarge": "Very large",
|
||||||
"solid": "Solid",
|
"solid": "Solid",
|
||||||
"hachure": "Hachure",
|
"hachure": "Hachure",
|
||||||
|
"zigzag": "Zigzag",
|
||||||
"crossHatch": "Cross-hatch",
|
"crossHatch": "Cross-hatch",
|
||||||
"thin": "Thin",
|
"thin": "Thin",
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
|
|||||||
@@ -11,6 +11,38 @@ The change should be grouped under one of the below section and must contain PR
|
|||||||
Please add the latest change on the top under the correct section.
|
Please add the latest change on the top under the correct section.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 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)
|
## 0.15.0 (2023-04-18)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ Excalidraw takes _100%_ of `width` and `height` of the containing block so make
|
|||||||
|
|
||||||
## Integration
|
## Integration
|
||||||
|
|
||||||
Head over to the [docs](https://docs.excalidraw.com/docs/package/integration)
|
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration)
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
Head over to the [docs](https://docs.excalidraw.com/docs/package/api)
|
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@excalidraw/excalidraw",
|
"name": "@excalidraw/excalidraw",
|
||||||
"version": "0.15.0",
|
"version": "0.15.2",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"types": "types/packages/excalidraw/index.d.ts",
|
"types": "types/packages/excalidraw/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -220,15 +220,7 @@ export const exportToClipboard = async (
|
|||||||
} else if (opts.type === "png") {
|
} else if (opts.type === "png") {
|
||||||
await copyBlobToClipboardAsPng(exportToBlob(opts));
|
await copyBlobToClipboardAsPng(exportToBlob(opts));
|
||||||
} else if (opts.type === "json") {
|
} else if (opts.type === "json") {
|
||||||
const appState = {
|
await copyToClipboard(opts.elements, opts.files);
|
||||||
offsetTop: 0,
|
|
||||||
offsetLeft: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
...getDefaultAppState(),
|
|
||||||
...opts.appState,
|
|
||||||
};
|
|
||||||
await copyToClipboard(opts.elements, appState, opts.files);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid export type");
|
throw new Error("Invalid export type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ import {
|
|||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxHeight,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@@ -864,17 +864,21 @@ const drawElementFromCanvas = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
|
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
||||||
|
"true" &&
|
||||||
hasBoundTextElement(element)
|
hasBoundTextElement(element)
|
||||||
) {
|
) {
|
||||||
|
const textElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
) as ExcalidrawTextElementWithContainer;
|
||||||
const coords = getContainerCoords(element);
|
const coords = getContainerCoords(element);
|
||||||
context.strokeStyle = "#c92a2a";
|
context.strokeStyle = "#c92a2a";
|
||||||
context.lineWidth = 3;
|
context.lineWidth = 3;
|
||||||
context.strokeRect(
|
context.strokeRect(
|
||||||
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
||||||
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
||||||
getMaxContainerWidth(element) * window.devicePixelRatio,
|
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||||
getMaxContainerHeight(element) * window.devicePixelRatio,
|
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import { isTextElement, refreshTextDimensions } from "../element";
|
import { isTextElement, refreshTextDimensions } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
@@ -52,7 +53,7 @@ export class Fonts {
|
|||||||
let didUpdate = false;
|
let didUpdate = false;
|
||||||
|
|
||||||
this.scene.mapElements((element) => {
|
this.scene.mapElements((element) => {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element) && !isBoundToContainer(element)) {
|
||||||
invalidateShapeForElement(element);
|
invalidateShapeForElement(element);
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { render, waitFor, GlobalTestState } from "./test-utils";
|
import {
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
GlobalTestState,
|
||||||
|
createPasteEvent,
|
||||||
|
} from "./test-utils";
|
||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -9,6 +14,8 @@ import {
|
|||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import { NormalizedZoomValue } from "../types";
|
import { NormalizedZoomValue } from "../types";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import { copyToClipboard } from "../clipboard";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -35,38 +42,28 @@ const setClipboardText = (text: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPasteEvent = () => {
|
const sendPasteEvent = (text?: string) => {
|
||||||
const clipboardEvent = new Event("paste", {
|
const clipboardEvent = createPasteEvent(
|
||||||
bubbles: true,
|
text || (() => window.navigator.clipboard.readText()),
|
||||||
cancelable: true,
|
);
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set `clipboardData` properties.
|
|
||||||
// @ts-ignore
|
|
||||||
clipboardEvent.clipboardData = {
|
|
||||||
getData: () => window.navigator.clipboard.readText(),
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
document.dispatchEvent(clipboardEvent);
|
document.dispatchEvent(clipboardEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pasteWithCtrlCmdShiftV = () => {
|
const pasteWithCtrlCmdShiftV = (text?: string) => {
|
||||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||||
//triggering keydown with an empty clipboard
|
//triggering keydown with an empty clipboard
|
||||||
Keyboard.keyPress(KEYS.V);
|
Keyboard.keyPress(KEYS.V);
|
||||||
//triggering paste event with faked clipboard
|
//triggering paste event with faked clipboard
|
||||||
sendPasteEvent();
|
sendPasteEvent(text);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const pasteWithCtrlCmdV = () => {
|
const pasteWithCtrlCmdV = (text?: string) => {
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
//triggering keydown with an empty clipboard
|
//triggering keydown with an empty clipboard
|
||||||
Keyboard.keyPress(KEYS.V);
|
Keyboard.keyPress(KEYS.V);
|
||||||
//triggering paste event with faked clipboard
|
//triggering paste event with faked clipboard
|
||||||
sendPasteEvent();
|
sendPasteEvent(text);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +86,32 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("general paste behavior", () => {
|
||||||
|
it("should randomize seed on paste", async () => {
|
||||||
|
const rectangle = API.createElement({ type: "rectangle" });
|
||||||
|
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
|
||||||
|
|
||||||
|
pasteWithCtrlCmdV(clipboardJSON);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].seed).not.toBe(rectangle.seed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retain seed on shift-paste", async () => {
|
||||||
|
const rectangle = API.createElement({ type: "rectangle" });
|
||||||
|
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
|
||||||
|
|
||||||
|
// assert we don't randomize seed on shift-paste
|
||||||
|
pasteWithCtrlCmdShiftV(clipboardJSON);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].seed).toBe(rectangle.seed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("paste text as single lines", () => {
|
describe("paste text as single lines", () => {
|
||||||
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
|
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
|
||||||
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
|
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
|
||||||
|
|||||||
+7
-14
@@ -1,5 +1,10 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { GlobalTestState, render, waitFor } from "./test-utils";
|
import {
|
||||||
|
createPasteEvent,
|
||||||
|
GlobalTestState,
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
} from "./test-utils";
|
||||||
import { UI, Pointer } from "./helpers/ui";
|
import { UI, Pointer } from "./helpers/ui";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
|
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
|
||||||
@@ -680,19 +685,7 @@ describe("freedraw", () => {
|
|||||||
describe("image", () => {
|
describe("image", () => {
|
||||||
const createImage = async () => {
|
const createImage = async () => {
|
||||||
const sendPasteEvent = (file?: File) => {
|
const sendPasteEvent = (file?: File) => {
|
||||||
const clipboardEvent = new Event("paste", {
|
const clipboardEvent = createPasteEvent("", file ? [file] : []);
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set `clipboardData` properties.
|
|
||||||
// @ts-ignore
|
|
||||||
clipboardEvent.clipboardData = {
|
|
||||||
getData: () => window.navigator.clipboard.readText(),
|
|
||||||
files: [file],
|
|
||||||
};
|
|
||||||
|
|
||||||
document.dispatchEvent(clipboardEvent);
|
document.dispatchEvent(clipboardEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { resize, rotate } from "./utils";
|
|||||||
import {
|
import {
|
||||||
getBoundTextElementPosition,
|
getBoundTextElementPosition,
|
||||||
wrapText,
|
wrapText,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import * as textElementUtils from "../element/textElement";
|
import * as textElementUtils from "../element/textElement";
|
||||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
@@ -729,10 +729,11 @@ describe("Test Linear Elements", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
text: wrapText(text, font, getMaxContainerWidth(container)),
|
text: wrapText(text, font, getBoundTextMaxWidth(container)),
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 20,
|
height: 20,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
}) as ExcalidrawTextElementWithContainer;
|
}) as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
container = {
|
container = {
|
||||||
@@ -1108,9 +1109,17 @@ describe("Test Linear Elements", () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not render vertical align tool when element selected", () => {
|
it("should render vertical align tool when element selected only for two pointer arrow", () => {
|
||||||
createTwoPointerLinearElement("arrow");
|
let arrow = createTwoPointerLinearElement("arrow");
|
||||||
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
||||||
|
createBoundTextElement(DEFAULT_TEXT, arrow);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(queryByTestId(container, "align-top")).not.toBeNull();
|
||||||
|
expect(queryByTestId(container, "align-middle")).not.toBeNull();
|
||||||
|
expect(queryByTestId(container, "align-bottom")).not.toBeNull();
|
||||||
|
|
||||||
|
arrow = createThreePointerLinearElement("arrow");
|
||||||
|
|
||||||
createBoundTextElement(DEFAULT_TEXT, arrow);
|
createBoundTextElement(DEFAULT_TEXT, arrow);
|
||||||
API.setSelectedElements([arrow]);
|
API.setSelectedElements([arrow]);
|
||||||
@@ -1149,7 +1158,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(rect.x).toBe(400);
|
expect(rect.x).toBe(400);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(
|
expect(
|
||||||
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard collaboration
|
"Online whiteboard collaboration
|
||||||
made easy"
|
made easy"
|
||||||
@@ -1172,7 +1181,7 @@ describe("Test Linear Elements", () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
|
|||||||
@@ -190,3 +190,24 @@ export const toggleMenu = (container: HTMLElement) => {
|
|||||||
// open menu
|
// open menu
|
||||||
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
|
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createPasteEvent = (
|
||||||
|
text:
|
||||||
|
| string
|
||||||
|
| /* getData function */ ((type: string) => string | Promise<string>),
|
||||||
|
files?: File[],
|
||||||
|
) => {
|
||||||
|
return Object.assign(
|
||||||
|
new Event("paste", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
clipboardData: {
|
||||||
|
getData: typeof text === "string" ? () => text : text,
|
||||||
|
files: files || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+4
-4
@@ -29,9 +29,9 @@ import { isOverScrollBars } from "./scene";
|
|||||||
import { MaybeTransformHandleType } from "./element/transformHandles";
|
import { MaybeTransformHandleType } from "./element/transformHandles";
|
||||||
import Library from "./data/library";
|
import Library from "./data/library";
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
import { Merge, ForwardRef } from "./utility-types";
|
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
@@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" };
|
|||||||
|
|
||||||
export type BinaryFileData = {
|
export type BinaryFileData = {
|
||||||
mimeType:
|
mimeType:
|
||||||
| typeof ALLOWED_IMAGE_MIME_TYPES[number]
|
| ValueOf<typeof IMAGE_MIME_TYPES>
|
||||||
// future user or unknown file type
|
// future user or unknown file type
|
||||||
| typeof MIME_TYPES.binary;
|
| typeof MIME_TYPES.binary;
|
||||||
id: FileId;
|
id: FileId;
|
||||||
@@ -419,7 +419,7 @@ export type AppClassProperties = {
|
|||||||
FileId,
|
FileId,
|
||||||
{
|
{
|
||||||
image: HTMLImageElement | Promise<HTMLImageElement>;
|
image: HTMLImageElement | Promise<HTMLImageElement>;
|
||||||
mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number];
|
mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
|
|||||||
@@ -10601,9 +10601,9 @@ webpack-sources@^3.2.3:
|
|||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@^5.64.4:
|
webpack@^5.64.4:
|
||||||
version "5.75.0"
|
version "5.76.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
|
||||||
integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==
|
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.3"
|
"@types/eslint-scope" "^3.7.3"
|
||||||
"@types/estree" "^0.0.51"
|
"@types/estree" "^0.0.51"
|
||||||
|
|||||||
Reference in New Issue
Block a user