Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8adfa1a9 | ||
|
|
a977dd1bf5 | ||
|
|
3fe1883f3f | ||
|
|
a80cb5896a | ||
|
|
6dfa18414a | ||
|
|
8ca4cf3260 | ||
|
|
f3f0ab7c83 | ||
|
|
44a1c8d857 | ||
|
|
e0a22edfbd | ||
|
|
c07f5a0c80 | ||
|
|
508f16dc04 | ||
|
|
c1b310c56b | ||
|
|
d4900e8f19 | ||
|
|
caf2db934c | ||
|
|
60e3801691 | ||
|
|
80f3b75d42 | ||
|
|
dc812bee19 | ||
|
|
01e83cc9a5 | ||
|
|
813f9b702e | ||
|
|
fd39712ba6 | ||
|
|
b46ca0192b | ||
|
|
72b7c937b1 | ||
|
|
d107215564 | ||
|
|
6959a363f0 | ||
|
|
5a11c70714 | ||
|
|
6ff56c36e3 | ||
|
|
51ea184938 | ||
|
|
21fff26d31 |
@@ -8,3 +8,4 @@ public/workbox
|
|||||||
packages/excalidraw/types
|
packages/excalidraw/types
|
||||||
examples/**/public
|
examples/**/public
|
||||||
dev-dist
|
dev-dist
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ exportToCanvas({<br/>
|
|||||||
getDimensions,<br/>
|
getDimensions,<br/>
|
||||||
files,<br/>
|
files,<br/>
|
||||||
exportPadding?: number;<br/>
|
exportPadding?: number;<br/>
|
||||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a>
|
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/utils/export.ts#L24">ExportOpts</a>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
|||||||
+41
-22
@@ -1547,7 +1547,7 @@
|
|||||||
"@docusaurus/theme-search-algolia" "2.2.0"
|
"@docusaurus/theme-search-algolia" "2.2.0"
|
||||||
"@docusaurus/types" "2.2.0"
|
"@docusaurus/types" "2.2.0"
|
||||||
|
|
||||||
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
"@docusaurus/react-loadable@5.5.2":
|
||||||
version "5.5.2"
|
version "5.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||||
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||||
@@ -2789,7 +2789,14 @@ brace-expansion@^1.1.7:
|
|||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
braces@^3.0.2, braces@~3.0.2:
|
braces@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||||
|
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||||
|
dependencies:
|
||||||
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
|
braces@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||||
@@ -4011,6 +4018,13 @@ fill-range@^7.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
|
fill-range@^7.1.1:
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
|
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||||
|
dependencies:
|
||||||
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
finalhandler@1.2.0:
|
finalhandler@1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
|
||||||
@@ -5207,11 +5221,11 @@ methods@~1.1.2:
|
|||||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||||
|
|
||||||
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
||||||
version "4.0.5"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||||
dependencies:
|
dependencies:
|
||||||
braces "^3.0.2"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
||||||
@@ -6190,14 +6204,13 @@ react-dev-utils@^12.0.1:
|
|||||||
strip-ansi "^6.0.1"
|
strip-ansi "^6.0.1"
|
||||||
text-table "^0.2.0"
|
text-table "^0.2.0"
|
||||||
|
|
||||||
react-dom@^17.0.2:
|
react-dom@18.2.0:
|
||||||
version "17.0.2"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
scheduler "^0.23.0"
|
||||||
scheduler "^0.20.2"
|
|
||||||
|
|
||||||
react-error-overlay@^6.0.11:
|
react-error-overlay@^6.0.11:
|
||||||
version "6.0.11"
|
version "6.0.11"
|
||||||
@@ -6260,6 +6273,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.3"
|
"@babel/runtime" "^7.10.3"
|
||||||
|
|
||||||
|
"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
||||||
|
version "5.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||||
|
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
react-router-config@^5.1.1:
|
react-router-config@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
||||||
@@ -6310,13 +6331,12 @@ react-textarea-autosize@^8.3.2:
|
|||||||
use-composed-ref "^1.3.0"
|
use-composed-ref "^1.3.0"
|
||||||
use-latest "^1.2.1"
|
use-latest "^1.2.1"
|
||||||
|
|
||||||
react@^17.0.2:
|
react@18.2.0:
|
||||||
version "17.0.2"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
|
||||||
|
|
||||||
readable-stream@^2.0.1:
|
readable-stream@^2.0.1:
|
||||||
version "2.3.7"
|
version "2.3.7"
|
||||||
@@ -6664,13 +6684,12 @@ sax@^1.2.4:
|
|||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
|
||||||
scheduler@^0.20.2:
|
scheduler@^0.23.0:
|
||||||
version "0.20.2"
|
version "0.23.2"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
|
||||||
|
|
||||||
schema-utils@2.7.0:
|
schema-utils@2.7.0:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
|
|||||||
+2
-2
@@ -40,7 +40,7 @@ import type {
|
|||||||
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
||||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
|
||||||
|
|
||||||
import "./App.scss";
|
import "./ExampleApp.scss";
|
||||||
|
|
||||||
type Comment = {
|
type Comment = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -73,7 +73,7 @@ export interface AppProps {
|
|||||||
excalidrawLib: typeof TExcalidraw;
|
excalidrawLib: typeof TExcalidraw;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App({
|
export default function ExampleApp({
|
||||||
appTitle,
|
appTitle,
|
||||||
useCustom,
|
useCustom,
|
||||||
customArgs,
|
customArgs,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import App from "../../components/App";
|
import App from "../../components/ExampleApp";
|
||||||
|
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import App from "../components/App";
|
import App from "../components/ExampleApp";
|
||||||
import React, { StrictMode } from "react";
|
import React, { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
|||||||
@@ -649,7 +649,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
// Render the debug scene if the debug canvas is available
|
// Render the debug scene if the debug canvas is available
|
||||||
if (debugCanvasRef.current && excalidrawAPI) {
|
if (debugCanvasRef.current && excalidrawAPI) {
|
||||||
debugRenderer(debugCanvasRef.current, appState, window.devicePixelRatio);
|
debugRenderer(
|
||||||
|
debugCanvasRef.current,
|
||||||
|
appState,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
() => forceRefresh((prev) => !prev),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
||||||
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
|
|||||||
@@ -68,12 +68,17 @@ const _debugRenderer = (
|
|||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
scale: number,
|
scale: number,
|
||||||
|
refresh: () => void,
|
||||||
) => {
|
) => {
|
||||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appState.height !== canvas.height || appState.width !== canvas.width) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
const context = bootstrapCanvas({
|
const context = bootstrapCanvas({
|
||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
@@ -138,8 +143,13 @@ export const saveDebugState = (debug: { enabled: boolean }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const debugRenderer = throttleRAF(
|
export const debugRenderer = throttleRAF(
|
||||||
(canvas: HTMLCanvasElement, appState: AppState, scale: number) => {
|
(
|
||||||
_debugRenderer(canvas, appState, scale);
|
canvas: HTMLCanvasElement,
|
||||||
|
appState: AppState,
|
||||||
|
scale: number,
|
||||||
|
refresh: () => void,
|
||||||
|
) => {
|
||||||
|
_debugRenderer(canvas, appState, scale, refresh);
|
||||||
},
|
},
|
||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import {
|
|||||||
get,
|
get,
|
||||||
} from "idb-keyval";
|
} from "idb-keyval";
|
||||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||||
|
import {
|
||||||
|
CANVAS_SEARCH_TAB,
|
||||||
|
DEFAULT_SIDEBAR,
|
||||||
|
} from "../../packages/excalidraw/constants";
|
||||||
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
||||||
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||||
@@ -66,13 +70,22 @@ const saveDataStateToLocalStorage = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
const _appState = clearAppStateForLocalStorage(appState);
|
||||||
|
|
||||||
|
if (
|
||||||
|
_appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
|
||||||
|
_appState.openSidebar.tab === CANVAS_SEARCH_TAB
|
||||||
|
) {
|
||||||
|
_appState.openSidebar = null;
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
JSON.stringify(clearElementsForLocalStorage(elements)),
|
||||||
);
|
);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
JSON.stringify(_appState),
|
||||||
);
|
);
|
||||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -130,15 +130,6 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Register Assistant as the UI font, before the scene inits -->
|
<!-- Register Assistant as the UI font, before the scene inits -->
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
// don't auto-inline small assets (i.e. fonts hosted on CDN)
|
||||||
|
assetsInlineLimit: 0,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
woff2BrowserPlugin(),
|
woff2BrowserPlugin(),
|
||||||
|
|||||||
+8
-7
@@ -21,8 +21,8 @@
|
|||||||
"@types/react-dom": "18.2.0",
|
"@types/react-dom": "18.2.0",
|
||||||
"@types/socket.io-client": "3.0.0",
|
"@types/socket.io-client": "3.0.0",
|
||||||
"@vitejs/plugin-react": "3.1.0",
|
"@vitejs/plugin-react": "3.1.0",
|
||||||
"@vitest/coverage-v8": "0.33.0",
|
"@vitest/coverage-v8": "2.0.5",
|
||||||
"@vitest/ui": "0.32.2",
|
"@vitest/ui": "2.0.5",
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"vite": "5.0.12",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-checker": "0.6.1",
|
"vite-plugin-checker": "0.7.2",
|
||||||
"vite-plugin-ejs": "1.7.0",
|
"vite-plugin-ejs": "1.7.0",
|
||||||
"vite-plugin-pwa": "0.17.4",
|
"vite-plugin-pwa": "0.17.4",
|
||||||
"vite-plugin-svgr": "2.4.0",
|
"vite-plugin-svgr": "4.2.0",
|
||||||
"vitest": "1.6.0",
|
"vitest": "2.0.5",
|
||||||
"vitest-canvas-mock": "0.3.2"
|
"vitest-canvas-mock": "0.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.0.0 - 20.x.x"
|
"node": "18.0.0 - 20.x.x"
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
"clean-install": "yarn rm:node_modules && yarn install"
|
"clean-install": "yarn rm:node_modules && yarn install"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "18.2.0"
|
"@types/react": "18.2.0",
|
||||||
|
"strip-ansi": "6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Prefer user defined coordinates and dimensions when creating a frame using [`convertToExcalidrawElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements) [#8517](https://github.com/excalidraw/excalidraw/pull/8517)
|
||||||
|
|
||||||
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
|
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
|
||||||
|
|
||||||
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { CODES, KEYS } from "../keys";
|
|||||||
import { getNormalizedZoom } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import type { AppState } from "../types";
|
import type { AppState, Offsets } from "../types";
|
||||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
@@ -38,7 +38,7 @@ import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
|||||||
import type { SceneBounds } from "../element/bounds";
|
import type { SceneBounds } from "../element/bounds";
|
||||||
import { setCursor } from "../cursor";
|
import { setCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { clamp } from "../../math";
|
import { clamp, roundToStep } from "../../math";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@@ -259,89 +259,85 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
const adjustedZoomValue =
|
const adjustedZoomValue =
|
||||||
smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
|
smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
|
||||||
|
|
||||||
const zoomAdjustedToSteps =
|
return Math.min(adjustedZoomValue, 1);
|
||||||
Math.floor(adjustedZoomValue / ZOOM_STEP) * ZOOM_STEP;
|
|
||||||
|
|
||||||
return getNormalizedZoom(Math.min(zoomAdjustedToSteps, 1));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFitBounds = ({
|
export const zoomToFitBounds = ({
|
||||||
bounds,
|
bounds,
|
||||||
appState,
|
appState,
|
||||||
|
canvasOffsets,
|
||||||
fitToViewport = false,
|
fitToViewport = false,
|
||||||
viewportZoomFactor = 1,
|
viewportZoomFactor = 1,
|
||||||
|
minZoom = -Infinity,
|
||||||
|
maxZoom = Infinity,
|
||||||
}: {
|
}: {
|
||||||
bounds: SceneBounds;
|
bounds: SceneBounds;
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
fitToViewport: boolean;
|
fitToViewport: boolean;
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM);
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = bounds;
|
const [x1, y1, x2, y2] = bounds;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
|
||||||
let newZoomValue;
|
const canvasOffsetLeft = canvasOffsets?.left ?? 0;
|
||||||
let scrollX;
|
const canvasOffsetTop = canvasOffsets?.top ?? 0;
|
||||||
let scrollY;
|
const canvasOffsetRight = canvasOffsets?.right ?? 0;
|
||||||
|
const canvasOffsetBottom = canvasOffsets?.bottom ?? 0;
|
||||||
|
|
||||||
|
const effectiveCanvasWidth =
|
||||||
|
appState.width - canvasOffsetLeft - canvasOffsetRight;
|
||||||
|
const effectiveCanvasHeight =
|
||||||
|
appState.height - canvasOffsetTop - canvasOffsetBottom;
|
||||||
|
|
||||||
|
let adjustedZoomValue;
|
||||||
|
|
||||||
if (fitToViewport) {
|
if (fitToViewport) {
|
||||||
const commonBoundsWidth = x2 - x1;
|
const commonBoundsWidth = x2 - x1;
|
||||||
const commonBoundsHeight = y2 - y1;
|
const commonBoundsHeight = y2 - y1;
|
||||||
|
|
||||||
newZoomValue =
|
adjustedZoomValue =
|
||||||
Math.min(
|
Math.min(
|
||||||
appState.width / commonBoundsWidth,
|
effectiveCanvasWidth / commonBoundsWidth,
|
||||||
appState.height / commonBoundsHeight,
|
effectiveCanvasHeight / commonBoundsHeight,
|
||||||
) * clamp(viewportZoomFactor, 0.1, 1);
|
) * viewportZoomFactor;
|
||||||
|
|
||||||
newZoomValue = getNormalizedZoom(newZoomValue);
|
|
||||||
|
|
||||||
let appStateWidth = appState.width;
|
|
||||||
|
|
||||||
if (appState.openSidebar) {
|
|
||||||
const sidebarDOMElem = document.querySelector(
|
|
||||||
".sidebar",
|
|
||||||
) as HTMLElement | null;
|
|
||||||
const sidebarWidth = sidebarDOMElem?.offsetWidth ?? 0;
|
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
|
||||||
|
|
||||||
appStateWidth = !isRTL
|
|
||||||
? appState.width - sidebarWidth
|
|
||||||
: appState.width + sidebarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX;
|
|
||||||
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
|
||||||
} else {
|
} else {
|
||||||
newZoomValue = zoomValueToFitBoundsOnViewport(
|
adjustedZoomValue = zoomValueToFitBoundsOnViewport(
|
||||||
bounds,
|
bounds,
|
||||||
{
|
{
|
||||||
width: appState.width,
|
width: effectiveCanvasWidth,
|
||||||
height: appState.height,
|
height: effectiveCanvasHeight,
|
||||||
},
|
},
|
||||||
viewportZoomFactor,
|
viewportZoomFactor,
|
||||||
);
|
);
|
||||||
|
|
||||||
const centerScroll = centerScrollOn({
|
|
||||||
scenePoint: { x: centerX, y: centerY },
|
|
||||||
viewportDimensions: {
|
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
},
|
|
||||||
zoom: { value: newZoomValue },
|
|
||||||
});
|
|
||||||
|
|
||||||
scrollX = centerScroll.scrollX;
|
|
||||||
scrollY = centerScroll.scrollY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newZoomValue = getNormalizedZoom(
|
||||||
|
clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom),
|
||||||
|
);
|
||||||
|
|
||||||
|
const centerScroll = centerScrollOn({
|
||||||
|
scenePoint: { x: centerX, y: centerY },
|
||||||
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
offsets: canvasOffsets,
|
||||||
|
zoom: { value: newZoomValue },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
scrollX,
|
scrollX: centerScroll.scrollX,
|
||||||
scrollY,
|
scrollY: centerScroll.scrollY,
|
||||||
zoom: { value: newZoomValue },
|
zoom: { value: newZoomValue },
|
||||||
},
|
},
|
||||||
storeAction: StoreAction.NONE,
|
storeAction: StoreAction.NONE,
|
||||||
@@ -349,25 +345,34 @@ export const zoomToFitBounds = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFit = ({
|
export const zoomToFit = ({
|
||||||
|
canvasOffsets,
|
||||||
targetElements,
|
targetElements,
|
||||||
appState,
|
appState,
|
||||||
fitToViewport,
|
fitToViewport,
|
||||||
viewportZoomFactor,
|
viewportZoomFactor,
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
}: {
|
}: {
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
targetElements: readonly ExcalidrawElement[];
|
targetElements: readonly ExcalidrawElement[];
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
fitToViewport: boolean;
|
fitToViewport: boolean;
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
||||||
|
|
||||||
return zoomToFitBounds({
|
return zoomToFitBounds({
|
||||||
|
canvasOffsets,
|
||||||
bounds: commonBounds,
|
bounds: commonBounds,
|
||||||
appState,
|
appState,
|
||||||
fitToViewport,
|
fitToViewport,
|
||||||
viewportZoomFactor,
|
viewportZoomFactor,
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -388,6 +393,7 @@ export const actionZoomToFitSelectionInViewport = register({
|
|||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
fitToViewport: false,
|
fitToViewport: false,
|
||||||
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
||||||
@@ -413,7 +419,7 @@ export const actionZoomToFitSelection = register({
|
|||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
fitToViewport: true,
|
fitToViewport: true,
|
||||||
viewportZoomFactor: 0.7,
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// NOTE this action should use shift-2 per figma, alas
|
// NOTE this action should use shift-2 per figma, alas
|
||||||
@@ -430,7 +436,7 @@ export const actionZoomToFit = register({
|
|||||||
icon: zoomAreaIcon,
|
icon: zoomAreaIcon,
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState, _, app) =>
|
||||||
zoomToFit({
|
zoomToFit({
|
||||||
targetElements: elements,
|
targetElements: elements,
|
||||||
appState: {
|
appState: {
|
||||||
@@ -438,6 +444,7 @@ export const actionZoomToFit = register({
|
|||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
fitToViewport: false,
|
fitToViewport: false,
|
||||||
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
}),
|
}),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ export const actionFinalize = register({
|
|||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
visible={appState.multiElement != null}
|
visible={appState.multiElement != null}
|
||||||
size={data?.size || "medium"}
|
size={data?.size || "medium"}
|
||||||
|
style={{ pointerEvents: "all" }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Excalidraw } from "../index";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { point } from "../../math";
|
||||||
|
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("flipping re-centers selection", () => {
|
||||||
|
it("elbow arrow touches group selection side yet it remains in place after multiple moves", async () => {
|
||||||
|
const elements = [
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rec1",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
boundElements: [{ id: "arr", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rec2",
|
||||||
|
x: 220,
|
||||||
|
y: 250,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
boundElements: [{ id: "arr", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arr",
|
||||||
|
x: 149.9,
|
||||||
|
y: 95,
|
||||||
|
width: 156,
|
||||||
|
height: 239.9,
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rec1",
|
||||||
|
focus: 0,
|
||||||
|
gap: 5,
|
||||||
|
fixedPoint: [0.49, -0.05],
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: "rec2",
|
||||||
|
focus: 0,
|
||||||
|
gap: 5,
|
||||||
|
fixedPoint: [-0.05, 0.49],
|
||||||
|
},
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: "arrow",
|
||||||
|
points: [
|
||||||
|
point(0, 0),
|
||||||
|
point(0, -35),
|
||||||
|
point(-90.9, -35),
|
||||||
|
point(-90.9, 204.9),
|
||||||
|
point(65.1, 204.9),
|
||||||
|
],
|
||||||
|
elbowed: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw initialData={{ elements }} />);
|
||||||
|
|
||||||
|
API.setSelectedElements(elements);
|
||||||
|
|
||||||
|
expect(Object.keys(h.state.selectedElementIds).length).toBe(3);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
|
||||||
|
const rec1 = h.elements.find((el) => el.id === "rec1");
|
||||||
|
expect(rec1?.x).toBeCloseTo(100);
|
||||||
|
expect(rec1?.y).toBeCloseTo(100);
|
||||||
|
|
||||||
|
const rec2 = h.elements.find((el) => el.id === "rec2");
|
||||||
|
expect(rec2?.x).toBeCloseTo(220);
|
||||||
|
expect(rec2?.y).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flipping arrowheads", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow should flip arrowheads only", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: null,
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe(null);
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipVertical);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe(null);
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow should flip arrowheads only 2", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: "circle",
|
||||||
|
startBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect2.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, rect2, arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("circle");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipVertical);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping unbound arrow shouldn't flip arrowheads", () => {
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: "circle",
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow shouldn't flip arrowheads if selected alongside non-arrow eleemnt", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: null,
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setSelectedElements([rect, arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,8 @@ import { register } from "./register";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
@@ -18,7 +20,13 @@ import {
|
|||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { flipHorizontal, flipVertical } from "../components/icons";
|
import { flipHorizontal, flipVertical } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isElbowArrow,
|
||||||
|
isLinearElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
@@ -109,7 +117,23 @@ const flipElements = (
|
|||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
if (
|
||||||
|
selectedElements.every(
|
||||||
|
(element) =>
|
||||||
|
isArrowElement(element) && (element.startBinding || element.endBinding),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return selectedElements.map((element) => {
|
||||||
|
const _element = element as ExcalidrawArrowElement;
|
||||||
|
return newElementWith(_element, {
|
||||||
|
startArrowhead: _element.endArrowhead,
|
||||||
|
endArrowhead: _element.startArrowhead,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { minX, minY, maxX, maxY, midX, midY } =
|
||||||
|
getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -131,5 +155,48 @@ const flipElements = (
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flipping arrow elements (and potentially other) makes the selection group
|
||||||
|
// "move" across the canvas because of how arrows can bump against the "wall"
|
||||||
|
// of the selection, so we need to center the group back to the original
|
||||||
|
// position so that repeated flips don't accumulate the offset
|
||||||
|
|
||||||
|
const { elbowArrows, otherElements } = selectedElements.reduce(
|
||||||
|
(
|
||||||
|
acc: {
|
||||||
|
elbowArrows: ExcalidrawElbowArrowElement[];
|
||||||
|
otherElements: ExcalidrawElement[];
|
||||||
|
},
|
||||||
|
element,
|
||||||
|
) =>
|
||||||
|
isElbowArrow(element)
|
||||||
|
? { ...acc, elbowArrows: acc.elbowArrows.concat(element) }
|
||||||
|
: { ...acc, otherElements: acc.otherElements.concat(element) },
|
||||||
|
{ elbowArrows: [], otherElements: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { midX: newMidX, midY: newMidY } =
|
||||||
|
getCommonBoundingBox(selectedElements);
|
||||||
|
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||||
|
otherElements.forEach((element) =>
|
||||||
|
mutateElement(element, {
|
||||||
|
x: element.x + diffX,
|
||||||
|
y: element.y + diffY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
elbowArrows.forEach((element) =>
|
||||||
|
mutateElbowArrow(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
element.points,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
informMutation: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
return selectedElements;
|
return selectedElements;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1685,19 +1685,6 @@ export const actionChangeArrowType = register({
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
mutateElement(
|
|
||||||
newElement,
|
|
||||||
{
|
|
||||||
startBinding: newElement.startBinding
|
|
||||||
? { ...newElement.startBinding, fixedPoint: null }
|
|
||||||
: null,
|
|
||||||
endBinding: newElement.endBinding
|
|
||||||
? { ...newElement.endBinding, fixedPoint: null }
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { register } from "./register";
|
||||||
|
import type { AppState } from "../types";
|
||||||
|
import { searchIcon } from "../components/icons";
|
||||||
|
import { StoreAction } from "../store";
|
||||||
|
import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants";
|
||||||
|
|
||||||
|
export const actionToggleSearchMenu = register({
|
||||||
|
name: "searchMenu",
|
||||||
|
icon: searchIcon,
|
||||||
|
keywords: ["search", "find"],
|
||||||
|
label: "search.title",
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: {
|
||||||
|
category: "search_menu",
|
||||||
|
action: "toggle",
|
||||||
|
predicate: (appState) => appState.gridModeEnabled,
|
||||||
|
},
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
if (
|
||||||
|
appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
|
||||||
|
appState.openSidebar.tab === CANVAS_SEARCH_TAB
|
||||||
|
) {
|
||||||
|
const searchInput =
|
||||||
|
app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>(
|
||||||
|
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchInput?.matches(":focus")) {
|
||||||
|
return {
|
||||||
|
appState: { ...appState, openSidebar: null },
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput?.focus();
|
||||||
|
searchInput?.select();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
openSidebar: { name: DEFAULT_SIDEBAR.name, tab: CANVAS_SEARCH_TAB },
|
||||||
|
openDialog: null,
|
||||||
|
},
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState: AppState) => appState.gridModeEnabled,
|
||||||
|
predicate: (element, appState, props) => {
|
||||||
|
return props.gridModeEnabled === undefined;
|
||||||
|
},
|
||||||
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F,
|
||||||
|
});
|
||||||
@@ -86,3 +86,5 @@ export { actionUnbindText, actionBindText } from "./actionBoundText";
|
|||||||
export { actionLink } from "./actionLink";
|
export { actionLink } from "./actionLink";
|
||||||
export { actionToggleElementLock } from "./actionElementLock";
|
export { actionToggleElementLock } from "./actionElementLock";
|
||||||
export { actionToggleLinearEditor } from "./actionLinearEditor";
|
export { actionToggleLinearEditor } from "./actionLinearEditor";
|
||||||
|
|
||||||
|
export { actionToggleSearchMenu } from "./actionToggleSearchMenu";
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ export type ShortcutName =
|
|||||||
>
|
>
|
||||||
| "saveScene"
|
| "saveScene"
|
||||||
| "imageExport"
|
| "imageExport"
|
||||||
| "commandPalette";
|
| "commandPalette"
|
||||||
|
| "searchMenu";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
@@ -112,6 +113,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
saveFileToDisk: [getShortcutKey("CtrlOrCmd+S")],
|
saveFileToDisk: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
|
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
toggleShortcuts: [getShortcutKey("?")],
|
toggleShortcuts: [getShortcutKey("?")],
|
||||||
|
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ export type ActionName =
|
|||||||
| "wrapTextInContainer"
|
| "wrapTextInContainer"
|
||||||
| "commandPalette"
|
| "commandPalette"
|
||||||
| "autoResize"
|
| "autoResize"
|
||||||
| "elementStats";
|
| "elementStats"
|
||||||
|
| "searchMenu";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@@ -191,7 +192,8 @@ export interface Action {
|
|||||||
| "history"
|
| "history"
|
||||||
| "menu"
|
| "menu"
|
||||||
| "collab"
|
| "collab"
|
||||||
| "hyperlink";
|
| "hyperlink"
|
||||||
|
| "search_menu";
|
||||||
action?: string;
|
action?: string;
|
||||||
predicate?: (
|
predicate?: (
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
objectsSnapModeEnabled: false,
|
objectsSnapModeEnabled: false,
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
followedBy: new Set(),
|
followedBy: new Set(),
|
||||||
|
searchMatches: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,6 +237,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
||||||
userToFollow: { browser: false, export: false, server: false },
|
userToFollow: { browser: false, export: false, server: false },
|
||||||
followedBy: { browser: false, export: false, server: false },
|
followedBy: { browser: false, export: false, server: false },
|
||||||
|
searchMatches: { browser: false, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ import type {
|
|||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
@@ -259,6 +260,7 @@ import type {
|
|||||||
ElementsPendingErasure,
|
ElementsPendingErasure,
|
||||||
GenerateDiagramToCode,
|
GenerateDiagramToCode,
|
||||||
NullableGridSize,
|
NullableGridSize,
|
||||||
|
Offsets,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
@@ -286,6 +288,7 @@ import {
|
|||||||
getDateTime,
|
getDateTime,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
|
toBrandedType,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
createSrcDoc,
|
createSrcDoc,
|
||||||
@@ -434,12 +437,13 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
|||||||
import { getVisibleSceneBounds } from "../element/bounds";
|
import { getVisibleSceneBounds } from "../element/bounds";
|
||||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||||
import NewElementCanvas from "./canvases/NewElementCanvas";
|
import NewElementCanvas from "./canvases/NewElementCanvas";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
import { mutateElbowArrow, updateElbowArrow } from "../element/routing";
|
||||||
import {
|
import {
|
||||||
FlowChartCreator,
|
FlowChartCreator,
|
||||||
FlowChartNavigator,
|
FlowChartNavigator,
|
||||||
getLinkDirectionFromKey,
|
getLinkDirectionFromKey,
|
||||||
} from "../element/flowchart";
|
} from "../element/flowchart";
|
||||||
|
import { searchItemInFocusAtom } from "./SearchMenu";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { point, pointDistance, vector } from "../../math";
|
import { point, pointDistance, vector } from "../../math";
|
||||||
|
|
||||||
@@ -548,6 +552,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public scene: Scene;
|
public scene: Scene;
|
||||||
public fonts: Fonts;
|
public fonts: Fonts;
|
||||||
public renderer: Renderer;
|
public renderer: Renderer;
|
||||||
|
public visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
@@ -555,7 +560,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public id: string;
|
public id: string;
|
||||||
private store: Store;
|
private store: Store;
|
||||||
private history: History;
|
private history: History;
|
||||||
private excalidrawContainerValue: {
|
public excalidrawContainerValue: {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@@ -682,6 +687,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
this.rc = rough.canvas(this.canvas);
|
this.rc = rough.canvas(this.canvas);
|
||||||
this.renderer = new Renderer(this.scene);
|
this.renderer = new Renderer(this.scene);
|
||||||
|
this.visibleElements = [];
|
||||||
|
|
||||||
this.store = new Store();
|
this.store = new Store();
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
@@ -1480,6 +1486,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
newElementId: this.state.newElement?.id,
|
newElementId: this.state.newElement?.id,
|
||||||
pendingImageElementId: this.state.pendingImageElementId,
|
pendingImageElementId: this.state.pendingImageElementId,
|
||||||
});
|
});
|
||||||
|
this.visibleElements = visibleElements;
|
||||||
|
|
||||||
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
@@ -2295,6 +2302,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// clear the shape and image cache so that any images in initialData
|
||||||
|
// can be loaded fresh
|
||||||
|
this.clearImageShapeCache();
|
||||||
// FontFaceSet loadingdone event we listen on may not always
|
// FontFaceSet loadingdone event we listen on may not always
|
||||||
// fire (looking at you Safari), so on init we manually load all
|
// fire (looking at you Safari), so on init we manually load all
|
||||||
// fonts and rerender scene text elements once done. This also
|
// fonts and rerender scene text elements once done. This also
|
||||||
@@ -2360,6 +2370,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private clearImageShapeCache(filesMap?: BinaryFiles) {
|
||||||
|
const files = filesMap ?? this.files;
|
||||||
|
this.scene.getNonDeletedElements().forEach((element) => {
|
||||||
|
if (isInitializedImageElement(element) && files[element.fileId]) {
|
||||||
|
this.imageCache.delete(element.fileId);
|
||||||
|
ShapeCache.delete(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.excalidrawContainerValue.container =
|
this.excalidrawContainerValue.container =
|
||||||
@@ -3091,7 +3111,45 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
let elements = opts.elements.map((el, _, elements) => {
|
||||||
|
if (isElbowArrow(el)) {
|
||||||
|
const startEndElements = [
|
||||||
|
el.startBinding &&
|
||||||
|
elements.find((l) => l.id === el.startBinding?.elementId),
|
||||||
|
el.endBinding &&
|
||||||
|
elements.find((l) => l.id === el.endBinding?.elementId),
|
||||||
|
];
|
||||||
|
const startBinding = startEndElements[0] ? el.startBinding : null;
|
||||||
|
const endBinding = startEndElements[1] ? el.endBinding : null;
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
...updateElbowArrow(
|
||||||
|
{
|
||||||
|
...el,
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
|
},
|
||||||
|
toBrandedType<NonDeletedSceneElementsMap>(
|
||||||
|
new Map(
|
||||||
|
startEndElements
|
||||||
|
.filter((x) => x != null)
|
||||||
|
.map(
|
||||||
|
(el) =>
|
||||||
|
[el!.id, el] as [
|
||||||
|
string,
|
||||||
|
Ordered<NonDeletedExcalidrawElement>,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[el.points[0], el.points[el.points.length - 1]],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
elements = restoreElements(elements, null, undefined);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
@@ -3215,6 +3273,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (opts.fitToContent) {
|
if (opts.fitToContent) {
|
||||||
this.scrollToContent(newElements, {
|
this.scrollToContent(newElements, {
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3527,7 +3586,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
target:
|
target:
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
||||||
opts?:
|
opts?: (
|
||||||
| {
|
| {
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
fitToViewport?: never;
|
fitToViewport?: never;
|
||||||
@@ -3544,7 +3603,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
},
|
}
|
||||||
|
) & {
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
this.cancelInProgressAnimation?.();
|
this.cancelInProgressAnimation?.();
|
||||||
|
|
||||||
@@ -3557,10 +3621,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (opts?.fitToContent || opts?.fitToViewport) {
|
if (opts?.fitToContent || opts?.fitToViewport) {
|
||||||
const { appState } = zoomToFit({
|
const { appState } = zoomToFit({
|
||||||
|
canvasOffsets: opts.canvasOffsets,
|
||||||
targetElements,
|
targetElements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
fitToViewport: !!opts?.fitToViewport,
|
fitToViewport: !!opts?.fitToViewport,
|
||||||
viewportZoomFactor: opts?.viewportZoomFactor,
|
viewportZoomFactor: opts?.viewportZoomFactor,
|
||||||
|
minZoom: opts?.minZoom,
|
||||||
|
maxZoom: opts?.maxZoom,
|
||||||
});
|
});
|
||||||
zoom = appState.zoom;
|
zoom = appState.zoom;
|
||||||
scrollX = appState.scrollX;
|
scrollX = appState.scrollX;
|
||||||
@@ -3674,15 +3741,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
this.files = { ...this.files, ...Object.fromEntries(filesMap) };
|
this.files = { ...this.files, ...Object.fromEntries(filesMap) };
|
||||||
|
|
||||||
this.scene.getNonDeletedElements().forEach((element) => {
|
this.clearImageShapeCache(Object.fromEntries(filesMap));
|
||||||
if (
|
|
||||||
isInitializedImageElement(element) &&
|
|
||||||
filesMap.has(element.fileId)
|
|
||||||
) {
|
|
||||||
this.imageCache.delete(element.fileId);
|
|
||||||
ShapeCache.delete(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.scene.triggerUpdate();
|
this.scene.triggerUpdate();
|
||||||
|
|
||||||
this.addNewImagesToImageCache();
|
this.addNewImagesToImageCache();
|
||||||
@@ -3796,40 +3855,55 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private getEditorUIOffsets = (): {
|
public getEditorUIOffsets = (): Offsets => {
|
||||||
top: number;
|
|
||||||
right: number;
|
|
||||||
bottom: number;
|
|
||||||
left: number;
|
|
||||||
} => {
|
|
||||||
const toolbarBottom =
|
const toolbarBottom =
|
||||||
this.excalidrawContainerRef?.current
|
(this.excalidrawContainerRef?.current
|
||||||
?.querySelector(".App-toolbar")
|
?.querySelector(".App-toolbar")
|
||||||
?.getBoundingClientRect()?.bottom ?? 0;
|
?.getBoundingClientRect()?.bottom ?? 0) - this.state.offsetTop;
|
||||||
const sidebarWidth = Math.max(
|
const sidebarRect = this.excalidrawContainerRef?.current
|
||||||
this.excalidrawContainerRef?.current
|
?.querySelector(".sidebar")
|
||||||
?.querySelector(".default-sidebar")
|
?.getBoundingClientRect();
|
||||||
?.getBoundingClientRect()?.width ?? 0,
|
const propertiesPanelRect = this.excalidrawContainerRef?.current
|
||||||
);
|
?.querySelector(".App-menu__left")
|
||||||
const propertiesPanelWidth = Math.max(
|
?.getBoundingClientRect();
|
||||||
this.excalidrawContainerRef?.current
|
const PADDING = 16;
|
||||||
?.querySelector(".App-menu__left")
|
|
||||||
?.getBoundingClientRect()?.width ?? 0,
|
const adjustRectValueForOffset = (
|
||||||
0,
|
value: number | undefined,
|
||||||
);
|
fallback: number,
|
||||||
|
) => (value ?? fallback + this.state.offsetLeft) - this.state.offsetLeft;
|
||||||
|
|
||||||
return getLanguage().rtl
|
return getLanguage().rtl
|
||||||
? {
|
? {
|
||||||
top: toolbarBottom,
|
top: toolbarBottom + PADDING,
|
||||||
right: propertiesPanelWidth,
|
right:
|
||||||
bottom: 0,
|
Math.max(
|
||||||
left: sidebarWidth,
|
this.state.width -
|
||||||
|
adjustRectValueForOffset(
|
||||||
|
propertiesPanelRect?.left,
|
||||||
|
this.state.width,
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
) + PADDING,
|
||||||
|
bottom: PADDING,
|
||||||
|
left:
|
||||||
|
Math.max(adjustRectValueForOffset(sidebarRect?.right, 0), 0) +
|
||||||
|
PADDING,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
top: toolbarBottom,
|
top: toolbarBottom + PADDING,
|
||||||
right: sidebarWidth,
|
right: Math.max(
|
||||||
bottom: 0,
|
this.state.width -
|
||||||
left: propertiesPanelWidth,
|
adjustRectValueForOffset(sidebarRect?.left, this.state.width) +
|
||||||
|
PADDING,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
bottom: PADDING,
|
||||||
|
left:
|
||||||
|
Math.max(
|
||||||
|
adjustRectValueForOffset(propertiesPanelRect?.right, 0),
|
||||||
|
0,
|
||||||
|
) + PADDING,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3914,7 +3988,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
viewportZoomFactor: 0.8,
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3970,6 +4044,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scrollToContent(nextNode, {
|
this.scrollToContent(nextNode, {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4402,6 +4477,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scrollToContent(firstNode, {
|
this.scrollToContent(firstNode, {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5969,6 +6045,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
||||||
this.maybeUnfollowRemoteUser();
|
this.maybeUnfollowRemoteUser();
|
||||||
|
|
||||||
|
if (this.state.searchMatches) {
|
||||||
|
this.setState((state) => ({
|
||||||
|
searchMatches: state.searchMatches.map((searchMatch) => ({
|
||||||
|
...searchMatch,
|
||||||
|
focus: false,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
jotaiStore.set(searchItemInFocusAtom, null);
|
||||||
|
}
|
||||||
|
|
||||||
// since contextMenu options are potentially evaluated on each render,
|
// since contextMenu options are potentially evaluated on each render,
|
||||||
// and an contextMenu action may depend on selection state, we must
|
// and an contextMenu action may depend on selection state, we must
|
||||||
// close the contextMenu before we update the selection on pointerDown
|
// close the contextMenu before we update the selection on pointerDown
|
||||||
@@ -6397,8 +6483,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
|
|
||||||
|
// due to event.preventDefault below, container wouldn't get focus
|
||||||
|
// automatically
|
||||||
|
this.focusContainer();
|
||||||
|
|
||||||
|
// preventing defualt while text editing messes with cursor/focus
|
||||||
if (!this.state.editingTextElement) {
|
if (!this.state.editingTextElement) {
|
||||||
// preventing defualt while text editing messes with cursor/focus
|
// necessary to prevent browser from scrolling the page if excalidraw
|
||||||
|
// not full-page #4489
|
||||||
|
//
|
||||||
|
// as such, the above is broken when panning canvas while in wysiwyg
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ import { InlineIcon } from "../InlineIcon";
|
|||||||
import { SHAPES } from "../../shapes";
|
import { SHAPES } from "../../shapes";
|
||||||
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
|
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
|
||||||
import { useStableCallback } from "../../hooks/useStableCallback";
|
import { useStableCallback } from "../../hooks/useStableCallback";
|
||||||
import { actionClearCanvas, actionLink } from "../../actions";
|
import {
|
||||||
|
actionClearCanvas,
|
||||||
|
actionLink,
|
||||||
|
actionToggleSearchMenu,
|
||||||
|
} from "../../actions";
|
||||||
import { jotaiStore } from "../../jotai";
|
import { jotaiStore } from "../../jotai";
|
||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
import type { CommandPaletteItem } from "./types";
|
import type { CommandPaletteItem } from "./types";
|
||||||
@@ -382,6 +386,15 @@ function CommandPaletteInner({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("search.title"),
|
||||||
|
category: DEFAULT_CATEGORIES.app,
|
||||||
|
icon: searchIcon,
|
||||||
|
viewMode: true,
|
||||||
|
perform: () => {
|
||||||
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("labels.changeStroke"),
|
label: t("labels.changeStroke"),
|
||||||
keywords: ["color", "outline"],
|
keywords: ["color", "outline"],
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
|
import {
|
||||||
|
CANVAS_SEARCH_TAB,
|
||||||
|
DEFAULT_SIDEBAR,
|
||||||
|
LIBRARY_SIDEBAR_TAB,
|
||||||
|
} from "../constants";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
import { useUIAppState } from "../context/ui-appState";
|
import { useUIAppState } from "../context/ui-appState";
|
||||||
import { t } from "../i18n";
|
|
||||||
import type { MarkOptional, Merge } from "../utility-types";
|
import type { MarkOptional, Merge } from "../utility-types";
|
||||||
import { composeEventHandlers } from "../utils";
|
import { composeEventHandlers } from "../utils";
|
||||||
import { useExcalidrawSetAppState } from "./App";
|
import { useExcalidrawSetAppState } from "./App";
|
||||||
@@ -10,6 +13,9 @@ import { withInternalFallback } from "./hoc/withInternalFallback";
|
|||||||
import { LibraryMenu } from "./LibraryMenu";
|
import { LibraryMenu } from "./LibraryMenu";
|
||||||
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
|
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
|
||||||
import { Sidebar } from "./Sidebar/Sidebar";
|
import { Sidebar } from "./Sidebar/Sidebar";
|
||||||
|
import "../components/dropdownMenu/DropdownMenu.scss";
|
||||||
|
import { SearchMenu } from "./SearchMenu";
|
||||||
|
import { LibraryIcon, searchIcon } from "./icons";
|
||||||
|
|
||||||
const DefaultSidebarTrigger = withInternalFallback(
|
const DefaultSidebarTrigger = withInternalFallback(
|
||||||
"DefaultSidebarTrigger",
|
"DefaultSidebarTrigger",
|
||||||
@@ -31,14 +37,11 @@ const DefaultSidebarTrigger = withInternalFallback(
|
|||||||
);
|
);
|
||||||
DefaultSidebarTrigger.displayName = "DefaultSidebarTrigger";
|
DefaultSidebarTrigger.displayName = "DefaultSidebarTrigger";
|
||||||
|
|
||||||
const DefaultTabTriggers = ({
|
const DefaultTabTriggers = ({ children }: { children: React.ReactNode }) => {
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => {
|
|
||||||
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
|
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
|
||||||
return (
|
return (
|
||||||
<DefaultSidebarTabTriggersTunnel.In>
|
<DefaultSidebarTabTriggersTunnel.In>
|
||||||
<Sidebar.TabTriggers {...rest}>{children}</Sidebar.TabTriggers>
|
{children}
|
||||||
</DefaultSidebarTabTriggersTunnel.In>
|
</DefaultSidebarTabTriggersTunnel.In>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -65,17 +68,21 @@ export const DefaultSidebar = Object.assign(
|
|||||||
|
|
||||||
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
|
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
|
||||||
|
|
||||||
|
const isForceDocked = appState.openSidebar?.tab === CANVAS_SEARCH_TAB;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
{...rest}
|
{...rest}
|
||||||
name="default"
|
name="default"
|
||||||
key="default"
|
key="default"
|
||||||
className={clsx("default-sidebar", className)}
|
className={clsx("default-sidebar", className)}
|
||||||
docked={docked ?? appState.defaultSidebarDockedPreference}
|
docked={
|
||||||
|
isForceDocked || (docked ?? appState.defaultSidebarDockedPreference)
|
||||||
|
}
|
||||||
onDock={
|
onDock={
|
||||||
// `onDock=false` disables docking.
|
// `onDock=false` disables docking.
|
||||||
// if `docked` passed, but no onDock passed, disable manual docking.
|
// if `docked` passed, but no onDock passed, disable manual docking.
|
||||||
onDock === false || (!onDock && docked != null)
|
isForceDocked || onDock === false || (!onDock && docked != null)
|
||||||
? undefined
|
? undefined
|
||||||
: // compose to allow the host app to listen on default behavior
|
: // compose to allow the host app to listen on default behavior
|
||||||
composeEventHandlers(onDock, (docked) => {
|
composeEventHandlers(onDock, (docked) => {
|
||||||
@@ -85,26 +92,22 @@ export const DefaultSidebar = Object.assign(
|
|||||||
>
|
>
|
||||||
<Sidebar.Tabs>
|
<Sidebar.Tabs>
|
||||||
<Sidebar.Header>
|
<Sidebar.Header>
|
||||||
{rest.__fallback && (
|
<Sidebar.TabTriggers>
|
||||||
<div
|
<Sidebar.TabTrigger tab={CANVAS_SEARCH_TAB}>
|
||||||
style={{
|
{searchIcon}
|
||||||
color: "var(--color-primary)",
|
</Sidebar.TabTrigger>
|
||||||
fontSize: "1.2em",
|
<Sidebar.TabTrigger tab={LIBRARY_SIDEBAR_TAB}>
|
||||||
fontWeight: "bold",
|
{LibraryIcon}
|
||||||
textOverflow: "ellipsis",
|
</Sidebar.TabTrigger>
|
||||||
overflow: "hidden",
|
<DefaultSidebarTabTriggersTunnel.Out />
|
||||||
whiteSpace: "nowrap",
|
</Sidebar.TabTriggers>
|
||||||
paddingRight: "1em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("toolBar.library")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DefaultSidebarTabTriggersTunnel.Out />
|
|
||||||
</Sidebar.Header>
|
</Sidebar.Header>
|
||||||
<Sidebar.Tab tab={LIBRARY_SIDEBAR_TAB}>
|
<Sidebar.Tab tab={LIBRARY_SIDEBAR_TAB}>
|
||||||
<LibraryMenu />
|
<LibraryMenu />
|
||||||
</Sidebar.Tab>
|
</Sidebar.Tab>
|
||||||
|
<Sidebar.Tab tab={CANVAS_SEARCH_TAB}>
|
||||||
|
<SearchMenu />
|
||||||
|
</Sidebar.Tab>
|
||||||
{children}
|
{children}
|
||||||
</Sidebar.Tabs>
|
</Sidebar.Tabs>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@@ -288,6 +288,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("stats.fullTitle")}
|
label={t("stats.fullTitle")}
|
||||||
shortcuts={[getShortcutKey("Alt+/")]}
|
shortcuts={[getShortcutKey("Alt+/")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("search.title")}
|
||||||
|
shortcuts={[getShortcutFromShortcutName("searchMenu")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("commandPalette.title")}
|
label={t("commandPalette.title")}
|
||||||
shortcuts={
|
shortcuts={
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { isEraserActive } from "../appState";
|
|||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { isNodeInFlowchart } from "../element/flowchart";
|
import { isNodeInFlowchart } from "../element/flowchart";
|
||||||
import { isGridModeEnabled } from "../snapping";
|
import { isGridModeEnabled } from "../snapping";
|
||||||
|
import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "../constants";
|
||||||
|
|
||||||
interface HintViewerProps {
|
interface HintViewerProps {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
@@ -30,6 +31,14 @@ const getHints = ({
|
|||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
|
||||||
|
appState.openSidebar.tab === CANVAS_SEARCH_TAB &&
|
||||||
|
appState.searchMatches?.length
|
||||||
|
) {
|
||||||
|
return t("hints.dismissSearch");
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ import { LibraryIcon } from "./icons";
|
|||||||
import { UIAppStateContext } from "../context/ui-appState";
|
import { UIAppStateContext } from "../context/ui-appState";
|
||||||
import { DefaultSidebar } from "./DefaultSidebar";
|
import { DefaultSidebar } from "./DefaultSidebar";
|
||||||
import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
|
import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
|
||||||
|
|
||||||
import "./LayerUI.scss";
|
|
||||||
import "./Toolbar.scss";
|
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
@@ -64,6 +61,9 @@ import { TTDDialog } from "./TTDDialog/TTDDialog";
|
|||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
|
|
||||||
|
import "./LayerUI.scss";
|
||||||
|
import "./Toolbar.scss";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
@@ -99,6 +99,7 @@ const DefaultMainMenu: React.FC<{
|
|||||||
{UIOptions.canvasActions.saveAsImage && (
|
{UIOptions.canvasActions.saveAsImage && (
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
)}
|
)}
|
||||||
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
@import "open-color/open-color";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.layer-ui__search {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__search-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
.ExcTextField {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExcTextField__input {
|
||||||
|
background-color: #f5f5f9;
|
||||||
|
@at-root .excalidraw.theme--dark#{&} {
|
||||||
|
background-color: #31303b;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__search-count {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 8px 0 8px;
|
||||||
|
margin: 0 0.75rem 0.25rem 0.75rem;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
.result-nav {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.result-nav-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
--button-border: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-surface-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__search-result-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
margin: 0 0.75rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
|
||||||
|
.text-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-text {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 48px;
|
||||||
|
line-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-surface-high);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-surface-high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,718 @@
|
|||||||
|
import { Fragment, memo, useEffect, useRef, useState } from "react";
|
||||||
|
import { collapseDownIcon, upIcon, searchIcon } from "./icons";
|
||||||
|
import { TextField } from "./TextField";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { useApp, useExcalidrawSetAppState } from "./App";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import type { AppClassProperties } from "../types";
|
||||||
|
import { isTextElement, newTextElement } from "../element";
|
||||||
|
import type { ExcalidrawTextElement } from "../element/types";
|
||||||
|
import { measureText } from "../element/textElement";
|
||||||
|
import { addEventListener, getFontString } from "../utils";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { isElementCompletelyInViewport } from "../element/sizeHelpers";
|
||||||
|
import { randomInteger } from "../random";
|
||||||
|
import { CLASSES, EVENT } from "../constants";
|
||||||
|
import { useStable } from "../hooks/useStable";
|
||||||
|
|
||||||
|
import "./SearchMenu.scss";
|
||||||
|
import { round } from "../../math";
|
||||||
|
|
||||||
|
const searchQueryAtom = atom<string>("");
|
||||||
|
export const searchItemInFocusAtom = atom<number | null>(null);
|
||||||
|
|
||||||
|
const SEARCH_DEBOUNCE = 350;
|
||||||
|
|
||||||
|
type SearchMatchItem = {
|
||||||
|
textElement: ExcalidrawTextElement;
|
||||||
|
searchQuery: SearchQuery;
|
||||||
|
index: number;
|
||||||
|
preview: {
|
||||||
|
indexInSearchQuery: number;
|
||||||
|
previewText: string;
|
||||||
|
moreBefore: boolean;
|
||||||
|
moreAfter: boolean;
|
||||||
|
};
|
||||||
|
matchedLines: {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchMatches = {
|
||||||
|
nonce: number | null;
|
||||||
|
items: SearchMatchItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchQuery = string & { _brand: "SearchQuery" };
|
||||||
|
|
||||||
|
export const SearchMenu = () => {
|
||||||
|
const app = useApp();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useAtom(searchQueryAtom, jotaiScope);
|
||||||
|
const searchQuery = inputValue.trim() as SearchQuery;
|
||||||
|
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
const [searchMatches, setSearchMatches] = useState<SearchMatches>({
|
||||||
|
nonce: null,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
const searchedQueryRef = useRef<SearchQuery | null>(null);
|
||||||
|
const lastSceneNonceRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const [focusIndex, setFocusIndex] = useAtom(
|
||||||
|
searchItemInFocusAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSearching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
searchQuery !== searchedQueryRef.current ||
|
||||||
|
app.scene.getSceneNonce() !== lastSceneNonceRef.current
|
||||||
|
) {
|
||||||
|
searchedQueryRef.current = null;
|
||||||
|
handleSearch(searchQuery, app, (matchItems, index) => {
|
||||||
|
setSearchMatches({
|
||||||
|
nonce: randomInteger(),
|
||||||
|
items: matchItems,
|
||||||
|
});
|
||||||
|
searchedQueryRef.current = searchQuery;
|
||||||
|
lastSceneNonceRef.current = app.scene.getSceneNonce();
|
||||||
|
setAppState({
|
||||||
|
searchMatches: matchItems.map((searchMatch) => ({
|
||||||
|
id: searchMatch.textElement.id,
|
||||||
|
focus: false,
|
||||||
|
matchedLines: searchMatch.matchedLines,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isSearching,
|
||||||
|
searchQuery,
|
||||||
|
elementsMap,
|
||||||
|
app,
|
||||||
|
setAppState,
|
||||||
|
setFocusIndex,
|
||||||
|
lastSceneNonceRef,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const goToNextItem = () => {
|
||||||
|
if (searchMatches.items.length > 0) {
|
||||||
|
setFocusIndex((focusIndex) => {
|
||||||
|
if (focusIndex === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (focusIndex + 1) % searchMatches.items.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPreviousItem = () => {
|
||||||
|
if (searchMatches.items.length > 0) {
|
||||||
|
setFocusIndex((focusIndex) => {
|
||||||
|
if (focusIndex === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return focusIndex - 1 < 0
|
||||||
|
? searchMatches.items.length - 1
|
||||||
|
: focusIndex - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAppState((state) => {
|
||||||
|
return {
|
||||||
|
searchMatches: state.searchMatches.map((match, index) => {
|
||||||
|
if (index === focusIndex) {
|
||||||
|
return { ...match, focus: true };
|
||||||
|
}
|
||||||
|
return { ...match, focus: false };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [focusIndex, setAppState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchMatches.items.length > 0 && focusIndex !== null) {
|
||||||
|
const match = searchMatches.items[focusIndex];
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const zoomValue = app.state.zoom.value;
|
||||||
|
|
||||||
|
const matchAsElement = newTextElement({
|
||||||
|
text: match.searchQuery,
|
||||||
|
x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0),
|
||||||
|
y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0),
|
||||||
|
width: match.matchedLines[0]?.width,
|
||||||
|
height: match.matchedLines[0]?.height,
|
||||||
|
fontSize: match.textElement.fontSize,
|
||||||
|
fontFamily: match.textElement.fontFamily,
|
||||||
|
});
|
||||||
|
|
||||||
|
const FONT_SIZE_LEGIBILITY_THRESHOLD = 14;
|
||||||
|
|
||||||
|
const fontSize = match.textElement.fontSize;
|
||||||
|
const isTextTiny =
|
||||||
|
fontSize * zoomValue < FONT_SIZE_LEGIBILITY_THRESHOLD;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isElementCompletelyInViewport(
|
||||||
|
[matchAsElement],
|
||||||
|
app.canvas.width / window.devicePixelRatio,
|
||||||
|
app.canvas.height / window.devicePixelRatio,
|
||||||
|
{
|
||||||
|
offsetLeft: app.state.offsetLeft,
|
||||||
|
offsetTop: app.state.offsetTop,
|
||||||
|
scrollX: app.state.scrollX,
|
||||||
|
scrollY: app.state.scrollY,
|
||||||
|
zoom: app.state.zoom,
|
||||||
|
},
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
app.getEditorUIOffsets(),
|
||||||
|
) ||
|
||||||
|
isTextTiny
|
||||||
|
) {
|
||||||
|
let zoomOptions: Parameters<AppClassProperties["scrollToContent"]>[1];
|
||||||
|
|
||||||
|
if (isTextTiny) {
|
||||||
|
if (fontSize >= FONT_SIZE_LEGIBILITY_THRESHOLD) {
|
||||||
|
zoomOptions = { fitToContent: true };
|
||||||
|
} else {
|
||||||
|
zoomOptions = {
|
||||||
|
fitToViewport: true,
|
||||||
|
// calculate zoom level to make the fontSize ~equal to FONT_SIZE_THRESHOLD, rounded to nearest 10%
|
||||||
|
maxZoom: round(FONT_SIZE_LEGIBILITY_THRESHOLD / fontSize, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zoomOptions = { fitToContent: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
app.scrollToContent(matchAsElement, {
|
||||||
|
animate: true,
|
||||||
|
duration: 300,
|
||||||
|
...zoomOptions,
|
||||||
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [focusIndex, searchMatches, app]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setFocusIndex(null);
|
||||||
|
searchedQueryRef.current = null;
|
||||||
|
lastSceneNonceRef.current = undefined;
|
||||||
|
setAppState({
|
||||||
|
searchMatches: [],
|
||||||
|
});
|
||||||
|
setIsSearching(false);
|
||||||
|
};
|
||||||
|
}, [setAppState, setFocusIndex]);
|
||||||
|
|
||||||
|
const stableState = useStable({
|
||||||
|
goToNextItem,
|
||||||
|
goToPreviousItem,
|
||||||
|
searchMatches,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventHandler = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
event.key === KEYS.ESCAPE &&
|
||||||
|
!app.state.openDialog &&
|
||||||
|
!app.state.openPopup
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setAppState({
|
||||||
|
openSidebar: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!searchInputRef.current?.matches(":focus")) {
|
||||||
|
if (app.state.openDialog) {
|
||||||
|
setAppState({
|
||||||
|
openDialog: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
searchInputRef.current?.select();
|
||||||
|
} else {
|
||||||
|
setAppState({
|
||||||
|
openSidebar: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.target instanceof HTMLElement &&
|
||||||
|
event.target.closest(".layer-ui__search")
|
||||||
|
) {
|
||||||
|
if (stableState.searchMatches.items.length) {
|
||||||
|
if (event.key === KEYS.ENTER) {
|
||||||
|
event.stopPropagation();
|
||||||
|
stableState.goToNextItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ARROW_UP) {
|
||||||
|
event.stopPropagation();
|
||||||
|
stableState.goToPreviousItem();
|
||||||
|
} else if (event.key === KEYS.ARROW_DOWN) {
|
||||||
|
event.stopPropagation();
|
||||||
|
stableState.goToNextItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// `capture` needed to prevent firing on initial open from App.tsx,
|
||||||
|
// as well as to handle events before App ones
|
||||||
|
return addEventListener(window, EVENT.KEYDOWN, eventHandler, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
}, [setAppState, stableState, app]);
|
||||||
|
|
||||||
|
const matchCount = `${searchMatches.items.length} ${
|
||||||
|
searchMatches.items.length === 1
|
||||||
|
? t("search.singleResult")
|
||||||
|
: t("search.multipleResults")
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layer-ui__search">
|
||||||
|
<div className="layer-ui__search-header">
|
||||||
|
<TextField
|
||||||
|
className={CLASSES.SEARCH_MENU_INPUT_WRAPPER}
|
||||||
|
value={inputValue}
|
||||||
|
ref={searchInputRef}
|
||||||
|
placeholder={t("search.placeholder")}
|
||||||
|
icon={searchIcon}
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputValue(value);
|
||||||
|
setIsSearching(true);
|
||||||
|
const searchQuery = value.trim() as SearchQuery;
|
||||||
|
handleSearch(searchQuery, app, (matchItems, index) => {
|
||||||
|
setSearchMatches({
|
||||||
|
nonce: randomInteger(),
|
||||||
|
items: matchItems,
|
||||||
|
});
|
||||||
|
setFocusIndex(index);
|
||||||
|
searchedQueryRef.current = searchQuery;
|
||||||
|
lastSceneNonceRef.current = app.scene.getSceneNonce();
|
||||||
|
setAppState({
|
||||||
|
searchMatches: matchItems.map((searchMatch) => ({
|
||||||
|
id: searchMatch.textElement.id,
|
||||||
|
focus: false,
|
||||||
|
matchedLines: searchMatch.matchedLines,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSearching(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
selectOnRender
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="layer-ui__search-count">
|
||||||
|
{searchMatches.items.length > 0 && (
|
||||||
|
<>
|
||||||
|
{focusIndex !== null && focusIndex > -1 ? (
|
||||||
|
<div>
|
||||||
|
{focusIndex + 1} / {matchCount}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>{matchCount}</div>
|
||||||
|
)}
|
||||||
|
<div className="result-nav">
|
||||||
|
<Button
|
||||||
|
onSelect={() => {
|
||||||
|
goToNextItem();
|
||||||
|
}}
|
||||||
|
className="result-nav-btn"
|
||||||
|
>
|
||||||
|
{collapseDownIcon}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onSelect={() => {
|
||||||
|
goToPreviousItem();
|
||||||
|
}}
|
||||||
|
className="result-nav-btn"
|
||||||
|
>
|
||||||
|
{upIcon}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchMatches.items.length === 0 &&
|
||||||
|
searchQuery &&
|
||||||
|
searchedQueryRef.current && (
|
||||||
|
<div style={{ margin: "1rem auto" }}>{t("search.noMatch")}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MatchList
|
||||||
|
matches={searchMatches}
|
||||||
|
onItemClick={setFocusIndex}
|
||||||
|
focusIndex={focusIndex}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListItem = (props: {
|
||||||
|
preview: SearchMatchItem["preview"];
|
||||||
|
searchQuery: SearchQuery;
|
||||||
|
highlighted: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
const preview = [
|
||||||
|
props.preview.moreBefore ? "..." : "",
|
||||||
|
props.preview.previewText.slice(0, props.preview.indexInSearchQuery),
|
||||||
|
props.preview.previewText.slice(
|
||||||
|
props.preview.indexInSearchQuery,
|
||||||
|
props.preview.indexInSearchQuery + props.searchQuery.length,
|
||||||
|
),
|
||||||
|
props.preview.previewText.slice(
|
||||||
|
props.preview.indexInSearchQuery + props.searchQuery.length,
|
||||||
|
),
|
||||||
|
props.preview.moreAfter ? "..." : "",
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
className={clsx("layer-ui__result-item", {
|
||||||
|
active: props.highlighted,
|
||||||
|
})}
|
||||||
|
onClick={props.onClick}
|
||||||
|
ref={(ref) => {
|
||||||
|
if (props.highlighted) {
|
||||||
|
ref?.scrollIntoView({ behavior: "auto", block: "nearest" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="preview-text">
|
||||||
|
{preview.flatMap((text, idx) => (
|
||||||
|
<Fragment key={idx}>{idx === 2 ? <b>{text}</b> : text}</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MatchListProps {
|
||||||
|
matches: SearchMatches;
|
||||||
|
onItemClick: (index: number) => void;
|
||||||
|
focusIndex: number | null;
|
||||||
|
searchQuery: SearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatchListBase = (props: MatchListProps) => {
|
||||||
|
return (
|
||||||
|
<div className="layer-ui__search-result-container">
|
||||||
|
{props.matches.items.map((searchMatch, index) => (
|
||||||
|
<ListItem
|
||||||
|
key={searchMatch.textElement.id + searchMatch.index}
|
||||||
|
searchQuery={props.searchQuery}
|
||||||
|
preview={searchMatch.preview}
|
||||||
|
highlighted={index === props.focusIndex}
|
||||||
|
onClick={() => props.onItemClick(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const areEqual = (prevProps: MatchListProps, nextProps: MatchListProps) => {
|
||||||
|
return (
|
||||||
|
prevProps.matches.nonce === nextProps.matches.nonce &&
|
||||||
|
prevProps.focusIndex === nextProps.focusIndex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MatchList = memo(MatchListBase, areEqual);
|
||||||
|
|
||||||
|
const getMatchPreview = (
|
||||||
|
text: string,
|
||||||
|
index: number,
|
||||||
|
searchQuery: SearchQuery,
|
||||||
|
) => {
|
||||||
|
const WORDS_BEFORE = 2;
|
||||||
|
const WORDS_AFTER = 5;
|
||||||
|
|
||||||
|
const substrBeforeQuery = text.slice(0, index);
|
||||||
|
const wordsBeforeQuery = substrBeforeQuery.split(/\s+/);
|
||||||
|
// text = "small", query = "mall", not complete before
|
||||||
|
// text = "small", query = "smal", complete before
|
||||||
|
const isQueryCompleteBefore = substrBeforeQuery.endsWith(" ");
|
||||||
|
const startWordIndex =
|
||||||
|
wordsBeforeQuery.length -
|
||||||
|
WORDS_BEFORE -
|
||||||
|
1 -
|
||||||
|
(isQueryCompleteBefore ? 0 : 1);
|
||||||
|
let wordsBeforeAsString =
|
||||||
|
wordsBeforeQuery.slice(startWordIndex <= 0 ? 0 : startWordIndex).join(" ") +
|
||||||
|
(isQueryCompleteBefore ? " " : "");
|
||||||
|
|
||||||
|
const MAX_ALLOWED_CHARS = 20;
|
||||||
|
|
||||||
|
wordsBeforeAsString =
|
||||||
|
wordsBeforeAsString.length > MAX_ALLOWED_CHARS
|
||||||
|
? wordsBeforeAsString.slice(-MAX_ALLOWED_CHARS)
|
||||||
|
: wordsBeforeAsString;
|
||||||
|
|
||||||
|
const substrAfterQuery = text.slice(index + searchQuery.length);
|
||||||
|
const wordsAfter = substrAfterQuery.split(/\s+/);
|
||||||
|
// text = "small", query = "mall", complete after
|
||||||
|
// text = "small", query = "smal", not complete after
|
||||||
|
const isQueryCompleteAfter = !substrAfterQuery.startsWith(" ");
|
||||||
|
const numberOfWordsToTake = isQueryCompleteAfter
|
||||||
|
? WORDS_AFTER + 1
|
||||||
|
: WORDS_AFTER;
|
||||||
|
const wordsAfterAsString =
|
||||||
|
(isQueryCompleteAfter ? "" : " ") +
|
||||||
|
wordsAfter.slice(0, numberOfWordsToTake).join(" ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
indexInSearchQuery: wordsBeforeAsString.length,
|
||||||
|
previewText: wordsBeforeAsString + searchQuery + wordsAfterAsString,
|
||||||
|
moreBefore: startWordIndex > 0,
|
||||||
|
moreAfter: wordsAfter.length > numberOfWordsToTake,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeWrappedText = (
|
||||||
|
wrappedText: string,
|
||||||
|
originalText: string,
|
||||||
|
): string => {
|
||||||
|
const wrappedLines = wrappedText.split("\n");
|
||||||
|
const normalizedLines: string[] = [];
|
||||||
|
let originalIndex = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < wrappedLines.length; i++) {
|
||||||
|
let currentLine = wrappedLines[i];
|
||||||
|
const nextLine = wrappedLines[i + 1];
|
||||||
|
|
||||||
|
if (nextLine) {
|
||||||
|
const nextLineIndexInOriginal = originalText.indexOf(
|
||||||
|
nextLine,
|
||||||
|
originalIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextLineIndexInOriginal > currentLine.length + originalIndex) {
|
||||||
|
let j = nextLineIndexInOriginal - (currentLine.length + originalIndex);
|
||||||
|
|
||||||
|
while (j > 0) {
|
||||||
|
currentLine += " ";
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedLines.push(currentLine);
|
||||||
|
originalIndex = originalIndex + currentLine.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedLines.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMatchedLines = (
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
searchQuery: SearchQuery,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
const normalizedText = normalizeWrappedText(
|
||||||
|
textElement.text,
|
||||||
|
textElement.originalText,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = normalizedText.split("\n");
|
||||||
|
|
||||||
|
const lineIndexRanges = [];
|
||||||
|
let currentIndex = 0;
|
||||||
|
let lineNumber = 0;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const startIndex = currentIndex;
|
||||||
|
const endIndex = startIndex + line.length - 1;
|
||||||
|
|
||||||
|
lineIndexRanges.push({
|
||||||
|
line,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
lineNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to the next line's start index
|
||||||
|
currentIndex = endIndex + 1;
|
||||||
|
lineNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startIndex = index;
|
||||||
|
let remainingQuery = textElement.originalText.slice(
|
||||||
|
index,
|
||||||
|
index + searchQuery.length,
|
||||||
|
);
|
||||||
|
const matchedLines: {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const lineIndexRange of lineIndexRanges) {
|
||||||
|
if (remainingQuery === "") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
startIndex >= lineIndexRange.startIndex &&
|
||||||
|
startIndex <= lineIndexRange.endIndex
|
||||||
|
) {
|
||||||
|
const matchCapacity = lineIndexRange.endIndex + 1 - startIndex;
|
||||||
|
const textToStart = lineIndexRange.line.slice(
|
||||||
|
0,
|
||||||
|
startIndex - lineIndexRange.startIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchedWord = remainingQuery.slice(0, matchCapacity);
|
||||||
|
remainingQuery = remainingQuery.slice(matchCapacity);
|
||||||
|
|
||||||
|
const offset = measureText(
|
||||||
|
textToStart,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// measureText returns a non-zero width for the empty string
|
||||||
|
// which is not what we're after here, hence the check and the correction
|
||||||
|
if (textToStart === "") {
|
||||||
|
offset.width = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textElement.textAlign !== "left" && lineIndexRange.line.length > 0) {
|
||||||
|
const lineLength = measureText(
|
||||||
|
lineIndexRange.line,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const spaceToStart =
|
||||||
|
textElement.textAlign === "center"
|
||||||
|
? (textElement.width - lineLength.width) / 2
|
||||||
|
: textElement.width - lineLength.width;
|
||||||
|
offset.width += spaceToStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = measureText(
|
||||||
|
matchedWord,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const offsetX = offset.width;
|
||||||
|
const offsetY = lineIndexRange.lineNumber * offset.height;
|
||||||
|
|
||||||
|
matchedLines.push({
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
startIndex += matchCapacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedLines;
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeSpecialCharacters = (string: string) => {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = debounce(
|
||||||
|
(
|
||||||
|
searchQuery: SearchQuery,
|
||||||
|
app: AppClassProperties,
|
||||||
|
cb: (matchItems: SearchMatchItem[], focusIndex: number | null) => void,
|
||||||
|
) => {
|
||||||
|
if (!searchQuery || searchQuery === "") {
|
||||||
|
cb([], null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = app.scene.getNonDeletedElements();
|
||||||
|
const texts = elements.filter((el) =>
|
||||||
|
isTextElement(el),
|
||||||
|
) as ExcalidrawTextElement[];
|
||||||
|
|
||||||
|
texts.sort((a, b) => a.y - b.y);
|
||||||
|
|
||||||
|
const matchItems: SearchMatchItem[] = [];
|
||||||
|
|
||||||
|
const regex = new RegExp(escapeSpecialCharacters(searchQuery), "gi");
|
||||||
|
|
||||||
|
for (const textEl of texts) {
|
||||||
|
let match = null;
|
||||||
|
const text = textEl.originalText;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const preview = getMatchPreview(text, match.index, searchQuery);
|
||||||
|
const matchedLines = getMatchedLines(textEl, searchQuery, match.index);
|
||||||
|
|
||||||
|
if (matchedLines.length > 0) {
|
||||||
|
matchItems.push({
|
||||||
|
textElement: textEl,
|
||||||
|
searchQuery,
|
||||||
|
preview,
|
||||||
|
index: match.index,
|
||||||
|
matchedLines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleIds = new Set(
|
||||||
|
app.visibleElements.map((visibleElement) => visibleElement.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const focusIndex =
|
||||||
|
matchItems.findIndex((matchItem) =>
|
||||||
|
visibleIds.has(matchItem.textElement.id),
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
cb(matchItems, focusIndex);
|
||||||
|
},
|
||||||
|
SEARCH_DEBOUNCE,
|
||||||
|
);
|
||||||
@@ -3,16 +3,29 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
--ExcTextField--color: var(--color-on-surface);
|
--ExcTextField--color: var(--color-on-surface);
|
||||||
--ExcTextField--label-color: var(--color-on-surface);
|
--ExcTextField--label-color: var(--color-on-surface);
|
||||||
--ExcTextField--background: transparent;
|
--ExcTextField--background: var(--color-surface-low);
|
||||||
--ExcTextField--readonly--background: var(--color-surface-high);
|
--ExcTextField--readonly--background: var(--color-surface-high);
|
||||||
--ExcTextField--readonly--color: var(--color-on-surface);
|
--ExcTextField--readonly--color: var(--color-on-surface);
|
||||||
--ExcTextField--border: var(--color-border-outline);
|
--ExcTextField--border: var(--color-gray-20);
|
||||||
--ExcTextField--readonly--border: var(--color-border-outline-variant);
|
--ExcTextField--readonly--border: var(--color-border-outline-variant);
|
||||||
--ExcTextField--border-hover: var(--color-brand-hover);
|
--ExcTextField--border-hover: var(--color-brand-hover);
|
||||||
--ExcTextField--border-active: var(--color-brand-active);
|
--ExcTextField--border-active: var(--color-brand-active);
|
||||||
--ExcTextField--placeholder: var(--color-border-outline-variant);
|
--ExcTextField--placeholder: var(--color-border-outline-variant);
|
||||||
|
|
||||||
.ExcTextField {
|
.ExcTextField {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; // 50% is not exactly in the center of the input
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: 0.75rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&--fullWidth {
|
&--fullWidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -37,7 +50,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 1rem;
|
|
||||||
|
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
|
||||||
@@ -45,6 +57,8 @@
|
|||||||
border: 1px solid var(--ExcTextField--border);
|
border: 1px solid var(--ExcTextField--border);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
|
||||||
&:not(&--readonly) {
|
&:not(&--readonly) {
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--ExcTextField--border-hover);
|
border-color: var(--ExcTextField--border-hover);
|
||||||
@@ -80,10 +94,6 @@
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--ExcTextField--placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:focus) {
|
&:not(:focus) {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: initial;
|
background-color: initial;
|
||||||
@@ -105,5 +115,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--hasIcon .ExcTextField__input {
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ type TextFieldProps = {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
selectOnRender?: boolean;
|
selectOnRender?: boolean;
|
||||||
|
|
||||||
|
icon?: React.ReactNode;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isRedacted?: boolean;
|
isRedacted?: boolean;
|
||||||
} & ({ value: string } | { defaultValue: string });
|
} & ({ value: string } | { defaultValue: string });
|
||||||
@@ -37,6 +39,8 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
selectOnRender,
|
selectOnRender,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
isRedacted = false,
|
isRedacted = false,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -47,6 +51,8 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (selectOnRender) {
|
if (selectOnRender) {
|
||||||
|
// focusing first is needed because vitest/jsdom
|
||||||
|
innerRef.current?.focus();
|
||||||
innerRef.current?.select();
|
innerRef.current?.select();
|
||||||
}
|
}
|
||||||
}, [selectOnRender]);
|
}, [selectOnRender]);
|
||||||
@@ -56,14 +62,16 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("ExcTextField", {
|
className={clsx("ExcTextField", className, {
|
||||||
"ExcTextField--fullWidth": fullWidth,
|
"ExcTextField--fullWidth": fullWidth,
|
||||||
|
"ExcTextField--hasIcon": !!icon,
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
innerRef.current?.focus();
|
innerRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="ExcTextField__label">{label}</div>
|
{icon}
|
||||||
|
{label && <div className="ExcTextField__label">{label}</div>}
|
||||||
<div
|
<div
|
||||||
className={clsx("ExcTextField__input", {
|
className={clsx("ExcTextField__input", {
|
||||||
"ExcTextField__input--readonly": readonly,
|
"ExcTextField__input--readonly": readonly,
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ const getRelevantAppStateProps = (
|
|||||||
snapLines: appState.snapLines,
|
snapLines: appState.snapLines,
|
||||||
zenModeEnabled: appState.zenModeEnabled,
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
editingTextElement: appState.editingTextElement,
|
editingTextElement: appState.editingTextElement,
|
||||||
|
searchMatches: appState.searchMatches,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
|||||||
@@ -2139,3 +2139,11 @@ export const collapseUpIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const upIcon = createIcon(
|
||||||
|
<g>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M6 15l6 -6l6 6" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
LoadIcon,
|
LoadIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
save,
|
save,
|
||||||
|
searchIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
usersIcon,
|
usersIcon,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
actionLoadScene,
|
actionLoadScene,
|
||||||
actionSaveToActiveFile,
|
actionSaveToActiveFile,
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
|
actionToggleSearchMenu,
|
||||||
actionToggleTheme,
|
actionToggleTheme,
|
||||||
} from "../../actions";
|
} from "../../actions";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -40,7 +42,6 @@ import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemConten
|
|||||||
import { THEME } from "../../constants";
|
import { THEME } from "../../constants";
|
||||||
import type { Theme } from "../../element/types";
|
import type { Theme } from "../../element/types";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
|
|
||||||
import "./DefaultItems.scss";
|
import "./DefaultItems.scss";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
@@ -145,6 +146,27 @@ export const CommandPalette = (opts?: { className?: string }) => {
|
|||||||
};
|
};
|
||||||
CommandPalette.displayName = "CommandPalette";
|
CommandPalette.displayName = "CommandPalette";
|
||||||
|
|
||||||
|
export const SearchMenu = (opts?: { className?: string }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={searchIcon}
|
||||||
|
data-testid="search-menu-button"
|
||||||
|
onSelect={() => {
|
||||||
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
|
}}
|
||||||
|
shortcut={getShortcutFromShortcutName("searchMenu")}
|
||||||
|
aria-label={t("search.title")}
|
||||||
|
className={opts?.className}
|
||||||
|
>
|
||||||
|
{t("search.title")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
SearchMenu.displayName = "SearchMenu";
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export const ENV = {
|
|||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -376,6 +377,7 @@ export const DEFAULT_ELEMENT_PROPS: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LIBRARY_SIDEBAR_TAB = "library";
|
export const LIBRARY_SIDEBAR_TAB = "library";
|
||||||
|
export const CANVAS_SEARCH_TAB = "search";
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR = {
|
export const DEFAULT_SIDEBAR = {
|
||||||
name: "default",
|
name: "default",
|
||||||
|
|||||||
@@ -144,9 +144,9 @@
|
|||||||
--border-radius-md: 0.375rem;
|
--border-radius-md: 0.375rem;
|
||||||
--border-radius-lg: 0.5rem;
|
--border-radius-lg: 0.5rem;
|
||||||
|
|
||||||
--color-surface-high: hsl(244, 100%, 97%);
|
--color-surface-high: #f1f0ff;
|
||||||
--color-surface-mid: hsl(240 25% 96%);
|
--color-surface-mid: #f2f2f7;
|
||||||
--color-surface-low: hsl(240 25% 94%);
|
--color-surface-low: #ececf4;
|
||||||
--color-surface-lowest: #ffffff;
|
--color-surface-lowest: #ffffff;
|
||||||
--color-on-surface: #1b1b1f;
|
--color-on-surface: #1b1b1f;
|
||||||
--color-brand-hover: #5753d0;
|
--color-brand-hover: #5753d0;
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "#d8f5a2",
|
"backgroundColor": "#d8f5a2",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id45",
|
"id": "id47",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "id46",
|
"id": "id48",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id46",
|
"id": "id48",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id47",
|
"elementId": "id49",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": -0.08139534883720931,
|
"focus": -0.08139534883720931,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id45",
|
"id": "id47",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id48",
|
"id": "id50",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id48",
|
"id": "id50",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id49",
|
"id": "id51",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id48",
|
"containerId": "id50",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id38",
|
"id": "id40",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id40",
|
"elementId": "id42",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id39",
|
"elementId": "id41",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id37",
|
"containerId": "id39",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id37",
|
"id": "id39",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id37",
|
"id": "id39",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id42",
|
"id": "id44",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id44",
|
"elementId": "id46",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id43",
|
"elementId": "id45",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id41",
|
"containerId": "id43",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id41",
|
"id": "id43",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id41",
|
"id": "id43",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id54",
|
"id": "id56",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id55",
|
"id": "id57",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id56",
|
"id": "id58",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id57",
|
"id": "id59",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id58",
|
"id": "id60",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id59",
|
"id": "id61",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ export const base64ToString = async (base64: string, isByteString = false) => {
|
|||||||
: byteStringToString(window.atob(base64));
|
: byteStringToString(window.atob(base64));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const base64ToArrayBuffer = (base64: string): ArrayBuffer => {
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
// Node.js environment
|
||||||
|
return Buffer.from(base64, "base64").buffer;
|
||||||
|
}
|
||||||
|
// Browser environment
|
||||||
|
return byteStringToArrayBuffer(atob(base64));
|
||||||
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// text encoding
|
// text encoding
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
FixedPointBinding,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
PointBinding,
|
PointBinding,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
isFixedPointBinding,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
@@ -101,8 +103,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
|
|
||||||
const repairBinding = (
|
const repairBinding = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
binding: PointBinding | null,
|
binding: PointBinding | FixedPointBinding | null,
|
||||||
): PointBinding | null => {
|
): PointBinding | FixedPointBinding | null => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -110,9 +112,11 @@ const repairBinding = (
|
|||||||
return {
|
return {
|
||||||
...binding,
|
...binding,
|
||||||
focus: binding.focus || 0,
|
focus: binding.focus || 0,
|
||||||
fixedPoint: isElbowArrow(element)
|
...(isElbowArrow(element) && isFixedPointBinding(binding)
|
||||||
? normalizeFixedPoint(binding.fixedPoint ?? [0, 0])
|
? {
|
||||||
: null,
|
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -309,28 +309,32 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Test Frames", () => {
|
describe("Test Frames", () => {
|
||||||
|
const elements: ExcalidrawElementSkeleton[] = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
it("should transform frames and update frame ids when regenerated", () => {
|
it("should transform frames and update frame ids when regenerated", () => {
|
||||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
{
|
...elements,
|
||||||
type: "rectangle",
|
|
||||||
x: 10,
|
|
||||||
y: 10,
|
|
||||||
strokeWidth: 2,
|
|
||||||
id: "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 120,
|
|
||||||
y: 20,
|
|
||||||
backgroundColor: "#fff3bf",
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
text: "HELLO EXCALIDRAW",
|
|
||||||
strokeColor: "#099268",
|
|
||||||
fontSize: 30,
|
|
||||||
},
|
|
||||||
id: "2",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "frame",
|
type: "frame",
|
||||||
children: ["1", "2"],
|
children: ["1", "2"],
|
||||||
@@ -352,28 +356,9 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should consider max of calculated and frame dimensions when provided", () => {
|
it("should consider user defined frame dimensions over calculated when provided", () => {
|
||||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
{
|
...elements,
|
||||||
type: "rectangle",
|
|
||||||
x: 10,
|
|
||||||
y: 10,
|
|
||||||
strokeWidth: 2,
|
|
||||||
id: "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 120,
|
|
||||||
y: 20,
|
|
||||||
backgroundColor: "#fff3bf",
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
text: "HELLO EXCALIDRAW",
|
|
||||||
strokeColor: "#099268",
|
|
||||||
fontSize: 30,
|
|
||||||
},
|
|
||||||
id: "2",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "frame",
|
type: "frame",
|
||||||
children: ["1", "2"],
|
children: ["1", "2"],
|
||||||
@@ -388,7 +373,27 @@ describe("Test Transform", () => {
|
|||||||
);
|
);
|
||||||
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
||||||
expect(frame.width).toBe(800);
|
expect(frame.width).toBe(800);
|
||||||
expect(frame.height).toBe(126);
|
expect(frame.height).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should consider user defined frame coordinates calculated when provided", () => {
|
||||||
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
|
...elements,
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
x: 100,
|
||||||
|
y: 300,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excalidrawElements = convertToExcalidrawElements(
|
||||||
|
elementsSkeleton,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
||||||
|
expect(frame.x).toBe(100);
|
||||||
|
expect(frame.y).toBe(300);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
assertNever,
|
assertNever,
|
||||||
cloneJSON,
|
cloneJSON,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
isDevEnv,
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
@@ -717,7 +718,7 @@ export const convertToExcalidrawElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Once all the excalidraw elements are created, we can add frames since we
|
// Once all the excalidraw elements are created, we can add frames since we
|
||||||
// need to calculate coordinates and dimensions of frame which is possibe after all
|
// need to calculate coordinates and dimensions of frame which is possible after all
|
||||||
// frame children are processed.
|
// frame children are processed.
|
||||||
for (const [id, element] of elementsWithIds) {
|
for (const [id, element] of elementsWithIds) {
|
||||||
if (element.type !== "frame" && element.type !== "magicframe") {
|
if (element.type !== "frame" && element.type !== "magicframe") {
|
||||||
@@ -764,10 +765,26 @@ export const convertToExcalidrawElements = (
|
|||||||
maxX = maxX + PADDING;
|
maxX = maxX + PADDING;
|
||||||
maxY = maxY + PADDING;
|
maxY = maxY + PADDING;
|
||||||
|
|
||||||
// Take the max of calculated and provided frame dimensions, whichever is higher
|
const frameX = frame?.x || minX;
|
||||||
const width = Math.max(frame?.width, maxX - minX);
|
const frameY = frame?.y || minY;
|
||||||
const height = Math.max(frame?.height, maxY - minY);
|
const frameWidth = frame?.width || maxX - minX;
|
||||||
Object.assign(frame, { x: minX, y: minY, width, height });
|
const frameHeight = frame?.height || maxY - minY;
|
||||||
|
|
||||||
|
Object.assign(frame, {
|
||||||
|
x: frameX,
|
||||||
|
y: frameY,
|
||||||
|
width: frameWidth,
|
||||||
|
height: frameHeight,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
isDevEnv() &&
|
||||||
|
element.children.length &&
|
||||||
|
(frame?.x || frame?.y || frame?.width || frame?.height)
|
||||||
|
) {
|
||||||
|
console.info(
|
||||||
|
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return elementStore.getElements();
|
return elementStore.getElements();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
isBindingElement,
|
isBindingElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
isFixedPointBinding,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isRectangularElement,
|
isRectangularElement,
|
||||||
@@ -797,7 +798,7 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
isVertical
|
isVertical
|
||||||
? Math.abs(p[1] - i[1]) < 0.1
|
? Math.abs(p[1] - i[1]) < 0.1
|
||||||
: Math.abs(p[0] - i[0]) < 0.1,
|
: Math.abs(p[0] - i[0]) < 0.1,
|
||||||
)[0] ?? point;
|
)[0] ?? p;
|
||||||
}
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
@@ -1013,7 +1014,7 @@ const updateBoundPoint = (
|
|||||||
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
||||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||||
|
|
||||||
if (isElbowArrow(linearElement)) {
|
if (isElbowArrow(linearElement) && isFixedPointBinding(binding)) {
|
||||||
const fixedPoint =
|
const fixedPoint =
|
||||||
normalizeFixedPoint(binding.fixedPoint) ??
|
normalizeFixedPoint(binding.fixedPoint) ??
|
||||||
calculateFixedPointForElbowArrowBinding(
|
calculateFixedPointForElbowArrowBinding(
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const dragSelectedElements = (
|
|||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
_selectedElements.length === 1 &&
|
_selectedElements.length === 1 &&
|
||||||
isArrowElement(_selectedElements[0]) &&
|
|
||||||
isElbowArrow(_selectedElements[0]) &&
|
isElbowArrow(_selectedElements[0]) &&
|
||||||
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
|
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
|
||||||
) {
|
) {
|
||||||
@@ -43,13 +42,7 @@ export const dragSelectedElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = _selectedElements.filter(
|
const selectedElements = _selectedElements.filter(
|
||||||
(el) =>
|
(el) => !(isElbowArrow(el) && el.startBinding && el.endBinding),
|
||||||
!(
|
|
||||||
isArrowElement(el) &&
|
|
||||||
isElbowArrow(el) &&
|
|
||||||
el.startBinding &&
|
|
||||||
el.endBinding
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// we do not want a frame and its elements to be selected at the same time
|
// we do not want a frame and its elements to be selected at the same time
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ const RE_GENERIC_EMBED =
|
|||||||
const RE_GIPHY =
|
const RE_GIPHY =
|
||||||
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
||||||
|
|
||||||
|
const RE_REDDIT =
|
||||||
|
/^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/;
|
||||||
|
|
||||||
|
const RE_REDDIT_EMBED =
|
||||||
|
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
@@ -59,6 +65,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
"giphy.com",
|
"giphy.com",
|
||||||
|
"reddit.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ALLOW_SAME_ORIGIN = new Set([
|
const ALLOW_SAME_ORIGIN = new Set([
|
||||||
@@ -71,6 +78,7 @@ const ALLOW_SAME_ORIGIN = new Set([
|
|||||||
"x.com",
|
"x.com",
|
||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
|
"reddit.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const createSrcDoc = (body: string) => {
|
export const createSrcDoc = (body: string) => {
|
||||||
@@ -218,6 +226,24 @@ export const getEmbedLink = (
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RE_REDDIT.test(link)) {
|
||||||
|
const [, page, postId, title] = link.match(RE_REDDIT)!;
|
||||||
|
const safeURL = sanitizeHTMLAttribute(
|
||||||
|
`https://reddit.com/r/${page}/comments/${postId}/${title}`,
|
||||||
|
);
|
||||||
|
const ret: IframeDataWithSandbox = {
|
||||||
|
type: "document",
|
||||||
|
srcdoc: (theme: string) =>
|
||||||
|
createSrcDoc(
|
||||||
|
`<blockquote class="reddit-embed-bq" data-embed-theme="${theme}"><a href="${safeURL}"></a><br></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>`,
|
||||||
|
),
|
||||||
|
intrinsicSize: { w: 480, h: 480 },
|
||||||
|
sandbox: { allowSameOrigin },
|
||||||
|
};
|
||||||
|
embeddedLinkCache.set(originalLink, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
if (RE_GH_GIST.test(link)) {
|
if (RE_GH_GIST.test(link)) {
|
||||||
const [, user, gistId] = link.match(RE_GH_GIST)!;
|
const [, user, gistId] = link.match(RE_GH_GIST)!;
|
||||||
const safeURL = sanitizeHTMLAttribute(
|
const safeURL = sanitizeHTMLAttribute(
|
||||||
@@ -361,6 +387,11 @@ export const maybeParseEmbedSrc = (str: string): string => {
|
|||||||
return twitterMatch[1];
|
return twitterMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const redditMatch = str.match(RE_REDDIT_EMBED);
|
||||||
|
if (redditMatch && redditMatch.length === 2) {
|
||||||
|
return redditMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
||||||
if (gistMatch && gistMatch.length === 2) {
|
if (gistMatch && gistMatch.length === 2) {
|
||||||
return gistMatch[1];
|
return gistMatch[1];
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export class LinearElementEditor {
|
|||||||
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||||
public readonly hoverPointIndex: number;
|
public readonly hoverPointIndex: number;
|
||||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
|
public readonly elbowed: boolean;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
@@ -131,6 +132,7 @@ export class LinearElementEditor {
|
|||||||
};
|
};
|
||||||
this.hoverPointIndex = -1;
|
this.hoverPointIndex = -1;
|
||||||
this.segmentMidPointHoveredCoords = null;
|
this.segmentMidPointHoveredCoords = null;
|
||||||
|
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1477,7 +1479,9 @@ export class LinearElementEditor {
|
|||||||
nextPoints,
|
nextPoints,
|
||||||
vector(offsetX, offsetY),
|
vector(offsetX, offsetY),
|
||||||
bindings,
|
bindings,
|
||||||
options,
|
{
|
||||||
|
isDragging: options?.isDragging,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ export const newTextElement = (
|
|||||||
verticalAlign?: VerticalAlign;
|
verticalAlign?: VerticalAlign;
|
||||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
|
||||||
autoResize?: ExcalidrawTextElement["autoResize"];
|
autoResize?: ExcalidrawTextElement["autoResize"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -909,6 +910,8 @@ export const resizeMultipleElements = (
|
|||||||
fontSize?: ExcalidrawTextElement["fontSize"];
|
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
scale?: ExcalidrawImageElement["scale"];
|
scale?: ExcalidrawImageElement["scale"];
|
||||||
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
|
startBinding?: ExcalidrawArrowElement["startBinding"];
|
||||||
|
endBinding?: ExcalidrawArrowElement["endBinding"];
|
||||||
};
|
};
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
@@ -993,19 +996,6 @@ export const resizeMultipleElements = (
|
|||||||
|
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, update, false);
|
||||||
|
|
||||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
|
||||||
mutateElbowArrow(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
element.points,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
informMutation: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
updateBoundElements(element, elementsMap, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
oldSize: { width: oldWidth, height: oldHeight },
|
oldSize: { width: oldWidth, height: oldHeight },
|
||||||
@@ -1059,7 +1049,7 @@ const rotateMultipleElements = (
|
|||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
const points = getArrowLocalFixedPoints(element, elementsMap);
|
const points = getArrowLocalFixedPoints(element, elementsMap);
|
||||||
mutateElbowArrow(element, elementsMap, points);
|
mutateElbowArrow(element, elementsMap, points);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -94,7 +94,16 @@ describe("elbow arrow routing", () => {
|
|||||||
|
|
||||||
describe("elbow arrow ui", () => {
|
describe("elbow arrow ui", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = UI.queryContextMenu();
|
||||||
|
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can follow bound shapes", async () => {
|
it("can follow bound shapes", async () => {
|
||||||
@@ -130,8 +139,8 @@ describe("elbow arrow ui", () => {
|
|||||||
expect(arrow.elbowed).toBe(true);
|
expect(arrow.elbowed).toBe(true);
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[45, 0],
|
||||||
[35, 200],
|
[45, 200],
|
||||||
[90, 200],
|
[90, 200],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -163,14 +172,6 @@ describe("elbow arrow ui", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawArrowElement;
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
|
||||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
|
||||||
|
|
||||||
mouse.click(51, 51);
|
mouse.click(51, 51);
|
||||||
|
|
||||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||||
@@ -182,8 +183,8 @@ describe("elbow arrow ui", () => {
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[35, 0],
|
||||||
[35, 90],
|
[35, 90],
|
||||||
[25, 90],
|
[35, 90], // Note that coordinates are rounded above!
|
||||||
[25, 165],
|
[35, 165],
|
||||||
[103, 165],
|
[103, 165],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ import {
|
|||||||
HEADING_UP,
|
HEADING_UP,
|
||||||
vectorToHeading,
|
vectorToHeading,
|
||||||
} from "./heading";
|
} from "./heading";
|
||||||
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPointBinding,
|
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -72,16 +72,48 @@ export const mutateElbowArrow = (
|
|||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offset?: Vector,
|
offset?: Vector,
|
||||||
otherUpdates?: {
|
otherUpdates?: Omit<
|
||||||
startBinding?: FixedPointBinding | null;
|
ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||||
endBinding?: FixedPointBinding | null;
|
"angle" | "x" | "y" | "width" | "height" | "elbowed" | "points"
|
||||||
|
>,
|
||||||
|
options?: {
|
||||||
|
isDragging?: boolean;
|
||||||
|
informMutation?: boolean;
|
||||||
},
|
},
|
||||||
|
) => {
|
||||||
|
const update = updateElbowArrow(
|
||||||
|
arrow,
|
||||||
|
elementsMap,
|
||||||
|
nextPoints,
|
||||||
|
offset,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (update) {
|
||||||
|
mutateElement(
|
||||||
|
arrow,
|
||||||
|
{
|
||||||
|
...otherUpdates,
|
||||||
|
...update,
|
||||||
|
angle: 0 as Radians,
|
||||||
|
},
|
||||||
|
options?.informMutation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("Elbow arrow cannot find a route");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateElbowArrow = (
|
||||||
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
nextPoints: readonly LocalPoint[],
|
||||||
|
offset?: Vector,
|
||||||
options?: {
|
options?: {
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
disableBinding?: boolean;
|
disableBinding?: boolean;
|
||||||
informMutation?: boolean;
|
informMutation?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
|
||||||
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
||||||
pointTranslate<LocalPoint, GlobalPoint>(
|
pointTranslate<LocalPoint, GlobalPoint>(
|
||||||
nextPoints[0],
|
nextPoints[0],
|
||||||
@@ -235,6 +267,8 @@ export const mutateElbowArrow = (
|
|||||||
BASE_PADDING,
|
BASE_PADDING,
|
||||||
),
|
),
|
||||||
boundsOverlap,
|
boundsOverlap,
|
||||||
|
hoveredStartElement && aabbForElement(hoveredStartElement),
|
||||||
|
hoveredEndElement && aabbForElement(hoveredEndElement),
|
||||||
);
|
);
|
||||||
const startDonglePosition = getDonglePosition(
|
const startDonglePosition = getDonglePosition(
|
||||||
dynamicAABBs[0],
|
dynamicAABBs[0],
|
||||||
@@ -295,18 +329,10 @@ export const mutateElbowArrow = (
|
|||||||
startDongle && points.unshift(startGlobalPoint);
|
startDongle && points.unshift(startGlobalPoint);
|
||||||
endDongle && points.push(endGlobalPoint);
|
endDongle && points.push(endGlobalPoint);
|
||||||
|
|
||||||
mutateElement(
|
return normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0);
|
||||||
arrow,
|
|
||||||
{
|
|
||||||
...otherUpdates,
|
|
||||||
...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0),
|
|
||||||
angle: 0 as Radians,
|
|
||||||
},
|
|
||||||
options?.informMutation,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("Elbow arrow cannot find a route");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetFromHeading = (
|
const offsetFromHeading = (
|
||||||
@@ -475,7 +501,11 @@ const generateDynamicAABBs = (
|
|||||||
startDifference?: [number, number, number, number],
|
startDifference?: [number, number, number, number],
|
||||||
endDifference?: [number, number, number, number],
|
endDifference?: [number, number, number, number],
|
||||||
disableSideHack?: boolean,
|
disableSideHack?: boolean,
|
||||||
|
startElementBounds?: Bounds | null,
|
||||||
|
endElementBounds?: Bounds | null,
|
||||||
): Bounds[] => {
|
): Bounds[] => {
|
||||||
|
const startEl = startElementBounds ?? a;
|
||||||
|
const endEl = endElementBounds ?? b;
|
||||||
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
|
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
|
||||||
0, 0, 0, 0,
|
0, 0, 0, 0,
|
||||||
];
|
];
|
||||||
@@ -484,29 +514,29 @@ const generateDynamicAABBs = (
|
|||||||
const first = [
|
const first = [
|
||||||
a[0] > b[2]
|
a[0] > b[2]
|
||||||
? a[1] > b[3] || a[3] < b[1]
|
? a[1] > b[3] || a[3] < b[1]
|
||||||
? Math.min((a[0] + b[2]) / 2, a[0] - startLeft)
|
? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft)
|
||||||
: (a[0] + b[2]) / 2
|
: (startEl[0] + endEl[2]) / 2
|
||||||
: a[0] > b[0]
|
: a[0] > b[0]
|
||||||
? a[0] - startLeft
|
? a[0] - startLeft
|
||||||
: common[0] - startLeft,
|
: common[0] - startLeft,
|
||||||
a[1] > b[3]
|
a[1] > b[3]
|
||||||
? a[0] > b[2] || a[2] < b[0]
|
? a[0] > b[2] || a[2] < b[0]
|
||||||
? Math.min((a[1] + b[3]) / 2, a[1] - startUp)
|
? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp)
|
||||||
: (a[1] + b[3]) / 2
|
: (startEl[1] + endEl[3]) / 2
|
||||||
: a[1] > b[1]
|
: a[1] > b[1]
|
||||||
? a[1] - startUp
|
? a[1] - startUp
|
||||||
: common[1] - startUp,
|
: common[1] - startUp,
|
||||||
a[2] < b[0]
|
a[2] < b[0]
|
||||||
? a[1] > b[3] || a[3] < b[1]
|
? a[1] > b[3] || a[3] < b[1]
|
||||||
? Math.max((a[2] + b[0]) / 2, a[2] + startRight)
|
? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight)
|
||||||
: (a[2] + b[0]) / 2
|
: (startEl[2] + endEl[0]) / 2
|
||||||
: a[2] < b[2]
|
: a[2] < b[2]
|
||||||
? a[2] + startRight
|
? a[2] + startRight
|
||||||
: common[2] + startRight,
|
: common[2] + startRight,
|
||||||
a[3] < b[1]
|
a[3] < b[1]
|
||||||
? a[0] > b[2] || a[2] < b[0]
|
? a[0] > b[2] || a[2] < b[0]
|
||||||
? Math.max((a[3] + b[1]) / 2, a[3] + startDown)
|
? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown)
|
||||||
: (a[3] + b[1]) / 2
|
: (startEl[3] + endEl[1]) / 2
|
||||||
: a[3] < b[3]
|
: a[3] < b[3]
|
||||||
? a[3] + startDown
|
? a[3] + startDown
|
||||||
: common[3] + startDown,
|
: common[3] + startDown,
|
||||||
@@ -514,29 +544,29 @@ const generateDynamicAABBs = (
|
|||||||
const second = [
|
const second = [
|
||||||
b[0] > a[2]
|
b[0] > a[2]
|
||||||
? b[1] > a[3] || b[3] < a[1]
|
? b[1] > a[3] || b[3] < a[1]
|
||||||
? Math.min((b[0] + a[2]) / 2, b[0] - endLeft)
|
? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft)
|
||||||
: (b[0] + a[2]) / 2
|
: (endEl[0] + startEl[2]) / 2
|
||||||
: b[0] > a[0]
|
: b[0] > a[0]
|
||||||
? b[0] - endLeft
|
? b[0] - endLeft
|
||||||
: common[0] - endLeft,
|
: common[0] - endLeft,
|
||||||
b[1] > a[3]
|
b[1] > a[3]
|
||||||
? b[0] > a[2] || b[2] < a[0]
|
? b[0] > a[2] || b[2] < a[0]
|
||||||
? Math.min((b[1] + a[3]) / 2, b[1] - endUp)
|
? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp)
|
||||||
: (b[1] + a[3]) / 2
|
: (endEl[1] + startEl[3]) / 2
|
||||||
: b[1] > a[1]
|
: b[1] > a[1]
|
||||||
? b[1] - endUp
|
? b[1] - endUp
|
||||||
: common[1] - endUp,
|
: common[1] - endUp,
|
||||||
b[2] < a[0]
|
b[2] < a[0]
|
||||||
? b[1] > a[3] || b[3] < a[1]
|
? b[1] > a[3] || b[3] < a[1]
|
||||||
? Math.max((b[2] + a[0]) / 2, b[2] + endRight)
|
? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight)
|
||||||
: (b[2] + a[0]) / 2
|
: (endEl[2] + startEl[0]) / 2
|
||||||
: b[2] < a[2]
|
: b[2] < a[2]
|
||||||
? b[2] + endRight
|
? b[2] + endRight
|
||||||
: common[2] + endRight,
|
: common[2] + endRight,
|
||||||
b[3] < a[1]
|
b[3] < a[1]
|
||||||
? b[0] > a[2] || b[2] < a[0]
|
? b[0] > a[2] || b[2] < a[0]
|
||||||
? Math.max((b[3] + a[1]) / 2, b[3] + endDown)
|
? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown)
|
||||||
: (b[3] + a[1]) / 2
|
: (endEl[3] + startEl[1]) / 2
|
||||||
: b[3] < a[3]
|
: b[3] < a[3]
|
||||||
? b[3] + endDown
|
? b[3] + endDown
|
||||||
: common[3] + endDown,
|
: common[3] + endDown,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ElementsMap, ExcalidrawElement } from "./types";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import type { AppState, Zoom } from "../types";
|
import type { AppState, Offsets, Zoom } from "../types";
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { viewportCoordsToSceneCoords } from "../utils";
|
import { viewportCoordsToSceneCoords } from "../utils";
|
||||||
|
|
||||||
@@ -67,12 +67,7 @@ export const isElementCompletelyInViewport = (
|
|||||||
scrollY: number;
|
scrollY: number;
|
||||||
},
|
},
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
padding?: Partial<{
|
padding?: Offsets,
|
||||||
top: number;
|
|
||||||
right: number;
|
|
||||||
bottom: number;
|
|
||||||
left: number;
|
|
||||||
}>,
|
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
|
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
|
||||||
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||||
|
|||||||
@@ -284,16 +284,17 @@ export const measureText = (
|
|||||||
text: string,
|
text: string,
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
forceAdvanceWidth?: true,
|
||||||
) => {
|
) => {
|
||||||
text = text
|
const _text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const fontSize = parseFloat(font);
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, fontSize, lineHeight);
|
const height = getTextHeight(_text, fontSize, lineHeight);
|
||||||
const width = getTextWidth(text, font);
|
const width = getTextWidth(_text, font, forceAdvanceWidth);
|
||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
// adding left and right padding buffer, so that browser does not cut the glyphs (does not work in Safari)
|
// adding left and right padding buffer, so that browser does not cut the glyphs (does not work in Safari)
|
||||||
const padding = !isSafari
|
const padding = !isSafari
|
||||||
? Math.ceil(updatedTextElement.fontSize / 2)
|
? Math.ceil(updatedTextElement.fontSize / appState.zoom.value / 2)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
|
|||||||
@@ -320,9 +320,12 @@ export const getDefaultRoundnessTypeForElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isFixedPointBinding = (
|
export const isFixedPointBinding = (
|
||||||
binding: PointBinding,
|
binding: PointBinding | FixedPointBinding,
|
||||||
): binding is FixedPointBinding => {
|
): binding is FixedPointBinding => {
|
||||||
return binding.fixedPoint != null;
|
return (
|
||||||
|
Object.hasOwn(binding, "fixedPoint") &&
|
||||||
|
(binding as FixedPointBinding).fixedPoint != null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move this to @excalidraw/math
|
// TODO: Move this to @excalidraw/math
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawGenericElement
|
| ExcalidrawGenericElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawLinearElement
|
| ExcalidrawLinearElement
|
||||||
|
| ExcalidrawArrowElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawFrameElement
|
| ExcalidrawFrameElement
|
||||||
@@ -268,15 +269,19 @@ export type PointBinding = {
|
|||||||
elementId: ExcalidrawBindableElement["id"];
|
elementId: ExcalidrawBindableElement["id"];
|
||||||
focus: number;
|
focus: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
// Represents the fixed point binding information in form of a vertical and
|
|
||||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
|
||||||
// gives the user selected fixed point by multiplying the bound element width
|
|
||||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
|
||||||
// bound element-local point coordinate.
|
|
||||||
fixedPoint: FixedPoint | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FixedPointBinding = Merge<PointBinding, { fixedPoint: FixedPoint }>;
|
export type FixedPointBinding = Merge<
|
||||||
|
PointBinding,
|
||||||
|
{
|
||||||
|
// Represents the fixed point binding information in form of a vertical and
|
||||||
|
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||||
|
// gives the user selected fixed point by multiplying the bound element width
|
||||||
|
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||||
|
// bound element-local point coordinate.
|
||||||
|
fixedPoint: FixedPoint;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export type Arrowhead =
|
export type Arrowhead =
|
||||||
| "arrow"
|
| "arrow"
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { stringToBase64, toByteString } from "../data/encode";
|
import {
|
||||||
|
base64ToArrayBuffer,
|
||||||
|
stringToBase64,
|
||||||
|
toByteString,
|
||||||
|
} from "../data/encode";
|
||||||
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
||||||
import loadWoff2 from "./wasm/woff2.loader";
|
import loadWoff2 from "./wasm/woff2.loader";
|
||||||
import loadHbSubset from "./wasm/hb-subset.loader";
|
import loadHbSubset from "./wasm/hb-subset.loader";
|
||||||
@@ -49,10 +53,7 @@ export class ExcalidrawFont implements Font {
|
|||||||
|
|
||||||
// it's dataurl (server), the font is inlined as base64, no need to fetch
|
// it's dataurl (server), the font is inlined as base64, no need to fetch
|
||||||
if (url.protocol === "data:") {
|
if (url.protocol === "data:") {
|
||||||
const arrayBuffer = Buffer.from(
|
const arrayBuffer = base64ToArrayBuffer(url.toString().split(",")[1]);
|
||||||
url.toString().split(",")[1],
|
|
||||||
"base64",
|
|
||||||
).buffer;
|
|
||||||
|
|
||||||
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||||
arrayBuffer,
|
arrayBuffer,
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2";
|
|||||||
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
||||||
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
||||||
|
|
||||||
import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
||||||
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
||||||
|
|
||||||
import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
||||||
import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
||||||
import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
||||||
import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
||||||
import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
|
|||||||
@@ -162,6 +162,13 @@
|
|||||||
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
|
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
|
||||||
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
|
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"title": "Find on canvas",
|
||||||
|
"noMatch": "No matches found...",
|
||||||
|
"singleResult": "result",
|
||||||
|
"multipleResults": "results",
|
||||||
|
"placeholder": "Find text on canvas..."
|
||||||
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
"exportJSON": "Export to file",
|
"exportJSON": "Export to file",
|
||||||
@@ -297,6 +304,7 @@
|
|||||||
"shapes": "Shapes"
|
"shapes": "Shapes"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
|
"dismissSearch": "Escape to dismiss search",
|
||||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ import {
|
|||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { arrayToMap, throttleRAF } from "../utils";
|
import { arrayToMap, throttleRAF } from "../utils";
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import {
|
||||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants";
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
|
FRAME_STYLE,
|
||||||
|
THEME,
|
||||||
|
} from "../constants";
|
||||||
|
import { type InteractiveCanvasAppState } from "../types";
|
||||||
|
|
||||||
import { renderSnaps } from "../renderer/renderSnaps";
|
import { renderSnaps } from "../renderer/renderSnaps";
|
||||||
|
|
||||||
@@ -48,7 +52,6 @@ import {
|
|||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -803,7 +806,6 @@ const _renderInteractiveScene = ({
|
|||||||
// Elbow arrow elements cannot be selected when bound on either end
|
// Elbow arrow elements cannot be selected when bound on either end
|
||||||
(
|
(
|
||||||
isSingleLinearElementSelected &&
|
isSingleLinearElementSelected &&
|
||||||
isArrowElement(element) &&
|
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
(element.startBinding || element.endBinding)
|
(element.startBinding || element.endBinding)
|
||||||
)
|
)
|
||||||
@@ -952,9 +954,48 @@ const _renderInteractiveScene = ({
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appState.searchMatches.forEach(({ id, focus, matchedLines }) => {
|
||||||
|
const element = elementsMap.get(id);
|
||||||
|
|
||||||
|
if (element && isTextElement(element)) {
|
||||||
|
const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
if (appState.theme === THEME.LIGHT) {
|
||||||
|
if (focus) {
|
||||||
|
context.fillStyle = "rgba(255, 124, 0, 0.4)";
|
||||||
|
} else {
|
||||||
|
context.fillStyle = "rgba(255, 226, 0, 0.4)";
|
||||||
|
}
|
||||||
|
} else if (focus) {
|
||||||
|
context.fillStyle = "rgba(229, 82, 0, 0.4)";
|
||||||
|
} else {
|
||||||
|
context.fillStyle = "rgba(99, 52, 0, 0.4)";
|
||||||
|
}
|
||||||
|
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
context.translate(cx, cy);
|
||||||
|
context.rotate(element.angle);
|
||||||
|
|
||||||
|
matchedLines.forEach((matchedLine) => {
|
||||||
|
context.fillRect(
|
||||||
|
elementX1 + matchedLine.offsetX - cx,
|
||||||
|
elementY1 + matchedLine.offsetY - cy,
|
||||||
|
matchedLine.width,
|
||||||
|
matchedLine.height,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
renderSnaps(context, appState);
|
renderSnaps(context, appState);
|
||||||
|
|
||||||
// Reset zoom
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
renderRemoteCursors({
|
renderRemoteCursors({
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ const renderElementToSvg = (
|
|||||||
image.setAttribute("width", "100%");
|
image.setAttribute("width", "100%");
|
||||||
image.setAttribute("height", "100%");
|
image.setAttribute("height", "100%");
|
||||||
image.setAttribute("href", fileData.dataURL);
|
image.setAttribute("href", fileData.dataURL);
|
||||||
|
image.setAttribute("preserveAspectRatio", "none");
|
||||||
|
|
||||||
symbol.appendChild(image);
|
symbol.appendChild(image);
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ export const exportToCanvas = async (
|
|||||||
exportingFrame ?? null,
|
exportingFrame ?? null,
|
||||||
appState.frameRendering ?? null,
|
appState.frameRendering ?? null,
|
||||||
);
|
);
|
||||||
|
// for canvas export, don't clip if exporting a specific frame as it would
|
||||||
|
// clip the corners of the content
|
||||||
|
if (exportingFrame) {
|
||||||
|
frameRendering.clip = false;
|
||||||
|
}
|
||||||
|
|
||||||
const elementsForRender = prepareElementsForRender({
|
const elementsForRender = prepareElementsForRender({
|
||||||
elements,
|
elements,
|
||||||
@@ -351,6 +356,11 @@ export const exportToSvg = async (
|
|||||||
}) rotate(${frame.angle} ${cx} ${cy})"
|
}) rotate(${frame.angle} ${cx} ${cy})"
|
||||||
width="${frame.width}"
|
width="${frame.width}"
|
||||||
height="${frame.height}"
|
height="${frame.height}"
|
||||||
|
${
|
||||||
|
exportingFrame
|
||||||
|
? ""
|
||||||
|
: `rx=${FRAME_STYLE.radius} ry=${FRAME_STYLE.radius}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
</rect>
|
</rect>
|
||||||
</clipPath>`;
|
</clipPath>`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AppState, PointerCoords, Zoom } from "../types";
|
import type { AppState, Offsets, PointerCoords, Zoom } from "../types";
|
||||||
import type { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import {
|
import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
@@ -31,14 +31,28 @@ export const centerScrollOn = ({
|
|||||||
scenePoint,
|
scenePoint,
|
||||||
viewportDimensions,
|
viewportDimensions,
|
||||||
zoom,
|
zoom,
|
||||||
|
offsets,
|
||||||
}: {
|
}: {
|
||||||
scenePoint: PointerCoords;
|
scenePoint: PointerCoords;
|
||||||
viewportDimensions: { height: number; width: number };
|
viewportDimensions: { height: number; width: number };
|
||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
|
offsets?: Offsets;
|
||||||
}) => {
|
}) => {
|
||||||
|
let scrollX =
|
||||||
|
(viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value -
|
||||||
|
scenePoint.x;
|
||||||
|
|
||||||
|
scrollX += (offsets?.left ?? 0) / 2 / zoom.value;
|
||||||
|
|
||||||
|
let scrollY =
|
||||||
|
(viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value -
|
||||||
|
scenePoint.y;
|
||||||
|
|
||||||
|
scrollY += (offsets?.top ?? 0) / 2 / zoom.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollX: viewportDimensions.width / 2 / zoom.value - scenePoint.x,
|
scrollX,
|
||||||
scrollY: viewportDimensions.height / 2 / zoom.value - scenePoint.y,
|
scrollY,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -866,6 +866,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -1068,6 +1069,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -1283,6 +1285,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -1613,6 +1616,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -1943,6 +1947,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -2158,6 +2163,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -2397,6 +2403,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0_copy": true,
|
"id0_copy": true,
|
||||||
},
|
},
|
||||||
@@ -2699,6 +2706,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -3065,6 +3073,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -3539,6 +3548,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -3861,6 +3871,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -4185,6 +4196,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -5370,6 +5382,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -6496,6 +6509,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -7431,6 +7445,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -8339,6 +8354,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -9235,6 +9251,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -239,6 +239,55 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
Ctrl+Shift+E
|
Ctrl+Shift+E
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="Find on canvas"
|
||||||
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="search-menu-button"
|
||||||
|
title="Find on canvas"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 21l-6 -6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__text"
|
||||||
|
>
|
||||||
|
Find on canvas
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__shortcut"
|
||||||
|
>
|
||||||
|
Ctrl+F
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="Help"
|
aria-label="Help"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -80,6 +80,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id166": true,
|
"id166": true,
|
||||||
},
|
},
|
||||||
@@ -681,6 +682,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id161": true,
|
"id161": true,
|
||||||
},
|
},
|
||||||
@@ -1187,6 +1189,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -1554,6 +1557,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -1922,6 +1926,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -2185,6 +2190,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id180": true,
|
"id180": true,
|
||||||
},
|
},
|
||||||
@@ -2627,6 +2633,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -2925,6 +2932,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -3208,6 +3216,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -3501,6 +3510,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -3786,6 +3796,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -4020,6 +4031,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -4278,6 +4290,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -4550,6 +4563,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -4780,6 +4794,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -5010,6 +5025,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -5238,6 +5254,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -5466,6 +5483,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -5723,6 +5741,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -6053,6 +6072,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -6477,6 +6497,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id100": true,
|
"id100": true,
|
||||||
"id101": true,
|
"id101": true,
|
||||||
@@ -6855,6 +6876,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id105": true,
|
"id105": true,
|
||||||
"id106": true,
|
"id106": true,
|
||||||
@@ -7170,6 +7192,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id113": true,
|
"id113": true,
|
||||||
},
|
},
|
||||||
@@ -7467,6 +7490,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -7695,6 +7719,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -8049,6 +8074,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -8406,6 +8432,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id127": true,
|
"id127": true,
|
||||||
"id128": true,
|
"id128": true,
|
||||||
@@ -8806,6 +8833,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -9092,6 +9120,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id124": true,
|
"id124": true,
|
||||||
},
|
},
|
||||||
@@ -9356,6 +9385,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id81": true,
|
"id81": true,
|
||||||
},
|
},
|
||||||
@@ -9619,6 +9649,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id83": true,
|
"id83": true,
|
||||||
},
|
},
|
||||||
@@ -9852,6 +9883,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -10149,6 +10181,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id92": true,
|
"id92": true,
|
||||||
},
|
},
|
||||||
@@ -10488,6 +10521,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -10725,6 +10759,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -11174,6 +11209,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id84": true,
|
"id84": true,
|
||||||
},
|
},
|
||||||
@@ -11427,6 +11463,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -11665,6 +11702,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id14": true,
|
"id14": true,
|
||||||
},
|
},
|
||||||
@@ -11905,6 +11943,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -12308,6 +12347,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": -50,
|
"scrollX": -50,
|
||||||
"scrollY": -50,
|
"scrollY": -50,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -12551,6 +12591,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id78": true,
|
"id78": true,
|
||||||
},
|
},
|
||||||
@@ -12791,6 +12832,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id19": true,
|
"id19": true,
|
||||||
},
|
},
|
||||||
@@ -13033,6 +13075,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -13277,6 +13320,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id8": true,
|
"id8": true,
|
||||||
},
|
},
|
||||||
@@ -13611,6 +13655,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -13779,6 +13824,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
@@ -14069,6 +14115,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -14334,6 +14381,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id11": true,
|
"id11": true,
|
||||||
},
|
},
|
||||||
@@ -14609,6 +14657,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -14768,6 +14817,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id55": true,
|
"id55": true,
|
||||||
},
|
},
|
||||||
@@ -15463,6 +15513,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id49": true,
|
"id49": true,
|
||||||
},
|
},
|
||||||
@@ -16082,6 +16133,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id61": true,
|
"id61": true,
|
||||||
},
|
},
|
||||||
@@ -16699,6 +16751,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id62": true,
|
"id62": true,
|
||||||
},
|
},
|
||||||
@@ -17412,6 +17465,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id69": true,
|
"id69": true,
|
||||||
"id71": true,
|
"id71": true,
|
||||||
@@ -18161,6 +18215,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id39": true,
|
"id39": true,
|
||||||
"id41": true,
|
"id41": true,
|
||||||
@@ -18634,6 +18689,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id35_copy_copy": true,
|
"id35_copy_copy": true,
|
||||||
"id36_copy_copy": true,
|
"id36_copy_copy": true,
|
||||||
@@ -19155,6 +19211,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -19610,6 +19667,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id27": true,
|
"id27": true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
@@ -495,6 +496,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -893,6 +895,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id7": true,
|
"id7": true,
|
||||||
},
|
},
|
||||||
@@ -1434,6 +1437,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -1636,6 +1640,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
@@ -2007,6 +2012,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -2241,6 +2247,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -2419,6 +2426,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -2733,6 +2741,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -2977,6 +2986,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -3216,6 +3226,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -3442,6 +3453,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -3694,6 +3706,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -3999,6 +4012,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -4412,6 +4426,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -4691,6 +4706,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -4939,6 +4955,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -5145,6 +5162,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -5338,6 +5356,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id2": true,
|
"id2": true,
|
||||||
},
|
},
|
||||||
@@ -5719,6 +5738,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -6002,6 +6022,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -6809,6 +6830,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -7134,6 +7156,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -7406,6 +7429,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -7636,6 +7660,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -7867,6 +7892,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -8043,6 +8069,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -8219,6 +8246,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -8395,12 +8423,14 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -8613,12 +8643,14 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -8830,6 +8862,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -9020,12 +9053,14 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -9238,6 +9273,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -9414,12 +9450,14 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -9631,6 +9669,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -9807,6 +9846,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -9997,6 +10037,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -10177,6 +10218,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -10685,6 +10727,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -10956,6 +10999,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"scrollX": "-6.25000",
|
"scrollX": "-6.25000",
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -11080,6 +11124,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -11276,6 +11321,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -11584,6 +11630,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -11993,6 +12040,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@@ -12600,6 +12648,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"scrollX": 60,
|
"scrollX": 60,
|
||||||
"scrollY": 60,
|
"scrollY": 60,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -12724,6 +12773,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -13305,6 +13355,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -13638,6 +13689,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@@ -13897,6 +13949,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"scrollX": 20,
|
"scrollX": 20,
|
||||||
"scrollY": "-18.53553",
|
"scrollY": "-18.53553",
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -14019,6 +14072,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@@ -14394,6 +14448,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@@ -14519,6 +14574,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { Excalidraw } from "../index";
|
|||||||
import type { NormalizedZoomValue } from "../types";
|
import type { NormalizedZoomValue } from "../types";
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import * as blob from "../data/blob";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElementPosition } from "../element/textElement";
|
import { getBoundTextElementPosition } from "../element/textElement";
|
||||||
import { createPasteEvent } from "../clipboard";
|
import { createPasteEvent } from "../clipboard";
|
||||||
@@ -33,15 +32,15 @@ import { point, type Radians } from "../../math";
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
// This needs to fixed in vitest mock, as when importActual used with mock
|
|
||||||
// the tests hangs - https://github.com/vitest-dev/vitest/issues/546.
|
|
||||||
// But fortunately spying and mocking the return value of spy works :p
|
|
||||||
|
|
||||||
const resizeImageFileSpy = vi.spyOn(blob, "resizeImageFile");
|
vi.mock("../data/blob", async (actual) => {
|
||||||
const generateIdFromFileSpy = vi.spyOn(blob, "generateIdFromFile");
|
const orig: Object = await actual();
|
||||||
|
return {
|
||||||
resizeImageFileSpy.mockImplementation(async (imageFile: File) => imageFile);
|
...orig,
|
||||||
generateIdFromFileSpy.mockImplementation(async () => "fileId" as FileId);
|
resizeImageFile: (imageFile: File) => imageFile,
|
||||||
|
generateIdFromFile: () => "fileId" as FileId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawMagicFrameElement,
|
ExcalidrawMagicFrameElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { newElement, newTextElement, newLinearElement } from "../../element";
|
import { newElement, newTextElement, newLinearElement } from "../../element";
|
||||||
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
|
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
|
||||||
@@ -76,11 +78,12 @@ export class API {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static updateElement = (
|
// eslint-disable-next-line prettier/prettier
|
||||||
...[element, updates]: Parameters<typeof mutateElement>
|
static updateElement = <T extends ExcalidrawElement>(
|
||||||
|
...args: Parameters<typeof mutateElement<T>>
|
||||||
) => {
|
) => {
|
||||||
act(() => {
|
act(() => {
|
||||||
mutateElement(element, updates);
|
mutateElement<T>(...args);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,6 +129,10 @@ export class API {
|
|||||||
expect(API.getSelectedElements().length).toBe(0);
|
expect(API.getSelectedElements().length).toBe(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static getElement = <T extends ExcalidrawElement>(element: T): T => {
|
||||||
|
return h.app.scene.getElementsMapIncludingDeleted().get(element.id) as T || element;
|
||||||
|
}
|
||||||
|
|
||||||
static createElement = <
|
static createElement = <
|
||||||
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
|
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
|
||||||
>({
|
>({
|
||||||
@@ -178,10 +185,16 @@ export class API {
|
|||||||
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
||||||
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
|
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
|
||||||
startBinding?: T extends "arrow"
|
startBinding?: T extends "arrow"
|
||||||
? ExcalidrawLinearElement["startBinding"]
|
? ExcalidrawArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"]
|
||||||
: never;
|
: never;
|
||||||
endBinding?: T extends "arrow"
|
endBinding?: T extends "arrow"
|
||||||
? ExcalidrawLinearElement["endBinding"]
|
? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"]
|
||||||
|
: never;
|
||||||
|
startArrowhead?: T extends "arrow"
|
||||||
|
? ExcalidrawArrowElement["startArrowhead"] | ExcalidrawElbowArrowElement["startArrowhead"]
|
||||||
|
: never;
|
||||||
|
endArrowhead?: T extends "arrow"
|
||||||
|
? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
|
||||||
: never;
|
: never;
|
||||||
elbowed?: boolean;
|
elbowed?: boolean;
|
||||||
}): T extends "arrow" | "line"
|
}): T extends "arrow" | "line"
|
||||||
@@ -339,6 +352,8 @@ export class API {
|
|||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
element.startBinding = rest.startBinding ?? null;
|
element.startBinding = rest.startBinding ?? null;
|
||||||
element.endBinding = rest.endBinding ?? null;
|
element.endBinding = rest.endBinding ?? null;
|
||||||
|
element.startArrowhead = rest.startArrowhead ?? null;
|
||||||
|
element.endArrowhead = rest.endArrowhead ?? null;
|
||||||
}
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
element.id = id;
|
element.id = id;
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ export class Keyboard {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static keyDown = (key: string) => {
|
static keyDown = (
|
||||||
fireEvent.keyDown(document, {
|
key: string,
|
||||||
|
target: HTMLElement | Document | Window = document,
|
||||||
|
) => {
|
||||||
|
fireEvent.keyDown(target, {
|
||||||
key,
|
key,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
@@ -78,8 +81,11 @@ export class Keyboard {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static keyUp = (key: string) => {
|
static keyUp = (
|
||||||
fireEvent.keyUp(document, {
|
key: string,
|
||||||
|
target: HTMLElement | Document | Window = document,
|
||||||
|
) => {
|
||||||
|
fireEvent.keyUp(target, {
|
||||||
key,
|
key,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
@@ -87,9 +93,9 @@ export class Keyboard {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static keyPress = (key: string) => {
|
static keyPress = (key: string, target?: HTMLElement | Document | Window) => {
|
||||||
Keyboard.keyDown(key);
|
Keyboard.keyDown(key, target);
|
||||||
Keyboard.keyUp(key);
|
Keyboard.keyUp(key, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
static codeDown = (code: string) => {
|
static codeDown = (code: string) => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
FixedPointBinding,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -2049,13 +2050,13 @@ describe("history", () => {
|
|||||||
focus: -0.001587301587301948,
|
focus: -0.001587301587301948,
|
||||||
gap: 5,
|
gap: 5,
|
||||||
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
||||||
},
|
} as FixedPointBinding,
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
||||||
focus: -0.0016129032258049847,
|
focus: -0.0016129032258049847,
|
||||||
gap: 3.537079145500037,
|
gap: 3.537079145500037,
|
||||||
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
||||||
},
|
} as FixedPointBinding,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
@@ -4455,7 +4456,7 @@ describe("history", () => {
|
|||||||
elements: [
|
elements: [
|
||||||
h.elements[0],
|
h.elements[0],
|
||||||
newElementWith(h.elements[1], { boundElements: [] }),
|
newElementWith(h.elements[1], { boundElements: [] }),
|
||||||
newElementWith(h.elements[2] as ExcalidrawLinearElement, {
|
newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, {
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: remoteContainer.id,
|
elementId: remoteContainer.id,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
@@ -4655,7 +4656,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
API.updateScene({
|
API.updateScene({
|
||||||
elements: [
|
elements: [
|
||||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, {
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe("event callbacks", () => {
|
|||||||
// files
|
// files
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
expect(onChange.mock.lastCall[1].viewBackgroundColor).not.toBe(
|
expect(onChange.mock?.lastCall?.[1].viewBackgroundColor).not.toBe(
|
||||||
origBackgroundColor,
|
origBackgroundColor,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getTextEditor = async (selector: string, waitForEditor = true) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateTextEditor = (
|
export const updateTextEditor = (
|
||||||
editor: HTMLTextAreaElement,
|
editor: HTMLTextAreaElement | HTMLInputElement,
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
fireEvent.change(editor, { target: { value } });
|
fireEvent.change(editor, { target: { value } });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { render } from "./test-utils";
|
|||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -333,6 +334,62 @@ describe("arrow element", () => {
|
|||||||
expect(label.angle).toBeCloseTo(0);
|
expect(label.angle).toBeCloseTo(0);
|
||||||
expect(label.fontSize).toEqual(20);
|
expect(label.fontSize).toEqual(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flips the fixed point binding on negative resize for single bindable", () => {
|
||||||
|
const rectangle = UI.createElement("rectangle", {
|
||||||
|
x: -100,
|
||||||
|
y: -75,
|
||||||
|
width: 95,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-5, 0);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(120, 200);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
|
UI.resize(rectangle, "se", [-200, -150]);
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flips the fixed point binding on negative resize for group selection", () => {
|
||||||
|
const rectangle = UI.createElement("rectangle", {
|
||||||
|
x: -100,
|
||||||
|
y: -75,
|
||||||
|
width: 95,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-5, 0);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(120, 200);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144, 2);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("text element", () => {
|
describe("text element", () => {
|
||||||
@@ -828,7 +885,6 @@ describe("multiple selection", () => {
|
|||||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||||
leftArrowBinding.elementId,
|
leftArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
expect(leftBoundArrow.endBinding?.fixedPoint).toBeNull();
|
|
||||||
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||||
|
|
||||||
expect(rightBoundArrow.x).toBeCloseTo(210);
|
expect(rightBoundArrow.x).toBeCloseTo(210);
|
||||||
@@ -843,7 +899,6 @@ describe("multiple selection", () => {
|
|||||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||||
rightArrowBinding.elementId,
|
rightArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
expect(rightBoundArrow.endBinding?.fixedPoint).toBeNull();
|
|
||||||
expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus);
|
expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { act, render, waitFor } from "./test-utils";
|
||||||
|
import { Excalidraw } from "../index";
|
||||||
|
import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants";
|
||||||
|
import { Keyboard } from "./helpers/ui";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { updateTextEditor } from "./queries/dom";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import type { ExcalidrawTextElement } from "../element/types";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
const querySearchInput = async () => {
|
||||||
|
const input =
|
||||||
|
h.app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>(
|
||||||
|
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`,
|
||||||
|
)!;
|
||||||
|
await waitFor(() => expect(input).not.toBeNull());
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("search", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally />);
|
||||||
|
API.setAppState({
|
||||||
|
openSidebar: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle search on cmd+f", async () => {
|
||||||
|
expect(h.app.state.openSidebar).toBeNull();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name);
|
||||||
|
expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB);
|
||||||
|
|
||||||
|
const searchInput = await querySearchInput();
|
||||||
|
expect(searchInput.matches(":focus")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refocus search input with cmd+f when search sidebar is still open", async () => {
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput =
|
||||||
|
h.app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>(
|
||||||
|
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
searchInput?.blur();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(searchInput?.matches(":focus")).toBe(false);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(searchInput?.matches(":focus")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match text and cycle through matches on Enter", async () => {
|
||||||
|
const scrollIntoViewMock = jest.fn();
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||||
|
|
||||||
|
API.setElements([
|
||||||
|
API.createElement({ type: "text", text: "test one" }),
|
||||||
|
API.createElement({ type: "text", text: "test two" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(h.app.state.openSidebar).toBeNull();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name);
|
||||||
|
expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB);
|
||||||
|
|
||||||
|
const searchInput = await querySearchInput();
|
||||||
|
|
||||||
|
expect(searchInput.matches(":focus")).toBe(true);
|
||||||
|
|
||||||
|
updateTextEditor(searchInput, "test");
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.app.state.searchMatches.length).toBe(2);
|
||||||
|
expect(h.app.state.searchMatches[0].focus).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER, searchInput);
|
||||||
|
expect(h.app.state.searchMatches[0].focus).toBe(false);
|
||||||
|
expect(h.app.state.searchMatches[1].focus).toBe(true);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER, searchInput);
|
||||||
|
expect(h.app.state.searchMatches[0].focus).toBe(true);
|
||||||
|
expect(h.app.state.searchMatches[1].focus).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match text split across multiple lines", async () => {
|
||||||
|
const scrollIntoViewMock = jest.fn();
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||||
|
|
||||||
|
API.setElements([
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.updateElement(h.elements[0] as ExcalidrawTextElement, {
|
||||||
|
text: "t\ne\ns\nt \nt\ne\nx\nt \ns\np\nli\nt \ni\nn\nt\no\nm\nu\nlt\ni\np\nl\ne \nli\nn\ne\ns",
|
||||||
|
originalText: "test text split into multiple lines",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.app.state.openSidebar).toBeNull();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name);
|
||||||
|
expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB);
|
||||||
|
|
||||||
|
const searchInput = await querySearchInput();
|
||||||
|
|
||||||
|
expect(searchInput.matches(":focus")).toBe(true);
|
||||||
|
|
||||||
|
updateTextEditor(searchInput, "test");
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.app.state.searchMatches.length).toBe(1);
|
||||||
|
expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTextEditor(searchInput, "ext spli");
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.app.state.searchMatches.length).toBe(1);
|
||||||
|
expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -198,6 +198,8 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
snapLines: AppState["snapLines"];
|
snapLines: AppState["snapLines"];
|
||||||
zenModeEnabled: AppState["zenModeEnabled"];
|
zenModeEnabled: AppState["zenModeEnabled"];
|
||||||
editingTextElement: AppState["editingTextElement"];
|
editingTextElement: AppState["editingTextElement"];
|
||||||
|
// Search matches
|
||||||
|
searchMatches: AppState["searchMatches"];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -384,8 +386,20 @@ export interface AppState {
|
|||||||
userToFollow: UserToFollow | null;
|
userToFollow: UserToFollow | null;
|
||||||
/** the socket ids of the users following the current user */
|
/** the socket ids of the users following the current user */
|
||||||
followedBy: Set<SocketId>;
|
followedBy: Set<SocketId>;
|
||||||
|
searchMatches: readonly SearchMatch[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SearchMatch = {
|
||||||
|
id: string;
|
||||||
|
focus: boolean;
|
||||||
|
matchedLines: {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
| "suggestedBindings"
|
| "suggestedBindings"
|
||||||
@@ -642,6 +656,9 @@ export type AppClassProperties = {
|
|||||||
getEffectiveGridSize: App["getEffectiveGridSize"];
|
getEffectiveGridSize: App["getEffectiveGridSize"];
|
||||||
setPlugins: App["setPlugins"];
|
setPlugins: App["setPlugins"];
|
||||||
plugins: App["plugins"];
|
plugins: App["plugins"];
|
||||||
|
getEditorUIOffsets: App["getEditorUIOffsets"];
|
||||||
|
visibleElements: App["visibleElements"];
|
||||||
|
excalidrawContainerValue: App["excalidrawContainerValue"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
@@ -834,3 +851,10 @@ export type GenerateDiagramToCode = (props: {
|
|||||||
frame: ExcalidrawMagicFrameElement;
|
frame: ExcalidrawMagicFrameElement;
|
||||||
children: readonly ExcalidrawElement[];
|
children: readonly ExcalidrawElement[];
|
||||||
}) => MaybePromise<{ html: string }>;
|
}) => MaybePromise<{ html: string }>;
|
||||||
|
|
||||||
|
export type Offsets = Partial<{
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}>;
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ export const debugDrawBoundingBox = (
|
|||||||
export const debugDrawBounds = (
|
export const debugDrawBounds = (
|
||||||
box: Bounds | Bounds[],
|
box: Bounds | Bounds[],
|
||||||
opts?: {
|
opts?: {
|
||||||
color: string;
|
color?: string;
|
||||||
permanent: boolean;
|
permanent?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
||||||
@@ -136,7 +136,7 @@ export const debugDrawBounds = (
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "green",
|
color: opts?.color ?? "green",
|
||||||
permanent: opts?.permanent,
|
permanent: !!opts?.permanent,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+18
-5
@@ -1,14 +1,27 @@
|
|||||||
export const PRECISION = 10e-5;
|
export const PRECISION = 10e-5;
|
||||||
|
|
||||||
export function clamp(value: number, min: number, max: number) {
|
export const clamp = (value: number, min: number, max: number) => {
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function round(value: number, precision: number) {
|
export const round = (
|
||||||
|
value: number,
|
||||||
|
precision: number,
|
||||||
|
func: "round" | "floor" | "ceil" = "round",
|
||||||
|
) => {
|
||||||
const multiplier = Math.pow(10, precision);
|
const multiplier = Math.pow(10, precision);
|
||||||
|
|
||||||
return Math.round((value + Number.EPSILON) * multiplier) / multiplier;
|
return Math[func]((value + Number.EPSILON) * multiplier) / multiplier;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const roundToStep = (
|
||||||
|
value: number,
|
||||||
|
step: number,
|
||||||
|
func: "round" | "floor" | "ceil" = "round",
|
||||||
|
): number => {
|
||||||
|
const factor = 1 / step;
|
||||||
|
return Math[func](value * factor) / factor;
|
||||||
|
};
|
||||||
|
|
||||||
export const average = (a: number, b: number) => (a + b) / 2;
|
export const average = (a: number, b: number) => (a + b) / 2;
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ describe("exportToCanvas", async () => {
|
|||||||
|
|
||||||
describe("exportToBlob", async () => {
|
describe("exportToBlob", async () => {
|
||||||
describe("mime type", () => {
|
describe("mime type", () => {
|
||||||
// afterEach(vi.restoreAllMocks);
|
|
||||||
it("should change image/jpg to image/jpeg", async () => {
|
it("should change image/jpg to image/jpeg", async () => {
|
||||||
const blob = await utils.exportToBlob({
|
const blob = await utils.exportToBlob({
|
||||||
...diagramFactory(),
|
...diagramFactory(),
|
||||||
|
|||||||
@@ -68,7 +68,6 @@
|
|||||||
"css-loader": "6.7.1",
|
"css-loader": "6.7.1",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"fonteditor-core": "2.4.0",
|
"fonteditor-core": "2.4.0",
|
||||||
"node-fetch": "3.3.2",
|
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
"ts-loader": "9.3.1",
|
"ts-loader": "9.3.1",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const { build } = require("esbuild");
|
const { build } = require("esbuild");
|
||||||
const { sassPlugin } = require("esbuild-sass-plugin");
|
const { sassPlugin } = require("esbuild-sass-plugin");
|
||||||
const { externalGlobalPlugin } = require("esbuild-plugin-external-global");
|
const { externalGlobalPlugin } = require("esbuild-plugin-external-global");
|
||||||
const { woff2BrowserPlugin } = require("./woff2/woff2-esbuild-plugins");
|
|
||||||
|
|
||||||
// Will be used later for treeshaking
|
// Will be used later for treeshaking
|
||||||
//const fs = require("fs");
|
//const fs = require("fs");
|
||||||
@@ -45,13 +44,15 @@ const browserConfig = {
|
|||||||
format: "esm",
|
format: "esm",
|
||||||
plugins: [
|
plugins: [
|
||||||
sassPlugin(),
|
sassPlugin(),
|
||||||
woff2BrowserPlugin(),
|
|
||||||
externalGlobalPlugin({
|
externalGlobalPlugin({
|
||||||
react: "React",
|
react: "React",
|
||||||
"react-dom": "ReactDOM",
|
"react-dom": "ReactDOM",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
splitting: true,
|
splitting: true,
|
||||||
|
loader: {
|
||||||
|
".woff2": "file",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const createESMBrowserBuild = async () => {
|
const createESMBrowserBuild = async () => {
|
||||||
// Development unminified build with source maps
|
// Development unminified build with source maps
|
||||||
@@ -100,9 +101,10 @@ const rawConfig = {
|
|||||||
entryPoints: ["index.tsx"],
|
entryPoints: ["index.tsx"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
plugins: [sassPlugin(), woff2BrowserPlugin()],
|
plugins: [sassPlugin()],
|
||||||
loader: {
|
loader: {
|
||||||
".json": "copy",
|
".json": "copy",
|
||||||
|
".woff2": "file",
|
||||||
},
|
},
|
||||||
packages: "external",
|
packages: "external",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { build } = require("esbuild");
|
const { build } = require("esbuild");
|
||||||
const { sassPlugin } = require("esbuild-sass-plugin");
|
const { sassPlugin } = require("esbuild-sass-plugin");
|
||||||
const {
|
const { woff2ServerPlugin } = require("./woff2/woff2-esbuild-plugins");
|
||||||
woff2BrowserPlugin,
|
|
||||||
woff2ServerPlugin,
|
|
||||||
} = require("./woff2/woff2-esbuild-plugins");
|
|
||||||
|
|
||||||
const browserConfig = {
|
const browserConfig = {
|
||||||
entryPoints: ["index.ts"],
|
entryPoints: ["index.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
plugins: [sassPlugin(), woff2BrowserPlugin()],
|
plugins: [sassPlugin()],
|
||||||
assetNames: "assets/[name]",
|
assetNames: "assets/[name]",
|
||||||
|
loader: {
|
||||||
|
".woff2": "file",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Will be used later for treeshaking
|
// Will be used later for treeshaking
|
||||||
|
|||||||
@@ -2,45 +2,9 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const which = require("which");
|
const which = require("which");
|
||||||
const fetch = require("node-fetch");
|
|
||||||
const wawoff = require("wawoff2");
|
const wawoff = require("wawoff2");
|
||||||
const { Font } = require("fonteditor-core");
|
const { Font } = require("fonteditor-core");
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom esbuild plugin to convert url woff2 imports into a text.
|
|
||||||
* Other woff2 imports are handled by a "file" loader.
|
|
||||||
*
|
|
||||||
* @returns {import("esbuild").Plugin}
|
|
||||||
*/
|
|
||||||
module.exports.woff2BrowserPlugin = () => {
|
|
||||||
return {
|
|
||||||
name: "woff2BrowserPlugin",
|
|
||||||
setup(build) {
|
|
||||||
build.initialOptions.loader = {
|
|
||||||
".woff2": "file",
|
|
||||||
...build.initialOptions.loader,
|
|
||||||
};
|
|
||||||
|
|
||||||
build.onResolve({ filter: /^https:\/\/.+?\.woff2$/ }, (args) => {
|
|
||||||
return {
|
|
||||||
path: args.path,
|
|
||||||
namespace: "woff2BrowserPlugin",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
build.onLoad(
|
|
||||||
{ filter: /.*/, namespace: "woff2BrowserPlugin" },
|
|
||||||
async (args) => {
|
|
||||||
return {
|
|
||||||
contents: args.path,
|
|
||||||
loader: "text",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom esbuild plugin to:
|
* Custom esbuild plugin to:
|
||||||
* 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs)
|
* 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs)
|
||||||
@@ -53,27 +17,6 @@ module.exports.woff2BrowserPlugin = () => {
|
|||||||
* @returns {import("esbuild").Plugin}
|
* @returns {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
module.exports.woff2ServerPlugin = (options = {}) => {
|
module.exports.woff2ServerPlugin = (options = {}) => {
|
||||||
// google CDN fails time to time, so let's retry
|
|
||||||
async function fetchRetry(url, options = {}, retries = 0, delay = 1000) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Status: ${response.status}, ${await response.json()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (e) {
|
|
||||||
if (retries > 0) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
return fetchRetry(url, options, retries - 1, delay * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Couldn't fetch: ${url}, error: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "woff2ServerPlugin",
|
name: "woff2ServerPlugin",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
@@ -82,9 +25,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
|
|||||||
const fonts = new Map();
|
const fonts = new Map();
|
||||||
|
|
||||||
build.onResolve({ filter: /\.woff2$/ }, (args) => {
|
build.onResolve({ filter: /\.woff2$/ }, (args) => {
|
||||||
const resolvedPath = args.path.startsWith("http")
|
const resolvedPath = path.resolve(args.resolveDir, args.path);
|
||||||
? args.path // url
|
|
||||||
: path.resolve(args.resolveDir, args.path); // absolute path
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: resolvedPath,
|
path: resolvedPath,
|
||||||
@@ -101,9 +42,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
|
|||||||
// read local woff2 as a buffer (WARN: `readFileSync` does not work!)
|
// read local woff2 as a buffer (WARN: `readFileSync` does not work!)
|
||||||
woff2Buffer = await fs.promises.readFile(args.path);
|
woff2Buffer = await fs.promises.readFile(args.path);
|
||||||
} else {
|
} else {
|
||||||
// fetch remote woff2 as a buffer (i.e. from a cdn)
|
throw new Error(`Font path has to be absolute! "${args.path}"`);
|
||||||
const response = await fetchRetry(args.path, {}, 3);
|
|
||||||
woff2Buffer = await response.buffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// google's brotli decompression into snft
|
// google's brotli decompression into snft
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
|
// `EXCALIDRAW_ASSET_PATH` as a SSOT
|
||||||
const OSS_FONTS_CDN =
|
const OSS_FONTS_CDN =
|
||||||
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
|
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom vite plugin to convert url woff2 imports into a text.
|
* Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`.
|
||||||
* Other woff2 imports are automatically served and resolved as a file uri.
|
|
||||||
*
|
*
|
||||||
* @returns {import("vite").PluginOption}
|
* @returns {import("vite").PluginOption}
|
||||||
*/
|
*/
|
||||||
module.exports.woff2BrowserPlugin = () => {
|
module.exports.woff2BrowserPlugin = () => {
|
||||||
// for now limited to woff2 only, might be extended to any assets in the future
|
|
||||||
const regex = /^https:\/\/.+?\.woff2$/;
|
|
||||||
let isDev;
|
let isDev;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -18,34 +16,9 @@ module.exports.woff2BrowserPlugin = () => {
|
|||||||
config(_, { command }) {
|
config(_, { command }) {
|
||||||
isDev = command === "serve";
|
isDev = command === "serve";
|
||||||
},
|
},
|
||||||
resolveId(source) {
|
|
||||||
if (!regex.test(source)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// getting the url to the dependency tree
|
|
||||||
return source;
|
|
||||||
},
|
|
||||||
load(id) {
|
|
||||||
if (!regex.test(id)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading the url as string
|
|
||||||
return `export default "${id}"`;
|
|
||||||
},
|
|
||||||
// necessary for dev as vite / rollup does skips https imports in serve (~dev) mode
|
|
||||||
// aka dev mode equivalent of "export default x" above (resolveId + load)
|
|
||||||
transform(code, id) {
|
transform(code, id) {
|
||||||
// treat https woff2 imports as a text
|
// using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically),
|
||||||
if (isDev && id.endsWith("/excalidraw/fonts/index.ts")) {
|
// but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app`
|
||||||
return code.replaceAll(
|
|
||||||
/import\s+(\w+)\s+from\s+(["']https:\/\/.+?\.woff2["'])/g,
|
|
||||||
`const $1 = $2`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// use CDN for Assistant
|
|
||||||
if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
|
if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
|
||||||
return `/* WARN: The following content is generated during excalidraw-app build */
|
return `/* WARN: The following content is generated during excalidraw-app build */
|
||||||
|
|
||||||
@@ -90,7 +63,6 @@ module.exports.woff2BrowserPlugin = () => {
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// using EXCALIDRAW_ASSET_PATH as a SSOT
|
|
||||||
if (!isDev && id.endsWith("excalidraw-app/index.html")) {
|
if (!isDev && id.endsWith("excalidraw-app/index.html")) {
|
||||||
return code.replace(
|
return code.replace(
|
||||||
"<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
|
"<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
|
||||||
@@ -110,9 +82,10 @@ module.exports.woff2BrowserPlugin = () => {
|
|||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
|
href="${OSS_FONTS_CDN}Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg-DqUjjPte.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
@@ -124,6 +97,13 @@ module.exports.woff2BrowserPlugin = () => {
|
|||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ vi.mock(
|
|||||||
const url = this.urls[0];
|
const url = this.urls[0];
|
||||||
|
|
||||||
if (url.protocol !== "file:") {
|
if (url.protocol !== "file:") {
|
||||||
return super.getContent();
|
return super.getContent(new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
// read local assets directly, without running a server
|
// read local assets directly, without running a server
|
||||||
|
|||||||
+12
-6
@@ -1,20 +1,26 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import { woff2BrowserPlugin } from "./scripts/woff2/woff2-vite-plugins";
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// @ts-ignore
|
//@ts-ignore
|
||||||
plugins: [woff2BrowserPlugin()],
|
|
||||||
test: {
|
test: {
|
||||||
|
// Since hooks are running in stack in v2, which means all hooks run serially whereas
|
||||||
|
// we need to run them in parallel
|
||||||
|
sequence: {
|
||||||
|
hooks: "parallel",
|
||||||
|
},
|
||||||
setupFiles: ["./setupTests.ts"],
|
setupFiles: ["./setupTests.ts"],
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ["text", "json-summary", "json", "html", "lcovonly"],
|
reporter: ["text", "json-summary", "json", "html", "lcovonly"],
|
||||||
|
// Since v2, it ignores empty lines by default and we need to disable it as it affects the coverage
|
||||||
|
// Additionally the thresholds also needs to be updated slightly as a result of this change
|
||||||
|
ignoreEmptyLines: false,
|
||||||
thresholds: {
|
thresholds: {
|
||||||
lines: 70,
|
lines: 60,
|
||||||
branches: 70,
|
branches: 70,
|
||||||
functions: 68,
|
functions: 63,
|
||||||
statements: 70,
|
statements: 60,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user