Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
commit
ef347cc685
@ -23,6 +23,11 @@ REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
|||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
# MATOMO
|
||||||
|
REACT_APP_MATOMO_URL=
|
||||||
|
REACT_APP_CDN_MATOMO_TRACKER_URL=
|
||||||
|
REACT_APP_MATOMO_SITE_ID=
|
||||||
|
|
||||||
#Debug flags
|
#Debug flags
|
||||||
|
|
||||||
# To enable bounding box for text containers
|
# To enable bounding box for text containers
|
||||||
|
@ -12,6 +12,13 @@ REACT_APP_WS_SERVER_URL=
|
|||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
# production-only vars
|
# production-only vars
|
||||||
|
# GOOGLE ANALYTICS
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
|
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
|
||||||
|
# MATOMO
|
||||||
|
REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/
|
||||||
|
REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js
|
||||||
|
REACT_APP_MATOMO_SITE_ID=1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
REACT_APP_PLUS_APP=https://app.excalidraw.com
|
REACT_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
An open source virtual hand-drawn style whiteboard. </br>
|
An open source virtual hand-drawn style whiteboard. </br>
|
||||||
Collaborative and end-to-end encrypted. </br>
|
Collaborative and end-to-end encrypted. </br>
|
||||||
<br />
|
<br />
|
||||||
</h3>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
# ref
|
# ref
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">createRef</a> | <a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a> | <a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">callbackRef</a> | <br/>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">resolvablePromise</a> } }
|
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">
|
||||||
|
createRef
|
||||||
|
</a>{" "}
|
||||||
|
|{" "}
|
||||||
|
<a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a>{" "}
|
||||||
|
|{" "}
|
||||||
|
<a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">
|
||||||
|
callbackRef
|
||||||
|
</a>{" "}
|
||||||
|
| <br />
|
||||||
|
{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
|
||||||
|
resolvablePromise
|
||||||
|
</a> } }
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
|
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
|
||||||
@ -139,7 +152,9 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: "500px" }}>
|
<div style={{ height: "500px" }}>
|
||||||
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
|
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
|
||||||
<button className="custom-button" onClick={updateScene}>Update Scene</button>
|
<button className="custom-button" onClick={updateScene}>
|
||||||
|
Update Scene
|
||||||
|
</button>
|
||||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -187,7 +202,8 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: "500px" }}>
|
<div style={{ height: "500px" }}>
|
||||||
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
|
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
|
||||||
<button className="custom-button"
|
<button
|
||||||
|
className="custom-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const libraryItems = [
|
const libraryItems = [
|
||||||
{
|
{
|
||||||
@ -205,10 +221,8 @@ function App() {
|
|||||||
];
|
];
|
||||||
excalidrawAPI.updateLibrary({
|
excalidrawAPI.updateLibrary({
|
||||||
libraryItems,
|
libraryItems,
|
||||||
openLibraryMenu: true
|
openLibraryMenu: true,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Update Library
|
Update Library
|
||||||
@ -250,7 +264,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
|
|||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
() =>{" "}
|
() =>{" "}
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
ExcalidrawElement[]
|
ExcalidrawElement[]
|
||||||
</a>
|
</a>
|
||||||
</pre>
|
</pre>
|
||||||
@ -261,7 +275,7 @@ Returns all the elements including the deleted in the scene.
|
|||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
() => NonDeleted<
|
() => NonDeleted<
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
</a>
|
</a>
|
||||||
[]>
|
[]>
|
||||||
@ -293,18 +307,31 @@ This is the history API. history.clear() will clear the history.
|
|||||||
## scrollToContent
|
## scrollToContent
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(target?:{" "}
|
(<br />
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
{" "}
|
||||||
|
target?:{" "}
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
|{" "}
|
|{" "}
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
</a>
|
</a>
|
||||||
[]) => void
|
[],
|
||||||
|
<br />
|
||||||
|
{" "}opts?: { fitToContent?: boolean; animate?: boolean; duration?: number
|
||||||
|
}
|
||||||
|
<br />) => void
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene.
|
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
|
||||||
|
|
||||||
|
| Attribute | type | default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| target | <code>ExcalidrawElement | ExcalidrawElement[]</code> | All scene elements | The element(s) to scroll to. |
|
||||||
|
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. |
|
||||||
|
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
|
||||||
|
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
|
||||||
|
|
||||||
## refresh
|
## refresh
|
||||||
|
|
||||||
@ -323,7 +350,7 @@ For any other cases if the position of excalidraw is updated (example due to scr
|
|||||||
This API can be used to show the toast with custom message.
|
This API can be used to show the toast with custom message.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
({ message: string, closable?:boolean,duration?:number
|
({ message: string, closable?:boolean,duration?:number
|
||||||
} | null) => void
|
} | null) => void
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -358,15 +385,18 @@ This API can be used to get the files present in the scene. It may contain files
|
|||||||
|
|
||||||
This API has the below signature. It sets the `tool` passed in param as the active tool.
|
This API has the below signature. It sets the `tool` passed in param as the active tool.
|
||||||
|
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(tool: <br/> { type: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">SHAPES</a>[number]["value"]| "eraser" } |<br/> { type: "custom"; customType: string }) => void
|
(tool: <br /> { type:{" "}
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">
|
||||||
|
SHAPES
|
||||||
|
</a>
|
||||||
|
[number]["value"]| "eraser" } |
|
||||||
|
<br /> { type: "custom"; customType: string }) => void
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## setCursor
|
## setCursor
|
||||||
|
|
||||||
This API can be used to customise the mouse cursor on the canvas and has the below signature.
|
This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param.
|
||||||
It sets the mouse cursor to the cursor passed in param.
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
(cursor: string) => void
|
(cursor: string) => void
|
||||||
|
@ -7542,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
|
|||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@^5.73.0:
|
webpack@^5.73.0:
|
||||||
version "5.74.0"
|
version "5.76.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
|
||||||
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
|
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"
|
||||||
|
@ -147,8 +147,10 @@
|
|||||||
// setting this so that libraries installation reuses this window tab.
|
// setting this so that libraries installation reuses this window tab.
|
||||||
window.name = "_excalidraw";
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true' &&
|
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
|
||||||
process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
|
||||||
|
<!-- LEGACY GOOGLE ANALYTICS -->
|
||||||
|
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
||||||
<script
|
<script
|
||||||
async
|
async
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
|
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
|
||||||
@ -161,6 +163,33 @@
|
|||||||
gtag("js", new Date());
|
gtag("js", new Date());
|
||||||
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
|
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
|
||||||
</script>
|
</script>
|
||||||
|
<% } %>
|
||||||
|
<!-- end LEGACY GOOGLE ANALYTICS -->
|
||||||
|
|
||||||
|
<!-- Matomo -->
|
||||||
|
<% if (process.env.REACT_APP_MATOMO_URL &&
|
||||||
|
process.env.REACT_APP_MATOMO_SITE_ID &&
|
||||||
|
process.env.REACT_APP_CDN_MATOMO_TRACKER_URL) { %>
|
||||||
|
<script>
|
||||||
|
var _paq = (window._paq = window._paq || []);
|
||||||
|
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||||
|
_paq.push(["trackPageView"]);
|
||||||
|
_paq.push(["enableLinkTracking"]);
|
||||||
|
(function () {
|
||||||
|
var u = "%REACT_APP_MATOMO_URL%";
|
||||||
|
_paq.push(["setTrackerUrl", u + "matomo.php"]);
|
||||||
|
_paq.push(["setSiteId", "%REACT_APP_MATOMO_SITE_ID%"]);
|
||||||
|
var d = document,
|
||||||
|
g = d.createElement("script"),
|
||||||
|
s = d.getElementsByTagName("script")[0];
|
||||||
|
g.async = true;
|
||||||
|
g.src = "%REACT_APP_CDN_MATOMO_TRACKER_URL%";
|
||||||
|
s.parentNode.insertBefore(g, s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
<!-- end Matomo analytics -->
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { AppState } from "../types";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
@ -182,99 +183,110 @@ export const actionCreateContainerFromText = register({
|
|||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
|
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
||||||
|
return selectedElements.length > 0 && areTextElements;
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
const updatedElements = elements.slice();
|
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||||
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
const containerIds: AppState["selectedElementIds"] = {};
|
||||||
const textElement = selectedElements[0];
|
|
||||||
const container = newElement({
|
|
||||||
type: "rectangle",
|
|
||||||
backgroundColor: appState.currentItemBackgroundColor,
|
|
||||||
boundElements: [
|
|
||||||
...(textElement.boundElements || []),
|
|
||||||
{ id: textElement.id, type: "text" },
|
|
||||||
],
|
|
||||||
angle: textElement.angle,
|
|
||||||
fillStyle: appState.currentItemFillStyle,
|
|
||||||
strokeColor: appState.currentItemStrokeColor,
|
|
||||||
roughness: appState.currentItemRoughness,
|
|
||||||
strokeWidth: appState.currentItemStrokeWidth,
|
|
||||||
strokeStyle: appState.currentItemStrokeStyle,
|
|
||||||
roundness:
|
|
||||||
appState.currentItemRoundness === "round"
|
|
||||||
? {
|
|
||||||
type: isUsingAdaptiveRadius("rectangle")
|
|
||||||
? ROUNDNESS.ADAPTIVE_RADIUS
|
|
||||||
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
opacity: 100,
|
|
||||||
locked: false,
|
|
||||||
x: textElement.x - BOUND_TEXT_PADDING,
|
|
||||||
y: textElement.y - BOUND_TEXT_PADDING,
|
|
||||||
width: computeContainerDimensionForBoundText(
|
|
||||||
textElement.width,
|
|
||||||
"rectangle",
|
|
||||||
),
|
|
||||||
height: computeContainerDimensionForBoundText(
|
|
||||||
textElement.height,
|
|
||||||
"rectangle",
|
|
||||||
),
|
|
||||||
groupIds: textElement.groupIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
// update bindings
|
for (const textElement of selectedElements) {
|
||||||
if (textElement.boundElements?.length) {
|
if (isTextElement(textElement)) {
|
||||||
const linearElementIds = textElement.boundElements
|
const container = newElement({
|
||||||
.filter((ele) => ele.type === "arrow")
|
type: "rectangle",
|
||||||
.map((el) => el.id);
|
backgroundColor: appState.currentItemBackgroundColor,
|
||||||
const linearElements = updatedElements.filter((ele) =>
|
boundElements: [
|
||||||
linearElementIds.includes(ele.id),
|
...(textElement.boundElements || []),
|
||||||
) as ExcalidrawLinearElement[];
|
{ id: textElement.id, type: "text" },
|
||||||
linearElements.forEach((ele) => {
|
],
|
||||||
let startBinding = null;
|
angle: textElement.angle,
|
||||||
let endBinding = null;
|
fillStyle: appState.currentItemFillStyle,
|
||||||
if (ele.startBinding) {
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
startBinding = { ...ele.startBinding, elementId: container.id };
|
roughness: appState.currentItemRoughness,
|
||||||
}
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
if (ele.endBinding) {
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
endBinding = { ...ele.endBinding, elementId: container.id };
|
roundness:
|
||||||
}
|
appState.currentItemRoundness === "round"
|
||||||
mutateElement(ele, { startBinding, endBinding });
|
? {
|
||||||
|
type: isUsingAdaptiveRadius("rectangle")
|
||||||
|
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||||
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
opacity: 100,
|
||||||
|
locked: false,
|
||||||
|
x: textElement.x - BOUND_TEXT_PADDING,
|
||||||
|
y: textElement.y - BOUND_TEXT_PADDING,
|
||||||
|
width: computeContainerDimensionForBoundText(
|
||||||
|
textElement.width,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
height: computeContainerDimensionForBoundText(
|
||||||
|
textElement.height,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
groupIds: textElement.groupIds,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(textElement, {
|
// update bindings
|
||||||
containerId: container.id,
|
if (textElement.boundElements?.length) {
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
const linearElementIds = textElement.boundElements
|
||||||
boundElements: null,
|
.filter((ele) => ele.type === "arrow")
|
||||||
});
|
.map((el) => el.id);
|
||||||
redrawTextBoundingBox(textElement, container);
|
const linearElements = updatedElements.filter((ele) =>
|
||||||
|
linearElementIds.includes(ele.id),
|
||||||
|
) as ExcalidrawLinearElement[];
|
||||||
|
linearElements.forEach((ele) => {
|
||||||
|
let startBinding = ele.startBinding;
|
||||||
|
let endBinding = ele.endBinding;
|
||||||
|
|
||||||
return {
|
if (startBinding?.elementId === textElement.id) {
|
||||||
elements: pushContainerBelowText(
|
startBinding = {
|
||||||
[...elements, container],
|
...startBinding,
|
||||||
|
elementId: container.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endBinding?.elementId === textElement.id) {
|
||||||
|
endBinding = { ...endBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startBinding || endBinding) {
|
||||||
|
mutateElement(ele, { startBinding, endBinding }, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(
|
||||||
|
textElement,
|
||||||
|
{
|
||||||
|
containerId: container.id,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
boundElements: null,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
redrawTextBoundingBox(textElement, container);
|
||||||
|
|
||||||
|
updatedElements = pushContainerBelowText(
|
||||||
|
[...updatedElements, container],
|
||||||
container,
|
container,
|
||||||
textElement,
|
textElement,
|
||||||
),
|
);
|
||||||
appState: {
|
containerIds[container.id] = true;
|
||||||
...appState,
|
}
|
||||||
selectedElementIds: {
|
|
||||||
[container.id]: true,
|
|
||||||
[textElement.id]: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
commitToHistory: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: updatedElements,
|
elements: updatedElements,
|
||||||
appState,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: containerIds,
|
||||||
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -226,7 +226,7 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoomToFitElements = (
|
export const zoomToFitElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
zoomToSelection: boolean,
|
zoomToSelection: boolean,
|
||||||
|
@ -54,6 +54,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
|
|||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
@ -637,6 +638,7 @@ export const actionChangeFontFamily = register({
|
|||||||
oldElement,
|
oldElement,
|
||||||
{
|
{
|
||||||
fontFamily: value,
|
fontFamily: value,
|
||||||
|
lineHeight: getDefaultLineHeight(value),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||||
|
@ -12,7 +12,10 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getBoundTextElement } from "../element/textElement";
|
import {
|
||||||
|
getBoundTextElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
@ -92,12 +95,18 @@ export const actionPasteStyles = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isTextElement(newElement)) {
|
if (isTextElement(newElement)) {
|
||||||
|
const fontSize =
|
||||||
|
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
|
||||||
|
const fontFamily =
|
||||||
|
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
|
fontSize,
|
||||||
fontFamily:
|
fontFamily,
|
||||||
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
textAlign:
|
textAlign:
|
||||||
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
|
lineHeight:
|
||||||
|
elementStylesToCopyFrom.lineHeight ||
|
||||||
|
getDefaultLineHeight(fontFamily),
|
||||||
});
|
});
|
||||||
let container = null;
|
let container = null;
|
||||||
if (newElement.containerId) {
|
if (newElement.containerId) {
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
export const trackEvent =
|
export const trackEvent = (
|
||||||
typeof process !== "undefined" &&
|
category: string,
|
||||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
action: string,
|
||||||
typeof window !== "undefined" &&
|
label?: string,
|
||||||
window.gtag
|
value?: number,
|
||||||
? (category: string, action: string, label?: string, value?: number) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
window.gtag("event", action, {
|
// Uncomment the next line to track locally
|
||||||
event_category: category,
|
// console.log("Track Event", { category, action, label, value });
|
||||||
event_label: label,
|
|
||||||
value,
|
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
|
||||||
});
|
return;
|
||||||
} catch (error) {
|
}
|
||||||
console.error("error logging to ga", error);
|
|
||||||
}
|
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) {
|
||||||
}
|
window.gtag("event", action, {
|
||||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
event_category: category,
|
||||||
? (category: string, action: string, label?: string, value?: number) => {}
|
event_label: label,
|
||||||
: (category: string, action: string, label?: string, value?: number) => {
|
value,
|
||||||
// Uncomment the next line to track locally
|
});
|
||||||
// console.log("Track Event", { category, action, label, value });
|
}
|
||||||
};
|
|
||||||
|
// MATOMO event tracking _paq must be same as the one in index.html
|
||||||
|
if (window._paq) {
|
||||||
|
window._paq.push(["trackEvent", category, action, label, value]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error during analytics", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -230,6 +230,7 @@ import {
|
|||||||
updateActiveTool,
|
updateActiveTool,
|
||||||
getShortcutKey,
|
getShortcutKey,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
|
easeToValuesRAF,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@ -269,13 +270,14 @@ import throttle from "lodash.throttle";
|
|||||||
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
||||||
import {
|
import {
|
||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getApproxLineHeight,
|
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
getTextBindableContainerAtPosition,
|
getTextBindableContainerAtPosition,
|
||||||
isMeasureTextSupported,
|
isMeasureTextSupported,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
@ -292,7 +294,10 @@ import {
|
|||||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||||
import { Fonts } from "../scene/Fonts";
|
import { Fonts } from "../scene/Fonts";
|
||||||
import { actionPaste } from "../actions/actionClipboard";
|
import { actionPaste } from "../actions/actionClipboard";
|
||||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
import {
|
||||||
|
actionToggleHandTool,
|
||||||
|
zoomToFitElements,
|
||||||
|
} from "../actions/actionCanvas";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
||||||
@ -1793,12 +1798,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(acc: ExcalidrawTextElement[], line, idx) => {
|
(acc: ExcalidrawTextElement[], line, idx) => {
|
||||||
const text = line.trim();
|
const text = line.trim();
|
||||||
|
|
||||||
|
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
||||||
if (text.length) {
|
if (text.length) {
|
||||||
const element = newTextElement({
|
const element = newTextElement({
|
||||||
...textElementProps,
|
...textElementProps,
|
||||||
x,
|
x,
|
||||||
y: currentY,
|
y: currentY,
|
||||||
text,
|
text,
|
||||||
|
lineHeight,
|
||||||
});
|
});
|
||||||
acc.push(element);
|
acc.push(element);
|
||||||
currentY += element.height + LINE_GAP;
|
currentY += element.height + LINE_GAP;
|
||||||
@ -1807,14 +1814,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// add paragraph only if previous line was not empty, IOW don't add
|
// add paragraph only if previous line was not empty, IOW don't add
|
||||||
// more than one empty line
|
// more than one empty line
|
||||||
if (prevLine) {
|
if (prevLine) {
|
||||||
const defaultLineHeight = getApproxLineHeight(
|
currentY +=
|
||||||
getFontString({
|
getLineHeightInPx(textElementProps.fontSize, lineHeight) +
|
||||||
fontSize: textElementProps.fontSize,
|
LINE_GAP;
|
||||||
fontFamily: textElementProps.fontFamily,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
currentY += defaultLineHeight + LINE_GAP;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1907,18 +1909,89 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.actionManager.executeAction(actionToggleHandTool);
|
this.actionManager.executeAction(actionToggleHandTool);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zooms on canvas viewport center
|
||||||
|
*/
|
||||||
|
zoomCanvas = (
|
||||||
|
/** decimal fraction between 0.1 (10% zoom) and 30 (3000% zoom) */
|
||||||
|
value: number,
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
...getStateForZoom(
|
||||||
|
{
|
||||||
|
viewportX: this.state.width / 2 + this.state.offsetLeft,
|
||||||
|
viewportY: this.state.height / 2 + this.state.offsetTop,
|
||||||
|
nextZoom: getNormalizedZoom(value),
|
||||||
|
},
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private cancelInProgresAnimation: (() => void) | null = null;
|
||||||
|
|
||||||
scrollToContent = (
|
scrollToContent = (
|
||||||
target:
|
target:
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
||||||
|
opts?: { fitToContent?: boolean; animate?: boolean; duration?: number },
|
||||||
) => {
|
) => {
|
||||||
this.setState({
|
this.cancelInProgresAnimation?.();
|
||||||
...calculateScrollCenter(
|
|
||||||
Array.isArray(target) ? target : [target],
|
// convert provided target into ExcalidrawElement[] if necessary
|
||||||
this.state,
|
const targets = Array.isArray(target) ? target : [target];
|
||||||
this.canvas,
|
|
||||||
),
|
let zoom = this.state.zoom;
|
||||||
});
|
let scrollX = this.state.scrollX;
|
||||||
|
let scrollY = this.state.scrollY;
|
||||||
|
|
||||||
|
if (opts?.fitToContent) {
|
||||||
|
// compute an appropriate viewport location (scroll X, Y) and zoom level
|
||||||
|
// that fit the target elements on the scene
|
||||||
|
const { appState } = zoomToFitElements(targets, this.state, false);
|
||||||
|
zoom = appState.zoom;
|
||||||
|
scrollX = appState.scrollX;
|
||||||
|
scrollY = appState.scrollY;
|
||||||
|
} else {
|
||||||
|
// compute only the viewport location, without any zoom adjustment
|
||||||
|
const scroll = calculateScrollCenter(targets, this.state, this.canvas);
|
||||||
|
scrollX = scroll.scrollX;
|
||||||
|
scrollY = scroll.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when animating, we use RequestAnimationFrame to prevent the animation
|
||||||
|
// from slowing down other processes
|
||||||
|
if (opts?.animate) {
|
||||||
|
const origScrollX = this.state.scrollX;
|
||||||
|
const origScrollY = this.state.scrollY;
|
||||||
|
|
||||||
|
// zoom animation could become problematic on scenes with large number
|
||||||
|
// of elements, setting it to its final value to improve user experience.
|
||||||
|
//
|
||||||
|
// using zoomCanvas() to zoom on current viewport center
|
||||||
|
this.zoomCanvas(zoom.value);
|
||||||
|
|
||||||
|
const cancel = easeToValuesRAF(
|
||||||
|
[origScrollX, origScrollY],
|
||||||
|
[scrollX, scrollY],
|
||||||
|
(scrollX, scrollY) => this.setState({ scrollX, scrollY }),
|
||||||
|
{ duration: opts?.duration ?? 500 },
|
||||||
|
);
|
||||||
|
this.cancelInProgresAnimation = () => {
|
||||||
|
cancel();
|
||||||
|
this.cancelInProgresAnimation = null;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.setState({ scrollX, scrollY, zoom });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** use when changing scrollX/scrollY/zoom based on user interaction */
|
||||||
|
private translateCanvas: React.Component<any, AppState>["setState"] = (
|
||||||
|
state,
|
||||||
|
) => {
|
||||||
|
this.cancelInProgresAnimation?.();
|
||||||
|
this.setState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
setToast = (
|
setToast = (
|
||||||
@ -2119,9 +2192,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
offset = -offset;
|
offset = -offset;
|
||||||
}
|
}
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.setState((state) => ({ scrollX: state.scrollX + offset }));
|
this.translateCanvas((state) => ({
|
||||||
|
scrollX: state.scrollX + offset,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.setState((state) => ({ scrollY: state.scrollY + offset }));
|
this.translateCanvas((state) => ({
|
||||||
|
scrollY: state.scrollY + offset,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2669,6 +2746,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontFamily =
|
||||||
|
existingTextElement?.fontFamily || this.state.currentItemFontFamily;
|
||||||
|
|
||||||
|
const lineHeight =
|
||||||
|
existingTextElement?.lineHeight || getDefaultLineHeight(fontFamily);
|
||||||
|
const fontSize = this.state.currentItemFontSize;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!existingTextElement &&
|
!existingTextElement &&
|
||||||
shouldBindToContainer &&
|
shouldBindToContainer &&
|
||||||
@ -2676,11 +2760,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isArrowElement(container)
|
!isArrowElement(container)
|
||||||
) {
|
) {
|
||||||
const fontString = {
|
const fontString = {
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily,
|
||||||
};
|
};
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(fontString));
|
const minWidth = getApproxMinLineWidth(
|
||||||
const minHeight = getApproxMinLineHeight(getFontString(fontString));
|
getFontString(fontString),
|
||||||
|
lineHeight,
|
||||||
|
);
|
||||||
|
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
const newHeight = Math.max(containerDims.height, minHeight);
|
const newHeight = Math.max(containerDims.height, minHeight);
|
||||||
const newWidth = Math.max(containerDims.width, minWidth);
|
const newWidth = Math.max(containerDims.width, minWidth);
|
||||||
@ -2714,8 +2801,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
text: "",
|
text: "",
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily,
|
||||||
textAlign: parentCenterPosition
|
textAlign: parentCenterPosition
|
||||||
? "center"
|
? "center"
|
||||||
: this.state.currentItemTextAlign,
|
: this.state.currentItemTextAlign,
|
||||||
@ -2726,6 +2813,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
locked: false,
|
locked: false,
|
||||||
|
lineHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingTextElement && shouldBindToContainer && container) {
|
if (!existingTextElement && shouldBindToContainer && container) {
|
||||||
@ -2992,12 +3080,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
this.translateCanvas({
|
||||||
zoom: zoomState.zoom,
|
zoom: zoomState.zoom,
|
||||||
scrollX: zoomState.scrollX + deltaX / nextZoom,
|
scrollX: zoomState.scrollX + deltaX / nextZoom,
|
||||||
scrollY: zoomState.scrollY + deltaY / nextZoom,
|
scrollY: zoomState.scrollY + deltaY / nextZoom,
|
||||||
shouldCacheIgnoreZoom: true,
|
shouldCacheIgnoreZoom: true,
|
||||||
};
|
});
|
||||||
});
|
});
|
||||||
this.resetShouldCacheIgnoreZoomDebounced();
|
this.resetShouldCacheIgnoreZoomDebounced();
|
||||||
} else {
|
} else {
|
||||||
@ -3773,7 +3861,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
window.addEventListener(EVENT.POINTER_UP, enableNextPaste);
|
window.addEventListener(EVENT.POINTER_UP, enableNextPaste);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.translateCanvas({
|
||||||
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
|
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
|
||||||
scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
|
scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
|
||||||
});
|
});
|
||||||
@ -4922,7 +5010,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (pointerDownState.scrollbars.isOverHorizontal) {
|
if (pointerDownState.scrollbars.isOverHorizontal) {
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const dx = x - pointerDownState.lastCoords.x;
|
const dx = x - pointerDownState.lastCoords.x;
|
||||||
this.setState({
|
this.translateCanvas({
|
||||||
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.x = x;
|
pointerDownState.lastCoords.x = x;
|
||||||
@ -4932,7 +5020,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (pointerDownState.scrollbars.isOverVertical) {
|
if (pointerDownState.scrollbars.isOverVertical) {
|
||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
const dy = y - pointerDownState.lastCoords.y;
|
const dy = y - pointerDownState.lastCoords.y;
|
||||||
this.setState({
|
this.translateCanvas({
|
||||||
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.y = y;
|
pointerDownState.lastCoords.y = y;
|
||||||
@ -6384,7 +6472,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// reduced amplification for small deltas (small movements on a trackpad)
|
// reduced amplification for small deltas (small movements on a trackpad)
|
||||||
Math.min(1, absDelta / 20);
|
Math.min(1, absDelta / 20);
|
||||||
|
|
||||||
this.setState((state) => ({
|
this.translateCanvas((state) => ({
|
||||||
...getStateForZoom(
|
...getStateForZoom(
|
||||||
{
|
{
|
||||||
viewportX: cursorX,
|
viewportX: cursorX,
|
||||||
@ -6401,14 +6489,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// scroll horizontally when shift pressed
|
// scroll horizontally when shift pressed
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.setState(({ zoom, scrollX }) => ({
|
this.translateCanvas(({ zoom, scrollX }) => ({
|
||||||
// on Mac, shift+wheel tends to result in deltaX
|
// on Mac, shift+wheel tends to result in deltaX
|
||||||
scrollX: scrollX - (deltaY || deltaX) / zoom.value,
|
scrollX: scrollX - (deltaY || deltaX) / zoom.value,
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(({ zoom, scrollX, scrollY }) => ({
|
this.translateCanvas(({ zoom, scrollX, scrollY }) => ({
|
||||||
scrollX: scrollX - deltaX / zoom.value,
|
scrollX: scrollX - deltaX / zoom.value,
|
||||||
scrollY: scrollY - deltaY / zoom.value,
|
scrollY: scrollY - deltaY / zoom.value,
|
||||||
}));
|
}));
|
||||||
|
@ -89,7 +89,9 @@ export const exportCanvas = async (
|
|||||||
return await fileSave(blob, {
|
return await fileSave(blob, {
|
||||||
description: "Export to PNG",
|
description: "Export to PNG",
|
||||||
name,
|
name,
|
||||||
extension: appState.exportEmbedScene ? "excalidraw.png" : "png",
|
// FIXME reintroduce `excalidraw.png` when most people upgrade away
|
||||||
|
// from 111.0.5563.64 (arm64), see #6349
|
||||||
|
extension: /* appState.exportEmbedScene ? "excalidraw.png" : */ "png",
|
||||||
fileHandle,
|
fileHandle,
|
||||||
});
|
});
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
|
@ -36,6 +36,7 @@ import { arrayToMap } from "../utils";
|
|||||||
import { isValidSubtype } from "../subtypes";
|
import { isValidSubtype } from "../subtypes";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { MarkOptional, Mutable } from "../utility-types";
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
|
import { detectLineHeight, getDefaultLineHeight } from "../element/textElement";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@ -170,17 +171,32 @@ const restoreElement = (
|
|||||||
const [fontPx, _fontFamily]: [string, string] = (
|
const [fontPx, _fontFamily]: [string, string] = (
|
||||||
element as any
|
element as any
|
||||||
).font.split(" ");
|
).font.split(" ");
|
||||||
fontSize = parseInt(fontPx, 10);
|
fontSize = parseFloat(fontPx);
|
||||||
fontFamily = getFontFamilyByName(_fontFamily);
|
fontFamily = getFontFamilyByName(_fontFamily);
|
||||||
}
|
}
|
||||||
|
const text = element.text ?? "";
|
||||||
|
|
||||||
element = restoreElementWithProperties(element, {
|
element = restoreElementWithProperties(element, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
text: element.text ?? "",
|
text,
|
||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: element.containerId ?? null,
|
containerId: element.containerId ?? null,
|
||||||
originalText: element.originalText || element.text,
|
originalText: element.originalText || text,
|
||||||
|
// line-height might not be specified either when creating elements
|
||||||
|
// programmatically, or when importing old diagrams.
|
||||||
|
// For the latter we want to detect the original line height which
|
||||||
|
// will likely differ from our per-font fixed line height we now use,
|
||||||
|
// to maintain backward compatibility.
|
||||||
|
lineHeight:
|
||||||
|
element.lineHeight ||
|
||||||
|
(element.height
|
||||||
|
? // detect line-height from current element height and font-size
|
||||||
|
detectLineHeight(element)
|
||||||
|
: // no element height likely means programmatic use, so default
|
||||||
|
// to a fixed line height
|
||||||
|
getDefaultLineHeight(element.fontFamily)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (refreshDimensions) {
|
if (refreshDimensions) {
|
||||||
@ -490,7 +506,9 @@ export const restoreAppState = (
|
|||||||
? {
|
? {
|
||||||
value: appState.zoom as NormalizedZoomValue,
|
value: appState.zoom as NormalizedZoomValue,
|
||||||
}
|
}
|
||||||
: appState.zoom || defaultAppState.zoom,
|
: appState.zoom?.value
|
||||||
|
? appState.zoom
|
||||||
|
: defaultAppState.zoom,
|
||||||
// when sidebar docked and user left it open in last session,
|
// when sidebar docked and user left it open in last session,
|
||||||
// keep it open. If not docked, keep it closed irrespective of last state.
|
// keep it open. If not docked, keep it closed irrespective of last state.
|
||||||
openSidebar:
|
openSidebar:
|
||||||
|
@ -786,7 +786,12 @@ export const findFocusPointForEllipse = (
|
|||||||
orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
|
orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
|
||||||
squares;
|
squares;
|
||||||
|
|
||||||
const n = (-m * px - 1) / py;
|
let n = (-m * px - 1) / py;
|
||||||
|
|
||||||
|
if (n === 0) {
|
||||||
|
// if zero {-0, 0}, fall back to a same-sign value in the similar range
|
||||||
|
n = (Object.is(n, -0) ? -1 : 1) * 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
|
const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
|
||||||
return GA.point(x, (-m * x - 1) / n);
|
return GA.point(x, (-m * x - 1) / n);
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
normalizeText,
|
normalizeText,
|
||||||
wrapTextElement,
|
wrapTextElement,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
@ -168,15 +169,20 @@ export const newTextElement = (
|
|||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId?: ExcalidrawTextContainer["id"];
|
containerId?: ExcalidrawTextContainer["id"];
|
||||||
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
const map = getSubtypeMethods(opts?.subtype);
|
const map = getSubtypeMethods(opts?.subtype);
|
||||||
map?.clean && map.clean(opts);
|
map?.clean && map.clean(opts);
|
||||||
|
const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const text = normalizeText(opts.text);
|
||||||
const metrics = measureTextElement(opts, {
|
const metrics = measureTextElement(
|
||||||
text,
|
{ ...opts, lineHeight },
|
||||||
customData: opts.customData,
|
{
|
||||||
});
|
text,
|
||||||
|
customData: opts.customData,
|
||||||
|
},
|
||||||
|
);
|
||||||
const offsets = getTextElementPositionOffsets(opts, metrics);
|
const offsets = getTextElementPositionOffsets(opts, metrics);
|
||||||
const textElement = newElementWith(
|
const textElement = newElementWith(
|
||||||
{
|
{
|
||||||
@ -192,6 +198,7 @@ export const newTextElement = (
|
|||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
|
lineHeight,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@ -220,9 +227,7 @@ const getAdjustedDimensions = (
|
|||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureTextElement(element, {
|
const prevMetrics = measureTextElement(element);
|
||||||
fontSize: element.fontSize,
|
|
||||||
});
|
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
|
@ -39,13 +39,13 @@ import {
|
|||||||
import { Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
|
getApproxMinLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
@ -360,7 +360,7 @@ export const resizeSingleElement = (
|
|||||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||||
|
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
let boundTextFontSize: number | null = null;
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
@ -410,9 +410,7 @@ export const resizeSingleElement = (
|
|||||||
boundTextElement.id,
|
boundTextElement.id,
|
||||||
) as typeof boundTextElement | undefined;
|
) as typeof boundTextElement | undefined;
|
||||||
if (stateOfBoundTextElementAtResize) {
|
if (stateOfBoundTextElementAtResize) {
|
||||||
boundTextFont = {
|
boundTextFontSize = stateOfBoundTextElementAtResize.fontSize;
|
||||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (shouldMaintainAspectRatio) {
|
if (shouldMaintainAspectRatio) {
|
||||||
const updatedElement = {
|
const updatedElement = {
|
||||||
@ -428,12 +426,16 @@ export const resizeSingleElement = (
|
|||||||
if (nextFontSize === null) {
|
if (nextFontSize === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boundTextFont = {
|
boundTextFontSize = nextFontSize;
|
||||||
fontSize: nextFontSize,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
const minWidth = getApproxMinLineWidth(
|
||||||
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
getFontString(boundTextElement),
|
||||||
|
boundTextElement.lineHeight,
|
||||||
|
);
|
||||||
|
const minHeight = getApproxMinLineHeight(
|
||||||
|
boundTextElement.fontSize,
|
||||||
|
boundTextElement.lineHeight,
|
||||||
|
);
|
||||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||||
}
|
}
|
||||||
@ -566,8 +568,10 @@ export const resizeSingleElement = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(element, resizedElement);
|
mutateElement(element, resizedElement);
|
||||||
if (boundTextElement && boundTextFont) {
|
if (boundTextElement && boundTextFontSize != null) {
|
||||||
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
mutateElement(boundTextElement, {
|
||||||
|
fontSize: boundTextFontSize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
handleBindTextResize(element, transformHandleDirection);
|
handleBindTextResize(element, transformHandleDirection);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
@ -6,6 +6,9 @@ import {
|
|||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
getMaxContainerHeight,
|
getMaxContainerHeight,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
detectLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
@ -40,9 +43,7 @@ describe("Test wrapText", () => {
|
|||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 80,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello \nwhats \nup`,
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break all characters when width of each character is less than container width",
|
desc: "break all characters when width of each character is less than container width",
|
||||||
@ -64,8 +65,7 @@ p`,
|
|||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
|
|
||||||
width: 140,
|
width: 140,
|
||||||
res: `Hello whats
|
res: `Hello whats \nup`,
|
||||||
up`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "fit the container",
|
desc: "fit the container",
|
||||||
@ -95,9 +95,7 @@ whats up`;
|
|||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 80,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello\nwhats \nup`,
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break all characters when width of each character is less than container width",
|
desc: "break all characters when width of each character is less than container width",
|
||||||
@ -143,11 +141,7 @@ whats up`,
|
|||||||
{
|
{
|
||||||
desc: "fit characters of long string as per container width",
|
desc: "fit characters of long string as per container width",
|
||||||
width: 170,
|
width: 170,
|
||||||
res: `hellolongtextth
|
res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
|
||||||
isiswhatsupwith
|
|
||||||
youIamtypingggg
|
|
||||||
gandtypinggg
|
|
||||||
break it now`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -166,8 +160,7 @@ now`,
|
|||||||
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
||||||
|
|
||||||
width: 600,
|
width: 600,
|
||||||
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
|
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
|
||||||
break it now`,
|
|
||||||
},
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should ${data.desc}`, () => {
|
it(`should ${data.desc}`, () => {
|
||||||
@ -181,8 +174,14 @@ break it now`,
|
|||||||
const text = "Hello Excalidraw";
|
const text = "Hello Excalidraw";
|
||||||
// Length of "Excalidraw" is 100 and exacty equal to max width
|
// Length of "Excalidraw" is 100 and exacty equal to max width
|
||||||
const res = wrapText(text, font, 100);
|
const res = wrapText(text, font, 100);
|
||||||
expect(res).toEqual(`Hello
|
expect(res).toEqual(`Hello \nExcalidraw`);
|
||||||
Excalidraw`);
|
});
|
||||||
|
|
||||||
|
it("should return the text as is if max width is invalid", () => {
|
||||||
|
const text = "Hello Excalidraw";
|
||||||
|
expect(wrapText(text, font, NaN)).toEqual(text);
|
||||||
|
expect(wrapText(text, font, -1)).toEqual(text);
|
||||||
|
expect(wrapText(text, font, Infinity)).toEqual(text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -305,3 +304,41 @@ describe("Test measureText", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const textElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 1,
|
||||||
|
height: 175,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test detectLineHeight", () => {
|
||||||
|
it("should return correct line height", () => {
|
||||||
|
expect(detectLineHeight(textElement)).toBe(1.25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getLineHeightInPx", () => {
|
||||||
|
it("should return correct line height", () => {
|
||||||
|
expect(
|
||||||
|
getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
|
||||||
|
).toBe(25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getDefaultLineHeight", () => {
|
||||||
|
it("should return line height using default font family when not passed", () => {
|
||||||
|
//@ts-ignore
|
||||||
|
expect(getDefaultLineHeight()).toBe(1.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return line height using default font family for unknown font", () => {
|
||||||
|
const UNKNOWN_FONT = 5;
|
||||||
|
expect(getDefaultLineHeight(UNKNOWN_FONT)).toBe(1.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct line height", () => {
|
||||||
|
expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
|
FONT_FAMILY,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
@ -41,7 +43,7 @@ export const measureTextElement = function (element, next) {
|
|||||||
const fontSize = next?.fontSize ?? element.fontSize;
|
const fontSize = next?.fontSize ?? element.fontSize;
|
||||||
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||||
const text = next?.text ?? element.text;
|
const text = next?.text ?? element.text;
|
||||||
return measureText(text, font);
|
return measureText(text, font, element.lineHeight);
|
||||||
} as SubtypeMethods["measureText"];
|
} as SubtypeMethods["measureText"];
|
||||||
|
|
||||||
export const wrapTextElement = function (element, containerWidth, next) {
|
export const wrapTextElement = function (element, containerWidth, next) {
|
||||||
@ -66,12 +68,15 @@ export const normalizeText = (text: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const splitIntoLines = (text: string) => {
|
||||||
|
return normalizeText(text).split("\n");
|
||||||
|
};
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
|
|
||||||
const boundTextUpdates = {
|
const boundTextUpdates = {
|
||||||
x: textElement.x,
|
x: textElement.x,
|
||||||
y: textElement.y,
|
y: textElement.y,
|
||||||
@ -285,32 +290,52 @@ const computeBoundTextPosition = (
|
|||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||||
|
|
||||||
export const measureText = (text: string, font: FontString) => {
|
export const measureText = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
text = text
|
text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, font);
|
const height = getTextHeight(text, fontSize, lineHeight);
|
||||||
const width = getTextWidth(text, font);
|
const width = getTextWidth(text, font);
|
||||||
|
|
||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
/**
|
||||||
const cacheApproxLineHeight: { [key: FontString]: number } = {};
|
* To get unitless line-height (if unknown) we can calculate it by dividing
|
||||||
|
* height-per-line by fontSize.
|
||||||
|
*/
|
||||||
|
export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
|
||||||
|
const lineCount = splitIntoLines(textElement.text).length;
|
||||||
|
return (textElement.height /
|
||||||
|
lineCount /
|
||||||
|
textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
|
||||||
|
};
|
||||||
|
|
||||||
export const getApproxLineHeight = (font: FontString) => {
|
/**
|
||||||
if (cacheApproxLineHeight[font]) {
|
* We calculate the line height from the font size and the unitless line height,
|
||||||
return cacheApproxLineHeight[font];
|
* aligning with the W3C spec.
|
||||||
}
|
*/
|
||||||
const fontSize = parseInt(font);
|
export const getLineHeightInPx = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return fontSize * lineHeight;
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate line height relative to font size
|
// FIXME rename to getApproxMinContainerHeight
|
||||||
cacheApproxLineHeight[font] = fontSize * 1.2;
|
export const getApproxMinLineHeight = (
|
||||||
return cacheApproxLineHeight[font];
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
@ -333,7 +358,7 @@ const getLineWidth = (text: string, font: FontString) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = splitIntoLines(text);
|
||||||
let width = 0;
|
let width = 0;
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
@ -341,13 +366,23 @@ export const getTextWidth = (text: string, font: FontString) => {
|
|||||||
return width;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextHeight = (text: string, font: FontString) => {
|
export const getTextHeight = (
|
||||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
text: string,
|
||||||
const lineHeight = getApproxLineHeight(font);
|
fontSize: number,
|
||||||
return lineHeight * lines.length;
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const lineCount = splitIntoLines(text).length;
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
|
// computation, we need to make sure we don't continue as we'll end up
|
||||||
|
// in an infinite loop
|
||||||
|
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
@ -485,21 +520,23 @@ export const charWidth = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
|
|
||||||
|
// FIXME rename to getApproxMinContainerWidth
|
||||||
|
export const getApproxMinLineWidth = (
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
if (maxCharWidth === 0) {
|
if (maxCharWidth === 0) {
|
||||||
return (
|
return (
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
||||||
BOUND_TEXT_PADDING * 2
|
BOUND_TEXT_PADDING * 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApproxMinLineHeight = (font: FontString) => {
|
|
||||||
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinCharWidth = (font: FontString) => {
|
export const getMinCharWidth = (font: FontString) => {
|
||||||
const cache = charWidth.getCache(font);
|
const cache = charWidth.getCache(font);
|
||||||
if (!cache) {
|
if (!cache) {
|
||||||
@ -845,3 +882,32 @@ export const isMeasureTextSupported = () => {
|
|||||||
);
|
);
|
||||||
return width > 0;
|
return width > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unitless line height
|
||||||
|
*
|
||||||
|
* In previous versions we used `normal` line height, which browsers interpret
|
||||||
|
* differently, and based on font-family and font-size.
|
||||||
|
*
|
||||||
|
* To make line heights consistent across browsers we hardcode the values for
|
||||||
|
* each of our fonts based on most common average line-heights.
|
||||||
|
* See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
|
||||||
|
* where the values come from.
|
||||||
|
*/
|
||||||
|
const DEFAULT_LINE_HEIGHT = {
|
||||||
|
// ~1.25 is the average for Virgil in WebKit and Blink.
|
||||||
|
// Gecko (FF) uses ~1.28.
|
||||||
|
[FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
// ~1.15 is the average for Virgil in WebKit and Blink.
|
||||||
|
// Gecko if all over the place.
|
||||||
|
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
// ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
|
||||||
|
[FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
||||||
|
if (fontFamily in DEFAULT_LINE_HEIGHT) {
|
||||||
|
return DEFAULT_LINE_HEIGHT[fontFamily];
|
||||||
|
}
|
||||||
|
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
||||||
|
};
|
||||||
|
@ -526,6 +526,44 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
|
||||||
|
const diamond = API.createElement({
|
||||||
|
type: "diamond",
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
});
|
||||||
|
h.elements = [diamond];
|
||||||
|
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].id).toBe(diamond.id);
|
||||||
|
|
||||||
|
API.setSelectedElements([diamond]);
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
const value = new Array(1000).fill("1").join("\n");
|
||||||
|
|
||||||
|
// Pasting large text to simulate height increase
|
||||||
|
expect(() =>
|
||||||
|
fireEvent.input(editor, { target: { value } }),
|
||||||
|
).not.toThrow();
|
||||||
|
|
||||||
|
expect(diamond.height).toBe(50020);
|
||||||
|
|
||||||
|
// Clearing text to simulate height decrease
|
||||||
|
expect(() =>
|
||||||
|
fireEvent.input(editor, { target: { value: "" } }),
|
||||||
|
).not.toThrow();
|
||||||
|
|
||||||
|
expect(diamond.height).toBe(70);
|
||||||
|
});
|
||||||
|
|
||||||
it("should bind text to container when double clicked on center of transparent container", async () => {
|
it("should bind text to container when double clicked on center of transparent container", async () => {
|
||||||
const rectangle = API.createElement({
|
const rectangle = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
@ -783,7 +821,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
);
|
);
|
||||||
expect(text.x).toBe(25);
|
expect(text.x).toBe(25);
|
||||||
expect(text.height).toBe(48);
|
expect(text.height).toBe(50);
|
||||||
expect(text.width).toBe(60);
|
expect(text.width).toBe(60);
|
||||||
|
|
||||||
// Edit and text by removing second line and it should
|
// Edit and text by removing second line and it should
|
||||||
@ -810,7 +848,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
expect(text.text).toBe("Hello");
|
expect(text.text).toBe("Hello");
|
||||||
expect(text.originalText).toBe("Hello");
|
expect(text.originalText).toBe("Hello");
|
||||||
expect(text.height).toBe(24);
|
expect(text.height).toBe(25);
|
||||||
expect(text.width).toBe(50);
|
expect(text.width).toBe(50);
|
||||||
expect(text.y).toBe(
|
expect(text.y).toBe(
|
||||||
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
@ -903,7 +941,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
85,
|
85,
|
||||||
5,
|
4.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -929,7 +967,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -1067,9 +1105,9 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
||||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||||
expect(rectangle.x).toBe(80);
|
expect(rectangle.x).toBe(80);
|
||||||
expect(rectangle.y).toBe(-35);
|
expect(rectangle.y).toBe(-40);
|
||||||
expect(text.x).toBe(85);
|
expect(text.x).toBe(85);
|
||||||
expect(text.y).toBe(-30);
|
expect(text.y).toBe(-35);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
Keyboard.keyPress(KEYS.Z);
|
Keyboard.keyPress(KEYS.Z);
|
||||||
@ -1112,7 +1150,7 @@ describe("textWysiwyg", () => {
|
|||||||
target: { value: "Online whiteboard collaboration made easy" },
|
target: { value: "Online whiteboard collaboration made easy" },
|
||||||
});
|
});
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(178);
|
expect(rectangle.height).toBe(185);
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@ -1181,9 +1219,42 @@ describe("textWysiwyg", () => {
|
|||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
||||||
).toEqual(36);
|
).toEqual(36);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(97);
|
||||||
96.39999999999999,
|
});
|
||||||
);
|
|
||||||
|
it("should update line height when font family updated", async () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
editor.blur();
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
|
).toEqual(1.25);
|
||||||
|
|
||||||
|
mouse.select(rectangle);
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
|
).toEqual(FONT_FAMILY.Cascadia);
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
|
).toEqual(1.2);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle(/normal/i));
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
|
).toEqual(FONT_FAMILY.Helvetica);
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
|
).toEqual(1.15);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should align correctly", () => {
|
describe("should align correctly", () => {
|
||||||
@ -1245,7 +1316,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
45.5,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1257,7 +1328,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
30,
|
30,
|
||||||
45.5,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1269,7 +1340,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
45,
|
45,
|
||||||
45.5,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1281,7 +1352,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1292,7 +1363,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
30,
|
30,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1303,7 +1374,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
45,
|
45,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1333,7 +1404,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(textElement.width).toBe(600);
|
expect(textElement.width).toBe(600);
|
||||||
expect(textElement.height).toBe(24);
|
expect(textElement.height).toBe(25);
|
||||||
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
||||||
expect((textElement as ExcalidrawTextElement).text).toBe(
|
expect((textElement as ExcalidrawTextElement).text).toBe(
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
@ -1365,7 +1436,7 @@ describe("textWysiwyg", () => {
|
|||||||
],
|
],
|
||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
height: 34,
|
height: 35,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
link: null,
|
link: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
@ -35,6 +34,7 @@ import {
|
|||||||
wrapText,
|
wrapText,
|
||||||
getMaxContainerHeight,
|
getMaxContainerHeight,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
|
computeContainerDimensionForBoundText,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
@ -160,9 +160,7 @@ export const textWysiwyg = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { textAlign, verticalAlign } = updatedTextElement;
|
const { textAlign, verticalAlign } = updatedTextElement;
|
||||||
const approxLineHeight = getApproxLineHeight(
|
|
||||||
getFontString(updatedTextElement),
|
|
||||||
);
|
|
||||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||||
let coordX = updatedTextElement.x;
|
let coordX = updatedTextElement.x;
|
||||||
let coordY = updatedTextElement.y;
|
let coordY = updatedTextElement.y;
|
||||||
@ -179,6 +177,7 @@ export const textWysiwyg = ({
|
|||||||
)
|
)
|
||||||
: updatedTextElement.originalText,
|
: updatedTextElement.originalText,
|
||||||
getFontString(updatedTextElement),
|
getFontString(updatedTextElement),
|
||||||
|
updatedTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
let maxHeight = eMetrics.height;
|
let maxHeight = eMetrics.height;
|
||||||
@ -217,7 +216,7 @@ export const textWysiwyg = ({
|
|||||||
// update height of the editor after properties updated
|
// update height of the editor after properties updated
|
||||||
const font = getFontString(updatedTextElement);
|
const font = getFontString(updatedTextElement);
|
||||||
textElementHeight =
|
textElementHeight =
|
||||||
getApproxLineHeight(font) *
|
updatedTextElement.lineHeight *
|
||||||
wrapText(
|
wrapText(
|
||||||
updatedTextElement.originalText,
|
updatedTextElement.originalText,
|
||||||
font,
|
font,
|
||||||
@ -250,11 +249,12 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
||||||
const diff = Math.min(
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
textElementHeight - maxHeight,
|
textElementHeight,
|
||||||
approxLineHeight,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: containerDims.height + diff });
|
|
||||||
|
mutateElement(container, { height: targetContainerHeight });
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
// autoshrink container height until original container height
|
// autoshrink container height until original container height
|
||||||
@ -263,11 +263,11 @@ export const textWysiwyg = ({
|
|||||||
containerDims.height > originalContainerData.height &&
|
containerDims.height > originalContainerData.height &&
|
||||||
textElementHeight < maxHeight
|
textElementHeight < maxHeight
|
||||||
) {
|
) {
|
||||||
const diff = Math.min(
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
maxHeight - textElementHeight,
|
textElementHeight,
|
||||||
approxLineHeight,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: containerDims.height - diff });
|
mutateElement(container, { height: targetContainerHeight });
|
||||||
}
|
}
|
||||||
// Start pushing text upward until a diff of 30px (padding)
|
// Start pushing text upward until a diff of 30px (padding)
|
||||||
// is reached
|
// is reached
|
||||||
@ -305,10 +305,6 @@ export const textWysiwyg = ({
|
|||||||
editable.selectionEnd = editable.value.length - diff;
|
editable.selectionEnd = editable.value.length - diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = updatedTextElement.originalText.split("\n");
|
|
||||||
const lineHeight = updatedTextElement.containerId
|
|
||||||
? approxLineHeight
|
|
||||||
: eMetrics.height / lines.length;
|
|
||||||
let transformWidth = updatedTextElement.width;
|
let transformWidth = updatedTextElement.width;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
@ -338,7 +334,7 @@ export const textWysiwyg = ({
|
|||||||
Object.assign(editable.style, {
|
Object.assign(editable.style, {
|
||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: `${lineHeight}px`,
|
lineHeight: element.lineHeight,
|
||||||
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${textElementHeight}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
@ -360,6 +356,7 @@ export const textWysiwyg = ({
|
|||||||
filter: "var(--theme-filter)",
|
filter: "var(--theme-filter)",
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
|
editable.scrollTop = 0;
|
||||||
// For some reason updating font attribute doesn't set font family
|
// For some reason updating font attribute doesn't set font family
|
||||||
// hence updating font family explicitly for test environment
|
// hence updating font family explicitly for test environment
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
@ -447,7 +444,11 @@ export const textWysiwyg = ({
|
|||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container!),
|
getMaxContainerWidth(container!),
|
||||||
);
|
);
|
||||||
const { width, height } = measureText(wrappedText, font);
|
const { width, height } = measureText(
|
||||||
|
wrappedText,
|
||||||
|
font,
|
||||||
|
updatedTextElement.lineHeight,
|
||||||
|
);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
editable.style.height = `${height}px`;
|
editable.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
|
@ -137,6 +137,11 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId: ExcalidrawGenericElement["id"] | null;
|
containerId: ExcalidrawGenericElement["id"] | null;
|
||||||
originalText: string;
|
originalText: string;
|
||||||
|
/**
|
||||||
|
* Unitless line height (aligned to W3C). To get line height in px, multiply
|
||||||
|
* with font size (using `getLineHeightInPx` helper).
|
||||||
|
*/
|
||||||
|
lineHeight: number & { _brand: "unitlessLineHeight" };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawBindableElement =
|
export type ExcalidrawBindableElement =
|
||||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -19,6 +19,8 @@ interface Window {
|
|||||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||||
gtag: Function;
|
gtag: Function;
|
||||||
|
_paq: any[];
|
||||||
|
_mtm: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanvasRenderingContext2D {
|
interface CanvasRenderingContext2D {
|
||||||
|
@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- [`ExcalidrawAPI.scrolToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
|
||||||
|
|
||||||
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)
|
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)
|
||||||
|
|
||||||
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
|
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
"terser-webpack-plugin": "5.3.3",
|
"terser-webpack-plugin": "5.3.3",
|
||||||
"ts-loader": "9.3.1",
|
"ts-loader": "9.3.1",
|
||||||
"typescript": "4.7.4",
|
"typescript": "4.7.4",
|
||||||
"webpack": "5.73.0",
|
"webpack": "5.76.0",
|
||||||
"webpack-bundle-analyzer": "4.5.0",
|
"webpack-bundle-analyzer": "4.5.0",
|
||||||
"webpack-cli": "4.10.0",
|
"webpack-cli": "4.10.0",
|
||||||
"webpack-dev-server": "4.9.3",
|
"webpack-dev-server": "4.9.3",
|
||||||
|
@ -1393,10 +1393,10 @@ acorn-walk@^8.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
|
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
|
||||||
integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
|
integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
|
||||||
|
|
||||||
acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0:
|
acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
|
||||||
version "8.7.1"
|
version "8.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
|
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||||
|
|
||||||
ajv-formats@^2.1.1:
|
ajv-formats@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
@ -2068,10 +2068,10 @@ encodeurl@~1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||||
|
|
||||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
|
enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
|
||||||
version "5.10.0"
|
version "5.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
|
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
|
||||||
integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
|
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs "^4.2.4"
|
graceful-fs "^4.2.4"
|
||||||
tapable "^2.2.0"
|
tapable "^2.2.0"
|
||||||
@ -3751,10 +3751,10 @@ vary@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
||||||
|
|
||||||
watchpack@^2.3.1:
|
watchpack@^2.4.0:
|
||||||
version "2.3.1"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
|
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
glob-to-regexp "^0.4.1"
|
glob-to-regexp "^0.4.1"
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
@ -3858,21 +3858,21 @@ webpack-sources@^3.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@5.73.0:
|
webpack@5.76.0:
|
||||||
version "5.73.0"
|
version "5.76.0"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
|
||||||
integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
|
integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.3"
|
"@types/eslint-scope" "^3.7.3"
|
||||||
"@types/estree" "^0.0.51"
|
"@types/estree" "^0.0.51"
|
||||||
"@webassemblyjs/ast" "1.11.1"
|
"@webassemblyjs/ast" "1.11.1"
|
||||||
"@webassemblyjs/wasm-edit" "1.11.1"
|
"@webassemblyjs/wasm-edit" "1.11.1"
|
||||||
"@webassemblyjs/wasm-parser" "1.11.1"
|
"@webassemblyjs/wasm-parser" "1.11.1"
|
||||||
acorn "^8.4.1"
|
acorn "^8.7.1"
|
||||||
acorn-import-assertions "^1.7.6"
|
acorn-import-assertions "^1.7.6"
|
||||||
browserslist "^4.14.5"
|
browserslist "^4.14.5"
|
||||||
chrome-trace-event "^1.0.2"
|
chrome-trace-event "^1.0.2"
|
||||||
enhanced-resolve "^5.9.3"
|
enhanced-resolve "^5.10.0"
|
||||||
es-module-lexer "^0.9.0"
|
es-module-lexer "^0.9.0"
|
||||||
eslint-scope "5.1.1"
|
eslint-scope "5.1.1"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
@ -3885,7 +3885,7 @@ webpack@5.73.0:
|
|||||||
schema-utils "^3.1.0"
|
schema-utils "^3.1.0"
|
||||||
tapable "^2.1.1"
|
tapable "^2.1.1"
|
||||||
terser-webpack-plugin "^5.1.3"
|
terser-webpack-plugin "^5.1.3"
|
||||||
watchpack "^2.3.1"
|
watchpack "^2.4.0"
|
||||||
webpack-sources "^3.2.3"
|
webpack-sources "^3.2.3"
|
||||||
|
|
||||||
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
|
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
|
||||||
|
@ -3,7 +3,6 @@ import fallbackMathJaxLangData from "./locales/en.json";
|
|||||||
import { FONT_FAMILY, SVG_NS } from "../../../../constants";
|
import { FONT_FAMILY, SVG_NS } from "../../../../constants";
|
||||||
import { getFontString, getFontFamilyString, isRTL } from "../../../../utils";
|
import { getFontString, getFontFamilyString, isRTL } from "../../../../utils";
|
||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
@ -555,6 +554,7 @@ const getCacheKey = (
|
|||||||
const measureMarkup = (
|
const measureMarkup = (
|
||||||
markup: Array<string | Element>,
|
markup: Array<string | Element>,
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
mathProps: MathProps,
|
mathProps: MathProps,
|
||||||
isMathJaxLoaded: boolean,
|
isMathJaxLoaded: boolean,
|
||||||
maxWidth?: number | null,
|
maxWidth?: number | null,
|
||||||
@ -567,11 +567,10 @@ const measureMarkup = (
|
|||||||
container.style.minHeight = "1em";
|
container.style.minHeight = "1em";
|
||||||
|
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
const lineHeight = getApproxLineHeight(font);
|
|
||||||
container.style.maxWidth = `${String(maxWidth)}px`;
|
container.style.maxWidth = `${String(maxWidth)}px`;
|
||||||
container.style.overflow = "hidden";
|
container.style.overflow = "hidden";
|
||||||
container.style.wordBreak = "break-word";
|
container.style.wordBreak = "break-word";
|
||||||
container.style.lineHeight = `${String(lineHeight)}px`;
|
container.style.lineHeight = `${String(lineHeight)}`;
|
||||||
container.style.whiteSpace = "pre-wrap";
|
container.style.whiteSpace = "pre-wrap";
|
||||||
}
|
}
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
@ -635,7 +634,7 @@ const measureMarkup = (
|
|||||||
const constrainedText = maxWidth
|
const constrainedText = maxWidth
|
||||||
? wrapText(text, font, maxWidth)
|
? wrapText(text, font, maxWidth)
|
||||||
: text;
|
: text;
|
||||||
const textMetrics = measureText(constrainedText, font);
|
const textMetrics = measureText(constrainedText, font, lineHeight);
|
||||||
childMetrics.push({
|
childMetrics.push({
|
||||||
x: nextX,
|
x: nextX,
|
||||||
y: baseline,
|
y: baseline,
|
||||||
@ -656,6 +655,7 @@ const measureMarkup = (
|
|||||||
const getMetrics = (
|
const getMetrics = (
|
||||||
markup: string[][],
|
markup: string[][],
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
mathProps: MathProps,
|
mathProps: MathProps,
|
||||||
isMathJaxLoaded: boolean,
|
isMathJaxLoaded: boolean,
|
||||||
maxWidth?: number | null,
|
maxWidth?: number | null,
|
||||||
@ -699,6 +699,7 @@ const getMetrics = (
|
|||||||
const { width, height, baseline, childMetrics } = measureMarkup(
|
const { width, height, baseline, childMetrics } = measureMarkup(
|
||||||
lineMarkup,
|
lineMarkup,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
mathProps,
|
mathProps,
|
||||||
isMathJaxLoaded,
|
isMathJaxLoaded,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
@ -725,6 +726,7 @@ const getMetrics = (
|
|||||||
const renderMath = (
|
const renderMath = (
|
||||||
text: string,
|
text: string,
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
textAlign: string,
|
textAlign: string,
|
||||||
mathProps: MathProps,
|
mathProps: MathProps,
|
||||||
isMathJaxLoaded: boolean,
|
isMathJaxLoaded: boolean,
|
||||||
@ -743,7 +745,13 @@ const renderMath = (
|
|||||||
isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
|
isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
|
||||||
);
|
);
|
||||||
const { markup, aria } = markupText(text, mathProps, isMathJaxLoaded);
|
const { markup, aria } = markupText(text, mathProps, isMathJaxLoaded);
|
||||||
const metrics = getMetrics(markup, fontSize, mathProps, isMathJaxLoaded);
|
const metrics = getMetrics(
|
||||||
|
markup,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
mathProps,
|
||||||
|
isMathJaxLoaded,
|
||||||
|
);
|
||||||
const width = parentWidth ?? metrics.imageMetrics.width;
|
const width = parentWidth ?? metrics.imageMetrics.width;
|
||||||
|
|
||||||
let y = 0;
|
let y = 0;
|
||||||
@ -799,13 +807,20 @@ const renderMath = (
|
|||||||
const getImageMetrics = (
|
const getImageMetrics = (
|
||||||
text: string,
|
text: string,
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
mathProps: MathProps,
|
mathProps: MathProps,
|
||||||
isMathJaxLoaded: boolean,
|
isMathJaxLoaded: boolean,
|
||||||
maxWidth?: number | null,
|
maxWidth?: number | null,
|
||||||
) => {
|
) => {
|
||||||
const markup = markupText(text, mathProps, isMathJaxLoaded).markup;
|
const markup = markupText(text, mathProps, isMathJaxLoaded).markup;
|
||||||
return getMetrics(markup, fontSize, mathProps, isMathJaxLoaded, maxWidth)
|
return getMetrics(
|
||||||
.imageMetrics;
|
markup,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
mathProps,
|
||||||
|
isMathJaxLoaded,
|
||||||
|
maxWidth,
|
||||||
|
).imageMetrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedMathElements = (
|
const getSelectedMathElements = (
|
||||||
@ -877,10 +892,17 @@ const measureMathElement = function (element, next) {
|
|||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
const fontSize = next?.fontSize ?? element.fontSize;
|
const fontSize = next?.fontSize ?? element.fontSize;
|
||||||
|
const lineHeight = element.lineHeight;
|
||||||
const text = next?.text ?? element.text;
|
const text = next?.text ?? element.text;
|
||||||
const customData = next?.customData ?? element.customData;
|
const customData = next?.customData ?? element.customData;
|
||||||
const mathProps = getMathProps.ensureMathProps(customData);
|
const mathProps = getMathProps.ensureMathProps(customData);
|
||||||
const metrics = getImageMetrics(text, fontSize, mathProps, isMathJaxLoaded);
|
const metrics = getImageMetrics(
|
||||||
|
text,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
mathProps,
|
||||||
|
isMathJaxLoaded,
|
||||||
|
);
|
||||||
const { width, height } = metrics;
|
const { width, height } = metrics;
|
||||||
return { width, height };
|
return { width, height };
|
||||||
} as SubtypeMethods["measureText"];
|
} as SubtypeMethods["measureText"];
|
||||||
@ -891,6 +913,7 @@ const renderMathElement = function (element, context, renderCb) {
|
|||||||
const _element = element as NonDeleted<ExcalidrawMathElement>;
|
const _element = element as NonDeleted<ExcalidrawMathElement>;
|
||||||
const text = _element.text;
|
const text = _element.text;
|
||||||
const fontSize = _element.fontSize;
|
const fontSize = _element.fontSize;
|
||||||
|
const lineHeight = _element.lineHeight;
|
||||||
const strokeColor = _element.strokeColor;
|
const strokeColor = _element.strokeColor;
|
||||||
const textAlign = _element.textAlign;
|
const textAlign = _element.textAlign;
|
||||||
const opacity = _element.opacity / 100;
|
const opacity = _element.opacity / 100;
|
||||||
@ -1005,6 +1028,7 @@ const renderMathElement = function (element, context, renderCb) {
|
|||||||
element.customData!.ariaLabel = renderMath(
|
element.customData!.ariaLabel = renderMath(
|
||||||
text,
|
text,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
textAlign,
|
textAlign,
|
||||||
mathProps,
|
mathProps,
|
||||||
isMathJaxLoaded,
|
isMathJaxLoaded,
|
||||||
@ -1023,6 +1047,7 @@ const renderSvgMathElement = function (svgRoot, root, element, opt) {
|
|||||||
const mathProps = getMathProps.ensureMathProps(_element.customData);
|
const mathProps = getMathProps.ensureMathProps(_element.customData);
|
||||||
const text = _element.text;
|
const text = _element.text;
|
||||||
const fontSize = _element.fontSize;
|
const fontSize = _element.fontSize;
|
||||||
|
const lineHeight = _element.lineHeight;
|
||||||
const strokeColor = _element.strokeColor;
|
const strokeColor = _element.strokeColor;
|
||||||
const textAlign = _element.textAlign;
|
const textAlign = _element.textAlign;
|
||||||
const opacity = _element.opacity / 100;
|
const opacity = _element.opacity / 100;
|
||||||
@ -1078,6 +1103,7 @@ const renderSvgMathElement = function (svgRoot, root, element, opt) {
|
|||||||
const { width, height } = getImageMetrics(
|
const { width, height } = getImageMetrics(
|
||||||
text,
|
text,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
mathProps,
|
mathProps,
|
||||||
isMathJaxLoaded,
|
isMathJaxLoaded,
|
||||||
parentWidth,
|
parentWidth,
|
||||||
@ -1140,6 +1166,7 @@ const renderSvgMathElement = function (svgRoot, root, element, opt) {
|
|||||||
element.customData!.ariaLabel = renderMath(
|
element.customData!.ariaLabel = renderMath(
|
||||||
text,
|
text,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
textAlign,
|
textAlign,
|
||||||
mathProps,
|
mathProps,
|
||||||
isMathJaxLoaded,
|
isMathJaxLoaded,
|
||||||
@ -1164,6 +1191,7 @@ const wrapMathElement = function (element, containerWidth, next) {
|
|||||||
const isMathJaxLoaded = mathJaxLoaded;
|
const isMathJaxLoaded = mathJaxLoaded;
|
||||||
const fontSize =
|
const fontSize =
|
||||||
next?.fontSize !== undefined ? next.fontSize : element.fontSize;
|
next?.fontSize !== undefined ? next.fontSize : element.fontSize;
|
||||||
|
const lineHeight = element.lineHeight;
|
||||||
const text = next?.text !== undefined ? next.text : element.originalText;
|
const text = next?.text !== undefined ? next.text : element.originalText;
|
||||||
const customData = next?.customData ?? element.customData;
|
const customData = next?.customData ?? element.customData;
|
||||||
const mathProps = getMathProps.ensureMathProps(customData);
|
const mathProps = getMathProps.ensureMathProps(customData);
|
||||||
@ -1178,7 +1206,13 @@ const wrapMathElement = function (element, containerWidth, next) {
|
|||||||
const maxWidth = containerWidth;
|
const maxWidth = containerWidth;
|
||||||
|
|
||||||
const markup = markupText(text, mathProps, isMathJaxLoaded).markup;
|
const markup = markupText(text, mathProps, isMathJaxLoaded).markup;
|
||||||
const metrics = getMetrics(markup, fontSize, mathProps, isMathJaxLoaded);
|
const metrics = getMetrics(
|
||||||
|
markup,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
mathProps,
|
||||||
|
isMathJaxLoaded,
|
||||||
|
);
|
||||||
|
|
||||||
const lines = consumeMathNewlines(text, mathProps, isMathJaxLoaded).split(
|
const lines = consumeMathNewlines(text, mathProps, isMathJaxLoaded).split(
|
||||||
isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
|
isMathJaxLoaded ? getMathNewline(mathProps) : "\n",
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
"ts-loader": "9.3.1",
|
"ts-loader": "9.3.1",
|
||||||
"webpack": "5.73.0",
|
"webpack": "5.76.0",
|
||||||
"webpack-bundle-analyzer": "4.5.0",
|
"webpack-bundle-analyzer": "4.5.0",
|
||||||
"webpack-cli": "4.10.0"
|
"webpack-cli": "4.10.0"
|
||||||
},
|
},
|
||||||
|
@ -1187,10 +1187,10 @@ acorn-walk@^8.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
|
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
|
||||||
integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
|
integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
|
||||||
|
|
||||||
acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0:
|
acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
|
||||||
version "8.7.1"
|
version "8.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
|
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||||
|
|
||||||
ajv-keywords@^3.5.2:
|
ajv-keywords@^3.5.2:
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
@ -1383,18 +1383,7 @@ braces@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
browserslist@^4.14.5:
|
browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.21.2:
|
||||||
version "4.19.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.3.tgz#29b7caad327ecf2859485f696f9604214bedd383"
|
|
||||||
integrity sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==
|
|
||||||
dependencies:
|
|
||||||
caniuse-lite "^1.0.30001312"
|
|
||||||
electron-to-chromium "^1.4.71"
|
|
||||||
escalade "^3.1.1"
|
|
||||||
node-releases "^2.0.2"
|
|
||||||
picocolors "^1.0.0"
|
|
||||||
|
|
||||||
browserslist@^4.20.2, browserslist@^4.21.2:
|
|
||||||
version "4.21.2"
|
version "4.21.2"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf"
|
||||||
integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==
|
integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==
|
||||||
@ -1417,11 +1406,6 @@ call-bind@^1.0.0:
|
|||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
get-intrinsic "^1.0.2"
|
get-intrinsic "^1.0.2"
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001312:
|
|
||||||
version "1.0.30001312"
|
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
|
|
||||||
integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
|
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001366:
|
caniuse-lite@^1.0.30001366:
|
||||||
version "1.0.30001367"
|
version "1.0.30001367"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz#2b97fe472e8fa29c78c5970615d7cd2ee414108a"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz#2b97fe472e8fa29c78c5970615d7cd2ee414108a"
|
||||||
@ -1601,20 +1585,15 @@ electron-to-chromium@^1.4.188:
|
|||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.195.tgz#139b2d95a42a3f17df217589723a1deac71d1473"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.195.tgz#139b2d95a42a3f17df217589723a1deac71d1473"
|
||||||
integrity sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==
|
integrity sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==
|
||||||
|
|
||||||
electron-to-chromium@^1.4.71:
|
|
||||||
version "1.4.75"
|
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.75.tgz#d1ad9bb46f2f1bf432118c2be21d27ffeae82fdd"
|
|
||||||
integrity sha512-LxgUNeu3BVU7sXaKjUDD9xivocQLxFtq6wgERrutdY/yIOps3ODOZExK1jg8DTEg4U8TUCb5MLGeWFOYuxjF3Q==
|
|
||||||
|
|
||||||
emojis-list@^3.0.0:
|
emojis-list@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||||
|
|
||||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
|
enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
|
||||||
version "5.9.3"
|
version "5.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
|
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
|
||||||
integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
|
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs "^4.2.4"
|
graceful-fs "^4.2.4"
|
||||||
tapable "^2.2.0"
|
tapable "^2.2.0"
|
||||||
@ -2011,11 +1990,6 @@ neo-async@^2.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
node-releases@^2.0.2:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
|
|
||||||
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
|
|
||||||
|
|
||||||
node-releases@^2.0.6:
|
node-releases@^2.0.6:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
|
||||||
@ -2494,10 +2468,10 @@ util-deprecate@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||||
|
|
||||||
watchpack@^2.3.1:
|
watchpack@^2.4.0:
|
||||||
version "2.3.1"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
|
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
glob-to-regexp "^0.4.1"
|
glob-to-regexp "^0.4.1"
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
@ -2548,21 +2522,21 @@ webpack-sources@^3.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@5.73.0:
|
webpack@5.76.0:
|
||||||
version "5.73.0"
|
version "5.76.0"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
|
||||||
integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
|
integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.3"
|
"@types/eslint-scope" "^3.7.3"
|
||||||
"@types/estree" "^0.0.51"
|
"@types/estree" "^0.0.51"
|
||||||
"@webassemblyjs/ast" "1.11.1"
|
"@webassemblyjs/ast" "1.11.1"
|
||||||
"@webassemblyjs/wasm-edit" "1.11.1"
|
"@webassemblyjs/wasm-edit" "1.11.1"
|
||||||
"@webassemblyjs/wasm-parser" "1.11.1"
|
"@webassemblyjs/wasm-parser" "1.11.1"
|
||||||
acorn "^8.4.1"
|
acorn "^8.7.1"
|
||||||
acorn-import-assertions "^1.7.6"
|
acorn-import-assertions "^1.7.6"
|
||||||
browserslist "^4.14.5"
|
browserslist "^4.14.5"
|
||||||
chrome-trace-event "^1.0.2"
|
chrome-trace-event "^1.0.2"
|
||||||
enhanced-resolve "^5.9.3"
|
enhanced-resolve "^5.10.0"
|
||||||
es-module-lexer "^0.9.0"
|
es-module-lexer "^0.9.0"
|
||||||
eslint-scope "5.1.1"
|
eslint-scope "5.1.1"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
@ -2575,7 +2549,7 @@ webpack@5.73.0:
|
|||||||
schema-utils "^3.1.0"
|
schema-utils "^3.1.0"
|
||||||
tapable "^2.1.1"
|
tapable "^2.1.1"
|
||||||
terser-webpack-plugin "^5.1.3"
|
terser-webpack-plugin "^5.1.3"
|
||||||
watchpack "^2.3.1"
|
watchpack "^2.4.0"
|
||||||
webpack-sources "^3.2.3"
|
webpack-sources "^3.2.3"
|
||||||
|
|
||||||
which@^2.0.1:
|
which@^2.0.1:
|
||||||
|
@ -41,10 +41,10 @@ import {
|
|||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getStroke, StrokeOptions } from "perfect-freehand";
|
import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getLineHeightInPx,
|
||||||
getMaxContainerHeight,
|
getMaxContainerHeight,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
@ -88,12 +88,66 @@ export interface ExcalidrawElementWithCanvas {
|
|||||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
theme: RenderConfig["theme"];
|
theme: RenderConfig["theme"];
|
||||||
canvasZoom: Zoom["value"];
|
scale: number;
|
||||||
canvasOffsetX: number;
|
canvasOffsetX: number;
|
||||||
canvasOffsetY: number;
|
canvasOffsetY: number;
|
||||||
boundTextElementVersion: number | null;
|
boundTextElementVersion: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cappedElementCanvasSize = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
zoom: Zoom,
|
||||||
|
): {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
scale: number;
|
||||||
|
} => {
|
||||||
|
// these limits are ballpark, they depend on specific browsers and device.
|
||||||
|
// We've chosen lower limits to be safe. We might want to change these limits
|
||||||
|
// based on browser/device type, if we get reports of low quality rendering
|
||||||
|
// on zoom.
|
||||||
|
//
|
||||||
|
// ~ safari mobile canvas area limit
|
||||||
|
const AREA_LIMIT = 16777216;
|
||||||
|
// ~ safari width/height limit based on developer.mozilla.org.
|
||||||
|
const WIDTH_HEIGHT_LIMIT = 32767;
|
||||||
|
|
||||||
|
const padding = getCanvasPadding(element);
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
|
const elementWidth =
|
||||||
|
isLinearElement(element) || isFreeDrawElement(element)
|
||||||
|
? distance(x1, x2)
|
||||||
|
: element.width;
|
||||||
|
const elementHeight =
|
||||||
|
isLinearElement(element) || isFreeDrawElement(element)
|
||||||
|
? distance(y1, y2)
|
||||||
|
: element.height;
|
||||||
|
|
||||||
|
let width = elementWidth * window.devicePixelRatio + padding * 2;
|
||||||
|
let height = elementHeight * window.devicePixelRatio + padding * 2;
|
||||||
|
|
||||||
|
let scale: number = zoom.value;
|
||||||
|
|
||||||
|
// rescale to ensure width and height is within limits
|
||||||
|
if (
|
||||||
|
width * scale > WIDTH_HEIGHT_LIMIT ||
|
||||||
|
height * scale > WIDTH_HEIGHT_LIMIT
|
||||||
|
) {
|
||||||
|
scale = Math.min(WIDTH_HEIGHT_LIMIT / width, WIDTH_HEIGHT_LIMIT / height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rescale to ensure canvas area is within limits
|
||||||
|
if (width * height * scale * scale > AREA_LIMIT) {
|
||||||
|
scale = Math.sqrt(AREA_LIMIT / (width * height));
|
||||||
|
}
|
||||||
|
|
||||||
|
width = Math.floor(width * scale);
|
||||||
|
height = Math.floor(height * scale);
|
||||||
|
|
||||||
|
return { width, height, scale };
|
||||||
|
};
|
||||||
|
|
||||||
const generateElementCanvas = (
|
const generateElementCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
@ -103,44 +157,35 @@ const generateElementCanvas = (
|
|||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
const padding = getCanvasPadding(element);
|
const padding = getCanvasPadding(element);
|
||||||
|
|
||||||
|
const { width, height, scale } = cappedElementCanvasSize(element, zoom);
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
let canvasOffsetX = 0;
|
let canvasOffsetX = 0;
|
||||||
let canvasOffsetY = 0;
|
let canvasOffsetY = 0;
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1] = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
canvas.width =
|
|
||||||
distance(x1, x2) * window.devicePixelRatio * zoom.value +
|
|
||||||
padding * zoom.value * 2;
|
|
||||||
canvas.height =
|
|
||||||
distance(y1, y2) * window.devicePixelRatio * zoom.value +
|
|
||||||
padding * zoom.value * 2;
|
|
||||||
|
|
||||||
canvasOffsetX =
|
canvasOffsetX =
|
||||||
element.x > x1
|
element.x > x1
|
||||||
? distance(element.x, x1) * window.devicePixelRatio * zoom.value
|
? distance(element.x, x1) * window.devicePixelRatio * scale
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
canvasOffsetY =
|
canvasOffsetY =
|
||||||
element.y > y1
|
element.y > y1
|
||||||
? distance(element.y, y1) * window.devicePixelRatio * zoom.value
|
? distance(element.y, y1) * window.devicePixelRatio * scale
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
context.translate(canvasOffsetX, canvasOffsetY);
|
context.translate(canvasOffsetX, canvasOffsetY);
|
||||||
} else {
|
|
||||||
canvas.width =
|
|
||||||
element.width * window.devicePixelRatio * zoom.value +
|
|
||||||
padding * zoom.value * 2;
|
|
||||||
canvas.height =
|
|
||||||
element.height * window.devicePixelRatio * zoom.value +
|
|
||||||
padding * zoom.value * 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(padding * zoom.value, padding * zoom.value);
|
context.translate(padding * scale, padding * scale);
|
||||||
context.scale(
|
context.scale(
|
||||||
window.devicePixelRatio * zoom.value,
|
window.devicePixelRatio * scale,
|
||||||
window.devicePixelRatio * zoom.value,
|
window.devicePixelRatio * scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
@ -157,7 +202,7 @@ const generateElementCanvas = (
|
|||||||
element,
|
element,
|
||||||
canvas,
|
canvas,
|
||||||
theme: renderConfig.theme,
|
theme: renderConfig.theme,
|
||||||
canvasZoom: zoom.value,
|
scale,
|
||||||
canvasOffsetX,
|
canvasOffsetX,
|
||||||
canvasOffsetY,
|
canvasOffsetY,
|
||||||
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
||||||
@ -286,9 +331,7 @@ const drawElementOnCanvas = (
|
|||||||
|
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
const lineHeight = element.containerId
|
|
||||||
? getApproxLineHeight(getFontString(element))
|
|
||||||
: element.height / lines.length;
|
|
||||||
const horizontalOffset =
|
const horizontalOffset =
|
||||||
element.textAlign === "center"
|
element.textAlign === "center"
|
||||||
? element.width / 2
|
? element.width / 2
|
||||||
@ -297,11 +340,16 @@ const drawElementOnCanvas = (
|
|||||||
: 0;
|
: 0;
|
||||||
context.textBaseline = "bottom";
|
context.textBaseline = "bottom";
|
||||||
|
|
||||||
|
const lineHeightPx = getLineHeightInPx(
|
||||||
|
element.fontSize,
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
for (let index = 0; index < lines.length; index++) {
|
for (let index = 0; index < lines.length; index++) {
|
||||||
context.fillText(
|
context.fillText(
|
||||||
lines[index],
|
lines[index],
|
||||||
horizontalOffset,
|
horizontalOffset,
|
||||||
(index + 1) * lineHeight,
|
(index + 1) * lineHeightPx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
context.restore();
|
context.restore();
|
||||||
@ -674,7 +722,7 @@ const generateElementWithCanvas = (
|
|||||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||||
const shouldRegenerateBecauseZoom =
|
const shouldRegenerateBecauseZoom =
|
||||||
prevElementWithCanvas &&
|
prevElementWithCanvas &&
|
||||||
prevElementWithCanvas.canvasZoom !== zoom.value &&
|
prevElementWithCanvas.scale !== zoom.value &&
|
||||||
!renderConfig?.shouldCacheIgnoreZoom;
|
!renderConfig?.shouldCacheIgnoreZoom;
|
||||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||||
|
|
||||||
@ -705,7 +753,7 @@ const drawElementFromCanvas = (
|
|||||||
) => {
|
) => {
|
||||||
const element = elementWithCanvas.element;
|
const element = elementWithCanvas.element;
|
||||||
const padding = getCanvasPadding(element);
|
const padding = getCanvasPadding(element);
|
||||||
const zoom = elementWithCanvas.canvasZoom;
|
const zoom = elementWithCanvas.scale;
|
||||||
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
// Free draw elements will otherwise "shuffle" as the min x and y change
|
// Free draw elements will otherwise "shuffle" as the min x and y change
|
||||||
@ -732,10 +780,10 @@ const drawElementFromCanvas = (
|
|||||||
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
|
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
|
||||||
tempCanvas.width =
|
tempCanvas.width =
|
||||||
maxDim * window.devicePixelRatio * zoom +
|
maxDim * window.devicePixelRatio * zoom +
|
||||||
padding * elementWithCanvas.canvasZoom * 10;
|
padding * elementWithCanvas.scale * 10;
|
||||||
tempCanvas.height =
|
tempCanvas.height =
|
||||||
maxDim * window.devicePixelRatio * zoom +
|
maxDim * window.devicePixelRatio * zoom +
|
||||||
padding * elementWithCanvas.canvasZoom * 10;
|
padding * elementWithCanvas.scale * 10;
|
||||||
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
|
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
|
||||||
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
|
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
|
||||||
|
|
||||||
@ -816,11 +864,11 @@ const drawElementFromCanvas = (
|
|||||||
context.drawImage(
|
context.drawImage(
|
||||||
elementWithCanvas.canvas!,
|
elementWithCanvas.canvas!,
|
||||||
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||||
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||||
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
|
||||||
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -1325,7 +1373,10 @@ export const renderElementToSvg = (
|
|||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
const lineHeight = element.height / lines.length;
|
const lineHeightPx = getLineHeightInPx(
|
||||||
|
element.fontSize,
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
const horizontalOffset =
|
const horizontalOffset =
|
||||||
element.textAlign === "center"
|
element.textAlign === "center"
|
||||||
? element.width / 2
|
? element.width / 2
|
||||||
@ -1343,7 +1394,7 @@ export const renderElementToSvg = (
|
|||||||
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
||||||
text.textContent = lines[i];
|
text.textContent = lines[i];
|
||||||
text.setAttribute("x", `${horizontalOffset}`);
|
text.setAttribute("x", `${horizontalOffset}`);
|
||||||
text.setAttribute("y", `${i * lineHeight}`);
|
text.setAttribute("y", `${i * lineHeightPx}`);
|
||||||
text.setAttribute("font-family", getFontFamilyString(element));
|
text.setAttribute("font-family", getFontFamilyString(element));
|
||||||
text.setAttribute("font-size", `${element.fontSize}px`);
|
text.setAttribute("font-size", `${element.fontSize}px`);
|
||||||
text.setAttribute("fill", element.strokeColor);
|
text.setAttribute("fill", element.strokeColor);
|
||||||
|
@ -225,7 +225,12 @@ export type SubtypeMethods = {
|
|||||||
measureText: (
|
measureText: (
|
||||||
element: Pick<
|
element: Pick<
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
"subtype" | "customData" | "fontSize" | "fontFamily" | "text"
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
|
| "fontSize"
|
||||||
|
| "fontFamily"
|
||||||
|
| "text"
|
||||||
|
| "lineHeight"
|
||||||
>,
|
>,
|
||||||
next?: {
|
next?: {
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
@ -247,7 +252,12 @@ export type SubtypeMethods = {
|
|||||||
wrapText: (
|
wrapText: (
|
||||||
element: Pick<
|
element: Pick<
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
"subtype" | "customData" | "fontSize" | "fontFamily" | "originalText"
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
|
| "fontSize"
|
||||||
|
| "fontFamily"
|
||||||
|
| "originalText"
|
||||||
|
| "lineHeight"
|
||||||
>,
|
>,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
next?: {
|
next?: {
|
||||||
|
@ -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: 24px; left: 35px; top: 8px; transform-origin: 5px 12px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
|
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"
|
||||||
/>
|
/>
|
||||||
|
@ -4,6 +4,7 @@ import { UI, Pointer, Keyboard } from "./helpers/ui";
|
|||||||
import { getTransformHandles } from "../element/transformHandles";
|
import { getTransformHandles } from "../element/transformHandles";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -209,4 +210,103 @@ describe("element binding", () => {
|
|||||||
).toBe(null);
|
).toBe(null);
|
||||||
expect(arrow.endBinding?.elementId).toBe(text.id);
|
expect(arrow.endBinding?.elementId).toBe(text.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update binding when text containerized", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rectangle1",
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
boundElements: [
|
||||||
|
{ id: "arrow1", type: "arrow" },
|
||||||
|
{ id: "arrow2", type: "arrow" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow1 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -87.45777932247563],
|
||||||
|
],
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: "text1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow2 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow2",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -87.45777932247563],
|
||||||
|
],
|
||||||
|
startBinding: {
|
||||||
|
elementId: "text1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const text1 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "text1",
|
||||||
|
text: "ola",
|
||||||
|
boundElements: [
|
||||||
|
{ id: "arrow1", type: "arrow" },
|
||||||
|
{ id: "arrow2", type: "arrow" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [rectangle1, arrow1, arrow2, text1];
|
||||||
|
|
||||||
|
API.setSelectedElements([text1]);
|
||||||
|
|
||||||
|
expect(h.state.selectedElementIds[text1.id]).toBe(true);
|
||||||
|
|
||||||
|
h.app.actionManager.executeAction(actionCreateContainerFromText);
|
||||||
|
|
||||||
|
// new text container will be placed before the text element
|
||||||
|
const container = h.elements.at(-2)!;
|
||||||
|
|
||||||
|
expect(container.type).toBe("rectangle");
|
||||||
|
expect(container.id).not.toBe(rectangle1.id);
|
||||||
|
|
||||||
|
expect(container).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
boundElements: expect.arrayContaining([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
id: text1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
id: arrow1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
id: arrow2.id,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(arrow1.startBinding?.elementId).toBe(rectangle1.id);
|
||||||
|
expect(arrow1.endBinding?.elementId).toBe(container.id);
|
||||||
|
expect(arrow2.startBinding?.elementId).toBe(container.id);
|
||||||
|
expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,8 +3,10 @@ import { render, waitFor, GlobalTestState } 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";
|
||||||
import { getApproxLineHeight } from "../element/textElement";
|
import {
|
||||||
import { getFontString } from "../utils";
|
getDefaultLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
|
} from "../element/textElement";
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import { NormalizedZoomValue } from "../types";
|
import { NormalizedZoomValue } from "../types";
|
||||||
|
|
||||||
@ -118,12 +120,10 @@ describe("paste text as single lines", () => {
|
|||||||
|
|
||||||
it("should space items correctly", async () => {
|
it("should space items correctly", async () => {
|
||||||
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
|
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
|
||||||
const lineHeight =
|
const lineHeightPx =
|
||||||
getApproxLineHeight(
|
getLineHeightInPx(
|
||||||
getFontString({
|
h.app.state.currentItemFontSize,
|
||||||
fontSize: h.app.state.currentItemFontSize,
|
getDefaultLineHeight(h.state.currentItemFontFamily),
|
||||||
fontFamily: h.app.state.currentItemFontFamily,
|
|
||||||
}),
|
|
||||||
) +
|
) +
|
||||||
10 / h.app.state.zoom.value;
|
10 / h.app.state.zoom.value;
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
@ -135,19 +135,17 @@ describe("paste text as single lines", () => {
|
|||||||
for (let i = 1; i < h.elements.length; i++) {
|
for (let i = 1; i < h.elements.length; i++) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [fx, elY] = getElementBounds(h.elements[i]);
|
const [fx, elY] = getElementBounds(h.elements[i]);
|
||||||
expect(elY).toEqual(firstElY + lineHeight * i);
|
expect(elY).toEqual(firstElY + lineHeightPx * i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should leave a space for blank new lines", async () => {
|
it("should leave a space for blank new lines", async () => {
|
||||||
const text = "hkhkjhki\n\njgkjhffjh";
|
const text = "hkhkjhki\n\njgkjhffjh";
|
||||||
const lineHeight =
|
const lineHeightPx =
|
||||||
getApproxLineHeight(
|
getLineHeightInPx(
|
||||||
getFontString({
|
h.app.state.currentItemFontSize,
|
||||||
fontSize: h.app.state.currentItemFontSize,
|
getDefaultLineHeight(h.state.currentItemFontFamily),
|
||||||
fontFamily: h.app.state.currentItemFontFamily,
|
|
||||||
}),
|
|
||||||
) +
|
) +
|
||||||
10 / h.app.state.zoom.value;
|
10 / h.app.state.zoom.value;
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
@ -158,7 +156,7 @@ describe("paste text as single lines", () => {
|
|||||||
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [lx, lastElY] = getElementBounds(h.elements[1]);
|
const [lx, lastElY] = getElementBounds(h.elements[1]);
|
||||||
expect(lastElY).toEqual(firstElY + lineHeight * 2);
|
expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -224,7 +222,7 @@ describe("Paste bound text container", () => {
|
|||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
const container = h.elements[0];
|
const container = h.elements[0];
|
||||||
expect(container.height).toBe(354);
|
expect(container.height).toBe(368);
|
||||||
expect(container.width).toBe(166);
|
expect(container.width).toBe(166);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -247,7 +245,7 @@ describe("Paste bound text container", () => {
|
|||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
const container = h.elements[0];
|
const container = h.elements[0];
|
||||||
expect(container.height).toBe(740);
|
expect(container.height).toBe(770);
|
||||||
expect(container.width).toBe(166);
|
expect(container.width).toBe(166);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -291,6 +291,7 @@ Object {
|
|||||||
"height": 100,
|
"height": 100,
|
||||||
"id": "id-text01",
|
"id": "id-text01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
"lineHeight": 1.25,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -312,7 +313,7 @@ Object {
|
|||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -8.4,
|
"y": -8.75,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -329,6 +330,7 @@ Object {
|
|||||||
"height": 100,
|
"height": 100,
|
||||||
"id": "id-text01",
|
"id": "id-text01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
"lineHeight": 1.25,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
189
src/tests/fitToContent.test.tsx
Normal file
189
src/tests/fitToContent.test.tsx
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { render } from "./test-utils";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("fitToContent", () => {
|
||||||
|
it("should zoom to fit the selected element", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
h.state.width = 10;
|
||||||
|
h.state.height = 10;
|
||||||
|
|
||||||
|
const rectElement = API.createElement({
|
||||||
|
width: 50,
|
||||||
|
height: 100,
|
||||||
|
x: 50,
|
||||||
|
y: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.state.zoom.value).toBe(1);
|
||||||
|
|
||||||
|
h.app.scrollToContent(rectElement, { fitToContent: true });
|
||||||
|
|
||||||
|
// element is 10x taller than the viewport size,
|
||||||
|
// zoom should be at least 1/10
|
||||||
|
expect(h.state.zoom.value).toBeLessThanOrEqual(0.1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should zoom to fit multiple elements", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
const topLeft = API.createElement({
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bottomRight = API.createElement({
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
x: 80,
|
||||||
|
y: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.state.width = 10;
|
||||||
|
h.state.height = 10;
|
||||||
|
|
||||||
|
expect(h.state.zoom.value).toBe(1);
|
||||||
|
|
||||||
|
h.app.scrollToContent([topLeft, bottomRight], {
|
||||||
|
fitToContent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// elements take 100x100, which is 10x bigger than the viewport size,
|
||||||
|
// zoom should be at least 1/10
|
||||||
|
expect(h.state.zoom.value).toBeLessThanOrEqual(0.1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should scroll the viewport to the selected element", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
h.state.width = 10;
|
||||||
|
h.state.height = 10;
|
||||||
|
|
||||||
|
const rectElement = API.createElement({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.state.zoom.value).toBe(1);
|
||||||
|
expect(h.state.scrollX).toBe(0);
|
||||||
|
expect(h.state.scrollY).toBe(0);
|
||||||
|
|
||||||
|
h.app.scrollToContent(rectElement);
|
||||||
|
|
||||||
|
// zoom level should stay the same
|
||||||
|
expect(h.state.zoom.value).toBe(1);
|
||||||
|
|
||||||
|
// state should reflect some scrolling
|
||||||
|
expect(h.state.scrollX).not.toBe(0);
|
||||||
|
expect(h.state.scrollY).not.toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitForNextAnimationFrame = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("fitToContent animated", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(window, "requestAnimationFrame");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ease scroll the viewport to the selected element", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
h.state.width = 10;
|
||||||
|
h.state.height = 10;
|
||||||
|
|
||||||
|
const rectElement = API.createElement({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: -100,
|
||||||
|
y: -100,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.app.scrollToContent(rectElement, { animate: true });
|
||||||
|
|
||||||
|
expect(window.requestAnimationFrame).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Since this is an animation, we expect values to change through time.
|
||||||
|
// We'll verify that the scroll values change at 50ms and 100ms
|
||||||
|
expect(h.state.scrollX).toBe(0);
|
||||||
|
expect(h.state.scrollY).toBe(0);
|
||||||
|
|
||||||
|
await waitForNextAnimationFrame();
|
||||||
|
|
||||||
|
const prevScrollX = h.state.scrollX;
|
||||||
|
const prevScrollY = h.state.scrollY;
|
||||||
|
|
||||||
|
expect(h.state.scrollX).not.toBe(0);
|
||||||
|
expect(h.state.scrollY).not.toBe(0);
|
||||||
|
|
||||||
|
await waitForNextAnimationFrame();
|
||||||
|
|
||||||
|
expect(h.state.scrollX).not.toBe(prevScrollX);
|
||||||
|
expect(h.state.scrollY).not.toBe(prevScrollY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should animate the scroll but not the zoom", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
h.state.width = 50;
|
||||||
|
h.state.height = 50;
|
||||||
|
|
||||||
|
const rectElement = API.createElement({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.state.scrollX).toBe(0);
|
||||||
|
expect(h.state.scrollY).toBe(0);
|
||||||
|
|
||||||
|
h.app.scrollToContent(rectElement, { animate: true, fitToContent: true });
|
||||||
|
|
||||||
|
expect(window.requestAnimationFrame).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Since this is an animation, we expect values to change through time.
|
||||||
|
// We'll verify that the zoom/scroll values change in each animation frame
|
||||||
|
|
||||||
|
// zoom is not animated, it should be set to its final value, which in our
|
||||||
|
// case zooms out to 50% so that th element is fully visible (it's 2x large
|
||||||
|
// as the canvas)
|
||||||
|
expect(h.state.zoom.value).toBeLessThanOrEqual(0.5);
|
||||||
|
|
||||||
|
// FIXME I think this should be [-100, -100] so we may have a bug in our zoom
|
||||||
|
// hadnling, alas
|
||||||
|
expect(h.state.scrollX).toBe(25);
|
||||||
|
expect(h.state.scrollY).toBe(25);
|
||||||
|
|
||||||
|
await waitForNextAnimationFrame();
|
||||||
|
|
||||||
|
const prevScrollX = h.state.scrollX;
|
||||||
|
const prevScrollY = h.state.scrollY;
|
||||||
|
|
||||||
|
expect(h.state.scrollX).not.toBe(0);
|
||||||
|
expect(h.state.scrollY).not.toBe(0);
|
||||||
|
|
||||||
|
await waitForNextAnimationFrame();
|
||||||
|
|
||||||
|
expect(h.state.scrollX).not.toBe(prevScrollX);
|
||||||
|
expect(h.state.scrollY).not.toBe(prevScrollY);
|
||||||
|
});
|
||||||
|
});
|
@ -139,6 +139,9 @@ export class API {
|
|||||||
fileId?: T extends "image" ? string : never;
|
fileId?: T extends "image" ? string : never;
|
||||||
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
||||||
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
|
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
|
||||||
|
startBinding?: T extends "arrow"
|
||||||
|
? ExcalidrawLinearElement["startBinding"]
|
||||||
|
: never;
|
||||||
endBinding?: T extends "arrow"
|
endBinding?: T extends "arrow"
|
||||||
? ExcalidrawLinearElement["endBinding"]
|
? ExcalidrawLinearElement["endBinding"]
|
||||||
: never;
|
: never;
|
||||||
@ -215,11 +218,13 @@ export class API {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "text":
|
case "text":
|
||||||
|
const fontSize = rest.fontSize ?? appState.currentItemFontSize;
|
||||||
|
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
|
||||||
element = newTextElement({
|
element = newTextElement({
|
||||||
...base,
|
...base,
|
||||||
text: rest.text || "test",
|
text: rest.text || "test",
|
||||||
fontSize: rest.fontSize ?? appState.currentItemFontSize,
|
fontSize,
|
||||||
fontFamily: rest.fontFamily ?? appState.currentItemFontFamily,
|
fontFamily,
|
||||||
textAlign: rest.textAlign ?? appState.currentItemTextAlign,
|
textAlign: rest.textAlign ?? appState.currentItemTextAlign,
|
||||||
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: rest.containerId ?? undefined,
|
containerId: rest.containerId ?? undefined,
|
||||||
@ -258,6 +263,10 @@ export class API {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (element.type === "arrow") {
|
||||||
|
element.startBinding = rest.startBinding ?? null;
|
||||||
|
element.endBinding = rest.endBinding ?? null;
|
||||||
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
element.id = id;
|
element.id = id;
|
||||||
}
|
}
|
||||||
|
@ -1031,7 +1031,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"height": 128,
|
"height": 130,
|
||||||
"width": 367,
|
"width": 367,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -1040,7 +1040,7 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 272,
|
"x": 272,
|
||||||
"y": 46,
|
"y": 45,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
@ -1052,11 +1052,11 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
20,
|
20,
|
||||||
36,
|
35,
|
||||||
502,
|
502,
|
||||||
94,
|
95,
|
||||||
205.9061448421403,
|
205.9061448421403,
|
||||||
53,
|
52.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1090,7 +1090,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"height": 128,
|
"height": 130,
|
||||||
"width": 340,
|
"width": 340,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -1099,7 +1099,7 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 75,
|
"x": 75,
|
||||||
"y": -4,
|
"y": -5,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(textElement.text).toMatchInlineSnapshot(`
|
expect(textElement.text).toMatchInlineSnapshot(`
|
||||||
|
@ -163,7 +163,8 @@ const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
|||||||
: next?.fontSize ?? element.fontSize;
|
: next?.fontSize ?? element.fontSize;
|
||||||
const fontFamily = element.fontFamily;
|
const fontFamily = element.fontFamily;
|
||||||
const fontString = getFontString({ fontSize, fontFamily });
|
const fontString = getFontString({ fontSize, fontFamily });
|
||||||
const metrics = textElementUtils.measureText(text, fontString);
|
const lineHeight = element.lineHeight;
|
||||||
|
const metrics = textElementUtils.measureText(text, fontString, lineHeight);
|
||||||
const width = Math.max(metrics.width - 10, 0);
|
const width = Math.max(metrics.width - 10, 0);
|
||||||
const height = Math.max(metrics.height - 5, 0);
|
const height = Math.max(metrics.height - 5, 0);
|
||||||
return { width, height, baseline: 1 };
|
return { width, height, baseline: 1 };
|
||||||
@ -410,11 +411,7 @@ describe("subtypes", () => {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
await render(<ExcalidrawApp />, { localStorageData: { elements } });
|
await render(<ExcalidrawApp />, { localStorageData: { elements } });
|
||||||
const mockMeasureText = (
|
const mockMeasureText = (text: string, font: FontString) => {
|
||||||
text: string,
|
|
||||||
font: FontString,
|
|
||||||
maxWidth?: number | null,
|
|
||||||
) => {
|
|
||||||
if (text === testString) {
|
if (text === testString) {
|
||||||
let multiplier = 1;
|
let multiplier = 1;
|
||||||
if (font.includes(`${DBFONTSIZE}`)) {
|
if (font.includes(`${DBFONTSIZE}`)) {
|
||||||
@ -423,9 +420,7 @@ describe("subtypes", () => {
|
|||||||
if (font.includes(`${TRFONTSIZE}`)) {
|
if (font.includes(`${TRFONTSIZE}`)) {
|
||||||
multiplier = 3;
|
multiplier = 3;
|
||||||
}
|
}
|
||||||
const width = maxWidth
|
const width = multiplier * TWIDTH;
|
||||||
? Math.min(multiplier * TWIDTH, maxWidth)
|
|
||||||
: multiplier * TWIDTH;
|
|
||||||
const height = multiplier * THEIGHT;
|
const height = multiplier * THEIGHT;
|
||||||
const baseline = multiplier * TBASELINE;
|
const baseline = multiplier * TBASELINE;
|
||||||
return { width, height, baseline };
|
return { width, height, baseline };
|
||||||
|
79
src/utils.ts
79
src/utils.ts
@ -181,6 +181,79 @@ export const throttleRAF = <T extends any[]>(
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exponential ease-out method
|
||||||
|
*
|
||||||
|
* @param {number} k - The value to be tweened.
|
||||||
|
* @returns {number} The tweened value.
|
||||||
|
*/
|
||||||
|
function easeOut(k: number): number {
|
||||||
|
return 1 - Math.pow(1 - k, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute new values based on the same ease function and trigger the
|
||||||
|
* callback through a requestAnimationFrame call
|
||||||
|
*
|
||||||
|
* use `opts` to define a duration and/or an easeFn
|
||||||
|
*
|
||||||
|
* for example:
|
||||||
|
* ```ts
|
||||||
|
* easeToValuesRAF([10, 20, 10], [0, 0, 0], (a, b, c) => setState(a,b, c))
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param fromValues The initial values, must be numeric
|
||||||
|
* @param toValues The destination values, must also be numeric
|
||||||
|
* @param callback The callback receiving the values
|
||||||
|
* @param opts default to 250ms duration and the easeOut function
|
||||||
|
*/
|
||||||
|
export const easeToValuesRAF = (
|
||||||
|
fromValues: number[],
|
||||||
|
toValues: number[],
|
||||||
|
callback: (...values: number[]) => void,
|
||||||
|
opts?: { duration?: number; easeFn?: (value: number) => number },
|
||||||
|
) => {
|
||||||
|
let canceled = false;
|
||||||
|
let frameId = 0;
|
||||||
|
let startTime: number;
|
||||||
|
|
||||||
|
const duration = opts?.duration || 250; // default animation to 0.25 seconds
|
||||||
|
const easeFn = opts?.easeFn || easeOut; // default the easeFn to easeOut
|
||||||
|
|
||||||
|
function step(timestamp: number) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (startTime === undefined) {
|
||||||
|
startTime = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = timestamp - startTime;
|
||||||
|
|
||||||
|
if (elapsed < duration) {
|
||||||
|
// console.log(elapsed, duration, elapsed / duration);
|
||||||
|
const factor = easeFn(elapsed / duration);
|
||||||
|
const newValues = fromValues.map(
|
||||||
|
(fromValue, index) =>
|
||||||
|
(toValues[index] - fromValue) * factor + fromValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
callback(...newValues);
|
||||||
|
frameId = window.requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
// ensure final values are reached at the end of the transition
|
||||||
|
callback(...toValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frameId = window.requestAnimationFrame(step);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
window.cancelAnimationFrame(frameId);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// https://github.com/lodash/lodash/blob/es/chunk.js
|
// https://github.com/lodash/lodash/blob/es/chunk.js
|
||||||
export const chunk = <T extends any>(
|
export const chunk = <T extends any>(
|
||||||
array: readonly T[],
|
array: readonly T[],
|
||||||
@ -616,11 +689,7 @@ export const arrayToMapWithIndex = <T extends { id: string }>(
|
|||||||
return acc;
|
return acc;
|
||||||
}, new Map<string, [element: T, index: number]>());
|
}, new Map<string, [element: T, index: number]>());
|
||||||
|
|
||||||
export const isTestEnv = () =>
|
export const isTestEnv = () => process.env.NODE_ENV === "test";
|
||||||
typeof process !== "undefined" && process.env?.NODE_ENV === "test";
|
|
||||||
|
|
||||||
export const isProdEnv = () =>
|
|
||||||
typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
||||||
|
|
||||||
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
|
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
|
||||||
return new CustomEvent(name, {
|
return new CustomEvent(name, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user