Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a22927d4d1 | |||
| ca9b7a505e | |||
| 36b387f973 | |||
| 2ac55067cd | |||
| 78ab12c7e6 | |||
| f2f8219917 | |||
| 12c39d1034 | |||
| d33e42e3a1 | |||
| 3b9ffd9586 | |||
| b63689c230 | |||
| c84babf574 | |||
| 36274f1f3e | |||
| 798c795405 | |||
| 107eae3916 | |||
| 56fca30bd0 | |||
| 1e3399eac8 | |||
| 873698a1a2 | |||
| 606ac6c743 | |||
| d99e4a23ca | |||
| 551bae07a7 | |||
| 2af3221974 | |||
| 9b401f6ea3 | |||
| 8a1152ce36 | |||
| b5652b8e36 | |||
| 31e2a0cb4a | |||
| c0b80a03bd | |||
| a758aaf8f6 |
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
||||||
<a href="https://blog.excalidraw.com">Blog</a> |
|
<a href="https://plus.excalidraw.com/blog">Blog</a> |
|
||||||
<a href="https://docs.excalidraw.com">Documentation</a> |
|
<a href="https://docs.excalidraw.com">Documentation</a> |
|
||||||
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@@ -3,31 +3,32 @@
|
|||||||
All `props` are _optional_.
|
All `props` are _optional_.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | _ | Callback triggered with the excalidraw api once rendered |
|
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||||
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
|
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||||
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
|
| [`onChange`](#onchange) | `function` | \_ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
|
||||||
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
|
| [`onPointerUpdate`](#onpointerupdate) | `function` | \_ | Callback triggered when mouse pointer is updated. |
|
||||||
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down events |
|
| [`onPointerDown`](#onpointerdown) | `function` | \_ | This prop if passed gets triggered on pointer down events |
|
||||||
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when something is pasted into the scene |
|
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
||||||
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||||
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
|
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||||
|
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner |
|
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||||
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. |
|
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | \_ | Render function that can be used to render custom stats on the stats dialog. |
|
||||||
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. |
|
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | \_ | This indicates if the app is in `view` mode. |
|
||||||
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled |
|
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | \_ | This indicates if the `zen` mode is enabled |
|
||||||
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled |
|
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | \_ | This indicates if the `grid` mode is enabled |
|
||||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | \_ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||||
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
||||||
| [`name`](#name) | `string` | | Name of the drawing |
|
| [`name`](#name) | `string` | | Name of the drawing |
|
||||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
|
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
|
||||||
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
||||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
|
||||||
@@ -93,9 +94,8 @@ This callback is triggered when mouse pointer is updated.
|
|||||||
|
|
||||||
This prop if passed will be triggered on pointer down events and has the below signature.
|
This prop if passed will be triggered on pointer down events and has the below signature.
|
||||||
|
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(activeTool:{" "}
|
(activeTool:{" "}
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115">
|
||||||
{" "}
|
{" "}
|
||||||
AppState["activeTool"]
|
AppState["activeTool"]
|
||||||
@@ -143,6 +143,14 @@ This callback if supplied will get triggered when the library is updated and has
|
|||||||
|
|
||||||
It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage.
|
It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage.
|
||||||
|
|
||||||
|
### generateLinkForSelection
|
||||||
|
|
||||||
|
This prop if passed will be used to replace the default link generation function. The idea is that the host app can take over the creation of element links, which can be used to navigate to a particular element or a group. If the host app chooses a different key for element link id, then the host app should also take care of the handling and the navigation in `onLinkOpen`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
(id: string, type: "element" | "group") => string;
|
||||||
|
```
|
||||||
|
|
||||||
### onLinkOpen
|
### onLinkOpen
|
||||||
|
|
||||||
This prop if passed will be triggered when clicked on `link`. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
|
This prop if passed will be triggered when clicked on `link`. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
|
||||||
@@ -207,8 +215,7 @@ This prop indicates whether the shows the grid. When supplied, the value takes p
|
|||||||
|
|
||||||
### libraryReturnUrl
|
### libraryReturnUrl
|
||||||
|
|
||||||
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Defaults to _window.location.origin + window.location.pathname_. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
|
||||||
Defaults to _window.location.origin + window.location.pathname_. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
|
|
||||||
|
|
||||||
### theme
|
### theme
|
||||||
|
|
||||||
@@ -220,7 +227,6 @@ You can use [`THEME`](/docs/@excalidraw/excalidraw/api/utils#theme) to specify t
|
|||||||
|
|
||||||
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over _intialData.appState.name_, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
|
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over _intialData.appState.name_, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
|
||||||
|
|
||||||
|
|
||||||
### detectScroll
|
### detectScroll
|
||||||
|
|
||||||
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
|
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
|
||||||
|
|||||||
@@ -235,3 +235,5 @@ root.render(React.createElement(App));
|
|||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
You can try it out [here](https://codesandbox.io/p/sandbox/excalidraw-in-browser-tlqom?file=%2Findex.html%3A1%2C10).
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const config = {
|
|||||||
label: "Docs",
|
label: "Docs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: "https://blog.excalidraw.com",
|
to: "https://plus.excalidraw.com/blog",
|
||||||
label: "Blog",
|
label: "Blog",
|
||||||
position: "left",
|
position: "left",
|
||||||
},
|
},
|
||||||
@@ -111,7 +111,7 @@ const config = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Blog",
|
label: "Blog",
|
||||||
to: "https://blog.excalidraw.com",
|
to: "https://plus.excalidraw.com/blog",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GitHub",
|
label: "GitHub",
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ import DebugCanvas, {
|
|||||||
} from "./components/DebugCanvas";
|
} from "./components/DebugCanvas";
|
||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||||
|
import { isElementLink } from "../packages/excalidraw/element/elementLink";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -848,6 +849,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
onLinkOpen={(element, event) => {
|
||||||
|
if (element.link && isElementLink(element.link)) {
|
||||||
|
event.preventDefault();
|
||||||
|
excalidrawAPI?.scrollToContent(element.link, { animate: true });
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AppMainMenu
|
<AppMainMenu
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
onCollabDialogOpen={onCollabDialogOpen}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const EncryptedIcon = () => {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
content="https://excalidraw.com/og-image-3.png"
|
content="https://excalidraw.com/og-image-3.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<link rel="canonical" href="https://excalidraw.com" />
|
||||||
|
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<!-- to minimize white flash on load when user has dark mode enabled -->
|
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export const actionClearCanvas = register({
|
|||||||
predicate: (elements, appState, props, app) => {
|
predicate: (elements, appState, props, app) => {
|
||||||
return (
|
return (
|
||||||
!!app.props.UIOptions.canvasActions.clearCanvas &&
|
!!app.props.UIOptions.canvasActions.clearCanvas &&
|
||||||
!appState.viewModeEnabled
|
!appState.viewModeEnabled &&
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export const actionDeleteSelected = register({
|
|||||||
element,
|
element,
|
||||||
selectedPointsIndices,
|
selectedPointsIndices,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
|
import { copyIcon, elementLinkIcon } from "../components/icons";
|
||||||
|
import {
|
||||||
|
canCreateLinkFromElements,
|
||||||
|
defaultGetElementLinkFromSelection,
|
||||||
|
getLinkIdAndTypeFromSelection,
|
||||||
|
} from "../element/elementLink";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { StoreAction } from "../store";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionCopyElementLink = register({
|
||||||
|
name: "copyElementLink",
|
||||||
|
label: "labels.copyElementLink",
|
||||||
|
icon: copyIcon,
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
perform: async (elements, appState, _, app) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.location) {
|
||||||
|
const idAndType = getLinkIdAndTypeFromSelection(
|
||||||
|
selectedElements,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idAndType) {
|
||||||
|
await copyTextToSystemClipboard(
|
||||||
|
app.props.generateLinkForSelection
|
||||||
|
? app.props.generateLinkForSelection(idAndType.id, idAndType.type)
|
||||||
|
: defaultGetElementLinkFromSelection(
|
||||||
|
idAndType.id,
|
||||||
|
idAndType.type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
toast: {
|
||||||
|
message: t("toast.elementLinkCopied"),
|
||||||
|
closable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
app,
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
app,
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
predicate: (elements, appState) =>
|
||||||
|
canCreateLinkFromElements(getSelectedElements(elements, appState)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionLinkToElement = register({
|
||||||
|
name: "linkToElement",
|
||||||
|
label: "labels.linkToElement",
|
||||||
|
icon: elementLinkIcon,
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedElements.length !== 1 ||
|
||||||
|
!canCreateLinkFromElements(selectedElements)
|
||||||
|
) {
|
||||||
|
return { elements, appState, app, storeAction: StoreAction.NONE };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
openDialog: {
|
||||||
|
name: "elementLinkSelector",
|
||||||
|
sourceElementId: getSelectedElements(elements, appState)[0].id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
predicate: (elements, appState, appProps, app) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
canCreateLinkFromElements(selectedElements)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
trackEvent: false,
|
||||||
|
});
|
||||||
@@ -12,7 +12,6 @@ import { resizeMultipleElements } from "../element/resizeElements";
|
|||||||
import type { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getCommonBoundingBox } from "../element/bounds";
|
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
@@ -132,19 +132,14 @@ const flipElements = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY, midX, midY } =
|
const { midX, midY } = getCommonBoundingBox(selectedElements);
|
||||||
getCommonBoundingBox(selectedElements);
|
|
||||||
|
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(selectedElements, elementsMap, "nw", app.scene, {
|
||||||
elementsMap,
|
flipByX: flipDirection === "horizontal",
|
||||||
selectedElements,
|
flipByY: flipDirection === "vertical",
|
||||||
elementsMap,
|
shouldResizeFromCenter: true,
|
||||||
"nw",
|
shouldMaintainAspectRatio: true,
|
||||||
true,
|
});
|
||||||
true,
|
|
||||||
flipDirection === "horizontal" ? maxX : minX,
|
|
||||||
flipDirection === "horizontal" ? minY : maxY,
|
|
||||||
);
|
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
@@ -153,6 +148,7 @@ const flipElements = (
|
|||||||
app.scene,
|
app.scene,
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
|
appState.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ import {
|
|||||||
sharpArrowIcon,
|
sharpArrowIcon,
|
||||||
roundArrowIcon,
|
roundArrowIcon,
|
||||||
elbowArrowIcon,
|
elbowArrowIcon,
|
||||||
|
ArrowheadCrowfootIcon,
|
||||||
|
ArrowheadCrowfootOneIcon,
|
||||||
|
ArrowheadCrowfootOneOrManyIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
ARROW_TYPE,
|
ARROW_TYPE,
|
||||||
@@ -1405,59 +1408,65 @@ const getArrowheadOptions = (flip: boolean) => {
|
|||||||
keyBinding: "w",
|
keyBinding: "w",
|
||||||
icon: <ArrowheadArrowIcon flip={flip} />,
|
icon: <ArrowheadArrowIcon flip={flip} />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "bar",
|
|
||||||
text: t("labels.arrowhead_bar"),
|
|
||||||
keyBinding: "e",
|
|
||||||
icon: <ArrowheadBarIcon flip={flip} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "dot",
|
|
||||||
text: t("labels.arrowhead_circle"),
|
|
||||||
keyBinding: null,
|
|
||||||
icon: <ArrowheadCircleIcon flip={flip} />,
|
|
||||||
showInPicker: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "circle",
|
|
||||||
text: t("labels.arrowhead_circle"),
|
|
||||||
keyBinding: "r",
|
|
||||||
icon: <ArrowheadCircleIcon flip={flip} />,
|
|
||||||
showInPicker: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "circle_outline",
|
|
||||||
text: t("labels.arrowhead_circle_outline"),
|
|
||||||
keyBinding: null,
|
|
||||||
icon: <ArrowheadCircleOutlineIcon flip={flip} />,
|
|
||||||
showInPicker: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "triangle",
|
value: "triangle",
|
||||||
text: t("labels.arrowhead_triangle"),
|
text: t("labels.arrowhead_triangle"),
|
||||||
icon: <ArrowheadTriangleIcon flip={flip} />,
|
icon: <ArrowheadTriangleIcon flip={flip} />,
|
||||||
keyBinding: "t",
|
keyBinding: "e",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "triangle_outline",
|
value: "triangle_outline",
|
||||||
text: t("labels.arrowhead_triangle_outline"),
|
text: t("labels.arrowhead_triangle_outline"),
|
||||||
icon: <ArrowheadTriangleOutlineIcon flip={flip} />,
|
icon: <ArrowheadTriangleOutlineIcon flip={flip} />,
|
||||||
keyBinding: null,
|
keyBinding: "r",
|
||||||
showInPicker: false,
|
},
|
||||||
|
{
|
||||||
|
value: "circle",
|
||||||
|
text: t("labels.arrowhead_circle"),
|
||||||
|
keyBinding: "a",
|
||||||
|
icon: <ArrowheadCircleIcon flip={flip} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "circle_outline",
|
||||||
|
text: t("labels.arrowhead_circle_outline"),
|
||||||
|
keyBinding: "s",
|
||||||
|
icon: <ArrowheadCircleOutlineIcon flip={flip} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "diamond",
|
value: "diamond",
|
||||||
text: t("labels.arrowhead_diamond"),
|
text: t("labels.arrowhead_diamond"),
|
||||||
icon: <ArrowheadDiamondIcon flip={flip} />,
|
icon: <ArrowheadDiamondIcon flip={flip} />,
|
||||||
keyBinding: null,
|
keyBinding: "d",
|
||||||
showInPicker: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "diamond_outline",
|
value: "diamond_outline",
|
||||||
text: t("labels.arrowhead_diamond_outline"),
|
text: t("labels.arrowhead_diamond_outline"),
|
||||||
icon: <ArrowheadDiamondOutlineIcon flip={flip} />,
|
icon: <ArrowheadDiamondOutlineIcon flip={flip} />,
|
||||||
keyBinding: null,
|
keyBinding: "f",
|
||||||
showInPicker: false,
|
},
|
||||||
|
{
|
||||||
|
value: "bar",
|
||||||
|
text: t("labels.arrowhead_bar"),
|
||||||
|
keyBinding: "z",
|
||||||
|
icon: <ArrowheadBarIcon flip={flip} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "crowfoot_one",
|
||||||
|
text: t("labels.arrowhead_crowfoot_one"),
|
||||||
|
icon: <ArrowheadCrowfootOneIcon flip={flip} />,
|
||||||
|
keyBinding: "c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "crowfoot_many",
|
||||||
|
text: t("labels.arrowhead_crowfoot_many"),
|
||||||
|
icon: <ArrowheadCrowfootIcon flip={flip} />,
|
||||||
|
keyBinding: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "crowfoot_one_or_many",
|
||||||
|
text: t("labels.arrowhead_crowfoot_one_or_many"),
|
||||||
|
icon: <ArrowheadCrowfootOneOrManyIcon flip={flip} />,
|
||||||
|
keyBinding: "v",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
};
|
};
|
||||||
@@ -1521,6 +1530,7 @@ export const actionChangeArrowhead = register({
|
|||||||
appState.currentItemStartArrowhead,
|
appState.currentItemStartArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
onChange={(value) => updateData({ position: "start", type: value })}
|
||||||
|
numberOfOptionsToAlwaysShow={4}
|
||||||
/>
|
/>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
label="arrowhead_end"
|
label="arrowhead_end"
|
||||||
@@ -1537,6 +1547,7 @@ export const actionChangeArrowhead = register({
|
|||||||
appState.currentItemEndArrowhead,
|
appState.currentItemEndArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "end", type: value })}
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
numberOfOptionsToAlwaysShow={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -1591,6 +1602,7 @@ export const actionChangeArrowType = register({
|
|||||||
tupleToCoors(startGlobalPoint),
|
tupleToCoors(startGlobalPoint),
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const endHoveredElement =
|
const endHoveredElement =
|
||||||
@@ -1599,6 +1611,7 @@ export const actionChangeArrowType = register({
|
|||||||
tupleToCoors(endGlobalPoint),
|
tupleToCoors(endGlobalPoint),
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startElement = startHoveredElement
|
const startElement = startHoveredElement
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ export type ActionName =
|
|||||||
| "autoResize"
|
| "autoResize"
|
||||||
| "elementStats"
|
| "elementStats"
|
||||||
| "searchMenu"
|
| "searchMenu"
|
||||||
|
| "copyElementLink"
|
||||||
|
| "linkToElement"
|
||||||
| "cropEditor";
|
| "cropEditor";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
scrollX: 0,
|
scrollX: 0,
|
||||||
scrollY: 0,
|
scrollY: 0,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
|
hoveredElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedElementsAreBeingDragged: false,
|
selectedElementsAreBeingDragged: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
@@ -210,6 +211,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
scrollX: { browser: true, export: false, server: false },
|
scrollX: { browser: true, export: false, server: false },
|
||||||
scrollY: { browser: true, export: false, server: false },
|
scrollY: { browser: true, export: false, server: false },
|
||||||
selectedElementIds: { browser: true, export: false, server: false },
|
selectedElementIds: { browser: true, export: false, server: false },
|
||||||
|
hoveredElementIds: { browser: false, export: false, server: false },
|
||||||
selectedGroupIds: { browser: true, export: false, server: false },
|
selectedGroupIds: { browser: true, export: false, server: false },
|
||||||
selectedElementsAreBeingDragged: {
|
selectedElementsAreBeingDragged: {
|
||||||
browser: false,
|
browser: false,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { deepCopyElement } from "./element/newElement";
|
|||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { getContainingFrame } from "./frame";
|
import { getContainingFrame } from "./frame";
|
||||||
import { arrayToMap, isMemberOf, isPromiseLike } from "./utils";
|
import { arrayToMap, isMemberOf, isPromiseLike } from "./utils";
|
||||||
|
import { createFile, isSupportedImageFileType } from "./data/blob";
|
||||||
|
import { ExcalidrawError } from "./errors";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@@ -39,7 +41,7 @@ export interface ClipboardData {
|
|||||||
|
|
||||||
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number];
|
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number];
|
||||||
|
|
||||||
type ParsedClipboardEvent =
|
type ParsedClipboardEventTextData =
|
||||||
| { type: "text"; value: string }
|
| { type: "text"; value: string }
|
||||||
| { type: "mixedContent"; value: PastedMixedContent };
|
| { type: "mixedContent"; value: PastedMixedContent };
|
||||||
|
|
||||||
@@ -75,7 +77,7 @@ export const createPasteEvent = ({
|
|||||||
types,
|
types,
|
||||||
files,
|
files,
|
||||||
}: {
|
}: {
|
||||||
types?: { [key in AllowedPasteMimeTypes]?: string };
|
types?: { [key in AllowedPasteMimeTypes]?: string | File };
|
||||||
files?: File[];
|
files?: File[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!types && !files) {
|
if (!types && !files) {
|
||||||
@@ -88,6 +90,11 @@ export const createPasteEvent = ({
|
|||||||
|
|
||||||
if (types) {
|
if (types) {
|
||||||
for (const [type, value] of Object.entries(types)) {
|
for (const [type, value] of Object.entries(types)) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
files = files || [];
|
||||||
|
files.push(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
event.clipboardData?.setData(type, value);
|
event.clipboardData?.setData(type, value);
|
||||||
if (event.clipboardData?.getData(type) !== value) {
|
if (event.clipboardData?.getData(type) !== value) {
|
||||||
@@ -217,14 +224,14 @@ function parseHTMLTree(el: ChildNode) {
|
|||||||
const maybeParseHTMLPaste = (
|
const maybeParseHTMLPaste = (
|
||||||
event: ClipboardEvent,
|
event: ClipboardEvent,
|
||||||
): { type: "mixedContent"; value: PastedMixedContent } | null => {
|
): { type: "mixedContent"; value: PastedMixedContent } | null => {
|
||||||
const html = event.clipboardData?.getData("text/html");
|
const html = event.clipboardData?.getData(MIME_TYPES.html);
|
||||||
|
|
||||||
if (!html) {
|
if (!html) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
const doc = new DOMParser().parseFromString(html, MIME_TYPES.html);
|
||||||
|
|
||||||
const content = parseHTMLTree(doc.body);
|
const content = parseHTMLTree(doc.body);
|
||||||
|
|
||||||
@@ -238,34 +245,44 @@ const maybeParseHTMLPaste = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads OS clipboard programmatically. May not work on all browsers.
|
||||||
|
* Will prompt user for permission if not granted.
|
||||||
|
*/
|
||||||
export const readSystemClipboard = async () => {
|
export const readSystemClipboard = async () => {
|
||||||
const types: { [key in AllowedPasteMimeTypes]?: string } = {};
|
const types: { [key in AllowedPasteMimeTypes]?: string | File } = {};
|
||||||
|
|
||||||
try {
|
|
||||||
if (navigator.clipboard?.readText) {
|
|
||||||
return { "text/plain": await navigator.clipboard?.readText() };
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (navigator.clipboard?.read) {
|
|
||||||
console.warn(
|
|
||||||
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let clipboardItems: ClipboardItems;
|
let clipboardItems: ClipboardItems;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clipboardItems = await navigator.clipboard?.read();
|
clipboardItems = await navigator.clipboard?.read();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.name === "DataError") {
|
try {
|
||||||
console.warn(
|
if (navigator.clipboard?.readText) {
|
||||||
`navigator.clipboard.read() error, clipboard is probably empty: ${error.message}`,
|
console.warn(
|
||||||
);
|
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
|
||||||
return types;
|
);
|
||||||
|
const readText = await navigator.clipboard?.readText();
|
||||||
|
if (readText) {
|
||||||
|
return { [MIME_TYPES.text]: readText };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (navigator.clipboard?.read) {
|
||||||
|
console.warn(
|
||||||
|
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (error.name === "DataError") {
|
||||||
|
console.warn(
|
||||||
|
`navigator.clipboard.read() error, clipboard is probably empty: ${error.message}`,
|
||||||
|
);
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -276,10 +293,20 @@ export const readSystemClipboard = async () => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
types[type] = await (await item.getType(type)).text();
|
if (type === MIME_TYPES.text || type === MIME_TYPES.html) {
|
||||||
|
types[type] = await (await item.getType(type)).text();
|
||||||
|
} else if (isSupportedImageFileType(type)) {
|
||||||
|
const imageBlob = await item.getType(type);
|
||||||
|
const file = createFile(imageBlob, type, undefined);
|
||||||
|
types[type] = file;
|
||||||
|
} else {
|
||||||
|
throw new ExcalidrawError(`Unsupported clipboard type: ${type}`);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
error instanceof ExcalidrawError
|
||||||
|
? error.message
|
||||||
|
: `Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,10 +323,10 @@ export const readSystemClipboard = async () => {
|
|||||||
/**
|
/**
|
||||||
* Parses "paste" ClipboardEvent.
|
* Parses "paste" ClipboardEvent.
|
||||||
*/
|
*/
|
||||||
const parseClipboardEvent = async (
|
const parseClipboardEventTextData = async (
|
||||||
event: ClipboardEvent,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<ParsedClipboardEvent> => {
|
): Promise<ParsedClipboardEventTextData> => {
|
||||||
try {
|
try {
|
||||||
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
||||||
|
|
||||||
@@ -308,7 +335,7 @@ const parseClipboardEvent = async (
|
|||||||
return {
|
return {
|
||||||
type: "text",
|
type: "text",
|
||||||
value:
|
value:
|
||||||
event.clipboardData?.getData("text/plain") ||
|
event.clipboardData?.getData(MIME_TYPES.text) ||
|
||||||
mixedContent.value
|
mixedContent.value
|
||||||
.map((item) => item.value)
|
.map((item) => item.value)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
@@ -319,7 +346,7 @@ const parseClipboardEvent = async (
|
|||||||
return mixedContent;
|
return mixedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = event.clipboardData?.getData("text/plain");
|
const text = event.clipboardData?.getData(MIME_TYPES.text);
|
||||||
|
|
||||||
return { type: "text", value: (text || "").trim() };
|
return { type: "text", value: (text || "").trim() };
|
||||||
} catch {
|
} catch {
|
||||||
@@ -328,13 +355,16 @@ const parseClipboardEvent = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to parse clipboard. Prefers system clipboard.
|
* Attempts to parse clipboard event.
|
||||||
*/
|
*/
|
||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
const parsedEventData = await parseClipboardEventTextData(
|
||||||
|
event,
|
||||||
|
isPlainPaste,
|
||||||
|
);
|
||||||
|
|
||||||
if (parsedEventData.type === "mixedContent") {
|
if (parsedEventData.type === "mixedContent") {
|
||||||
return {
|
return {
|
||||||
@@ -423,8 +453,8 @@ export const copyTextToSystemClipboard = async (
|
|||||||
// (2) if fails and we have access to ClipboardEvent, use plain old setData()
|
// (2) if fails and we have access to ClipboardEvent, use plain old setData()
|
||||||
try {
|
try {
|
||||||
if (clipboardEvent) {
|
if (clipboardEvent) {
|
||||||
clipboardEvent.clipboardData?.setData("text/plain", text || "");
|
clipboardEvent.clipboardData?.setData(MIME_TYPES.text, text || "");
|
||||||
if (clipboardEvent.clipboardData?.getData("text/plain") !== text) {
|
if (clipboardEvent.clipboardData?.getData(MIME_TYPES.text) !== text) {
|
||||||
throw new Error("Failed to setData on clipboardEvent");
|
throw new Error("Failed to setData on clipboardEvent");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import {
|
|||||||
} from "../appState";
|
} from "../appState";
|
||||||
import type { PastedMixedContent } from "../clipboard";
|
import type { PastedMixedContent } from "../clipboard";
|
||||||
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
||||||
import { ARROW_TYPE, isSafari, type EXPORT_IMAGE_TYPES } from "../constants";
|
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
CURSOR_TYPE,
|
CURSOR_TYPE,
|
||||||
@@ -88,6 +87,11 @@ import {
|
|||||||
supportsResizeObserver,
|
supportsResizeObserver,
|
||||||
DEFAULT_COLLISION_THRESHOLD,
|
DEFAULT_COLLISION_THRESHOLD,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
|
ARROW_TYPE,
|
||||||
|
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
||||||
|
isSafari,
|
||||||
|
type EXPORT_IMAGE_TYPES,
|
||||||
|
DOUBLE_CLICK_POINTERUP_TIMEOUT,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import type { ExportedElements } from "../data";
|
import type { ExportedElements } from "../data";
|
||||||
import { exportCanvas, loadFromBlob } from "../data";
|
import { exportCanvas, loadFromBlob } from "../data";
|
||||||
@@ -461,6 +465,8 @@ import {
|
|||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { cropElement } from "../element/cropElement";
|
import { cropElement } from "../element/cropElement";
|
||||||
import { wrapText } from "../element/textWrapping";
|
import { wrapText } from "../element/textWrapping";
|
||||||
|
import { actionCopyElementLink } from "../actions/actionElementLink";
|
||||||
|
import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -1202,6 +1208,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
getContainingFrame(el, this.scene.getNonDeletedElementsMap()),
|
getContainingFrame(el, this.scene.getNonDeletedElementsMap()),
|
||||||
this.elementsPendingErasure,
|
this.elementsPendingErasure,
|
||||||
null,
|
null,
|
||||||
|
this.state.openDialog?.name === "elementLinkSelector"
|
||||||
|
? DEFAULT_REDUCED_GLOBAL_ALPHA
|
||||||
|
: 1,
|
||||||
),
|
),
|
||||||
["--embeddable-radius" as string]: `${getCornerRadius(
|
["--embeddable-radius" as string]: `${getCornerRadius(
|
||||||
Math.min(el.width, el.height),
|
Math.min(el.width, el.height),
|
||||||
@@ -1333,8 +1342,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
_cache: new Map(),
|
_cache: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
|
||||||
|
if (frame) {
|
||||||
|
mutateElement(frame, { name: frame.name?.trim() || null });
|
||||||
|
}
|
||||||
|
this.setState({ editingFrame: null });
|
||||||
|
};
|
||||||
|
|
||||||
private renderFrameNames = () => {
|
private renderFrameNames = () => {
|
||||||
if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
|
if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
|
||||||
|
if (this.state.editingFrame) {
|
||||||
|
this.resetEditingFrame(null);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1356,6 +1375,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
if (this.state.editingFrame === f.id) {
|
||||||
|
this.resetEditingFrame(f);
|
||||||
|
}
|
||||||
// if frame not visible, don't render its name
|
// if frame not visible, don't render its name
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1367,11 +1389,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const FRAME_NAME_EDIT_PADDING = 6;
|
const FRAME_NAME_EDIT_PADDING = 6;
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
mutateElement(f, { name: f.name?.trim() || null });
|
|
||||||
this.setState({ editingFrame: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
let frameNameJSX;
|
let frameNameJSX;
|
||||||
|
|
||||||
const frameName = getFrameLikeTitle(f);
|
const frameName = getFrameLikeTitle(f);
|
||||||
@@ -1389,13 +1406,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => e.target.select()}
|
onFocus={(e) => e.target.select()}
|
||||||
onBlur={() => reset()}
|
onBlur={() => this.resetEditingFrame(f)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
// for some inexplicable reason, `onBlur` triggered on ESC
|
// for some inexplicable reason, `onBlur` triggered on ESC
|
||||||
// does not reset `state.editingFrame` despite being called,
|
// does not reset `state.editingFrame` despite being called,
|
||||||
// and we need to reset it here as well
|
// and we need to reset it here as well
|
||||||
if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
||||||
reset();
|
this.resetEditingFrame(f);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
@@ -1520,7 +1537,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw excalidraw-container", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
"excalidraw--view-mode": this.state.viewModeEnabled,
|
"excalidraw--view-mode":
|
||||||
|
this.state.viewModeEnabled ||
|
||||||
|
this.state.openDialog?.name === "elementLinkSelector",
|
||||||
"excalidraw--mobile": this.device.editor.isMobile,
|
"excalidraw--mobile": this.device.editor.isMobile,
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
@@ -1579,6 +1598,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
app={this}
|
app={this}
|
||||||
isCollaborating={this.props.isCollaborating}
|
isCollaborating={this.props.isCollaborating}
|
||||||
|
generateLinkForSelection={
|
||||||
|
this.props.generateLinkForSelection
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
@@ -1590,6 +1612,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
trails={[this.laserTrails, this.eraserTrail]}
|
trails={[this.laserTrails, this.eraserTrail]}
|
||||||
/>
|
/>
|
||||||
{selectedElements.length === 1 &&
|
{selectedElements.length === 1 &&
|
||||||
|
this.state.openDialog?.name !==
|
||||||
|
"elementLinkSelector" &&
|
||||||
this.state.showHyperlinkPopup && (
|
this.state.showHyperlinkPopup && (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
key={firstSelectedElement.id}
|
key={firstSelectedElement.id}
|
||||||
@@ -2325,6 +2349,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.fonts.loadSceneFonts().then((fontFaces) => {
|
this.fonts.loadSceneFonts().then((fontFaces) => {
|
||||||
this.fonts.onLoaded(fontFaces);
|
this.fonts.onLoaded(fontFaces);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isElementLink(window.location.href)) {
|
||||||
|
this.scrollToContent(window.location.href, { animate: false });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private isMobileBreakpoint = (width: number, height: number) => {
|
private isMobileBreakpoint = (width: number, height: number) => {
|
||||||
@@ -2761,6 +2789,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.deselectElements();
|
this.deselectElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
if (
|
||||||
|
(prevState.openDialog?.name === "elementLinkSelector" ||
|
||||||
|
this.state.openDialog?.name === "elementLinkSelector") &&
|
||||||
|
prevState.openDialog?.name !== this.state.openDialog?.name
|
||||||
|
) {
|
||||||
|
this.deselectElements();
|
||||||
|
this.setState({
|
||||||
|
hoveredElementIds: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
|
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
|
||||||
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
|
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
|
||||||
}
|
}
|
||||||
@@ -3176,6 +3216,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
[el.points[0], el.points[el.points.length - 1]],
|
[el.points[0], el.points[el.points.length - 1]],
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
zoom: this.state.zoom,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3623,7 +3667,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private cancelInProgressAnimation: (() => void) | null = null;
|
private cancelInProgressAnimation: (() => void) | null = null;
|
||||||
|
|
||||||
scrollToContent = (
|
scrollToContent = (
|
||||||
|
/**
|
||||||
|
* target to scroll to
|
||||||
|
*
|
||||||
|
* - string - id of element or group, or url containing elementLink
|
||||||
|
* - ExcalidrawElement | ExcalidrawElement[] - element(s) objects
|
||||||
|
*/
|
||||||
target:
|
target:
|
||||||
|
| string
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
||||||
opts?: (
|
opts?: (
|
||||||
@@ -3650,6 +3701,34 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
canvasOffsets?: Offsets;
|
canvasOffsets?: Offsets;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
if (typeof target === "string") {
|
||||||
|
let id: string | null;
|
||||||
|
if (isElementLink(target)) {
|
||||||
|
id = parseElementLinkFromURL(target);
|
||||||
|
} else {
|
||||||
|
id = target;
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
const elements = this.scene.getElementsFromId(id);
|
||||||
|
|
||||||
|
if (elements?.length) {
|
||||||
|
this.scrollToContent(elements, {
|
||||||
|
fitToContent: opts?.fitToContent ?? true,
|
||||||
|
animate: opts?.animate ?? true,
|
||||||
|
});
|
||||||
|
} else if (isElementLink(target)) {
|
||||||
|
this.setState({
|
||||||
|
toast: {
|
||||||
|
message: t("elementLink.notFound"),
|
||||||
|
duration: 3000,
|
||||||
|
closable: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.cancelInProgressAnimation?.();
|
this.cancelInProgressAnimation?.();
|
||||||
|
|
||||||
// convert provided target into ExcalidrawElement[] if necessary
|
// convert provided target into ExcalidrawElement[] if necessary
|
||||||
@@ -3804,14 +3883,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
nextFiles[fileData.id] = fileData;
|
nextFiles[fileData.id] = fileData;
|
||||||
|
|
||||||
if (fileData.mimeType === MIME_TYPES.svg) {
|
if (fileData.mimeType === MIME_TYPES.svg) {
|
||||||
const restoredDataURL = getDataURL_sync(
|
try {
|
||||||
normalizeSVG(dataURLToString(fileData.dataURL)),
|
const restoredDataURL = getDataURL_sync(
|
||||||
MIME_TYPES.svg,
|
normalizeSVG(dataURLToString(fileData.dataURL)),
|
||||||
);
|
MIME_TYPES.svg,
|
||||||
if (fileData.dataURL !== restoredDataURL) {
|
);
|
||||||
// bump version so persistence layer can update the store
|
if (fileData.dataURL !== restoredDataURL) {
|
||||||
fileData.version = (fileData.version ?? 1) + 1;
|
// bump version so persistence layer can update the store
|
||||||
fileData.dataURL = restoredDataURL;
|
fileData.version = (fileData.version ?? 1) + 1;
|
||||||
|
fileData.dataURL = restoredDataURL;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4214,6 +4297,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.actionManager.handleKeyDown(event)) {
|
if (this.actionManager.handleKeyDown(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4290,6 +4377,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||||
simultaneouslyUpdated: selectedElements,
|
simultaneouslyUpdated: selectedElements,
|
||||||
|
zoom: this.state.zoom,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4299,6 +4387,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(element) => element.id !== elbowArrow?.id || step !== 0,
|
(element) => element.id !== elbowArrow?.id || step !== 0,
|
||||||
),
|
),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4485,7 +4574,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.SPACE) {
|
if (event.key === KEYS.SPACE) {
|
||||||
if (this.state.viewModeEnabled) {
|
if (
|
||||||
|
this.state.viewModeEnabled ||
|
||||||
|
this.state.openDialog?.name === "elementLinkSelector"
|
||||||
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
} else if (this.state.activeTool.type === "selection") {
|
} else if (this.state.activeTool.type === "selection") {
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
@@ -4511,6 +4603,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene,
|
this.scene,
|
||||||
isBindingEnabled(this.state),
|
isBindingEnabled(this.state),
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
this.setState({ suggestedBindings: [] });
|
this.setState({ suggestedBindings: [] });
|
||||||
}
|
}
|
||||||
@@ -5008,7 +5101,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isImageElement(element) ? 0 : this.getElementHitThreshold(),
|
isImageElement(element) ? 0 : this.getElementHitThreshold(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isPointInShape(pointFrom(x, y), selectionShape);
|
// if hitting the bounding box, return early
|
||||||
|
// but if not, we should check for other cases as well (e.g. frame name)
|
||||||
|
if (isPointInShape(pointFrom(x, y), selectionShape)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// take bound text element into consideration for hit collision as well
|
// take bound text element into consideration for hit collision as well
|
||||||
@@ -5253,6 +5350,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private handleCanvasDoubleClick = (
|
private handleCanvasDoubleClick = (
|
||||||
event: React.MouseEvent<HTMLCanvasElement>,
|
event: React.MouseEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
|
if (
|
||||||
|
this.lastPointerDownEvent &&
|
||||||
|
event.timeStamp - this.lastPointerDownEvent.timeStamp >
|
||||||
|
DOUBLE_CLICK_POINTERUP_TIMEOUT
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// case: double-clicking with arrow/line tool selected would both create
|
// case: double-clicking with arrow/line tool selected would both create
|
||||||
// text and enter multiElement mode
|
// text and enter multiElement mode
|
||||||
if (this.state.multiElement) {
|
if (this.state.multiElement) {
|
||||||
@@ -5372,18 +5477,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scenePointer: Readonly<{ x: number; y: number }>,
|
scenePointer: Readonly<{ x: number; y: number }>,
|
||||||
hitElement: NonDeletedExcalidrawElement | null,
|
hitElement: NonDeletedExcalidrawElement | null,
|
||||||
): ExcalidrawElement | undefined => {
|
): ExcalidrawElement | undefined => {
|
||||||
// Reversing so we traverse the elements in decreasing order
|
const elements = this.scene.getNonDeletedElements();
|
||||||
// of z-index
|
let hitElementIndex = -1;
|
||||||
const elements = this.scene.getNonDeletedElements().slice().reverse();
|
|
||||||
let hitElementIndex = Infinity;
|
|
||||||
|
|
||||||
return elements.find((element, index) => {
|
for (let index = elements.length - 1; index >= 0; index--) {
|
||||||
|
const element = elements[index];
|
||||||
if (hitElement && element.id === hitElement.id) {
|
if (hitElement && element.id === hitElement.id) {
|
||||||
hitElementIndex = index;
|
hitElementIndex = index;
|
||||||
}
|
}
|
||||||
return (
|
if (
|
||||||
element.link &&
|
element.link &&
|
||||||
index <= hitElementIndex &&
|
index >= hitElementIndex &&
|
||||||
isPointHittingLink(
|
isPointHittingLink(
|
||||||
element,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
@@ -5391,8 +5495,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointFrom(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
)
|
)
|
||||||
);
|
) {
|
||||||
});
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private redirectToLink = (
|
private redirectToLink = (
|
||||||
@@ -5409,12 +5515,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.lastPointerUpEvent!.clientY,
|
this.lastPointerUpEvent!.clientY,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (
|
if (!this.hitLinkElement || draggedDistance > DRAGGING_THRESHOLD) {
|
||||||
!this.hitLinkElement ||
|
|
||||||
// For touch screen allow dragging threshold else strict check
|
|
||||||
(isTouchScreen && draggedDistance > DRAGGING_THRESHOLD) ||
|
|
||||||
(!isTouchScreen && draggedDistance !== 0)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
||||||
@@ -5441,6 +5542,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
|
hideHyperlinkToolip();
|
||||||
let url = this.hitLinkElement.link;
|
let url = this.hitLinkElement.link;
|
||||||
if (url) {
|
if (url) {
|
||||||
url = normalizeLink(url);
|
url = normalizeLink(url);
|
||||||
@@ -5768,6 +5870,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{
|
{
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
informMutation: false,
|
informMutation: false,
|
||||||
|
zoom: this.state.zoom,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -5827,6 +5930,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
(!this.state.selectedLinearElement ||
|
(!this.state.selectedLinearElement ||
|
||||||
this.state.selectedLinearElement.hoverPointIndex === -1) &&
|
this.state.selectedLinearElement.hoverPointIndex === -1) &&
|
||||||
|
this.state.openDialog?.name !== "elementLinkSelector" &&
|
||||||
!(selectedElements.length === 1 && isElbowArrow(selectedElements[0]))
|
!(selectedElements.length === 1 && isElbowArrow(selectedElements[0]))
|
||||||
) {
|
) {
|
||||||
const elementWithTransformHandleType =
|
const elementWithTransformHandleType =
|
||||||
@@ -5851,7 +5955,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (selectedElements.length > 1 && !isOverScrollBar) {
|
} else if (
|
||||||
|
selectedElements.length > 1 &&
|
||||||
|
!isOverScrollBar &&
|
||||||
|
this.state.openDialog?.name !== "elementLinkSelector"
|
||||||
|
) {
|
||||||
const transformHandleType = getTransformHandleTypeFromCoords(
|
const transformHandleType = getTransformHandleTypeFromCoords(
|
||||||
getCommonBounds(selectedElements),
|
getCommonBounds(selectedElements),
|
||||||
scenePointerX,
|
scenePointerX,
|
||||||
@@ -5910,6 +6018,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
} else if (this.state.viewModeEnabled) {
|
} else if (this.state.viewModeEnabled) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
|
} else if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||||
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (isOverScrollBar) {
|
} else if (isOverScrollBar) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (this.state.selectedLinearElement) {
|
} else if (this.state.selectedLinearElement) {
|
||||||
@@ -5955,6 +6065,32 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.openDialog?.name === "elementLinkSelector" && hitElement) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
return {
|
||||||
|
hoveredElementIds: updateStable(
|
||||||
|
prevState.hoveredElementIds,
|
||||||
|
selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
editingGroupId: prevState.editingGroupId,
|
||||||
|
selectedElementIds: { [hitElement.id]: true },
|
||||||
|
},
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
|
).selectedElementIds,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
this.state.openDialog?.name === "elementLinkSelector" &&
|
||||||
|
!hitElement
|
||||||
|
) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
hoveredElementIds: updateStable(prevState.hoveredElementIds, {}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleEraser = (
|
private handleEraser = (
|
||||||
@@ -6152,6 +6288,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
) => {
|
) => {
|
||||||
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
||||||
|
|
||||||
this.maybeUnfollowRemoteUser();
|
this.maybeUnfollowRemoteUser();
|
||||||
|
|
||||||
if (this.state.searchMatches) {
|
if (this.state.searchMatches) {
|
||||||
@@ -6212,7 +6349,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction:
|
||||||
|
this.state.openDialog?.name === "elementLinkSelector"
|
||||||
|
? StoreAction.NONE
|
||||||
|
: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6939,6 +7079,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||||
|
pointerDownState.origin,
|
||||||
|
pointerDownState.hit.element,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.hitLinkElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.croppingElementId &&
|
this.state.croppingElementId &&
|
||||||
pointerDownState.hit.element?.id !== this.state.croppingElementId
|
pointerDownState.hit.element?.id !== this.state.croppingElementId
|
||||||
@@ -7032,7 +7181,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
) {
|
) {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
const nextSelectedElementIds: { [id: string]: true } = {
|
let nextSelectedElementIds: { [id: string]: true } = {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
[hitElement.id]: true,
|
[hitElement.id]: true,
|
||||||
};
|
};
|
||||||
@@ -7103,6 +7252,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, in shape selection mode, we'd like to
|
||||||
|
// keep only one shape or group selected at a time.
|
||||||
|
// This means, if the hitElement is a different shape or group
|
||||||
|
// than the previously selected ones, we deselect the previous ones
|
||||||
|
// and select the hitElement
|
||||||
|
if (prevState.openDialog?.name === "elementLinkSelector") {
|
||||||
|
if (
|
||||||
|
!hitElement.groupIds.some(
|
||||||
|
(gid) => prevState.selectedGroupIds[gid],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
nextSelectedElementIds = {
|
||||||
|
[hitElement.id]: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...selectGroupsForSelectedElements(
|
...selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
@@ -7253,6 +7419,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -7550,6 +7717,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
isElbowArrow(element),
|
isElbowArrow(element),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -7747,6 +7915,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
) {
|
) {
|
||||||
return withBatchedUpdatesThrottled((event: PointerEvent) => {
|
return withBatchedUpdatesThrottled((event: PointerEvent) => {
|
||||||
|
if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
||||||
const lastPointerCoords =
|
const lastPointerCoords =
|
||||||
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
||||||
@@ -8125,6 +8296,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
suggestedBindings: getSuggestedBindingsForArrows(
|
suggestedBindings: getSuggestedBindingsForArrows(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -8293,6 +8465,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{
|
{
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
informMutation: false,
|
informMutation: false,
|
||||||
|
zoom: this.state.zoom,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (points.length === 2) {
|
} else if (points.length === 2) {
|
||||||
@@ -9257,6 +9430,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene,
|
this.scene,
|
||||||
isBindingEnabled(this.state),
|
isBindingEnabled(this.state),
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9749,6 +9923,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerCoords,
|
pointerCoords,
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
suggestedBindings:
|
suggestedBindings:
|
||||||
@@ -9777,6 +9952,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
coords,
|
coords,
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
isArrowElement(linearElement) && isElbowArrow(linearElement),
|
isArrowElement(linearElement) && isElbowArrow(linearElement),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
@@ -10217,7 +10393,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const [x, y] = getGridPoint(
|
const [x, y] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||||
this.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const croppingElement = this.scene
|
const croppingElement = this.scene
|
||||||
@@ -10243,6 +10419,28 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
image &&
|
image &&
|
||||||
!(image instanceof Promise)
|
!(image instanceof Promise)
|
||||||
) {
|
) {
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dragOffset = {
|
||||||
|
x: gridX - pointerDownState.originInGrid.x,
|
||||||
|
y: gridY - pointerDownState.originInGrid.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.maybeCacheReferenceSnapPoints(event, [croppingElement]);
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapResizingElements(
|
||||||
|
[croppingElement],
|
||||||
|
[croppingAtStateStart],
|
||||||
|
this,
|
||||||
|
event,
|
||||||
|
dragOffset,
|
||||||
|
transformHandleType,
|
||||||
|
);
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
croppingElement,
|
croppingElement,
|
||||||
cropElement(
|
cropElement(
|
||||||
@@ -10250,8 +10448,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
transformHandleType,
|
transformHandleType,
|
||||||
image.naturalWidth,
|
image.naturalWidth,
|
||||||
image.naturalHeight,
|
image.naturalHeight,
|
||||||
x,
|
x + snapOffset.x,
|
||||||
y,
|
y + snapOffset.y,
|
||||||
event.shiftKey
|
event.shiftKey
|
||||||
? croppingAtStateStart.width / croppingAtStateStart.height
|
? croppingAtStateStart.width / croppingAtStateStart.height
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -10271,6 +10469,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isCropping: transformHandleType && transformHandleType !== "rotation",
|
isCropping: transformHandleType && transformHandleType !== "rotation",
|
||||||
|
snapLines,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10381,6 +10580,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
transformHandleType,
|
transformHandleType,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
|
this.scene,
|
||||||
shouldRotateWithDiscreteAngle(event),
|
shouldRotateWithDiscreteAngle(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
selectedElements.some((element) => isImageElement(element))
|
selectedElements.some((element) => isImageElement(element))
|
||||||
@@ -10395,6 +10595,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const suggestedBindings = getSuggestedBindingsForArrows(
|
const suggestedBindings = getSuggestedBindingsForArrows(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
const elementsToHighlight = new Set<ExcalidrawElement>();
|
const elementsToHighlight = new Set<ExcalidrawElement>();
|
||||||
@@ -10498,7 +10699,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionFlipVertical,
|
actionFlipVertical,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionToggleLinearEditor,
|
actionToggleLinearEditor,
|
||||||
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionLink,
|
actionLink,
|
||||||
|
actionCopyElementLink,
|
||||||
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionDuplicateSelection,
|
actionDuplicateSelection,
|
||||||
actionToggleElementLock,
|
actionToggleElementLock,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ import { trackEvent } from "../../analytics";
|
|||||||
import { useStable } from "../../hooks/useStable";
|
import { useStable } from "../../hooks/useStable";
|
||||||
|
|
||||||
import "./CommandPalette.scss";
|
import "./CommandPalette.scss";
|
||||||
|
import {
|
||||||
|
actionCopyElementLink,
|
||||||
|
actionLinkToElement,
|
||||||
|
} from "../../actions/actionElementLink";
|
||||||
|
|
||||||
const lastUsedPaletteItem = atom<CommandPaletteItem | null>(null);
|
const lastUsedPaletteItem = atom<CommandPaletteItem | null>(null);
|
||||||
|
|
||||||
@@ -281,6 +285,8 @@ function CommandPaletteInner({
|
|||||||
actionManager.actions.toggleLinearEditor,
|
actionManager.actions.toggleLinearEditor,
|
||||||
actionManager.actions.cropEditor,
|
actionManager.actions.cropEditor,
|
||||||
actionLink,
|
actionLink,
|
||||||
|
actionCopyElementLink,
|
||||||
|
actionLinkToElement,
|
||||||
].map((action: Action) =>
|
].map((action: Action) =>
|
||||||
actionToCommand(
|
actionToCommand(
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { flushSync } from "react-dom";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import type { DialogProps } from "./Dialog";
|
import type { DialogProps } from "./Dialog";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
@@ -43,7 +44,14 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
setIsLibraryMenuOpen(false);
|
setIsLibraryMenuOpen(false);
|
||||||
onCancel();
|
// flush any pending updates synchronously,
|
||||||
|
// otherwise it could lead to crash in some chromium versions (131.0.6778.86),
|
||||||
|
// when `.focus` is invoked with container in some intermediate state
|
||||||
|
// (container seems mounted in DOM, but focus still causes a crash)
|
||||||
|
flushSync(() => {
|
||||||
|
onCancel();
|
||||||
|
});
|
||||||
|
|
||||||
container?.focus();
|
container?.focus();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -52,7 +60,14 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
setIsLibraryMenuOpen(false);
|
setIsLibraryMenuOpen(false);
|
||||||
onConfirm();
|
// flush any pending updates synchronously,
|
||||||
|
// otherwise it leads to crash in some chromium versions (131.0.6778.86),
|
||||||
|
// when `.focus` is invoked with container in some intermediate state
|
||||||
|
// (container seems mounted in DOM, but focus still causes a crash)
|
||||||
|
flushSync(() => {
|
||||||
|
onConfirm();
|
||||||
|
});
|
||||||
|
|
||||||
container?.focus();
|
container?.focus();
|
||||||
}}
|
}}
|
||||||
actionType="danger"
|
actionType="danger"
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
@import "../css/variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.ElementLinkDialog {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--editor-container-padding);
|
||||||
|
left: var(--editor-container-padding);
|
||||||
|
|
||||||
|
z-index: var(--zIndex-modal);
|
||||||
|
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
left: 0;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ElementLinkDialog__header {
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ElementLinkDialog__input {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.ElementLinkDialog__input-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ElementLinkDialog__remove {
|
||||||
|
color: $oc-red-9;
|
||||||
|
margin-left: 1rem;
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon svg {
|
||||||
|
color: $oc-red-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ElementLinkDialog__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { TextField } from "./TextField";
|
||||||
|
import type { AppProps, AppState, UIAppState } from "../types";
|
||||||
|
import DialogActionButton from "./DialogActionButton";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import {
|
||||||
|
defaultGetElementLinkFromSelection,
|
||||||
|
getLinkIdAndTypeFromSelection,
|
||||||
|
} from "../element/elementLink";
|
||||||
|
import { mutateElement } from "../element/mutateElement";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import type { ElementsMap, ExcalidrawElement } from "../element/types";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { TrashIcon } from "./icons";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
|
import "./ElementLinkDialog.scss";
|
||||||
|
import { normalizeLink } from "../data/url";
|
||||||
|
|
||||||
|
const ElementLinkDialog = ({
|
||||||
|
sourceElementId,
|
||||||
|
onClose,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||||
|
}: {
|
||||||
|
sourceElementId: ExcalidrawElement["id"];
|
||||||
|
elementsMap: ElementsMap;
|
||||||
|
appState: UIAppState;
|
||||||
|
onClose?: () => void;
|
||||||
|
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||||
|
}) => {
|
||||||
|
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||||
|
|
||||||
|
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||||
|
const [linkEdited, setLinkEdited] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedElements = getSelectedElements(elementsMap, appState);
|
||||||
|
let nextLink = originalLink;
|
||||||
|
|
||||||
|
if (selectedElements.length > 0 && generateLinkForSelection) {
|
||||||
|
const idAndType = getLinkIdAndTypeFromSelection(
|
||||||
|
selectedElements,
|
||||||
|
appState as AppState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idAndType) {
|
||||||
|
nextLink = normalizeLink(
|
||||||
|
generateLinkForSelection(idAndType.id, idAndType.type),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setNextLink(nextLink);
|
||||||
|
}, [
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
appState.selectedElementIds,
|
||||||
|
originalLink,
|
||||||
|
generateLinkForSelection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
||||||
|
const elementToLink = elementsMap.get(sourceElementId);
|
||||||
|
elementToLink &&
|
||||||
|
mutateElement(elementToLink, {
|
||||||
|
link: nextLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextLink && linkEdited && sourceElementId) {
|
||||||
|
const elementToLink = elementsMap.get(sourceElementId);
|
||||||
|
elementToLink &&
|
||||||
|
mutateElement(elementToLink, {
|
||||||
|
link: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose?.();
|
||||||
|
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
appState.openDialog?.name === "elementLinkSelector" &&
|
||||||
|
event.key === KEYS.ENTER
|
||||||
|
) {
|
||||||
|
handleConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.openDialog?.name === "elementLinkSelector" &&
|
||||||
|
event.key === KEYS.ESCAPE
|
||||||
|
) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [appState, onClose, handleConfirm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ElementLinkDialog">
|
||||||
|
<div className="ElementLinkDialog__header">
|
||||||
|
<h2>{t("elementLink.title")}</h2>
|
||||||
|
<p>{t("elementLink.desc")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ElementLinkDialog__input">
|
||||||
|
<TextField
|
||||||
|
value={nextLink ?? ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!linkEdited) {
|
||||||
|
setLinkEdited(true);
|
||||||
|
}
|
||||||
|
setNextLink(value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === KEYS.ENTER) {
|
||||||
|
handleConfirm();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="ElementLinkDialog__input-field"
|
||||||
|
selectOnRender
|
||||||
|
/>
|
||||||
|
|
||||||
|
{originalLink && nextLink && (
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.remove")}
|
||||||
|
aria-label={t("buttons.remove")}
|
||||||
|
label={t("buttons.remove")}
|
||||||
|
onClick={() => {
|
||||||
|
// removes the link from the input
|
||||||
|
// but doesn't update the element
|
||||||
|
|
||||||
|
// when confirmed, will remove the link from the element
|
||||||
|
setNextLink(null);
|
||||||
|
setLinkEdited(true);
|
||||||
|
}}
|
||||||
|
className="ElementLinkDialog__remove"
|
||||||
|
icon={TrashIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ElementLinkDialog__actions">
|
||||||
|
<DialogActionButton
|
||||||
|
label={t("buttons.cancel")}
|
||||||
|
onClick={() => {
|
||||||
|
onClose?.();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogActionButton
|
||||||
|
label={t("buttons.confirm")}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
actionType="primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ElementLinkDialog;
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
top: var(--editor-container-padding);
|
top: var(--editor-container-padding);
|
||||||
right: var(--editor-container-padding);
|
right: var(--editor-container-padding);
|
||||||
bottom: var(--editor-container-padding);
|
bottom: var(--editor-container-padding);
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_top.zen-mode {
|
.FixedSideContainer_side_top.zen-mode {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
@import "../css/variables.module.scss";
|
@import "../css/variables.module.scss";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.picker-container {
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker {
|
.picker {
|
||||||
|
padding: 0.5rem;
|
||||||
background: var(--popup-bg-color);
|
background: var(--popup-bg-color);
|
||||||
border: 0 solid transparentize($oc-white, 0.75);
|
border: 0 solid transparentize($oc-white, 0.75);
|
||||||
// ˇˇ yeah, i dunno, open to suggestions here :D
|
box-shadow: var(--shadow-island);
|
||||||
box-shadow: rgb(0 0 0 / 25%) 2px 2px 4px 2px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
:root[dir="rtl"] & {
|
||||||
|
padding: 0.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-container button,
|
.picker-container button,
|
||||||
@@ -55,47 +52,16 @@
|
|||||||
padding: 0.25rem 0.28rem 0.35rem 0.25rem;
|
padding: 0.25rem 0.28rem 0.35rem 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-triangle {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: relative;
|
|
||||||
top: -10px;
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
right: 12px;
|
|
||||||
}
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0 9px 10px;
|
|
||||||
border-color: transparent transparent transparentize($oc-black, 0.9);
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0 9px 10px;
|
|
||||||
border-color: transparent transparent var(--popup-bg-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-content {
|
.picker-content {
|
||||||
padding: 0.5rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, auto);
|
grid-template-columns: repeat(4, auto);
|
||||||
grid-gap: 0.5rem;
|
grid-gap: 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
:root[dir="rtl"] & {
|
}
|
||||||
padding: 0.4rem;
|
|
||||||
}
|
.picker-collapsible {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-keybinding {
|
.picker-keybinding {
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Popover } from "./Popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import "./IconPicker.scss";
|
import "./IconPicker.scss";
|
||||||
import { isArrowKey, KEYS } from "../keys";
|
import { isArrowKey, KEYS } from "../keys";
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import Collapsible from "./Stats/Collapsible";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
import { useDevice } from "..";
|
||||||
|
|
||||||
|
const moreOptionsAtom = atom(false);
|
||||||
|
|
||||||
|
type Option<T> = {
|
||||||
|
value: T;
|
||||||
|
text: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
keyBinding: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
function Picker<T>({
|
function Picker<T>({
|
||||||
options,
|
options,
|
||||||
@@ -12,30 +25,16 @@ function Picker<T>({
|
|||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
|
numberOfOptionsToAlwaysShow = options.length,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: T;
|
value: T;
|
||||||
options: {
|
options: readonly Option<T>[];
|
||||||
value: T;
|
|
||||||
text: string;
|
|
||||||
icon: JSX.Element;
|
|
||||||
keyBinding: string | null;
|
|
||||||
}[];
|
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
numberOfOptionsToAlwaysShow?: number;
|
||||||
}) {
|
}) {
|
||||||
const rFirstItem = React.useRef<HTMLButtonElement>();
|
const device = useDevice();
|
||||||
const rActiveItem = React.useRef<HTMLButtonElement>();
|
|
||||||
const rGallery = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// After the component is first mounted focus on first input
|
|
||||||
if (rActiveItem.current) {
|
|
||||||
rActiveItem.current.focus();
|
|
||||||
} else if (rGallery.current) {
|
|
||||||
rGallery.current.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
const pressedOption = options.find(
|
const pressedOption = options.find(
|
||||||
@@ -44,28 +43,19 @@ function Picker<T>({
|
|||||||
|
|
||||||
if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
|
if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
|
||||||
// Keybinding navigation
|
// Keybinding navigation
|
||||||
const index = options.indexOf(pressedOption);
|
onChange(pressedOption.value);
|
||||||
(rGallery!.current!.children![index] as any).focus();
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.TAB) {
|
} else if (event.key === KEYS.TAB) {
|
||||||
// Tab navigation cycle through options. If the user tabs
|
const index = options.findIndex((option) => option.value === value);
|
||||||
// away from the picker, close the picker. We need to use
|
const nextIndex = event.shiftKey
|
||||||
// a timeout here to let the stack clear before checking.
|
? (options.length + index - 1) % options.length
|
||||||
setTimeout(() => {
|
: (index + 1) % options.length;
|
||||||
const active = rActiveItem.current;
|
onChange(options[nextIndex].value);
|
||||||
const docActive = document.activeElement;
|
|
||||||
if (active !== docActive) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
} else if (isArrowKey(event.key)) {
|
} else if (isArrowKey(event.key)) {
|
||||||
// Arrow navigation
|
// Arrow navigation
|
||||||
const { activeElement } = document;
|
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
const index = Array.prototype.indexOf.call(
|
const index = options.findIndex((option) => option.value === value);
|
||||||
rGallery!.current!.children,
|
|
||||||
activeElement,
|
|
||||||
);
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const length = options.length;
|
const length = options.length;
|
||||||
let nextIndex = index;
|
let nextIndex = index;
|
||||||
@@ -73,19 +63,26 @@ function Picker<T>({
|
|||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
// Select the next option
|
// Select the next option
|
||||||
case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT:
|
case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT:
|
||||||
case KEYS.ARROW_DOWN: {
|
|
||||||
nextIndex = (index + 1) % length;
|
nextIndex = (index + 1) % length;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
// Select the previous option
|
// Select the previous option
|
||||||
case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT:
|
case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT:
|
||||||
case KEYS.ARROW_UP: {
|
|
||||||
nextIndex = (length + index - 1) % length;
|
nextIndex = (length + index - 1) % length;
|
||||||
break;
|
break;
|
||||||
|
// Go the next row
|
||||||
|
case KEYS.ARROW_DOWN: {
|
||||||
|
nextIndex = (index + (numberOfOptionsToAlwaysShow ?? 1)) % length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Go the previous row
|
||||||
|
case KEYS.ARROW_UP: {
|
||||||
|
nextIndex =
|
||||||
|
(length + index - (numberOfOptionsToAlwaysShow ?? 1)) % length;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(rGallery.current!.children![nextIndex] as any).focus();
|
onChange(options[nextIndex].value);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
||||||
@@ -97,15 +94,29 @@ function Picker<T>({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const [showMoreOptions, setShowMoreOptions] = useAtom(
|
||||||
<div
|
moreOptionsAtom,
|
||||||
className={`picker`}
|
jotaiScope,
|
||||||
role="dialog"
|
);
|
||||||
aria-modal="true"
|
|
||||||
aria-label={label}
|
const alwaysVisibleOptions = React.useMemo(
|
||||||
onKeyDown={handleKeyDown}
|
() => options.slice(0, numberOfOptionsToAlwaysShow),
|
||||||
>
|
[options, numberOfOptionsToAlwaysShow],
|
||||||
<div className="picker-content" ref={rGallery}>
|
);
|
||||||
|
const moreOptions = React.useMemo(
|
||||||
|
() => options.slice(numberOfOptionsToAlwaysShow),
|
||||||
|
[options, numberOfOptionsToAlwaysShow],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!alwaysVisibleOptions.some((option) => option.value === value)) {
|
||||||
|
setShowMoreOptions(true);
|
||||||
|
}
|
||||||
|
}, [value, alwaysVisibleOptions, setShowMoreOptions]);
|
||||||
|
|
||||||
|
const renderOptions = (options: Option<T>[]) => {
|
||||||
|
return (
|
||||||
|
<div className="picker-content">
|
||||||
{options.map((option, i) => (
|
{options.map((option, i) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -113,7 +124,6 @@ function Picker<T>({
|
|||||||
active: value === option.value,
|
active: value === option.value,
|
||||||
})}
|
})}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
(event.currentTarget as HTMLButtonElement).focus();
|
|
||||||
onChange(option.value);
|
onChange(option.value);
|
||||||
}}
|
}}
|
||||||
title={`${option.text} ${
|
title={`${option.text} ${
|
||||||
@@ -122,16 +132,13 @@ function Picker<T>({
|
|||||||
aria-label={option.text || "none"}
|
aria-label={option.text || "none"}
|
||||||
aria-keyshortcuts={option.keyBinding || undefined}
|
aria-keyshortcuts={option.keyBinding || undefined}
|
||||||
key={option.text}
|
key={option.text}
|
||||||
ref={(el) => {
|
ref={(ref) => {
|
||||||
if (el && i === 0) {
|
if (value === option.value) {
|
||||||
rFirstItem.current = el;
|
// Use a timeout here to render focus properly
|
||||||
|
setTimeout(() => {
|
||||||
|
ref?.focus();
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
if (el && option.value === value) {
|
|
||||||
rActiveItem.current = el;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
onChange(option.value);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.icon}
|
{option.icon}
|
||||||
@@ -141,7 +148,43 @@ function Picker<T>({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Content
|
||||||
|
side={
|
||||||
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "top"
|
||||||
|
: "bottom"
|
||||||
|
}
|
||||||
|
align="start"
|
||||||
|
sideOffset={12}
|
||||||
|
style={{ zIndex: "var(--zIndex-popup)" }}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`picker`}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{renderOptions(alwaysVisibleOptions)}
|
||||||
|
|
||||||
|
{moreOptions.length > 0 && (
|
||||||
|
<Collapsible
|
||||||
|
label={t("labels.more_options")}
|
||||||
|
open={showMoreOptions}
|
||||||
|
openTrigger={() => {
|
||||||
|
setShowMoreOptions((value) => !value);
|
||||||
|
}}
|
||||||
|
className="picker-collapsible"
|
||||||
|
>
|
||||||
|
{renderOptions(moreOptions)}
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +194,7 @@ export function IconPicker<T>({
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
group = "",
|
group = "",
|
||||||
|
numberOfOptionsToAlwaysShow,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: T;
|
value: T;
|
||||||
@@ -159,51 +203,40 @@ export function IconPicker<T>({
|
|||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
keyBinding: string | null;
|
keyBinding: string | null;
|
||||||
showInPicker?: boolean;
|
|
||||||
}[];
|
}[];
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
|
numberOfOptionsToAlwaysShow?: number;
|
||||||
group?: string;
|
group?: string;
|
||||||
}) {
|
}) {
|
||||||
const [isActive, setActive] = React.useState(false);
|
const [isActive, setActive] = React.useState(false);
|
||||||
const rPickerButton = React.useRef<any>(null);
|
const rPickerButton = React.useRef<any>(null);
|
||||||
const isRTL = getLanguage().rtl;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Popover.Root open={isActive} onOpenChange={(open) => setActive(open)}>
|
||||||
name={group}
|
<Popover.Trigger
|
||||||
type="button"
|
name={group}
|
||||||
className={isActive ? "active" : ""}
|
type="button"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onClick={() => setActive(!isActive)}
|
onClick={() => setActive(!isActive)}
|
||||||
ref={rPickerButton}
|
ref={rPickerButton}
|
||||||
>
|
className={isActive ? "active" : ""}
|
||||||
{options.find((option) => option.value === value)?.icon}
|
>
|
||||||
</button>
|
{options.find((option) => option.value === value)?.icon}
|
||||||
<React.Suspense fallback="">
|
</Popover.Trigger>
|
||||||
{isActive ? (
|
{isActive && (
|
||||||
<>
|
<Picker
|
||||||
<Popover
|
options={options}
|
||||||
onCloseRequest={(event) =>
|
value={value}
|
||||||
event.target !== rPickerButton.current && setActive(false)
|
label={label}
|
||||||
}
|
onChange={onChange}
|
||||||
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
|
onClose={() => {
|
||||||
>
|
setActive(false);
|
||||||
<Picker
|
}}
|
||||||
options={options.filter((opt) => opt.showInPicker !== false)}
|
numberOfOptionsToAlwaysShow={numberOfOptionsToAlwaysShow}
|
||||||
value={value}
|
/>
|
||||||
label={label}
|
)}
|
||||||
onChange={onChange}
|
</Popover.Root>
|
||||||
onClose={() => {
|
|
||||||
setActive(false);
|
|
||||||
rPickerButton.current?.focus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
<div className="picker-triangle" />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</React.Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import { LaserPointerButton } from "./LaserPointerButton";
|
|||||||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
|
import ElementLinkDialog from "./ElementLinkDialog";
|
||||||
|
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import "./Toolbar.scss";
|
import "./Toolbar.scss";
|
||||||
@@ -84,6 +85,7 @@ interface LayerUIProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
|
generateLinkForSelection?: AppProps["generateLinkForSelection"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
@@ -141,6 +143,7 @@ const LayerUI = ({
|
|||||||
children,
|
children,
|
||||||
app,
|
app,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
|
generateLinkForSelection,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
@@ -232,7 +235,8 @@ const LayerUI = ({
|
|||||||
const shouldShowStats =
|
const shouldShowStats =
|
||||||
appState.stats.open &&
|
appState.stats.open &&
|
||||||
!appState.zenModeEnabled &&
|
!appState.zenModeEnabled &&
|
||||||
!appState.viewModeEnabled;
|
!appState.viewModeEnabled &&
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
@@ -241,90 +245,91 @@ const LayerUI = ({
|
|||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled &&
|
||||||
<Section heading="shapes" className="shapes-section">
|
appState.openDialog?.name !== "elementLinkSelector" && (
|
||||||
{(heading: React.ReactNode) => (
|
<Section heading="shapes" className="shapes-section">
|
||||||
<div style={{ position: "relative" }}>
|
{(heading: React.ReactNode) => (
|
||||||
{renderWelcomeScreen && (
|
<div style={{ position: "relative" }}>
|
||||||
<tunnels.WelcomeScreenToolbarHintTunnel.Out />
|
{renderWelcomeScreen && (
|
||||||
)}
|
<tunnels.WelcomeScreenToolbarHintTunnel.Out />
|
||||||
<Stack.Col gap={4} align="start">
|
)}
|
||||||
<Stack.Row
|
<Stack.Col gap={4} align="start">
|
||||||
gap={1}
|
<Stack.Row
|
||||||
className={clsx("App-toolbar-container", {
|
gap={1}
|
||||||
"zen-mode": appState.zenModeEnabled,
|
className={clsx("App-toolbar-container", {
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Island
|
|
||||||
padding={1}
|
|
||||||
className={clsx("App-toolbar", {
|
|
||||||
"zen-mode": appState.zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HintViewer
|
<Island
|
||||||
appState={appState}
|
padding={1}
|
||||||
isMobile={device.editor.isMobile}
|
className={clsx("App-toolbar", {
|
||||||
device={device}
|
"zen-mode": appState.zenModeEnabled,
|
||||||
app={app}
|
})}
|
||||||
/>
|
>
|
||||||
{heading}
|
<HintViewer
|
||||||
<Stack.Row gap={1}>
|
|
||||||
<PenModeButton
|
|
||||||
zenModeEnabled={appState.zenModeEnabled}
|
|
||||||
checked={appState.penMode}
|
|
||||||
onChange={() => onPenModeToggle(null)}
|
|
||||||
title={t("toolBar.penMode")}
|
|
||||||
penDetected={appState.penDetected}
|
|
||||||
/>
|
|
||||||
<LockButton
|
|
||||||
checked={appState.activeTool.locked}
|
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="App-toolbar__divider" />
|
|
||||||
|
|
||||||
<HandButton
|
|
||||||
checked={isHandToolActive(appState)}
|
|
||||||
onChange={() => onHandToolToggle()}
|
|
||||||
title={t("toolBar.hand")}
|
|
||||||
isMobile
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ShapesSwitcher
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
activeTool={appState.activeTool}
|
isMobile={device.editor.isMobile}
|
||||||
UIOptions={UIOptions}
|
device={device}
|
||||||
app={app}
|
app={app}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
{heading}
|
||||||
</Island>
|
<Stack.Row gap={1}>
|
||||||
{isCollaborating && (
|
<PenModeButton
|
||||||
<Island
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
style={{
|
checked={appState.penMode}
|
||||||
marginLeft: 8,
|
onChange={() => onPenModeToggle(null)}
|
||||||
alignSelf: "center",
|
title={t("toolBar.penMode")}
|
||||||
height: "fit-content",
|
penDetected={appState.penDetected}
|
||||||
}}
|
/>
|
||||||
>
|
<LockButton
|
||||||
<LaserPointerButton
|
checked={appState.activeTool.locked}
|
||||||
title={t("toolBar.laser")}
|
onChange={onLockToggle}
|
||||||
checked={
|
title={t("toolBar.lock")}
|
||||||
appState.activeTool.type === TOOL_TYPE.laser
|
/>
|
||||||
}
|
|
||||||
onChange={() =>
|
<div className="App-toolbar__divider" />
|
||||||
app.setActiveTool({ type: TOOL_TYPE.laser })
|
|
||||||
}
|
<HandButton
|
||||||
isMobile
|
checked={isHandToolActive(appState)}
|
||||||
/>
|
onChange={() => onHandToolToggle()}
|
||||||
|
title={t("toolBar.hand")}
|
||||||
|
isMobile
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShapesSwitcher
|
||||||
|
appState={appState}
|
||||||
|
activeTool={appState.activeTool}
|
||||||
|
UIOptions={UIOptions}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
)}
|
{isCollaborating && (
|
||||||
</Stack.Row>
|
<Island
|
||||||
</Stack.Col>
|
style={{
|
||||||
</div>
|
marginLeft: 8,
|
||||||
)}
|
alignSelf: "center",
|
||||||
</Section>
|
height: "fit-content",
|
||||||
)}
|
}}
|
||||||
|
>
|
||||||
|
<LaserPointerButton
|
||||||
|
title={t("toolBar.laser")}
|
||||||
|
checked={
|
||||||
|
appState.activeTool.type === TOOL_TYPE.laser
|
||||||
|
}
|
||||||
|
onChange={() =>
|
||||||
|
app.setActiveTool({ type: TOOL_TYPE.laser })
|
||||||
|
}
|
||||||
|
isMobile
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
)}
|
||||||
|
</Stack.Row>
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||||
@@ -341,6 +346,7 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
||||||
{!appState.viewModeEnabled &&
|
{!appState.viewModeEnabled &&
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||||
// hide button when sidebar docked
|
// hide button when sidebar docked
|
||||||
(!isSidebarDocked ||
|
(!isSidebarDocked ||
|
||||||
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
|
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
|
||||||
@@ -471,6 +477,19 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActiveConfirmDialog />
|
<ActiveConfirmDialog />
|
||||||
|
{appState.openDialog?.name === "elementLinkSelector" && (
|
||||||
|
<ElementLinkDialog
|
||||||
|
sourceElementId={appState.openDialog.sourceElementId}
|
||||||
|
onClose={() => {
|
||||||
|
setAppState({
|
||||||
|
openDialog: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
|
appState={appState}
|
||||||
|
generateLinkForSelection={generateLinkForSelection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
|
|||||||
@@ -91,9 +91,10 @@ export const MobileMenu = ({
|
|||||||
</Island>
|
</Island>
|
||||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||||
<div className="mobile-misc-tools-container">
|
<div className="mobile-misc-tools-container">
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled &&
|
||||||
<DefaultSidebarTriggerTunnel.Out />
|
appState.openDialog?.name !== "elementLinkSelector" && (
|
||||||
)}
|
<DefaultSidebarTriggerTunnel.Out />
|
||||||
|
)}
|
||||||
<PenModeButton
|
<PenModeButton
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={() => onPenModeToggle(null)}
|
onChange={() => onPenModeToggle(null)}
|
||||||
@@ -129,7 +130,10 @@ export const MobileMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderAppToolbar = () => {
|
const renderAppToolbar = () => {
|
||||||
if (appState.viewModeEnabled) {
|
if (
|
||||||
|
appState.viewModeEnabled ||
|
||||||
|
appState.openDialog?.name === "elementLinkSelector"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
<MainMenuTunnel.Out />
|
<MainMenuTunnel.Out />
|
||||||
@@ -154,7 +158,9 @@ export const MobileMenu = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderSidebars()}
|
{renderSidebars()}
|
||||||
{!appState.viewModeEnabled && renderToolbar()}
|
{!appState.viewModeEnabled &&
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||||
|
renderToolbar()}
|
||||||
<div
|
<div
|
||||||
className="App-bottom-bar"
|
className="App-bottom-bar"
|
||||||
style={{
|
style={{
|
||||||
@@ -166,6 +172,7 @@ export const MobileMenu = ({
|
|||||||
<Island padding={0}>
|
<Island padding={0}>
|
||||||
{appState.openMenu === "shape" &&
|
{appState.openMenu === "shape" &&
|
||||||
!appState.viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface CollapsibleProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
openTrigger: () => void;
|
openTrigger: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Collapsible = ({
|
const Collapsible = ({
|
||||||
@@ -16,6 +17,7 @@ const Collapsible = ({
|
|||||||
open,
|
open,
|
||||||
openTrigger,
|
openTrigger,
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
}: CollapsibleProps) => {
|
}: CollapsibleProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -26,6 +28,7 @@ const Collapsible = ({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
|
className={className}
|
||||||
onClick={openTrigger}
|
onClick={openTrigger}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, resizeElement } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||||
|
import { resizeSingleElement } from "../../element/resizeElements";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
import { isImageElement } from "../../element/typeChecks";
|
||||||
|
import {
|
||||||
|
MINIMAL_CROP_SIZE,
|
||||||
|
getUncroppedWidthAndHeight,
|
||||||
|
} from "../../element/cropElement";
|
||||||
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
|
import { clamp, round } from "../../../math";
|
||||||
|
|
||||||
interface DimensionDragInputProps {
|
interface DimensionDragInputProps {
|
||||||
property: "width" | "height";
|
property: "width" | "height";
|
||||||
@@ -23,20 +31,124 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
> = ({
|
> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
|
originalElementsMap,
|
||||||
shouldKeepAspectRatio,
|
shouldKeepAspectRatio,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
property,
|
property,
|
||||||
|
originalAppState,
|
||||||
|
instantChange,
|
||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement) {
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
if (origElement && latestElement) {
|
||||||
const keepAspectRatio =
|
const keepAspectRatio =
|
||||||
shouldKeepAspectRatio || _shouldKeepAspectRatio(origElement);
|
shouldKeepAspectRatio || _shouldKeepAspectRatio(origElement);
|
||||||
const aspectRatio = origElement.width / origElement.height;
|
const aspectRatio = origElement.width / origElement.height;
|
||||||
|
|
||||||
|
if (originalAppState.croppingElementId === origElement.id) {
|
||||||
|
const element = elementsMap.get(origElement.id);
|
||||||
|
|
||||||
|
if (!element || !isImageElement(element) || !element.crop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const crop = element.crop;
|
||||||
|
let nextCrop = { ...crop };
|
||||||
|
|
||||||
|
const isFlippedByX = element.scale[0] === -1;
|
||||||
|
const isFlippedByY = element.scale[1] === -1;
|
||||||
|
|
||||||
|
const { width: uncroppedWidth, height: uncroppedHeight } =
|
||||||
|
getUncroppedWidthAndHeight(element);
|
||||||
|
|
||||||
|
const naturalToUncroppedWidthRatio = crop.naturalWidth / uncroppedWidth;
|
||||||
|
const naturalToUncroppedHeightRatio =
|
||||||
|
crop.naturalHeight / uncroppedHeight;
|
||||||
|
|
||||||
|
const MAX_POSSIBLE_WIDTH = isFlippedByX
|
||||||
|
? crop.width + crop.x
|
||||||
|
: crop.naturalWidth - crop.x;
|
||||||
|
|
||||||
|
const MAX_POSSIBLE_HEIGHT = isFlippedByY
|
||||||
|
? crop.height + crop.y
|
||||||
|
: crop.naturalHeight - crop.y;
|
||||||
|
|
||||||
|
const MIN_WIDTH = MINIMAL_CROP_SIZE * naturalToUncroppedWidthRatio;
|
||||||
|
const MIN_HEIGHT = MINIMAL_CROP_SIZE * naturalToUncroppedHeightRatio;
|
||||||
|
|
||||||
|
if (nextValue !== undefined) {
|
||||||
|
if (property === "width") {
|
||||||
|
const nextValueInNatural = nextValue * naturalToUncroppedWidthRatio;
|
||||||
|
|
||||||
|
const nextCropWidth = clamp(
|
||||||
|
nextValueInNatural,
|
||||||
|
MIN_WIDTH,
|
||||||
|
MAX_POSSIBLE_WIDTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
nextCrop = {
|
||||||
|
...nextCrop,
|
||||||
|
width: nextCropWidth,
|
||||||
|
x: isFlippedByX ? crop.x + crop.width - nextCropWidth : crop.x,
|
||||||
|
};
|
||||||
|
} else if (property === "height") {
|
||||||
|
const nextValueInNatural = nextValue * naturalToUncroppedHeightRatio;
|
||||||
|
const nextCropHeight = clamp(
|
||||||
|
nextValueInNatural,
|
||||||
|
MIN_HEIGHT,
|
||||||
|
MAX_POSSIBLE_HEIGHT,
|
||||||
|
);
|
||||||
|
|
||||||
|
nextCrop = {
|
||||||
|
...nextCrop,
|
||||||
|
height: nextCropHeight,
|
||||||
|
y: isFlippedByY ? crop.y + crop.height - nextCropHeight : crop.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
crop: nextCrop,
|
||||||
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeInWidth = property === "width" ? instantChange : 0;
|
||||||
|
const changeInHeight = property === "height" ? instantChange : 0;
|
||||||
|
|
||||||
|
const nextCropWidth = clamp(
|
||||||
|
crop.width + changeInWidth,
|
||||||
|
MIN_WIDTH,
|
||||||
|
MAX_POSSIBLE_WIDTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextCropHeight = clamp(
|
||||||
|
crop.height + changeInHeight,
|
||||||
|
MIN_WIDTH,
|
||||||
|
MAX_POSSIBLE_HEIGHT,
|
||||||
|
);
|
||||||
|
|
||||||
|
nextCrop = {
|
||||||
|
...crop,
|
||||||
|
x: isFlippedByX ? crop.x + crop.width - nextCropWidth : crop.x,
|
||||||
|
y: isFlippedByY ? crop.y + crop.height - nextCropHeight : crop.y,
|
||||||
|
width: nextCropWidth,
|
||||||
|
height: nextCropHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
crop: nextCrop,
|
||||||
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextWidth = Math.max(
|
const nextWidth = Math.max(
|
||||||
property === "width"
|
property === "width"
|
||||||
@@ -55,14 +167,17 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
MIN_WIDTH_OR_HEIGHT,
|
MIN_WIDTH_OR_HEIGHT,
|
||||||
);
|
);
|
||||||
|
|
||||||
resizeElement(
|
resizeSingleElement(
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
keepAspectRatio,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
originalElementsMap,
|
||||||
scene,
|
property === "width" ? "e" : "s",
|
||||||
|
{
|
||||||
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -99,14 +214,17 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||||
|
|
||||||
resizeElement(
|
resizeSingleElement(
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
keepAspectRatio,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
originalElementsMap,
|
||||||
scene,
|
property === "width" ? "e" : "s",
|
||||||
|
{
|
||||||
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -117,9 +235,25 @@ const DimensionDragInput = ({
|
|||||||
scene,
|
scene,
|
||||||
appState,
|
appState,
|
||||||
}: DimensionDragInputProps) => {
|
}: DimensionDragInputProps) => {
|
||||||
const value =
|
let value = round(property === "width" ? element.width : element.height, 2);
|
||||||
Math.round((property === "width" ? element.width : element.height) * 100) /
|
|
||||||
100;
|
if (
|
||||||
|
appState.croppingElementId &&
|
||||||
|
appState.croppingElementId === element.id &&
|
||||||
|
isImageElement(element) &&
|
||||||
|
element.crop
|
||||||
|
) {
|
||||||
|
const { width: uncroppedWidth, height: uncroppedHeight } =
|
||||||
|
getUncroppedWidthAndHeight(element);
|
||||||
|
if (property === "width") {
|
||||||
|
const ratio = uncroppedWidth / element.crop.naturalWidth;
|
||||||
|
value = round(element.crop.width * ratio, 2);
|
||||||
|
}
|
||||||
|
if (property === "height") {
|
||||||
|
const ratio = uncroppedHeight / element.crop.naturalHeight;
|
||||||
|
value = round(element.crop.height * ratio, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragInput
|
<DragInput
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { useMemo } from "react";
|
|||||||
import { getCommonBounds, isTextElement } from "../../element";
|
import { getCommonBounds, isTextElement } from "../../element";
|
||||||
import { updateBoundElements } from "../../element/binding";
|
import { updateBoundElements } from "../../element/binding";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { rescalePointsInElement } from "../../element/resizeElements";
|
import {
|
||||||
|
rescalePointsInElement,
|
||||||
|
resizeSingleElement,
|
||||||
|
} from "../../element/resizeElements";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
@@ -17,7 +20,7 @@ import type { AppState } from "../../types";
|
|||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
import { getElementsInAtomicUnit } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||||
import { pointFrom, type GlobalPoint } from "../../../math";
|
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||||
@@ -150,7 +153,6 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
property,
|
property,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of atomicUnits) {
|
for (const atomicUnit of atomicUnits) {
|
||||||
@@ -223,15 +225,17 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||||
|
|
||||||
resizeElement(
|
resizeSingleElement(
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
false,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
originalElementsMap,
|
||||||
scene,
|
property === "width" ? "e" : "s",
|
||||||
false,
|
{
|
||||||
|
shouldInformMutation: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,14 +328,17 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||||
|
|
||||||
resizeElement(
|
resizeSingleElement(
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
false,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
originalElementsMap,
|
||||||
scene,
|
property === "width" ? "e" : "s",
|
||||||
|
{
|
||||||
|
shouldInformMutation: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import type { DragInputCallbackType } from "./DragInput";
|
|||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { pointFrom, pointRotateRads } from "../../../math";
|
import { clamp, pointFrom, pointRotateRads, round } from "../../../math";
|
||||||
|
import { isImageElement } from "../../element/typeChecks";
|
||||||
|
import {
|
||||||
|
getFlipAdjustedCropPosition,
|
||||||
|
getUncroppedWidthAndHeight,
|
||||||
|
} from "../../element/cropElement";
|
||||||
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
@@ -18,12 +24,14 @@ const STEP_SIZE = 10;
|
|||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
|
instantChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
property,
|
property,
|
||||||
scene,
|
scene,
|
||||||
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
const elements = scene.getNonDeletedElements();
|
||||||
@@ -38,6 +46,82 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (originalAppState.croppingElementId === origElement.id) {
|
||||||
|
const element = elementsMap.get(origElement.id);
|
||||||
|
|
||||||
|
if (!element || !isImageElement(element) || !element.crop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const crop = element.crop;
|
||||||
|
let nextCrop = crop;
|
||||||
|
const isFlippedByX = element.scale[0] === -1;
|
||||||
|
const isFlippedByY = element.scale[1] === -1;
|
||||||
|
const { width: uncroppedWidth, height: uncroppedHeight } =
|
||||||
|
getUncroppedWidthAndHeight(element);
|
||||||
|
|
||||||
|
if (nextValue !== undefined) {
|
||||||
|
if (property === "x") {
|
||||||
|
const nextValueInNatural =
|
||||||
|
nextValue * (crop.naturalWidth / uncroppedWidth);
|
||||||
|
|
||||||
|
if (isFlippedByX) {
|
||||||
|
nextCrop = {
|
||||||
|
...crop,
|
||||||
|
x: clamp(
|
||||||
|
crop.naturalWidth - nextValueInNatural - crop.width,
|
||||||
|
0,
|
||||||
|
crop.naturalWidth - crop.width,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
nextCrop = {
|
||||||
|
...crop,
|
||||||
|
x: clamp(
|
||||||
|
nextValue * (crop.naturalWidth / uncroppedWidth),
|
||||||
|
0,
|
||||||
|
crop.naturalWidth - crop.width,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property === "y") {
|
||||||
|
nextCrop = {
|
||||||
|
...crop,
|
||||||
|
y: clamp(
|
||||||
|
nextValue * (crop.naturalHeight / uncroppedHeight),
|
||||||
|
0,
|
||||||
|
crop.naturalHeight - crop.height,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
crop: nextCrop,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeInX =
|
||||||
|
(property === "x" ? instantChange : 0) * (isFlippedByX ? -1 : 1);
|
||||||
|
const changeInY =
|
||||||
|
(property === "y" ? instantChange : 0) * (isFlippedByY ? -1 : 1);
|
||||||
|
|
||||||
|
nextCrop = {
|
||||||
|
...crop,
|
||||||
|
x: clamp(crop.x + changeInX, 0, crop.naturalWidth - crop.width),
|
||||||
|
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
||||||
|
};
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
crop: nextCrop,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const newTopLeftX = property === "x" ? nextValue : topLeftX;
|
const newTopLeftX = property === "x" ? nextValue : topLeftX;
|
||||||
const newTopLeftY = property === "y" ? nextValue : topLeftY;
|
const newTopLeftY = property === "y" ? nextValue : topLeftY;
|
||||||
@@ -97,8 +181,22 @@ const Position = ({
|
|||||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const value =
|
let value = round(property === "x" ? topLeftX : topLeftY, 2);
|
||||||
Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
|
||||||
|
if (
|
||||||
|
appState.croppingElementId === element.id &&
|
||||||
|
isImageElement(element) &&
|
||||||
|
element.crop
|
||||||
|
) {
|
||||||
|
const flipAdjustedPosition = getFlipAdjustedCropPosition(element);
|
||||||
|
|
||||||
|
if (flipAdjustedPosition) {
|
||||||
|
value = round(
|
||||||
|
property === "x" ? flipAdjustedPosition.x : flipAdjustedPosition.y,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ import Collapsible from "./Collapsible";
|
|||||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||||
import { getAtomicUnits } from "./utils";
|
import { getAtomicUnits } from "./utils";
|
||||||
import { STATS_PANELS } from "../../constants";
|
import { STATS_PANELS } from "../../constants";
|
||||||
import { isElbowArrow } from "../../element/typeChecks";
|
import { isElbowArrow, isImageElement } from "../../element/typeChecks";
|
||||||
import CanvasGrid from "./CanvasGrid";
|
import CanvasGrid from "./CanvasGrid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import "./Stats.scss";
|
import "./Stats.scss";
|
||||||
import { isGridModeEnabled } from "../../snapping";
|
import { isGridModeEnabled } from "../../snapping";
|
||||||
|
import { getUncroppedWidthAndHeight } from "../../element/cropElement";
|
||||||
|
import { round } from "../../../math";
|
||||||
|
|
||||||
interface StatsProps {
|
interface StatsProps {
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
@@ -128,6 +130,13 @@ export const StatsInner = memo(
|
|||||||
const multipleElements =
|
const multipleElements =
|
||||||
selectedElements.length > 1 ? selectedElements : null;
|
selectedElements.length > 1 ? selectedElements : null;
|
||||||
|
|
||||||
|
const cropMode =
|
||||||
|
appState.croppingElementId && isImageElement(singleElement);
|
||||||
|
|
||||||
|
const unCroppedDimension = cropMode
|
||||||
|
? getUncroppedWidthAndHeight(singleElement)
|
||||||
|
: null;
|
||||||
|
|
||||||
const [sceneDimension, setSceneDimension] = useState<{
|
const [sceneDimension, setSceneDimension] = useState<{
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@@ -244,8 +253,34 @@ export const StatsInner = memo(
|
|||||||
<StatsRows>
|
<StatsRows>
|
||||||
{singleElement && (
|
{singleElement && (
|
||||||
<>
|
<>
|
||||||
|
{cropMode && (
|
||||||
|
<StatsRow heading>
|
||||||
|
{t("labels.unCroppedDimension")}
|
||||||
|
</StatsRow>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{appState.croppingElementId &&
|
||||||
|
isImageElement(singleElement) &&
|
||||||
|
unCroppedDimension && (
|
||||||
|
<StatsRow columns={2}>
|
||||||
|
<div>{t("stats.width")}</div>
|
||||||
|
<div>{round(unCroppedDimension.width, 2)}</div>
|
||||||
|
</StatsRow>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{appState.croppingElementId &&
|
||||||
|
isImageElement(singleElement) &&
|
||||||
|
unCroppedDimension && (
|
||||||
|
<StatsRow columns={2}>
|
||||||
|
<div>{t("stats.height")}</div>
|
||||||
|
<div>{round(unCroppedDimension.height, 2)}</div>
|
||||||
|
</StatsRow>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatsRow heading data-testid="stats-element-type">
|
<StatsRow heading data-testid="stats-element-type">
|
||||||
{t(`element.${singleElement.type}`)}
|
{appState.croppingElementId
|
||||||
|
? t("labels.imageCropping")
|
||||||
|
: t(`element.${singleElement.type}`)}
|
||||||
</StatsRow>
|
</StatsRow>
|
||||||
|
|
||||||
<StatsRow>
|
<StatsRow>
|
||||||
@@ -387,7 +422,8 @@ export const StatsInner = memo(
|
|||||||
prev.selectedElements === next.selectedElements &&
|
prev.selectedElements === next.selectedElements &&
|
||||||
prev.appState.stats.panels === next.appState.stats.panels &&
|
prev.appState.stats.panels === next.appState.stats.panels &&
|
||||||
prev.gridModeEnabled === next.gridModeEnabled &&
|
prev.gridModeEnabled === next.gridModeEnabled &&
|
||||||
prev.appState.gridStep === next.appState.gridStep
|
prev.appState.gridStep === next.appState.gridStep &&
|
||||||
|
prev.appState.croppingElementId === next.appState.croppingElementId
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,17 +5,7 @@ import {
|
|||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
} from "../../element/binding";
|
} from "../../element/binding";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import {
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
measureFontSizeFromWidth,
|
|
||||||
rescalePointsInElement,
|
|
||||||
} from "../../element/resizeElements";
|
|
||||||
import {
|
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
|
||||||
getBoundTextMaxWidth,
|
|
||||||
handleBindTextResize,
|
|
||||||
} from "../../element/textElement";
|
|
||||||
import {
|
import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -34,7 +24,6 @@ import {
|
|||||||
} from "../../groups";
|
} from "../../groups";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { getFontString } from "../../utils";
|
|
||||||
|
|
||||||
export type StatsInputProperty =
|
export type StatsInputProperty =
|
||||||
| "x"
|
| "x"
|
||||||
@@ -121,95 +110,6 @@ export const newOrigin = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizeElement = (
|
|
||||||
nextWidth: number,
|
|
||||||
nextHeight: number,
|
|
||||||
keepAspectRatio: boolean,
|
|
||||||
origElement: ExcalidrawElement,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
|
||||||
shouldInformMutation = true,
|
|
||||||
) => {
|
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
|
||||||
if (!latestElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
|
||||||
|
|
||||||
if (boundTextElement) {
|
|
||||||
const minWidth = getApproxMinLineWidth(
|
|
||||||
getFontString(boundTextElement),
|
|
||||||
boundTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
const minHeight = getApproxMinLineHeight(
|
|
||||||
boundTextElement.fontSize,
|
|
||||||
boundTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
nextWidth = Math.max(nextWidth, minWidth);
|
|
||||||
nextHeight = Math.max(nextHeight, minHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(
|
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
...newOrigin(
|
|
||||||
latestElement.x,
|
|
||||||
latestElement.y,
|
|
||||||
latestElement.width,
|
|
||||||
latestElement.height,
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
latestElement.angle,
|
|
||||||
),
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
...rescalePointsInElement(origElement, nextWidth, nextHeight, true),
|
|
||||||
},
|
|
||||||
shouldInformMutation,
|
|
||||||
);
|
|
||||||
updateBindings(latestElement, elementsMap, elements, scene, {
|
|
||||||
newSize: {
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (boundTextElement) {
|
|
||||||
boundTextFont = {
|
|
||||||
fontSize: boundTextElement.fontSize,
|
|
||||||
};
|
|
||||||
if (keepAspectRatio) {
|
|
||||||
const updatedElement = {
|
|
||||||
...latestElement,
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextFont = measureFontSizeFromWidth(
|
|
||||||
boundTextElement,
|
|
||||||
elementsMap,
|
|
||||||
getBoundTextMaxWidth(updatedElement, boundTextElement),
|
|
||||||
);
|
|
||||||
boundTextFont = {
|
|
||||||
fontSize: nextFont?.size ?? boundTextElement.fontSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBoundElements(latestElement, elementsMap, {
|
|
||||||
newSize: { width: nextWidth, height: nextHeight },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont) {
|
|
||||||
mutateElement(boundTextElement, {
|
|
||||||
fontSize: boundTextFont.fontSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleBindTextResize(latestElement, elementsMap, "e", keepAspectRatio);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const moveElement = (
|
export const moveElement = (
|
||||||
newTopLeftX: number,
|
newTopLeftX: number,
|
||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
@@ -300,6 +200,7 @@ export const updateBindings = (
|
|||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (isLinearElement(latestElement)) {
|
if (isLinearElement(latestElement)) {
|
||||||
@@ -310,6 +211,7 @@ export const updateBindings = (
|
|||||||
scene,
|
scene,
|
||||||
true,
|
true,
|
||||||
[],
|
[],
|
||||||
|
options?.zoom,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
updateBoundElements(latestElement, elementsMap, options);
|
updateBoundElements(latestElement, elementsMap, options);
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ const getRelevantAppStateProps = (
|
|||||||
width: appState.width,
|
width: appState.width,
|
||||||
height: appState.height,
|
height: appState.height,
|
||||||
viewModeEnabled: appState.viewModeEnabled,
|
viewModeEnabled: appState.viewModeEnabled,
|
||||||
|
openDialog: appState.openDialog,
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
editingLinearElement: appState.editingLinearElement,
|
editingLinearElement: appState.editingLinearElement,
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ const getRelevantAppStateProps = (
|
|||||||
width: appState.width,
|
width: appState.width,
|
||||||
height: appState.height,
|
height: appState.height,
|
||||||
viewModeEnabled: appState.viewModeEnabled,
|
viewModeEnabled: appState.viewModeEnabled,
|
||||||
|
openDialog: appState.openDialog,
|
||||||
|
hoveredElementIds: appState.hoveredElementIds,
|
||||||
offsetLeft: appState.offsetLeft,
|
offsetLeft: appState.offsetLeft,
|
||||||
offsetTop: appState.offsetTop,
|
offsetTop: appState.offsetTop,
|
||||||
theme: appState.theme,
|
theme: appState.theme,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type {
|
|||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
|
|
||||||
import { ToolButton } from "../ToolButton";
|
import { ToolButton } from "../ToolButton";
|
||||||
import { FreedrawIcon, TrashIcon } from "../icons";
|
import { FreedrawIcon, TrashIcon, elementLinkIcon } from "../icons";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -30,18 +30,19 @@ import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
|
|||||||
import { getSelectedElements } from "../../scene";
|
import { getSelectedElements } from "../../scene";
|
||||||
import { hitElementBoundingBox } from "../../element/collision";
|
import { hitElementBoundingBox } from "../../element/collision";
|
||||||
import { isLocalLink, normalizeLink } from "../../data/url";
|
import { isLocalLink, normalizeLink } from "../../data/url";
|
||||||
|
|
||||||
import "./Hyperlink.scss";
|
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { useAppProps, useExcalidrawAppState } from "../App";
|
import { useAppProps, useDevice, useExcalidrawAppState } from "../App";
|
||||||
import { isEmbeddableElement } from "../../element/typeChecks";
|
import { isEmbeddableElement } from "../../element/typeChecks";
|
||||||
import { getLinkHandleFromCoords } from "./helpers";
|
import { getLinkHandleFromCoords } from "./helpers";
|
||||||
import { pointFrom, type GlobalPoint } from "../../../math";
|
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||||
|
import { isElementLink } from "../../element/elementLink";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
import "./Hyperlink.scss";
|
||||||
|
|
||||||
|
const POPUP_WIDTH = 380;
|
||||||
|
const POPUP_HEIGHT = 42;
|
||||||
|
const POPUP_PADDING = 5;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
const CONTAINER_PADDING = 5;
|
|
||||||
const CONTAINER_HEIGHT = 42;
|
|
||||||
const AUTO_HIDE_TIMEOUT = 500;
|
const AUTO_HIDE_TIMEOUT = 500;
|
||||||
|
|
||||||
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||||
@@ -73,6 +74,7 @@ export const Hyperlink = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const appProps = useAppProps();
|
const appProps = useAppProps();
|
||||||
|
const device = useDevice();
|
||||||
|
|
||||||
const linkVal = element.link || "";
|
const linkVal = element.link || "";
|
||||||
|
|
||||||
@@ -170,6 +172,15 @@ export const Hyperlink = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeoutId: number | null = null;
|
let timeoutId: number | null = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
inputRef &&
|
||||||
|
inputRef.current &&
|
||||||
|
!(device.viewport.isMobile || device.isTouchScreen)
|
||||||
|
) {
|
||||||
|
inputRef.current.select();
|
||||||
|
}
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return;
|
return;
|
||||||
@@ -196,16 +207,21 @@ export const Hyperlink = ({
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [appState, element, isEditing, setAppState, elementsMap]);
|
}, [
|
||||||
|
appState,
|
||||||
|
element,
|
||||||
|
isEditing,
|
||||||
|
setAppState,
|
||||||
|
elementsMap,
|
||||||
|
device.viewport.isMobile,
|
||||||
|
device.isTouchScreen,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
trackEvent("hyperlink", "delete");
|
trackEvent("hyperlink", "delete");
|
||||||
mutateElement(element, { link: null });
|
mutateElement(element, { link: null });
|
||||||
if (isEditing) {
|
|
||||||
inputRef.current!.value = "";
|
|
||||||
}
|
|
||||||
setAppState({ showHyperlinkPopup: false });
|
setAppState({ showHyperlinkPopup: false });
|
||||||
}, [setAppState, element, isEditing]);
|
}, [setAppState, element]);
|
||||||
|
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
trackEvent("hyperlink", "edit", "popup-ui");
|
trackEvent("hyperlink", "edit", "popup-ui");
|
||||||
@@ -229,19 +245,14 @@ export const Hyperlink = ({
|
|||||||
style={{
|
style={{
|
||||||
top: `${y}px`,
|
top: `${y}px`,
|
||||||
left: `${x}px`,
|
left: `${x}px`,
|
||||||
width: CONTAINER_WIDTH,
|
width: POPUP_WIDTH,
|
||||||
padding: CONTAINER_PADDING,
|
padding: POPUP_PADDING,
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (!element.link && !isEditing) {
|
|
||||||
setAppState({ showHyperlinkPopup: "editor" });
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
className={clsx("excalidraw-hyperlinkContainer-input")}
|
className={clsx("excalidraw-hyperlinkContainer-input")}
|
||||||
placeholder="Type or paste your link here"
|
placeholder={t("labels.link.hint")}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={inputVal}
|
value={inputVal}
|
||||||
onChange={(event) => setInputVal(event.target.value)}
|
onChange={(event) => setInputVal(event.target.value)}
|
||||||
@@ -302,6 +313,21 @@ export const Hyperlink = ({
|
|||||||
icon={FreedrawIcon}
|
icon={FreedrawIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
title={t("labels.linkToElement")}
|
||||||
|
aria-label={t("labels.linkToElement")}
|
||||||
|
label={t("labels.linkToElement")}
|
||||||
|
onClick={() => {
|
||||||
|
setAppState({
|
||||||
|
openDialog: {
|
||||||
|
name: "elementLinkSelector",
|
||||||
|
sourceElementId: element.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={elementLinkIcon}
|
||||||
|
/>
|
||||||
{linkVal && !isEmbeddableElement(element) && (
|
{linkVal && !isEmbeddableElement(element) && (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -328,7 +354,7 @@ const getCoordsForPopover = (
|
|||||||
{ sceneX: x1 + element.width / 2, sceneY: y1 },
|
{ sceneX: x1 + element.width / 2, sceneY: y1 },
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2;
|
const x = viewportX - appState.offsetLeft - POPUP_WIDTH / 2;
|
||||||
const y = viewportY - appState.offsetTop - SPACE_BOTTOM;
|
const y = viewportY - appState.offsetTop - SPACE_BOTTOM;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
@@ -338,12 +364,10 @@ export const getContextMenuLabel = (
|
|||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
const label = selectedElements[0]?.link
|
const label = isEmbeddableElement(selectedElements[0])
|
||||||
? isEmbeddableElement(selectedElements[0])
|
? "labels.link.editEmbed"
|
||||||
? "labels.link.editEmbed"
|
: selectedElements[0]?.link
|
||||||
: "labels.link.edit"
|
? "labels.link.edit"
|
||||||
: isEmbeddableElement(selectedElements[0])
|
|
||||||
? "labels.link.createEmbed"
|
|
||||||
: "labels.link.create";
|
: "labels.link.create";
|
||||||
return label;
|
return label;
|
||||||
};
|
};
|
||||||
@@ -376,7 +400,9 @@ const renderTooltip = (
|
|||||||
|
|
||||||
tooltipDiv.classList.add("excalidraw-tooltip--visible");
|
tooltipDiv.classList.add("excalidraw-tooltip--visible");
|
||||||
tooltipDiv.style.maxWidth = "20rem";
|
tooltipDiv.style.maxWidth = "20rem";
|
||||||
tooltipDiv.textContent = element.link;
|
tooltipDiv.textContent = isElementLink(element.link)
|
||||||
|
? t("labels.link.goToElement")
|
||||||
|
: element.link;
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
|
||||||
@@ -450,9 +476,9 @@ const shouldHideLinkPopup = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
clientX >= popoverX - threshold &&
|
clientX >= popoverX - threshold &&
|
||||||
clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
|
clientX <= popoverX + POPUP_WIDTH + POPUP_PADDING * 2 + threshold &&
|
||||||
clientY >= popoverY - threshold &&
|
clientY >= popoverY - threshold &&
|
||||||
clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
|
clientY <= popoverY + threshold + POPUP_PADDING * 2 + POPUP_HEIGHT
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
|||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
|
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
|
export const ELEMENT_LINK_IMG = document.createElement("img");
|
||||||
|
ELEMENT_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-big-right-line"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 9v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-6v-6h6z" /><path d="M3 9v6" /></svg>`,
|
||||||
|
)}`;
|
||||||
|
|
||||||
export const getLinkHandleFromCoords = (
|
export const getLinkHandleFromCoords = (
|
||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: Radians,
|
angle: Radians,
|
||||||
|
|||||||
@@ -1352,6 +1352,54 @@ export const ArrowheadDiamondOutlineIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ArrowheadCrowfootIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
transform={flip ? "" : "translate(40, 0) scale(-1, 1)"}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M34,10 H6 M15,10 L7,5 M15,10 L7,15" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadCrowfootOneIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
transform={flip ? "" : "translate(40, 0) scale(-1, 1)"}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M34,10 H6 M15,10 L15,15 L15,5" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadCrowfootOneOrManyIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
transform={flip ? "" : "translate(40, 0) scale(-1, 1)"}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M34,10 H6 M15,10 L15,16 L15,4 M15,10 L7,5 M15,10 L7,15" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const FontSizeSmallIcon = createIcon(
|
export const FontSizeSmallIcon = createIcon(
|
||||||
<>
|
<>
|
||||||
<g clipPath="url(#a)">
|
<g clipPath="url(#a)">
|
||||||
@@ -2156,3 +2204,18 @@ export const cropIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const elementLinkIcon = createIcon(
|
||||||
|
<g>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M5 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M19 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M5 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M19 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M5 7l0 10" />
|
||||||
|
<path d="M7 5l10 0" />
|
||||||
|
<path d="M7 19l10 0" />
|
||||||
|
<path d="M19 7l0 10" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|||||||
@@ -214,9 +214,9 @@ export const IMAGE_MIME_TYPES = {
|
|||||||
jfif: "image/jfif",
|
jfif: "image/jfif",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
|
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
|
text: "text/plain",
|
||||||
|
html: "text/html",
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
// excalidraw data
|
// excalidraw data
|
||||||
excalidraw: "application/vnd.excalidraw+json",
|
excalidraw: "application/vnd.excalidraw+json",
|
||||||
@@ -230,6 +230,12 @@ export const MIME_TYPES = {
|
|||||||
...IMAGE_MIME_TYPES,
|
...IMAGE_MIME_TYPES,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const ALLOWED_PASTE_MIME_TYPES = [
|
||||||
|
MIME_TYPES.text,
|
||||||
|
MIME_TYPES.html,
|
||||||
|
...Object.values(IMAGE_MIME_TYPES),
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const EXPORT_IMAGE_TYPES = {
|
export const EXPORT_IMAGE_TYPES = {
|
||||||
png: "png",
|
png: "png",
|
||||||
svg: "svg",
|
svg: "svg",
|
||||||
@@ -249,6 +255,14 @@ export const EXPORT_SOURCE =
|
|||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
export const IMAGE_RENDER_TIMEOUT = 500;
|
export const IMAGE_RENDER_TIMEOUT = 500;
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
|
/**
|
||||||
|
* The time the user has from 2nd pointerdown to following pointerup
|
||||||
|
* before it's not considered a double click.
|
||||||
|
*
|
||||||
|
* Helps prevent cases where you double-click by mistake but then drag/keep
|
||||||
|
* the pointer down for to cancel the double click or do another action.
|
||||||
|
*/
|
||||||
|
export const DOUBLE_CLICK_POINTERUP_TIMEOUT = 300;
|
||||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||||
export const TITLE_TIMEOUT = 10000;
|
export const TITLE_TIMEOUT = 10000;
|
||||||
export const VERSION_TIMEOUT = 30000;
|
export const VERSION_TIMEOUT = 30000;
|
||||||
@@ -449,3 +463,6 @@ export const ARROW_TYPE: { [T in AppState["currentItemArrowType"]]: T } = {
|
|||||||
round: "round",
|
round: "round",
|
||||||
elbow: "elbow",
|
elbow: "elbow",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_REDUCED_GLOBAL_ALPHA = 0.3;
|
||||||
|
export const ELEMENT_LINK_KEY = "element";
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 35,
|
"height": 33.519031369643244,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -109,8 +109,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
0.5,
|
0.5,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
394.5,
|
382.47606040672997,
|
||||||
34.5,
|
34.019031369643244,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -128,9 +128,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 7,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 395,
|
"width": 381.97606040672997,
|
||||||
"x": 247,
|
"x": 247,
|
||||||
"y": 420,
|
"y": 420,
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
399.5,
|
389.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -186,10 +186,10 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 400,
|
"width": 390,
|
||||||
"x": 227,
|
"x": 237,
|
||||||
"y": 450,
|
"y": 450,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -319,7 +319,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 560,
|
"x": 560,
|
||||||
"y": 226.5,
|
"y": 236.95454545454544,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -339,13 +339,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 1.625925925925924,
|
||||||
"gap": 205,
|
"gap": 14,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": 18.278619528619487,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -356,11 +356,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0.5,
|
||||||
0,
|
-0.5,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
357.2037037037038,
|
||||||
0,
|
-17.778619528619487,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -378,11 +378,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 357.7037037037038,
|
||||||
"x": 255,
|
"x": 171,
|
||||||
"y": 239,
|
"y": 249.45454545454544,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -482,7 +482,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
@@ -660,7 +660,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
@@ -1505,7 +1505,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
272.485,
|
270.98528125,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1526,10 +1526,10 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 7,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 272.985,
|
"width": 270.48528125,
|
||||||
"x": 111.262,
|
"x": 112.76171875,
|
||||||
"y": 57,
|
"y": 57,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1587,11 +1587,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 0,
|
"width": 0,
|
||||||
"x": 77.017,
|
"x": 83.015625,
|
||||||
"y": 79,
|
"y": 81.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { clearElementsForExport } from "../element";
|
|||||||
import type { ExcalidrawElement, FileId } from "../element/types";
|
import type { ExcalidrawElement, FileId } from "../element/types";
|
||||||
import { CanvasError, ImageSceneDataError } from "../errors";
|
import { CanvasError, ImageSceneDataError } from "../errors";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
|
import { decodeSvgBase64Payload } from "../scene/export";
|
||||||
import type { AppState, DataURL, LibraryItem } from "../types";
|
import type { AppState, DataURL, LibraryItem } from "../types";
|
||||||
import type { ValueOf } from "../utility-types";
|
import type { ValueOf } from "../utility-types";
|
||||||
import { bytesToHexString, isPromiseLike } from "../utils";
|
import { bytesToHexString, isPromiseLike } from "../utils";
|
||||||
@@ -47,7 +48,7 @@ const parseFileContents = async (blob: Blob | File): Promise<string> => {
|
|||||||
}
|
}
|
||||||
if (blob.type === MIME_TYPES.svg) {
|
if (blob.type === MIME_TYPES.svg) {
|
||||||
try {
|
try {
|
||||||
return (await import("./image")).decodeSvgMetadata({
|
return decodeSvgBase64Payload({
|
||||||
svg: contents,
|
svg: contents,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -106,11 +107,15 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
|
|||||||
return type === "png" || type === "svg";
|
return type === "png" || type === "svg";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isSupportedImageFileType = (type: string | null | undefined) => {
|
||||||
|
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
|
||||||
|
};
|
||||||
|
|
||||||
export const isSupportedImageFile = (
|
export const isSupportedImageFile = (
|
||||||
blob: Blob | null | undefined,
|
blob: Blob | null | undefined,
|
||||||
): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
|
): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
|
||||||
const { type } = blob || {};
|
const { type } = blob || {};
|
||||||
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
|
return isSupportedImageFileType(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadSceneOrLibraryFromBlob = async (
|
export const loadSceneOrLibraryFromBlob = async (
|
||||||
@@ -329,7 +334,7 @@ export const resizeImageFile = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new File(
|
return new File(
|
||||||
[await reduce.toBlob(file, { max: opts.maxWidthOrHeight })],
|
[await reduce.toBlob(file, { max: opts.maxWidthOrHeight, alpha: true })],
|
||||||
file.name,
|
file.name,
|
||||||
{
|
{
|
||||||
type: opts.outputType || file.type,
|
type: opts.outputType || file.type,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const fileSave = (
|
|||||||
name: string;
|
name: string;
|
||||||
/** file extension */
|
/** file extension */
|
||||||
extension: FILE_EXTENSION;
|
extension: FILE_EXTENSION;
|
||||||
|
mimeTypes?: string[];
|
||||||
description: string;
|
description: string;
|
||||||
/** existing FileSystemHandle */
|
/** existing FileSystemHandle */
|
||||||
fileHandle?: FileSystemHandle | null;
|
fileHandle?: FileSystemHandle | null;
|
||||||
@@ -93,10 +94,11 @@ export const fileSave = (
|
|||||||
fileName: `${opts.name}.${opts.extension}`,
|
fileName: `${opts.name}.${opts.extension}`,
|
||||||
description: opts.description,
|
description: opts.description,
|
||||||
extensions: [`.${opts.extension}`],
|
extensions: [`.${opts.extension}`],
|
||||||
|
mimeTypes: opts.mimeTypes,
|
||||||
},
|
},
|
||||||
opts.fileHandle,
|
opts.fileHandle,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { FileSystemHandle };
|
|
||||||
export { nativeFileSystemSupported };
|
export { nativeFileSystemSupported };
|
||||||
|
export type { FileSystemHandle };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import decodePng from "png-chunks-extract";
|
import decodePng from "png-chunks-extract";
|
||||||
import tEXt from "png-chunk-text";
|
import tEXt from "png-chunk-text";
|
||||||
import encodePng from "png-chunks-encode";
|
import encodePng from "png-chunks-encode";
|
||||||
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
import { encode, decode } from "./encode";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { blobToArrayBuffer } from "./blob";
|
import { blobToArrayBuffer } from "./blob";
|
||||||
|
|
||||||
@@ -67,56 +67,3 @@ export const decodePngMetadata = async (blob: Blob) => {
|
|||||||
}
|
}
|
||||||
throw new Error("INVALID");
|
throw new Error("INVALID");
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// SVG
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const encodeSvgMetadata = ({ text }: { text: string }) => {
|
|
||||||
const base64 = stringToBase64(
|
|
||||||
JSON.stringify(encode({ text })),
|
|
||||||
true /* is already byte string */,
|
|
||||||
);
|
|
||||||
|
|
||||||
let metadata = "";
|
|
||||||
metadata += `<!-- payload-type:${MIME_TYPES.excalidraw} -->`;
|
|
||||||
metadata += `<!-- payload-version:2 -->`;
|
|
||||||
metadata += "<!-- payload-start -->";
|
|
||||||
metadata += base64;
|
|
||||||
metadata += "<!-- payload-end -->";
|
|
||||||
return metadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeSvgMetadata = ({ svg }: { svg: string }) => {
|
|
||||||
if (svg.includes(`payload-type:${MIME_TYPES.excalidraw}`)) {
|
|
||||||
const match = svg.match(
|
|
||||||
/<!-- payload-start -->\s*(.+?)\s*<!-- payload-end -->/,
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error("INVALID");
|
|
||||||
}
|
|
||||||
const versionMatch = svg.match(/<!-- payload-version:(\d+) -->/);
|
|
||||||
const version = versionMatch?.[1] || "1";
|
|
||||||
const isByteString = version !== "1";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const json = base64ToString(match[1], isByteString);
|
|
||||||
const encodedData = JSON.parse(json);
|
|
||||||
if (!("encoded" in encodedData)) {
|
|
||||||
// legacy, un-encoded scene JSON
|
|
||||||
if (
|
|
||||||
"type" in encodedData &&
|
|
||||||
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
|
||||||
) {
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
throw new Error("FAILED");
|
|
||||||
}
|
|
||||||
return decode(encodedData);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
throw new Error("FAILED");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("INVALID");
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DEFAULT_EXPORT_PADDING,
|
DEFAULT_EXPORT_PADDING,
|
||||||
DEFAULT_FILENAME,
|
DEFAULT_FILENAME,
|
||||||
|
IMAGE_MIME_TYPES,
|
||||||
isFirefox,
|
isFirefox,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
@@ -15,8 +16,9 @@ import type {
|
|||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
|
import { getElementsOverlappingFrame } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isSomeElementSelected, getSelectedElements } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
import type { ExportType } from "../scene/types";
|
import type { ExportType } from "../scene/types";
|
||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
@@ -25,7 +27,6 @@ import { canvasToBlob } from "./blob";
|
|||||||
import type { FileSystemHandle } from "./filesystem";
|
import type { FileSystemHandle } from "./filesystem";
|
||||||
import { fileSave } from "./filesystem";
|
import { fileSave } from "./filesystem";
|
||||||
import { serializeAsJSON } from "./json";
|
import { serializeAsJSON } from "./json";
|
||||||
import { getElementsOverlappingFrame } from "../frame";
|
|
||||||
|
|
||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { loadFromJSON, saveAsJSON } from "./json";
|
export { loadFromJSON, saveAsJSON } from "./json";
|
||||||
@@ -130,6 +131,7 @@ export const exportCanvas = async (
|
|||||||
description: "Export to SVG",
|
description: "Export to SVG",
|
||||||
name,
|
name,
|
||||||
extension: appState.exportEmbedScene ? "excalidraw.svg" : "svg",
|
extension: appState.exportEmbedScene ? "excalidraw.svg" : "svg",
|
||||||
|
mimeTypes: [IMAGE_MIME_TYPES.svg],
|
||||||
fileHandle,
|
fileHandle,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -168,9 +170,8 @@ export const exportCanvas = async (
|
|||||||
return fileSave(blob, {
|
return fileSave(blob, {
|
||||||
description: "Export to PNG",
|
description: "Export to PNG",
|
||||||
name,
|
name,
|
||||||
// FIXME reintroduce `excalidraw.png` when most people upgrade away
|
extension: appState.exportEmbedScene ? "excalidraw.png" : "png",
|
||||||
// from 111.0.5563.64 (arm64), see #6349
|
mimeTypes: [IMAGE_MIME_TYPES.png],
|
||||||
extension: /* appState.exportEmbedScene ? "excalidraw.png" : */ "png",
|
|
||||||
fileHandle,
|
fileHandle,
|
||||||
});
|
});
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ import type { MaybePromise } from "../utility-types";
|
|||||||
import { Emitter } from "../emitter";
|
import { Emitter } from "../emitter";
|
||||||
import { Queue } from "../queue";
|
import { Queue } from "../queue";
|
||||||
import { hashElementsVersion, hashString } from "../element";
|
import { hashElementsVersion, hashString } from "../element";
|
||||||
|
import { toValidURL } from "./url";
|
||||||
|
|
||||||
|
const ALLOWED_LIBRARY_HOSTNAMES = ["excalidraw.com"];
|
||||||
|
|
||||||
type LibraryUpdate = {
|
type LibraryUpdate = {
|
||||||
/** deleted library items since last onLibraryChange event */
|
/** deleted library items since last onLibraryChange event */
|
||||||
@@ -467,6 +470,28 @@ export const distributeLibraryItemsOnSquareGrid = (
|
|||||||
return resElements;
|
return resElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateLibraryUrl = (
|
||||||
|
libraryUrl: string,
|
||||||
|
/**
|
||||||
|
* If supplied, takes precedence over the default whitelist.
|
||||||
|
* Return `true` if the URL is valid.
|
||||||
|
*/
|
||||||
|
validator?: (libraryUrl: string) => boolean,
|
||||||
|
): boolean => {
|
||||||
|
if (
|
||||||
|
validator
|
||||||
|
? validator(libraryUrl)
|
||||||
|
: ALLOWED_LIBRARY_HOSTNAMES.includes(
|
||||||
|
new URL(libraryUrl).hostname.split(".").slice(-2).join("."),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Invalid or disallowed library URL: "${libraryUrl}"`);
|
||||||
|
throw new Error("Invalid or disallowed library URL");
|
||||||
|
};
|
||||||
|
|
||||||
export const parseLibraryTokensFromUrl = () => {
|
export const parseLibraryTokensFromUrl = () => {
|
||||||
const libraryUrl =
|
const libraryUrl =
|
||||||
// current
|
// current
|
||||||
@@ -608,6 +633,11 @@ const persistLibraryUpdate = async (
|
|||||||
export const useHandleLibrary = (
|
export const useHandleLibrary = (
|
||||||
opts: {
|
opts: {
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
||||||
|
/**
|
||||||
|
* Return `true` if the library install url should be allowed.
|
||||||
|
* If not supplied, only the excalidraw.com base domain is allowed.
|
||||||
|
*/
|
||||||
|
validateLibraryUrl?: (libraryUrl: string) => boolean;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
/** @deprecated we recommend using `opts.adapter` instead */
|
/** @deprecated we recommend using `opts.adapter` instead */
|
||||||
@@ -650,7 +680,13 @@ export const useHandleLibrary = (
|
|||||||
}) => {
|
}) => {
|
||||||
const libraryPromise = new Promise<Blob>(async (resolve, reject) => {
|
const libraryPromise = new Promise<Blob>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const request = await fetch(decodeURIComponent(libraryUrl));
|
libraryUrl = decodeURIComponent(libraryUrl);
|
||||||
|
|
||||||
|
libraryUrl = toValidURL(libraryUrl);
|
||||||
|
|
||||||
|
validateLibraryUrl(libraryUrl, optsRef.current.validateLibraryUrl);
|
||||||
|
|
||||||
|
const request = await fetch(libraryUrl);
|
||||||
const blob = await request.blob();
|
const blob = await request.blob();
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -678,7 +714,12 @@ export const useHandleLibrary = (
|
|||||||
defaultStatus: "published",
|
defaultStatus: "published",
|
||||||
openLibraryMenu: true,
|
openLibraryMenu: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
excalidrawAPI.updateScene({
|
||||||
|
appState: {
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
|
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
|
||||||
|
|||||||
@@ -639,6 +639,7 @@ export const restoreAppState = (
|
|||||||
gridStep: getNormalizedGridStep(
|
gridStep: getNormalizedGridStep(
|
||||||
isFiniteNumber(appState.gridStep) ? appState.gridStep : DEFAULT_GRID_STEP,
|
isFiniteNumber(appState.gridStep) ? appState.gridStep : DEFAULT_GRID_STEP,
|
||||||
),
|
),
|
||||||
|
editingFrame: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -779,7 +779,7 @@ describe("Test Transform", () => {
|
|||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
fixedPoint: null,
|
fixedPoint: null,
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 205,
|
gap: 14,
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe("normalizeLink", () => {
|
|||||||
expect(normalizeLink("file://")).toBe("file://");
|
expect(normalizeLink("file://")).toBe("file://");
|
||||||
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
|
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
|
||||||
expect(normalizeLink("[[test]]")).toBe("[[test]]");
|
expect(normalizeLink("[[test]]")).toBe("[[test]]");
|
||||||
expect(normalizeLink("<test>")).toBe("<test>");
|
expect(normalizeLink("<test>")).toBe("<test>");
|
||||||
|
expect(normalizeLink("test&")).toBe("test&");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
import { sanitizeHTMLAttribute } from "../utils";
|
||||||
export const sanitizeHTMLAttribute = (html: string) => {
|
|
||||||
return html.replace(/"/g, """);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
export const normalizeLink = (link: string) => {
|
||||||
link = link.trim();
|
link = link.trim();
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FIXED_BINDING_DISTANCE = 5;
|
export const FIXED_BINDING_DISTANCE = 5;
|
||||||
|
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||||
|
export const BINDING_HIGHLIGHT_OFFSET = 4;
|
||||||
|
|
||||||
const getNonDeletedElements = (
|
const getNonDeletedElements = (
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
@@ -213,6 +215,7 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
edge: "start" | "end",
|
edge: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): NonDeleted<ExcalidrawElement> | null => {
|
): NonDeleted<ExcalidrawElement> | null => {
|
||||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||||
const elementId =
|
const elementId =
|
||||||
@@ -223,7 +226,7 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|||||||
const element = elementsMap.get(elementId);
|
const element = elementsMap.get(elementId);
|
||||||
if (
|
if (
|
||||||
isBindableElement(element) &&
|
isBindableElement(element) &&
|
||||||
bindingBorderTest(element, coors, elementsMap)
|
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||||
) {
|
) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -235,12 +238,14 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|||||||
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||||
["start", "end"].map((edge) =>
|
["start", "end"].map((edge) =>
|
||||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
||||||
linearElement,
|
linearElement,
|
||||||
edge as "start" | "end",
|
edge as "start" | "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
zoom,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -250,6 +255,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
draggingPoints: readonly number[],
|
draggingPoints: readonly number[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
||||||
const startIdx = 0;
|
const startIdx = 0;
|
||||||
const endIdx = selectedElement.points.length - 1;
|
const endIdx = selectedElement.points.length - 1;
|
||||||
@@ -262,6 +268,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and start is dragged, break all binds
|
: null // If binding is disabled and start is dragged, break all binds
|
||||||
: // We have to update the focus and gap of the binding, so let's rebind
|
: // We have to update the focus and gap of the binding, so let's rebind
|
||||||
@@ -270,6 +277,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
@@ -278,6 +286,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and end is dragged, break all binds
|
: null // If binding is disabled and end is dragged, break all binds
|
||||||
: // We have to update the focus and gap of the binding, so let's rebind
|
: // We have to update the focus and gap of the binding, so let's rebind
|
||||||
@@ -286,6 +295,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [start, end];
|
return [start, end];
|
||||||
@@ -296,10 +306,12 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
||||||
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
|
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
const start = startIsClose
|
const start = startIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
@@ -308,6 +320,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
: null;
|
: null;
|
||||||
@@ -318,6 +331,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
: null;
|
: null;
|
||||||
@@ -332,6 +346,7 @@ export const bindOrUnbindLinearElements = (
|
|||||||
scene: Scene,
|
scene: Scene,
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
draggingPoints: readonly number[] | null,
|
draggingPoints: readonly number[] | null,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): void => {
|
): void => {
|
||||||
selectedElements.forEach((selectedElement) => {
|
selectedElements.forEach((selectedElement) => {
|
||||||
const [start, end] = draggingPoints?.length
|
const [start, end] = draggingPoints?.length
|
||||||
@@ -342,6 +357,7 @@ export const bindOrUnbindLinearElements = (
|
|||||||
draggingPoints ?? [],
|
draggingPoints ?? [],
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
|
zoom,
|
||||||
)
|
)
|
||||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||||
getBindingStrategyForDraggingArrowOrJoints(
|
getBindingStrategyForDraggingArrowOrJoints(
|
||||||
@@ -349,6 +365,7 @@ export const bindOrUnbindLinearElements = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
||||||
@@ -358,6 +375,7 @@ export const bindOrUnbindLinearElements = (
|
|||||||
export const getSuggestedBindingsForArrows = (
|
export const getSuggestedBindingsForArrows = (
|
||||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
zoom: AppState["zoom"],
|
||||||
): SuggestedBinding[] => {
|
): SuggestedBinding[] => {
|
||||||
// HOT PATH: Bail out if selected elements list is too large
|
// HOT PATH: Bail out if selected elements list is too large
|
||||||
if (selectedElements.length > 50) {
|
if (selectedElements.length > 50) {
|
||||||
@@ -368,7 +386,7 @@ export const getSuggestedBindingsForArrows = (
|
|||||||
selectedElements
|
selectedElements
|
||||||
.filter(isLinearElement)
|
.filter(isLinearElement)
|
||||||
.flatMap((element) =>
|
.flatMap((element) =>
|
||||||
getOriginalBindingsIfStillCloseToArrowEnds(element, elementsMap),
|
getOriginalBindingsIfStillCloseToArrowEnds(element, elementsMap, zoom),
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
||||||
@@ -406,6 +424,7 @@ export const maybeBindLinearElement = (
|
|||||||
pointerCoords,
|
pointerCoords,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
isElbowArrow(linearElement) && isElbowArrow(linearElement),
|
isElbowArrow(linearElement) && isElbowArrow(linearElement),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -422,6 +441,26 @@ export const maybeBindLinearElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizePointBinding = (
|
||||||
|
binding: { focus: number; gap: number },
|
||||||
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
|
) => {
|
||||||
|
let gap = binding.gap;
|
||||||
|
const maxGap = maxBindingGap(
|
||||||
|
hoveredElement,
|
||||||
|
hoveredElement.width,
|
||||||
|
hoveredElement.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gap > maxGap) {
|
||||||
|
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
gap,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const bindLinearElement = (
|
export const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
@@ -433,11 +472,14 @@ export const bindLinearElement = (
|
|||||||
}
|
}
|
||||||
const binding: PointBinding = {
|
const binding: PointBinding = {
|
||||||
elementId: hoveredElement.id,
|
elementId: hoveredElement.id,
|
||||||
...calculateFocusAndGap(
|
...normalizePointBinding(
|
||||||
linearElement,
|
calculateFocusAndGap(
|
||||||
|
linearElement,
|
||||||
|
hoveredElement,
|
||||||
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
...(isElbowArrow(linearElement)
|
...(isElbowArrow(linearElement)
|
||||||
? calculateFixedPointForElbowArrowBinding(
|
? calculateFixedPointForElbowArrowBinding(
|
||||||
@@ -462,6 +504,12 @@ export const bindLinearElement = (
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update bound elements to make sure the binding tips are in sync with
|
||||||
|
// the normalized gap from above
|
||||||
|
if (!isElbowArrow(linearElement)) {
|
||||||
|
updateBoundElements(hoveredElement, elementsMap);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't bind both ends of a simple segment
|
// Don't bind both ends of a simple segment
|
||||||
@@ -514,6 +562,7 @@ export const getHoveredElementForBinding = (
|
|||||||
},
|
},
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
const hoveredElement = getElementAtPosition(
|
const hoveredElement = getElementAtPosition(
|
||||||
@@ -524,11 +573,13 @@ export const getHoveredElementForBinding = (
|
|||||||
element,
|
element,
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
zoom,
|
||||||
// disable fullshape snapping for frame elements so we
|
// disable fullshape snapping for frame elements so we
|
||||||
// can bind to frame children
|
// can bind to frame children
|
||||||
fullShape && !isFrameLikeElement(element),
|
fullShape && !isFrameLikeElement(element),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -578,9 +629,11 @@ export const updateBoundElements = (
|
|||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { newSize, simultaneouslyUpdated, changedElements } = options ?? {};
|
const { newSize, simultaneouslyUpdated, changedElements, zoom } =
|
||||||
|
options ?? {};
|
||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
@@ -670,6 +723,7 @@ export const updateBoundElements = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
changedElements,
|
changedElements,
|
||||||
|
zoom,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -703,6 +757,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
aabb: Bounds | undefined | null,
|
aabb: Bounds | undefined | null,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): Heading => {
|
): Heading => {
|
||||||
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
||||||
|
|
||||||
@@ -714,6 +769,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
origPoint,
|
origPoint,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
@@ -737,6 +793,7 @@ const getDistanceForBinding = (
|
|||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(
|
const distance = distanceToBindableElement(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
@@ -747,6 +804,7 @@ const getDistanceForBinding = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
bindableElement.width,
|
bindableElement.width,
|
||||||
bindableElement.height,
|
bindableElement.height,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
return distance > bindDistance ? null : distance;
|
return distance > bindDistance ? null : distance;
|
||||||
@@ -1174,11 +1232,13 @@ const getElligibleElementForBindingElement = (
|
|||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
return getHoveredElementForBinding(
|
return getHoveredElementForBinding(
|
||||||
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
|
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1341,9 +1401,11 @@ export const bindingBorderTest = (
|
|||||||
element: NonDeleted<ExcalidrawBindableElement>,
|
element: NonDeleted<ExcalidrawBindableElement>,
|
||||||
{ x, y }: { x: number; y: number },
|
{ x, y }: { x: number; y: number },
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = maxBindingGap(element, element.width, element.height);
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
|
||||||
const shape = getElementShape(element, elementsMap);
|
const shape = getElementShape(element, elementsMap);
|
||||||
return (
|
return (
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
||||||
@@ -1356,12 +1418,21 @@ export const maxBindingGap = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementWidth: number,
|
elementWidth: number,
|
||||||
elementHeight: number,
|
elementHeight: number,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
): number => {
|
): number => {
|
||||||
|
const zoomValue = zoom?.value && zoom.value < 1 ? zoom.value : 1;
|
||||||
|
|
||||||
// Aligns diamonds with rectangles
|
// Aligns diamonds with rectangles
|
||||||
const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
|
const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
|
||||||
const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
|
const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
|
||||||
// We make the bindable boundary bigger for bigger elements
|
|
||||||
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
|
return Math.max(
|
||||||
|
16,
|
||||||
|
// bigger bindable boundary for bigger elements
|
||||||
|
Math.min(0.25 * smallerDimension, 32),
|
||||||
|
// keep in sync with the zoomed highlight
|
||||||
|
BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
export const distanceToBindableElement = (
|
||||||
|
|||||||
@@ -556,6 +556,10 @@ export const getArrowheadSize = (arrowhead: Arrowhead): number => {
|
|||||||
case "diamond":
|
case "diamond":
|
||||||
case "diamond_outline":
|
case "diamond_outline":
|
||||||
return 12;
|
return 12;
|
||||||
|
case "crowfoot_many":
|
||||||
|
case "crowfoot_one":
|
||||||
|
case "crowfoot_one_or_many":
|
||||||
|
return 20;
|
||||||
default:
|
default:
|
||||||
return 15;
|
return 15;
|
||||||
}
|
}
|
||||||
@@ -669,6 +673,21 @@ export const getArrowheadPoints = (
|
|||||||
|
|
||||||
const angle = getArrowheadAngle(arrowhead);
|
const angle = getArrowheadAngle(arrowhead);
|
||||||
|
|
||||||
|
if (arrowhead === "crowfoot_many" || arrowhead === "crowfoot_one_or_many") {
|
||||||
|
// swap (xs, ys) with (x2, y2)
|
||||||
|
const [x3, y3] = pointRotateRads(
|
||||||
|
pointFrom(x2, y2),
|
||||||
|
pointFrom(xs, ys),
|
||||||
|
degreesToRadians(-angle as Degrees),
|
||||||
|
);
|
||||||
|
const [x4, y4] = pointRotateRads(
|
||||||
|
pointFrom(x2, y2),
|
||||||
|
pointFrom(xs, ys),
|
||||||
|
degreesToRadians(angle),
|
||||||
|
);
|
||||||
|
return [xs, ys, x3, y3, x4, y4];
|
||||||
|
}
|
||||||
|
|
||||||
// Return points
|
// Return points
|
||||||
const [x3, y3] = pointRotateRads(
|
const [x3, y3] = pointRotateRads(
|
||||||
pointFrom(xs, ys),
|
pointFrom(xs, ys),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
const MINIMAL_CROP_SIZE = 10;
|
export const MINIMAL_CROP_SIZE = 10;
|
||||||
|
|
||||||
export const cropElement = (
|
export const cropElement = (
|
||||||
element: ExcalidrawImageElement,
|
element: ExcalidrawImageElement,
|
||||||
@@ -585,3 +585,41 @@ const adjustCropPosition = (
|
|||||||
cropY,
|
cropY,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFlipAdjustedCropPosition = (
|
||||||
|
element: ExcalidrawImageElement,
|
||||||
|
natural = false,
|
||||||
|
) => {
|
||||||
|
const crop = element.crop;
|
||||||
|
if (!crop) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlippedByX = element.scale[0] === -1;
|
||||||
|
const isFlippedByY = element.scale[1] === -1;
|
||||||
|
|
||||||
|
let cropX = crop.x;
|
||||||
|
let cropY = crop.y;
|
||||||
|
|
||||||
|
if (isFlippedByX) {
|
||||||
|
cropX = crop.naturalWidth - crop.width - crop.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFlippedByY) {
|
||||||
|
cropY = crop.naturalHeight - crop.height - crop.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (natural) {
|
||||||
|
return {
|
||||||
|
x: cropX,
|
||||||
|
y: cropY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = getUncroppedWidthAndHeight(element);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: cropX / (crop.naturalWidth / width),
|
||||||
|
y: cropY / (crop.naturalHeight / height),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Create and link between shapes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ELEMENT_LINK_KEY } from "../constants";
|
||||||
|
import { normalizeLink } from "../data/url";
|
||||||
|
import { elementsAreInSameGroup } from "../groups";
|
||||||
|
import type { AppProps, AppState } from "../types";
|
||||||
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
export const defaultGetElementLinkFromSelection: Exclude<
|
||||||
|
AppProps["generateLinkForSelection"],
|
||||||
|
undefined
|
||||||
|
> = (id, type) => {
|
||||||
|
const url = window.location.href;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const link = new URL(url);
|
||||||
|
link.searchParams.set(ELEMENT_LINK_KEY, id);
|
||||||
|
|
||||||
|
return normalizeLink(link.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeLink(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLinkIdAndTypeFromSelection = (
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
): {
|
||||||
|
id: string;
|
||||||
|
type: "element" | "group";
|
||||||
|
} | null => {
|
||||||
|
if (
|
||||||
|
selectedElements.length > 0 &&
|
||||||
|
canCreateLinkFromElements(selectedElements)
|
||||||
|
) {
|
||||||
|
if (selectedElements.length === 1) {
|
||||||
|
return {
|
||||||
|
id: selectedElements[0].id,
|
||||||
|
type: "element",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedElements.length > 1) {
|
||||||
|
const selectedGroupId = Object.keys(appState.selectedGroupIds)[0];
|
||||||
|
|
||||||
|
if (selectedGroupId) {
|
||||||
|
return {
|
||||||
|
id: selectedGroupId,
|
||||||
|
type: "group",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: selectedElements[0].groupIds[0],
|
||||||
|
type: "group",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canCreateLinkFromElements = (
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
if (selectedElements.length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedElements.length > 1 && elementsAreInSameGroup(selectedElements)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isElementLink = (url: string) => {
|
||||||
|
try {
|
||||||
|
const _url = new URL(url);
|
||||||
|
return (
|
||||||
|
_url.searchParams.has(ELEMENT_LINK_KEY) &&
|
||||||
|
_url.host === window.location.host
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseElementLinkFromURL = (url: string) => {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(url);
|
||||||
|
if (searchParams.has(ELEMENT_LINK_KEY)) {
|
||||||
|
const id = searchParams.get(ELEMENT_LINK_KEY);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { register } from "../actions/register";
|
import { register } from "../actions/register";
|
||||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||||
import type { ExcalidrawProps } from "../types";
|
import type { ExcalidrawProps } from "../types";
|
||||||
import { getFontString, updateActiveTool } from "../utils";
|
import {
|
||||||
|
getFontString,
|
||||||
|
sanitizeHTMLAttribute,
|
||||||
|
updateActiveTool,
|
||||||
|
} from "../utils";
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
import { newTextElement } from "./newElement";
|
import { newTextElement } from "./newElement";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
@@ -11,7 +15,6 @@ import type {
|
|||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
IframeData,
|
IframeData,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { sanitizeHTMLAttribute } from "../data/url";
|
|
||||||
import type { MarkRequired } from "../utility-types";
|
import type { MarkRequired } from "../utility-types";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|
||||||
|
|||||||
@@ -105,20 +105,42 @@ export const normalizeSVG = (SVGString: string) => {
|
|||||||
svg.setAttribute("xmlns", SVG_NS);
|
svg.setAttribute("xmlns", SVG_NS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!svg.hasAttribute("width") || !svg.hasAttribute("height")) {
|
let width = svg.getAttribute("width");
|
||||||
const viewBox = svg.getAttribute("viewBox");
|
let height = svg.getAttribute("height");
|
||||||
let width = svg.getAttribute("width") || "50";
|
|
||||||
let height = svg.getAttribute("height") || "50";
|
// Do not use % or auto values for width/height
|
||||||
|
// to avoid scaling issues when rendering at different sizes/zoom levels
|
||||||
|
if (width?.includes("%") || width === "auto") {
|
||||||
|
width = null;
|
||||||
|
}
|
||||||
|
if (height?.includes("%") || height === "auto") {
|
||||||
|
height = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewBox = svg.getAttribute("viewBox");
|
||||||
|
|
||||||
|
if (!width || !height) {
|
||||||
|
width = width || "50";
|
||||||
|
height = height || "50";
|
||||||
|
|
||||||
if (viewBox) {
|
if (viewBox) {
|
||||||
const match = viewBox.match(/\d+ +\d+ +(\d+) +(\d+)/);
|
const match = viewBox.match(
|
||||||
|
/\d+ +\d+ +(\d+(?:\.\d+)?) +(\d+(?:\.\d+)?)/,
|
||||||
|
);
|
||||||
if (match) {
|
if (match) {
|
||||||
[, width, height] = match;
|
[, width, height] = match;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.setAttribute("width", width);
|
svg.setAttribute("width", width);
|
||||||
svg.setAttribute("height", height);
|
svg.setAttribute("height", height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure viewBox is set
|
||||||
|
if (!viewBox) {
|
||||||
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||||
|
}
|
||||||
|
|
||||||
return svg.outerHTML;
|
return svg.outerHTML;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ export class LinearElementEditor {
|
|||||||
),
|
),
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -787,6 +788,7 @@ export class LinearElementEditor {
|
|||||||
scenePointer,
|
scenePointer,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
app.state.zoom,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -911,6 +913,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
[points.length - 1],
|
[points.length - 1],
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
app.state.zoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -964,6 +967,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
[{ point: newPoint }],
|
[{ point: newPoint }],
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
app.state.zoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -1218,6 +1222,7 @@ export class LinearElementEditor {
|
|||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
pointIndices: readonly number[],
|
pointIndices: readonly number[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
zoom: AppState["zoom"],
|
||||||
) {
|
) {
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
@@ -1260,6 +1265,7 @@ export class LinearElementEditor {
|
|||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { point: LocalPoint }[],
|
targetPoints: { point: LocalPoint }[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
zoom: AppState["zoom"],
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const offsetX = 0;
|
||||||
const offsetY = 0;
|
const offsetY = 0;
|
||||||
@@ -1285,6 +1291,7 @@ export class LinearElementEditor {
|
|||||||
options?: {
|
options?: {
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
@@ -1337,6 +1344,7 @@ export class LinearElementEditor {
|
|||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
changedElements: options?.changedElements,
|
changedElements: options?.changedElements,
|
||||||
|
zoom: options?.zoom,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1451,6 +1459,7 @@ export class LinearElementEditor {
|
|||||||
options?: {
|
options?: {
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
@@ -1487,6 +1496,7 @@ export class LinearElementEditor {
|
|||||||
bindings,
|
bindings,
|
||||||
{
|
{
|
||||||
isDragging: options?.isDragging,
|
isDragging: options?.isDragging,
|
||||||
|
zoom: options?.zoom,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ import {
|
|||||||
import BinaryHeap from "../binaryheap";
|
import BinaryHeap from "../binaryheap";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import { aabbForElement, pointInsideBounds } from "../shapes";
|
import { aabbForElement, pointInsideBounds } from "../shapes";
|
||||||
|
import type { AppState } from "../types";
|
||||||
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
||||||
import {
|
import {
|
||||||
bindPointToSnapToElementOutline,
|
bindPointToSnapToElementOutline,
|
||||||
@@ -79,6 +80,7 @@ export const mutateElbowArrow = (
|
|||||||
options?: {
|
options?: {
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
informMutation?: boolean;
|
informMutation?: boolean;
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const update = updateElbowArrow(
|
const update = updateElbowArrow(
|
||||||
@@ -112,6 +114,7 @@ export const updateElbowArrow = (
|
|||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
disableBinding?: boolean;
|
disableBinding?: boolean;
|
||||||
informMutation?: boolean;
|
informMutation?: boolean;
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
|
): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
|
||||||
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
||||||
@@ -136,7 +139,12 @@ export const updateElbowArrow = (
|
|||||||
arrow.endBinding &&
|
arrow.endBinding &&
|
||||||
getBindableElementForId(arrow.endBinding.elementId, elementsMap);
|
getBindableElementForId(arrow.endBinding.elementId, elementsMap);
|
||||||
const [hoveredStartElement, hoveredEndElement] = options?.isDragging
|
const [hoveredStartElement, hoveredEndElement] = options?.isDragging
|
||||||
? getHoveredElements(origStartGlobalPoint, origEndGlobalPoint, elementsMap)
|
? getHoveredElements(
|
||||||
|
origStartGlobalPoint,
|
||||||
|
origEndGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
|
options?.zoom,
|
||||||
|
)
|
||||||
: [startElement, endElement];
|
: [startElement, endElement];
|
||||||
const startGlobalPoint = getGlobalPoint(
|
const startGlobalPoint = getGlobalPoint(
|
||||||
arrow.startBinding?.fixedPoint,
|
arrow.startBinding?.fixedPoint,
|
||||||
@@ -1072,6 +1080,7 @@ const getHoveredElements = (
|
|||||||
origStartGlobalPoint: GlobalPoint,
|
origStartGlobalPoint: GlobalPoint,
|
||||||
origEndGlobalPoint: GlobalPoint,
|
origEndGlobalPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
// TODO: Might be a performance bottleneck and the Map type
|
// TODO: Might be a performance bottleneck and the Map type
|
||||||
// remembers the insertion order anyway...
|
// remembers the insertion order anyway...
|
||||||
@@ -1084,12 +1093,14 @@ const getHoveredElements = (
|
|||||||
tupleToCoors(origStartGlobalPoint),
|
tupleToCoors(origStartGlobalPoint),
|
||||||
elements,
|
elements,
|
||||||
nonDeletedSceneElementsMap,
|
nonDeletedSceneElementsMap,
|
||||||
|
zoom,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
getHoveredElementForBinding(
|
getHoveredElementForBinding(
|
||||||
tupleToCoors(origEndGlobalPoint),
|
tupleToCoors(origEndGlobalPoint),
|
||||||
elements,
|
elements,
|
||||||
nonDeletedSceneElementsMap,
|
nonDeletedSceneElementsMap,
|
||||||
|
zoom,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const showSelectedShapeActions = (
|
|||||||
) =>
|
) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
!appState.viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
|
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||||
((appState.activeTool.type !== "custom" &&
|
((appState.activeTool.type !== "custom" &&
|
||||||
(appState.editingTextElement ||
|
(appState.editingTextElement ||
|
||||||
(appState.activeTool.type !== "selection" &&
|
(appState.activeTool.type !== "selection" &&
|
||||||
|
|||||||
@@ -303,7 +303,10 @@ export type Arrowhead =
|
|||||||
| "triangle"
|
| "triangle"
|
||||||
| "triangle_outline"
|
| "triangle_outline"
|
||||||
| "diamond"
|
| "diamond"
|
||||||
| "diamond_outline";
|
| "diamond_outline"
|
||||||
|
| "crowfoot_one"
|
||||||
|
| "crowfoot_many"
|
||||||
|
| "crowfoot_one_or_many";
|
||||||
|
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
generateIdForFile,
|
generateIdForFile,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
|
generateLinkForSelection,
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
onPointerUp,
|
onPointerUp,
|
||||||
onScrollChange,
|
onScrollChange,
|
||||||
@@ -132,6 +133,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
generateIdForFile={generateIdForFile}
|
generateIdForFile={generateIdForFile}
|
||||||
onLinkOpen={onLinkOpen}
|
onLinkOpen={onLinkOpen}
|
||||||
|
generateLinkForSelection={generateLinkForSelection}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDown={onPointerDown}
|
||||||
onPointerUp={onPointerUp}
|
onPointerUp={onPointerUp}
|
||||||
onScrollChange={onScrollChange}
|
onScrollChange={onScrollChange}
|
||||||
@@ -291,3 +293,4 @@ export {
|
|||||||
|
|
||||||
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
||||||
export { getDataURL } from "./data/blob";
|
export { getDataURL } from "./data/blob";
|
||||||
|
export { isElementLink } from "./element/elementLink";
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
"arrowhead_triangle_outline": "Triangle (outline)",
|
"arrowhead_triangle_outline": "Triangle (outline)",
|
||||||
"arrowhead_diamond": "Diamond",
|
"arrowhead_diamond": "Diamond",
|
||||||
"arrowhead_diamond_outline": "Diamond (outline)",
|
"arrowhead_diamond_outline": "Diamond (outline)",
|
||||||
|
"arrowhead_crowfoot_many": "Crow's foot (many)",
|
||||||
|
"arrowhead_crowfoot_one": "Crow's foot (one)",
|
||||||
|
"arrowhead_crowfoot_one_or_many": "Crow's foot (one or many)",
|
||||||
|
"more_options": "More options",
|
||||||
"arrowtypes": "Arrow type",
|
"arrowtypes": "Arrow type",
|
||||||
"arrowtype_sharp": "Sharp arrow",
|
"arrowtype_sharp": "Sharp arrow",
|
||||||
"arrowtype_round": "Curved arrow",
|
"arrowtype_round": "Curved arrow",
|
||||||
@@ -125,12 +129,13 @@
|
|||||||
"createContainerFromText": "Wrap text in a container",
|
"createContainerFromText": "Wrap text in a container",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edit link",
|
"edit": "Edit link",
|
||||||
"editEmbed": "Edit link & embed",
|
"editEmbed": "Edit embeddable link",
|
||||||
"create": "Create link",
|
"create": "Add link",
|
||||||
"createEmbed": "Create link & embed",
|
|
||||||
"label": "Link",
|
"label": "Link",
|
||||||
"labelEmbed": "Link & embed",
|
"labelEmbed": "Link & embed",
|
||||||
"empty": "No link is set"
|
"empty": "No link is set",
|
||||||
|
"hint": "Type or paste your link here",
|
||||||
|
"goToElement": "Go to target element"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Edit line",
|
"edit": "Edit line",
|
||||||
@@ -155,7 +160,16 @@
|
|||||||
"zoomToFitSelection": "Zoom to fit selection",
|
"zoomToFitSelection": "Zoom to fit selection",
|
||||||
"zoomToFit": "Zoom to fit all elements",
|
"zoomToFit": "Zoom to fit all elements",
|
||||||
"installPWA": "Install Excalidraw locally (PWA)",
|
"installPWA": "Install Excalidraw locally (PWA)",
|
||||||
"autoResize": "Enable text auto-resizing"
|
"autoResize": "Enable text auto-resizing",
|
||||||
|
"imageCropping": "Image cropping",
|
||||||
|
"unCroppedDimension": "Uncropped dimension",
|
||||||
|
"copyElementLink": "Copy link to object",
|
||||||
|
"linkToElement": "Link to object"
|
||||||
|
},
|
||||||
|
"elementLink": {
|
||||||
|
"title": "Link to object",
|
||||||
|
"desc": "Click on a shape on canvas or paste a link.",
|
||||||
|
"notFound": "Linked object wasn't found on canvas."
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "No items added yet...",
|
"noItems": "No items added yet...",
|
||||||
@@ -501,7 +515,8 @@
|
|||||||
"selection": "selection",
|
"selection": "selection",
|
||||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
||||||
"unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted",
|
"unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted",
|
||||||
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site"
|
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site",
|
||||||
|
"elementLinkCopied": "Link copied to clipboard"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Transparent",
|
"transparent": "Transparent",
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ import type {
|
|||||||
SuggestedBinding,
|
SuggestedBinding,
|
||||||
SuggestedPointBinding,
|
SuggestedPointBinding,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { maxBindingGap } from "../element/binding";
|
import {
|
||||||
|
BINDING_HIGHLIGHT_OFFSET,
|
||||||
|
BINDING_HIGHLIGHT_THICKNESS,
|
||||||
|
maxBindingGap,
|
||||||
|
} from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import {
|
import {
|
||||||
bootstrapCanvas,
|
bootstrapCanvas,
|
||||||
@@ -217,17 +221,18 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
zoom: InteractiveCanvasAppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
const width = x2 - x1;
|
const width = x2 - x1;
|
||||||
const height = y2 - y1;
|
const height = y2 - y1;
|
||||||
const thickness = 10;
|
|
||||||
|
|
||||||
// So that we don't overlap the element itself
|
|
||||||
const strokeOffset = 4;
|
|
||||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||||
context.lineWidth = thickness - strokeOffset;
|
// When zooming out, make line width greater for visibility
|
||||||
const padding = strokeOffset / 2 + thickness / 2;
|
const zoomValue = zoom.value < 1 ? zoom.value : 1;
|
||||||
|
context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue;
|
||||||
|
// To ensure the binding highlight doesn't overlap the element itself
|
||||||
|
const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
|
||||||
|
|
||||||
const radius = getCornerRadius(
|
const radius = getCornerRadius(
|
||||||
Math.min(element.width, element.height),
|
Math.min(element.width, element.height),
|
||||||
@@ -285,6 +290,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
|
|||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
suggestedBinding: SuggestedPointBinding,
|
suggestedBinding: SuggestedPointBinding,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
zoom: InteractiveCanvasAppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const [element, startOrEnd, bindableElement] = suggestedBinding;
|
const [element, startOrEnd, bindableElement] = suggestedBinding;
|
||||||
|
|
||||||
@@ -292,6 +298,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
bindableElement.width,
|
bindableElement.width,
|
||||||
bindableElement.height,
|
bindableElement.height,
|
||||||
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
context.strokeStyle = "rgba(0,0,0,0)";
|
context.strokeStyle = "rgba(0,0,0,0)";
|
||||||
@@ -390,7 +397,7 @@ const renderBindingHighlight = (
|
|||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(appState.scrollX, appState.scrollY);
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
renderHighlight(context, suggestedBinding as any, elementsMap);
|
renderHighlight(context, suggestedBinding as any, elementsMap, appState.zoom);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import type {
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
|
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
||||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
@@ -109,10 +110,13 @@ export const getRenderOpacity = (
|
|||||||
containingFrame: ExcalidrawFrameLikeElement | null,
|
containingFrame: ExcalidrawFrameLikeElement | null,
|
||||||
elementsPendingErasure: ElementsPendingErasure,
|
elementsPendingErasure: ElementsPendingErasure,
|
||||||
pendingNodes: Readonly<PendingExcalidrawElements> | null,
|
pendingNodes: Readonly<PendingExcalidrawElements> | null,
|
||||||
|
globalAlpha: number = 1,
|
||||||
) => {
|
) => {
|
||||||
// multiplying frame opacity with element opacity to combine them
|
// multiplying frame opacity with element opacity to combine them
|
||||||
// (e.g. frame 50% and element 50% opacity should result in 25% opacity)
|
// (e.g. frame 50% and element 50% opacity should result in 25% opacity)
|
||||||
let opacity = ((containingFrame?.opacity ?? 100) * element.opacity) / 10000;
|
let opacity =
|
||||||
|
(((containingFrame?.opacity ?? 100) * element.opacity) / 10000) *
|
||||||
|
globalAlpha;
|
||||||
|
|
||||||
// if pending erasure, multiply again to combine further
|
// if pending erasure, multiply again to combine further
|
||||||
// (so that erasing always results in lower opacity than original)
|
// (so that erasing always results in lower opacity than original)
|
||||||
@@ -700,11 +704,17 @@ export const renderElement = (
|
|||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
|
const reduceAlphaForSelection =
|
||||||
|
appState.openDialog?.name === "elementLinkSelector" &&
|
||||||
|
!appState.selectedElementIds[element.id] &&
|
||||||
|
!appState.hoveredElementIds[element.id];
|
||||||
|
|
||||||
context.globalAlpha = getRenderOpacity(
|
context.globalAlpha = getRenderOpacity(
|
||||||
element,
|
element,
|
||||||
getContainingFrame(element, elementsMap),
|
getContainingFrame(element, elementsMap),
|
||||||
renderConfig.elementsPendingErasure,
|
renderConfig.elementsPendingErasure,
|
||||||
renderConfig.pendingFlowchartNodes,
|
renderConfig.pendingFlowchartNodes,
|
||||||
|
reduceAlphaForSelection ? DEFAULT_REDUCED_GLOBAL_ALPHA : 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ import type {
|
|||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import {
|
import {
|
||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
|
ELEMENT_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
} from "../components/hyperlink/helpers";
|
} from "../components/hyperlink/helpers";
|
||||||
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
|
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
|
||||||
import { throttleRAF } from "../utils";
|
import { throttleRAF } from "../utils";
|
||||||
import { getBoundTextElement } from "../element/textElement";
|
import { getBoundTextElement } from "../element/textElement";
|
||||||
|
import { isElementLink } from "../element/elementLink";
|
||||||
|
|
||||||
const GridLineColor = {
|
const GridLineColor = {
|
||||||
Bold: "#dddddd",
|
Bold: "#dddddd",
|
||||||
@@ -133,7 +135,16 @@ const frameClip = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let linkCanvasCache: any;
|
type LinkIconCanvas = HTMLCanvasElement & { zoom: number };
|
||||||
|
|
||||||
|
const linkIconCanvasCache: {
|
||||||
|
regularLink: LinkIconCanvas | null;
|
||||||
|
elementLink: LinkIconCanvas | null;
|
||||||
|
} = {
|
||||||
|
regularLink: null,
|
||||||
|
elementLink: null,
|
||||||
|
};
|
||||||
|
|
||||||
const renderLinkIcon = (
|
const renderLinkIcon = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -153,38 +164,44 @@ const renderLinkIcon = (
|
|||||||
context.translate(appState.scrollX + centerX, appState.scrollY + centerY);
|
context.translate(appState.scrollX + centerX, appState.scrollY + centerY);
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
|
|
||||||
if (!linkCanvasCache || linkCanvasCache.zoom !== appState.zoom.value) {
|
const canvasKey = isElementLink(element.link)
|
||||||
linkCanvasCache = document.createElement("canvas");
|
? "elementLink"
|
||||||
linkCanvasCache.zoom = appState.zoom.value;
|
: "regularLink";
|
||||||
linkCanvasCache.width =
|
|
||||||
width * window.devicePixelRatio * appState.zoom.value;
|
let linkCanvas = linkIconCanvasCache[canvasKey];
|
||||||
linkCanvasCache.height =
|
|
||||||
|
if (!linkCanvas || linkCanvas.zoom !== appState.zoom.value) {
|
||||||
|
linkCanvas = Object.assign(document.createElement("canvas"), {
|
||||||
|
zoom: appState.zoom.value,
|
||||||
|
});
|
||||||
|
linkCanvas.width = width * window.devicePixelRatio * appState.zoom.value;
|
||||||
|
linkCanvas.height =
|
||||||
height * window.devicePixelRatio * appState.zoom.value;
|
height * window.devicePixelRatio * appState.zoom.value;
|
||||||
const linkCanvasCacheContext = linkCanvasCache.getContext("2d")!;
|
linkIconCanvasCache[canvasKey] = linkCanvas;
|
||||||
|
|
||||||
|
const linkCanvasCacheContext = linkCanvas.getContext("2d")!;
|
||||||
linkCanvasCacheContext.scale(
|
linkCanvasCacheContext.scale(
|
||||||
window.devicePixelRatio * appState.zoom.value,
|
window.devicePixelRatio * appState.zoom.value,
|
||||||
window.devicePixelRatio * appState.zoom.value,
|
window.devicePixelRatio * appState.zoom.value,
|
||||||
);
|
);
|
||||||
linkCanvasCacheContext.fillStyle = "#fff";
|
linkCanvasCacheContext.fillStyle = "#fff";
|
||||||
linkCanvasCacheContext.fillRect(0, 0, width, height);
|
linkCanvasCacheContext.fillRect(0, 0, width, height);
|
||||||
linkCanvasCacheContext.drawImage(EXTERNAL_LINK_IMG, 0, 0, width, height);
|
|
||||||
|
if (canvasKey === "elementLink") {
|
||||||
|
linkCanvasCacheContext.drawImage(ELEMENT_LINK_IMG, 0, 0, width, height);
|
||||||
|
} else {
|
||||||
|
linkCanvasCacheContext.drawImage(
|
||||||
|
EXTERNAL_LINK_IMG,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
linkCanvasCacheContext.restore();
|
linkCanvasCacheContext.restore();
|
||||||
context.drawImage(
|
|
||||||
linkCanvasCache,
|
|
||||||
x - centerX,
|
|
||||||
y - centerY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
context.drawImage(
|
|
||||||
linkCanvasCache,
|
|
||||||
x - centerX,
|
|
||||||
y - centerY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
context.drawImage(linkCanvas, x - centerX, y - centerY, width, height);
|
||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ const renderElementToSvg = (
|
|||||||
|
|
||||||
symbol.appendChild(image);
|
symbol.appendChild(image);
|
||||||
|
|
||||||
root.prepend(symbol);
|
(root.querySelector("defs") || root).prepend(symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
const use = svgRoot.ownerDocument!.createElementNS(SVG_NS, "use");
|
const use = svgRoot.ownerDocument!.createElementNS(SVG_NS, "use");
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { toBrandedType } from "../utils";
|
import { toBrandedType } from "../utils";
|
||||||
import { ENV } from "../constants";
|
import { ENV } from "../constants";
|
||||||
|
import { getElementsInGroup } from "../groups";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
@@ -437,6 +438,18 @@ class Scene {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getElementsFromId = (id: string): ExcalidrawElement[] => {
|
||||||
|
const elementsMap = this.getNonDeletedElementsMap();
|
||||||
|
// first check if the id is an element
|
||||||
|
const el = elementsMap.get(id);
|
||||||
|
if (el) {
|
||||||
|
return [el];
|
||||||
|
}
|
||||||
|
|
||||||
|
// then, check if the id is a group
|
||||||
|
return getElementsInGroup(elementsMap, id);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
||||||
|
|||||||
@@ -177,6 +177,19 @@ const getArrowheadShapes = (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateCrowfootOne = (
|
||||||
|
arrowheadPoints: number[] | null,
|
||||||
|
options: Options,
|
||||||
|
) => {
|
||||||
|
if (arrowheadPoints === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, , x3, y3, x4, y4] = arrowheadPoints;
|
||||||
|
|
||||||
|
return [generator.line(x3, y3, x4, y4, options)];
|
||||||
|
};
|
||||||
|
|
||||||
switch (arrowhead) {
|
switch (arrowhead) {
|
||||||
case "dot":
|
case "dot":
|
||||||
case "circle":
|
case "circle":
|
||||||
@@ -255,8 +268,12 @@ const getArrowheadShapes = (
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
case "crowfoot_one":
|
||||||
|
return generateCrowfootOne(arrowheadPoints, options);
|
||||||
case "bar":
|
case "bar":
|
||||||
case "arrow":
|
case "arrow":
|
||||||
|
case "crowfoot_many":
|
||||||
|
case "crowfoot_one_or_many":
|
||||||
default: {
|
default: {
|
||||||
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
||||||
|
|
||||||
@@ -272,6 +289,12 @@ const getArrowheadShapes = (
|
|||||||
return [
|
return [
|
||||||
generator.line(x3, y3, x2, y2, options),
|
generator.line(x3, y3, x2, y2, options),
|
||||||
generator.line(x4, y4, x2, y2, options),
|
generator.line(x4, y4, x2, y2, options),
|
||||||
|
...(arrowhead === "crowfoot_one_or_many"
|
||||||
|
? generateCrowfootOne(
|
||||||
|
getArrowheadPoints(element, shape, position, "crowfoot_one"),
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
SVG_NS,
|
SVG_NS,
|
||||||
THEME,
|
THEME,
|
||||||
THEME_FILTER,
|
THEME_FILTER,
|
||||||
|
MIME_TYPES,
|
||||||
|
EXPORT_DATA_TYPES,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { serializeAsJSON } from "../data/json";
|
import { serializeAsJSON } from "../data/json";
|
||||||
@@ -39,8 +41,7 @@ import type { RenderableElementsMap } from "./types";
|
|||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
|
import { base64ToString, decode, encode, stringToBase64 } from "../data/encode";
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
|
||||||
|
|
||||||
const truncateText = (element: ExcalidrawTextElement, maxWidth: number) => {
|
const truncateText = (element: ExcalidrawTextElement, maxWidth: number) => {
|
||||||
if (element.width <= maxWidth) {
|
if (element.width <= maxWidth) {
|
||||||
@@ -254,6 +255,13 @@ export const exportToCanvas = async (
|
|||||||
return canvas;
|
return canvas;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createHTMLComment = (text: string) => {
|
||||||
|
// surrounding with spaces to maintain prettified consistency with previous
|
||||||
|
// iterations
|
||||||
|
// <!-- comment -->
|
||||||
|
return document.createComment(` ${text} `);
|
||||||
|
};
|
||||||
|
|
||||||
export const exportToSvg = async (
|
export const exportToSvg = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: {
|
appState: {
|
||||||
@@ -302,31 +310,20 @@ export const exportToSvg = async (
|
|||||||
exportPadding = 0;
|
exportPadding = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = "";
|
|
||||||
|
|
||||||
// we need to serialize the "original" elements before we put them through
|
|
||||||
// the tempScene hack which duplicates and regenerates ids
|
|
||||||
if (exportEmbedScene) {
|
|
||||||
try {
|
|
||||||
metadata = (await import("../data/image")).encodeSvgMetadata({
|
|
||||||
// when embedding scene, we want to embed the origionally supplied
|
|
||||||
// elements which don't contain the temp frame labels.
|
|
||||||
// But it also requires that the exportToSvg is being supplied with
|
|
||||||
// only the elements that we're exporting, and no extra.
|
|
||||||
text: serializeAsJSON(elements, appState, files || {}, "local"),
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [minX, minY, width, height] = getCanvasSize(
|
const [minX, minY, width, height] = getCanvasSize(
|
||||||
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
||||||
exportPadding,
|
exportPadding,
|
||||||
);
|
);
|
||||||
|
|
||||||
// initialize SVG root
|
const offsetX = -minX + exportPadding;
|
||||||
|
const offsetY = -minY + exportPadding;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// initialize SVG root element
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const svgRoot = document.createElementNS(SVG_NS, "svg");
|
const svgRoot = document.createElementNS(SVG_NS, "svg");
|
||||||
|
|
||||||
svgRoot.setAttribute("version", "1.1");
|
svgRoot.setAttribute("version", "1.1");
|
||||||
svgRoot.setAttribute("xmlns", SVG_NS);
|
svgRoot.setAttribute("xmlns", SVG_NS);
|
||||||
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||||
@@ -336,53 +333,105 @@ export const exportToSvg = async (
|
|||||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetX = -minX + exportPadding;
|
const defsElement = svgRoot.ownerDocument.createElementNS(SVG_NS, "defs");
|
||||||
const offsetY = -minY + exportPadding;
|
|
||||||
|
const metadataElement = svgRoot.ownerDocument.createElementNS(
|
||||||
|
SVG_NS,
|
||||||
|
"metadata",
|
||||||
|
);
|
||||||
|
|
||||||
|
svgRoot.appendChild(createHTMLComment("svg-source:excalidraw"));
|
||||||
|
svgRoot.appendChild(metadataElement);
|
||||||
|
svgRoot.appendChild(defsElement);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// scene embed
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// we need to serialize the "original" elements before we put them through
|
||||||
|
// the tempScene hack which duplicates and regenerates ids
|
||||||
|
if (exportEmbedScene) {
|
||||||
|
try {
|
||||||
|
encodeSvgBase64Payload({
|
||||||
|
metadataElement,
|
||||||
|
// when embedding scene, we want to embed the origionally supplied
|
||||||
|
// elements which don't contain the temp frame labels.
|
||||||
|
// But it also requires that the exportToSvg is being supplied with
|
||||||
|
// only the elements that we're exporting, and no extra.
|
||||||
|
payload: serializeAsJSON(elements, appState, files || {}, "local"),
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// frame clip paths
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const frameElements = getFrameLikeElements(elements);
|
const frameElements = getFrameLikeElements(elements);
|
||||||
|
|
||||||
let exportingFrameClipPath = "";
|
if (frameElements.length) {
|
||||||
const elementsMap = arrayToMap(elements);
|
const elementsMap = arrayToMap(elements);
|
||||||
for (const frame of frameElements) {
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
|
|
||||||
const cx = (x2 - x1) / 2 - (frame.x - x1);
|
|
||||||
const cy = (y2 - y1) / 2 - (frame.y - y1);
|
|
||||||
|
|
||||||
exportingFrameClipPath += `<clipPath id=${frame.id}>
|
for (const frame of frameElements) {
|
||||||
<rect transform="translate(${frame.x + offsetX} ${
|
const clipPath = svgRoot.ownerDocument.createElementNS(
|
||||||
frame.y + offsetY
|
SVG_NS,
|
||||||
}) rotate(${frame.angle} ${cx} ${cy})"
|
"clipPath",
|
||||||
width="${frame.width}"
|
);
|
||||||
height="${frame.height}"
|
|
||||||
${
|
clipPath.setAttribute("id", frame.id);
|
||||||
exportingFrame
|
|
||||||
? ""
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||||
: `rx=${FRAME_STYLE.radius} ry=${FRAME_STYLE.radius}`
|
const cx = (x2 - x1) / 2 - (frame.x - x1);
|
||||||
}
|
const cy = (y2 - y1) / 2 - (frame.y - y1);
|
||||||
>
|
|
||||||
</rect>
|
const rect = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
|
||||||
</clipPath>`;
|
rect.setAttribute(
|
||||||
|
"transform",
|
||||||
|
`translate(${frame.x + offsetX} ${frame.y + offsetY}) rotate(${
|
||||||
|
frame.angle
|
||||||
|
} ${cx} ${cy})`,
|
||||||
|
);
|
||||||
|
rect.setAttribute("width", `${frame.width}`);
|
||||||
|
rect.setAttribute("height", `${frame.height}`);
|
||||||
|
|
||||||
|
if (!exportingFrame) {
|
||||||
|
rect.setAttribute("rx", `${FRAME_STYLE.radius}`);
|
||||||
|
rect.setAttribute("ry", `${FRAME_STYLE.radius}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
clipPath.appendChild(rect);
|
||||||
|
|
||||||
|
defsElement.appendChild(clipPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// inline font faces
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const fontFaces = !opts?.skipInliningFonts
|
const fontFaces = !opts?.skipInliningFonts
|
||||||
? await Fonts.generateFontFaceDeclarations(elements)
|
? await Fonts.generateFontFaceDeclarations(elements)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const delimiter = "\n "; // 6 spaces
|
const delimiter = "\n "; // 6 spaces
|
||||||
|
|
||||||
svgRoot.innerHTML = `
|
const style = svgRoot.ownerDocument.createElementNS(SVG_NS, "style");
|
||||||
${SVG_EXPORT_TAG}
|
style.classList.add("style-fonts");
|
||||||
${metadata}
|
style.appendChild(
|
||||||
<defs>
|
document.createTextNode(`${delimiter}${fontFaces.join(delimiter)}`),
|
||||||
<style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}
|
);
|
||||||
</style>
|
|
||||||
${exportingFrameClipPath}
|
defsElement.appendChild(style);
|
||||||
</defs>
|
|
||||||
`;
|
// ---------------------------------------------------------------------------
|
||||||
|
// background
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// render background rect
|
// render background rect
|
||||||
if (appState.exportBackground && viewBackgroundColor) {
|
if (appState.exportBackground && viewBackgroundColor) {
|
||||||
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
|
const rect = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
|
||||||
rect.setAttribute("x", "0");
|
rect.setAttribute("x", "0");
|
||||||
rect.setAttribute("y", "0");
|
rect.setAttribute("y", "0");
|
||||||
rect.setAttribute("width", `${width}`);
|
rect.setAttribute("width", `${width}`);
|
||||||
@@ -391,6 +440,10 @@ export const exportToSvg = async (
|
|||||||
svgRoot.appendChild(rect);
|
svgRoot.appendChild(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// render elements
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const rsvg = rough.svg(svgRoot);
|
const rsvg = rough.svg(svgRoot);
|
||||||
|
|
||||||
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
||||||
@@ -420,9 +473,66 @@ export const exportToSvg = async (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
return svgRoot;
|
return svgRoot;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const encodeSvgBase64Payload = ({
|
||||||
|
payload,
|
||||||
|
metadataElement,
|
||||||
|
}: {
|
||||||
|
payload: string;
|
||||||
|
metadataElement: SVGMetadataElement;
|
||||||
|
}) => {
|
||||||
|
const base64 = stringToBase64(
|
||||||
|
JSON.stringify(encode({ text: payload })),
|
||||||
|
true /* is already byte string */,
|
||||||
|
);
|
||||||
|
|
||||||
|
metadataElement.appendChild(
|
||||||
|
createHTMLComment(`payload-type:${MIME_TYPES.excalidraw}`),
|
||||||
|
);
|
||||||
|
metadataElement.appendChild(createHTMLComment("payload-version:2"));
|
||||||
|
metadataElement.appendChild(createHTMLComment("payload-start"));
|
||||||
|
metadataElement.appendChild(document.createTextNode(base64));
|
||||||
|
metadataElement.appendChild(createHTMLComment("payload-end"));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeSvgBase64Payload = ({ svg }: { svg: string }) => {
|
||||||
|
if (svg.includes(`payload-type:${MIME_TYPES.excalidraw}`)) {
|
||||||
|
const match = svg.match(
|
||||||
|
/<!-- payload-start -->\s*(.+?)\s*<!-- payload-end -->/,
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("INVALID");
|
||||||
|
}
|
||||||
|
const versionMatch = svg.match(/<!-- payload-version:(\d+) -->/);
|
||||||
|
const version = versionMatch?.[1] || "1";
|
||||||
|
const isByteString = version !== "1";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = base64ToString(match[1], isByteString);
|
||||||
|
const encodedData = JSON.parse(json);
|
||||||
|
if (!("encoded" in encodedData)) {
|
||||||
|
// legacy, un-encoded scene JSON
|
||||||
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
throw new Error("FAILED");
|
||||||
|
}
|
||||||
|
return decode(encodedData);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error("FAILED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("INVALID");
|
||||||
|
};
|
||||||
|
|
||||||
// calculate smallest area to fit the contents in
|
// calculate smallest area to fit the contents in
|
||||||
const getCanvasSize = (
|
const getCanvasSize = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
|||||||
@@ -728,6 +728,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -762,6 +763,42 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"category": "hyperlink",
|
"category": "hyperlink",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": <svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</svg>,
|
||||||
|
"label": "labels.copyElementLink",
|
||||||
|
"name": "copyElementLink",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -880,6 +917,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -968,14 +1006,14 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 1278240551,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -1004,14 +1042,14 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 449462985,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -1088,6 +1126,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1306,6 +1345,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1639,6 +1679,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1972,6 +2013,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2190,6 +2232,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2432,6 +2475,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2735,6 +2779,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3106,6 +3151,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3583,6 +3629,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3908,6 +3955,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4233,6 +4281,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5313,6 +5362,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -5347,6 +5397,42 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"category": "hyperlink",
|
"category": "hyperlink",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": <svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</svg>,
|
||||||
|
"label": "labels.copyElementLink",
|
||||||
|
"name": "copyElementLink",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -5465,6 +5551,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -6486,6 +6573,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -6520,6 +6608,42 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"category": "hyperlink",
|
"category": "hyperlink",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": <svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</svg>,
|
||||||
|
"label": "labels.copyElementLink",
|
||||||
|
"name": "copyElementLink",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -6638,6 +6762,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7575,6 +7700,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8381,6 +8507,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -8415,6 +8542,42 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"category": "hyperlink",
|
"category": "hyperlink",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": <svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</svg>,
|
||||||
|
"label": "labels.copyElementLink",
|
||||||
|
"name": "copyElementLink",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -8533,6 +8696,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9321,6 +9485,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -9355,6 +9520,42 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"category": "hyperlink",
|
"category": "hyperlink",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": <svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</svg>,
|
||||||
|
"label": "labels.copyElementLink",
|
||||||
|
"name": "copyElementLink",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
@@ -9473,6 +9674,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9590,14 +9792,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 1278240551,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -9624,14 +9826,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 449462985,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -53,6 +53,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -196,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 99,
|
"height": 125,
|
||||||
"id": "id166",
|
"id": "id166",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -210,8 +211,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.20800",
|
125,
|
||||||
99,
|
125,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -225,9 +226,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 40,
|
"version": 47,
|
||||||
"width": "98.20800",
|
"width": 125,
|
||||||
"x": 1,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -297,7 +298,7 @@ History {
|
|||||||
"focus": "0.00990",
|
"focus": "0.00990",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.98017",
|
"height": "0.98000",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -305,7 +306,7 @@ History {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
98,
|
||||||
"-0.98017",
|
"-0.98000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -319,10 +320,10 @@ History {
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id165",
|
"elementId": "id165",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": "-0.02000",
|
"focus": "-0.02040",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.00169",
|
"height": "0.02000",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -330,13 +331,13 @@ History {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
98,
|
||||||
"0.00169",
|
"0.02000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id164",
|
"elementId": "id164",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": "0.02000",
|
"focus": "0.01959",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -392,18 +393,20 @@ History {
|
|||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": 99,
|
"height": 125,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.20800",
|
125,
|
||||||
99,
|
125,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
|
"width": 125,
|
||||||
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -413,7 +416,7 @@ History {
|
|||||||
"focus": "0.00990",
|
"focus": "0.00990",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.98161",
|
"height": "0.98000",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -421,7 +424,7 @@ History {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
98,
|
||||||
"-0.98161",
|
"-0.98000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -430,7 +433,9 @@ History {
|
|||||||
"focus": "0.02970",
|
"focus": "0.02970",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"y": "0.99245",
|
"width": 98,
|
||||||
|
"x": 1,
|
||||||
|
"y": "0.99000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id169" => Delta {
|
"id169" => Delta {
|
||||||
@@ -657,6 +662,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -821,9 +827,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 30,
|
"version": 37,
|
||||||
"width": 0,
|
"width": 100,
|
||||||
"x": 200,
|
"x": 150,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -860,6 +866,8 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
"width": 0,
|
||||||
|
"x": 149,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"points": [
|
"points": [
|
||||||
@@ -868,10 +876,12 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
"width": "98.00000",
|
||||||
|
"x": "1.00000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -928,6 +938,8 @@ History {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
|
"width": 100,
|
||||||
|
"x": 150,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
@@ -952,6 +964,8 @@ History {
|
|||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
|
"width": 0,
|
||||||
|
"x": 149,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1165,6 +1179,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1535,6 +1550,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1906,6 +1922,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2175,6 +2192,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2357,9 +2375,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": 498,
|
"width": 498,
|
||||||
"x": 1,
|
"x": "1.00000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -2498,7 +2516,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -2517,8 +2535,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "98.00000",
|
||||||
"x": 0,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -2617,6 +2635,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2918,6 +2937,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3204,6 +3224,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3500,6 +3521,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3788,6 +3810,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4025,6 +4048,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4286,6 +4310,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4561,6 +4586,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4794,6 +4820,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5027,6 +5054,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5258,6 +5286,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5489,6 +5518,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5750,6 +5780,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -6083,6 +6114,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -6510,6 +6542,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -6890,6 +6923,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7211,6 +7245,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7511,6 +7546,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7742,6 +7778,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8099,6 +8136,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8456,6 +8494,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8862,6 +8901,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9151,6 +9191,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9418,6 +9459,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9684,6 +9726,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9917,6 +9960,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -10220,6 +10264,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -10562,6 +10607,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -10799,6 +10845,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11254,6 +11301,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11510,6 +11558,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11751,6 +11800,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11994,6 +12044,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -12397,6 +12448,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -12646,6 +12698,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -12889,6 +12942,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -13132,6 +13186,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -13381,6 +13436,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -13715,6 +13771,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -13889,6 +13946,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14179,6 +14237,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14448,6 +14507,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14725,6 +14785,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14888,6 +14949,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -15117,9 +15179,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "98.00000",
|
"width": "98.00000",
|
||||||
"x": 1,
|
"x": "1.00000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -15158,7 +15220,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -15171,7 +15233,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -15467,7 +15529,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -15486,8 +15548,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "98.00000",
|
||||||
"x": 0,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -15586,6 +15648,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -15815,9 +15878,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "98.00000",
|
"width": "98.00000",
|
||||||
"x": 1,
|
"x": "1.00000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -16089,7 +16152,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -16108,8 +16171,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "98.00000",
|
||||||
"x": 0,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -16208,6 +16271,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -16437,9 +16501,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "98.00000",
|
"width": "98.00000",
|
||||||
"x": 1,
|
"x": "1.00000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -16711,7 +16775,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -16730,8 +16794,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "98.00000",
|
||||||
"x": 0,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -16830,6 +16894,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -17057,9 +17122,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 12,
|
||||||
"width": "98.00000",
|
"width": "98.00000",
|
||||||
"x": 1,
|
"x": "1.00000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -17115,7 +17180,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -17133,7 +17198,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -17402,7 +17467,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -17421,8 +17486,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "98.00000",
|
||||||
"x": 0,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -17544,6 +17609,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -17774,9 +17840,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 13,
|
||||||
"width": "98.00000",
|
"width": "98.00000",
|
||||||
"x": 1,
|
"x": "1.00000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -17847,7 +17913,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -17866,7 +17932,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -18135,7 +18201,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"98.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -18154,8 +18220,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "98.00000",
|
||||||
"x": 0,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@@ -18296,6 +18362,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -18772,6 +18839,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -19296,6 +19364,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -19754,6 +19823,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 745419401,
|
"versionNonce": 2066753033,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 15,
|
||||||
"versionNonce": 1996028265,
|
"versionNonce": 271613161,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": 50,
|
"y": 50,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -467,6 +468,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -872,6 +874,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": false,
|
"isBindingEnabled": false,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1416,6 +1419,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1619,6 +1623,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -1993,6 +1998,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2232,6 +2238,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2411,6 +2418,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2730,6 +2738,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -2975,6 +2984,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3217,6 +3227,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3446,6 +3457,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -3701,6 +3713,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4011,6 +4024,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4424,6 +4438,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4706,6 +4721,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -4958,6 +4974,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5167,6 +5184,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5365,6 +5383,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -5746,6 +5765,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -6035,6 +6055,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -6842,6 +6863,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7171,6 +7193,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7446,6 +7469,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7679,6 +7703,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -7915,6 +7940,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8094,6 +8120,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8273,6 +8300,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8452,6 +8480,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8674,6 +8703,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -8895,6 +8925,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9088,6 +9119,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9310,6 +9342,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9489,6 +9522,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9710,6 +9744,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -9889,6 +9924,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -10082,6 +10118,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -10261,6 +10298,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -10774,6 +10812,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11050,6 +11089,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11175,6 +11215,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11373,6 +11414,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -11683,6 +11725,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -12094,6 +12137,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -12706,6 +12750,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -12834,6 +12879,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -13417,6 +13463,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -13754,6 +13801,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14018,6 +14066,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14143,6 +14192,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14521,6 +14571,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
@@ -14646,6 +14697,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { copiedStyles } from "../actions/actionStyles";
|
|||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { setDateTimeForTests } from "../utils";
|
import { setDateTimeForTests } from "../utils";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import type { ActionName } from "../actions/types";
|
||||||
|
|
||||||
const checkpoint = (name: string) => {
|
const checkpoint = (name: string) => {
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||||
@@ -115,7 +116,7 @@ describe("contextMenu element", () => {
|
|||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
contextMenu?.querySelectorAll(".context-menu li");
|
contextMenu?.querySelectorAll(".context-menu li");
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
const expectedContextMenuItems: ActionName[] = [
|
||||||
"cut",
|
"cut",
|
||||||
"copy",
|
"copy",
|
||||||
"paste",
|
"paste",
|
||||||
@@ -131,14 +132,15 @@ describe("contextMenu element", () => {
|
|||||||
"bringToFront",
|
"bringToFront",
|
||||||
"duplicateSelection",
|
"duplicateSelection",
|
||||||
"hyperlink",
|
"hyperlink",
|
||||||
|
"copyElementLink",
|
||||||
"toggleElementLock",
|
"toggleElementLock",
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
expect(contextMenu).not.toBeNull();
|
||||||
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
expect(contextMenuOptions?.length).toBe(expectedContextMenuItems.length);
|
||||||
expectedShortcutNames.forEach((shortcutName) => {
|
expectedContextMenuItems.forEach((item) => {
|
||||||
expect(
|
expect(
|
||||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
contextMenu?.querySelector(`li[data-testid="${item}"]`),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -263,13 +265,14 @@ describe("contextMenu element", () => {
|
|||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
contextMenu?.querySelectorAll(".context-menu li");
|
contextMenu?.querySelectorAll(".context-menu li");
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
const expectedContextMenuItems: ActionName[] = [
|
||||||
"cut",
|
"cut",
|
||||||
"copy",
|
"copy",
|
||||||
"paste",
|
"paste",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
|
"copyElementLink",
|
||||||
"ungroup",
|
"ungroup",
|
||||||
"addToLibrary",
|
"addToLibrary",
|
||||||
"flipHorizontal",
|
"flipHorizontal",
|
||||||
@@ -283,10 +286,10 @@ describe("contextMenu element", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
expect(contextMenu).not.toBeNull();
|
||||||
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
expect(contextMenuOptions?.length).toBe(expectedContextMenuItems.length);
|
||||||
expectedShortcutNames.forEach((shortcutName) => {
|
expectedContextMenuItems.forEach((item) => {
|
||||||
expect(
|
expect(
|
||||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
contextMenu?.querySelector(`li[data-testid="${item}"]`),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,14 +186,14 @@ describe("Crop an image", () => {
|
|||||||
// 50 x 50 square
|
// 50 x 50 square
|
||||||
UI.crop(image, "nw", naturalWidth, naturalHeight, [150, 50]);
|
UI.crop(image, "nw", naturalWidth, naturalHeight, [150, 50]);
|
||||||
UI.crop(image, "n", naturalWidth, naturalHeight, [0, -100], true);
|
UI.crop(image, "n", naturalWidth, naturalHeight, [0, -100], true);
|
||||||
expect(image.width).toEqual(image.height);
|
expect(image.width).toBeCloseTo(image.height);
|
||||||
// image is at the corner, not space to its right to expand, should not be able to resize
|
// image is at the corner, not space to its right to expand, should not be able to resize
|
||||||
expect(image.height).toBeCloseTo(50);
|
expect(image.height).toBeCloseTo(50);
|
||||||
|
|
||||||
UI.crop(image, "nw", naturalWidth, naturalHeight, [-150, -100], true);
|
UI.crop(image, "nw", naturalWidth, naturalHeight, [-150, -100], true);
|
||||||
expect(image.width).toEqual(image.height);
|
expect(image.width).toBeCloseTo(image.height);
|
||||||
// max height should be reached
|
// max height should be reached
|
||||||
expect(image.height).toEqual(initialHeight);
|
expect(image.height).toBeCloseTo(initialHeight);
|
||||||
expect(image.width).toBe(initialHeight);
|
expect(image.width).toBe(initialHeight);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ import React from "react";
|
|||||||
import { render, waitFor } from "./test-utils";
|
import { render, waitFor } from "./test-utils";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import {
|
import { encodePngMetadata } from "../data/image";
|
||||||
encodePngMetadata,
|
|
||||||
encodeSvgMetadata,
|
|
||||||
decodeSvgMetadata,
|
|
||||||
} from "../data/image";
|
|
||||||
import { serializeAsJSON } from "../data/json";
|
import { serializeAsJSON } from "../data/json";
|
||||||
import { exportToSvg } from "../scene/export";
|
import {
|
||||||
|
decodeSvgBase64Payload,
|
||||||
|
encodeSvgBase64Payload,
|
||||||
|
exportToSvg,
|
||||||
|
} from "../scene/export";
|
||||||
import type { FileId } from "../element/types";
|
import type { FileId } from "../element/types";
|
||||||
import { getDataURL } from "../data/blob";
|
import { getDataURL } from "../data/blob";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import { SVG_NS } from "../constants";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -62,15 +63,32 @@ describe("export", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("test encoding/decoding scene for SVG export", async () => {
|
it("test encoding/decoding scene for SVG export", async () => {
|
||||||
const encoded = encodeSvgMetadata({
|
const metadataElement = document.createElementNS(SVG_NS, "metadata");
|
||||||
text: serializeAsJSON(testElements, h.state, {}, "local"),
|
|
||||||
|
encodeSvgBase64Payload({
|
||||||
|
metadataElement,
|
||||||
|
payload: serializeAsJSON(testElements, h.state, {}, "local"),
|
||||||
});
|
});
|
||||||
const decoded = JSON.parse(decodeSvgMetadata({ svg: encoded }));
|
|
||||||
|
const decoded = JSON.parse(
|
||||||
|
decodeSvgBase64Payload({ svg: metadataElement.innerHTML }),
|
||||||
|
);
|
||||||
expect(decoded.elements).toEqual([
|
expect(decoded.elements).toEqual([
|
||||||
expect.objectContaining({ type: "text", text: "😀" }),
|
expect.objectContaining({ type: "text", text: "😀" }),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("export svg-embedded scene", async () => {
|
||||||
|
const svg = await exportToSvg(
|
||||||
|
testElements,
|
||||||
|
{ ...getDefaultAppState(), exportEmbedScene: true },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const svgText = svg.outerHTML;
|
||||||
|
|
||||||
|
expect(svgText).toMatchSnapshot(`svg-embdedded scene export output`);
|
||||||
|
});
|
||||||
|
|
||||||
it("import embedded png (legacy v1)", async () => {
|
it("import embedded png (legacy v1)", async () => {
|
||||||
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
|
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -220,7 +220,6 @@ export class API {
|
|||||||
| "width"
|
| "width"
|
||||||
| "height"
|
| "height"
|
||||||
| "type"
|
| "type"
|
||||||
| "seed"
|
|
||||||
| "version"
|
| "version"
|
||||||
| "versionNonce"
|
| "versionNonce"
|
||||||
| "isDeleted"
|
| "isDeleted"
|
||||||
@@ -228,6 +227,7 @@ export class API {
|
|||||||
| "link"
|
| "link"
|
||||||
| "updated"
|
| "updated"
|
||||||
> = {
|
> = {
|
||||||
|
seed: 1,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
frameId: rest.frameId ?? null,
|
frameId: rest.frameId ?? null,
|
||||||
|
|||||||
@@ -4785,21 +4785,17 @@ describe("history", () => {
|
|||||||
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[100, 0],
|
|
||||||
],
|
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
fixedPoint: null,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: 1,
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
fixedPoint: null,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: 0,
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: 1,
|
||||||
}),
|
}),
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
|||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="blog.excalidaw.com"
|
href="https://plus.excalidraw.com/blog"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { LinearElementEditor } from "../element/linearElementEditor";
|
|||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { pointFrom } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
import { resizeSingleElement } from "../element/resizeElements";
|
||||||
|
import { getSizeFromPoints } from "../points";
|
||||||
|
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -235,7 +237,7 @@ describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("resizes", async () => {
|
it("resizes", async () => {
|
||||||
const element = UI.createElement(type, { points: points[type] });
|
const element = UI.createElement("freedraw", { points: points.freedraw });
|
||||||
const bounds = getBoundsFromPoints(element);
|
const bounds = getBoundsFromPoints(element);
|
||||||
|
|
||||||
UI.resize(element, "ne", [30, -60]);
|
UI.resize(element, "ne", [30, -60]);
|
||||||
@@ -249,7 +251,7 @@ describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("flips while resizing", async () => {
|
it("flips while resizing", async () => {
|
||||||
const element = UI.createElement(type, { points: points[type] });
|
const element = UI.createElement("freedraw", { points: points.freedraw });
|
||||||
const bounds = getBoundsFromPoints(element);
|
const bounds = getBoundsFromPoints(element);
|
||||||
|
|
||||||
UI.resize(element, "sw", [140, -80]);
|
UI.resize(element, "sw", [140, -80]);
|
||||||
@@ -263,7 +265,7 @@ describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with locked aspect ratio", async () => {
|
it("resizes with locked aspect ratio", async () => {
|
||||||
const element = UI.createElement(type, { points: points[type] });
|
const element = UI.createElement("freedraw", { points: points.freedraw });
|
||||||
const bounds = getBoundsFromPoints(element);
|
const bounds = getBoundsFromPoints(element);
|
||||||
|
|
||||||
UI.resize(element, "ne", [30, -60], { shift: true });
|
UI.resize(element, "ne", [30, -60], { shift: true });
|
||||||
@@ -280,7 +282,7 @@ describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resizes from center", async () => {
|
it("resizes from center", async () => {
|
||||||
const element = UI.createElement(type, { points: points[type] });
|
const element = UI.createElement("freedraw", { points: points.freedraw });
|
||||||
const bounds = getBoundsFromPoints(element);
|
const bounds = getBoundsFromPoints(element);
|
||||||
|
|
||||||
UI.resize(element, "nw", [-20, -30], { alt: true });
|
UI.resize(element, "nw", [-20, -30], { alt: true });
|
||||||
@@ -294,6 +296,147 @@ describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("line element", () => {
|
||||||
|
const points: LocalPoint[] = [
|
||||||
|
pointFrom(0, 0),
|
||||||
|
pointFrom(60, -20),
|
||||||
|
pointFrom(20, 40),
|
||||||
|
pointFrom(-40, 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
it("resizes", async () => {
|
||||||
|
UI.createElement("line", { points });
|
||||||
|
|
||||||
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
const {
|
||||||
|
x: prevX,
|
||||||
|
y: prevY,
|
||||||
|
width: prevWidth,
|
||||||
|
height: prevHeight,
|
||||||
|
} = element;
|
||||||
|
|
||||||
|
const nextWidth = prevWidth + 30;
|
||||||
|
const nextHeight = prevHeight + 30;
|
||||||
|
|
||||||
|
resizeSingleElement(
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
element,
|
||||||
|
element,
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
"ne",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(element.x).not.toBe(prevX);
|
||||||
|
expect(element.y).not.toBe(prevY);
|
||||||
|
|
||||||
|
expect(element.width).toBe(nextWidth);
|
||||||
|
expect(element.height).toBe(nextHeight);
|
||||||
|
|
||||||
|
expect(element.points[0]).toEqual([0, 0]);
|
||||||
|
|
||||||
|
const { width, height } = getSizeFromPoints(element.points);
|
||||||
|
expect(width).toBe(element.width);
|
||||||
|
expect(height).toBe(element.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flips while resizing", async () => {
|
||||||
|
UI.createElement("line", { points });
|
||||||
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width: prevWidth,
|
||||||
|
height: prevHeight,
|
||||||
|
points: prevPoints,
|
||||||
|
} = element;
|
||||||
|
|
||||||
|
const nextWidth = prevWidth * -1;
|
||||||
|
const nextHeight = prevHeight * -1;
|
||||||
|
|
||||||
|
resizeSingleElement(
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
element,
|
||||||
|
element,
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
"se",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(element.width).toBe(prevWidth);
|
||||||
|
expect(element.height).toBe(prevHeight);
|
||||||
|
|
||||||
|
element.points.forEach((point, idx) => {
|
||||||
|
expect(point[0]).toBeCloseTo(prevPoints[idx][0] * -1);
|
||||||
|
expect(point[1]).toBeCloseTo(prevPoints[idx][1] * -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resizes with locked aspect ratio", async () => {
|
||||||
|
UI.createElement("line", { points });
|
||||||
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
const { width: prevWidth, height: prevHeight } = element;
|
||||||
|
|
||||||
|
UI.resize(element, "ne", [30, -60], { shift: true });
|
||||||
|
|
||||||
|
const scaleHeight = element.width / prevWidth;
|
||||||
|
const scaleWidth = element.height / prevHeight;
|
||||||
|
|
||||||
|
expect(scaleHeight).toBeCloseTo(scaleWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resizes from center", async () => {
|
||||||
|
UI.createElement("line", {
|
||||||
|
points: [
|
||||||
|
pointFrom(0, 0),
|
||||||
|
pointFrom(338.05644048727373, -180.4761618151104),
|
||||||
|
pointFrom(338.05644048727373, 180.4761618151104),
|
||||||
|
pointFrom(-338.05644048727373, 180.4761618151104),
|
||||||
|
pointFrom(-338.05644048727373, -180.4761618151104),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
const {
|
||||||
|
x: prevX,
|
||||||
|
y: prevY,
|
||||||
|
width: prevWidth,
|
||||||
|
height: prevHeight,
|
||||||
|
} = element;
|
||||||
|
|
||||||
|
const prevSmallestX = Math.min(...element.points.map((p) => p[0]));
|
||||||
|
const prevBiggestX = Math.max(...element.points.map((p) => p[0]));
|
||||||
|
|
||||||
|
resizeSingleElement(
|
||||||
|
prevWidth + 20,
|
||||||
|
prevHeight,
|
||||||
|
element,
|
||||||
|
element,
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
"e",
|
||||||
|
{
|
||||||
|
shouldResizeFromCenter: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(element.width).toBeCloseTo(prevWidth + 20);
|
||||||
|
expect(element.height).toBeCloseTo(prevHeight);
|
||||||
|
|
||||||
|
expect(element.x).toBeCloseTo(prevX);
|
||||||
|
expect(element.y).toBeCloseTo(prevY);
|
||||||
|
|
||||||
|
const smallestX = Math.min(...element.points.map((p) => p[0]));
|
||||||
|
const biggestX = Math.max(...element.points.map((p) => p[0]));
|
||||||
|
|
||||||
|
expect(prevSmallestX - smallestX).toBeCloseTo(10);
|
||||||
|
expect(biggestX - prevBiggestX).toBeCloseTo(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("arrow element", () => {
|
describe("arrow element", () => {
|
||||||
it("resizes with a label", async () => {
|
it("resizes with a label", async () => {
|
||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,19 @@
|
|||||||
import * as utils from "../utils";
|
import { isTransparent, sanitizeHTMLAttribute } from "../utils";
|
||||||
|
|
||||||
describe("Test isTransparent", () => {
|
describe("Test isTransparent", () => {
|
||||||
it("should return true when color is rgb transparent", () => {
|
it("should return true when color is rgb transparent", () => {
|
||||||
expect(utils.isTransparent("#ff00")).toEqual(true);
|
expect(isTransparent("#ff00")).toEqual(true);
|
||||||
expect(utils.isTransparent("#fff00000")).toEqual(true);
|
expect(isTransparent("#fff00000")).toEqual(true);
|
||||||
expect(utils.isTransparent("transparent")).toEqual(true);
|
expect(isTransparent("transparent")).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false when color is not transparent", () => {
|
it("should return false when color is not transparent", () => {
|
||||||
expect(utils.isTransparent("#ced4da")).toEqual(false);
|
expect(isTransparent("#ced4da")).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitizeHTMLAttribute()", () => {
|
||||||
|
it("should escape HTML attribute special characters & not double escape", () => {
|
||||||
|
expect(sanitizeHTMLAttribute(`&"'><`)).toBe("&"'><");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ type _CommonCanvasAppState = {
|
|||||||
width: AppState["width"];
|
width: AppState["width"];
|
||||||
height: AppState["height"];
|
height: AppState["height"];
|
||||||
viewModeEnabled: AppState["viewModeEnabled"];
|
viewModeEnabled: AppState["viewModeEnabled"];
|
||||||
|
openDialog: AppState["openDialog"];
|
||||||
editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible
|
editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible
|
||||||
selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible
|
selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible
|
||||||
frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible
|
frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible
|
||||||
@@ -181,6 +182,7 @@ export type StaticCanvasAppState = Readonly<
|
|||||||
gridStep: AppState["gridStep"];
|
gridStep: AppState["gridStep"];
|
||||||
frameRendering: AppState["frameRendering"];
|
frameRendering: AppState["frameRendering"];
|
||||||
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
||||||
|
hoveredElementIds: AppState["hoveredElementIds"];
|
||||||
// Cropping
|
// Cropping
|
||||||
croppingElementId: AppState["croppingElementId"];
|
croppingElementId: AppState["croppingElementId"];
|
||||||
}
|
}
|
||||||
@@ -332,7 +334,9 @@ export interface AppState {
|
|||||||
| null
|
| null
|
||||||
| { name: "imageExport" | "help" | "jsonExport" }
|
| { name: "imageExport" | "help" | "jsonExport" }
|
||||||
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
||||||
| { name: "commandPalette" };
|
| { name: "commandPalette" }
|
||||||
|
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* Reflects user preference for whether the default sidebar should be docked.
|
||||||
*
|
*
|
||||||
@@ -344,6 +348,7 @@ export interface AppState {
|
|||||||
|
|
||||||
lastPointerDownWith: PointerType;
|
lastPointerDownWith: PointerType;
|
||||||
selectedElementIds: Readonly<{ [id: string]: true }>;
|
selectedElementIds: Readonly<{ [id: string]: true }>;
|
||||||
|
hoveredElementIds: Readonly<{ [id: string]: true }>;
|
||||||
previousSelectedElementIds: { [id: string]: true };
|
previousSelectedElementIds: { [id: string]: true };
|
||||||
selectedElementsAreBeingDragged: boolean;
|
selectedElementsAreBeingDragged: boolean;
|
||||||
shouldCacheIgnoreZoom: boolean;
|
shouldCacheIgnoreZoom: boolean;
|
||||||
@@ -530,6 +535,7 @@ export interface ExcalidrawProps {
|
|||||||
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
generateIdForFile?: (file: File) => string | Promise<string>;
|
generateIdForFile?: (file: File) => string | Promise<string>;
|
||||||
|
generateLinkForSelection?: (id: string, type: "element" | "group") => string;
|
||||||
onLinkOpen?: (
|
onLinkOpen?: (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
event: CustomEvent<{
|
event: CustomEvent<{
|
||||||
|
|||||||
@@ -1225,3 +1225,16 @@ export class PromisePool<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sanitizeHTMLAttribute = (html: string) => {
|
||||||
|
return (
|
||||||
|
html
|
||||||
|
// note, if we're not doing stupid things, escaping " is enough,
|
||||||
|
// but we might end up doing stupid things
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"gridModeEnabled": false,
|
"gridModeEnabled": false,
|
||||||
"gridSize": 20,
|
"gridSize": 20,
|
||||||
"gridStep": 5,
|
"gridStep": 5,
|
||||||
|
"hoveredElementIds": {},
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
"isCropping": false,
|
"isCropping": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { decodePngMetadata, decodeSvgMetadata } from "../excalidraw/data/image";
|
|
||||||
import type { ImportedDataState } from "../excalidraw/data/types";
|
import type { ImportedDataState } from "../excalidraw/data/types";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { API } from "../excalidraw/tests/helpers/api";
|
import { API } from "../excalidraw/tests/helpers/api";
|
||||||
|
import { decodeSvgBase64Payload } from "../excalidraw/scene/export";
|
||||||
|
import { decodePngMetadata } from "../excalidraw/data/image";
|
||||||
|
|
||||||
// NOTE this test file is using the actual API, unmocked. Hence splitting it
|
// NOTE this test file is using the actual API, unmocked. Hence splitting it
|
||||||
// from the other test file, because I couldn't figure out how to test
|
// from the other test file, because I couldn't figure out how to test
|
||||||
@@ -27,7 +28,7 @@ describe("embedding scene data", () => {
|
|||||||
|
|
||||||
const svg = svgNode.outerHTML;
|
const svg = svgNode.outerHTML;
|
||||||
|
|
||||||
const parsedString = decodeSvgMetadata({ svg });
|
const parsedString = decodeSvgBase64Payload({ svg });
|
||||||
const importedData: ImportedDataState = JSON.parse(parsedString);
|
const importedData: ImportedDataState = JSON.parse(parsedString);
|
||||||
|
|
||||||
expect(sourceElements.map((x) => x.id)).toEqual(
|
expect(sourceElements.map((x) => x.id)).toEqual(
|
||||||
|
|||||||
@@ -158,5 +158,8 @@ const createESMRawBuild = async () => {
|
|||||||
await buildProd(rawConfigChunks);
|
await buildProd(rawConfigChunks);
|
||||||
};
|
};
|
||||||
|
|
||||||
createESMRawBuild();
|
// otherwise throws "ERROR: Could not resolve "./subset-worker.chunk"
|
||||||
createESMBrowserBuild();
|
(async () => {
|
||||||
|
await createESMRawBuild();
|
||||||
|
await createESMBrowserBuild();
|
||||||
|
})();
|
||||||
|
|||||||
@@ -3887,11 +3887,6 @@ ansi-regex@^5.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||||
|
|
||||||
ansi-regex@^6.0.1:
|
|
||||||
version "6.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
|
|
||||||
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
|
|
||||||
|
|
||||||
ansi-styles@^2.2.1:
|
ansi-styles@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||||
@@ -9743,27 +9738,13 @@ stringify-object@^3.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^5.0.1"
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
strip-ansi@^3.0.0:
|
strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
|
||||||
integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^2.0.0"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^5.0.1"
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
strip-ansi@^7.0.1:
|
|
||||||
version "7.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
|
||||||
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^6.0.1"
|
|
||||||
|
|
||||||
strip-bom@^3.0.0:
|
strip-bom@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||||
|
|||||||
Reference in New Issue
Block a user