Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c23fe8653 | ||
|
|
2a4799d8c8 | ||
|
|
c4445c181b | ||
|
|
d12a9fdd40 | ||
|
|
9368a9ce3e | ||
|
|
851b9b7aec | ||
|
|
5ddb28d378 | ||
|
|
404a79e241 | ||
|
|
eea30da05a | ||
|
|
98a77d7426 | ||
|
|
ff3c2e5a16 | ||
|
|
b64beaf5ba | ||
|
|
89304c9f66 | ||
|
|
1d0653ce50 | ||
|
|
95e796840a | ||
|
|
7098013671 |
@@ -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();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getFontString } from "../utils";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
@@ -51,10 +50,11 @@ export const actionUnbindText = register({
|
|||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureText(
|
const { width, height, baseline } = measureTextElement(
|
||||||
boundTextElement.originalText,
|
boundTextElement,
|
||||||
getFontString(boundTextElement),
|
{
|
||||||
boundTextElement.lineHeight,
|
text: boundTextElement.originalText,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||||
element.id,
|
element.id,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2744,6 +2744,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) {
|
||||||
|
|||||||
+8
-8
@@ -31,14 +31,14 @@ import {
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { bumpVersion } from "../element/mutateElement";
|
import { bumpVersion } from "../element/mutateElement";
|
||||||
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { MarkOptional, Mutable } from "../utility-types";
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
measureBaseline,
|
measureTextElement,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
@@ -80,7 +80,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
||||||
|
subtype?: ExcalidrawElement["subtype"];
|
||||||
customData?: ExcalidrawElement["customData"];
|
customData?: ExcalidrawElement["customData"];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||||
@@ -143,6 +144,9 @@ const restoreElementWithProperties = <
|
|||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ("subtype" in element) {
|
||||||
|
base.subtype = element.subtype;
|
||||||
|
}
|
||||||
if ("customData" in element) {
|
if ("customData" in element) {
|
||||||
base.customData = element.customData;
|
base.customData = element.customData;
|
||||||
}
|
}
|
||||||
@@ -188,11 +192,7 @@ const restoreElement = (
|
|||||||
: // no element height likely means programmatic use, so default
|
: // no element height likely means programmatic use, so default
|
||||||
// to a fixed line height
|
// to a fixed line height
|
||||||
getDefaultLineHeight(element.fontFamily));
|
getDefaultLineHeight(element.fontFamily));
|
||||||
const baseline = measureBaseline(
|
const baseline = measureTextElement(element, { text }).baseline;
|
||||||
element.text,
|
|
||||||
getFontString(element),
|
|
||||||
lineHeight,
|
|
||||||
);
|
|
||||||
element = restoreElementWithProperties(element, {
|
element = restoreElementWithProperties(element, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
|
|||||||
+17
-22
@@ -13,12 +13,7 @@ import {
|
|||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||||
arrayToMap,
|
|
||||||
getFontString,
|
|
||||||
getUpdatedTimestamp,
|
|
||||||
isTestEnv,
|
|
||||||
} from "../utils";
|
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { mutateElement, newElementWith } from "./mutateElement";
|
import { mutateElement, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
@@ -30,9 +25,9 @@ import {
|
|||||||
getBoundTextElementOffset,
|
getBoundTextElementOffset,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureText,
|
measureTextElement,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapTextElement,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
@@ -58,6 +53,8 @@ type ElementConstructorOpts = MarkOptional<
|
|||||||
| "version"
|
| "version"
|
||||||
| "versionNonce"
|
| "versionNonce"
|
||||||
| "link"
|
| "link"
|
||||||
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
| "strokeStyle"
|
| "strokeStyle"
|
||||||
| "fillStyle"
|
| "fillStyle"
|
||||||
| "strokeColor"
|
| "strokeColor"
|
||||||
@@ -167,10 +164,12 @@ export const newTextElement = (
|
|||||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
|
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const text = normalizeText(opts.text);
|
||||||
const metrics = measureText(
|
const metrics = measureTextElement(
|
||||||
text,
|
{ ...opts, fontSize, fontFamily, lineHeight },
|
||||||
getFontString({ fontFamily, fontSize }),
|
{
|
||||||
lineHeight,
|
text,
|
||||||
|
customData: opts.customData,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
||||||
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
||||||
@@ -217,7 +216,9 @@ const getAdjustedDimensions = (
|
|||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
baseline: nextBaseline,
|
baseline: nextBaseline,
|
||||||
} = measureText(nextText, getFontString(element), element.lineHeight);
|
} = measureTextElement(element, {
|
||||||
|
text: nextText,
|
||||||
|
});
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
let x: number;
|
let x: number;
|
||||||
let y: number;
|
let y: number;
|
||||||
@@ -226,11 +227,7 @@ const getAdjustedDimensions = (
|
|||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(
|
const prevMetrics = measureTextElement(element);
|
||||||
element.text,
|
|
||||||
getFontString(element),
|
|
||||||
element.lineHeight,
|
|
||||||
);
|
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
@@ -307,11 +304,9 @@ export const refreshTextDimensions = (
|
|||||||
}
|
}
|
||||||
const container = getContainerElement(textElement);
|
const container = getContainerElement(textElement);
|
||||||
if (container) {
|
if (container) {
|
||||||
text = wrapText(
|
text = wrapTextElement(textElement, getMaxContainerWidth(container), {
|
||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
});
|
||||||
getMaxContainerWidth(container),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, text);
|
const dimensions = getAdjustedDimensions(textElement, text);
|
||||||
return { text, ...dimensions };
|
return { text, ...dimensions };
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
|
parseTokens,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
@@ -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", () => {
|
||||||
|
|||||||
+77
-29
@@ -1,3 +1,4 @@
|
|||||||
|
import { getSubtypeMethods, SubtypeMethods } from "../subtypes";
|
||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -34,6 +35,30 @@ import {
|
|||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
import { ExtractSetType } from "../utility-types";
|
import { ExtractSetType } from "../utility-types";
|
||||||
|
|
||||||
|
export const measureTextElement = function (element, next) {
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.measureText) {
|
||||||
|
return map.measureText(element, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontSize = next?.fontSize ?? element.fontSize;
|
||||||
|
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||||
|
const text = next?.text ?? element.text;
|
||||||
|
return measureText(text, font, element.lineHeight);
|
||||||
|
} as SubtypeMethods["measureText"];
|
||||||
|
|
||||||
|
export const wrapTextElement = function (element, containerWidth, next) {
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.wrapText) {
|
||||||
|
return map.wrapText(element, containerWidth, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontSize = next?.fontSize ?? element.fontSize;
|
||||||
|
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||||
|
const text = next?.text ?? element.originalText;
|
||||||
|
return wrapText(text, font, containerWidth);
|
||||||
|
} as SubtypeMethods["wrapText"];
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
text
|
text
|
||||||
@@ -66,22 +91,24 @@ export const redrawTextBoundingBox = (
|
|||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getMaxContainerWidth(container);
|
||||||
boundTextUpdates.text = wrapText(
|
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
||||||
textElement.originalText,
|
|
||||||
getFontString(textElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
const metrics = measureTextElement(textElement, {
|
||||||
boundTextUpdates.text,
|
text: boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
});
|
||||||
textElement.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
boundTextUpdates.width = metrics.width;
|
boundTextUpdates.width = metrics.width;
|
||||||
boundTextUpdates.height = metrics.height;
|
boundTextUpdates.height = metrics.height;
|
||||||
boundTextUpdates.baseline = metrics.baseline;
|
boundTextUpdates.baseline = metrics.baseline;
|
||||||
|
|
||||||
|
// Maintain coordX for non left-aligned text in case the width has changed
|
||||||
|
if (!container) {
|
||||||
|
if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
|
boundTextUpdates.x += textElement.width - metrics.width;
|
||||||
|
} else if (textElement.textAlign === TEXT_ALIGN.CENTER) {
|
||||||
|
boundTextUpdates.x += textElement.width / 2 - metrics.width / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (container) {
|
if (container) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const centerX = textElement.x + textElement.width / 2;
|
const centerX = textElement.x + textElement.width / 2;
|
||||||
@@ -189,17 +216,9 @@ export const handleBindTextResize = (
|
|||||||
let nextBaseLine = textElement.baseline;
|
let nextBaseLine = textElement.baseline;
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapText(
|
text = wrapTextElement(textElement, maxWidth);
|
||||||
textElement.originalText,
|
|
||||||
getFontString(textElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
const metrics = measureTextElement(textElement, { text });
|
||||||
text,
|
|
||||||
getFontString(textElement),
|
|
||||||
textElement.lineHeight,
|
|
||||||
);
|
|
||||||
nextHeight = metrics.height;
|
nextHeight = metrics.height;
|
||||||
nextWidth = metrics.width;
|
nextWidth = metrics.width;
|
||||||
nextBaseLine = metrics.baseline;
|
nextBaseLine = metrics.baseline;
|
||||||
@@ -419,6 +438,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 +481,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 +508,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 +529,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 +555,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
|
offsetX: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
@@ -64,7 +65,7 @@ const getTransform = (
|
|||||||
if (height > maxHeight && zoom.value !== 1) {
|
if (height > maxHeight && zoom.value !== 1) {
|
||||||
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
||||||
}
|
}
|
||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg) translate(${offsetX}px, 0px)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalContainerCache: {
|
const originalContainerCache: {
|
||||||
@@ -158,13 +159,30 @@ export const textWysiwyg = ({
|
|||||||
const container = getContainerElement(updatedTextElement);
|
const container = getContainerElement(updatedTextElement);
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedTextElement.height;
|
// Editing metrics
|
||||||
let textElementWidth = updatedTextElement.width;
|
const eMetrics = measureText(
|
||||||
|
container && updatedTextElement.containerId
|
||||||
|
? wrapText(
|
||||||
|
updatedTextElement.originalText,
|
||||||
|
getFontString(updatedTextElement),
|
||||||
|
getMaxContainerWidth(container),
|
||||||
|
)
|
||||||
|
: updatedTextElement.originalText,
|
||||||
|
getFontString(updatedTextElement),
|
||||||
|
updatedTextElement.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
let maxHeight = eMetrics.height;
|
||||||
|
let textElementWidth = Math.max(updatedTextElement.width, eMetrics.width);
|
||||||
// Set to element height by default since that's
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let textElementHeight = updatedTextElement.height;
|
let textElementHeight = Math.max(updatedTextElement.height, maxHeight);
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
|
textElementHeight = Math.min(
|
||||||
|
getMaxContainerHeight(container),
|
||||||
|
textElementHeight,
|
||||||
|
);
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const boundTextCoords =
|
const boundTextCoords =
|
||||||
LinearElementEditor.getBoundTextElementPosition(
|
LinearElementEditor.getBoundTextElementPosition(
|
||||||
@@ -173,6 +191,8 @@ export const textWysiwyg = ({
|
|||||||
);
|
);
|
||||||
coordX = boundTextCoords.x;
|
coordX = boundTextCoords.x;
|
||||||
coordY = boundTextCoords.y;
|
coordY = boundTextCoords.y;
|
||||||
|
} else {
|
||||||
|
coordX = Math.max(coordX, getContainerCoords(container).x);
|
||||||
}
|
}
|
||||||
const propertiesUpdated = textPropertiesUpdated(
|
const propertiesUpdated = textPropertiesUpdated(
|
||||||
updatedTextElement,
|
updatedTextElement,
|
||||||
@@ -186,7 +206,18 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
if (propertiesUpdated) {
|
if (propertiesUpdated) {
|
||||||
// update height of the editor after properties updated
|
// update height of the editor after properties updated
|
||||||
textElementHeight = updatedTextElement.height;
|
const font = getFontString(updatedTextElement);
|
||||||
|
textElementHeight =
|
||||||
|
updatedTextElement.lineHeight *
|
||||||
|
wrapText(
|
||||||
|
updatedTextElement.originalText,
|
||||||
|
font,
|
||||||
|
getMaxContainerWidth(container),
|
||||||
|
).split("\n").length;
|
||||||
|
textElementHeight = Math.max(
|
||||||
|
textElementHeight,
|
||||||
|
updatedTextElement.height,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let originalContainerData;
|
let originalContainerData;
|
||||||
@@ -266,12 +297,29 @@ export const textWysiwyg = ({
|
|||||||
editable.selectionEnd = editable.value.length - diff;
|
editable.selectionEnd = editable.value.length - diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transformWidth = updatedTextElement.width;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
} else {
|
} else {
|
||||||
textElementWidth += 0.5;
|
textElementWidth += 0.5;
|
||||||
|
transformWidth += 0.5;
|
||||||
}
|
}
|
||||||
|
// Horizontal offset in case updatedTextElement has a non-WYSIWYG subtype
|
||||||
|
const offWidth = container
|
||||||
|
? Math.min(
|
||||||
|
0,
|
||||||
|
updatedTextElement.width - Math.min(maxWidth, eMetrics.width),
|
||||||
|
)
|
||||||
|
: Math.min(maxWidth, updatedTextElement.width) -
|
||||||
|
Math.min(maxWidth, eMetrics.width);
|
||||||
|
const offsetX =
|
||||||
|
textAlign === "right"
|
||||||
|
? offWidth
|
||||||
|
: textAlign === "center"
|
||||||
|
? offWidth / 2
|
||||||
|
: 0;
|
||||||
|
const { width: w, height: h } = updatedTextElement;
|
||||||
|
|
||||||
let lineHeight = updatedTextElement.lineHeight;
|
let lineHeight = updatedTextElement.lineHeight;
|
||||||
|
|
||||||
@@ -290,13 +338,15 @@ export const textWysiwyg = ({
|
|||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight,
|
lineHeight,
|
||||||
width: `${textElementWidth}px`,
|
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${textElementHeight}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
|
transformOrigin: `${w / 2}px ${h / 2}px`,
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
textElementWidth,
|
offsetX,
|
||||||
textElementHeight,
|
transformWidth,
|
||||||
|
updatedTextElement.height,
|
||||||
getTextElementAngle(updatedTextElement),
|
getTextElementAngle(updatedTextElement),
|
||||||
appState,
|
appState,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Subtype } from "../subtypes";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import {
|
import {
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
@@ -64,6 +65,7 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
updated: number;
|
updated: number;
|
||||||
link: string | null;
|
link: string | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
subtype?: Subtype;
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -864,7 +864,8 @@ 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 coords = getContainerCoords(element);
|
const coords = getContainerCoords(element);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
|
||||||
|
|
||||||
|
// Subtype Names
|
||||||
|
export type Subtype = string;
|
||||||
|
|
||||||
|
// Subtype Methods
|
||||||
|
export type SubtypeMethods = {
|
||||||
|
measureText: (
|
||||||
|
element: Pick<
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
|
| "fontSize"
|
||||||
|
| "fontFamily"
|
||||||
|
| "text"
|
||||||
|
| "lineHeight"
|
||||||
|
>,
|
||||||
|
next?: {
|
||||||
|
fontSize?: number;
|
||||||
|
text?: string;
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
},
|
||||||
|
) => { width: number; height: number; baseline: number };
|
||||||
|
wrapText: (
|
||||||
|
element: Pick<
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
|
| "fontSize"
|
||||||
|
| "fontFamily"
|
||||||
|
| "originalText"
|
||||||
|
| "lineHeight"
|
||||||
|
>,
|
||||||
|
containerWidth: number,
|
||||||
|
next?: {
|
||||||
|
fontSize?: number;
|
||||||
|
text?: string;
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MethodMap = { subtype: Subtype; methods: Partial<SubtypeMethods> };
|
||||||
|
const methodMaps = [] as Array<MethodMap>;
|
||||||
|
|
||||||
|
// Use `getSubtypeMethods` to call subtype-specialized methods, like `render`.
|
||||||
|
export const getSubtypeMethods = (
|
||||||
|
subtype: Subtype | undefined,
|
||||||
|
): Partial<SubtypeMethods> | undefined => {
|
||||||
|
const map = methodMaps.find((method) => method.subtype === subtype);
|
||||||
|
return map?.methods;
|
||||||
|
};
|
||||||
@@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
|
|||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform-origin: 5px 12.5px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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